Micooz

Make something different!


  • Home

  • About

  • Tags

  • Archives

  • Search

基于 EventEmitter 的双向数据 pipeline 实现

Posted on 2018-04-14 |

基于 EventEmitter 的双向数据 pipeline 实现

想法来源

Gulp

Gulp 是前端工具链中常用的流式任务执行器,适用于许多小型库的编译打包任务。它的设计思想其实很像 Linux 命令行里面的 Pipe(管道):

1
2
3
4
5
gulp.src(paths.scripts.src, { sourcemaps: true })
.pipe(babel())
.pipe(uglify())
.pipe(concat('main.min.js'))
.pipe(gulp.dest(paths.scripts.dest));

gulp 是单向的,即对于同一个 pipeline,数据一般不能被逆向还原。

TCP/IP stack

我们知道,计算机网络协议是分层设计的,每层分别为数据赋予不同的含义、完成不同的使命。源主机采用网络协议栈将原始二进制流 层层编码(encode) 后送往目的主机,目的主机采用同样的协议栈将数据 层层解码(decode) 后得到原始数据。典型的 HTTP 协议将请求数据通过 TCP/IP 协议栈自上而下编码后送出,之后自下而上解码后得到响应数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
                                                      +---------+
| "Hello" |
+---------+
+-------------+---------+
| HTTP header | PAYLOAD |
+-------------+---------+
+------------+-----------------------+
| TCP header | PAYLOAD |
+------------+-----------------------+
+------------+------------------------------------+
| IP header | PAYLOAD |
+------------+------------------------------------+
+------------+--------------------------------------------------+
| Eth header | PAYLOAD |
+------------+--------------------------------------------------+

TCP/IP 协议栈是双向的,即对于同一套协议,数据既可以被编码也可以被解码。

那么问题来了,是否可以抽象一种轻量的 Pipeline,实现类似网络协议栈双向数据流的处理能力,并且能够让用户定制化每层的处理逻辑?

数据流

设计之前,先根据数据流划分功能模块,这里 PIPE 是数据和各个数据处理单元的调度者,PIPE_UNIT_x 是每层数据的处理单元,可以有多个,并且按顺序前后串联。

1
2
3
                +------------------ PIPE -------------------+
[RAW_DATA] <==> | [PIPE_UNIT_1] <==> ... <==> [PIPE_UNIT_2] | <==> [ENCODED_DATA]
+-------------------------------------------+

用户可以实现自己的 PIPE_UNIT 来达到定制化处理逻辑的功能,也可以任意调换 PIPE_UNIT 的顺序来达到不同的处理效果。

Pipe 设计

Pipe 需要提供一个数据入口来启动链式处理流程:

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
const EventEmitter = require('events');

const PIPE_TYPE_ENCODE = 'PIPE_TYPE_ENCODE';
const PIPE_TYPE_DECODE = 'PIPE_TYPE_DECODE';

class Pipe extends EventEmitter {

// 构造 Pipe 时,传入的处理单元数组约定为 encode 顺序
constructor(units) {
super();
this._encode_units = units;
this._decode_units = [].concat(units).reverse();
}

// 数据处理入口
feed(type, data) {
const units = type === PIPE_TYPE_ENCODE ? this._encode_units : this._decode_units;
if (units.length < 1) {
return;
}
const first = units[0];
if (first.listenerCount(type) < 1) {
// 构建链式响应逻辑
const last = units.reduce((prev, next) => {
prev.on(type, (dt) => next._write(type, dt));
return next;
});
last.on(type, (dt) => {
// 最后一个 unit 完成之后 feed 的任务就结束了
this.emit(type, dt);
});
}
// 触发处理流程
first._write(type, data);
}

}

PipeUnit 接口设计

PipeUnit 需要暴露编码(encode)和解码(decode)两个接口,考虑到处理单元可能异步执行,因此使用 async 黑膜法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class PipeUnit extends EventEmitter {

async _write(type, data) {
if (type === PIPE_TYPE_ENCODE) {
this.emit(type, await this.encode(data));
} else {
this.emit(type, await this.decode(data));
}
}

// 编码接口
async encode(data) {
return data;
}

// 解码接口
async decode(data) {
return data;
}

}

实现 PipeUnit

首先实现一个提供压缩、解压缩功能的 PipeUnit:

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
const zlib = require('zlib');

