-
Notifications
You must be signed in to change notification settings - Fork 22
Expand file tree
/
Copy pathdrivers.html
More file actions
406 lines (301 loc) · 17.9 KB
/
Copy pathdrivers.html
File metadata and controls
406 lines (301 loc) · 17.9 KB
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
401
402
403
404
405
406
<!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">
<meta name="keyword" content="cycle.js,中文,cyclejs 中文,javascript,functional,函数式,reactive,响应式">
<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
## Effects 插件
这份文档网站中大量使用了 **driver**。其中 DOM Driver 是最常见的一个,也有用到 HTTP Driver。
什么是 driver 以及何时使用它们?何时创建自己的 driver,以及它们是如何工作的?这都是本章将为大家解答的问题。
**Driver 其实是一些函数,它用来监听 sink 流(输入),然后执行必要的副作用操作,最后可能会返回 source 流(输出)。**
在 JavaScript 中,driver 被用来封装必要的副作用操作。一般来说:每当你有不返回任何值的 JavaScript 函数(例如 `doSomething()`)时,该函数就应该被包裹在 driver 中。
下面通过分析最常见的 DOM Driver 来了解 driver 是如何工作的。
> ### 为什么取名“driver”?
>
> 在与 Cycle.js 性质相似 Haskell 1.0 Stream I/O 中,程序中的 `main` 函数和 Haskell 中的 `os` 函数之间存在一个循环交互。在操作系统中,driver 是一些调用某些硬件设备的软件接口,这些硬件设备会对外界产生副作用。
>
> 在 Cycle.js 中,可以认为“操作系统”就是围绕应用的执行环境。大致来说,DOM、console、JavaScript 和 JS API 都扮演了 web 开发中操作系统的角色。我们需要**软件适配器**来与浏览器或者其他环境(例如 Node.js)进行交互。Cycle.js 的 driver 就是外界(包括用户以及 JavaScript 执行环境)与 Cycle.js 工具构建的应用世界之间的适配器。
## DOM Driver
DOM Driver 是 Cycle.js 中最重要也是最常见的 driver。它可能是 Cycle.js 在构建响应式 web 应用时最重要的工具。实际上,Cycle **Run** 函数仅有 200 行代码,而 Cycle **DOM** 至少大 4 倍。
它的主要作用是成为用户使用计算机的代理。我们假设存在一个 `human()` 函数,如下图所示:

然而实际上,我们会针对 `domDriver()` 编写自己的 `main()` 函数。为了让用户与浏览器能交互,我们只需让 `main()` 与 DOM 能交互。每当需要向用户展示一些东西时,我们只需将这些东西展示给 DOM,DOM 会结合浏览器向用户展示。当需要检测用户的交互事件时,我们给 DOM 添加事件监听器,这样当用户在计算机上与浏览器交互时,DOM 会通知我们。

