# 插件模块化
Node.js (opens new window)、Webpack (opens new window) 的问世彻底解决了该问题,让前端走向新时代。其实 require.js (opens new window) 等也可以做到模块化。但不完美,所以他不是本次的主角。
准备
使用 Webpack 需要用到 Node.js,请确保安装了 Node.js。NPM 为 Node.js 包管理工具。
# 改造
首先进行项目初始化 npm init
,使用命令 npm install webpack --save-dev
安装 Webpack。
# 项目初始化
npm init
# 按照 Webpack
npm install webpack --save-dev
# 如果通过 cli 运行 Webpack,你还需要安装
npm install webpack-cli --save-dev
2
3
4
5
6
先将 library.js
进行模块化,拆分成 util.js
、browser.js
、element.js
、index.js
。
// src/util.js
// 随机数
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
// 加载图片
function loadImg(src, callback) {
var img = document.createElement("img");
img.src = src;
img.onload = function() {
callback && callback(img);
};
}
module.exports.getRandomInt = getRandomInt;
module.exports.loadImg = loadImg;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/browser.js
var inBrowser = typeof window !== "undefined";
var UA = inBrowser && window.navigator.userAgent.toLowerCase();
var isIE = UA && /msie|trident/.test(UA);
var isEdge = UA && UA.indexOf("edg/") > 0;
var isChrome = UA && UA.indexOf("chrome") > 0 && !isEdge;
function browser() {
return ["ie: " + isIE, "edge: " + isEdge, "chrome: " + isChrome].join(", ");
}
module.exports.browser = browser;
2
3
4
5
6
7
8
9
10
11
12
// src/element.js
var util = require("./util");
// 使用 background 显示图片
function createBackgroudImg(src, callback) {
util.loadImg(src, function(img) {
var div = document.createElement("div");
div.style.width = img.width + "px";
div.style.height = img.height + "px";
div.style.background = "url(" + src + ")";
callback && callback(div);
});
}
// 创建一个有随机数的节点
function createRandomTextElement() {
var div = document.createElement("div");
div.innerText = "random: " + util.getRandomInt(1000, 9999);
return div;
}
module.exports.createBackgroudImg = createBackgroudImg;
module.exports.createRandomTextElement = createRandomTextElement;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/index.js
var element = require("./element");
(function(window) {
var library = {
browser: require("./browser").browser,
getRandomInt: require("./util").getRandomInt,
createBackgroudImg: element.createBackgroudImg,
createRandomTextElement: element.createRandomTextElement,
};
window.library = library;
})(window);
2
3
4
5
6
7
8
9
10
11
12
13
根据官方文档中的使用规则,如果我们使用 cli 进行构建,就需要创建一个 webpack.config.js
配置文件。但本节为了加深对 Node.js、Webpack 的理解,采用创建 JS 文件用 Node.js 去运行的方式。Webpack Node.js API (opens new window)
创建一个 build.js
文件:
// build.js
var path = require("path");
var webpack = require("webpack");
webpack(
{
mode: "none", // 方便查看产物的代码,可以临时设置 none
// 入口
entry: "./src/index.js",
// 输出
output: {
filename: "library.js",
path: path.resolve(__dirname, "dist"),
},
},
(err, stats) => {
if (err || stats.hasErrors()) {
// 这里处理错误
console.log(err, stats);
}
// 处理完成
}
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
提示
为了方便查看产物的代码,可以临时设置 mode: "none"
。
运行 node build.js
,文件根目录就多出了一个 dist
文件夹。将 旧时代插件 章节中创建的 index.html
里的库引用切换成 dist/library.js
。然后在之前的浏览器试着运行。
<!-- <script src="./library.js"></script> -->
<script src="./dist/library.js"></script>
2
结果:Chrome 正常,IE 失败。这是为什么呢?
尝试
webpack 默认打包模式是 production
,可以切换成node
、development
。看看不同配置是否会带来什么样的产物。
# 切换构建目标
打开 dist/library.js
查看代码。发现代码中使用了 ES6 的箭头函数,导致 IE 无法运行。之所以使用箭头函数是 Webpack 构建目标配置默认是 web
即 es6
,我们需要设置为 es5
。文档说明 (opens new window)。
// build.js
webpack(
{
entry: ...
output: ...
+ target: ["web", "es5"],
},
);
2
3
4
5
6
7
8
运行 node build.js
,打开 index.html
, IE >= 9 浏览器正常访问。
# 更改输出方式
上面绑定库的方式,还是通过闭包给 window
添加属性实现的。Webpack 提供了 创建库 (opens new window) 的功能,而不用人工绑定了。根据文档修改配置:
// build.js
webpack(
{
entry: ...,
output: {
...
+ library: {
+ name: "library",
+ type: "umd",
+ },
},
target: ...,
},
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
提示
为了篇幅简洁、突出重点,后续章节中的未修改、非重点的代码将会省去。这些代码都是基于已有代码进行一点一点修改的,建议学习过程中在本地进行同步。
同时修改 src/index.js
原本的绑定方式:
// src/index.js
var element = require("./element");
module.exports = {
browser: require("./browser").browser,
getRandomInt: require("./util").getRandomInt,
createBackgroudImg: element.createBackgroudImg,
createRandomTextElement: element.createRandomTextElement,
};
2
3
4
5
6
7
8
9
运行 node build.js
,打开 index.html
,IE >= 9 浏览器正常访问。
# Tree Shaking
思考: 如果我们在库里存在一些无用代码,此时 Webpack 会自动移除吗?
尝试: 假设 src/util.js
写了很多辅助函数,但项目中只引用到其中几个。如下:
// src/util.js
// ...
function notUse() {
console.log("全局都没有引用的代码块");
}
// ...
module.exports.notUse = notUse;
2
3
4
5
6
7
8
// build.js
// ...
webpack(
{
// 需要设置 production,自动开启代码优化功能
// 或设置以下 optimization 手动开启代码优化功能
mode: "production",
// ...
// optimization: {
// usedExports: true,
// minimize: true,
// concatenateModules: true,
// },
}
// ...
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
运行 node build.js
,查看产物 dist/library.js
,发现有 全局都没有引用的代码块
文本。
思考: 如何让 Webpack 智能的删除这些无用代码,让库缩小体积呢?
因为 require()
只能在运行时才能知道引用情况,而 ES6 的 import
、export
具有静态结构特征,工具在编译时就可以知道引用情况。庆幸的是 Webpack >= 2 支持 ES6 的模块化语法。Tree Shaking (opens new window)
注意
Webpack 仅支持 ES6 的模块化,其他 ES6 语法和 API 等是不支持的。
警告
尽量避免使用 import
来导入 export default
的对象值,一般情况下并不会 Tree Shaking。可以尝试以下例子的三种情况:
// tree-shaking-example/a.js
function a1() {
console.log("a1");
}
function a2() {
// 不引用
console.log("not_use_a2");
}
export default { a1, a2 };
2
3
4
5
6
7
8
9
10
11
// tree-shaking-example/b.js
// 打包该文件
import a from "./a";
// 情况一:不打包 a2
const e = { a1: a.a1 };
console.log(e);
// 情况二:打包 a2
// (function () {
// const e = { a1: a.a1 };
// console.log(e);
// })();
// 情况三:打包 a2
// console.log({ a1: a.a1 });
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
接下来将模块化方式从 Commonjs 改为 ES6:
// src/util.js
// 随机数
export function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
// 加载图片
export function loadImg(src, callback) {
var img = document.createElement("img");
img.src = src;
img.onload = function() {
callback && callback(img);
};
}
export function notUse() {
console.log("全局都没有引用的代码块");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/browser.js
var inBrowser = typeof window !== "undefined";
var UA = inBrowser && window.navigator.userAgent.toLowerCase();
var isIE = UA && /msie|trident/.test(UA);
var isEdge = UA && UA.indexOf("edg/") > 0;
var isChrome = UA && UA.indexOf("chrome") > 0 && !isEdge;
export function browser() {
return ["ie: " + isIE, "edge: " + isEdge, "chrome: " + isChrome].join(", ");
}
2
3
4
5
6
7
8
9
10
// src/element.js
import * as util from "./util";
// 使用 background 显示图片
export function createBackgroudImg(src, callback) {
util.loadImg(src, function(img) {
var div = document.createElement("div");
div.style.width = img.width + "px";
div.style.height = img.height + "px";
div.style.background = "url(" + src + ")";
callback && callback(div);
});
}
// 创建一个有随机数的节点
export function createRandomTextElement() {
var div = document.createElement("div");
div.innerText = "random: " + util.getRandomInt(1000, 9999);
return div;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/index.js
import { browser } from "./browser";
import { getRandomInt } from "./util";
import { createBackgroudImg, createRandomTextElement } from "./element";
export { browser, getRandomInt, createBackgroudImg, createRandomTextElement };
// 或以下
// export { browser } from "./browser";
// export { getRandomInt } from "./util";
// export { createBackgroudImg, createRandomTextElement } from "./element";
2
3
4
5
6
7
8
9
10
11
12
运行 node build.js
,打开 index.html
,浏览器正常运行,查看 dist/library.js
里面也没有 全局未引用的代码块
。
注意
如果你使用 Commonjs 模块化并在 package.json
文件中开启 sideEffects: false
,让 Webpack 做进一步的 Tree Shaking 时,那么你需要确保你的代码无副产物。尤其是在 CSS 样式引入时,经常会发生编译后样式无效果的情况。sideEffects (opens new window)