开发一个文件下载的node模块

node 模块也就是 node 的包,常用来被其他 node 程序调用来完成一些功能。 例如 shelljs 这个模块,可以用来实现 shell 命令的执行,例如 request 这个模块可以帮助你进行 http 请求的处理,比如 cheerio 这个包可以用来实现非浏览器环境的 jquery 风格 dom 查询。

今天,我打算开发一款 node 模块,它提供了一个 API,可以帮助别人进行文件下载并提供下载进度信息。最终代码在这里: https://github.com/cuiyongjian/fetch-file

脚手架类型

我们开发 js 项目,目前来看一般有 3 种类型:

  1. 纯前端的库

  2. 前后端都可用的库,比如一些算法相关的

  3. 纯后端的库,比如用到了 node 特有的 API 的库

第一种和第二种,其实可以共用前端脚手架。因为一个基于 webpack 工作流的前端库,其本身就能兼容 node 环境调用的,因为 node 环境使用一个库时跟前端 webpack 工作流使用一个包时的引用方式是一致的,只需要找到该包的入口即可。所以如果是算法类型的模块,这种模块跟平台无关,node 和浏览器均可以使用。则你应该利用 felib-template 去初始化一个前端模板。 在 node 环境下只需要引用包中的 main 入口文件,在前端工作流中使用时也是引用 src 下的入口文件即可,当然前端也可以直接引用编译后的 dist 下的目标文件。

前端库的另一种使用方法是在浏览器端使用<script>标签的话,这种直接引用 webpack 编译后的 dist 目录下的.min.js 文件即可。其实大部分情况下是引用 CDN 上的 min.js 文件了。

前端库中应该把 main 文件设置为谁呢?我们一般在包中直接将 main 文件设置为 dist 下的 UMD 类型的编译结果就可以了。或者你可以像 Vue.js 一样编译一个 dist/common.js 的版本出来,作为包的 main 文件。 因为当这个包被 npm 的方式使用(不管前端还是 node 后端)时,必然是为了支持 commonjs 的环境(包括 webpack 工作流的前端开发或者 node 后端开发)。

所以,开发前端库的包中如何设置 main,我们只考虑目标是 commonjs 的环境即可。

纯后端库的 ES6 脚手架

但如果我们仅仅是开发一个 node 模块,这个模块使用了 FS 等模块,它注定不是应用在浏览器端的。此时如果使用 felib-tempalte 前端库的脚手架,显然 webpack 这些是多余的,也没必要编译到 dist 中 UMD 的版本(后端支持 commonjs 即可)。所以纯 node 的模块其实更简单,只需要项目中有个 package.json 就足够了。

不过后端现在为了跟上潮流,也要使用 ES6,那么这里就要存在于一个转码的过程。这里,我创建了一个 nodelib-tempalte 的脚手架类型,可以用来开发纯 node 端的模块。且使用了 babel 进行 ES6 转码,且入口中加载 babel-polyfill 全局 cover 了 ES6 的 API。所以可以尽情的使用 ES6 语法。

脚手架介绍

项目结构如下:

1
2
src;
lib;

babel 配置

关于 babel 的知识点,可以参考从零开始搭建一个 webpack 前端类库脚手架[3]-babel

该脚手架中使用 babel-cli 进行了 ES6 代码的编译。从而可以使用 ES6 编写代码,编译为 ES5 之后再发布和运行。

polyfill

关于 polyfill 有几种方案,在 babel 这篇文章中我也分析过了。 其中 babel-runtime 无法 cover 代码中类似 [1,2,3].from 这样的。而 babel-polyfill 全部引入可能会比较臃肿。 最好是能够根据目标平台的版本,来利用 preset-env 配合 useBuiltIns 来设置。

