Skip to content

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");
})();
  1. 首先执行 __webpack_require__("./src/index.js"),引入 './src/index.js'。

  2. webpack_require 内部会优先取缓存里的模块的 exports 属性。其次定义模块的 exports 属性,并设置缓存。

  3. 然后 webpack_require 执行模块内部的代码。内部代码可能又会递归执行 __webpack_require__("xxx.js") 函数。

  4. 最后 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()

Released under the MIT License.