生成打包结果
这一节将转换后的代码渲染到模板中,然后生成打包后的js文件。
安装ejs
为了方便,使用ejs来渲染模板。
shell
yarn add ejs
看一下现在的package.json
json
{
"name": "cheny-pack",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"bin": {
"cheny-pack": "./bin/cheny-pack.js"
},
"dependencies": {
"@babel/generator": "^7.17.0",
"@babel/traverse": "^7.17.0",
"@babel/types": "^7.17.0",
"babylon": "^6.18.0",
"ejs": "^3.1.6"
}
}
看一下之前webpack生成的模板代码
js
(function (modules) { // webpackBootstrap
// The module cache
var installedModules = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// Execute the module function
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// Flag the module as loaded
module.l = true;
// Return the exports of the module
return module.exports;
}
// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
({
"./src/a.js":
(function (module, exports, __webpack_require__) {
eval("// webpack\\webpack-dev-3\\src\\a.js\r\nlet b = __webpack_require__(/*! ./base/b.js */ \"./src/base/b.js\")\r\nmodule.exports = 'aaa' + b // aaabbb\n\n//# sourceURL=webpack:///./src/a.js?");
}),
"./src/base/b.js":
(function (module, exports) {
eval("// webpack\\webpack-dev-3\\src\\base\\b.js\r\nmodule.exports = 'bbb'\n\n//# sourceURL=webpack:///./src/base/b.js?");
}),
"./src/index.js":
(function (module, exports, __webpack_require__) {
eval("// webpack\\webpack-dev-3\\src\\index.js\r\nlet str = __webpack_require__(/*! ./a.js */ \"./src/a.js\")\r\nconsole.log(str); // aaabbb\n\n//# sourceURL=webpack:///./src/index.js?");
})
});
新建模板
根据上面模板代码,配合ejs渲染打包后的代码
模板代码
js
// webpack\cheny-pack\lib\main.ejs
(function (modules) { // webpackBootstrap
// The module cache
var installedModules = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// Execute the module function
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// Flag the module as loaded
module.l = true;
// Return the exports of the module
return module.exports;
}
// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = "<%-entryId%>");
})
({
<%for (let key in modules) {%>
"<%-key%>":
(function (module, exports, __webpack_require__) {
eval(`<%-modules[key]%>`);
}),
<%}%>
});
本节全部代码
js
// webpack\cheny-pack\lib\Complier.js
let path = require('path')
let fs = require('fs')
let babylon = require('babylon') // 把源码转换为ast语法树
let t = require('@babel/types') // 生成ast节点,可以生成各种类型
let traverse = require('@babel/traverse').default // 遍历ast语法树,替换节点
let generator = require('@babel/generator').default // 根据ast语法树,生成源代码
let ejs = require('ejs') // 模板引擎
class Complier {
constructor(config) {
// 将配置文件挂载到实例上,所有实例都能拿到了
// webpack.config.js
this.config = config
// 1. 保存入口文件的路径
this.entryId; // './src/index.js'
// 2. 需要保存所有模块的依赖
// 解析文件的依赖,变成webpack打包传递的参数,key value的形式
// key 是路径名,value就是模块代码
this.modules = {}
this.entry = config.entry // 入口路径
this.root = process.cwd() // 工作路径 运行 npx cheny-pack 时候的路径
}
run() {
// 执行,并且创建模块的依赖关系
// 从入口开始执行
this.buildModule(path.resolve(this.root, this.entry), true) // true 表示解析的是主模块
console.log(this.modules, this.entryId);
// 发射一个文件 打包后的文件
this.emitFile()
}
// 创建模块的依赖关系
buildModule(modulePath, isEntry) {
// 模块的key是相对路径 value是模块中的代码,不过需要做一些替换
// require都变成了webpack_require等
let source = this.getSource(modulePath) // 模块内容
// 模块id是一个相对路径 总路径 减去 rootPath
// 总路径modulePath F:\code\note_code\webpack\webpack-dev-3\src\index.js
// 工作路径this.root F:\code\note_code\webpack\webpack-dev-3
let moduleName = './' + path.relative(this.root, modulePath) // ./src/index.js 模块id
if (isEntry) { // 如果是主入口的话,记录一下主入口的id
this.entryId = moduleName // 保存入口的名字
}
// path.dirname(moduleName)获取父路径 ./src/index.js -> ./src
// 解析模块代码,替换相应的内容
let { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName))
// 把相对路径和模块中的内容对应起来
this.modules[moduleName] = sourceCode
// 在这里需要递归继续解析依赖项
dependencies.forEach(dep => { // 加载附模块
// 参数是一个绝对路径
this.buildModule(path.join(this.root, dep), false) // false 表示不是主入口文件
})
}
// 解析文件
// 需要把source源码进行改造 返回一个依赖列表
// 因为每一个模块里面还可能会引另外的模块,所以需要一个依赖列表
parse(source, parentPath) { // AST 解析语法树
let ast = babylon.parse(source) // 源码先解析成ast语法树
// https://astexplorer.net/
let dependencies = [] // 存放当源代码依赖的数组,即当前文件引入的第三方模块
// 遍历ast语法树
traverse(ast, {
CallExpression(p) { // 调用表达式,比如 a() require() ...
let node = p.node // 拿到对应的节点
// 如果是require 就改为 __webpack_require__
if (node.callee.name === 'require') {
node.callee.name = '__webpack_require__'
let moduleName = node.arguments[0].value // 取到的就是模块的引用名字 let a = require('./a') -> a
// 有时候我们引用的时候,没有带后缀,我们先把后缀加上
// path.extname('./a') === '' path.extname('./a.js') === '.js'
moduleName = moduleName + (path.extname(moduleName) ? '' : '.js') // ./a.js
// webpack的模板所有的路径都需要加上 ./src 所以需要把 ./a.js 变成 ./src/a.js
moduleName = './' + path.join(parentPath, moduleName) // ./src\a.js
// 把依赖的第三方模块 塞进依赖数组中
dependencies.push(moduleName)
// 替换原来的ast节点
node.arguments = [t.stringLiteral(moduleName)]
}
}
})
// 生成新的代码
let sourceCode = generator(ast).code
return { sourceCode, dependencies }
}
// 发射文件
emitFile() {
// 用转换后的数据,渲染模板
// 拿到输出路径
let main = path.join(this.config.output.path, this.config.output.filename)
// 获取到模板资源
let templateStr = this.getSource(path.join(__dirname, 'main.ejs'))
// 根据处理过的模块 渲染新代码
let code = ejs.render(templateStr, {
entryId: this.entryId,
modules: this.modules
})
// 如果是多入口打包,所以声明一个assets
this.assets = {}
this.assets[main] = code
// 生成打包结果
this.writeFile(main, this.assets[main])
}
// 生成打包结果
writeFile(filePath, code) {
// filePath: F:\code\note_code\webpack\webpack-dev-3\dist\bundle.js
// 先判断文件夹是否存在,不存在的话先创建文件夹
// path.dirname(filePath) F:\code\note_code\webpack\webpack-dev-3\dist
let outputFolder = path.dirname(filePath)
if (!fs.existsSync(outputFolder)) {
fs.mkdirSync(outputFolder) // 没有文件夹先创建文件夹
}
// 输出打包文件
fs.writeFileSync(filePath, code)
}
// 获取模块代码
getSource(modulePath) {
return fs.readFileSync(modulePath, 'utf8')
}
}
module.exports = Complier
然后在需要打包的项目中,运行我们自定义的打包指令 npx cheny-pack
发现,打包结果已经输出到指定目录下了
打包结果
js
// webpack\cheny-pack\lib\main.ejs
(function (modules) { // webpackBootstrap
// The module cache
var installedModules = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// Execute the module function
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// Flag the module as loaded
module.l = true;
// Return the exports of the module
return module.exports;
}
// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = "./src\index.js");
})
({
"./src\index.js":
(function (module, exports, __webpack_require__) {
eval(`// webpack\webpack-dev-3\src\index.js
let str = __webpack_require__("./src\\a.js");
console.log(str); // aaabbb`);
}),
"./src\a.js":
(function (module, exports, __webpack_require__) {
eval(`// webpack\webpack-dev-3\src\a.js
let b = __webpack_require__("./src\\base\\b.js");
module.exports = 'aaa' + b; // aaabbb`);
}),
"./src\base\b.js":
(function (module, exports, __webpack_require__) {
eval(`// webpack\webpack-dev-3\src\base\b.js
module.exports = 'bbb';`);
}),
});
我们直接可以运行这个文件的,发现好使,尝试在浏览器上运行,也正常输出结果,所以打包工具编写成功。
总结
本节做了下面几件事
定义模板代码,使用ejs,我们只需要在需要更换内容的地方将代码替换。
js// Load entry module and return exports return __webpack_require__(__webpack_require__.s = "<%-entryId%>"); // 这里的一个入口,需要替换 }) ({ <%for (let key in modules) {%> // 这里渲染各个依赖关系,我们已经处理好了 "<%-key%>": (function (module, exports, __webpack_require__) { eval(`<%-modules[key]%>`); }), <%}%> });
读取到输出路径,生成打包文件
js// 发射文件 emitFile() { // 用转换后的数据,渲染模板 // 拿到输出路径 let main = path.join(this.config.output.path, this.config.output.filename) // 获取到模板资源 let templateStr = this.getSource(path.join(__dirname, 'main.ejs')) // 根据处理过的模块 渲染新代码 let code = ejs.render(templateStr, { entryId: this.entryId, modules: this.modules }) // 如果是多入口打包,所以声明一个assets this.assets = {} this.assets[main] = code // 生成打包结果 this.writeFile(main, this.assets[main]) } // 生成打包结果 writeFile(filePath, code) { // filePath: F:\code\note_code\webpack\webpack-dev-3\dist\bundle.js // 先判断文件夹是否存在,不存在的话先创建文件夹 // path.dirname(filePath) F:\code\note_code\webpack\webpack-dev-3\dist let outputFolder = path.dirname(filePath) if (!fs.existsSync(outputFolder)) { fs.mkdirSync(outputFolder) // 没有文件夹先创建文件夹 } // 输出打包文件 fs.writeFileSync(filePath, code) }
参考
https://www.bilibili.com/video/BV1a4411e7Bz?p=37&spm_id_from=pageDriver