Skip to content
字数
885 字
阅读时间
5 分钟
js
const fs = require('fs')
const path = require('path')

// 负责 code -> ast
const { parse } = require('@babel/parser')
// 负责 ast -> ast
const traverse = require('@babel/traverse').default
// 负责 ast -> code
const generate = require('@babel/generator').default

let moduleId = 0
// 该函数用以解析该文件模块的所有依赖树。对所有目标代码根据 AST 构建为组件树的结构并添加 ID,ID 如果 webpack 一样为深度优先自增。数据结构为:
//
// const rootModule = {
//   id: 0,
//   filename: '/Documents/app/node_modules/hello/index.js',
//   deps: [ moduleA, moduleB ],
//   code: 'const a = 3; module.exports = 3',
// }
//
// 如果组件 A 依赖于组件B和组件C
//
// {
//   id: 0,
//   filename: A,
//   deps: [
//     { id: 1, filename: B, deps: [] },
//     { id: 2, filename: C, deps: [] },
//   ]
// }
function buildModule (filename) {
  // 如果入口位置为相对路径,则根据此时的 __dirname 生成绝对文件路径
  filename = path.resolve(__dirname, filename)

  // 同步读取文件,并使用 utf8 读做字符串
  const code = fs.readFileSync(filename, 'utf8')

  // 使用 babel 解析源码为 AST
  const ast = parse(code, {
    sourceType: 'module'
  })

  const deps = []
  const currentModuleId = moduleId

  traverse(ast, {
    enter({ node }) {
      // 根据 AST 定位到所有的 require 函数,寻找出所有的依赖
      if (node.type === 'CallExpression' && node.callee.name === 'require') {
        const argument = node.arguments[0]

        // 找到依赖的模块名称
        // require('lodash') -> lodash (argument.value)
        if (argument.type === 'StringLiteral') {

          // 深度优先搜索,当寻找到一个依赖时,则 moduleId 自增一
          // 并深度递归进入该模块,解析该模块的模块依赖树
          moduleId++;
          const nextFilename = path.join(path.dirname(filename), argument.value)

          // 如果 lodash 的 moduleId 为 3 的话
          // require('lodash') -> require(3)
          argument.value = moduleId
          deps.push(buildModule(nextFilename))
        }
      }
    }
  })
  return {
    filename,
    deps,
    code: generate(ast).code,
    id: currentModuleId
  }
}

// 把模块依赖由树结构更改为数组结构,方便更快的索引
//
// {
//   id: 0,
//   filename: A,
//   deps: [
//     { id: 1, filename: B, deps: [] },
//     { id: 2, filename: C, deps: [] },
//   ]
// }
// ====> 该函数把数据结构由以上转为以下
// [
//   { id: 0, filename: A }
//   { id: 1, filename: B }
//   { id: 2, filename: C }
// ]
function moduleTreeToQueue (moduleTree) {
  const { deps, ...module } = moduleTree

  const moduleQueue = deps.reduce((acc, m) => {
    return acc.concat(moduleTreeToQueue(m))
  }, [module])

  return moduleQueue
}

// 构建一个浏览器端中虚假的 Commonjs Wrapper
// 注入 exports、require、module 等全局变量,注意这里的顺序与 CommonJS 保持一致,但与 webpack 不一致,但影响不大
// 在 webpack 中,这里的 code 需要使用 webpack loader 进行处理
function createModuleWrapper (code) {
  return `
  (function(exports, require, module) {
    ${code}
  })`
}

// 根据入口文件进行打包,也是 mini-webpack 的入口函数
function createBundleTemplate (entry) {
  // 如同 webpack 中的 __webpack_modules__,以数组的形式存储项目所有依赖的模块
  const moduleTree = buildModule(entry)
  const modules = moduleTreeToQueue(moduleTree)

  // 生成打包的模板,也就是打包的真正过程
  return `
// 统一扔到块级作用域中,避免污染全局变量
// 为了方便,这里使用 {},而不用 IIFE
//
// 以下代码为打包的三个重要步骤:
// 1. 构建 modules
// 2. 构建 webpackRequire,加载模块,模拟 CommonJS 中的 require
// 3. 运行入口函数
{
  // 1. 构建 modules
  const modules = [
    ${modules.map(m => createModuleWrapper(m.code))}
  ]

  // 模块缓存,所有模块都仅仅会加载并执行一次
  const cacheModules = {}

  // 2. 加载模块,模拟代码中的 require 函数
  // 打包后,实际上根据模块的 ID 加载,并对 module.exports 进行缓存
  function webpackRequire (moduleId) {
    const cachedModule = cacheModules[moduleId]
    if (cachedModule) {
      return cachedModule.exports
    }
    const targetModule = { exports: {} }
    modules[moduleId](targetModule.exports, webpackRequire, targetModule)
    cacheModules[moduleId] = targetModule
    return targetModule.exports
  }

  // 3. 运行入口函数
  webpackRequire(0)
}
`
}

module.exports = createBundleTemplate

贡献者

jiechen

文件历史