在写一个mini-webpack之前,需要先了解一个概念:AST。
抽象语法树:在计算机科学中,抽象语法树,或者成为语法树,是源代码语法结构中的一种抽象表示。它以树状的形式表现编程语言的语法结构。树上的每个节点都表示源代码中的每个结构。
个人理解就是:AST抽象语法树根据JavaScript代码解析得到,JavaScript代码中的各个部分都可以在AST中表示出来。
AST的形成了反映出各部分代码之间的关系。
我们可以把webpack想象成一个黑匣子,给这个黑匣子一个入口文件(index.js
),这个入口文件在这个黑匣子里面经过一系列操作,最后输出bundle.js
文件。核心过程可以用下面的图来表示:
大体分为以下几个步骤:
读取入口文件index.js
,对index.js
中的代码进行解析生成AST抽象语法树。
根据AST抽象语法树,分别生成文件依赖关系图和浏览器可以执行的ES5代码。
实现CMD API,输出bundle文件。
首先准备一个样例。
// ./src/index.js
import info from './info.js'
console.log(info)
// ./src/info.js
import { name } from "./consts.js";
export default `my name is ${name}`
// ./src/consts.js
export const name = "yanwuhc"
在src文件夹外面创建一个fc_webpack.js
文件,开始mini-webpack的书写。
由于所有文件都需要解析,因此,先实现单个文件解析的方法createAsset
。
首先需要读取文件的内容:
function createAsset(filepath){
// 读取入口文件内容
const content = fs.readFileSync(filepath, 'utf-8');
}
接着,使用@babel/parser
来生成AST抽象语法树。
function createAsset(filepath){
const content = fs.readFileSync(filepath, 'utf-8');
// 利用@babel/parser将ES6代码装换成AST生成树
const ast = parser.parse(content, {
sourceType: "module" // 需要注明传入的数据是模块化代码,不然会解析不成功
})
}
这里有个问题:如何根据AST找到一个文件的依赖文件?
实际上,查看AST,可以发现AST中有一个ImportDeclaration
属性(表示源代码中的import节点),这个属性上面的source
属性的value值指向该文件的依赖文件路径。利用这个,就可以生成文件的依赖关系。
function createAsset(filepath){
const content = fs.readFileSync(filepath, 'utf-8');
const ast = parser.parse(content, {
sourceType: "module"
})
// 在AST里面,找到 import 节点的 source属性 的 value属性
const dependencise = []; // 创建一个数组用来存储依赖路径
traverse(ast, {
/*
原始JavaScript表达式,实际上用到的是path中的node值,
里面有个source属性,在里面有个value属性,我们利用的是这一部分
ImportDeclaration: (path, state)=>{
console.log(path)
}
*/
ImportDeclaration: ({node})=>{
dependencise.push(node.source.value);
}
})
}
到这里,dependencise
中存放的就是当前文件所有依赖文件的相对路径
。
// index.js 生成的依赖关系如下
[ './info.js' ]
利用AST生成ES5代码,需要借助@babel/core
来实现。同时,在使用@babel/core
的时候,需要借用@babel/preset-env
这个插件来完成。
function createAsset(filepath){
const content = fs.readFileSync(filepath, 'utf-8');
const ast = parser.parse(content, {
sourceType: "module"
})
const dependencise = [];
traverse(ast, {
ImportDeclaration: ({node})=>{
dependencise.push(node.source.value);
}
})
// 利用AST生成ES5代码
const { code } = babel.transformFromAstSync(ast, null, {
presets: ["@babel/preset-env"]
})
}
经过babel.transformFromAstSync
转换之后的内容中,只要提取其中的code
属性出来就可以了。获得的code代码如下所示:
// index.js 解析生成的ES5-code
"use strict";
var _info = _interopRequireDefault(require("./info.js"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
console.log(_info["default"]);
最后,需要将上面转换的结果导出去,供后面的函数使用。
let ID = 0;
function createAsset(filepath){
const content = fs.readFileSync(filepath, 'utf-8');
const ast = parser.parse(content, {
sourceType: "module"
})
const dependencise = [];
traverse(ast, {
ImportDeclaration: ({node})=>{
dependencise.push(node.source.value);
}
})
const { code } = babel.transformFromAstSync(ast, null, {
presets: ["@babel/preset-env"]
})
let id = ID++;
// 将转换的所有结果返回出去
return {
id,
filepath,
code,
dependencise
}
}
注意:导出的内容中新增了一个id
属性,这是作为解析当前文件的唯一标识。导出的结果如下:
{
id: 0,
filepath: './src/index.js',
code: '"use strict";\n' +
'\n' +
'var _info = _interopRequireDefault(require("./info.js"));\n' +
'\n' +
'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +
'\n' +
'console.log(_info["default"]);',
dependencies: [ './info.js' ]
}
createAsset
方法完成了单个文件的解析:传入一个文件的路径,会返回解析该文件生成的一个对象(包含id
、filepath
,code
,dependencise
属性)。利用这个函数,可以解析多个文件得到一个结果数组。
创建文件依赖关系图,实际上就是利用createAsset
这个方法来解析所有文件。
重点是如何做到所有文件都能被解析到?这里使用队列循环的思想进行文件解析,直到解析完成为止。
function createGraph(entry){
const mainAsset = createAsset(entry);
// 使用队列循环来创建依赖关系图
let queue = [mainAsset];
// 循环处理-形成依赖
for(const asset of queue){
const dirname = path.dirname(asset.filepath);
asset.mapping = {}; // 形成相对路径和模块id的依赖关系
asset.dependencise.forEach(relativePath=>{
const absolutePath = path.join(dirname, relativePath);
const child = createAsset(absolutePath);
asset.mapping[relativePath] = child.id; // 形成相对路径和模块id的映射关系
queue.push(child)
})
}
return queue;
}
这里添加了一个mapping
属性,用于建立相对路径和模块id之间的关系(在后面打包生成bundle会用到)。
解析得到的文件依赖关系图如下,包含所有的文件解析结果,以及一个mapping对应关系表。
[
{
id: 0,
filepath: './src/index.js',
code: '"use strict";\n' +
'\n' +
'var _info = _interopRequireDefault(require("./info.js"));\n' +
'\n' +
'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +
'\n' +
'console.log(_info["default"]);',
dependencies: [ './info.js' ],
mapping: { './info.js': 1 }
},
{
id: 1,
filepath: 'src\\info.js',
code: '"use strict";\n' +
'\n' +
'Object.defineProperty(exports, "__esModule", {\n' +
' value: true\n' +
'});\n' +
'exports["default"] = void 0;\n' +
'\n' +
'var _consts = require("./consts.js");\n' +
'\n' +
'var _default = "my name is ".concat(_consts.name);\n' +
'\n' +
'exports["default"] = _default;',
dependencies: [ './consts.js' ],
mapping: { './consts.js': 2 }
},
{
id: 2,
filepath: 'src\\consts.js',
code: '"use strict";\n' +
'\n' +
'Object.defineProperty(exports, "__esModule", {\n' +
' value: true\n' +
'});\n' +
'exports.name = void 0;\n' +
'var name = "yanwuhc";\n' +
'exports.name = name;',
dependencies: [],
mapping: {}
}
]
上一步中,生成了graph
文件依赖关系图。这一步,我们要利用生成的graph
来生成最终的bundle.js
。
首先,需要对graph
数据进行改造,生成清晰的module-map
表。
function bundle(graph){
// 对生成的graph进行改造生成清晰的module-map表
let modules = '';
graph.forEach(mod=>{
modules += `
${mod.id}:[
function(require, module, exports){
${mod.code}
},
${JSON.stringify(mod.mapping)}
],
`
})
}
得到的结果如下:每个解析生成文件的id对应一个数组,数组存放了一个function
和一个mapping
对应关系表。其中,function
的require
、module
、exports
需要在后面实现。
0:[
function(require, module, exports){
"use strict";
var _info = _interopRequireDefault(require("./info.js"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
console.log(_info["default"]);
},
{"./info.js":1}
],
1:[
function(require, module, exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = void 0;
var _consts = require("./consts.js");
var _default = "my name is ".concat(_consts.name);
exports["default"] = _default;
},
{"./consts.js":2}
],
2:[
function(require, module, exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.name = void 0;
var name = "yanwuhc";
exports.name = name;
},
{}
],
上面得到的modules
将作为参数传入一个立即执行函数中,在立即执行函数中,必须实现require
、module
、exports
这三个api。
实际上,该立即执行函数只需要执行第一个解析生成的代码即可(由index.js
生成)。
const result = `
(function(modules){
// 实现 require
function require(id){
const [fn, mapping] = modules[id];
function localRequire(relativePath){
return require(mapping[relativePath])
}
const module = {
exports: {}
}
fn(localRequire, module, module.exports);
return module.exports;
}
require(0);
})({${modules}})
`
给fn
传入参数使用localRequire
的原因是,在生成的代码中,require传入的参数是一个相对路径。因此,在这里需要将id转换为相对路径,mapping
属性就起到作用了。
最后生成输出的代码为:
(function(modules){
// 实现require API
function require(id){
const [fn, mapping] = modules[id];
const localRequire = relativePath=>{
return require(mapping[relativePath])
}
const module = {
exports: {}
}
fn(localRequire, module, module.exports);
return module.exports
}
// 打包后的文件,只需要执行入口文件就行
require(0)
})({
0:[
function(require, module, exports){
"use strict";
var _info = _interopRequireDefault(require("./info.js"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
console.log(_info["default"]);
},
{"./info.js":1}
],
1:[
function(require, module, exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = void 0;
var _consts = require("./consts.js");
var _default = "my name is ".concat(_consts.name);
exports["default"] = _default;
},
{"./consts.js":2}
],
2:[
function(require, module, exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.name = void 0;
var name = "yanwuhc";
exports.name = name;
},
{}
],
})
复制粘贴到浏览器中,看是否能输出:
const graph = createGraph("./src/index.js")
const bundleContent = bundle(graph);
fs.writeFileSync('bundle.js', bundleContent);
https://gitee.com/yanwuhc/mini-webpack
欢迎大家一起交流学习!