如何编写和构建一个 JS 库

在开始之前,我建议您阅读我以前的一篇关于创建极小 size 的 JavaScript 库的文章。Writing JS libraries less than 1TB size

今天的问题:

  • 为什么在库里使用 bundlers(e.g webpack) 和 transpilers(e.g babel) 可能会导致问题。
  • 为什么我说不要使用 不要使用 imports/exports ,你真的需要它吗?
Introduction

我将从一个与文章主题不太相关,但很重要的问题开始 —— 为什么我们需要构建 Libraries 呢?

如果我们使用require我们完全不需要构建库(因为我们阅读了前一篇文章并且不在库里使用 ES6,也没有必要使用 bundlers/transpilers 等工具)。

当我们使用在 Nodejs 中仍然不支持的 ES6 modules 时,情况会有所变化。如我之前所说,你只能在 Node v9 并且带上 -experimental-modules 标志才能使用 ES6 modules。

所以我们需要将我们代码中的的imports/exports替换成require/module.exports,我们需要工具来完成这个工作。

How typical developer build his code?
  • install rollup
  • install babel, rollup plugins to locate modules, resolve’em, some another features
  • build with umd format
  • Oh, wait, why so large???
错误 1 - using umd build

UMD(Unified Module Definition)意味着你可以在浏览器中使用<script>标签或者使用require来引用这个库。

但是它并不是你想象的那么完美 — 它给你的库里加了额外的代码。

假设我们一个 module (e.g EventBus),当我们要使用时:

1
2
3
4
5
6
7
8
9
import EventBus from '../common/module';
const bus1 = new EventBus();
bus1.on('hello', name => {
console.log('hello', name);
});
export default bus1;

假设我们需要在 Node.js app 里使用,先使用 rollup 来进行 UMD 构建。

1
2
3
4
5
6
7
8
9
10
export default [
{
input: 'good/a.js',
ouput: {
name: 'goodA',
file: 'bad/a.js',
format: 'umd'
}
}
];

然后我们可以看到构建出来的代码:

在 js 中创建一个简单的 EventBus

EventBus 在组件之间去耦合化(decoupling)有着极其重要的作用。

它就像是一把双刃剑,必须要谨慎使用,否则你的代码的可读性和可维护性可能会很差。

但毫无疑问,EventBus 能显著地加快你的原型制作过程,改进中小型应用程序的结构体系。对于大型的应用还需要有有一些其它额外的考虑。

在本文中,我将展示如何在 Javascript 中实现一个简单的 EventBus.

What’s an EventBus

EventBus 使用了发布/订阅(publisher/subscriber)架构。

它可以用来解耦一个应用程序里的组件。

这样某个组件可以对从另一个组件触发的事件作出反应,而不需要它们相互了解。它们仅仅需要知道 EventBus 就行了。

每一个订阅(subscriber)者都可以订阅(subscribe)一个特定的事件(event)。当这个订阅的事件触发的时候,订阅者将被通知到。

发布者(publisher)可以在 EventBus 中发布(publish)事件(events),以触发订阅者。

EventBus implementation

在这个实现当中,订阅者就是一个函数。

事件及其回调函数之间的关联,用一个名为 EventCallbacksPair 的对象来表示。

1
2
3
4
const EventCallbacksPair = (eventType, callback) => {
this.eventType = eventType;
this.callbacks = [callback];
};

EventBus 包含一个这样的对象对表。 每一个事件都有一个自己的 EventCallbacksPair。

Subscribe

每当一个订阅者订阅一个事件的时候,可能处于以下两种情况之一:

  1. 目前还没有订阅者订阅过这个事件,因此 EventBus 还不包含与该事件相关的任何 EventCallbacksPair。
  2. 已经有订阅者订阅过这个事件了,因此 EventBus 已经包含与该事件相关的 EventCallbacksPair。

对于第一种情况, 需要为事件新建一个 EventCallbacksPair, 并且添加到 EventBus 的的列表中。

对于第二种情况, 只需要把这个新的回调函数(callback)添加到这个事件的 EventCallbacksPair 中。

1
2
3
4
5
6
7
8
9
10
11
this.subscribe = (eventType, callback) => {
const eventCallbacksPair = fineEventCallbacksPair(eventType);
if (eventCallbacksPair) {
// condition 2
eventCallbacksPair.callbacks.push(callback);
} else {
// condition 1
eventCallbacksPairs.push(new EventCallbacksPair(eventType, callback));
}
};
Publish