class ZipPipeUnit extends PipeUnit {

async encode(data) {
console.log('ZipPipeUnit::encode <-', data);
return new Promise((resolve, reject) => {
zlib.deflate(data, (err, buffer) => {
if (err) {
reject(err);
} else {
resolve(buffer);
}
});
});
}

async decode(data) {
console.log('ZipPipeUnit::decode <-', data);
return new Promise((resolve, reject) => {
zlib.unzip(data, (err, buffer) => {
if (err) {
reject(err);
} else {
resolve(buffer);
}
});
});
}

}

下面再实现一个提供 AES 对称加解密功能的 PipeUnit,这次采用同步执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const crypto = require('crypto');

class CryptoPipeUnit extends PipeUnit {

// 编码实现
encode(plaintext) {
console.log('CryptoPipeUnit::encode <-', plaintext);
const cipher = crypto.createCipher('aes192', 'a password');
const encrypted = cipher.update(plaintext);
return Buffer.concat([encrypted, cipher.final()]);
}

// 解码实现
decode(ciphertext) {
console.log('CryptoPipeUnit::decode <-', ciphertext);
const decipher = crypto.createDecipher('aes192', 'a password');
const decrypted = decipher.update(ciphertext);
return Buffer.concat([decrypted, decipher.final()]);
}

}

实际运行

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
// 自由组合处理单元
const units = [
new ZipPipeUnit(),
new CryptoPipeUnit(),
// new CryptoPipeUnit(), // 再来一个也可以
];

// 来一个 pipe 对象
const pipe = new Pipe(units);

pipe.on(PIPE_TYPE_ENCODE, (data) => {
console.log('encoded:', data);
console.log('');
// 解码
pipe.feed(PIPE_TYPE_DECODE, data);
});
pipe.on(PIPE_TYPE_DECODE, (data) => console.log('decoded:', data.toString()));

// 编码
pipe.feed(PIPE_TYPE_ENCODE, Buffer.from('awesome nodejs'));

// 输出如下:
// ZipPipeUnit::encode <- <Buffer 61 77 65 73 6f 6d 65 20 6e 6f 64 65 6a 73>
// CryptoPipeUnit::encode <- <Buffer 78 9c 4b 2c 4f 2d ce cf 4d 55 c8 cb 4f 49 cd 2a 06 00 2a 0c 05 95>
// encoded: <Buffer a9 61 bc 37 1a 4c 41 e8 20 63 d2 90 86 94 7b 48 98 b1 91 16 84 66 58 9b 6d 88 53 da 9b b9 18 fb>

// CryptoPipeUnit::decode <- <Buffer a9 61 bc 37 1a 4c 41 e8 20 63 d2 90 86 94 7b 48 98 b1 91 16 84 66 58 9b 6d 88 53 da 9b b9 18 fb>
// ZipPipeUnit::decode <- <Buffer 78 9c 4b 2c 4f 2d ce cf 4d 55 c8 cb 4f 49 cd 2a 06 00 2a 0c 05 95>
// decoded: awesome nodejs

可以看到,通过对 EventEmitter 简单的封装就可以实现双向数据 pipeline,同时支持异步单元操作。

性能测试

功能实现了,性能又如何呢?抛开 PipeUnit 的业务实现,简单分析一下链式 EventEmitter 结构的性能影响因素,理论上很大程度取决于 EventEmitter 本身的性能,Pipe::feed 只在第一次被调用时构建响应链,之后的调用几乎不会有性能损失。

测试用例

Node.js 版本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> process.versions
{ http_parser: '2.8.0',
node: '9.11.1',
v8: '6.2.414.46-node.23',
uv: '1.19.2',
zlib: '1.2.11',
ares: '1.13.0',
modules: '59',
nghttp2: '1.29.0',
napi: '3',
openssl: '1.0.2o',
icu: '61.1',
unicode: '10.0',
cldr: '33.0',
tz: '2018c' }

下面分别考察 0 ~ 30000(每次递增 1000) 个 PipeUnit 实例的执行时间,来评估上述设计的性能表现:

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
const { performance } = require('perf_hooks');

const payload = Buffer.alloc(4096);

for (let i = 0; i <= 30; i++) {
const units = Array(i * 1000).fill().map(() => new PipeUnit());

performance.mark('A_' + i);
{
const pipe = new Pipe(units);
pipe.on(PIPE_TYPE_ENCODE, (data) => {
pipe.feed(PIPE_TYPE_DECODE, data);
});
pipe.on(PIPE_TYPE_DECODE, () => null);
pipe.feed(PIPE_TYPE_ENCODE, payload);
}
performance.mark('B_' + i);

performance.measure(`${units.length} units`, 'A_' + i, 'B_' + i);
}

