[译]设计和构建你自己的JavaScript代码库:提示与技巧

设计和构建你自己的JavaScript代码库:提示与技巧

本文转载自:众成翻译
译者:hpoenixf
链接:http://www.zcfy.cc/article/939
原文:https://www.sitepoint.com/design-and-build-your-own-javascript-library/

代码库:我们一直在使用它们。代码库是开发者把他们会在项目中使用到的代码打包起来形成的,这总能节省时间和避免重复造轮子。拥有一个可重复使用的包,不管是开源的还是闭源的,总比重复构建一样特性的包或者从过去的项目中手动复制粘贴要好。

来自作者的更多文章

除了被打包的代码,还可以更精确的形容代码库吗?除了少数的例外,代码库通常只是一个文件,或者是在同一个文件夹里的几个文件。它的代码应该可以单独保存和在你的项目中正常使用。库文件允许你根据项目的不同来调整结构或者行为。想象一下只能通过USB接口进行通讯的USB设备。一些设备,例如鼠标和键盘,可以通过设备提供的接口来进行配置。

在这篇文章,我会解释如何构建库文件。尽管大部分的方法可以应用到其他语言,但这篇文章主要讲述的是构建JavaScript库文件。

##为什么构建你自己的Javascript库?

首先和最重要的,库文件可以让现有的代码方便的重复利用。你不需要挖出陈旧的项目来复制文件,只需要引入库文件。这也可以让你的应用组件化,让应用的代码库更小更易维护。

Christ Church Library, Oxford

Christ Church Library (source)

任何可以让实现一个具体的功能更容易或者可以被重复利用的抽象的代码,都可以被打包进去库文件。jQuery是一个有趣的例子。尽管jQuery的API有大量的简化的DOM API,在跨浏览器DOM操作比较困难的过去有着相当重要的意义。

如果一个开源项目变得流行并且让许多开发者使用它,人们很有可能通过提出问题或者贡献代码来参加它的开发。不管怎样,都对库和依赖它的项目有所帮助。

一个流行的开源库也会带来很好的机会。公司可能会对你的工作质量表示认可并给你发offer。可能公司会要求你把你的项目整合进他们的应用。毕竟,没人可以比你更了解你的项目。

当然它可能只是一个习惯——享受敲码,帮助他人并在这个过程中学习和成长。你可以提升你的极限和尝试新的东西。

##范围和目标

在写下第一行代码之前,你需要确定你的库的功能——你需要设置一个目标。通过这个目标,你可以专注于你想利用这个库来解决的问题。要牢记你的代码库的原本的形式在解决问题上要更容易使用和记忆。API越简单,使用者学习你的代码库就更容易。引入一个Unix的设计哲学:

只做一件事情并把它做好

问下你自己:你的代码库解决了什么问题?你打算怎么取解决它?你将会一个人完成全部工作,还是引入别人的代码库?

不管代码库体积有多大,尝试制作一个路线图。列出你想要的每一个特性,并将它们尽可能的打散,知道你有一个小巧但是能解决问题的代码库,就像 minimum viable product。这会成为你的第一个版本。从这里开始,你可以在每一个新特性建立里程碑。从本质上来说,你把你的项目变成了比特级的代码块,让每一个特性完成的更好和更有趣。相信我,这会让你保持状态良好。

##API设计

在我看来,我想以使用者的角度来开发我的代码库。你可以称呼它为以用户为中心的设计。在本质上,你正在创造你的代码库的大纲,给予它更多的思考和让选择它的人使用起来更方便。在同时,你需要思考在什么地方需要可以定制,这会在这篇文章的后面讨论。

终极的API测试是尝试一下自己的技术,在你的项目使用你的代码库。试着用你的代码库替换之前的代码,看它是否满足了你想要的特性。试着让你的代码库尽可能的直观,让它可以更灵活的在边界条件下使用,还有定制化(在之后的文章会讲述)。

