JS打包的核心原理

引言

简单来说,前端打包就是把一个项目里所有的文件(HTML、CSS、JavaScript、图片、字体等)整理一下,把它们合并成一个或几个文件。为啥要这么做呢?因为浏览器加载文件的时候,文件越多,加载就越慢。打包后,浏览器加载的文件少了,网页就能更快地显示出来。

打包之后能带来以下几个好处:

  • 加载快:文件少了,浏览器加载起来就快。
  • 代码兼容:有些新特性浏览器不认识,打包工具会把它们转换成浏览器能理解的代码。
  • 代码简洁:去掉多余的空格和注释,文件体积变小,加载更快。

前端打包涉及到多种资源类型,例如 JS、CSS、图片、字体等,本文通过实现 minipack 打包工具介绍 JS 资源的核心处理原理。

项目说明

MiniPack 是一个极简版的 JavaScript 模块打包工具,它模拟 Webpack 的核心功能:依赖分析、代码转换、模块打包。通过这个项目,可以深入理解现代打包工具的工作原理。

项目结构

bash 复制代码
minipack/
├── package.json         
├── readme.md            
├── source/             # 源码文件夹(待打包的代码)
│   ├── main.js         # 入口文件
│   ├── plus.js         # 数学运算模块
│   └── minus.js        # 减法模块
└── src/                # 打包工具源码
    └── index.js        # 打包工具实现代码

源文件

入口文件

main.js 作为入口文件,里面涉及了几种常见场景:

  1. es module 模块导入;
  2. export default 内容导入;
  3. commonjs 模块导入;
javascript 复制代码
//----------main
import {plus,minusOne} from './plus.js';
import test from './test.js'
const{a} = require('./test2.js');

let initCount = 1;
let fiberArr = [1,1];

for(let i=2;i<20;i++){
  fiberArr[i] = plus(fiberArr[i-1],fiberArr[i-2]) 
}
console.log(fiberArr);

console.log(minusOne(10))

console.log(a)
console.log('test',test)

plus.js

plus.js 作为 main 的依赖模块,它自己也会导入其他模块。

javascript 复制代码
//--------plus
import minus from './minus.js';
let test = require('./test2.js');
const plus = function(num1,num2){
  return num1+num2
}

const plusOne = function(num){
  return num+1
}

const minusOne = function(num){
  return minus(num,1)
}

export {
  plus,
  plusOne,
  minusOne,
  test
}

minipack 打包实现

处理步骤:

1. 模块收集

读取文件内容,使用 Babel 解析成 AST(抽象语法树),遍历 AST 找到所有 import 和 require 语句。

对 AST 使用 babel 进行转换。

ini 复制代码
function createModuleGraph(filePath){
  let exist = moduledPath.filter(item=>item.filePath===filePath);
  if(exist.length)return {...exist[0],exist:true}
  
  const dep=[];
  const content = fs.readFileSync(filePath, 'utf-8');
  const ast = babelParser.parse(content,{
    sourceType:'module'
  });
  
  // 遍历AST,找到所有依赖
  traverse.default(ast,{
    ImportDeclaration:function({node}){
      dep.push(node.source.value)  // 收集 import 依赖
    },
    CallExpression ({ node }) {
      if (node.callee.name === 'require') {
        dep.push(node.arguments[0].value);  // 收集 require 依赖
      }
    }
  })
  
  // 分配模块ID并转换代码
  let moduleId = id++;
  const { code } = transformFromAst(ast, null, {
    presets: ['@babel/env']
  });

  return {
    id:moduleId,
    filePath,
    dependencies:dep,
    code
  }
}

2. 依赖关系映射

从入口文件开始,广度优先遍历所有依赖,为每个模块建立 相对路径 → 模块ID 的映射关系

避免重复处理同一个文件。