const entries = performance.getEntriesByType('measure');
for (const { name, duration } of entries) {
console.log(`${name}: ${duration}ms`);
}

执行4次,可以将结果绘制到一张图中:

可以看到每次运行的结果高度一致,由上万个 PipeUnit 构成的链式 EventEmitter 能够以令人满意的效率完成运行。

不过出人意料的是,在特定数量的 PipeUnit 上总会出现尖峰,这可能和 V8 引擎的优化机制有关,作者能力有限,感兴趣的同学可以深挖原因。

高效加载 WebAssembly 模块

Posted on 2018-04-13 |

【译文】高效加载 WebAssembly 模块

原文作者:Mathias Bynens
原文地址:https://developers.google.com/web/updates/2018/04/loading-wasm

在使用 WebAssembly 的时候,通常需要下载、编译、实例化一个模块,然后通过 JavaScript 调用由该模块导出的一些东西。这篇文章从一个常见但不是很优秀的代码片段开始,讨论几种可能的优化方法,最后得出最简单、最高效的通过 JavaScript 运行 WebAssembly 的方法。

注意:诸如 Emscripten 的一些工具可以为你准备样板代码,因此你不需要自己编写 wasm 代码。如果你要更加细粒度地控制 WebAssembly 模块加载,那么请专注于下面的最佳实践吧。

下面的代码片段完成了下载-编译-实例化的整个过程,尽管不是很优秀的方式:

1
2
3
4
5
6
7
8
9
// 别这么做
(async () => {
const response = await fetch('fibonacci.wasm');
const buffer = await response.arrayBuffer();
const module = new WebAssembly.Module(buffer);
const instance = new WebAssembly.Instance(module);
const result = instance.exports.fibonacci(42);
console.log(result);
})();

注意我们是如何使用 new WebAssembly.Module(buffer) 将响应数据转换成一个模块的。这是一个同步 API,意味着它在完成之前会阻塞主线程。为了防止滥用,Chrome 禁止在超过 4KB 的 buffer 上使用 WebAssembly.Module。为了避免大小限制,我们可以用 await WebAssembly.compile(buffer) 来代替:

1
2
3
4
5
6
7
8
(async () => {
const response = await fetch('fibonacci.wasm');
const buffer = await response.arrayBuffer();
const module = await WebAssembly.compile(buffer);
const instance = new WebAssembly.Instance(module);
const result = instance.exports.fibonacci(42);
console.log(result);
})();

await WebAssembly.compile(buffer) 仍然不是一个优秀的方式,不过姑且先这样。

在修改后的代码片段中,几乎所有的操作都是异步的,因为 await 使之变得很清晰。只有 new WebAssembly.Instance(module) 是个例外。为了一致性,我们可以用异步 WebAssembly.instantiate(module)。

1
2
3
4
5
6
7
8
(async () => {
const response = await fetch('fibonacci.wasm');
const buffer = await response.arrayBuffer();
const module = await WebAssembly.compile(buffer);
const instance = await WebAssembly.instantiate(module);
const result = instance.exports.fibonacci(42);
console.log(result);
})();

让我们回顾一下我之前提到的对于 compile 的优化。利用 流式编译,浏览器可以在模块数据下载过程中就开始编译 WebAssembly 模块。因为下载和编译过程是并发的,特别是对于大模块,这样将会更快。

使用 WebAssembly.compileStreaming 替换 WebAssembly.compile 可以开启这个功能。这么做之后还可以避免产生中间数据,因为现在我们可以直接传递由 await fetch(url) 返回的 Response 实例。

1
2
3
4
5
6
7
(async () => {
const response = await fetch('fibonacci.wasm');
const module = await WebAssembly.compileStreaming(response);
const instance = await WebAssembly.instantiate(module);
const result = instance.exports.fibonacci(42);
console.log(result);
})();

注意:服务端必须经过配置能够支持以 Content-Type: application/wasm 头发送 .wasm 文件。在之前的例子中,我们将响应数据当做 arraybuffer 传递,因此不需要进行 MIME 类型检查。

WebAssembly.compileStreaming API 也能接收一个 resolve 为 Response 的 promise 实例。如果你没在别的地方使用 response,你可以直接传递由 fetch 返回的 promise 对象,而不需要 await:

