两个月前,我曾发布了一篇基于 webpack 的 React 起步教程。你眼前的这篇文章跟那一篇差不多,只不过不包含 React 那一块。这篇教程稍微简单一些,但仍然会有一些棘手的部分。因此,我特意建了一个全新的代码仓库 webpack-library-starter,把创建一个 JavaScript 类库所需的所有素材都放了进去。
首先,我们说的 “类库” 是指什么
在 JavaScript 语境中,我对类库的定义是 “提供了特定功能的一段代段”。一个类库只做一件事,并且把这件事做好。在理想情况下,它不依赖其它类库或框架。jQuery 就是一个很好的例子。React 或者 Vue.js 也可以认为是一个类库。
一个类库应该:
用什么来开发这个类库并不重要,重要的是我们最终产出的文件。它只要满足上述要求就行。尽管如此,我还是比较喜欢用原生 JavaScript 写成的类库,因为这样更方便其它人贡献代码。
目录结构
我一般选择如下的目录结构:
1 2 3 4 5 6 |
+-- lib | +-- library.js | +-- library.min.js +-- src | +-- index.js +-- test |
其中 src
目录用于存放源码文件,而 lib
目录用于存放最终编译的结果。这意味着类库的入口文件应该放在 lib
目录下,而不是 src
目录下。
起步动作
我确实很喜欢最新的 ES6 规范。但坏消息是它身上绑了一堆的附加工序。也许将来某一天我们可以摆脱转译过程,所写即所得;但现在还不行。通常我们需要用到 Babel 来完成转译这件事。Babel 可以把我们的 ES6 文件转换为 ES5 格式,但它并不打算处理打包事宜。或者换句话说,如果我们有以下文件:
1 2 3 4 |
+-- lib +-- src +-- index.js (es6) +-- helpers.js (es6) |
然后我们用上 Babel,那我们将会得到:
1 2 3 4 5 6 |
+-- lib | +-- index.js (es5) | +-- helpers.js (es5) +-- src +-- index.js (es6) +-- helpers.js (es6) |
或者再换句话说,Babel 并不解析代码中的 import
或 require
指令。因此,我们需要一个打包工具,而你应该已经猜到了,我的选择正是 webpack。最终我想达到的效果是这样的:
1 2 3 4 5 6 |
+-- lib | +-- library.js (es5) | +-- library.min.js (es5) +-- src +-- index.js (es6) +-- helpers.js (es6) |
npm 命令
在运行任务方面,npm 提供了一套不错的机制——scripts(脚本)。我们至少需要注册以下三个脚本:
1 2 3 4 5 |
"scripts": { "build": "...", "dev": "...", "test": "..." } |
npm run build
– 这个脚本用来生成这个类库的最终压缩版文件。npm run dev
– 跟build
类似,但它并不压缩代码;此外还需要启动一个监视进程。npm run test
– 用来运行测试。
构建开发版本
npm run dev
需要调用 webpack 并生成 lib/library.js
文件。我们从 webpack 的配置文件开始着手:
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 |
// webpack.config.js var webpack = require('webpack'); var path = require('path'); var libraryName = 'library'; var outputFile = libraryName + '.js'; var config = { entry: __dirname + '/src/index.js', devtool: 'source-map', output: { path: __dirname + '/lib', filename: outputFile, library: libraryName, libraryTarget: 'umd', umdNamedDefine: true }, module: { loaders: [ { test: /(\.jsx|\.js)$/, loader: 'babel', exclude: /(node_modules|bower_components)/ }, { test: /(\.jsx|\.js)$/, loader: "eslint-loader", exclude: /node_modules/ } ] }, resolve: { root: path.resolve('./src'), extensions: ['', '.js'] } }; module.exports = config; |
即使你还没有使用 webpack 的经验,你或许也可以看明白这个配置文件做了些什么。我们定义了这个编译过程的输入(entry
)和输出(output
)。那个 module
属性指定了每个文件在处理过程中将被哪些模块处理。在我们的这个例子中,需要用到 Babel 和 ESLint,其中 ESLint 用来校验代码的语法和正确性。
这里有一个坑,花了我不少的时间。这个坑是关于 library
、libraryTarget
和 umdNamedDefine
属性的。最开始我没有把它们写到配置中,结果编译结果就成了下面这个样子:
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 |
(function(modules) { var installedModules = {}; function __webpack_require__(moduleId) { if(installedModules[moduleId]) return installedModules[moduleId].exports; var module = installedModules[moduleId] = { exports: {}, id: moduleId, loaded: false }; modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); module.loaded = true; return module.exports; } __webpack_require__.m = modules; __webpack_require__.c = installedModules; __webpack_require__.p = ""; return __webpack_require__(0); })([ function(module, exports) { // ... my code here } ]); |
经过 webpack 编译之后的文件差不多都是这个样子。它采用的方式跟 Browserify 很类似。编译结果是一个自调用的函数,它会接收应用程序中所用到的所有模块。每个模块都被存放到到 modules
数组中。上面这段代码只包含了一个模块,而 __webpack_require__(0)
实际上相当于运行 src/index.js
文件中的代码。
光是得到这样一个打包文件,并没有满足我们在文章开头所提到的所有需求,因为我们还没有导出任何东西。这个文件的运行结果在网页中必定会被丢弃。不过,如果我们加上 library
、libraryTarget
和umdNamedDefine
,就可以让 webpack 在文件顶部注入一小段非常漂亮的代码片断:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
(function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(); else if(typeof define === 'function' && define.amd) define("library", [], factory); |