1.核心打包原理:
1) 打包的主要流程如下:
- 需要读到入口文件里面的内容。
- 分析入口文件,递归的去读取模块所依赖的文件内容,生成AST语法树。
- 根据AST语法树,生成浏览器能够运行的代码
2. 常见的模块解析包
- @babel/parser:将获取到的模块内容 解析成AST(es6的)语法树
- @babel/traverse: 遍历AST语法树
- @babel/core @babel/preset-env:ES6的AST转化成ES5的AST
核心代码如下:
// 获取主入口文件
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 获取模块的内容
* @param {文件路径} file
* @return 该模块的路径(file),该模块的依赖(deps),该模块转化成es5的代码
*/
const getModuleInfo = (file) => {
const body = fs.readFileSync(file, "utf-8"); //同步获取文件的内容
const ast = parser.parse(body, {
sourceType: "module", //表示我们要解析的是ES模块
});
// traverse遍历AST语法树,保存Import依赖包的引用路径
const deps = {};
traverse(ast, {
// ImportDeclaration方法代表的是对type类型为ImportDeclaration的节点的处理。
ImportDeclaration({ node }) {
const dirname = path.dirname(file);
//node.source.value 指的是import的值,如,index.js文件中引入的 './add' 和 './minus'
const abspath = "./" + path.join(dirname, node.source.value);
deps[node.source.value] = abspath;
},
});
//transformFromAst传入的AST转化成我们在第三个参数里配置的模块类型
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"], //es5模块类型
});
const moduleInfo = {file,deps,code}
return moduleInfo
};
//递归循环遍历依赖
const parseModules = (file) =>{
const entry = getModuleInfo(file)
const temp = [entry];
const depsGraph = {};
for (let i = 0;i<temp.length;i++){
const deps = temp[i].deps
if (deps){
for (const key in deps){
if (deps.hasOwnProperty(key)){
temp.push(getModuleInfo(deps[key]))
}
}
}
}
temp.forEach(moduleInfo=>{
depsGraph[moduleInfo.file] = {
deps:moduleInfo.deps,
code:moduleInfo.code
}
})
return depsGraph;
}
const bundle = (file) =>{
const depsGraph = JSON.stringify(parseModules(file));
//因为浏览器不会识别执行require和exports,所以自己植入代码增加require和exports,让所有依赖代码都运行起来
//absRequire 获取的是依赖的绝对路径
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('./src/index.js')
//写入到我们的dist目录下
fs.mkdirSync('./dist');
fs.writeFileSync('./dist/bundle.js',content)
由上面的核心代码我们可以知道,webpack的打包过程其实就像一条加工的流水线,一个一个的loader其实就像一个转化器,把看似并不通的东西,转化为webpack可以识别的语法,然后继续处理下去,知道最后打包成成品,而pingins的各种已经注册进webpack的插件(插件更多地是在打包的过程中提供起承转合的作用,优化等等),则一直在监听流水线的工作,需要的时候,插件就再做一层加工,比如压缩,分析打包速度等等,最终得到一个浏览器可以识别并运行的包