什么是webpack?
我们先来看看webpack官方的定义:
本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。
相信这个定义已经说的非常清楚了。首先,它的本质是一个模块打包器,其工作是将每个模块打包成相应的bundle。那么在这中间究竟做了什么事情呢?
场景引入
现在有以下文件
// word.js
export const word = 'hello'
// message.js
import {word} from './word.js'
const message = `say ${word}`
export default message
// index.js
import message from './message.js'
console.log(message)
请编写一个bundler.js,将其中的ES6代码转换为ES5代码,并将这些文件打包,生成一段能在浏览器正确运行起来的代码。(最后输出say hello)
我们将需求进行拆解成下面的三个步骤
- 利用bebel完成代码转换,并生成单个文件的依赖。
- 生成依赖图谱
- 生成最后的打包代码
第一步:转换代码、生成依赖
//先安装好相应的包
npm install @babel/parser @babel/traverse @babel/core @babel/preset-env -D
转换代码需要利用@bebel/parser生成AST抽象语法树,然后利用@babel/traverse进行AST遍历,记录依赖关系,最后通过@babel/core和@babel/preset-env进行代码的转换。
function stepOne(filename) {
// 读取文件
const content = fs.readFileSync(path.resolve(__dirname, filename), "utf8");
// 解析文件成ast
const ast = parser.parse(content, {
sourceType: "module",
});
const dependencies = {};
// 遍历ast 语法树
traverse(ast, {
// 获取通过 import 导入的模块
ImportDeclaration({ node }) {
// 返回类似于unix目录的命令
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 // 转换后的代码
}
}
第二步:生成依赖图谱
// entry 为入口文件
function stepTwo(entry) {
const entryModule = stepOne(entry);
//
const graphArray = [entryModule]
for (let i = 0; i < graphArray.length; i++) {
const item = graphArray[i]
const { dependencies } = item // 拿到文件依赖的模块集合
for (let j in dependencies) {
const dep = stepOne(dependencies[j])
graphArray.push(dep) // 目的是让入口模块及其所有相关的模块放入数组
}
}
// 接下来生成依赖图
const graph = {}
graphArray.forEach(item => {
graph[item.filename] = {
dependencies: item.dependencies,
code: item.code
}
})
return graph
}
一边遍历依赖数组,一边根据最开始入口的依赖信息,分析出更深层的依赖文件,并调用第一步的函数生成依赖,并把其依次加入到依赖数组中。
最终遍历生成的这个数组,为了展示他们之间的关系,我们使用map来存储, 生成最终的依赖关系图。
{
fileName: 'index.js': {
dependencies: {'message.js': 'message.js'},
code: '转换后的代码' // 使用 babel.code 和 babel/preset-env转换之后的代码。
}
}
//测试一下 console.log(stepTwo('./src/index.js'))
// 结果如下
{
"index.js": {
dependencies: {
"./message.js": "./message.js",
},
code: "\"use strict\";\n\nvar _message = _interopRequireDefault(require(\"./message.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log(_message[\"default\"]);",
},
"./message.js": {
dependencies: {
"./word.js": "./word.js",
},
code: "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports[\"default\"] = void 0;\n\nvar _word = require(\"./word.js\");\n\nvar message = \"say \".concat(_word.word);\nvar _default = message;\nexports[\"default\"] = _default;",
},
"./word.js": {
dependencies: {
},
code: "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.word = void 0;\nvar word = \"word\";\nexports.word = word;",
},
}
第三步:生成代码字符串
// 第三步 生成代码字符串
// 到第三步,我们就需要了解一下 require 函数的实现了
function step3(entry) {
//要先把对象转换为字符串,不然在下面的模板字符串中会默认调取对象的toString方法,参数变成[Object object],显然不行
const graph = JSON.stringify(stepTwo(entry))
return `
(function(graph) {
// require 函数的本质是执行一个模块的代码,然后将相应的变量挂载到exports对象上
function require(module) {
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})
`
}
let code = step3("index.js");
console.log(code);
我们得到最终的代码字符串在控制台中执行, 便可以输出 say hello 的文本,
这篇文章中理解的不太深的地方就是 eval(code) 这里就是执行一段代码,这段代码里面会把 相应的变量挂载到 exports 对象上。