现在前端用 webpack 打包 JS 和其它文件已经是主流了, 加上 Node 的流行, 使得前端的工程方式和后端越来越像所有的东西都模块化, 最后统一编译 Webpack 因为版本的不断更新以及各种各样纷繁复杂的配置选项, 在使用中出现一些迷之错误常常让人无所适从所以了解一下 Webpack 究竟是怎么组织编译模块的, 生成的代码到底是怎么执行的, 还是很有好处的, 否则它就永远是个黑箱当然了我是前端小白, 最近也是刚开始研究 Webpack 的原理, 在这里做一点记录
编译模块
编译两个字听起来就很黑科技, 加上生成的代码往往是一大坨不知所云的东西, 所以常常会让人却步, 但其实里面的核心原理并没有什么难所谓的 Webpack 的编译, 其实只是 Webpack 在分析了你的源代码后, 对其作出一定的修改, 然后把所有源代码统一组织在一个文件里而已最后生成一个大的 bundle JS 文件, 被浏览器或者其它 Javascript 引擎执行并返回结果
在这里用一个简单的案例来说明 Webpack 打包模块的原理例如我们有一个模块 mA.js
- var aa = 1;
- function getDate() {return new Date();
- }
- module.exports = {
- aa: aa,
- getDate: getDate
- }
我随便定义了一个变量 aa 和一个函数 getDate, 然后 export 出来, 这里是用 CommonJS 的写法
然后再定义一个 app.js, 作为 main 文件, 仍然是 CommonJS 风格:
- var mA = require('./mA.js');
- console.log('mA.aa =' + mA.aa);
- mA.getDate();
现在我们有了两个模块, 使用 Webpack 来打包, 入口文件是 app.js, 依赖于模块 mA.js,Webpack 要做几件事情:
从入口模块 app.js 开始, 分析所有模块的依赖关系, 把所有用到的模块都读取进来
每一个模块的源代码都会被组织在一个立即执行的函数里
改写模块代码中和 require 和 export 相关的语法, 以及它们对应的引用变量
在最后生成的 bundle 文件里建立一套模块管理系统, 能够在 runtime 动态加载用到的模块
我们可以看一下上面这个例子, Webpack 打包出来的结果最后的 bundle 文件总的来说是一个大的立即执行的函数, 组织层次比较复杂, 大量的命名也比较晦涩, 所以我在这里做了一定改写和修饰, 把它整理得尽量简单易懂
首先是把所有用到的模块都罗列出来, 以它们的文件名 (一般是完整路径) 为 ID, 建立一张表:
- var modules = {
- './mA.js': generated_mA,
- './app.js': generated_app
- }
关键是上面的 generated_xxx 是什么? 它是一个函数, 它把每个模块的源代码包裹在里面, 使之成为一个局部的作用域, 从而不会暴露内部的变量, 实际上就把每个模块都变成一个执行函数它的定义一般是这样:
- function generated_module(module, exports, webpack_require) {
- // 模块的具体代码
- // ...
- }
在这里模块的具体代码是指生成代码, Webpack 称之为 generated code 例如 mA, 经过改写得到这样的结果:
- function generated_mA(module, exports, webpack_require) {
- var aa = 1;
- function getDate() {
- return new Date();
- }
- module.exports = {
- aa: aa,
- getDate: getDate
- }
- }
乍一看似乎和源代码一模一样的确, mA 没有 require 或者 import 其它模块, export 用的也是传统的 CommonJS 风格, 所以生成代码没有任何改动不过值得注意的是最后的 module.exports = ..., 这里的 module 就是外面传进来的参数 module, 这实际上是在告诉我们, 运行这个函数, 模块 mA 的源代码就会被执行, 并且最后需要 export 的内容就会被保存到外部, 到这里就标志着 mA 加载完成, 而那个外部的东西实际上就后面要说的模块管理系统
接下来看 app.js 的生成代码:
- function generated_app(module, exports, webpack_require) {
- var mA_imported_module = webpack_require('./mA.js');
- console.log('mA.aa =' + mA_imported_module['aa']);
- mA_imported_module['getDate']();
- }
可以看到, app.js 的源代码中关于引入的模块 mA 的部分做了修改, 因为无论是 require/exports, 或是 ES6 风格的 import/export, 都无法被 JavaScript 解释器直接执行, 它需要依赖模块管理系统, 把这些抽象的关键词具体化也就是说, webpack_require 就是 require 的具体实现, 它能够动态地载入模块 mA, 并且将结果返回给 app
到这里你脑海里可能已经初步逐渐构建出了一个模块管理系统的想法, 我们来看一下 webpack_require 的实现:
- // 加载完毕的所有模块
- var installedModules = {};
- function webpack_require(moduleId) {
- // 如果模块已经加载过了, 直接从 Cache 中读取
- if (installedModules[moduleId]) {
- return installedModules[moduleId].exports;
- }
- // 创建新模块并添加到 installedModules
- var module = installedModules[moduleId] = {
- id: moduleId,
- exports: {}
- };
- // 加载模块, 即运行模块的生成代码,
- modules[moduleId].call(
- module.exports, module, module.exports, webpack_require);
- return module.exports;
- }
注意倒数第二句里的 modules 就是我们之前定义过的所有模块的 generated code:
- var modules = {
- './mA.js': generated_mA,
- './app.js': generated_app
- }
webpack_require 的逻辑写得很清楚, 首先检查模块是否已经加载, 如果是则直接从 Cache 中返回模块的 exports 结果如果是全新的模块, 那么就建立相应的数据结构 module, 并且运行这个模块的 generated code, 这个函数传入的正是我们建立的 module 对象, 以及它的 exports 域, 这实际上就是 CommonJS 里 exports 和 module 的由来当运行完这个函数, 模块就被加载完成了, 需要 export 的结果保存到了 module 对象中
所以我们看到所谓的模块管理系统, 原理其实非常简单, 只要耐心将它们抽丝剥茧理清楚了, 根本没有什么深奥的东西, 就是由这三个部分组成:
- // 所有模块的生成代码
- var modules;
- // 所有已经加载的模块, 作为缓存表
- var installedModules;
- // 加载模块的函数
- function webpack_require(moduleId);
当然以上一切代码, 在整个编译后的 bundle 文件中, 都被包在一个大的立即执行的匿名函数中, 最后返回的就是这么一句话:
return webpack_require(./app.js');
即加载入口模块 app.js, 后面所有的依赖都会动态地递归地在 runtime 加载当然 Webpack 真正生成的代码略有不同, 它在结构上大致是这样:
- (function(modules) {
- var installedModules = {};
- function webpack_require(moduleId) {
- // ...
- }
- return webpack_require('./app.js');
- }) ({
- './mA.js': generated_mA,
- './app.js': generated_app
- });
可以看到它是直接把 modules 作为立即执行函数的参数传进去的而不是另外定义的, 当然这和上面的写法没什么本质不同, 我做这样的改写是为了解释起来更清楚
ES6 的 import 和 export
以上的例子里都是用传统的 CommonJS 的写法, 现在更通用的 ES6 风格是用 import 和 export 关键词, 在使用上也略有一些不同不过对于 Webpack 或者其它模块管理系统而言, 这些新特性应该只被视为语法糖, 它们本质上还是和 require/exports 一样的, 例如 export:
- export aa
- // 等价于:
- module.exports['aa'] = aa
- export default bb
- // 等价于:
- module.exports['default'] = bb
而对于 import:
- import {aa} from './mA.js'
- // 等价于
- var aa = require('./mA.js')['aa']
比较特殊的是这样的:
import m from './m.js'
情况会稍微复杂一点, 它需要载入模块 m 的 default export, 而模块 m 可能并非是由 ES6 的 export 来写的, 也可能根本没有 export default, 所以 Webpack 在为模块生成 generated code 的时候, 会判断它是不是 ES6 风格的 export, 例如我们定义模块 mB.js:
- let x = 3;
- let printX = () => {
- console.log('x =' + x);
- }
- export {printX}
- export default x
它使用了 ES6 的 export, 那么 Webpack 在 mB 的 generated code 就会加上一句话:
- function generated_mB(module, exports, webpack_require) {
- Object.defineProperty(module.exports, '__esModule', {value: true});
- // mB 的具体代码
- // ....
- }
也就是说, 它给 mB 的 export 标注了一个__esModule, 说明它是 ES6 风格的 export 这样在其它模块中, 当一个依赖模块以类似 import m from './m.js'这样的方式加载时, 会首先判断得到的是不是一个 ES6 export 出来的模块如果是, 则返回它的 default, 如果不是, 则返回整个 export 对象例如上面的 mA 是传统 CommonJS 的, mB 是 ES6 风格的:
- // mA is CommonJS module
- import mA from './mA.js'
- console.log(mA);
- // mB is ES6 module
- import mB from './mB.js'
- console.log(mB);
我们定义 get_export_default 函数:
- function get_export_default(module) {
- return module && module.__esModule? module['default'] : module;
- }
这样 generated code 运行后在 mA 和 mB 上会得到不同的结果:
- var mA_imported_module = webpack_require('./mA.js');
- // 打印完整的 mA_imported_module
- console.log(get_export_default(mA_imported_module));
- var mB_imported_module = webpack_require('./mB.js');
- // 打印 mB_imported_module['default']
- console.log(get_export_default(mB_imported_module));
这就是在 ES6 的 import 上, Webpack 需要做一些特殊处理的地方不过总体而言, ES6 的 import/export 在本质上和 CommonJS 没有区别, 而且 Webpack 最后生成的 generated code 也还是基于 CommonJS 的 module/exports 这一套机制来实现模块的加载的
模块管理系统
以上就是 Webpack 如何打包组织模块, 实现 runtime 模块加载的解读, 其实它的原理并不难, 核心的思想就是建立模块的管理系统, 而这样的做法也是具有普遍性的, 如果你读过 Node.js 的 Module 部分的源代码, 就会发现其实用的是类似的方法这里有一篇文章可以参考
来源: http://www.jb51.net/article/136186.htm