这篇文章主要介绍了深入理解 webpack 文件打包机制(小结),现在分享给大家,也给大家做个参考。

前言

最近在重拾 webpack 一些知识点,希望对前端模块化有更多的理解,以前对 webpack 打包机制有所好奇,没有理解深入,浅尝则止,最近通过对 webpack 打包后的文件进行查阅,对其如何打包 JS 文件有了更深的理解,希望通过这篇文章,能够帮助读者你理解:

webpack 单文件如何进行打包?

webpack 多文件如何进行代码切割?

webpack1 和 webpack2 在文件打包上有什么区别?

webpack2 如何做到 tree shaking?

webpack3 如何做到 scope hoisting?

本文所有示例代码全部放在我的 Github 上,看兴趣的可以看看:

git clone https://github.com/happylindz/blog.git cd blog/code/webpackBundleAnalysis npm installwebpack 单文件如何打包?

首先现在 webpack 作为当前主流的前端模块化工具,在 webpack 刚开始流行的时候,我们经常通过 webpack 将所有处理文件全部打包成一个 bundle 文件, 先通过一个简单的例子来看:

// src/single/index.js var index2 = require('./index2'); var util = require('./util'); console.log(index2); console.log(util); // src/single/index2.js var util = require('./util'); console.log(util); module.exports = "index 2"; // src/single/util.js module.exports = "Hello World"; // 通过 config/webpack.config.single.js 打包 const webpack = require('webpack'); const path = require('path') module.exports = { entry: { index: [path.resolve(__dirname, '../src/single/index.js')], }, output: { path: path.resolve(__dirname, '../dist'), filename: '[name].[chunkhash:8].js' }, }

通过 npm run build:single 可看到打包效果,打包内容大致如下(经过精简):

// dist/index.xxxx.js (function(modules) { // 已经加载过的模块 var installedModules = {}; // 模块加载函数 function __webpack_require__(moduleId) { if(installedModules[moduleId]) { return installedModules[moduleId].exports; } var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {} }; modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); module.l = true; return module.exports; } return __webpack_require__(__webpack_require__.s = 3); })([ /* 0 */ (function(module, exports, __webpack_require__) { var util = __webpack_require__(1); console.log(util); module.exports = "index 2"; }), /* 1 */ (function(module, exports) { module.exports = "Hello World"; }), /* 2 */ (function(module, exports, __webpack_require__) { var index2 = __webpack_require__(0); index2 = __webpack_require__(0); var util = __webpack_require__(1); console.log(index2); console.log(util); }), /* 3 */ (function(module, exports, __webpack_require__) { module.exports = __webpack_require__(2); })]);

将相对无关的代码剔除掉后,剩下主要的代码:

首先 webpack 将所有模块(可以简单理解成文件)包裹于一个函数中,并传入默认参数,这里有三个文件再加上一个入口模块一共四个模块,将它们放入一个数组中,取名为 modules,并通过数组的下标来作为 moduleId。

将 modules 传入一个自执行函数中,自执行函数中包含一个 installedModules 已经加载过的模块和一个模块加载函数,最后加载入口模块并返回。

__webpack_require__ 模块加载,先判断 installedModules 是否已加载,加载过了就直接返回 exports 数据,没有加载过该模块就通过 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__) 执行模块并且将 module.exports 给返回。

很简单是不是,有些点需要注意的是:

每个模块 webpack 只会加载一次,所以重复加载的模块只会执行一次,加载过的模块会放到 installedModules,下次需要需要该模块的值就直接从里面拿了。

模块的 id 直接通过数组下标去一一对应的,这样能保证简单且唯一,通过其它方式比如文件名或文件路径的方式就比较麻烦,因为文件名可能出现重名,不唯一,文件路径则会增大文件体积,并且将路径暴露给前端,不够安全。

modules[moduleId].call(module.exports, module, module.exports, __webpack_require__) 保证了模块加载时 this 的指向 module.exports 并且传入默认参数,很简单,不过多解释。

webpack 多文件如何进行代码切割?

webpack 单文件打包的方式应付一些简单场景就足够了,但是我们在开发一些复杂的应用,如果没有对代码进行切割,将第三方库(jQuery)或框架(React)和业务代码全部打包在一起,就会导致用户访问页面速度很慢,不能有效利用缓存,你的老板可能就要找你谈话了。

那么 webpack 多文件入口如何进行代码切割,让我先写一个简单的例子:

// src/multiple/pageA.js const utilA = require('./js/utilA'); const utilB = require('./js/utilB'); console.log(utilA); console.log(utilB); // src/multiple/pageB.js const utilB = require('./js/utilB'); console.log(utilB); // 异步加载文件,类似于 import() const utilC = () => require.ensure(['./js/utilC'], function(require) { console.log(require('./js/utilC')) }); utilC(); // src/multiple/js/utilA.js 可类比于公共库,如 jQuery module.exports = "util A"; // src/multiple/js/utilB.js module.exports = 'util B'; // src/multiple/js/utilC.js module.exports = "util C";

这里我们定义了两个入口 pageA 和 pageB 和三个库 util,我们希望代码切割做到:

因为两入口都是用到了 utilB,我们希望把它抽离成单独文件,并且当用户访问 pageA 和 pageB 的时候都能去加载 utilB 这个公共模块,而不是存在于各自的入口文件中。