1
2
3
4
5
6
7
(async () => {
const fetchPromise = fetch('fibonacci.wasm');
const module = await WebAssembly.compileStreaming(fetchPromise);
const instance = await WebAssembly.instantiate(module);
const result = instance.exports.fibonacci(42);
console.log(result);
})();

如果你也没在其他地方使用 fetch 的结果,你甚至也可以直接传递它:

1
2
3
4
5
6
7
(async () => {
const module = await WebAssembly.compileStreaming(
fetch('fibonacci.wasm'));
const instance = await WebAssembly.instantiate(module);
const result = instance.exports.fibonacci(42);
console.log(result);
})();

我个人认为将其单独成行可读性更好。

想知道我们是如何将响应数据编译为模块然后实例化的?事实证明,WebAssembly.instantiate 可以一步到位。

1
2
3
4
5
6
7
8
(async () => {
const fetchPromise = fetch('fibonacci.wasm');
const { module, instance } = await WebAssembly.instantiateStreaming(fetchPromise);
// 稍后创建 instance 对象:
const otherInstance = await WebAssembly.instantiate(module);
const result = instance.exports.fibonacci(42);
console.log(result);
})();

如果你只需要 instance 对象,那没理由再保留 module 对象,简化代码如下:

1
2
3
4
5
6
7
// 这是加载 WebAssembly 的建议方式。
(async () => {
const fetchPromise = fetch('fibonacci.wasm');
const { instance } = await WebAssembly.instantiateStreaming(fetchPromise);
const result = instance.exports.fibonacci(42);
console.log(result);
})();

总结一下我们所用过的优化方法:

  • 使用异步 API 来避免阻塞主线程
  • 使用流式 API 来加快 WebAssembly 模块的编译和实例化速度
  • 不要写你不需要的代码

尽情享用 WebAssembly 吧!

React 16.3 Context API 实践

Posted on 2018-03-12 |

React 16.3 Context API 实践

摘要

本文简单介绍了 Context API 的提出背景、API 设计和用法;之后比较了 React-Redux 的设计;然后提出了一种基于 Context API 的二次封装;最后再将二次封装和 mobx-react 进行了比较。

背景

React 项目组成员 @acdlite 于 2017-12-05 提出关于新 Context API 的 RFC。

实际上 Context 在 React 的早期版本中就已经存在,但不能解决当 shouldComponentUpdate 返回 false 时,组件无法响应 context 的改变的问题。由于 shouldComponentUpdate 常用于性能优化,被大量开源库或框架广泛使用,因此原版的 Context 变得十分鸡肋,新的 Context API 很好地解决了这一问题。

Context API

首先安装 16.3.x 版本的 react 及 react-dom:

1
$ yarn add react@next react-dom@next

来看一个简单的例子:

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
import React, { createContext } from 'react';
import ReactDOM from 'react-dom';

const { Provider, Consumer } = createContext({});

class Child extends React.Component {

render() {
// const { date } = this.props;
return (
<div>
<Consumer>
// 子组件在任意位置通过 <Consumer> 消费顶层给 <Provider> 传入的 value,
// 感知 value 的变化来重新渲染该区块。
{({ date }) => <p>{date}</p>}
</Consumer>
</div>
);
}

}

class App extends React.Component {

state = {
date: '',
};

componentDidMount() {
setInterval(() => {
// 在父组件中更新状态
this.setState({ date: new Date().toString() });
}, 1e3);
}

render() {
return (
<Provider value={this.state}>
// 父组件不用给子组件显式传入任何数据
<Child/>
</Provider>
);
}

}

ReactDOM.render(<App/>, document.querySelector('#root'));

使用 Context API 最大的好处就是解决深层嵌套组件层层传递 props 的问题。但这样做也存在一个问题: state 的被保存在 <App> 中,更新状态时必须调用 <App> 的 this.setState(),如果子组件需要更新 state,那么需要通过 <Provider> 向下传递封装了 this.setState() 的回调函数:

1
2
3
4
5
6
7
8
<Provider value={{ 
state: this.state,
actions: {
doSomething(newState) { this.setState(newState); }
}}}
>
<Child/>
</Provider>

之后,子组件要求改变状态时,在 <Consumer> 中调用该回调方法即可:

1
2
3
4
5
<Consumer>
{({ state: { date }, actions }) =>
<button onClick={() => actions.doSomething(...)}>{date}</button>
}
</Consumer>

另有一个问题是如何实现在 组件外 更新状态,让组件也能响应状态变化?

