使用 Webpack 打包 JavaScript 模块

上一篇粗略介绍了 Webpack 的基本用法,这里来做个实践和总结,实现一个小工具库,主要涉及 Webpack 基本用法和 JavaScript 模块化思想。

模块化

一、什么是模块?

Good authors divide their books into chapters and sections; good programmers divide their programs into modules.

好的作者会把书分为章和节,好的程序员会把代码分为模块。

二、模块化的好处

1)可维护性。

2)命名空间。

3)重用代码。

三、模块化的历史

最初,JavaScript 没有模块的概念,随着前端的快速发展,旧的模式的缺点日益凸显。JavaScript 模块化历程从无模块时代 -> 模块萌芽时代 -> 模块化标准涌现时代 -> 统一标准时代

无模块时代

1
2
3
4
5
6
7
8
if (x) {
// do something;
} else {
// else;
}
element.onclick = function() {
// ...
}

嵌入网页的 JavaScript 代码越来越庞大,越来越复杂,无法满足开发需求。开发者不得不使用软件工程的方法,管理网页的业务逻辑。

模块萌芽时代
2005 年,Ajax 概念的提出,Google 在 Gmail 等应用中使用了异步通讯,传统的网页慢慢的向“富客户端”发展。前端的业务逻辑越来越多,代码也越来越多,于是一些问题就暴漏了出来:

  1. 全局变量的灾难
  2. 函数命名冲突
  3. 依赖关系不好管理

为解决这些问题,这些解决方案被提出:

  1. 用立即执行函数来包装代码
  2. Java 风格的命名空间
  3. jQuery 风格的匿名立即执行函数

这一时期出现的模块化方案:

直接定义依赖 (1999): 由于当时 js 文件非常简单,模块化方式非常简单粗暴 —— 通过全局方法定义、引用模块。这种定义方式与现在的 CommonJS 非常神似,区别是 CommonJS 以文件作为模块,而这种方法可以在任何文件中定义模块,模块不与文件关联。

闭包模块化模式 (2003): 用闭包方式解决了变量污染问题,闭包内返回模块对象,只需对外暴露一个全局变量。

模块标准涌现时代
大部分的模块化标准在这一时期相继出现,不同程度上受到后端和其他语言的影响。

模版依赖定义 (2006): 这时候开始流行后端模版语法,通过后端语法聚合 js 文件,从而实现依赖加载,说实话,现在 go 语言等模版语法也很流行这种方式,写后端代码的时候不觉得,回头看看,还是挂在可维护性上。

注释依赖定义 (2006): 几乎和模版依赖定义同时出现,与 1999 年方案不同的,不仅仅是模块定义方式,而是终于以文件为单位定义模块了,通过 lazyjs 加载文件,同时读取文件注释,继续递归加载剩下的文件。

外部依赖定义 (2007): 这种定义方式在 cocos2d-js 开发中普遍使用,其核心思想是将依赖抽出单独文件定义,这种方式不利于项目管理,毕竟依赖抽到代码之外,我是不是得两头找呢?所以才有通过 Webwpack 打包为一个文件的方式暴力替换为 CommonJS 的方式出现。

Sandbox 模式 (2009): 这种模块化方式很简单,暴力,将所有模块塞到一个 sanbox 变量中,硬伤是无法解决明明冲突问题,毕竟都塞到一个 sandbox 对象里,而 Sandbox 对象也需要定义在全局,存在被覆盖的风险。模块化需要保证全局变量尽量干净,目前为止的模块化方案都没有很好的做到这一点。

依赖注入 (2009): 就是大家熟知的 Angular1.0,依赖注入的思想现在已广泛运用在 React、Vue 等流行框架中。但依赖注入和解决模块化问题还差得远。

CommonJS (2009): 真正解决模块化问题,从 node 后端逐渐发力到前端,前端需要使用构建工具模拟。

Amd (2009): 都是同一时期的产物,这个方案主要解决前端动态加载依赖,相比 CommonJS,体积更小,按需加载。

