简单打包具体步骤
因为是简单实现,这边就只考虑一个出口时,不考虑分包的情况,其实我们只得到了一个bundle.js的文件,这个文件里包含了我们所有用到的js模块,可以直接被加载执行,大概可以分为4步:
- 利用
babel完成代码转换及解析,并生成单个文件的依赖模块Map - 从入口开始递归分析,并生成整个项目的依赖图谱
- 将各个引用模块打包为一个立即执行函数
- 将最终的
bundle文件写入bundle.js中
单个文件的依赖模块Map
-
我们会可以使用这几个包:
@babel/parser:负责将代码解析为抽象语法树@babel/traverse:遍历抽象语法树的工具,我们可以在语法树中解析特定的节点,然后做一些操作,如ImportDeclaration获取通过import引入的模块,FunctionDeclaration获取函数@babel/core:代码转换,如ES6的代码转为ES5的模式 由这几个模块的作用,其实已经可以推断出应该怎样获取单个文件的依赖模块了,转为Ast->遍历Ast->调用ImportDeclaration。代码如下
// exportDependencies.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 exportDependencies = (filename) => {
const content = fs.readFileSync(filename, 'utf-8');
// 转换Ast
const ast = parser.parse(content, {
sourceType: 'module',
});
const dependencies = {};
// 遍历抽象语法树 TraverseOptions<babel.types.Node>
traverse(ast, {
//调用ImportDeclaration获取通过import引入的模块
ImportDeclaration({ node }) {
const dirname = path.dirname(filename);
const newFile = './' + path.join(dirname, node.source.value);
//保存所依赖的模块
dependencies[node.source.value] = newFile;
},
});
//通过@babel/core和@babel/preset-env进行代码的转换
const { code } = babel.transformFromAst(ast, null, {
presets: ['@babel/preset-env'],
});
return {
filename, //该文件名
dependencies, //该文件所依赖的模块集合(键值对存储)
code, //转换后的代码
};
};
module.exports = exportDependencies;
可以跑一个例子:
//info.js
export const a = 1
// index.js
import info from'./info.js'
console.log(info)
//testExport.js
const exportDependencies = require('./exportDependencies')
console.log(exportDependencies('./src/index.js'))
单个文件的依赖模块Map
有了获取单个文件依赖的基础,我们就可以在这基础上,进一步得出整个项目的模块依赖图谱了。首先,从入口开始计算,得到
entryMap,然后遍历entryMap.dependencies,取出其value(即依赖的模块的路径),然后再获取这个依赖模块的依赖图谱,以此类推递归下去即可,代码如下
// exportGraph.js
const exportDependencies = require('./exportDependencies.js');
const exportGraph = (entry) => {
const entryModule = exportDependencies(entry);
const graphArray = [entryModule];
for (let index = 0; index < graphArray.length; index++) {
const item = graphArray[index];
//拿到文件所依赖的模块集合,dependencies的值参考exportDependencies
const { dependencies } = item;
for (let j in dependencies) {
graphArray.push(exportDependencies(dependencies[j])); //关键代码,目的是将入口模块及其所有相关的模块放入数组
}
}
// console.log(graphArray);
//接下来生成图谱
const graph = {};
graphArray.forEach((item) => {
graph[item.filename] = {
dependencies: item.dependencies,
code: item.code,
};
});
//可以看出,graph其实是 文件路径名:文件内容 的集合
return graph;
};
module.exports = exportGraph;
最后导出生成模块实现
// exportBundle.js
const fs = require('fs');
const path = require('path');
const exportBundle = (str) => {
fs.mkdirSync('./dist');
fs.writeFileSync('./dist/bundle.js', str);
};
module.exports = exportBundle;
输出立即执行函数
首先,我们的代码被加载到页面中的时候,是需要立即执行的。所以输出的
bundle.js实质上要是一个立即执行函数。我们主要注意以下几点:
- 我们写模块的时候,用的是
import/export.经转换后,变成了require/exports - 我们要让
require/exports能正常运行,那么我们得定义这两个东西,并加到bundle.js里 - 在依赖图谱里,代码都成了字符串。要执行,可以使用
eval
因此,我们要做这些工作:
- 定义一个
require函数,require函数的本质是执行一个模块的代码,然后将相应变量挂载到exports对象上 - 获取整个项目的依赖图谱,从入口开始,调用
require方法。完整代码如下:
// exportCode.js
const exportGraph = require('./exportGraph.js');
const exportBundle = require('./exportBundle.js');
const exportCode = (entry) => {
const graph = JSON.stringify(exportGraph(entry));
exportBundle(`
(function(graph) {
//require函数的本质是执行一个模块的代码,然后将相应变量挂载到exports对象上
function require(module) {
//localRequire的本质是拿到依赖包的exports变量
function localRequire(relativePath) {
return require(graph[module].dependencies[relativePath]);
}
var exports = {};
(function(require, exports, code) {
eval(code);
})(localRequire, exports, graph[module].code);
return exports;
//函数返回指向局部变量,形成闭包,exports变量在函数执行后不会被摧毁
}
require('${entry}')
})(${graph})`);
};
module.exports = exportCode;
最后我们直接在index.js 中输入指定路径后,可以直接生成对应的
/dist/bundle.js,最后生成的最终可执行文件
// /dist/bundle.js
(function (graph) {
//require函数的本质是执行一个模块的代码,然后将相应变量挂载到exports对象上
function require(module) {
//localRequire的本质是拿到依赖包的exports变量
function localRequire(relativePath) {
return require(graph[module].dependencies[relativePath]);
}
var exports = {};
(function (require, exports, code) {
eval(code);
})(localRequire, exports, graph[module].code);
return exports;
//函数返回指向局部变量,形成闭包,exports变量在函数执行后不会被摧毁
}
require('./src/index.js');
})({
'./src/index.js': {
dependencies: {
'./info.js': './src/info.js',
'./base.js': './src/base.js',
},
code: '"use strict";\n\nvar _info = require("./info.js");\n\nvar _base = require("./base.js");\n\nvar result = (0, _base.sum)(_info.a, 5);\nconsole.log(result);',
},
'./src/info.js': {
dependencies: {},
code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports.a = void 0;\nvar a = 1;\nexports.a = a;',
},
'./src/base.js': {
dependencies: {},
code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports.sum = sum;\n\nfunction sum(a, b) {\n return a + b;\n}',
},
});
代码层级
|_dist:放置最后编译生成的最终文件
|__bundle.js:最终可执行文件
|_lib:放置编译的功能代码
|__exportBundle.js:输出指定名称和路径的打包文件
|__exportCode.js:输入一个文件入口路径,然后最终生成可执行文件
|__exportDependencies.js:解析模块依赖,转换代码 获取单个文件依赖
|__exportGraph.js 递归调用单个文件依赖功能,生成所有功能模块的依赖图库
|_node_modules:依赖库
|_src:放置测试打包的测试模块
|_index.js 测试的入口
|_package-lock.json
|_package.json
webpack打包流程概括
webpack的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:
- 初始化参
- 开始编译 用上一步得到的参数初始Compiler对象,加载所有配置的插件,通过执行对象的run方法开始执行编译
- 确定入口 根据配置中的 Entry 找出所有入口文件
- 编译模块 从入口文件出发,调用所有配置的 Loader 对模块进行编译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
- 完成模块编译 在经过第4步使用 Loader 翻译完所有模块后, 得到了每个模块被编译后的最终内容及它们之间的依赖关系
- 输出资源 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再将每个 Chunk 转换成一个单独的文件加入输出列表中,这是可以修改输出内容的最后机会
- 输出完成 在确定好输出内容后,根据配置确定输出的路径和文件名,将文件的内容写入文件系统中。
在以上过程中,
Webpack会在特定的时间点广播特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,井且插件可以调用Webpack提供的API改变Webpack的运行结果。其实以上7个步骤,可以简单归纳为初始化、编译、输出,三个过程,而这个过程其实就是前面说的基本模型的扩展。