# 插件模块化

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
1
2
3
4
5
6

先将 library.js 进行模块化,拆分成 util.jsbrowser.jselement.jsindex.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;
1
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;
1
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;
1
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);
1
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);
    }
    // 处理完成
  }
);
1
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>
1
2

结果:Chrome 正常,IE 失败。这是为什么呢?

尝试

webpack 默认打包模式是 production,可以切换成nodedevelopment。看看不同配置是否会带来什么样的产物。

# 切换构建目标

打开 dist/library.js 查看代码。发现代码中使用了 ES6 的箭头函数,导致 IE 无法运行。之所以使用箭头函数是 Webpack 构建目标配置默认是 webes6,我们需要设置为 es5文档说明 (opens new window)

// build.js
webpack(
  {
    entry: ...
    output: ...
+   target: ["web", "es5"],
  },
);
1
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: ...,
  },
);

1
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,
};
1
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;
1
2
3
4
5
6
7
8
// build.js
// ...

webpack(
  {
    // 需要设置 production,自动开启代码优化功能
    // 或设置以下 optimization 手动开启代码优化功能
    mode: "production",
    // ...
    // optimization: {
    //   usedExports: true,
    //   minimize: true,
    //   concatenateModules: true,
    // },
  }
  // ...
);
1
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 的 importexport 具有静态结构特征,工具在编译时就可以知道引用情况。庆幸的是 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 };
1
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 });
1
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("全局都没有引用的代码块");
}
1
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(", ");
}
1
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;
}
1
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";
1
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)