不过由于这是个 node 项目,所以不需要介意最终代码的臃肿,所以我暂且直接使用 preset-es2015 以及最新标准进行编译, 且引入了完整 polyfill。不过这样其实不是很好的,因为它默认了要把所有代码都编译为 es2015, 且 node 代码运行前就要加载一个 polyfill 完整版,这样肯定对于第一次启动 node 模块时有性能损失。由于性能问题不严重,我就暂且采用这种方法了,有兴趣的读者可以将 preset-es2015 这个预设修改为 env 并配合 useBuiltIns 来按需加载 polyfill 是极好的。

下面是我的.babelrc 配置:

1
2
3
{
"presets": ["es2015", "stage-0"]
}

当然,别忘记 npm i babel-preset-es2015 babel-preset-stage-0 --save-dev 安装他们。stage-0 表示最新标准的最晚提案,所以其特性已经囊括了 stage1, stage2, stage3. 然而我们会发现,其实我的配置就是要使用最新的 ES 特性,倒不如直接使用 babel 中的 latest 预设,而现在的 babel 已经飞起 latest,因为 preset-env 的默认情况下就是 latest的效果。

所以,最终,我把 babelrc 配置为这样:

1
2
3
{
"presets": ["env"]
}

在根目录下的 index.js 中,使用 require('babel-polyfill') 加载 ES 新标准的语言 API 垫片。

package.json 配置

1
2
{
}

实战开始

基于 lime-cli 生成一个 nodelib-template 类型的项目骨架,然后再开始。

首先请确保你安装了我写的 lime-cli 脚手架生成工具。如果没有,请执行:

1
npm i -g lime-cli

然后安装nodelib-template这个脚手架。

1
lime new nodelib-template

安装完成之后,再执行 npm i 来安装其各种依赖和开发依赖。

1
npm install

测试

我们使用抹茶 mocha 进行测试代码。

为了让测试代码可以基于 ES6 编写,我们可以利用 mocha 的 compiler 特性,给 mocha 执行测试代码的时候加入一个 babel 预编译器。
通过babel 官网教程 我们知道,需要使用 babel-register 作为 mocha 的 compiler。

1
2
npm install --save-dev mocha
npm install --save-dev babel-register

配置 mocha 脚本:

1
2
3
4
5
{
"scripts": {
"test": "mocha --compilers js:babel-register"
}
}

我们都知道 babel-register 的作用,其实 mocha 也是用它来提供了加载 js 代码(require(xxx))时候的 ES6 支持。

由于你可能在测试代码中用来 ES6 的一些 API 特性,所以可能会用到 polyfill,故更完整的应该这样:

1
2
3
4
5
{
"scripts": {
"test": "mocha --require babel-polyfill --compilers js:babel-register"
}
}

由于我们需要测试 lib 下的编译后文件,而不是测试源码,所以测试命令还需要修改下让他预先执行编译:

1
2
3
4
5
{
"scripts": {
"test": "npm run build && mocha --compilers js:babel-core/register"
}
}

最新版本已经替换为 babel-register 这个单独的库,且废弃了使用–compiler,转而直接全部使用 --require, 且要设置 glob 类型的测试目录和文件名咯。所以最终是这样:

1
2
3
4
5
"scripts": {
"build": "babel -d lib src",
"test": "npm run build && mocha --require babel-polyfill --require babel-register \"test/*.js\"",
"prepublish": "npm run build"
},

直接执行 mocha,他默认就会测试当前目录下 test 下的 js 文件,不会递归往下查找,只测试 test 下的第一层。递归的话要用 mocha —recursive. 或者按照官方建议使用 blob: test/**/*.js

发布

源码管理时,lib 目录不需要管理。发布 npm 包时,src 目录不需要发布。所以我们需要使用 .gitignore.npmignore 来完成这件事。

.gitignore 的设置如下:

1
2
3
*.log
lib
node_modules

.npmignore 的设置如下:

1
src

另外,我们在往 npm 仓库 publish 包时,可以基于 npm 脚本特性实现发布时自动编译:

1
2
3
4
5
{
"scripts": {
"prepublish": "npm run build"
}
}

Refer

https://cnodejs.org/topic/565c65c4b31692e827fdd00c