在上一篇 中,我们通过实现 requireJS,对模块化有了一些认识。今天我们更进一步,看看如何实现一个简单的 ,实现的源码参考 。
现在的 webpack 是一个庞然大物,我们不可能实现其所有功能。
那么, 应该将目光聚焦在哪儿呢?
从 可以看出,其当初最主要的目的是 在浏览器端复用符合 CommonJS 规范的代码模块 。这个目标不是很难,我们努力一把还是可以实现的。
注意:在此我们不考虑插件、loaders、多文件打包等等复杂的问题,仅仅考虑最基本的问题: 如何将多个符合 CommonJS 规范的模块打包成一个 JS 文件,以供浏览器执行。
显然,浏览器没法直接执行 CommonJS 规范的模块,怎么办呢?
答案: 将其转换成一个自执行表达式
注意:此处涉及到 webpack 构建出来的
的内部结构问题,如果不了解 bundle.js 具体是如何执行的,请务必搞清楚再往下阅读。可以参考 或者
- bundle.js
我们实际要处理的例子是 :example 依赖于 a、b 和 c,而且 c 位于 node_modules 文件夹中,我们要将所有模块构建成一个 JS 文件,就是
仔细观察 ,我们能够发现:
。
- example依赖于a、b和c
,这里肯定是存在某种自动查找的功能。
- require('c')
中,每个模块的唯一标识是模块的 ID,所以在拼接
- output.js
的时候,需要将每个模块的名字替换成模块的 ID。也就是说,
- output.js
- // 转换前
- 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 代码解析工具(如 {aa11aa} 或者 {aa10aa} ),将 JS 代码转换成抽象语法树(AST) ,再对 AST 进行遍历。这部分的核心代码是 。
在处理好了
的匹配之后,还有一个问题需要解决。那就是 匹配到
- require
之后需要干什么呢?
- require
举个例子:
- // example.js
- let a = require('a');
- let b = require('b');
- let c = require('c');
这里有三个
,按照 CommonJS 的规范,在检测到第一个
- require
的时候,根据
- require
的原则,程序应该立马去读取解析模块
- require即执行
。如果模块
- a
中又
- a
了其他模块,那么继续解析。也就是说,总体上遵循 深度优先遍历算法 。这部分的控制逻辑写在 中。
- require
在完成依赖分析的同时,我们需要解决另外一个问题,那就是 如何找到模块?也就是模块的寻址问题。
举个例子:
- // 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
目前实现的查找逻辑是:
当然,此处实现的算法还比较简陋,之后有时间可以再考虑实现 逐层往上的查找,就像 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
- }
- }
根据这个
对象,我们便能完成这最后的一步:**output.js 文件的拼接。** 其控制逻辑无非是一层循环,写在 中。
- depTree
但是这里有一个需要注意的地方,那就是本文思路章节提到的第 4 点:要把模块名转换成模块 ID,这是 所要完成的功能。
至此,我们就实现了一个非常简单的 webpack 了。
这种情况。
- require('a' + 'b')
========EOF===========
来源: http://www.tuicool.com/articles/VzA3uiB