在上一篇 #98 中,我们通过实现requireJS,对模块化有了一些认识。今天我们更进一步,看看如何实现一个简单的 webpack ,实现的源码参考 这里 。
现在的webpack是一个庞然大物,我们不可能实现其所有功能。
那么, 应该将目光聚焦在哪儿呢?
从 webpack的第一个commit 可以看出,其当初最主要的目的是 在浏览器端复用符合CommonJS规范的代码模块 。这个目标不是很难,我们努力一把还是可以实现的。
注意:在此我们不考虑插件、loaders、多文件打包等等复杂的问题,仅仅考虑最基本的问题: 如何将多个符合CommonJS规范的模块打包成一个JS文件,以供浏览器执行。
显然,浏览器没法直接执行CommonJS规范的模块,怎么办呢?
答案: 将其转换成一个自执行表达式
注意:此处涉及到webpack构建出来的 bundle.js
的内部结构问题,如果不了解bundle.js具体是如何执行的,请务必搞清楚再往下阅读。可以参考 #64
或者 这里
我们实际要处理的例子是 这个 :example依赖于a、b和c,而且c位于node_modules文件夹中,我们要将所有模块构建成一个JS文件,就是 这里的output.js
仔细观察 output.js ,我们能够发现:
example依赖于a、b和c
。 require('c')
,这里肯定是存在某种自动查找的功能。 output.js
中,每个模块的唯一标识是模块的ID,所以在拼接 output.js
的时候,需要将每个模块的名字替换成模块的ID。也就是说, // 转换前 let a = require('a'); let b = require('b'); let c = require('c'); // 转换后 let a = require(/* a */1); let b = require(/* b */2); let c = require(/* c */3);
ok,下面我们来逐一看看这些问题。
CommonJS不同于AMD,是不会在一开始声明所有依赖的。CommonJS最显著的特征就是
用到的时候再 require
,所以我们得
在整个文件的范围内查找到底有多少个 require
。
怎么办呢?
最先蹦入脑海的思路是 正则
。然而,用正则来匹配 require
,有以下两个缺点:
require
是写在注释中,也会匹配到。 require
的参数是表达式的情况,如 require('a'+'b')
,正则很难处理。 因此,正则行不通。
一种正确的思路是: 使用JS代码解析工具(如 esprima 或者 acorn ),将JS代码转换成抽象语法树(AST) ,再对AST进行遍历。这部分的核心代码是 parse.js 。
在处理好了 require
的匹配之后,还有一个问题需要解决。那就是
匹配到 require
之后需要干什么呢?
举个例子:
// example.js let a = require('a'); let b = require('b'); let c = require('c');
这里有三个 require
,按照CommonJS的规范,在检测到第一个 require
的时候,根据 require即执行
的原则,程序应该立马去读取解析模块 a
。如果模块 a
中又 require
了其他模块,那么继续解析。也就是说,总体上遵循 深度优先遍历算法
。这部分的控制逻辑写在 buildDeps.js
中。
在完成依赖分析的同时,我们需要解决另外一个问题,那就是 如何找到模块?也就是模块的寻址问题。
举个例子:
// example.js let a = require('a'); let b = require('b'); let c = require('c');
在模块 example.js
中,调用模块 a、b、c
的方式都是一样的。
但是,实际上他们所在的绝对路径层级并不一致:
a和b
跟 example
同级,而 c
位于与 example
同级的 node_modules
中
。所以,程序需要有一个查找模块的算法,这部分的逻辑在 resolve.js
中。
目前实现的查找逻辑是:
当然,此处实现的算法还比较简陋,之后有时间可以再考虑实现 逐层往上的查找,就像nodejs默认的模块查找算法那样。
这是最后一步了。
在解决了 模块依赖
和 模块查找
的问题之后,我们将会得到一个依赖关系对象 depTree
,此对象完整地描述了以下信息:都有哪些模块,各个模块的内容是什么,他们之间的依赖关系又是如何等等。具体的结构如下:
{ "modules": { "/Users/youngwind/www/fake-webpack/examples/simple/example.js": { "id": 0, "filename": "/Users/youngwind/www/fake-webpack/examples/simple/example.js", "name": "/Users/youngwind/www/fake-webpack/examples/simple/example.js", "requires": [ { "name": "a", "nameRange": [ 16, 19 ], "id": 1 }, { "name": "b", "nameRange": [ 38, 41 ], "id": 2 }, { "name": "c", "nameRange": [ 60, 63 ], "id": 3 } ], "source": "let a = require('a');/nlet b = require('b');/nlet c = require('c');/na();/nb();/nc();/n" }, "/Users/youngwind/www/fake-webpack/examples/simple/a.js": { "id": 1, "filename": "/Users/youngwind/www/fake-webpack/examples/simple/a.js", "name": "a", "requires": [], "source": "// module a/n/nmodule.exports = function () {/n console.log('a')/n};" }, "/Users/youngwind/www/fake-webpack/examples/simple/b.js": { "id": 2, "filename": "/Users/youngwind/www/fake-webpack/examples/simple/b.js", "name": "b", "requires": [], "source": "// module b/n/nmodule.exports = function () {/n console.log('b')/n};" }, "/Users/youngwind/www/fake-webpack/examples/simple/node_modules/c.js": { "id": 3, "filename": "/Users/youngwind/www/fake-webpack/examples/simple/node_modules/c.js", "name": "c", "requires": [], "source": "module.exports = function () {/n console.log('c')/n}" } }, "mapModuleNameToId": { "/Users/youngwind/www/fake-webpack/examples/simple/example.js": 0, "a": 1, "b": 2, "c": 3 } }
根据这个 depTree
对象,我们便能完成这最后的一步:**output.js文件的拼接。**其控制逻辑无非是一层循环,写在 writeChunk.js
中。
但是这里有一个需要注意的地方,那就是本文思路章节提到的第4点:要把模块名转换成模块ID,这是 writeSource.js 所要完成的功能。
至此,我们就实现了一个非常简单的webpack了。
require('a' + 'b')
这种情况。 ========EOF===========