Umd (2011): 兼容了 CommonJS 与 Amd,其核心思想是,如果在 CommonJS 环境(存在 module.exports,不存在 define),将函数执行结果交给 module.exports 实现 CommonJS,否则用 Amd 环境的 define,实现 Amd。

Labeled Modules (2012): 和 CommonJS 很像了,没什么硬伤,但生不逢时,碰上 CommonJS 与 Amd,那只有被人遗忘的份了。

YModules (2013): 既然都出了 CommonJS 和 Amd,文章还列出了此方案,一定有其独到之处。其核心思想在于使用 provide 取代 return,可以控制模块结束时机,处理异步结果;拿到第二个参数 module,修改其他模块的定义(虽然很有拓展性,但用在项目里是个搅屎棍)。

统一标准时代

ES2015 Modules (2015): 就是我们现在的模块化方案,还没有被浏览器实现,大部分项目已通过 Babel 或 TypeScript 提前体验。

虽然 ECMA 推出了官方的模块化标准,这一标准还未成熟,统一的 JavaScript 模块化标准时代还需要时日。

使用 Webpack 打包模块化文件

说了这么多,接下来我们就来实现一个小工具库,这个小工具库有几个模块组成,最终由 Webpack 打包成一个文件。

开始

安装

我们先使用 npm init 新建一个工程,并根据提示输入相应的信息。

1
npm init

简单来说:npm init 是一个创建(或修改)一个项目中的 package.json 文件的工具。
按照提示输入完成,它看起来是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"name": "javascript-module",
"version": "0.0.0",
"description": "A demo project for JavaScript modules",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"javascript",
"module"
],
"author": "leamtrop",
"license": "MIT"
}

接下来,安装需要的依赖包:

1
2
3
4
npm i -D webpack
npm i -D babel-core
npm i -D babel-loader
npm i -D babel-preset-es2015

我们需要用 Babel 将 ES6 转译为 ES5 的语法。新建模块源代码文件及相关文件夹,最终的文件结构变成以下的形式:

1
2
3
4
5
6
7
8
9
10
./
├── dist
├── node_modules
├── package.json
├── package-lock.json
├── src
│ ├── index.js
│ ├── storage.js
│ └── utils.js
└── webpack.config.js

模块

src/storage.jssrc/utils.js 分别是两个模块,我们把模块入口定义在 src/index.js,采用 ES6 Modules 来定义模块。

1
2
3
4
5
// src/indes.js
import { utils } from './utils';
import { storage } from './storage';

export { utils, storage }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/utils.js
class utils {
constructor(options) {
this.name = options.name;
}

someFunction(args) {
console.log(this.name);
console.log('someFunction');
return args;
}

otherFunction(args = {}) {
return args;
}
}

export { utils };

最终的文件都放在 GitHub 上。

打包

webpack.config.js

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
var path = require('path');

var config = {
defaultPath: '/dist',
path: {
src: '/src',
dist: '/dist'
}
};

module.exports = {
entry: {
MyLib: [__dirname + config.path.src]
},

output: {
publicPath: config.defaultPath,
path: path.join(__dirname, config.path.dist),
filename: '[name].js',
libraryTarget: 'umd',
// `library` 声明全局变量
library: '[name]'
},

module: {
loaders: [
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /(node_modules)/,
query: {
presets: ['es2015']
}
}
]
},
};

打包单个入口文件,将 ES6 转译为 ES5 代码,最终打包成 UMD 模块,暴露全局变量名 MyLib

调用

1
2
3
MyLib.storage.set({ key: "value" });
MyLib.storage.get('key');
...

到这里,我们已经用 Webpack 成功打包 JavaScript 模块组件,更多的打包功能可参考 Webpack 官方文档

总的来说,JavaScript 模块化方案越来越成熟,给开发者带来了很大的便利,加上强大的工具,使得 JavaScript 生态更加繁荣。

参考链接:

JavaScript Modules: A Beginner’s Guide

JavaScript 模块的前世今生

history-of-javascript

(完)