随着前端的快速发展,各种打包工具增出不穷。目前使用最为广泛的无疑是webpack。那么问题就来了,你了解 webpack 吗?
接下来教你如何手写一个webpack打包工具。demo
webpack 原理
webpack底层原理甚为复杂,在此只做简单描述,满足下面demo需要。日后会对webpack做一个详细的揭秘。
从webpack工程里面可以看出,首先一般会有一个入口模块路径。webpack会先读取这个入口模块的内容,然后对内容进行解析,找出依赖模块。接下来对依赖模块进行解析,找出依赖的依赖,不断递归,直至找出所有的依赖的模块。最后将所有模块的代码进行封装,输出到bundle文件,在html中引入。
手写webpack
目录结构
首先看一下demo的目录结构。
webpack.js是webpack实例。build/index.js运行webpack实例。src目录为待编译代码。
├── README.md
├── build
│ ├── index.js // webpack 配置
│ └── webpack.js // webpack 实例
├── index.html
├── package-lock.json
├── package.json
└── src // 待编译代码
├── component.js
├── helloworld.js
└── index.js
webpack 实例
1. 读取入口文件
在执行 run 方法之后,首先调用 readFile 方法传入入口模块路径,然后调用 parseFile 读取入口模块内容。
import helloworld from "./helloworld.js";
import component from "./component.js";
var setup = () => {
const wrap = document.createElement('div');
const helloworldElement = document.createElement('div');
helloworldElement.innerHTML = helloworld;
wrap.appendChild(helloworldElement);
wrap.appendChild(component());
document.body.appendChild(wrap);
}
setup();
然后通过babel遍历出依赖模块。
// 将code转化成ast树
const ast = babel.parse(text);
const dependences = [];
// 遍历 ast 所有 import 节点
babel.traverse(ast, {
ImportDeclaration(p) {
dependences.push(p.node.source.value);
},
});
// 将ast转化为code
const { code } = babel.transformFromAstSync(ast, null,{
presets:["@babel/preset-env"]
});
2. 遍历所有依赖
在处理完入口模块的解析之后,会得到它的依赖列表。接着对每个依赖模块执行解析,找出剩下的依赖模块。
result.dependences.forEach((value) => {
const re = this.parseFile(value);
modules.push(re);
});
将所有模块封装,并输出
最后调用assets方法,将所有模块封装成一个对象.
const modules = {
[module.filePath]: function (module, exports, require) {
// module.code
}
}
并输出到output文件中,在html中引入。
完整代码:
const fs = require('fs');
const path = require('path');
const babel = require('@babel/core');
class Webpack {
constructor(config) {
this.config = config;
}
run () {
this.readFile(this.config.entry);
}
assets(modules = []) {
const stats = [];
modules.map((module) => {
stats.push(`
'${module.filePath}': function (module, exports, require) {
${module.code}
}
`);
});
const code = `
const modules = {
${stats.join()}
}
var __webpack_module_cache__ = {};
function require(moduleId) {
console.log(modules, moduleId)
if(__webpack_module_cache__[moduleId]) {
return __webpack_module_cache__[moduleId].exports;
}
var module = __webpack_module_cache__[moduleId] = {
exports: {}
};
modules[moduleId](module, module.exports, require);
return module.exports;
}
require('${this.config.entry}');
`
const dir = path.dirname(path.resolve(this.config.output));
// 将代码写入bundle
fs.mkdir(dir, { recursive: true }, (err) => {
fs.writeFile(
path.resolve(this.config.output),
code,
'utf8',
() => {}
);
// 处理index.html
let html = fs.readFileSync(path.resolve('./index.html'), "utf-8");
html = html.replace('</head>', ` <script defer src="./index.js"></script>\n</head>`);
fs.writeFile(
`${dir}/index.html`,
html,
'utf8',
() => {}
)
});
}
readFile (filePath) {
const result = this.parseFile(filePath);
const modules = [result];
result.dependences.forEach((value) => {
const re = this.parseFile(value);
modules.push(re);
});
this.assets(modules);
}
parseFile (filePath) {
const text = fs.readFileSync(path.resolve('src', filePath.replace('/src', '')), "utf-8");
const ast = babel.parse(text);
const dependences = [];
babel.traverse(ast, {
ImportDeclaration(p) {
dependences.push(p.node.source.value);
},
});
const { code } = babel.transformFromAstSync(ast, null,{
presets:["@babel/preset-env"]
});
return {
code,
dependences,
filePath
}
}
}
module.exports = Webpack;