这是一个关于用户代理字符串的代码库的大纲的可能会有样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Start with empty UserAgent string
var userAgent = new UserAgent;
// Create and add first product: EvilCorpBrowser/1.2 (X11; Linux; en-us)
var application = new UserAgent.Product('EvilCorpBrowser', '1.2');
application.setComment('X11', 'Linux', 'en-us');
userAgent.addProduct(application);
// Create and add second product: Blink/20420101
var engine = new UserAgent.Product('Blink', '20420101');
userAgent.addProduct(engine);
// EvilCorpBrowser/1.2 (X11; Linux; en-us) Blink/20420101
userAgent.toString();
// Make some more changes to engine product
engine.setComment('Hello World');
// EvilCorpBrowser/1.2 (X11; Linux; en-us) Blink/20420101 (Hello World)
userAgent.toString();

根据你的代码的复杂度,你可能会在组织结构上花费一些时间。利用设计模式是组织你的代码库的好办法,甚至可以解决一些技术问题。这还能避免加入新特性带来的大面积重构。

##灵活性和定制性

灵活性是让代码库变得强大的要素,但是确定可以定制与不能定制的界限是很困难的。 chart.jsD3.js是很好的例子。这两个代码库都是用来进行数据可视化的。Chart.js可以很简单的创建不同形式的内置图表。但是如果你想对图像进行更多的掌控,D3.js才是你需要的。

有好几种方法可以把控制权交给用户:配置,暴露公共方法,通过回调和事件。

配置代码库通常在初始化之前完成。但是一些代码库允许尼在运行时对配置进行修改。配置通常被细小的部分限制,只有为了之后的使用而修改它们的数值才是被允许的。

1
2
3
4
5
6
7
8
9
10
// Configure at initialization
var userAgent = new UserAgent({
commentSeparator: ';'
});
// Run-time configuration using a public method
userAgent.setOption('commentSeparator', '-');
// Run-time configuration using a public property
userAgent.commentSeparator = '-';

方法通常是暴露给实例使用的,比如说从实例中获取数据,或者设置实例的数据和执行操作。

1
2
3
4
5
6
7
var userAgent = new UserAgent;
// A getter to retrieve comments from all products
userAgent.getComments();
// An action to shuffle the order of all products
userAgent.shuffleProducts();

回调通常是在公共的方法中被传递的,通常在异步操作后执行用户的代码。

1
2
3
4
5
var userAgent = new UserAgent;
userAgent.doAsyncThing(function asyncThingDone() {
// Run code after async thing is done
});

事件有很多种可能。有点像回调,除了增加事件句柄是不应该触发操作的。事件通常用于监听,你可能会猜到,这可是事件!更像回调的是,你可以提供更多的信息和返回一个数值给代码库去进行操作。

1
2
3
4
5
6
7
8
9
var userAgent = new UserAgent;
// Validate a product on addition
userAgent.on('product.add', function onProductAdd(e, product) {
var shouldAddProduct = product.toString().length < 5;
// Tell the library to add the product or not
return shouldAddProduct;
});

在一些例子中,你可能允许用户对你的代码库进行扩展。因此,你需要暴露一些公共方法或者属性来让用户填充,像Angular的模块 (angular.module('myModule'))和Jquery的 fn (jQuery.fn.myPlugin)或者什么都不做,只是简单的让用户获取你的代码库的命名空间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// AngryUserAgent module
// Has access to UserAgent namespace
(function AngryUserAgent(UserAgent) {
// Create new method .toAngryString()
UserAgent.prototype.toAngryString = function() {
return this.toString().toUpperCase();
};
})(UserAgent);
// Application code
var userAgent = new UserAgent;
// ...
// EVILCORPBROWSER/1.2 (X11; LINUX; EN-US) BLINK/20420101
userAgent.toAngryString();

类似的,这允许你重写方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// AngryUserAgent module
(function AngryUserAgent(UserAgent) {
// Store old .toString() method for later use
var _toString = UserAgent.prototype.toString;
// Overwrite .toString()
UserAgent.prototype.toString = function() {
return _toString.call(this).toUpperCase();
};
})(UserAgent);
var userAgent = new UserAgent;
// ...
// EVILCORPBROWSER/1.2 (X11; LINUX; EN-US) BLINK/20420101
userAgent.toString();

在后面的例子中,允许你的用户获取代码库的命名空间,让你在对扩展和插件的定义方面上的控制变小了。为了让插件遵循一些约定,你可以(或者是应该)写下文档。

##测试

