-
Notifications
You must be signed in to change notification settings - Fork 1
Description
webpack bundler 实现
前言
Q: 为什么我们要做这件事?
A: 太多小伙伴对 webpack 的认识都在 webpack.config.js 上,对 webpack 的使用也都是黑盒的,对其打包、loader、plugin等实现原理知之甚少
Q: webpack 现在如此庞大,应该如何着手?
A: 我们可以从 webpack 的第一个提交版本着手研究,它主要实现了 bundler和 loader,代码量很小,并且可读性很高 webpack 的第一个 commit
bundler 主要功能
- 将多个符合 CommonJS 规范的模块打包成一个 JS 文件,使其可以运行在浏览器中。
- 显然,浏览器没法直接执行 CommonJS 规范的模块,怎么办呢?我们可以将 module, module.exports, require 等函数在运行模块前定义好,各个模块分别调用执行逻辑
- 一个打包后的 bundle.js 如下
/******/(function(modules) {
/******/ var installedModules = {};
/******/ function require(moduleId) {
/******/ if(installedModules[moduleId])
/******/ return installedModules[moduleId].exports;
/******/ var module = installedModules[moduleId] = {
/******/ exports: {}
/******/ };
/******/ modules[moduleId](module, module.exports, require);
/******/ return module.exports;
/******/ }
/******/ return require(0);
/******/})
/******/({
/******/0: function(module, exports, require) {
var a = require(/* ./a.js */1);
var b = require(/* ./b.js */2);
var luna = require(/* @alipay/luna-core */3);
a();
b();
/******/},
/******/
/******/1: function(module, exports, require) {
// module a
module.exports = function () {
console.log('a')
};
/******/},
/******/
/******/2: function(module, exports, require) {
// module b
module.exports = function () {
console.log('b')
};
/******/},
/******/
/******/3: function(module, exports, require) {
module.exports = {
// ...
};
/******/},
/******/
/******/})
bundler 实现思路
分析 bundle.js,我们能够发现:
-
1、不管有多少个模块,头部那一块都是一样的,它实现了 commonJS 的 module、module.exports、require 等函数,所以可以写成一个模板,也就是 webpack 里面的 templateSingle.js。
-
2、需要分析出各个模块间的依赖关系。也就是说,bundler 需要知道 example 依赖于 a、b 和 模块 luna。
-
3、luna 模块位于 node_modules 文件夹当中,但是我们调用的时候却可以直接 require('@alipay/luna-core'),所以 bundler 肯定是存在某种自动查找的功能。
-
4、在生成的 bundle.js 中,每个模块的唯一标识是模块的 ID,所以在拼接bundle.js 的时候,需要将每个模块的名字替换成模块的 ID
// 转换前 var a = require('./a.js'); var b = require('./b.js'); var luna = require('@alipay/luna-core'); // 转换后 var a = require(/* ./a.js */1); var b = require(/* ./b.js */2); var luna = require(/* @alipay/luna-core */3);
下面我们逐一分析一下上面的 4 各部分
1、头部模板
- templateSingle
Q: 为什么叫 templateSingle?
A: 因为 webpack 在打包其他比如代码切割等时,头部模板会不一样,这儿为了区分,就叫 templateSingle 了,算是将所有的模块都打包到一个 JS 文件里面
// templateSingle
/******/(function(modules) {
/******/ var installedModules = {};
/******/ function require(moduleId) {
/******/ if(installedModules[moduleId])
/******/ return installedModules[moduleId].exports;
/******/ var module = installedModules[moduleId] = {
/******/ exports: {}
/******/ };
/******/ modules[moduleId](module, module.exports, require);
/******/ return module.exports;
/******/ }
/******/ return require(0);
/******/})
2、分析模块依赖
CommonJS 不同于 AMD,不会在模板定义时将所有依赖的声明。CommonJS 最显著的特征就是用到的时候再 require,所以我们得在整个文件的范围内查找依赖了哪些模块。
Q: 怎么在整个文件里面查找出依赖?
A: 正则匹配?
Q: 如果 require 是写在注释里面了怎么办?性能如何?
A: 正则行不通,我们可以采用 babel 等语言编译器的原理,将 JS 代码解析转换成 抽象语法树(AST),再对 AST 进行遍历,找到所有的 require 依赖。
Q: 如果模块 a 依赖 b,b 又依赖 c,然后 c 又依赖 d 这又怎么办?
A: 当解析 a 模块时,如果模块 a 中又 require 了其他模块,那么将继续解析依赖的模块。也就是说,总体上遵循深度优先遍历
- 解析依赖具体实现:(详见 parse.js )
/**
* @file 解析模块依赖
* @author chunk.cj
*/
const esprima = require('esprima');
/**
* 解析模块包含的依赖
* @param {string} source 模块内容字符串
* @returns {object} module 解析模块得出的依赖关系
*/
module.exports = source => {
const ast = esprima.parse(source, {
range: true
});
const module = {};
walkStatements(module, ast.body);
module.source = source;
return module;
};
/**
* 遍历块中的语句
* @param {object} module 模块对象
* @param {object} statements AST语法树
*/
function walkStatements(module, statements) {
statements.forEach(statement => walkStatement(module, statement));
}
/**
* 分析每一条语句
* @param {object} module 模块对象
* @param {object} statement AST语法树
*/
function walkStatement(module, statement) {
switch (statement.type) {
case 'VariableDeclaration':
if (statement.declarations) {
walkVariableDeclarators(module, statement.declarations);
}
break;
}
}
/**
* 处理定义变量的语句
* @param {object} module 模块对象
* @param {object} declarators
*/
function walkVariableDeclarators(module, declarators) {
declarators.forEach(declarator => {
switch (declarator.type) {
case 'VariableDeclarator':
if (declarator.init) {
walkExpression(module, declarator.init);
}
break;
}
});
}
/**
* 处理表达式
* @param {object} module 模块对象
* @param {object} expression 表达式
*/
function walkExpression(module, expression) {
switch (expression.type) {
case 'CallExpression':
// 处理普通的require
if (expression.callee && expression.callee.name === 'require' && expression.callee.type === 'Identifier' && expression.arguments && expression.arguments.length === 1) {
// TODO 此处还需处理require的计算参数
module.requires = module.requires || [];
const param = Array.from(expression.arguments)[0];
module.requires.push({
name: param.value,
nameRange: param.range
})
}
break;
}
}
3、深度优先遍历构建依赖树:(详见 buildDeep.js)
const fs = require('fs');
const path = require('path');
const parse = require('./parse');
const resolve = require('./resolve');
module.exports = async(mainModule, options) => {
let depTree = {
// 递增模块 id
nextModuleId: 0,
// 用于存储各个模块对象
modules: {},
// 用于映射模块名到模块 id 之间的关系
mapModuleNameToId: {},
};
depTree = await parseModule(depTree, mainModule, options.context, options);
return depTree;
};
const parseModule = async(depTree, moduleName, context, options) => {
// 查找模块
const absoluteFileName = resolve(moduleName, context, options.resolve);
// 用模块的绝对路径作为模块的键值,保证唯一性
module = depTree.modules[absoluteFileName] = {
id: depTree.nextModuleId++,
filename: absoluteFileName,
name: moduleName
};
if (!absoluteFileName) {
throw `找不到文件${absoluteFileName}`;
}
const source = fs.readFileSync(absoluteFileName).toString();
const parsedModule = parse(source);
module.requires = parsedModule.requires || [];
module.source = parsedModule.source;
// 写入映射关系
depTree.mapModuleNameToId[moduleName] = depTree.nextModuleId - 1;
// 如果此模块有依赖的模块,采取深度遍历的原则,遍历解析其依赖的模块
const requireModules = parsedModule.requires;
if (requireModules && requireModules.length > 0) {
for (let require of requireModules) {
depTree = await parseModule(depTree, require.name, path.dirname(absoluteFileName), options);
}
}
return depTree;
}
4、模块寻址:(详见 resolve.js)
简单的寻址方法
- 如果给出的是绝对路径/相对路径,只查找一次。找到?返回绝对路径。找不到?返回 false。
- 如果给出的是模块的名字,先在入口 js(example.js)文件所在目录下寻找同名JS文件(可省略扩展名)。找到?返回绝对路径。找不到?走第3步。
- 在入口js(example.js)同级的 node_modules 文件夹(如果存在的话)查找。找到?返回绝对路径。找不到?返回 false。
这儿可以再考虑实现逐层往上查找 node_modules,可以参考 nodejs 默认的模块查找算法
/**
* 查找模块所在绝对路径
* @author chunk.cj
*/
const fs = require('fs');
const path = require('path');
// 判断给出的文件是否存在
const isFile = path => {
try {
const stats = fs.statSync(path);
return stats && stats.isFile()
} catch(e) {
return false;
}
};
const isDir = path => {
try {
const stats = fs.statSync(path);
return stats && stats.isDirectory()
} catch(e) {
return false;
}
};
/**
* 根据模块的标志查找到模块的绝对路径
* @param {string} moduleIdentifier 模块的标志,可能是模块名/相对路径/绝对路径
* @param {string} context 上下文,入口 js 所在目录
* @returns {string} 返回模块绝对路径
*/
module.exports = (moduleIdentifier, context, options) => {
// 模块是绝对路径,只查找一次
if (path.isAbsolute(moduleIdentifier)) {
if (!path.extname(moduleIdentifier)) {
moduleIdentifier += '.js';
}
if (isFile(moduleIdentifier)) {
return moduleIdentifier;
};
} else if (moduleIdentifier.startsWith('./') || moduleIdentifier.startsWith('../')) {
if (!path.extname(moduleIdentifier)) {
moduleIdentifier += '.js';
}
moduleIdentifier = path.resolve(context, moduleIdentifier);
if (isFile(moduleIdentifier)) {
return moduleIdentifier;
};
} else {
// 如果上述的方式都找不到,那么尝试在当前目录的 node_modules 里面找
// 1、直接是node_modules文件夹下的文件
if (isFile(path.resolve(context, './node_modules', moduleIdentifier))) {
return moduleIdentifier;
};
if (isFile(path.resolve(context, './node_modules', `${moduleIdentifier}.js`))) {
return `${moduleIdentifier}.js`;
}
// 2、node_modules 文件夹下的文件夹
if (isDir(path.resolve(context, './node_modules', moduleIdentifier))) {
var pkg = fs.readFileSync(path.resolve(context, './node_modules', moduleIdentifier, 'package.json'));
var pkgJSON = JSON.parse(pkg);
var main = path.resolve(context, './node_modules', moduleIdentifier, pkgJSON.main);
if (isFile(main)) {
return main;
}
} else {
// 逐层往根目录查找
const dirList = context.split('/');
dirList.shift();
while (dirList.length > 0) {
dirList.pop();
const dir = `/${dirList.join('/')}`;
const moduleDir = path.resolve(dir, './node_modules', moduleIdentifier);
if (isFile(moduleDir)) {
return moduleDir;
};
if (isFile(`${moduleDir}.js`)) {
return `${moduleDir}.js`;
}
if (isDir(moduleDir)) {
// 解析 package.json 的 main 字段获取入口
const pkg = fs.readFileSync(path.resolve(moduleDir, 'package.json'), 'utf-8');
const pkgJSON = JSON.parse(pkg);
const main = path.resolve(moduleDir, pkgJSON.main);
if (isFile(main)) {
return main;
}
}
}
}
return moduleIdentifier;
}
};
拼接 bundle:(详见 webpack.js)
生成的 deepTree 如下:
{
"nextModuleId": 5,
"modules": {
"/Users/chunk/Alipay/github/webpack-bundler/example/bundle/entry.js": {
"id": 0,
"filename": "/Users/chunk/Alipay/github/webpack-bundler/example/bundle/entry.js",
"name": "/Users/chunk/Alipay/github/webpack-bundler/example/bundle/entry.js",
"requires": [
{
"name": "./a.js",
"nameRange": [
16,
24
]
},
{
"name": "./b.js",
"nameRange": [
43,
51
]
}
],
"source": "var a = require('./a.js');\nvar b = require('./b.js');\n\na();\nb();"
},
"/Users/chunk/Alipay/github/webpack-bundler/example/bundle/a.js": {
"id": 1,
"filename": "/Users/chunk/Alipay/github/webpack-bundler/example/bundle/a.js",
"name": "./a.js",
"requires": [
{
"name": "./c.js",
"nameRange": [
16,
24
]
}
],
"source": "var c = require('./c.js');\n\nmodule.exports = function() {\n console.log('a');\n};"
},
"/Users/chunk/Alipay/github/webpack-bundler/example/bundle/c.js": {
"id": 4,
"filename": "/Users/chunk/Alipay/github/webpack-bundler/example/bundle/c.js",
"name": "./c.js",
"requires": [],
"source": "module.exports = function() {\n console.log('c');\n};"
},
"/Users/chunk/Alipay/github/webpack-bundler/example/bundle/b.js": {
"id": 3,
"filename": "/Users/chunk/Alipay/github/webpack-bundler/example/bundle/b.js",
"name": "./b.js",
"requires": [
{
"name": "./c.js",
"nameRange": [
16,
24
]
}
],
"source": "var c = require('./c.js');\n\nmodule.exports = function() {\n console.log('b');\n c();\n};"
}
},
"mapModuleNameToId": {
"/Users/chunk/Alipay/github/webpack-bundler/example/bundle/entry.js": 0,
"./a.js": 1,
"./c.js": 4,
"./b.js": 3
}
}
循环遍历 modules,拼接 module 模块的 source,拼接结构如下:
/******/(function(modules) {
/******/ var installedModules = {};
/******/ function require(moduleId) {
/******/ if(installedModules[moduleId])
/******/ return installedModules[moduleId].exports;
/******/ var module = installedModules[moduleId] = {
/******/ exports: {}
/******/ };
/******/ modules[moduleId](module, module.exports, require);
/******/ return module.exports;
/******/ }
/******/ return require(0);
/******/})
// 上面是 templateSingle
({
// 入口
0: function(module, exports, require) {
// 模块 source
},
// 模块
1: function(module, exports, require) {
// 模块 source
},
// ...
// 结尾
});
具体实现如下:
const buffer = [];
const modules = deepTree.modules;
for(let moduleName in modules) {
const module = modules[moduleName];
buffer.push("/******/");
buffer.push(module.id);
buffer.push(": function(module, exports, require) {\n\n");
buffer.push(writeSource(module, deepTree));
buffer.push("\n\n/******/},\n/******/\n");
}
return buffer.join("");
我们发现模块 source 里面的模块名还需要替换成模块 id。具体实现如下:
module.exports = function(module, deepTree) {
const source = module.source;
if (!module.requires || !module.requires.length) {
return source.split('\n').map(line => ` ${line}\n`).join('');
}
const replaces = [];
module.requires.forEach(requireItem => {
if(requireItem.nameRange && requireItem.name) {
const prefix = `/* ${requireItem.name} */`;
replaces.push({
from: requireItem.nameRange[0],
to: requireItem.nameRange[1],
value: prefix + deepTree.mapModuleNameToId[requireItem.name]
});
}
});
const result = [source];
// 模块替换算法: https://github.com/coderwin/__/issues/20
replaces.sort((a, b) => b.from - a.from).forEach(replace => {
const remSource = result.shift();
result.unshift(
remSource.substr(0, replace.from),
replace.value,
remSource.substr(replace.to)
);
});
// 给每行加上两个空格
return result.join('').split('\n').map(line => ` ${line}\n`).join('');
};