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 配置项
相关推荐
zhangxingchao14 分钟前
Jetpack Compose 之 Modifier(中)
前端
JarvanMo14 分钟前
理解 Flutter 中 GoRouter 的context.push与context.go
前端
pe7er20 分钟前
使用 Vue 官方脚手架创建项目时遇到 Node 18 报错问题的排查与解决
前端·javascript·vue.js
星始流年23 分钟前
前端视角下认识AI Agent
前端·agent·ai编程
pe7er27 分钟前
使用 types / typings 实现全局 TypeScript 类型定义,无需 import/export
前端·javascript·vue.js
CH_Qing28 分钟前
【udev】关于/dev 设备节点的生成 &udev
linux·前端·网络
小诸葛的博客32 分钟前
gin如何返回html
前端·html·gin
islandzzzz39 分钟前
(第二篇)HMTL+CSS+JS-新手小白循序渐进案例入门
前端·javascript·css·html
喝拿铁写前端41 分钟前
前端实战优化:在中后台系统中用语义化映射替代 if-else,告别魔法数字的心智负担
前端·javascript·架构
超人不会飛1 小时前
就着HTTP聊聊SSE的前世今生
前端·javascript·http