ini 复制代码
let mainGraph = createModuleGraph('../source/main.js');
let modules = [mainGraph];
for(let graph of modules){
  graph.map = {};
  const dirname = path.dirname(graph.filePath);
  graph.dependencies.forEach(rePath=>{
    let absPath = path.join(dirname,rePath);
    let m = createModuleGraph(absPath);
    graph.map[rePath] = m.id;  // 建立路径到ID的映射
    if(!("exist" in m)){
      modules.push(m);
    }
  })
}

3. Bundle 生成

生成编译之后的文件。

java 复制代码
function createBundle(modules){
  return `(function(modules){
    const installedModules = {};
    
    function require(moduleId){
      // 模块缓存检查
      if(installedModules[moduleId]){
        return installedModules[moduleId].exports
      }
      
      // 创建模块对象
      var module = (installedModules[moduleId] = {
        i: moduleId,
        l: false,
        exports: {}
      })

      const [fn,map] = modules[moduleId]
      
      // 本地require函数,处理相对路径
      function localRequire(name){
        return require(map[name])
      }

      // 执行模块代码
      fn.call(module.exports, module, module.exports, localRequire)
      
      return module.exports
    }
    
    require(0)  // 启动入口模块
  })({${modules.map(item=>{
    return (`"${item.id}":[${wrapSource(item.code)},${util.inspect(item.map)}]`)
  })}})`
}

核心API介绍

babelParser.parse

babelParser.parse'@babel/parser' 中的API,用来将代码转化为 AST。

参数说明:

  • code: 字符串形式的 JavaScript 代码
  • options: 解析选项配置
    • sourceType: 解析模式,可选值 'module' | 'script' | 'unambiguous',使用 es6 的 import/export 语法时,应设为 module
    • plugins: 启用的插件。
    • 其他配置项参考官网 babel.dev/docs/babel-...

转换示例:

arduino 复制代码
// 输入代码
const code = `
import { sum } from './math.js';
const result = sum(1, 2);
export default result;
`;

// 解析为 AST
const ast = babelParser.parse(code, {
  sourceType: 'module'
});

// AST 结构(简化版)
{
  type: "File",
  program: {
    type: "Program",
    sourceType: "module",
    body: [
      {
        type: "ImportDeclaration",
        specifiers: [...],
        source: {
          type: "StringLiteral",
          value: "./math.js"
        }
      },
      // ... 其他节点
    ]
  }
}

traverse AST 遍历器

@babel/traverse 中的 traverse 提供了遍历和修改 AST 的能力,是 Babel 转换的核心工具。

php 复制代码
const { code } = transformFromAst(ast, null, {
    presets: ['@babel/env']
});

参数说明:

  • ast: 抽象语法树
  • source: 原始源码(可选,用于生成 source map)
  • options: babel 配置项
相关推荐
Mintopia2 分钟前
Next.js 全栈:接收和处理请求
前端·javascript·next.js
袁煦丞35 分钟前
2025.8.18实验室【代码跑酷指南】Jupyter Notebook程序员的魔法本:cpolar内网穿透实验室第622个成功挑战
前端·程序员·远程工作
Joker Zxc40 分钟前
【前端基础】flex布局中使用`justify-content`后,最后一行的布局问题
前端·css
无奈何杨43 分钟前
风控系统事件分析中心,关联关系、排行、时间分布
前端·后端
Moment1 小时前
nginx 如何配置防止慢速攻击 🤔🤔🤔
前端·后端·nginx
晓得迷路了1 小时前
栗子前端技术周刊第 94 期 - React Native 0.81、jQuery 4.0.0 RC1、Bun v1.2.20...
前端·javascript·react.js
前端小巷子1 小时前
Vue 自定义指令
前端·vue.js·面试
玲小珑1 小时前
Next.js 教程系列(二十七)React Server Components (RSC) 与未来趋势
前端·next.js
Mike_jia1 小时前
UptimeRobot API状态监控:零成本打造企业级业务健康看板
前端