代码导向:如何实现mini-webpack

85 阅读3分钟

🚀 1. 整体目标

构建一个简单的模块打包器,支持:

  • 入口文件分析
  • 依赖分析(import
  • 转换 ES6+ 为浏览器可识别代码(使用 Babel)
  • 输出一个 bundle.js

🛠️ 实现步骤详解

第一步:准备基本目录结构

bash
复制编辑
mini-webpack/
├── src/
│   └── index.js         // 入口文件
├── dist/
├── mini-webpack.js      // 你的打包器
└── package.json

第二步:安装依赖

bash
复制编辑
npm init -y
npm install @babel/parser @babel/traverse @babel/core @babel/preset-env

第三步:实现核心逻辑(mini-webpack.js)

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')

let id = 0

function createAsset(filename) {
  const content = fs.readFileSync(filename, 'utf-8')

  const ast = parser.parse(content, {
    sourceType: 'module',
  })

  const dependencies = []
  traverse(ast, {
    ImportDeclaration: ({ node }) => {
      dependencies.push(node.source.value)
    },
  })

  const { code } = babel.transformFromAstSync(ast, content, {
    presets: ['@babel/preset-env'],
  })

  return {
    id: id++,
    filename,
    dependencies,
    code,
  }
}

第四步:构建依赖图

js
复制编辑
function createGraph(entry) {
  const mainAsset = createAsset(entry)
  const queue = [mainAsset]

  for (const asset of queue) {
    const dirname = path.dirname(asset.filename)

    asset.mapping = {}

    asset.dependencies.forEach((relativePath) => {
      const absolutePath = path.join(dirname, relativePath)
      const child = createAsset(absolutePath)
      asset.mapping[relativePath] = child.id
      queue.push(child)
    })
  }

  return queue
}

第五步:生成最终打包文件

js
复制编辑
function bundle(graph) {
  let modules = ''

  graph.forEach((mod) => {
    modules += `
      ${mod.id}: [
        function(require, module, exports) {
          ${mod.code}
        },
        ${JSON.stringify(mod.mapping)},
      ],
    `
  })

  const result = `
    (function(modules){
      function require(id){
        const [fn, mapping] = modules[id]

        function localRequire(name){
          return require(mapping[name])
        }

        const module = { exports: {} }

        fn(localRequire, module, module.exports)

        return module.exports
      }

      require(0)
    })({${modules}})
  `

  fs.writeFileSync('./dist/bundle.js', result, 'utf-8')
}

第六步:入口执行

js
复制编辑
const graph = createGraph('./src/index.js')
bundle(graph)

运行:

bash
复制编辑
node mini-webpack.js

会生成 dist/bundle.js,可在 HTML 中通过 <script> 引入。


✅ 示例输入文件(src/index.js)

js
复制编辑
import message from './message.js'
console.log(message)
js
复制编辑
// message.js
export default 'Hello from mini-webpack!'

📦 打包结果解析(dist/bundle.js)

会包含一个简化版的模块系统:

  • 所有模块代码都被包在一个对象中(以 ID 为键)
  • 每个模块使用 function(require, module, exports) 形式封装
  • 自己实现了一个简化的 require 函数

总结:Webpack 核心原理

模块mini-webpack 实现方式
入口分析createAsset()
依赖收集AST 语法分析 + traverse
模块图构建createGraph()
模块打包bundle() + 自定义 require


✅ 可选扩展功能(按需添加)

  • 支持 CommonJS(require
  • CSS 打包
  • 图片/静态资源打包
  • 支持热更新(HMR)
  • 支持插件系统(类似 Webpack 插件)
  • 支持 Loader(编译 Sass、TS 等)

✅ 1. 支持 CommonJS(require

🔍 原理:

默认我们支持的是 import,如果要兼容 require,需要在 AST 遍历中也识别 CallExpression 类型的 require

✅ 改动:

js
复制编辑
traverse(ast, {
  ImportDeclaration({ node }) {
    dependencies.push(node.source.value)
  },
  CallExpression({ node }) {
    if (node.callee.name === 'require') {
      dependencies.push(node.arguments[0].value)
    }
  }
})

✅ 2. CSS 打包

🔍 原理:

遇到 .css 文件,不能转 AST,而是作为文本内容读入并注入页面

✅ 实现思路:

createAsset 中加入判断:

js
复制编辑
if (filename.endsWith('.css')) {
  const content = fs.readFileSync(filename, 'utf-8')
  return {
    id: id++,
    filename,
    dependencies: [],
    code: `
      const style = document.createElement('style');
      style.innerText = ${JSON.stringify(content)};
      document.head.appendChild(style);
    `,
  }
}

这样在运行 bundle 时,CSS 会注入 <style> 标签。


✅ 3. 图片/静态资源打包

🔍 原理:

遇到 .png.jpg 等文件,复制到 dist 目录,同时输出资源路径。

✅ 实现方式:

js
复制编辑
if (/.(png|jpg|jpeg|gif)$/.test(filename)) {
  const targetPath = path.join('./dist', path.basename(filename))
  fs.copyFileSync(filename, targetPath)
  return {
    id: id++,
    filename,
    dependencies: [],
    code: `module.exports = "./${path.basename(filename)}";`,
  }
}

✅ 4. 支持 HMR(Hot Module Replacement)

🔍 原理:

HMR 实现比较复杂,需要:

  • devServer(本地服务器,监听文件变化)
  • 使用 WebSocket 推送变更
  • 模块热替换逻辑

✅ 简化版方案:

  • 使用 chokidar 监听文件变化
  • 手动重新执行 bundle() 生成文件
  • 浏览器通过 EventSource(或 WebSocket)感知变化并 location.reload()

✅ 推荐结构:

bash
复制编辑
mini-webpack/
├── mini-webpack.js
├── dev-server.js    <-- 监听文件变更
└── hot-reload.js    <-- 客户端 JS 自动 reload

(如果你想实现这部分,我可以给你写一个最小 HMR demo)


✅ 5. 插件系统(模拟 Webpack 插件)

🔍 原理:

暴露生命周期钩子供插件注册,例如:beforeEmit, afterEmit

✅ 简易实现:

js
复制编辑
class Compiler {
  constructor(options) {
    this.options = options
    this.hooks = {
      beforeEmit: [],
      afterEmit: [],
    }
  }

  applyPlugins(hookName, args) {
    this.hooks[hookName].forEach(fn => fn(args))
  }

  run() {
    this.applyPlugins('beforeEmit')
    bundle()
    this.applyPlugins('afterEmit')
  }

  use(plugin) {
    plugin.apply(this)
  }
}

插件结构:

js
复制编辑
class MyPlugin {
  apply(compiler) {
    compiler.hooks.beforeEmit.push(() => {
      console.log('📦 构建即将开始')
    })
  }
}

✅ 6. 支持 Loader(例如处理 Sass、TS)

🔍 原理:

在模块加载前调用自定义函数对源码进行转换。

✅ Loader 机制:

js
复制编辑
function runLoaders(loaders, content) {
  return loaders.reduceRight((code, loader) => loader(code), content)
}
js
复制编辑
const cssLoader = (code) => `const style = document.createElement('style'); style.innerText = ${JSON.stringify(code)}; document.head.appendChild(style);`

const tsLoader = (code) => require('typescript').transpile(code)

加入到 createAsset() 中:

js
复制编辑
let source = fs.readFileSync(filename, 'utf-8')
const ext = path.extname(filename)

if (ext === '.ts') {
  source = runLoaders([tsLoader], source)
}
// Babel 转换再继续

你也可以从 webpack.config.js 中配置 loader 路径数组来动态加载。


🎁 Bonus:loader + plugin 的完整设计

功能建议实现方法
LoaderrunLoaders 按规则匹配后缀
Plugin生命周期 hooks + apply()
HMRdevServer + WebSocket + 客户端 reload.js
静态资源识别扩展名 + copy
CSS注入 style 标签
CommonJSAST 分析 CallExpression
TS/SASSloader 转换再走 Babel

✅ 总结

你可以像这样逐步增强 mini-webpack

text
复制编辑
🛠 最小核心:依赖图 + 代码合并
➕ 支持语法:import + require + Babel 转换
🎨 支持资源:CSS + 图片静态资源
🔥 开发优化:热更新 HMR
🧩 插件机制:hooks + apply
🎛 Loader 系统:loader 队列处理源码