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 配置项
相关推荐
Carlos_sam17 分钟前
Opnelayers:ol-wind之Field 类属性和方法详解
前端·javascript
小毛驴85028 分钟前
创建 Vue 项目的 4 种主流方式
前端·javascript·vue.js
誰能久伴不乏1 小时前
Linux如何执行系统调用及高效执行系统调用:深入浅出的解析
java·服务器·前端
涔溪2 小时前
响应式前端设计:CSS 自适应布局与字体大小的最佳实践
前端·css
今禾2 小时前
前端开发中的Mock技术:深入理解vite-plugin-mock
前端·react.js·vite
你这个年龄怎么睡得着的2 小时前
Babel AST 魔法:Vite 插件如何让你的 try...catch 不再“裸奔”?
前端·javascript·vite
我想说一句2 小时前
掘金移动端React开发实践:从布局到样式优化的完整指南
前端·react.js·前端框架
jqq6662 小时前
Vue3脚手架实现(九、渲染typescript配置)
前端
码间舞2 小时前
Zustand 与 useSyncExternalStore:现代 React 状态管理的极简之道
前端·react.js
Dream耀2 小时前
提升React移动端开发效率:Vant组件库
前端·javascript·前端框架