90 行代码的 webpack,你确定不学吗?
只有三个文件的测试
新建目录 demo-abc。然后依次执行一下命令:
shell
# 初始化项目
pnpm init
# 下载依赖
# webpack-cli 让用户可以用命令行执行 webpack
# webpack 是核心包
pnpm i webpack-cli webpack然后新建三个文件:
src/index.js, src/a.js, src/b.js.
index.js 依赖 a.js。a.js 又依赖 b.js。
- index.js
js
const printA = require('./a')
printA()- a.js
js
const printB = require('./b')
module.exports = function printA() {
console.log('module a!')
printB()
}- b.js
js
module.exports = function printB() {
console.log('module b!')
}接着执行 webpack 命令,默认以 src/index.js 文件作为打包入口:
shell
npx webpack --mode development打包后的代码
js
(() => {
var __webpack_modules__ = ({
"./src/a.js":
((module, __unused_webpack_exports, __webpack_require__) => {
eval("const printB = __webpack_require__(/*! ./b */ \"./src/b.js\")\r\nmodule.exports = function printA() {\r\n console.log('module a!')\r\n printB()\r\n}\n\n//# sourceURL=webpack://demo-abc/./src/a.js?");
}),
"./src/b.js":
((module) => {
eval("module.exports = function printB() {\r\n console.log('module b!')\r\n}\n\n//# sourceURL=webpack://demo-abc/./src/b.js?");
}),
"./src/index.js":
((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
eval("const printA = __webpack_require__(/*! ./a */ \"./src/a.js\")\r\nprintA()\n\n//# sourceURL=webpack://demo-abc/./src/index.js?");
})
});
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
var module = __webpack_module_cache__[moduleId] = {
exports: {}
};
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
var __webpack_exports__ = __webpack_require__("./src/index.js");
})();分析打包后的代码
我们来简单分析一下:
js
(() => {
// 定义一个对象。故名思意,webpack 的所有模块。
var __webpack_modules__ = ({
"./src/a.js":
((module, __unused_webpack_exports, __webpack_require__) => {
eval("const printB = __webpack_require__(/*! ./b */ \"./src/b.js\")\r\nmodule.exports = function printA() {\r\n console.log('module a!')\r\n printB()\r\n}\n\n//# sourceURL=webpack://demo-abc/./src/a.js?");
}),
"./src/b.js":
((module) => {
eval("module.exports = function printB() {\r\n console.log('module b!')\r\n}\n\n//# sourceURL=webpack://demo-abc/./src/b.js?");
}),
"./src/index.js":
((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
eval("const printA = __webpack_require__(/*! ./a */ \"./src/a.js\")\r\nprintA()\n\n//# sourceURL=webpack://demo-abc/./src/index.js?");
})
});
// webpack 缓存模块对象
var __webpack_module_cache__ = {};
// require 函数实现
function __webpack_require__(moduleId) {
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
var module = __webpack_module_cache__[moduleId] = {
exports: {}
};
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
// moduleId 就是模块的路径
var __webpack_exports__ = __webpack_require__("./src/index.js");
})();首先执行
__webpack_require__("./src/index.js"),引入 './src/index.js'。webpack_require 内部会优先取缓存里的模块的 exports 属性。其次定义模块的 exports 属性,并设置缓存。
然后 webpack_require 执行模块内部的代码。内部代码可能又会递归执行
__webpack_require__("xxx.js")函数。最后 webpack_require 递归执行完后,返回 module.exports 属性。
手写实现
创建目录 90-webpack。下载依赖:
shell
pnpm i @babel/traverse @babel/parser @babel/core- @babel/parser 将源代码解析为 ast
- @babel/traverse 遍历并操作 ast
- @babel/core 将 ast 反向转换为源代码
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 root = process.cwd()
// 可以认为一个项目是有千千万万的模块组成的。模块可以项目依赖。
// 但是我们需要一个模块来作为入口。
// 首先根据入口模块的路径,递归解析整个模块树。
function buildModuleTree(entryPath) {
// 入口模块的模块信息
const entryModuleInfo = readModuleInfo(entryPath)
// 整个项目的所有的模块数组
const moduleArr = []
moduleArr.push(entryModuleInfo)
for (const module of moduleArr) {
module.deps.forEach(depPath => {
const moduleInfo = readModuleInfo(path.resolve(depPath))
moduleArr.push(moduleInfo)
})
}
return moduleArr
}
function readModuleInfo(filePath) {
// path.resolve:将路径解析为绝对路径
// path.relative(from, to)
// 从 root 目录到 filePath 目录或文件的相对路径。
// 比如 src 到 src/index.js,值就为 index.js
// 已相对路径作为模块的 id。
// ./src/index.js
filePath = './' + path.relative(root, path.resolve(filePath)).replace(/\\+/g, '/')
// 读入源码
const content = fs.readFileSync(filePath, 'utf-8')
// 将源码转换成 ast
const ast = parser.parse(content)
// 将 require 引入的模块依赖搜集到 deps 中。
// 有多少个 require 语句,依赖数组的长度就有多少个
const deps = []
// 遍历并操作 ast
traverse(ast, {
// 当遍历到调用表达式时,执行该属性函数
CallExpression: (babelPath) => {
const node = babelPath.node
// 如果是 require 语句,则搜集依赖
if (node.callee.name === 'require') {
// 改写 require 关键字
node.callee.name = '__require__'
// 拿到传入 require 函数的参数
// 比如 ./util/a
let moduleName = node.arguments[0].value
// 如果用户省略了扩展名,那么自动加上 .js 扩展名
// ./util/a.js
moduleName += path.extname(moduleName) ? '' : '.js'
// require 的模块路径
// ./src/util/a.js
moduleName = path.join(path.dirname(filePath), moduleName)
// 将引入的模块路径转化为以 './' 开头的相对路径
// ./src/util/a.js
moduleName = './' + path.relative(root, moduleName).replace(/\\+/g, '/')
deps.push(moduleName)
// 重新赋值依赖的路径
node.arguments[0].value = moduleName
}
}
})
// 将操作过后的 ast 转会源代码
const { code } = babel.transformFromAstSync(ast)
return {
filePath, // 模块路径
deps, // 模块引入的依赖
code // 模块的源代码
}
}
function pack(moduleArr, entry) {
moduleArr = moduleArr.map(module => `
// 接收三个参数
"${module.filePath}": function(module, exports, __require__) {
eval(\`${module.code}\`)
}
`)
const output = `;(() => {
var __webpack_modules__ = {
${moduleArr.join(',\n')}
}
var __webpack_module_cache__ = {}
function __webpack_require__(moduleId) {
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
var module = __webpack_module_cache__[moduleId] = {
exports: {}
};
// 传入三个参数
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
var __webpack_exports__ = __webpack_require__('${entry}');
})()`
return output
}
function main(entry = './src/index.js', output = './dist.js') {
// pack:根据 entry 读取出所有模块依赖后,再执行 entry 文件。
fs.writeFileSync(output, pack(buildModuleTree(entry), entry))
}
main()