这是我参与更文挑战的第8天,活动详情查看:更文挑战
前言
本文我们来实现一个非常简单的类似webpack这样的bundler,实现这个bundler我们可以更加清楚的了解类似于webpack这种打包工具它的底层原理。
项目结构
- bundler
- src
- index.js
- message.js
- word.js
- bundler.js
// index.js
import message from './message.js';
console.log(message);
// message.js
import { word } from './word.js';
const message = `say ${word}`;
export default message;
// word.js
export const word = 'hello';
上述代码最终是输出一个say hello字符串,只不过里面它涉及到了几个模块之间的相互调用。现在我们运行上述代码肯定是不能在浏览器上运行的,因为浏览器根本就不认识import
这种语法,所以我们需要通过一个webpack或者类似的打包工具,帮我们去做项目打包,接下来我们对bundler.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');
const moduleAnalyser = (filename) => {
// 获取入口文件的内容
const content = fs.readFileSync(filename, 'utf-8');
// 转换成ast
const ast = parser.parse(content, {
sourceType: 'module'
});
const dependencies = {};
// 遍历ast 如果ast中包含了ImportDeclaration字段就执行
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(filename);
const newFile = './' + path.join(dirname, node.source.value);
dependencies[node.source.value] = newFile;
}
});
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
});
return {
filename,
dependencies,
code
}
}
const makeDependenciesGraph = (entry) => {
// 接收模块分析的结果
const entryModule = moduleAnalyser(entry);
// 将结果用数组包裹,方便往里写入数据,此时graphArray的长度是1
const graphArray = [ entryModule ];
// 因为graphArray目前的长度是1,所以只会遍历一次
for(let i = 0; i < graphArray.length; i++) {
const item = graphArray[i];
// 获取该文件依赖
const { dependencies } = item;
// 如果该文件有引入依赖 就进入
if(dependencies) {
for(let j in dependencies) {
graphArray.push(
// 将依赖的入口传入模块分析,再次进行解析
// push进去后,graphArray的长度+1就可以继续进行遍历
// 达到一种类似递归的效果 一层层找每个文件里的依赖进行解析
moduleAnalyser(dependencies[j])
);
}
}
}
const graph = {};
graphArray.forEach(item => {
graph[item.filename] = {
dependencies: item.dependencies,
code: item.code
}
});
// graph结果如图4
return graph;
}
const generateCode = (entry) => {
const graph = JSON.stringify(makeDependenciesGraph(entry));
return `
// 接收图4的结果
(function(graph){
// 执行require函数
function require(module) {
function localRequire(relativePath) {
return require(graph[module].dependencies[relativePath]);
}
var exports = {};
// 进入此立即函数 第一个参数localRequire在 eval(code)里被调用
(function(require, exports, code){
eval(code)
})(localRequire, exports, graph[module].code);
return exports;
};
require('${entry}')
})(${graph});
`;
}
const code = generateCode('./src/index.js');
// 返回的结果,放入浏览器执行的结果,看图5
console.log(code);
moduleAnalyser
就是分析模块,传入入口文件进行分析,fs获取入口文件的内容,接下来我们打印content
:
@babel/parser
将内容转换成AST,如果我们的代码是用es module编写的sourceType
就要填写module
;@babel/traverse
用来遍历AST,遍历AST往dependencies
里写入模块依赖;
@babel/core
配合@babel/preset-env
将es6代码转换成es5代码;
图4
图5
结果能打印出 say hello
总结
以上是个人的一些理解,具体实现形式还请翻阅其他文档进行学习。