作为一名前端开发工程师,想必大家在工作和学习中少不了跟Webpack打交道,本文从手写Webpack的角度,一步一步带你实现一个简易版的Webpack,顺便让大家了解一下Webpack的原理。 源码地址
基本实现流程
- 读取入口文件的内容
- 分析入口文件,读取模块并递归读取模块所依赖的文件内容,生成AST语法树。
- 根据AST语法树,生成浏览器能够运行的代码
工具模块包
path:路径操作fs:读写文件@babel/parser:将内容转成AST语法树@babel/traverse: 遍历AST收集依赖@babel/core和@babel/preset-env:将ES6语法转成ES5
功能实现
模块分析:读取入口文件,分析代码
const fs = require("fs");
const main = filename => {
const content = fs.readFileSync(filename, "utf-8");
console.log(content);
};
main("./index.js");
将内容转成AST语法树
使⽤@babel/parser,这是babel7的⼯具,来帮助我们分析内部的语法,包括es6,返回⼀个ast抽象语法树。我们解析出来的不单单是index.js文件里的内容,它也包括了文件的其他信息。而它的内容其实是它的属性program里的body里。
- 安装@babel/parser
$ npm install @babel/parser --save
- 具体代码
const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const parse = filename => {
const content = fs.readFileSync(filename, "utf-8");
const Ast = parser.parse(content, {
sourceType: "module"
});
console.log(Ast.program.body);
};
parse("./index.js");
遍历AST收集依赖
接下来我们就可以根据body⾥⾯的分析结果,遍历出所有的引⼊模块。其实就是将用import语句引入的文件路径收集起来。这⾥还是推荐babel的⼀个模块@babel/traverse,来帮我们处理。
- 安装
@babel/traverse
$ npm install @babel/traverse --save
- 具体代码
const parse = filename => {
const content = fs.readFileSync(filename, "utf-8");
const Ast = parser.parse(content, {
sourceType: "module"
});
const dependencies = [];
//分析ast抽象语法树,根据需要返回对应数据,
//根据结果返回对应的模块,定义⼀个数组,接受⼀下node.source.value的值
traverse(Ast, {
ImportDeclaration({ node }) {
console.log(node);
dependencies.push(node.source.value);
}
});
console.log(dependencies);
};
parse("./index.js");
ES6转成ES5(AST)
现在我们需要把获得的ES6的AST转化成ES5,执行这一步需要@babel/core和@babel/preset-env。
- 安装
@babel/core和@babel/preset-env
npm install @babel/core @babel/preset-env
- 具体代码
const parse = filename => {
const content = fs.readFileSync(filename, "utf-8");
const Ast = parser.parse(content, {
sourceType: "module"
});
const dependencies = [];
//分析ast抽象语法树,根据需要返回对应数据,
//根据结果返回对应的模块,定义⼀个数组,接受⼀下node.source.value的值
traverse(Ast, {
ImportDeclaration({ node }) {
console.log(node);
dependencies.push(node.source.value);
}
});
const {code} = babel.transformFromAst(ast,null,{
presets:["@babel/preset-env"]
})
console.log(code);
};
parse("./index.js");
递归获取所有依赖
将上面的parse方法返回一个对象,新建一个run函数,在里面写一个递归方法,递归获取依赖。
const run = filename => {
const entry = parse(filename)
const temp = [entry]
//!处理其他依赖模块,做一个信息汇总
for (let i = 0; i < temp.length; i++) {
const item = temp[i];
const { dependencies } = item;
if (dependencies) {
for (let j in dependencies) {
temp.push(parse(dependencies[j]));
}
}
}
console.log(temp)
}
const parse = filename => {
const content = fs.readFileSync(filename, "utf-8");
const Ast = parser.parse(content, {
sourceType: "module"
});
const dependencies = [];
//分析ast抽象语法树,根据需要返回对应数据,
//根据结果返回对应的模块,定义⼀个数组,接受⼀下node.source.value的值
traverse(Ast, {
ImportDeclaration({ node }) {
console.log(node);
dependencies.push(node.source.value);
}
});
const {code} = babel.transformFromAst(ast,null,{
presets:["@babel/preset-env"]
})
return {
entryFile,
dependencies,
code
};
};
run("./index.js");
自定义require和exports
浏览器不会识别执行require和exports,我们可以自己定义require和exports。
file(code) {
//! 生成bundle.js => ./dist/main.js
const filePath = path.join(this.output.path, this.output.filename);
console.log(filePath);
const newCode = JSON.stringify(code);
const bundle = `(function(graph){
function require(module){
function localRequire(relativePath){
return require( graph[module].dependencies[relativePath])
}
var exports={};
(function(require,exports,code){
eval(code)
})(localRequire,exports,graph[module].code)
return exports;
}
require('${this.entry}') //./src/index
})(${newCode})`;
fs.writeFileSync(filePath, bundle, "utf-8");
}
完整代码
以上这些功能的完整代码可参考:webpack.js
功能测试
- 在
src里面新建三个文件expo.js、index.js、test.js
///test.js
export const test = function() {
console.log("test webpack");
};
//expo.js
import { test } from "./test.js";
test()
export const add = function(a, b) {
return a + b;
};
export const minus = function(a, b) {
return a - b;
};
//index.js
import { add,minus} from "./expo.js";
console.log('add(1,2)',add(1, 2));
console.log('minus(10,3)',minus(10,3));
- 运行命令生成代码如下:
(function(graph){
function require(module){
function localRequire(relativePath){
return require( graph[module].dependencies[relativePath])
}
var exports={};
(function(require,exports,code){
eval(code)
})(localRequire,exports,graph[module].code)
return exports;
}
require('./src/index.js') //./src/index
})({"./src/index.js":{"dependencies":{"./expo.js":"./src\\expo.js"},"code":"\"use strict\";\n\nvar _expo = require(\"./expo.js\");\n\nconsole.log('add(1,2)', (0, _expo.add)(1, 2));\nconsole.log('minus(10,3)', (0, _expo.minus)(10, 3));"},"./src\\expo.js":{"dependencies":{"./test.js":"./src\\test.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.minus = exports.add = void 0;\n\nvar _test = require(\"./test.js\");\n\n(0, _test.test)();\n\nvar add = function add(a, b) {\n return a + b;\n};\n\nexports.add = add;\n\nvar minus = function minus(a, b) {\n return a - b;\n};\n\nexports.minus = minus;"},"./src\\test.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.test = void 0;\n\nvar test = function test() {\n console.log(\"test webpack\");\n};\n\nexports.test = test;"}})
- 将以上代码放到Chrome控制台执行,输入结果如下:测试成功!
VM2516:9 test webpack
VM2514:5 add(1,2) 3
VM2514:6 minus(10,3) 7