您好,如果喜欢我的文章,可以关注我的公众号「量子前端」,将不定期关注推送前端好文~
前言
Webpack是前端热门打包工具,是前端工程化的根基,在Webpack眼中,万物皆模块,因此Webpack打包的核心目的其实就是把所有模块都打包到一个文件中(bundle.js)
原理分析
如果现在有这些模块:
index.js
import add from './add.js';
console.log(add(1, 2));
add.js
export default function add(a, b) {
return a + b;
}
我们配置一个最基本的webpack.config.js文件:
const path = require('path');
module.exports = {
entry: './index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
mode: 'none'
}
执行npx webpack,打包后结果:
可以看到,webpack将多个js文件打包到了一个文件中,这就是webpack的原理,要实现这个原理,需要这些步骤:
- 分析entry文件;
- 基于entry文件的导入依赖,递归查找出整个项目的依赖;
- 将所有依赖转换成依赖图;
- 写入bundle.js;
分析entry文件
由于需要es6转es5,以及ast语法树的生成,便于我们查找每个文件中的依赖关系,下载这些依赖包:
npm i --save-dev @babel/parser @babel/traverse @babel/core
并且在项目中新建webpack.js文件,导入包:
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const babel = require('@babel/core');
接下来开始写第一个方法getModuleInfo,获取单个文件的信息,代码如下:
//生成单文件依赖树
function getModuleInfo(file) {
//读取入口文件内容
const body = fs.readFileSync(file, 'utf-8');
//转为ast语法树
const ast = parser.parse(body, {
sourceType: 'module'
})
//收集所有模块依赖
const deps = {
};
traverse(ast, {
ImportDeclaration({
node }) {
const dirname = path.dirname(file);
//转换当前文件的绝对路径
const absPath = path.join(dirname, node.source.value);
deps[node.source.value] = absPath;
}
})
//es6转es5代码
const {
code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"],
});
const moduleInfo = {
file, deps, code };
return moduleInfo;
}
从代码可以看到,读取到文件信息后,将文件转为ast语法树,并开始收集依赖,这里遍历器用的是babel-traverse自带的遍历器,让我们快速收集到所有import导入依赖的语句,最后将文件代码转为es5代码。
执行getModuleInfo('./index.js');
出现结果:
这就是依赖树种一个文件的内容,接下来的思路很简单,基于entry文件的deps,递归查找所有deps文件中所用到的依赖,直到无依赖,并将收集到的信息全部保存在一个对象中。
收集依赖
这里我们创建两个函数,parseModules和getDeps,代码如下:
//收集entry文件依赖,并整合所有依赖
function parseModules(file) {
const entry = getModuleInfo(file);
let temps = [entry];
const depsResult = {
}; //整个项目最终所有文件 -> 代码块的映射表
getDeps(entry, temps)
temps.forEach(item => {
depsResult[item.file] = {
deps: item.deps,
code: item.code
}
})
return depsResult;
}
//收集entry文件所依赖的子文件其他依赖
function getDeps({
deps }, temps) {
//遍历入口文件的所有依赖,寻找更多依赖
Object.keys(deps).forEach(d => {
const child = getModuleInfo(deps[d]);
temps.push(child);
getDeps(child, temps)
})
}
在parseModules函数中,我们首先获取到了入口文件的依赖树,基于他的deps,进行更多deps的获取,并记录他们的依赖树,最后保存在depsResult中,结果如下:
接下来,我们将所有文件的代码合并起来即可。
写入bundle
这里,我们写一个自执行函数,里面包括了自定义require函数和exports对象,让浏览器识别到我们自定义的导入导出:
function bundle(file) {
const depsGraph = JSON.stringify(parseModules(file));
return `(function (graph) {
function require(file) {
function absRequire(relPath) {
return require(graph[file].deps[relPath])
}
var exports = {};
(function (require,exports,code) {
eval(code)
})(absRequire,exports,graph[file].code)
return exports
}
require('${
file}')
})(${
depsGraph})`;
}
const content = bundle('./index.js');
!fs.existsSync("./dist") && fs.mkdirSync("./dist");
fs.writeFileSync("./dist/bundle.js", content);
最后把这块代码写入dist/bundle.js即可。
测试
我们在index.html中引入dist/bundle.js:
打开浏览器:
源码
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const babel = require('@babel/core');
//生成单文件依赖树
function getModuleInfo(file) {
//读取入口文件内容
const body = fs.readFileSync(file, 'utf-8');
//转为ast语法树
const ast = parser.parse(body, {
sourceType: 'module'
})
//收集所有模块依赖
const deps = {
};
traverse(ast, {
ImportDeclaration({
node }) {
const dirname = path.dirname(file);
//转换当前文件的绝对路径
const absPath = path.join(dirname, node.source.value);
deps[node.source.value] = absPath;
}
})
//es6转es5代码
const {
code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"],
});
const moduleInfo = {
file, deps, code };
return moduleInfo;
}
//收集entry文件依赖,并整合所有依赖
function parseModules(file) {
const entry = getModuleInfo(file);
let temps = [entry];
const depsResult = {
}; //整个项目最终所有文件 -> 代码块的映射表
getDeps(entry, temps)
temps.forEach(item => {
depsResult[item.file] = {
deps: item.deps,
code: item.code
}
})
return depsResult;
}
//收集entry文件所依赖的子文件其他依赖
function getDeps({
deps }, temps) {
//遍历入口文件的所有依赖,寻找更多依赖
Object.keys(deps).forEach(d => {
const child = getModuleInfo(deps[d]);
temps.push(child);
getDeps(child, temps)
})
}
function bundle(file) {
const depsGraph = JSON.stringify(parseModules(file));
return `(function (graph) {
function require(file) {
function absRequire(relPath) {
return require(graph[file].deps[relPath])
}
var exports = {};
(function (require,exports,code) {
eval(code)
})(absRequire,exports,graph[file].code)
return exports
}
require('${
file}')
})(${
depsGraph})`;
}
const content = bundle('./index.js');
!fs.existsSync("./dist") && fs.mkdirSync("./dist");
fs.writeFileSync("./dist/bundle.js", content);
ok,学费了~