引言
简单来说,前端打包就是把一个项目里所有的文件(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 作为入口文件,里面涉及了几种常见场景:
- es module 模块导入;
- export default 内容导入;
- 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-...
- sourceType: 解析模式,可选值
转换示例:
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 配置项