这样的需求通常在应用需要与第三方库交互时会遇到,举一个实际的例子:

Q: 一个 web 应用使用 websocket 做数据交换,我们需要在页面上实时显示 websocket 连接的延迟:

1
2
3
4
5
6
// ws.js
const ws = io.connect('/');

ws.on('pong', (latency) => {
// 如何将 latency 渲染到组件里?
});

先前例子中将 state 内化的方式显然不可行了,这个时候联想到 Redux,利用它全局 store 的设计,借助 store.dispatch 就可以实现上面的需求了。

Redux/React-Redux

React-Redux 是对 React 老版本 Context 的封装,它允许子组件通过 connect 方法建立对 store 中状态变化的响应,下面是一个简单的 Redux 应用:

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
// app.js
import React from 'react';
import { createStore } from 'redux';
import { connect } from 'react-redux';

// 创建一个 reducer 来处理 action
function reducer(state = { date: '' }, action) {
switch (action.type) {
case 'UPDATE_DATE':
return { date: action.date };
default:
return state;
}
}

// 创建一个全局 store 来存储状态
const store = createStore(reducer);

class App extends React.Component {

componentDidMount() {
setInterval(() => {
// 发一个 action 来更新 store
store.dispatch({ type: 'UPDATE_DATE', date: new Date().toString() });
}, 1e3);
}

render() {
return (
<Provider store={store}>
// 父组件不用给子组件显式传入任何数据
<Child/>
</Provider>
);
}

}

ReactDOM.render(<App/>, document.querySelector('#root'));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// child.js
function mapStateToProps(state) {
return { date: state.date };
}

// 通过 connect 来感知全局 store 的变化
@connect(mapStateToProps, null)
class Child extends React.Component {

render() {
return (
<div>{this.props.date}</div>
);
}

}

可以看到在 Redux 的套路中,完成一次 状态更新 需要 dispatch 一个 action 到 reducer,这个过程同时牵扯到三个概念,有些复杂;而在 Context API 的套路中,完成一次 状态更新 只需要 setState(...) 就够了,但单纯的 Context API 无法解决先前提到的 组件外 更新状态的问题。

对 Context API 的简单封装

下面对 Context API 进行二次封装,让它支持类似 Redux 全局 store 的特性,但用法又比 Redux 更加简单。

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
// context.js
import React, { createContext } from 'react';

const AppContext = createContext();

// self 是对 <Provider> 组件实例的引用
let self = null;

class Provider extends React.Component {

state = {};

constructor(props) {
super(props);
self = this;
}

render() {
return (
<AppContext.Provider value={this.state}>
{this.props.children}
</AppContext.Provider>
);
}

}

const Consumer = AppContext.Consumer;

function getState() {
if (self) {
return self.state;
} else {
console.warn('cannot getState() because <Provider> is not initialized');
}
}

function setState(...args) {
if (self) {
self.setState(...args);
} else {
console.warn('cannot setState() because <Provider> is not initialized');
}
}

function createStore() {
return { getState, setState };
}

export { Provider, Consumer, createStore };

用法如下:

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
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider, Consumer, createStore } from './context';

// 新建一个全局 store
const store = createStore();

class Child extends React.Component {

render() {
return (
<div>
<p>1. 通过回调参数获取最新状态</p>
<Consumer>
{({ date }) => <div>{date}</div>}
</Consumer>
<p>2. 通过 store.getState() 获取所有状态</p>
<Consumer>
{() => <pre>{JSON.stringify(store.getState(), null, 2)}</pre>}
</Consumer>
<p>3. 通过 store.setState() 更新状态</p>
<Consumer>
{() => <button onClick={() => store.setState({ foo: new Date().toString() })}>子组件触发状态更新</button>}
</Consumer>
</div>
);
}

}

class App extends React.Component {

componentDidMount() {
// 父组件触发状态更新
setInterval(() => {
store.setState({ date: new Date().toString() });
}, 1e3);
}

render() {
return (
// 现在 <Provider> 不需要任何参数了
<Provider>
<Child/>
</Provider>
);
}

}

ReactDOM.render(<App/>, document.querySelector('#root'));

现在在应用的任意位置调用 store.setState() 方法,就能更新组件的状态了:

1
2
3
4
5
6
7
8
9
import store from './store';

// ws.js
const ws = io.connect('/');

ws.on('pong', (latency) => {
// 如何将 latency 渲染到组件里?
store.setState({ latency });
});

