Skip to content

生成打包结果

这一节将转换后的代码渲染到模板中,然后生成打包后的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

image-20220204171149699

发现,打包结果已经输出到指定目录下了

打包结果

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';`);
    }), 
        
    });

我们直接可以运行这个文件的,发现好使,尝试在浏览器上运行,也正常输出结果,所以打包工具编写成功。

总结

本节做了下面几件事

  1. 定义模板代码,使用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]%>`);
        }), 
            <%}%>
        });
  2. 读取到输出路径,生成打包文件

    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