每当一个事件在 EventBus 中被发布时, 可能处于以下两种情况之一:

  1. 这个事件包含一个与其相关的 EventCallbacksPair。
  2. 这个事件不包含与其相关的 EventCallbacksPair。

对于第一种情况,我们只需要找到与这个事件相关的 EventCallbacksPair, 并且执行它所包含的所有回调函数(callbacks)。

对于第二种情况,我们什么也不用处理。 这个事件虽然被触发了,但是没有人在监听。

1
2
3
4
5
6
7
8
9
10
this.post = eventType => {
const eventCallbacksPair = findEventCallbacksPair(eventType);
if (!eventCallbacksPair) {
console.error(`No subscribers for event ${eventType}`);
return;
}
eventCallbacksPair.callbacks.forEach(cb => cb());
};
Complete implementation

下面是 EventBus 的完整实现。

同样的代码可参见this Gist

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
function EventBus() {
const eventCallbacksPairs = [];
this.subscribe = (eventType, cb) => {
const eventCallbacksPair = findEventCallbacksPair(eventType);
if (eventCallbacksPair) {
eventCallbacksPair.callbacks.push(cb);
} else {
eventCallbacksPairs.push(new EventCallbacksPair(eventType, cb));
}
};
this.post = (eventType, args) => {
const eventCallbacksPair = findEventCallbacksPair(eventType);
if (!eventCallbacksPair) {
console.error(`No subscribers for event ${eventType}`);
return;
}
eventCallbacksPair.callbacks.forEach(cb => cb(args));
};
function findEventCallbacksPair(eventType) {
return eventCallbacksPairs.find(
eventCallbacksPair => eventCallbacksPair.eventType === eventType
);
}
function EventCallbacksPair(eventType, cb) {
this.eventType = eventType;
this.callbacks = [cb];
}
}
Events

事件可以要任意数据类型,我一般使用strings,这样看起来是最合理的,但不是唯一的选择。

hexo gist plugin 阻塞页面

不知道小伙伴们在给自己写的hexo theme做unit test的时候,有没有发现,那篇tag plugin测试post的gist部分会导致页面加载像是卡住了一样。

hexo对gist标签的处理是这样的:

1
2
3
4
5
function gistTag(args, content) {
var id = args.shift();
var file = args.length ? '?file=' + args[0] : '';
return '<script src="//gist.github.com/' + id + '.js' + file + '"></script>';
}

这是没有问题的,问题在于,<script>元素在不使用deferasync属性的情况下,是会阻塞页面后面内容的解析,再由于国内网络环境,整个页面就像是被卡住了一样,体验非常不好。

于是就想,给上面那段代码的script标签加上deferasync不就好了吗?

嗯,整段gist的内容干脆不显示了。根据console上的提示search之,得到来自MDN的答案如下:

Note: document.write in deferred or asynchronous scripts will be ignored, and you’ll get a message like “A call to document.write() from an asynchronously-loaded external script was ignored” in the error console.

简单来说,'//gist.github.com/xxxx.js'这个js本质上就是用document.write往页面里写东西。你要是在script里加上deferasync还不行,整段代码会被忽略掉导致页面上毛线也没有。

所幸,在github发现在另外一个一样功能的插件:gist-embed

gist-embed是通过ajax来处理目标gist的json版本数据。而这个过程是异步的,所以并不会阻塞后面内容的解析。

具体的使用方法结合gist-embed上所写的,由于我相当于要使用这个来替换hexo默认的gist插件,所以在主题的文件夹下增加了一个scripts/customGist.js,内容如下:

1
2
3
4
5
hexo.extend.tag.register('gist', function (args) {
var id = args.shift();
var file = args.length ? '?file=' + args[0] : '';
return '<code data-gist-id="' + id + '" class="highlight plain"></code>';
});

一切就绪后hexo ghexo s,铛铛铛铛~

Gallery Post

This post contains 4 photos:

  • Widescreen wallpaper
  • Portrait photo
  • Dual widescreen wallpaper
  • Small photo

All photos should be displayed properly.

From Wallbase.cc

www.google.com

This is a link post without a title. The title should be the link with or without protocol. Clicking on the link should open Google in a new tab or window.