forked from CyclejsCN/cyclejs.cn
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdrivers.html
400 lines (296 loc) · 18.7 KB
/
drivers.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
<!doctype html>
<html>
<head>
<meta charset='utf-8'>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width">
<title>Cycle.js - Drivers</title>
<!-- Flatdoc -->
<script src='support/vendor/jquery.js'></script>
<script src='support/vendor/highlight.pack.js'></script>
<script src='legacy.js'></script>
<script src='flatdoc.js'></script>
<!-- Algolia's DocSearch main theme -->
<link href='//cdn.jsdelivr.net/docsearch.js/2/docsearch.min.css' rel='stylesheet' />
<!-- Others -->
<script async src="//static.jsbin.com/js/embed.js"></script>
<!-- Flatdoc theme -->
<link href='theme/style.css' rel='stylesheet'>
<script src='theme/script.js'></script>
<link href='support/vendor/highlight-github-gist.css' rel='stylesheet'>
<!-- Meta -->
<meta content="Cycle.js - Drivers" property="og:title">
<meta content="一个函数式和响应式的 JavaScript 框架,用来编写前瞻性代码。" name="description">
<!-- Content -->
<script id="markdown" type="text/markdown" src="index.html">
# Drivers
## Plugins for effects
Throughout this documentation site we have extensively used *drivers*. The DOM Driver has been the most common one, but also the HTTP driver was used.
What are drivers and when should you use them? When should you create your own driver, and how do they work? These are a few questions we will address in this chapter.
**Drivers are functions that listen to sink streams (their input), perform imperative side effects, and may return source streams (their output).**
They are meant for encapsulating imperative side effects in JavaScript. The rule of thumb is: whenever you have a JavaScript function such as `doSomething()` which returns nothing, it should be contained in a driver.
Let's study what drivers do by analyzing the most common one: the DOM Driver.
> ### Why the name "driver"?
>
> In Haskell 1.0 Stream I/O, similar in nature to Cycle.js, there is a cyclic interaction between the program's `main` function and Haskell's `os` function. In operating systems, drivers are software interfaces to use some hardware devices, which incur side effects in the external world.
>
> In Cycle.js, one can consider the "operating system" to be the execution environment surrounding your application. Roughly speaking, the DOM, the console, JavaScript and JS APIs assume the role of the operating system for the web. We need *software adapters* to interface with the browser and other environments such as Node.js. Cycle.js drivers are there as adapters between the external world (including the user and the JavaScript execution environment) and the application world built with Cycle.js tools.
## DOM Driver
The DOM Driver is the most important and most common driver in Cycle.js. When building interactive web apps, it is probably the most important tool in Cycle.js. In fact, while Cycle *Run* function is only about 200 lines of code, Cycle *DOM* is at least 4 times larger.
It's main purpose is to be a proxy to the user using the browser. Conceptually we would like to work assuming the existence of a `human()` function, as this diagram reminds us:
![Human computer diagram](img/human-computer-diagram.svg)
However, in practice, we write our `main()` function targeted at a `domDriver()`. For a user interacting with a browser, we only need to make our `main()` interact with the DOM. Whenever we need to show something to the user, we instead show that to the DOM, and the DOM together with the browser shows that to our user. When we need to detect the user's interaction events, we attach event listeners on the DOM, and the DOM will notify us when the user interacts with the browser on the computer.
![Main, DOM Driver, Side Effects](img/main-domdriver-side-effects.svg)
Notice there are two directions of interaction with the external world through the DOM. The *write* effect is the renderization of our Snabbdom VNodes to DOM elements which can be shown on the user's screen. The *read* effect is the detection of DOM events generated by the user manipulating the computer.
The `domDriver()` manages these two effects while allowing them to be interfaced with the `main()`. The *input* to `domDriver()` captures instructions for the *write* effect, and the *read* effect is exposed as the *output* of `domDriver()`. The anatomy of the `domDriver()` function is roughly the following:
```javascript
function domDriver(vdom$) {
// Use vdom$ as instructions to create DOM elements
// ...
return {
select: function select(selector) {
// returns an object with two functions: `events()`
// and `elements()`. Function `events(eventType)`
// returns the stream of `eventType` DOM events
// happening on the elements matched by `selector`.
// Function `elements()` is the stream of DOM
// elements matching the given `selector`.
}
};
}
```
The input `vdom$` is the output from `main()`, and the output of `domDriver()` is the input to `main()`:
```javascript
function main(sources) {
// Use sources.DOM.select(selector).events(eventType)
// ...
// Create vdom$ somehow
// ...
return {
DOM: vdom$
};
}
```
As a recap:
- `main()`: takes **sources** as input, returns **sinks**
- `domDriver()`: takes **sinks** as input, performs write and read effects, returns **sources**.
## Isolating side effects
Drivers should always be associated with some I/O effect. As we saw, even though the DOM Driver's main purpose is to represent the user, it has write and read effects.
In JavaScript, nothing stops you from writing your `main()` function with effects. A simple `console.log()` is already an effect. However, to keep `main()` pure and reap its benefits like testability and predictability, it is better to encapsulate all I/O effects in drivers.
Imagine, for instance, a driver for network requests. By isolating the network request effect, your application's `main()` function can focus on business logic related to the app's behavior, and not on lower-level instructions to interface with external resources. This also allows a simple method for testing network requests: you can replace the actual network driver with a fake network driver. It just needs to be a function that mimics the network driver function, and makes assertions.
Avoid making drivers if they do not have effects to the external world somehow. Especially do not create drivers to contain business logic. This is most likely a code smell.
Drivers should focus solely on being an interface for effects, and usually are libraries that simply enable your Cycle.js app to perform different effects. Sometimes, though, a one-liner driver can be created on the fly instead of being a library, for instance this simple logging driver:
```javascript
run(main, {
log: msg$ => { msg$.addListener({next: msg => console.log(msg)}) }
});
```
## Read-only and write-only
Most drivers, like the DOM Driver, take *sinks* (to describe a *write*) and return *sources* (to catch *reads*). However, we might have valid cases for write-only drivers and read-only drivers.
For instance, the one-liner `log` driver we just saw above is a write-only driver. Notice how it is a function that does not return any stream, it simply consumes the sink `msg$` it receives.
Other drivers only create source streams that emit events to the `main()`, but don't take in any `sink` from `main()`. An example of such would be a read-only Web Socket driver, drafted below:
```javascript
function WSDriver(/* no sinks */) {
return xs.create({
start: listener => {
this.connection = new WebSocket('ws://localhost:4000');
connection.onerror = (err) => {
listener.error(err)
}
connection.onmessage = (msg) => {
listener.next(msg)
}
},
stop: () => {
this.connection.close();
},
});
}
```
## How to make drivers
You should only be reading this section if you have clear intentions to make a driver and expose it as a library (unless it's a one-liner driver). Typically, when writing a Cycle.js app, you do not need to create your own drivers.
Consider first carefully which effects your driver is responsible for. And can it have both read and write effects?
Once you map out the read/write effects, consider how diverse those can be. Create an empathic API which covers the common cases elegantly.
The **input** to the driver function is expected to be a single `xstream` stream. This is a practical API for the app developer to use when returning the `sinks` object in `main()`. Notice how the DOM Driver takes a single `vdom$` stream as input, and how sophisticated and expressive VNodes from Snabbdom can be. On the other hand, don't always choose JavaScript objects as the values emitted in the Observable. Use objects when they make sense, and remember to keep the API simple rather than overly-generic. Don't over-engineer.
Note that even if you are using RxJS and `@cycle/rxjs-run`, the sink given to the driver is always an `xstream` Stream. You may need to convert from xstream to RxJS in the driver code, with `Rx.Observable.from(sink$)`. This is because xstream is powering Cycle.js internally.
As a second, optional, argument to the driver function, you can expect `name`. This is the name that was given to the driver in the `drivers` object in `run(main, drivers)`. For instance, the DOM Driver usually gets the name `DOM`. Typically, drivers don't need to use this argument, but it is available anyway. This is the expected signature for a driver function:
```javascript
function myDriver(sink$, name /* optional */)
```
The **output** of the driver function can either be a single stream or a *queryable collection* of streams.
In the case of a single stream as output source, depending on how diverse the values emitted by this stream are, you might want to make those values easily filterable (using the xstream or RxJS or Most.js `filter()` operator). Design an API which makes it easy to filter the stream, keeping in mind what was provided as the sink stream to the driver function.
In some cases it is necessary to output a queryable collection of streams, instead of a single one. A **queryable collection of streams** is essentially a JavaScript object with a function used to choose a particular stream based on a parameter, e.g. `get(param)`.
The DOM Driver, for instance, outputs a queryable collection of streams. The collection is in fact lazy: none of the streams outputted by `select(selector).events(eventType)` existed prior to the call of `events()`. This is because we cannot afford creating streams for *all* possible events on *all* elements on the DOM. Take inspiration from the lazy queryable collection of streams from the DOM Driver whenever the output source contains a large (possibly infinite) amount of streams.
In order to make your driver usable with many stream libraries, you should use the `adapt()` function from `@cycle/run/lib/adapt` to convert the stream to the same library used for `run`. `adapt()` takes an **xstream** stream as input and returns a stream for the library used in `run`.
```typescript
adapt(stream: xs.Stream<T>): xs.Stream<T>; // for @cycle/run
adapt(stream: xs.Stream<T>): Rx.Observable<T>; // for @cycle/rxjs-run
adapt(stream: xs.Stream<T>): most.Stream<T>; // for @cycle/most-run
```
Note that `adapt` is always imported from as `@cycle/run/lib/adapt`, **not** from `@cycle/rxjs-run/lib/adapt` nor `@cycle/most-run/lib/adapt`. Before returning a single-stream source from the driver, make sure to call `adapt`:
```js
import {adapt} from '@cycle/run/lib/adapt';
function WSDriver(/* no sinks */) {
const source = xs.create({
start: listener => {
this.connection = new WebSocket('ws://localhost:4000');
connection.onerror = (err) => {
listener.error(err)
}
connection.onmessage = (msg) => {
listener.next(msg)
}
},
stop: () => {
this.connection.close();
},
});
return adapt(source);
}
```
If you return *queryable collection of streams*, make sure that each method which returns a stream calls `adapt()` as well.
It is usually better to write drivers in xstream, since you don't need to convert the sink, and given xstream's tiny size as a dependency.
## Example driver
Suppose you have a fake real-time channel API called `Sock`. It is able to connect to a remote peer, send messages, and receive push-based messages. The API for `Sock` is:
```javascript
// Establish a connection to the peer
let sock = new Sock('unique-identifier-of-the-peer');
// Subscribe to messages received from the peer
sock.onReceive(function (msg) {
console.log('Received message: ' + msg);
});
// Send a single message to the peer
sock.send('Hello world');
```
**How do we build a driver for `Sock`?** We start by identifying the effects. The *write* effect is `sock.send(msg)` and the *read* effect is the listener for received messages. Our `sockDriver(sink)` should take `sink` as instructions to perform the `send(msg)` calls. The output from `sockDriver()` should be `source`, containing all received messages.
Since both input and output should be streams, it's easy to see `sink` in `sockDriver(sink)` should be an stream of outgoing messages to the peer. And conversely, the source should be an stream of incoming messages. This is a draft of our driver function:
```javascript
import {adapt} from '@cycle/run/lib/adapt';
function sockDriver(outgoing$) {
outgoing$.addListener({
next: outgoing => {
sock.send(outgoing);
},
error: () => {},
complete: () => {},
});
const incoming$ = xs.create({
start: listener => {
sock.onReceive(function (msg) {
listener.next(msg);
});
},
stop: () => {},
});
return adapt(incoming$);
}
```
The listener of `outgoing$` performs the `send()` effect, and the returned stream `incoming$` is based on `sock.onReceive` to take data from the external world. However, `sockDriver` is assuming `sock` to be available in the closure. As we saw, `sock` needs to be created with a constructor `new Sock()`. To solve this dependency, we need to create a factory that makes `sockDriver()` functions.
```javascript
import {adapt} from '@cycle/run/lib/adapt';
function makeSockDriver(peerId) {
let sock = new Sock(peerId);
function sockDriver(outgoing$) {
outgoing$.addListener({
next: outgoing => {
sock.send(outgoing));
},
error: () => {},
complete: () => {},
});
const incoming$ = xs.create({
start: listener => {
sock.onReceive(function (msg) {
listener.next(msg);
});
},
stop: () => {},
});
return adapt(incoming$);
}
return sockDriver;
}
```
`makeSockDriver(peerId)` creates the `sock` instance, and returns the `sockDriver()` function. We use this in a Cycle.js app as such:
```javascript
function main(sources) {
const incoming$ = sources.sock;
// Create outgoing$ (stream of string messages)
// ...
return {
sock: outgoing$
};
}
run(main, {
sock: makeSockDriver('B23A79D5-some-unique-id-F2930')
});
```
Notice we have the `peerId` specified when the driver is created in `makeSockDriver(peerId)`. If the `main()` needs to dynamically connect to different peers according to some logic, then we shouldn't use this API anymore. Instead, we need the driver function to take instructions as input, such as "connect to peerId", or "send message to peerId". This is one example of the considerations you should take when designing a driver API.
## Extensibility
Cycle *Core* is a very small framework, and Cycle *DOM*'s Driver is available as an optional plugin for your app. This means it is simple to replace the DOM Driver with any other driver function providing interaction with the user.
You can for instance fork the DOM Driver, adapt it to your preferences, and use it in a Cycle.js app. You can create a driver to interface with sockets. Drivers to perform network requests. Drivers meant for Node.js. Drivers that target other UI trees, such as `<canvas>` or even native mobile UI.
As a framework, it cannot be compared to monoliths which have ruled web development in the recent years. Cycle.js itself is after all just a small tool and a convention to create reactive dialogues with the external world using reactive streams.
</script>
<!-- Initializer -->
<script>
Flatdoc.run({
fetcher: function(callback) {
callback(null, document.getElementById('markdown').innerHTML);
},
highlight: function (code, value) {
return hljs.highlight(value, code).value;
},
});
</script>
</head>
<body role='flatdoc' class="no-literate">
<div class='header'>
<div class='left'>
<h1><a href="/"><img class="logo" src="img/cyclejs_logo.svg" >Cycle.js</a></h1>
<ul>
<li><a href='getting-started.html'>Documentation</a></li>
<li><a href='api/index.html'>API reference</a></li>
<li><a href='releases.html'>Releases</a></li>
<li><a href='https://github.com/cyclejs/cyclejs'>GitHub</a></li>
</ul>
<input id="docsearch" />
</div>
<div class='right'>
<!-- GitHub buttons: see https://ghbtns.com -->
<iframe src="https://ghbtns.com/github-btn.html?user=cyclejs&repo=cyclejs&type=watch&count=true" allowtransparency="true" frameborder="0" scrolling="0" width="110" height="20"></iframe>
</div>
</div>
<div class='content-root'>
<div class='menubar'>
<div class='menu section'>
<ul>
<li><a href="getting-started.html" class="level-1 out-link">起步</a></li>
<li><a href="dialogue.html" class="level-1 out-link">对话抽象</a></li>
<li><a href="streams.html" class="level-1 out-link">Streams</a></li>
<li><a href="basic-examples.html" class="level-1 out-link">Basic examples</a></li>
<li><a href="model-view-intent.html" class="level-1 out-link">Model-View-Intent</a></li>
<li><a href="components.html" class="level-1 out-link">Components</a></li>
</ul>
<div role='flatdoc-menu'></div>
</div>
</div>
<div role='flatdoc-content' class='content'></div>
</div>
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-101243593-1', 'auto');
ga('send', 'pageview');
</script>
<script>
((window.gitter = {}).chat = {}).options = {
room: 'cyclejs/cyclejs'
};
</script>
<script src="https://sidecar.gitter.im/dist/sidecar.v1.js" async defer></script>
<script src='//cdn.jsdelivr.net/docsearch.js/2/docsearch.min.js'></script>
<script src='docsearch.js'></script>
</body>
</html>