0
点赞
收藏
分享

微信扫一扫

揭秘Webpack:从零实现一个简易JavaScript打包器


揭秘Webpack:从零实现一个简易JavaScript打包器

在现代前端开发中,我们习惯将代码拆分成多个模块,使用 ES6 的 import/export 语法来管理依赖。同时,项目中还可能包含 TypeScript、JSX、CSS 等浏览器无法直接识别的资源。浏览器本身并不支持这些模块化语法,尤其是老版本浏览器,无法直接运行这些代码。

因此,我们需要一个工具,将这些模块化的代码预处理、转换并打包成浏览器能直接执行的 JavaScript 文件。这个过程就是“打包”(bundling)。

本文将带你一步步实现一个简易的 JavaScript 模块打包器,核心依赖 @babel/parser@babel/traverse@babel/core,完成以下流程:

  • 读取入口文件内容
  • 解析代码生成 AST(抽象语法树)
  • 遍历 AST,分析模块依赖
  • 将 ES6+ 代码转换为兼容性更好的 ES5 代码
  • 递归收集所有依赖模块,构建依赖图
  • 生成可执行的打包代码(IIFE)
  • 输出最终结果

一、项目目标

我们要实现一个函数 bundle(entryFile),它能够:

  • 以指定入口文件为起点;
  • 解析所有 import 语句,递归查找依赖;
  • 将每个模块代码转换为 ES5 兼容语法;
  • 生成一段浏览器可直接执行的 JavaScript 代码;
  • 并将代码输出到指定文件。

这正是 Webpack 等打包工具的核心流程。

二、依赖安装

npm install @babel/parser @babel/traverse @babel/core @babel/preset-env

我们还会用到 Node.js 内置模块:

  • fs:文件读写
  • path:路径处理

三、核心代码实现

1. 获取单个模块信息

const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const babel = require('@babel/core');

function getModuleInfo(filename) {
  const content = fs.readFileSync(filename, 'utf-8');
  const ast = parser.parse(content, {
    sourceType: 'module'
  });

  const dependencies = {};

  traverse(ast, {
    ImportDeclaration({ node }) {
      const dirname = path.dirname(filename);
      const depPath = path.resolve(dirname, node.source.value);
      dependencies[node.source.value] = depPath;
    }
  });

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

  return {
    filename,
    dependencies,
    code
  };
}

说明:

  • 读取文件内容,生成 AST。
  • 遍历 AST,收集所有依赖模块的绝对路径。
  • 使用 Babel 将代码转换为 ES5 兼容代码。
  • 返回模块信息对象。

2. 构建依赖图

function buildDependencyGraph(entry) {
  const entryInfo = getModuleInfo(entry);
  const modules = [entryInfo];

  for (const module of modules) {
    const deps = module.dependencies;
    for (const relativePath in deps) {
      if (deps.hasOwnProperty(relativePath)) {
        const depModule = getModuleInfo(deps[relativePath]);
        modules.push(depModule);
      }
    }
  }

  const graph = {};
  modules.forEach(mod => {
    graph[mod.filename] = {
      dependencies: mod.dependencies,
      code: mod.code
    };
  });

  return graph;
}

说明:

  • 从入口模块开始,递归遍历所有依赖。
  • 将所有模块信息扁平化存储到数组。
  • 构建依赖图对象,键为模块绝对路径,值为依赖和代码。

3. 生成打包代码

function bundle(entry) {
  const graph = buildDependencyGraph(entry);
  const graphString = JSON.stringify(graph);

  return `(function(graph){
    function require(module){
      function localRequire(relativePath){
        return require(graph[module].dependencies[relativePath]);
      }
      var exports = {};
      (function(require, exports, code){
        eval(code);
      })(localRequire, exports, graph[module].code);
      return exports;
    }
    require('${entry}');
  })(${graphString})`;
}

说明:

  • 将依赖图序列化为字符串,嵌入到 IIFE 中。
  • 定义 require 函数模拟模块加载。
  • localRequire 根据相对路径加载依赖模块。
  • 使用 eval 执行模块代码,传入 requireexports
  • 启动入口模块执行。

四、完整示例

假设项目结构:

/project
  /src
    index.js
    message.js
  bundle.js

src/index.js

import message from './message.js';
console.log(message);

src/message.js

export default 'Hello from message module!';

bundle.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');

function getModuleInfo(filename) {
  const content = fs.readFileSync(filename, 'utf-8');
  const ast = parser.parse(content, { sourceType: 'module' });
  const dependencies = {};

  traverse(ast, {
    ImportDeclaration({ node }) {
      const dirname = path.dirname(filename);
      const depPath = path.resolve(dirname, node.source.value);
      dependencies[node.source.value] = depPath;
    }
  });

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

  return { filename, dependencies, code };
}

function buildDependencyGraph(entry) {
  const entryInfo = getModuleInfo(entry);
  const modules = [entryInfo];

  for (const module of modules) {
    const deps = module.dependencies;
    for (const relativePath in deps) {
      if (deps.hasOwnProperty(relativePath)) {
        const depModule = getModuleInfo(deps[relativePath]);
        modules.push(depModule);
      }
    }
  }

  const graph = {};
  modules.forEach(mod => {
    graph[mod.filename] = {
      dependencies: mod.dependencies,
      code: mod.code
    };
  });

  return graph;
}

function bundle(entry) {
  const graph = buildDependencyGraph(entry);
  const graphString = JSON.stringify(graph);

  return `(function(graph){
    function require(module){
      function localRequire(relativePath){
        return require(graph[module].dependencies[relativePath]);
      }
      var exports = {};
      (function(require, exports, code){
        eval(code);
      })(localRequire, exports, graph[module].code);
      return exports;
    }
    require('${entry}');
  })(${graphString})`;
}

// 生成打包代码并写入文件
const bundledCode = bundle(path.resolve(__dirname, './src/index.js'));
fs.writeFileSync(path.resolve(__dirname, 'dist/bundle.js'), bundledCode, 'utf-8');
console.log('打包完成,输出文件 dist/bundle.js');

五、总结

通过以上步骤,我们实现了一个简易的 JavaScript 模块打包器,核心流程包括:

  • 读取入口文件,解析 AST,提取依赖;
  • 递归构建依赖图;
  • 使用 Babel 转换代码兼容性;
  • 生成一个自执行函数,模拟模块加载机制;
  • 输出浏览器可执行的打包文件。

这正是 Webpack 等现代打包工具的基础原理。理解并实现它,有助于深入掌握前端构建流程和模块化机制。

如果你想进一步扩展,可以考虑:

  • 支持 CSS、图片等资源的打包;
  • 支持代码分割和懒加载;
  • 支持热更新(HMR);
  • 优化性能和缓存机制。


举报

相关推荐

0 条评论