和 mobx/mobx-react 进行比较

MobX 基于观察者模式,通过 mobx-react 封装后许多地方和 Context API 类似,下面是官方提供的一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class App extends React.Component {
render() {
return (
<div>
{this.props.person.name}
<Observer>
{() => <div>{this.props.person.name}</div>}
</Observer>
</div>
)
}
}

const person = observable({ name: "John" })

React.render(<App person={person} />, document.body)
person.name = "Mike" // will cause the Observer region to re-render

在 mobx-react 的套路中,组件可以通过 <Observer> 消费由 observable() 创建出来的对象,直接修改该对象中的键值可以实现组件的重新渲染。

二次封装后的 Context API 相比 mobx-react 用法相近,但 mobx 得益于 setter/getter Hooks 具有更直观的状态改变方式。

参考资料

  • https://github.com/acdlite/rfcs/blob/new-version-of-context/text/0000-new-version-of-context.md
  • https://github.com/reactjs/rfcs/pull/2
  • https://github.com/facebook/react/pull/11818

Qnimate, an animated colorful voronoi diagram powered by d3.js

Posted on 2016-10-11 |

qnimate

An animated colorful Voronoi diagram
powered by d3.

Live demo

https://micooz.github.io/qnimate

Try it!

Install via npm:

1
2
$ git clone https://github.com/micooz/qnimate
$ cd qnimate && npm i

Run in development:

1
$ npm start

Build and bundle:

1
$ npm run build:prod

Usage

HTML:

<div id=""playground""></div>

js:

<script src=""qnimate.min.js""></script>

Qnimate will be exposed to window, create an instance of Qnimate, pass an option object and then call run():

1
2
3
4
5
6
7
8
9
document.addEventListener('DOMContentLoaded', function main() {
var qnimate = new Qnimate({
id: 'playground',
width: 960,
height: 500,
vertices: 40
});
qnimate.run();
});

Acknowledgments

d3.voronoi - https://github.com/d3/d3-voronoi

Known issues

  • New triangle appears suddenly.
  • White triangle appears from time to time.

Any advice?

Send me issue.

How to disable text selection in svg

Posted on 2016-07-26 |

In sass style:

1
2
3
4
5
6
7
text {
user-select: none;

&::selection {
background: none;
}
}

D3绘图套路

Posted on 2016-06-16 |

接触D3三天,发现有些套路可以反复运用,所以记录一下。

D3相比其他图表库,学习成本较高。但最为灵活,需要使用者精雕细琢图形的每个细节。D3处处体现了函数式的编程思维。

NOTE: 这里所用D3版本是:

1
2
3
""dependencies"": {
""d3"": ""^4.0.0-alpha.49""
}

下面以一个简单的横纵坐标图为例。

① 定义高宽

1
2
3
4
5
6
7
var margin = {left: 70, top: 20, right: 20, bottom: 50};

var svgWidth = 800;
var svgHeight = 500;

var graphWidth = svgWidth - margin.left - margin.right;
var graphHeight = svgHeight - margin.top - margin.bottom;

这一步实际上比较重要,图形高宽参数会在后面绘图中常常用到。

② 创建svg

1
2
3
var svg = d3.select('.container').append('svg')
.attr('width', svgWidth)
.attr('height', svgHeight);

这里可以直接在html中放一个<svg>,但为了可移植性,用脚本生成。

③ 创建绘制区域

1
2
3
4
var graph = svg.append('g')
.attr('width', graphWidth)
.attr('height', graphHeight)
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');

<g>中将包含所有图形元素(坐标轴、标签、线条……),现在svg树是这样的:

1
2
3
<svg>
<g></g>
</svg>

④ 设置X、Y的图形坐标范围

1
2
3
4
5
var x = d3.scaleTime()
.range([0, graphWidth]);

var y = d3.scaleLinear()
.range([graphHeight, 0]);

这个时候由于数据还没获取,只能先设置他们的图形坐标范围,注意这个不是数据的定义域。

这个两个函数通常有两个用途,以x为例:

1
2
var px = x(data); // 根据数据值计算对应的x坐标值
var data = x.invert(px); // 根据x坐标值反算对应的数据值

⑤ 获取数据

一般情况下都是从远端取回特定格式的数据,这是个异步过程:

1
2
3
d3.csv('data.csv', parser, function(err, data) {
// data
})

D3很人性化的给你留了个格式化数据的地方parser。