对测试驱动开发(test-driven development)来说,写下大纲是良好的开始。简单来说,指的是在你写实际的代码库之前,在你写下测试准则的时候。如果测试检查的是你的代码特性是否跟期待的一样,以及你在写代码库之前写测试,这就是行为驱动开发。不管怎样,如果你的测试覆盖了你的代码库的每一个特性,而且你的代码通过了所有的测试。你可以确定你的代码是可以正常工作的。

Jani Hartikainen讲述了如何利用Mocha来进行单元测试 Unit Test Your JavaScript Using Mocha and Chai。在使用Jsmine,Travis,Karma测试JavaScript (Testing JavaScript with Jasmine, Travis, and Karma)这篇文章中,Tim Evko展示了怎么通过另一个叫做Jasmine的框架来设置良好的测试流程。这两个测试框架都是非常流行的,但还有适应别的需求的其他框架。

我在这篇文章前面撰写的大纲,已经讲述了它期待怎样的输出。这是一切测试的开始:从期望出发。关于我的代码库的一个Jasmine测试像是这样:

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
describe('Basic usage', function () {
it('should generate a single product', function () {
// Create a single product
var product = new UserAgent.Product('EvilCorpBrowser', '1.2');
product.setComment('X11', 'Linux', 'en-us');
expect(product.toString())
.toBe('EvilCorpBrowser/1.2 (X11; Linux; en-us)');
});
it('should combine several products', function () {
var userAgent = new UserAgent;
// Create and add first product
var application = new UserAgent.Product('EvilCorpBrowser', '1.2');
application.setComment('X11', 'Linux', 'en-us');
userAgent.addProduct(application);
// Create and add second product
var engine = new UserAgent.Product('Blink', '20420101');
userAgent.addProduct(engine);
expect(userAgent.toString())
.toBe('EvilCorpBrowser/1.2 (X11; Linux; en-us) Blink/20420101');
});
it('should update products correctly', function () {
var userAgent = new UserAgent;
// Create and add first product
var application = new UserAgent.Product('EvilCorpBrowser', '1.2');
application.setComment('X11', 'Linux', 'en-us');
userAgent.addProduct(application);
// Update first product
application.setComment('X11', 'Linux', 'nl-nl');
expect(userAgent.toString())
.toBe('EvilCorpBrowser/1.2 (X11; Linux; nl-nl)');
});
});

一旦你对你的API设计的第一个版本完全满意,是时候开始思考结构和你的代码库应该如何被使用。

##模块加载器兼容性

你或许使用过模块加载器。使用你的代码库的开发者有可能使用加载器,所以你会希望自己的代码库与模块加载器是兼容的。但兼容哪一个呢?应该怎么从CommonJS,RequireJS,AMD和其他加载器中挑选呢?

实际上,你不需要挑选!通用模块定义(UMD)是一个目标就是支持多种加载器的规则。你可以在网上找到不同风格的代码段,或者从这里 UMD GitHub repository学习并让它与你的代码库兼容。从其中的某个模板开始,或者使用你喜欢的构建工具add UMD with your favorite build tool,你就不必再担心模块加载器的问题了。

如果你希望用上ES2015的 import/export 语法,我建议使用Babel和Babel’s UMD plugin来将代码转换成ES5。通过这个方法你可以在你的项目中使用ES2015,同时生成兼容性良好的代码库。

##文档

我完全赞成在每一个项目中使用文档。但这通常牵涉到大量的工作,导致编写文档被推迟和在最后被遗忘。

###基本信息

文档的编写应当从项目的名字和描述之类基本的信息开始。这会对别人明白你的代码库做了什么和是否对他们有用有所帮助。

你可以提供像是适用范围和目标之类的信息来更好的给用户提供信息,而提供路线图可以让他们明白在未来可能会有什么新变化以及他们可以提供怎样的帮助。

###API,教程和例子

当然,你需要确保用户知道如何去使用你的代码库。这从API文档开始。教程和例子是很好的补充,但编写他们会是一个庞大的工作。然而,内联文档Inline documentation不会这样。下面是一些利用JSDoc的,可以被解析和转换成文档页的注释

###元任务

一些用户想对你的代码库作出改进。在大多数情况中,会是贡献代码,但有一些会创建一个私人使用的定制版本。对于这些用户,为类似构建代码库,运行测试,生成,转换和下载数据之类的元任务提供文档很有帮助的。

