前言
自动化打包工具webpack,相信很多人和我一样尝试着研究下它,但是繁杂的功能以及高度抽象的代码实在是很难理解,所以笔者只能通过github的进行分析,实现,并将实现的一些心得分享一下。
功能分析
对于node端来讲,有commonjs来规范模块的标识,定义,引用。而浏览器端由于缺乏原生对象支持就需要通过自我实现来模拟commonjs规范。
webpack是通过一个IIFE立即调用函数表达式去实现这个规范的。简要的去注释,去除内部运行的代码,其格式如下:(function(module){})([function(){},function(){}]);
简单点说就是各个模块代码以数组的形式传递给运行函数,在进行存储。详细分析可以参考
所以实现以上的功能需求点如下:
- 文件路径分析与定位resolve
- 文件编译&解析,分析出依赖文件parse
- 生成需要打包的文件树depTree
- 将依赖文件写入输出文件内writeChunk
文件分析与定位
本功能和node的require类似,故有参考
文件分析,将文件为两种类型
-
以 "./","../","/" 标识符开头的路径文件模块
- 该类文件会通过path.join 转化为真实的路径而定位。
-
自定义的文件模块
- 这类相对比较麻烦,他在当前目录下面的node_modules,查找文件,未找到则一路向上查找,最终查找到或者抛出异常。如:
[ '/Users/zhujian/Documents/workspace/webpack/simple-webpack/node_modules', '/Users/zhujian/Documents/workspace/webpack/node_modules', '/Users/zhujian/Documents/workspace/node_modules', '/Users/zhujian/Documents/node_modules', '/Users/zhujian/node_modules', '/Users/node_modules', '/node_modules' ]
文件定位
- 对于部分文件并没有带扩展名,此时有默认的扩展名依次以.js,.jsx为后缀依次补充。当然我们可以用传入extensions,修改默认的扩展名。
{extensions:['js','jsx','jpg']}
-
当发现该路径为文件夹时则,则依次查找如下文件
- package.json(main字段)
- index+(扩展名)
文件解析
-
文件可以定位之后,则是解析定位下来的文件了,本文用的是exprima,文档如
- esprima解析文件,返回一个语法树。
- 对语法树进行遍历,对遇到type 为CallExpression,且其callee为name为require的节点,将该节点的value,以及下标包装成对象储存起来。
比如
const b = require('./b');
解析后
...."init": { "type": "CallExpression", "callee": { "type": "Identifier", "name": "require", "range": [ 10, 17 ] }, "arguments": [ { "type": "Literal", "value": "./b", "raw": "'./b'", "range": [ 18, 23 ] } ], "range": [ 10, 24 ] }, ....
我们要做的就是提取value "./b",以及该字符串在文件所处的位置range。
文件树生成
主要是从入口文件开始,将所有依赖的js,以及其内容,分配的id组成一个可操作的扁平化的对象和存储着name与id对应的map对象。
实现手法上也是递归resolve函数,获取到各个文件的依赖,文件,id的信息,最后得到depTree对象
举个例子:
{ modules: { '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/a.js': { filename: '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/a.js', id: 0, requires: [Array], rangeRequires: [Array], source: 'const b = require(\'./b\');\nconst c = require(\'c\');\nconst {e, f, g} = require(\'./m\');\n\n }, '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/b.js': { filename: '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/b.js', id: 1, requires: [], rangeRequires: [], source: 'const b = \'b\';\n\nmodule.exports = b;\n' }, '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/node_modules/c.js': { filename: '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/node_modules/c.js', id: 2, requires: [], rangeRequires: [], source: 'const c = \'c\';\n\nmodule.exports = c;\n' }, '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/m.js': { filename: '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/m.js', id: 3, requires: [], rangeRequires: [], source: '// const core = require(\'./core\');\nconst a = 1;\n\n }, nextModuleId: 4, mapNameToId: { '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/a.js': 0, '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/b.js': 1, '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/node_modules/c.js': 2, '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/m.js': 3 } }
文件写入
写入主函数,替换入口的执行函数。这块会用到之前的path和id关系的map对象,通过路口文件的绝对路径,找出入口文件的mainId,并进行替换。
写入参数数组。遍历文件树,将文件节点的source内容替换掉
大致如下:
require('module') 替换为__webpack_require__(0)
-
这个地方要考虑的点是
- 如果用replace替换的话,会影响source带部分关键字的内容,不可取。
- 用索引替换的字符串的话,一旦第一个替换成功,整个字符串长度发生变化,原先的索引下标就失效了。
官方实现
const result = [source]; replaces.forEach(replace => { const {from, value, end} = replace; const source = result.shift(); result.unshift(source.substr(0, from), value, source.substr(end)) });
代码实现
本人的简易版webpack实现
(完)