1
2
3
4
5
6
function parser(d) {
return {
date: d3.timeParse('%b %Y')(d.date),
value: +(d.price)
};
}

D3取出数据的每一行,依次传入该函数,然后可以返回你需要的格式化数据,最后以数组形成出现在data变量中。

⑥ 设置X、Y的值域

1
2
3
4
5
6
7
8
// value domain of x and y
x.domain(d3.extent(data, function (d) {
return d.date;
}));

y.domain([0, d3.max(data, function (d) {
return d.value;
})]);

现在可以给x和y设置值域了,值域类型可以很灵活,上面设置x的值域是一个时间范围。

⑦ 开始绘制

接下来就是构思你图形的各个部分了,坐标轴、标签、图形内容等等,也是逐步生成一个完整svg的过程。为了简便起见,这里不会贴出冗余代码。

绘制X轴:

1
2
3
4
5
6
// X、Y轴可以利用D3的axisXXXXXX函数简单创建,
// 它会把坐标轴的每个数据标签、包括刻度线都为你生成好
graph.append('g')
.attr('class', '.x-axis')
.attr('transform', 'translate(-1,' + graphHeight + ')')
.call(d3.axisBottom(x));

绘制折线:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 创建一个线段生成器,会根据绑定数据,通过x和y访问器计算每个点的坐标位置
var line = d3.line()
.x(function (d) {
return x(d.date)
})
.y(function (d) {
return y(d.value)
});

// 折线图可以用svg中的<path>并设置其d属性来绘制
// line函数需要一个点集来生成一个字符串,这个字符串可以直接填充<path>的d属性
// 为了使线段可见,还需要设置其stroke和stroke-width样式。
var path = graph.append('path')
.attr('d', line(data));

至此,就可以看到一个基本图形了。

【记坑】关于d3.zoom

Posted on 2016-06-14 |

D3.js的最新v4版提供了d3.zoom模块,用它可以给svg图形增加缩放/移动的特性,但通过非鼠标/触控的方式改变图形的位置和缩放比例后,d3.zoom的行为就变得不正常了。本文给出一个解决方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1. 初始化一个zoom行为
const zoom = d3.zoom();

// 2. 设置zoom行为参数
zoom.scaleExtent([.5, 5]);
...

// 3. 添加三个事件监听器
zoom.on('start', () => {...});
zoom.on('zoom', zoom);
zoom.on('end', () => {...});

// 4. 应用行为
g.call(zoom);

我们一般会关注zoom事件:

1
2
3
4
const zoom = () => {
// d3自动计算的transform值会放在d3.event里
container.attr('transform', d3.event.transform);
};

有趣的是,如果我们在其他地方主动设置:

1
container.attr('transform', 'translate(10, 10)');

再用鼠标或者触控板调整图形时,zoom行为并不会从(10, 10)位置开始计算下一个d3.event.transform,zoom行为似乎自己保存了上一次的transform,而不关心我们设置到container上的transform。

结果就是我们用attr设置的transform其实是临时的。

如果查看应用了zoom行为的宿主元素(这里是g)的属性,会发现其Element上有一个__zoom: Transform,这个玩意儿每次走zoom方法的时候都会变化,这正是zoom自己保存的transform对象。

所以我们在主动设置transform后,只需要想办法更新这个__zoom就可以骗过zoom行为,让它下一次调用zoom()事,从我们设置的值开始计算。

查阅文档:

https://github.com/d3/d3-zoom/blob/master/README.md#zoom_transform

https://github.com/d3/d3-zoom/blob/master/README.md#transform_scale

可以这样设置:

1
2
3
4
5
6
7
8
9
10
11
// 自定义transform
container.attr('transform', 'translate(10, 10)');

// 初始化一个空的Transform对象,这一点文档没说明怎么构造一个Transform对象
const transform = d3.zoomTransform(0)

// 填充{x, y}
.translate(10, 10);

// 注意一定要设置在.call(zoom)的元素上,它才有'__zoom'
d3.zoom().transform(g, transform);

之后d3触发zoom回调之前会取g的__zoom计算下一次的transform,这个transform才是我们想要的。

link in docker

Posted on 2016-05-12 |

–link in docker

容器A通过–link选项使用容器B的某个服务,docker-compose.yml 配置如下:

A:
  image: image_a
  links:
    - B:B
  ...

B:
  image: image_b
  ...

当容器B被重启后,A的link不会被自动更新,要一并重启A才行:

$ docker-compose restart B # A doesn't work
$ docker-compose restart A # it works well