###贡献

当你对你的代码库进行开源,得到代码贡献是很有帮助的。为了引导贡献者,你可以增加一些关于贡献代码的步骤和需要满足的标准的文档。这会对你审阅和接纳贡献的代码和他们正确的贡献代码有所帮助。

###授权许可

最后一点,使用授权许可。从技术上讲,即时你没有选择任何一种技术许可,你的代码库依然是拥有版权的,但不是每一个人都知道这一点。

我发现 ChooseALicense.com这个网站可以让你一个不需要成为法律专家也能挑选授权许可。在挑选授权许可之后,只需要在项目的根目录下添加 LICENSE.txt 文件。

##将它打包和发布到包管理器

对一个好的代码库来说,版本是很重要的。如果你想要加入重大的变化,用户可能需要保留他们现在正在使用的版本。

Semantic Versioning是流行的版本命名标准,或者叫它SemVer。SemVer版本包括三个数字,每一个代表不同程度的改变:重大改变,微小的改变和补丁

###在你的Git仓库加入版本和发布

如果你有一个git仓库,你可以在你的仓库添加版本数字。你可以把它想象成你的仓库的快照。我们也叫它标签 Tags。可以通过开启终端和输入下面的文字来创造标签:

1
2
# git tag -a [version] -m [version message]
git tag -a v1.2.0 -m "Awesome Library v1.2.0"

很多类似GitHub的服务会提供关于所有版本的概览和提供它们的下载链接。

###发布一个通用仓库

####npm

许多编程语言自带有包管理器,或者是第三方包管理器。这可以允许我们下载关于这些语言的特定代码库。比如PHP的Composer 和Ruby的RubyGems

Node.js,一种独立的JavaScript引擎,拥有 npm,如果你对npm不熟悉,我们有一个很好的教程 beginner’s guide

默认情况下,你的npm包会发布为公共包。不要害怕,你也可以发布私有包 private packages, 设置一个私有的注册private registry, 或者根本不发布avoid publishing.

为了发布你的包,你的项目需要有一个 package.json 文件。你可以手动或者交互问答的方式来创建。通过输入下面的代码来开始问答:

1
`npm init`

这个版本属性需要跟你的git标签吻合。另外,请确定有README.md 文件。像是GitHub,npm在你的包的展示页使用它。

之后,你可以通过输入下面的代码来发布你的包:

1
`npm publish`

就是这样!你已经成功发布了你的npm包。

####Bower

几年前,有另一个叫做Bower的包管理器。这个包管理器,实际上不是为了特定的语言准备的,而是为了互联网准备的。你可以发现大部分是前端资源。在Bower发布你的包的关键一点是你的代码库是否跟它兼容。

如果你对Bower不熟悉,我们也有一个教程beginner’s guide

跟npm一样,你也可以设置一个私有仓库private repository。你可以通过问答的方式避免发布。

有趣的是,在最近的一两年,很多人转为使用npm管理前端资源。近段npm包主要是跟JavaScript相关,大部分的前端资源也发布在了npm上。不管怎样,Bower仍然流行。我明确的推荐你在Bower上发布你的包。

我有提到Bower实际上是npm的一种模块,并最初是得到它的启发吗?它们的命令是很相似的,通过输入下面的代码产生bower.json文件:

1
`bower init`

npm init类似,指令是很直白的,最后,发布你的包:

1
`bower register awesomelib https://github.com/you/awesomelib`

像是把你的代码库放到了野外,任何人可以在他们的Node项目或者网络上使用它!

##总结

核心的产品是库文件。确定它解决了问题,容易和适合使用,你会使得你的团队或者许多开发者变得高兴。

我提到的很多任务都是自动化的,比如:运行测试,创建标签,在package.json升级版本或者在npm或者bower重发布你的包。 这是你像Travis CI 或 Jenkins一样踏入持续集成和使用工具的开始。我之前提到的文章 article by Tim Evko 也讲述到了这点。

你构建和发布代码库了吗?请在下面的评论区分享!

支持作者

如果我的文章对你有帮助,欢迎 关注和 star 本博客 或是关注我的 github,获取更新通知。欢迎发送邮件到hpoenixf@foxmail.com与作者交流