注意,通过 DOM 与外界的交互有两个方向。**write** effect 是将 Snabbdom VNodes 渲染至屏幕可显示的 DOM 元素上 。**read** effect 是检测用户操作计算机时生成的 DOM 事件。
`domDriver()` 管理这两个 effect,同时还让他们与 `main()` 之间的连接成为可能。`domDriver()` 的**输入**为 **write** effect 捕获指令,**read** effect 会作为 `domDriver()` 的**输出**暴露出来。`domDriver()` 函数的解析大致如下:
```javascript
function domDriver(vdom$) {
// 使用 vdom$ 作为指令来创建 DOM 元素
// ...
return {
select: function select(selector) {
// 返回一个拥有两个函数的对象:`events()` 和 `elements()`。
// `events(eventType)` 函数返回匹配 `selector` 的元素上
// 发生 `eventType` 类型的 DOM 事件流。
// `elements()` 函数返回匹配 `selector` 的元素流。
}
};
}
```
`domDriver()` 的输入 `vdom$` 是 `main()` 的输出,而 `domDriver()` 的输出又是 `main()` 的输入:
```javascript
function main(sources) {
// 使用 sources.DOM.select(selector).events(eventType)
// ...
// 以某种方式创建 vdom$
// ...
return {
DOM: vdom$
};
}
```
回顾:
- `main()`:将 **sources** 作为输入,返回 **sinks**
- `domDriver()`:将 **sinks** 作为输入,执行 write 和 read effect,返回 **sources**。
## 隔离副作用
Driver 应该始终与某些 I/O effect 联系在一起。如我们所见,虽然 DOM Driver 的主要目的是代表用户,但它有 write 和 read effect。
在 JavaScript 中,没有什么能阻止你编写一个有 effects 的 `main()` 函数。一句简单的 `console.log()` 也是一个 effect。然而,让 `main()` 保持纯净能获得诸如可测试性和可预测性等好处,所以最好将所有的 I/O 操作都封装在 driver 中。
例如,假设有一个网络请求的 driver。通过隔离网络请求的操作,应用程序里的 `main()` 函数就可以只关注与应用程序行为相关的业务逻辑,而不用关注与外部资源相关的底层指令。这也为测试网络请求提供了一种简单的方法:你可以用一个假的网络 driver 替代实际的网络 driver。它只需要是一个函数,该函数可以模拟网络 driver 函数,并且能够做出断言。
避免创建对外界没有 effects 的 driver。尤其不要创建包含业务逻辑的 driver。这简直就是一种代码异味。
Driver 应该把注意力集中在各种 effects 的接口上,通常是一个只允许 Cycle.js 应用执行不同 effects 的库。然而有时候,一个单行 driver 可以立即创建,而不必再写一个库,例如下面这个简单的日志 driver:
```javascript
run(main, {
log: msg$ => { msg$.addListener({next: msg => console.log(msg)}) }
});
```
## 只读与只写
大多数 driver,比如 DOM Driver,以 **sink** (为了描述 **write**)作为输入,以 **source** (为了捕获 **read**)作为输出。但是,在实际情况中我们可能也会用到只写 driver 和只读 driver。
例如,上一节中那个单行的 `log` driver 就是一个只写 driver。注意,它是一个没有返回任何流的函数,它仅仅接受 `msg$` sink 作为输入。
还有其他的 driver 仅创建 source 流来为 `main()` 发送事件,而不接受任何来自 `main()` 的 `sink`。例如一个只读的 Web Socket driver,代码如下:
```javascript
function WSDriver(/* 没有 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();
},
});
}
```
## 如何创建 driver
如果你有明确的目的去创建 driver 并将其开源为一个库(如果是单行 driver 就不必了),那你只需要阅读本章节。通常,在编写 Cycle.js 应用程序时,一般都不需要创建自己的 driver。
首先仔细考虑你的 driver 负责哪些 effect。它是否既包含 read effect,又包含 write effect?
如果设计有 read/write effects,一定要考虑这些 effect 的多样性。然后创建一个能优雅覆盖常见情况的兼容性 API。
Driver 函数的**输入**应该是单一的 `xstream` 流。这是应用开发者在 `main()` 中返回 `sink` 对象时使用的实用 API。需要注意 DOM Driver 是如何使用单一 `vdom$` 流作为输入,以及 Snabbdom 中的 VNode 可以有多么复杂和富有表现力。另一方面,不要总是选择 JavaScript 对象作为 Obserable 中发送的值。仅当对象有意义时再使用它们,并记住保持 API 简单而不是过度泛型。不要过度设计。
注意,即使你使用的是 RxJS 和 `@cycle/rxjs-run`,给 driver 的 sink 始终是 `xstream` 流。你需要在 driver 的代码中使用 `Rx.Observable.from(sink$)` 将 xstream 转换一下再给 RxJS。这是因为 xstream 是在内部驱动 Cycle.js 的。
作为 driver 函数的第二个可选参数 `name`。这个参数是给 `run(main,drivers)` 的 `drivers` 对象中 driver 取的名字。例如,DOM Driver 通常拿到的名字是 `DOM`。一般来说,driver 不需要使用这个参数,但是它是可用的。下面是 driver 函数的预期签名:
```javascript
function myDriver(sink$, name /* 可选 */)
```
Driver 函数的**输出**既可以是单一流,也可以是一个流的**可查询集合**。
在单一流作为输出源的情况下,由于流发出的值具有多样性,所以得让这些值容易过滤(使用 xstream,RxJS 或者 Most.js 的 `filter()` 运算子)。设计一个可轻松过滤这些流的 API,同时牢记为 driver 函数提供了什么作为 sink 流。
在有些情况下,我们需要输出可查询的流集合,而不是单一流。**可查询的流集合**本质上是 JavaScript 对象,这个对象拥有一个根据参数选择特定流的函数,例如,`get(param)`。
例如 DOM Driver 输出的就是可查询的流集合。这个集合实际上是惰性的:在调用 **events()** 之前,由 `select(selector).events(eventType)` 输出的流都是不存在的。这是因为,我们无法为 DOM 中**所有的**元素创建**所有**可能的事件流。当输出源中涵盖大量(可能是无限大)流时,你可以借鉴 DOM Driver 中惰性的可查询的流集合。
为了让其他 stream 库能使用 driver,应该使用 `@cycle/run/lib/adapt` 中的 `adapt()` 函数将流转换成 stream 库中 `run` 能使用的流。`adapt()` 以 **xstream** 流作为输入,并为 `run` 返回一个流。
```typescript
adapt(stream: xs.Stream<T>): xs.Stream<T>; // 给 @cycle/run 用
adapt(stream: xs.Stream<T>): Rx.Observable<T>; // 给 @cycle/rxjs-run 用
adapt(stream: xs.Stream<T>): most.Stream<T>; // 给 @cycle/most-run 用
```
注意,`adapt` 总是从 `@cycle/run/lib/adapt` 导入的,而不是 `@cycle/rxjs-run/lib/adapt`,也不是 `@cycle/most-run/lib/adapt`。在 driver 中返回单一流之前,请确保已经调用了 `adapt`:
```js
import {adapt} from '@cycle/run/lib/adapt';
function WSDriver(/* 没有 sink */) {
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);
}
```
如果你返回的是**可查询的流集合**,也要确保在每一个返回流的方法中调用 `adapt()`。
通常用 xstream 编写 driver 比较好,因为不需要转换 sink,并且可以考虑将 xstream 的小尺寸作为依赖项。
## Driver 示例
假设你有个叫做 `Sock` 的假实时 channel API。它可以连接一个远程的小伙伴,发送消息,接收推送的消息。这个 `Sock` 的 API 代码如下:
```javascript
// 建立与小伙伴的连接
let sock = new Sock('unique-identifier-of-the-peer');
// 订阅从小伙伴接收的消息
sock.onReceive(function (msg) {
console.log('Received message: ' + msg);
});
// 给小伙伴发送一条消息
sock.send('Hello world');
```
**如何给 `Sock` 创建 driver?**首先找出 effects。**write** 的 effect 是 `sock.send(msg)`,**read** effect 是接收消息的监听器。`sockDriver(sink)` 以 `sink` 作为指令来执行 `send(msg)` 的调用。 `sockDriver()` 的输出是 `source`,包含所有接收到的消息。
因为输入和输出都是流,所以很容易想到 `sockDriver(sink)` 中的 `sink` 也是给小伙伴的输出消息流。相反,源应该是个输入消息流。下面是 driver 函数的代码:
```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$);
}
```
`outgoing$` 的监听器执行了 `send()` effect,返回流 `incoming$` 基于 `sock.onReceive` 从外界获取数据。但是,`sockDriver` 假设了闭包中可以使用 `sock`。正如我们所看到的,`sock` 需要用构造函数 `new Sock()` 来创建。为了解决这个依赖问题,我们需要创建一个生产 `sockDriver()` 函数的工厂函数。
```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)` 创建了 `sock` 实例,返回 `sockDriver()` 函数。在 Cycle.js 应用中我们这么使用:
```javascript
function main(sources) {
const incoming$ = sources.sock;
// Create outgoing$ (stream of string messages)
// 创建 outgoing$(字符串消息流)
// ...
return {
sock: outgoing$
};
}
run(main, {
sock: makeSockDriver('B23A79D5-some-unique-id-F2930')
});
```
注意在 `makeSockDriver(peerId)` 中创建 driver 时指定了 `peerId`。如果 `main()` 需要根据不同的逻辑动态连接不同的小伙伴,那就不能再用这个 API 了。相反,需要让 driver 函数将指令作为输入,例如“connect to peerId”,或者“send message to peerId”。这是在设计 driver API 时需要考虑的一种情况。
## 可扩展性
Cycle **Core** 是一个非常小的框架,Cycle **DOM** Driver 是应用程序的一个可选插件。你可以轻松地以任何其他能提供与用户交互功能的 driver 函数来替代 DOM Dirver。
例如,可以 fork 然后修改 DOM Driver 的源代码来适应你的需求,然后在 Cycle.js 应用中使用。可以创建一个与 socket 交互的 driver。执行网络请求的 driver。适用于 Node.js 的 driver。针对其他 UI 树(例如 `<canvas>` 或更底层的 mobile UI)的 driver。
作为一个框架,它不能与近几年统治 web 开发的 monolith 比较。Cycle.js 本身毕竟只是个小工具,是一个使用响应式流创建与外界进行响应式对话的约定。
</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'>中文文档</a></li>
<li><a href='api/index.html'>API 索引</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">流</a></li>
<li><a href="basic-examples.html" class="level-1 out-link">基本例子</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">组件</a></li>
</ul>
<div role='flatdoc-menu'></div>
</div>
</div>
<div role='flatdoc-content' class='content'></div>
<footer style="text-align: center">
<a href="http://www.miitbeian.gov.cn/" target="_blank">
粤ICP备14056081号-3
</a>
</footer>
</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>