转载地址:原文链接
读取文件分析模块依赖
对模块进行解析执行(深度遍历)
针对不同的模块使用相应的loader
编译模块,生成抽象语法树AST。
循环遍历AST树,拼接输出js。
现在的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,我们能够发现:
不管有多少个模块,头部那一块都是一样的,所以可以写成一个模板,也就是templateSingle.js。
需要分析出各个模块间的依赖关系。也就是说,需要知道example依赖于a、b和c。
c模块位于node_modules文件夹当中,但是我们调用的时候却可以直接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中。
目前实现的查找逻辑是:
如果给出的是绝对路径/相对路径,只查找一次。找到?返回绝对路径。找不到?返回false。
如果给出的是模块的名字,先在入口js(example.js)文件所在目录下寻找同名JS文件(可省略扩展名)。找到?返回绝对路径。找不到?走第3步。
在入口js(example.js)同级的node_modules文件夹(如果存在的话)查找。找到?返回绝对路径。找不到?返回false。
当然,此处实现的算法还比较简陋,之后有时间可以再考虑实现逐层往上的查找,就像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了。