webpack-dev-server 最佳实践

Posted on 2016-05-10 |

简述

webpack-dev-server 是用express和websocket实现的一套在开发环境下前端自动更新的工具。

webpack-dev-server提供CLI接口,读取传入的webpack.config.js配置文件,根据webpack配置,建立一个静态服务器,供前端加载静态资源,其中有一个关键附加脚本是 webpack-dev-server.js,位于PATH根路径,即 /webpack-dev-server.js,其中存放着websocket客户端。

一般使用方法

可以通过下面的命令运行webpack-dev-server:

$ node node_modules/.bin/webpack-dev-server --config webpack/dev.config.js --inline --profile --colors --watch --display-error-details --display-cached

参数说明参考:这里

执行后会自动运行webpack进行打包等一系列操作。

在webpack配置文件中只需添加一个 devServer 配置项即可定义webpack-dev-server的行为:

1
2
3
4
5
6
7
8
9
10
devServer: {
port: 3000,
host: 'localhost',
historyApiFallback: true,
quiet: false,
watchOptions: {
aggregateTimeout: 300,
poll: 1000
}
},

在这个例子中,webpack-dev-server会在本地3000端口上启动一个静态服务器,服务器serve的目录是webpack的必选配置 output.path,这是一个绝对路径。

一些问题?

请考虑下面这个问题:

我有一个网站项目,分模块,每个模块是一个node项目,且每个模块可以独立存在(启动,调试,运行),它们有些用到了webpack-dev-server。

再次强调每个模块相互独立,它们之间的耦合方式只有一种:请求代理。

现在假设模块A作为API服务器,监听3000端口;模块B作为应用服务器,要提供资源给浏览器,于是用webpack-dev-server在端口3001的 / 上建立了静态服务器。模块B还要从模块A存取数据,那么必定存在从3001跨域请求到3000的问题,消除这个问题有多种解决办法:

  1. 在A上设置 Access-Control-Allow-Origin 为B的域。
  2. 在A、B上层建立代理服务器,屏蔽端口限制。

不深入讨论上面的方法,现在假设我们采用方法二解决了跨域请求问题,然后我们再考虑一下接下来的一个问题:

假设存在模块C,和B十分类似,也属于应用服务器;如果B和C存在同名资源,比如 main.js,访问该资源就会引发冲突,因为两个模块都在 / 上建立了静态服务器,而这又符合每个模块可以独立存在的先决条件:

// B
http://localhost/B/index.html
http://localhost/main.js
// C
http://localhost/C/index.html
http://localhost/main.js // 哪个 main.js ?

解决办法看似很明显:

// B
http://localhost/B/index.html
http://localhost/B/main.js
// C
http://localhost/C/index.html
http://localhost/C/main.js // everyone is happy

但这又破坏了每个模块的独立性,我希望单独启动C时,C总能从 / 上获取资源,而不是 /C/... 这么冗余。

最佳实践

问题就出在 webpack-dev-server,它适合作为静态资源服务器,而不是开发服务器。因此,我们的开发环境除了需要 webpack-dev-server,还需要专门的开发服务器。

// => Module B
// dev server
http://localhost/B/index.html
// webpack-dev-server for B
http://localhost:3001/...

// => Module C
// dev server
http://localhost/C/index.html
// webpack-dev-server for C
http://localhost:3003/...

每个模块从对应的 webpack-dev-server 获取资源,解决了冲突又保留了每个模块的独立性。

Angular2 如何多次触发子组件的 ngOnInit

Posted on 2016-03-29 |

通常子组件加载后只会执行一次ngOnInit,不利于子组件的自我更新,但设法使子组件从Dom中移除后重建就可以多次触发ngOnInit。

1
<person *ngIf="show"></person>
1
2
3
4
5
class PersonComponent {
ngOnInit() {
// triggered if show is available
}
}

像这种带星号的指令就是Angular2中一种模板语法糖,可以管控组件的生命周期。

12…5
Micooz Lee

Micooz Lee

FullStack JavaScript Engineer

50 posts
17 tags
RSS
GitHub E-Mail Twitter Instagram
Links
  • 哞菇菌
  • 海胖博客
  • 音風の部屋
  • DIYgod
  • BlueCocoa
  • ShinCurry
  • codelover
  • ChionTang
  • Rakume Hayashi
  • POJO
© 2018 Micooz Lee
Powered by Hexo v3.7.1
|
Theme — NexT.Pisces v6.1.0
0%