自动化构建的主线任务

72 阅读13分钟

1. 代码转换(Transpile)

目标:让现代语法能在所有浏览器运行。

  • 问题:浏览器兼容性不一致,ES6+/TS/JSX 等新语法不能直接跑。
  • 解决:用工具转译成 ES5。
    • 典型工具:Babel、TypeScript Compiler。
  • 例子
// 源码(TypeScript)
const greet = (name: string): string => `Hello, ${name}`;

// 构建产物(兼容老浏览器的 JS)
var greet = function(name) { return "Hello, " + name; };

初步认识

为什么需要代码转换?

在前端发展历史里,语言特性总是先出现 → 浏览器慢慢支持 → 老浏览器落伍
问题:

  • 开发者喜欢用 新语法(ES6/ES202x、TypeScript、JSX、装饰器等),提高可读性和开发效率。
  • 但浏览器支持不一致(例如:IE11 不支持 importasync/await)。

👉 于是需要“翻译机”把源码变成 所有浏览器都能执行的低版本 JS
这就是 Transpile(转译)


Transpile 和 Compile 的区别
  • Compile(编译):把高级语言编译成机器码(C++ → .exe)。
  • Transpile(转译):语言间的“平级翻译”,保持抽象层级不变。
    • 比如:ES6 → ES5,只是语法糖的转换,不改变运行环境。
    • TypeScript → JavaScript 也是 Transpile。

代码转换的核心步骤
  1. 解析(Parsing)
    • 把源码解析成 AST(抽象语法树)。
    • 例子:
const x = () => 1;

会被解析成:

VariableDeclaration
  - Identifier: x
  - ArrowFunctionExpression
    - NumberLiteral: 1
  1. 转换(Transform)
    • 遍历 AST,应用转换规则(插件)。
    • 例子:箭头函数转换成普通函数。
  2. 生成(Generate)
    • 把新的 AST 转成目标代码(ES5)。
    • 例子:
var x = function() { return 1; }

主流工具
  1. Babel(最经典)
    • 插件化:
      • @babel/preset-env → 把 ES6+ 转成目标浏览器支持的代码。
      • @babel/plugin-transform-runtime → 避免重复注入工具函数。
    • 配合 core-js 自动引入 Polyfill。
  2. TypeScript Compiler(tsc)
    • 既做 类型检查,也能把 TS 转译成 JS。
    • 配置 target: "es5" | "es6" | "esnext"
  3. SWC / esbuild(新一代)
    • 用 Rust/Go 写的,转译速度比 Babel 快 10~100 倍。
    • 原理相同:AST → 转换 → 代码。

具体转换例子
例子1:箭头函数
// 源码
const add = (a, b) => a + b;

// 转译产物(ES5)
var add = function(a, b) {
  return a + b;
};
例子2:Class
class Person {
  constructor(name) {
    this.name = name;
  }
  sayHi() { console.log("Hi " + this.name); }
}

// 转译产物
function Person(name) {
  this.name = name;
}
Person.prototype.sayHi = function() {
  console.log("Hi " + this.name);
};
例子3:async/await
async function getData() {
  const res = await fetch('/api');
  return res.json();
}

// 转译产物(用 Promise + Generator 实现)
function getData() {
  return regeneratorRuntime.async(function getData$(_context) {
    while(1) switch (_context.prev = _context.next) {
      case 0:
        _context.next = 2;
        return regeneratorRuntime.awrap(fetch('/api'));
      case 2:
        return _context.abrupt("return", _context.sent.json());
    }
  });
}

总结
  • 本质:代码转换就是把“新语法”翻译成“旧语法”,让浏览器都能跑。
  • 核心机制:AST → 转换 → 生成。
  • 关键工具:Babel、tsc、SWC、esbuild。
  • 意义:让开发者能用最先进的语法,而不用担心兼容性。

如何学习工程化中的代码转换

前置思考

在企业开发中我似乎只要写页面写逻辑,没有接触到如何配置代码转换,而且很多配置文件我压根看不懂,也不知道什么时候要配什么时候不配,怎么掌握?

这其实是很多前端从“写业务页面”过渡到“工程化开发”时的困惑

👉 在业务开发中,好像我只要写逻辑就行,工具链的配置都没管过。那我要怎么掌握「代码转换」这块?

拆开来讲,从 现象 → 本质 → 能力要求 三个角度 :

为什么你平时没接触过配置?
  1. 脚手架封装好了默认配置
    • Vue CLI、Vite 在项目初始化时就已经帮你生成了 vite.config.jstsconfig.json 等默认文件。
    • Babel、PostCSS、TS 的常见规则都已经写好,所以你写代码时感觉“一切都能跑”。
  2. 团队内角色分工
    • 在企业里,很多团队会有「架构组 / 工程化负责人」维护工具链配置。
    • 普通开发者只需要 写页面逻辑 + 调用 API
    • 所以你看到的就是一个“免配置”的环境。
  3. 大部分需求不需要改配置
    • 写 Vue 组件、写样式、调接口 → 默认配置都能覆盖。
    • 只有遇到特殊需求(兼容老浏览器、自定义语法、额外插件)才需要修改。

本质上,代码转换配置解决的是什么问题?

工具链配置文件 ≠ 花哨语法,而是解决几个核心问题:

配置文件主要作用什么时候需要改?
tsconfig.jsonTypeScript → JS 转换规则想启用/关闭严格检查、路径别名(@/xxx)
babel.config.js新语法(ESNext → ES5)转换需要兼容老浏览器,或者启用实验性语法
postcss.config.jsCSS 转换(自动加前缀、兼容性)要兼容旧浏览器,或用 TailwindCSS 这种工具
browserslist定义支持哪些浏览器公司要跑在 IE11,或者只兼容最新 Chrome

👉 换句话说:这些配置文件不是每天都要改,而是「遇到兼容性 or 特殊需求」时才会用到。


作为前端工程师,你需要掌握到什么程度?

我建议分 3 个层次去掌握:

1. 认识层(知道作用)

  • 知道 tsconfig.json 是控制 TS 的,
  • 知道 browserslist 是控制兼容目标的。
    👉 这就避免了遇到问题时连入口都找不到。

2. 应用层(能改常见配置)

  • 会加 paths 来写路径别名:
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}
  • 会在 browserslist 里加一行 "last 2 versions" 来控制兼容。

👉 日常 80% 的场景都够了。

3. 原理层(理解转换链路)

  • 知道 .vue → vite → esbuild → JS 的流向。
  • 知道为什么 Babel 可以转 async/await 成 ES5。
    👉 这个阶段是你想往「高级工程师/架构师」走时才需要的。

怎么掌握?

推荐一个学习路径:

  1. 先观摩:聚焦一个项目,打开 vite.config.js,对照官方文档,看默认配置干了什么。
  2. 再实验:自己开一个小 Vue 项目,改一下 browserslist,然后 build 看看打出来的 JS 有什么变化。
  3. 再深入:等熟悉后,再去理解「为什么」能转换,比如 Babel 的插件机制、TS 编译过程。

✅ 总结一句:
在企业里,普通开发者不需要天天改配置,但要知道每个配置文件“管什么事”。
等到了一定阶段,就要学会:改配置来解决问题,而不是只会写业务逻辑。


Vue项目中的代码转换

通过前面的学习,我想你已经把“代码转换(Transpile)”的概念抓住了。接下来我帮你从 Vue 项目初始化 → 配置 → 实际运行 这条链路,把代码转换在项目中的实现过程讲清楚。


Vue 项目初始化

在现代前端开发中,我们不会手写从零搭建 Babel、Webpack 等工具链,而是用 脚手架工具(如 Vue CLI / Vite)来初始化项目。

Vue 3 + Vite 为例:

npm create vite@latest my-vue-app
cd my-vue-app
npm install
npm run dev

此时项目已经具备了完整的工程化环境,其中就包含了 代码转换环节


代码转换在 Vue 项目中的体现
1. Vue 单文件组件(.vue → JS + CSS)
  • .vue 文件其实是一个「语法扩展」,浏览器本身无法直接识别。
  • Vite 使用 **@vitejs/plugin-vue** 插件,先把 .vue 文件拆解成:
    • <template> → 编译成 渲染函数(类似 render(h)
    • <script> → 保持 JS 代码,交给 Babel / ESBuild 处理
    • <style> → 编译成 CSS,支持 PostCSS 处理

👉 所以,**.vue**** 文件本质是通过编译器 → 转换成标准 JS/CSS**


2. JS 新特性 → 兼容性转换(Babel / ESBuild)
  • 开发时你可能写:
const greet = (name = "Vue") => {
  console.log(`Hello, ${name}!`);
}
  • 浏览器如果不支持箭头函数、默认参数,就会报错。
  • 工具会转换成 ES5 代码:
function greet(name) {
  if (name === void 0) { name = "Vue"; }
  console.log("Hello, " + name + "!");
}
  • 在 Vite 中,默认用 ESBuild(比 Babel 快),在构建时做这一步。

3. CSS 预处理器 / PostCSS 转换
  • 你写 style.scss
$primary: #42b983;

.btn {
  background: $primary;
}
  • 工具链会把它转换成普通 CSS:
.btn {
  background: #42b983;
}
  • 再由 PostCSS 做自动前缀:
.btn {
  background: #42b983;
  -webkit-background: #42b983; /* 兼容旧浏览器 */
}

4. TypeScript → JavaScript
  • 如果你用 .ts
function sum(a: number, b: number): number {
  return a + b;
}
  • 工具链(tsc 或 Vite 内置 ESBuild)会 去掉类型信息,生成:
function sum(a, b) {
  return a + b;
}

谁来负责这些转换?
  • 开发阶段:Vite(基于 ESBuild)负责实时转换,开发时几乎无感知。
  • 构建阶段:Rollup(Vite 内核)统一打包输出,结合 Babel / PostCSS / Vue Compiler 等插件进行代码转换。
  • 最终产物:都是浏览器能直接运行的 纯 JS + CSS + HTML

Vue 项目中开发者的应用方式
  1. 明确写法与目标环境的差距
    • 你写的是 Vue SFC / ES6 / TS / SCSS
    • 最终需要运行在浏览器支持的 ES5/ES6 + CSS2/3 环境
  2. 利用工具链配置转换规则
    • vite.config.js 中配置 Babel、PostCSS、TS 支持
    • 根据目标浏览器(browserslist)调整转换程度
  3. 开发者角度的思维模式
    • 不用关心“浏览器能不能直接理解 Vue/TS”,因为工具会转换
    • 但要知道“为什么需要转换 → 工具链在哪一环节完成 → 出来的产物是什么”

总结

在 Vue 项目中,代码转换就是一个“桥梁”,把你写的高层语言(Vue/TS/SCSS/ESNext)转译成浏览器能运行的低层语言(ES5+/CSS3/HTML)。脚手架(如 Vite)帮你自动化完成了这件事。


Vue 项目里“代码转换链路图”

Vue 项目里的代码转换链路梳理成一个整体图谱,你能一眼看到:

  • 各类源文件 .vue / .ts / .scss ...
  • 经过哪些工具处理
  • 最终产物落地到浏览器可执行的 JS / CSS / HTML

整体图谱(逻辑链路)
┌──────────────────────────────────────────────────┐
   │                Vue 项目源代码层                  │
   └──────────────────────────────────────────────────┘
          │
          │
          ▼
   ┌────────────┬─────────────┬──────────────┐
   │   .vue     │    .ts/.js   │   .scss/.css │
   └────────────┴─────────────┴──────────────┘
          │             │              │
          ▼             ▼              ▼
   ┌────────────┐ ┌──────────────┐ ┌──────────────┐
   │ Vue 编译器 │ │ TS 编译器 /   │ │ Sass 编译器  │
   │ (vue-compiler-│ │ Babel / ESBuild │ │ (dart-sass) │
   │ sfc + plugin)│ └──────────────┘ └──────────────┘
   └───────┬────┘        │                 │
           │              ▼                 ▼
           │       ┌──────────────┐ ┌──────────────┐
           │       │ Babel /      │ │ PostCSS       │
           │       │ ESBuild 转换 │ │ (autoprefixer │
           │       │ 新语法 → ES5 │ │  等插件)      │
           │       └──────────────┘ └──────────────┘
           │              │                 │
           ▼              ▼                 ▼
   ┌──────────────────────────────────────────────────┐
   │                Rollup (Vite 内核打包器)           │
   │   - 按依赖关系打包                                 │
   │   - Tree Shaking / 代码分割 / 压缩                 │
   └──────────────────────────────────────────────────┘
          │
          ▼
   ┌──────────────────────────────────────────────────┐
   │                浏览器可执行产物                   │
   │   - bundle.js  (标准 JS,兼容目标环境)             │
   │   - style.css  (标准 CSS,带前缀/兼容性处理)       │
   │   - index.html (最终入口 HTML)                     │
   └──────────────────────────────────────────────────┘

各个环节的解释
1. .vue
  • 谁处理?@vitejs/plugin-vue + vue-compiler-sfc
  • 做了啥?
    • <template> → 转换成 渲染函数(h函数形式的 JS 代码)
    • <script> → 交给 Babel / ESBuild 转换
    • <style> → 提取出 CSS,交给 Sass/PostCSS

👉 结果:一个 标准 JS 模块 + 对应 CSS


2. .ts / .js
  • 谁处理?
    • TypeScript → 去掉类型,生成普通 JS
    • Babel / ESBuild → 把 ESNext 新特性转成兼容性更好的语法

👉 结果:最终得到 兼容目标浏览器的 JS


3. .scss / .css
  • 谁处理?
    • sass 编译器 → 把 SCSS 转换为原生 CSS
    • postcss(常见插件:autoprefixer) → 自动加浏览器前缀,兼容不同浏览器

👉 结果:最终得到 兼容性的 CSS


4. Rollup 打包(Vite 内核)
  • 把上述转换后的 JS + CSS 作为模块输入
  • 做:
    • 模块依赖图分析
    • Tree-shaking(剔除没用的代码)
    • 代码分割(动态 import → 拆成多个 chunk)
    • 压缩混淆(Terser/ESBuild)

👉 输出:

  • bundle.js(或者多个 chunk)
  • style.css(汇总过的 CSS)

5. 浏览器最终执行
  • HTML 引入:
<link rel="stylesheet" href="/style.css">
<script type="module" src="/bundle.js"></script>
  • 浏览器运行时,完全看不到 .vue.ts.scss,只看到标准 JS/CSS/HTML。

✅ 总结一句:
Vue 项目中的 代码转换链路 就像一条流水线:
高阶源码(Vue/TS/SCSS) → 编译/转译工具 → Rollup 打包 → 最终产物(JS/CSS/HTML)


Vue 项目中开发者如何通过工具链配置转换规则

我们来深入 “Vue 项目中开发者如何通过工具链配置转换规则”。这其实就是 工程师 → 工具链 → 浏览器 之间的“契约”,开发者写新语法,工具链帮你转换成旧语法,确保浏览器能跑。


为什么需要配置转换规则?

因为:

  • 你写的代码(ES2022TSSCSS.vue)浏览器 并不一定原生支持
  • 浏览器版本差异很大(比如 IE11 / Chrome 最新版),需要工具 针对目标环境做降级转换

👉 所以:

  • 开发者 在配置文件里声明「我的目标运行环境」
  • 工具链(Vite → Rollup + Babel + PostCSS + TypeScript)自动决定:该转什么、不转什么。

配置的入口:vite.config.js
1. TypeScript 支持
  • 方式:Vite 原生支持 TS(借助 esbuild),不需要额外配置。
  • 额外需求:如果需要 类型检查,要在项目根目录加 tsconfig.json
{
  "compilerOptions": {
    "target": "es2019",  // 输出的 JS 版本
    "module": "esnext",  // 保留 ESM 模块语法,交给打包器处理
    "strict": true,      // 严格类型检查
    "jsx": "preserve"
  }
}

2. Babel 配置

虽然 Vite 默认用 esbuild 转换语法(更快),但有时候仍需要 Babel:

  • 使用 polyfill(比如 Promise、Array.flat 在老浏览器里没有)
  • 使用 高级插件(比如装饰器、宏)

做法:

npm install -D @vitejs/plugin-vue @vitejs/plugin-vue-jsx @vitejs/plugin-legacy
npm install -D @babel/core @babel/preset-env

vite.config.js 里启用 Babel:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import legacy from '@vitejs/plugin-legacy'

export default defineConfig({
  plugins: [
    vue(),
    legacy({
      targets: ['defaults', 'not IE 11'],  // 根据 browserslist 生成转换规则
      additionalLegacyPolyfills: ['regenerator-runtime/runtime']
    })
  ]
})

👉 @vitejs/plugin-legacy 会结合 browserslist 自动加 Babel 转换。


3. PostCSS 配置

CSS 也要考虑兼容性,比如:

  • 自动加前缀:-webkit-, -moz-
  • 新语法降级:比如 :has() → 老浏览器不支持

配置方式 → 在根目录创建 postcss.config.js

module.exports = {
  plugins: {
    autoprefixer: {},   // 根据 browserslist 自动加前缀
    'postcss-nesting': {} // 支持嵌套语法
  }
}

4. Browserslist(核心配置文件)

前端所有兼容性相关的工具,都会读取 browserslist 配置(放在 package.json.browserslistrc 文件中)。

例如:

{
  "browserslist": [
    ">0.2%",        // 全球使用率 > 0.2%
    "not dead",     // 不要淘汰的浏览器
    "not op_mini all"
  ]
}

👉 工具链的作用:

  • Babel 根据它决定要不要转语法
  • PostCSS/autoprefixer 根据它决定要不要加前缀
  • Vite/legacy 插件 根据它决定是否生成老版本 bundle

直观流程图
源代码(TS / Vue / SCSS)
                       │
             ┌─────────┴──────────┐
             │ 配置文件(开发者输入)│
             │  vite.config.js      │
             │  tsconfig.json       │
             │  postcss.config.js   │
             │  browserslist        │
             └─────────┬──────────┘
                       │
             ┌─────────▼──────────┐
             │    工具链(Vite)    │
             │  esbuild / Babel    │
             │  PostCSS / Sass     │
             └─────────┬──────────┘
                       │
                       ▼
        浏览器可运行的 JS / CSS / HTML

作为前端工程师的应用要点
  1. 明确目标环境 → 用 browserslist 定义清楚。
    • 比如公司产品只支持 Chrome ≥ 90,那转换就可以轻量化。
    • 如果还要兼容 IE11,就必须启用 @vitejs/plugin-legacy + polyfill。
  2. 理解配置文件的职责分工
    • vite.config.js → 全局构建策略
    • tsconfig.json → TypeScript 转换规则
    • postcss.config.js → CSS 转换规则
    • browserslist → 环境基准
  3. 按需引入 polyfill
    • core-js / regenerator-runtime → 确保旧环境也能跑新特性。

总结

开发者通过 配置工具链(Vite + Babel + PostCSS + TS),把“我要支持哪些浏览器”告诉构建工具,工具链再根据 browserslist 自动决定要不要转语法 / 加前缀 / 引 polyfill,保证最终产物能在目标环境稳定运行。


Vue 项目中常见的配置场景(基于 Vite + Vue3 + TS)

了解了前面的基础知识,接下来把 “代码转换(Transpile)”从抽象 → 具体配置案例 → 知道为什么要这么配。

下面整理几个 Vue 项目中常见的配置场景(基于 Vite + Vue3 + TS),这样你能一眼看到:

  • 问题(为什么需要转换)
  • 配置(在哪个文件里怎么写)
  • 效果(最终浏览器能跑起来)

TypeScript 转换

问题

浏览器不认识 .ts 文件。比如:

let count: number = 1
count = "abc" // 浏览器直接报错

配置

在 Vue + Vite 项目里,Vite 会自动处理 TS,你只需要 tsconfig.json

// tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",              // 转换目标(最终的 JS 语法版本)
    "module": "ESNext",              // 模块系统(交给 Vite 做进一步处理)
    "strict": true,                  // 开启严格模式
    "moduleResolution": "node",      // 模块解析方式
    "esModuleInterop": true,         // 兼容 CommonJS 和 ESM
    "skipLibCheck": true             // 跳过类型声明文件检查,加快构建
  }
}

效果

  • 开发时:VSCode 会用 tsconfig.json 提供智能提示、类型检查。
  • 构建时:Vite 调用 esbuild.ts 转成浏览器能跑的 .js

👉 为什么这样配?
因为现代浏览器能跑 ESNext,但不能直接识别 TS,所以需要在构建时降级。


SCSS / PostCSS 转换

问题

浏览器只认识 CSS,不认识 SCSS 语法(变量、嵌套)。

$primary: #42b983;

.button {
  color: $primary;
}

配置

vite.config.js 里指定预处理器:

// vite.config.js
export default {
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@import "@/styles/variables.scss";`
      }
    },
    postcss: './postcss.config.js'
  }
}

postcss.config.js:

module.exports = {
  plugins: {
    autoprefixer: {} // 自动加浏览器前缀
  }
}

效果

  • SCSS 被编译成纯 CSS。
  • autoprefixer 会把 display: flex; 变成兼容写法:
display: -webkit-box;
display: -ms-flexbox;
display: flex;

👉 为什么这样配?
因为 CSS 兼容性差,工具链要帮你转成能跑在目标浏览器的版本。


Babel 转换(语法降级)

问题

如果项目要兼容旧浏览器(比如 Chrome 49),直接写现代语法会报错:

const fn = () => {}

配置

babel.config.js(Vue CLI)或 Vite + 插件(如 @vitejs/plugin-vue 默认带 Babel 支持):

// babel.config.js
module.exports = {
  presets: [
    ['@babel/preset-env', {
      targets: "> 0.25%, not dead"  // 根据 browserslist 决定降级范围
    }]
  ]
}

.browserslistrc

> 0.25%
not dead

效果

  • 箭头函数等 ES6+ 语法会被 Babel 转换成 ES5。
  • 根据 .browserslistrc 自动决定要不要降级。

👉 为什么这样配?
因为并不是所有用户都用最新浏览器,Babel + Browserslist 就能智能决定“降级多少”。


Vue 单文件组件(.vue)

问题

浏览器不认识 .vue 文件,它是组合文件(template + script + style)。

配置

Vite 里通过插件 @vitejs/plugin-vue 自动处理:

// vite.config.js
import vue from '@vitejs/plugin-vue'

export default {
  plugins: [vue()]
}

效果

  • template → 转换成渲染函数(JS)。
  • script → TS 转成 JS,再交给打包工具。
  • style → SCSS/LESS 转成 CSS,再注入页面。

👉 为什么这样配?
因为 .vue 文件本质上要被分解成浏览器能跑的 JS + CSS + HTML


总结

代码转换(Transpile) 本质上就是:

让开发者能写“高级语法”,最后转成浏览器能看懂的“低级语法”。

你能优化的点:

  1. tsconfig.json → 决定 TS 转换到什么程度。
  2. postcss.config.js → 决定 CSS 怎么做兼容。
  3. babel.config.js + browserslist → 决定 JS 要不要降级。
  4. vite.config.js → 决定整个转换流程的规则(vue、scss、ts)。

2. 依赖解析 & 资源合并(Bundle)

目标:解决“依赖太多,加载慢”。

  • 问题:现代项目会有几百个模块,浏览器要发几百次 HTTP 请求,太慢。
  • 解决:把零散文件打包合并,减少请求数。
  • 工具:Webpack / Rollup / Vite(底层还是 Rollup)。
  • 例子
    • 源码:main.js 引用 math.jsapi.jsutils.js
    • 构建产物:一个 bundle.js,里面打包好所有依赖。

初步认识

为什么需要资源合并?

2000s 动态交互时代 时:

  • 网页要依赖多个 JS、CSS、图片文件。
  • 浏览器发起一次 HTTP 请求就要建立 TCP 连接(尤其是 HTTP/1.1,最多 6 并发)。
  • 如果一个页面要加载几十上百个小文件,延迟就会非常高。

👉 所以在 2010s 工程化时代

  • 开发时保持模块化(方便维护)。
  • 发布时合并打包成少量大文件(提高加载性能)。

本质目标: 把多个模块化源码(import/exportrequire)组织成 浏览器能直接运行的少量文件,避免请求过多。


把“源码世界”映射成“产物世界”:
  1. 源码世界(开发友好)
    • 多个 .js 模块(ESM / CommonJS)
    • 多个 .css 文件(Sass/Less/PostCSS)
    • 图片、字体、SVG 等静态资源
  2. 产物世界(运行友好)
    • 少量 JS 文件(bundle.js / chunk.js)
    • 合并后的 CSS 文件(style.css)
    • 优化后的图片(压缩过,文件名带 hash)

Bundle 的演进
  1. 早期(Grunt/Gulp)
    • 只是“机械式合并 + 压缩”。
  2. Webpack 时代
    • 引入 依赖图 思想,真正理解模块化 → bundle.js。
  3. Rollup / Vite 时代
    • 借助 ESM 静态分析,优化 tree-shaking。
    • 通过 ESBuild 提升构建速度。
    • Vite 在开发模式下甚至 跳过打包,利用浏览器原生 ESM,提升开发体验。

总结

资源合并就是把“开发时的碎片化模块”转化为“浏览器友好的少量文件”,其核心在于依赖图的构建与打包策略。

👉 结果:产物更少,减少 HTTP 请求。


Webpack 的合并过程

Webpack 是 静态分析 + 单文件输出为主 的打包工具。

1. 构建依赖图(Dependency Graph)
  • 从入口文件(通常是 src/main.js)开始,扫描其中的 import/export
  • 递归分析依赖,形成一个树状关系。

👉 例子:

// main.js
import { add } from './math.js'
import './style.css'

// math.js
export function add(a, b) { return a + b }

Webpack 会解析出依赖图:

main.js
 ├── math.js
 └── style.css

2. 模块封装

Webpack 把每个模块包裹进一个函数,形成一个「模块字典」。

产物大概长这样(简化版):

(function(modules) {
  function __webpack_require__(id) {
    const fn = modules[id]
    const module = { exports: {} }
    fn(module, module.exports, __webpack_require__)
    return module.exports
  }

  return __webpack_require__("./src/main.js")
})({
  "./src/main.js": function(module, exports, __webpack_require__) {
    const { add } = __webpack_require__("./src/math.js")
    console.log(add(2, 3))
  },
  "./src/math.js": function(module) {
    module.exports = { add: (a, b) => a + b }
  }
})

👉 这样,多个文件被合并成一个大文件 bundle.js,浏览器只要加载它就行。


3. 输出产物
  • JS → bundle.js
  • CSS(经 loader 处理) → bundle.css 或内联进 JS

结果:HTTP 请求更少,文件体积相对集中。


Vite 的合并过程

Vite 和 Webpack 不一样,它利用 原生 ES Modules + esbuild/rollup,思路更轻量。

1. 开发时(Dev Server)
  • Vite 直接让浏览器加载 原生 ES Module
<script type="module" src="/src/main.js"></script>
  • 浏览器自己发请求加载依赖(如 math.js)。
  • Vite 只在中间做「语法转换」和「路径处理」,不做合并。
    👉 所以开发时无打包,热更新快。

2. 构建时(Build)

Vite 内部调用 Rollup 来做真正的合并:

  • Rollup 从入口文件(src/main.js)开始,分析依赖图。
  • Tree Shaking:只打包用到的部分。
  • 输出多个 chunk(按需分割)。

👉 例子:

// main.js
import { add } from './math.js'
console.log(add(1, 2))

Rollup 打包结果可能是:

// app.js
function add(a, b) { return a + b }
console.log(add(1, 2))

如果有第三方库(如 lodash),会单独生成 vendor.js


3. 输出产物
  • JS → app.js, vendor.js, chunk-xxx.js
  • CSS → 单独抽离 style.css

Webpack vs Vite(Rollup)资源合并流程对照

阶段Webpack 流程Vite 流程(基于 Rollup 打包)
依赖解析静态分析 import
/ require,构建 依赖图
静态分析 import
/ export,构建 依赖图
模块处理每个模块 → 包装成函数 function(module, exports, require){...}模块直接展开,尽量合并到同一作用域(scope hoisting
运行时支持注入 Webpack runtime(__webpack_require__
管理模块加载与缓存)
基本无 runtime,直接依赖浏览器 / Node 原生模块机制
合并方式把所有模块函数放进一个“模块表”(对象/数组),入口文件调用 runtime 逐步加载静态拼接 import/export,能消除包装层,产物更接近原生 ESM
Tree Shaking依赖图级别,未使用的导出在 压缩阶段可能被剔除,但受 runtime 机制限制,有时效果不彻底构建阶段直接剔除未用的导出,更纯粹
默认产物1 个 bundle.js( 如果配置 代码分割,就会输出多个 bundle(chunk),含 runtime + 模块定义)多个 chunk.js(天然分割,按依赖拆分, 不需要额外 runtime 去加载 chunk,浏览器用原生 import() 就行)
CSS 处理通过 mini-css-extract-plugin
等插件单独抽取
内建 rollup-plugin-css
或 Vite 插件抽取,支持按需分割

  • Webpack打包方式
    • 处理模块 → 包装成函数 → 放进“模块表”
    • 运行时通过 __webpack_require__ 来动态调用这些模块。
    • 结果是 bundle.js 内部会有:
      1. Webpack runtime(模块加载器逻辑)
      2. 模块表(id: function(...) { ... })
      3. 入口模块调用
    • 所以说 Webpack 产物是一个“大容器”,里面有 runtime 和所有模块定义。
  • ** Rollup / Vite 打包方式 **:
    • 处理模块 → 静态展开 → scope hoisting
    • 不需要每个模块都包装成函数,而是直接把 import/export 的内容拼接到同一个作用域(如果可能)。
    • 产物更像是“手工合并的 ES Module”,没有 Webpack 那种运行时容器。

3. 优化阶段

压缩(Minify)

什么是 Minify?

Minify(代码压缩) = 在不改变运行逻辑的前提下,尽可能减小文件体积。
目标:减少网络传输体积、加快加载速度。

⚠️ 注意:

  • 压缩 ≠ Tree Shaking
    • 压缩是「把已有代码写得更紧凑」。
    • Tree Shaking 是「删除没用的代码」。
  • 两者往往一起使用。

JS 压缩(Terser / esbuild)

Terser 为例,它做的事大概有 3 类:

① 删除无关字符

源码:

function add(a, b) {
  return a + b; // 返回两数之和
}
console.log(add(2, 3));

压缩后:

function add(n,d){return n+d}console.log(add(2,3));
  • 去掉换行、缩进、注释。
  • 文件体积显著变小。

② 变量名混淆(Mangle)

源码:

function calculateTotal(price, taxRate) {
  let total = price + price * taxRate;
  return total;
}

压缩后:

function a(n,t){return n+n*t}
  • 变量名 price → ntaxRate → ttotal → 隐去
  • 减少字符数,同时也提高了代码“安全性”(难读)。

③ 常量折叠、内联优化

源码:

const tax = 1 + 0.05;
console.log(tax * 100);

压缩后:

console.log(105);
  • 计算能提前完成的,直接写死结果。

👉 这些优化称为 DCE(Dead Code Elimination) + Constant Folding,也是压缩工具常见手段。


CSS 压缩(cssnano)

CSS 压缩更直观:

源码:

body {
  margin: 0px;
  padding: 0px;
  color: #ffffff;
}

压缩后:

body{margin:0;padding:0;color:#fff}
  • 删除单位 0px → 0
  • 颜色缩写 #ffffff → #fff
  • 去掉空格、换行

HTML 压缩

源码:

<!DOCTYPE html>
<html>
  <head>
    <title>My Page</title>
    <!-- 这是一个注释 -->
  </head>
  <body>
    <div class="container">
      <p>Hello World</p>
    </div>
  </body>
</html>

压缩后:

<!DOCTYPE html><html><head><title>My Page</title></head><body><div class=container><p>Hello World</p></div></body></html>
  • 删除注释
  • 去掉不必要的属性引号
  • 空格最小化

工具链中的 Minify

在现代 Vue / React 项目里,你不需要手动压缩,构建工具会自动接管:

  • Vite:默认使用 esbuild 压缩 JS(快)+ cssnano 压缩 CSS。
  • Webpack:默认用 TerserPlugin 压缩 JS。

配置示例(Vite + esbuild):

// vite.config.js
export default {
  build: {
    minify: 'esbuild', // 或 'terser'
    terserOptions: {
      compress: {
        drop_console: true, // 删除 console.log
      }
    }
  }
}

总结

Minify = 减少体积,让代码更小更快。

  • JS → 删除空格/注释,变量名混淆,常量优化。
  • CSS → 去冗余单位,压缩颜色,合并规则。
  • HTML → 删除注释和多余空格。

👉 它不会删除“没用的模块”(那是 Tree Shaking 的事),只是在保证逻辑正确的前提下「把字数压到最少」。


Tree Shaking

问题背景

早期 JS 打包时,只要你 import 了一个库,不管你用没用,它都会整个被塞进最终 bundle。
比如:

import _ from 'lodash'

const a = _.cloneDeep(obj)

结果:即便你只用到了 cloneDeep,最后的 app.js 还是会包含整个 lodash(几百 KB)。
→ 页面加载慢,性能浪费。

于是,Tree Shaking(摇树优化) 出现:
像摇树一样,把“没用的枝叶”摇掉,保留你真正用到的部分。


Tree Shaking 的核心原理
(1) 基于 ES Module 静态分析
  • Tree Shaking 只能分析 ES Module(**import/export**,因为它是静态结构(编译时能看出依赖关系)。
import { cloneDeep } from 'lodash-es'   // ✅ 静态可分析
const fn = require('lodash')            // ❌ 动态 require,不好分析
  • 打包工具(Webpack、Rollup、Vite 内部的 esbuild)会扫描依赖树,发现未被引用的导出就丢弃。
(2) 死代码消除(DCE, Dead Code Elimination)
  • 除了没用的函数,也能移除 if (false) 这样的代码块。
if (false) {
  console.log('永远不会执行')
}

→ 构建产物里直接被删掉。

(3) 只保留“活的路径”

就像树的根(入口文件)往外生长,凡是能连通到的代码会保留,其他孤立的就被摇掉。


案例演示
🌰 例 1:按需导入 vs 整体导入
// case A
import { cloneDeep } from 'lodash-es'
const obj2 = cloneDeep(obj1)
// 打包结果:只留下 cloneDeep 相关代码

// case B
import _ from 'lodash'
const obj2 = _.cloneDeep(obj1)
// 打包结果:整个 lodash(~500KB)被打进去

👉 为什么?

  • lodash-es 是 ESM(模块化可静态分析),Tree Shaking 能起作用。
  • 普通 lodash 是 CJS(CommonJS,动态 require),Tree Shaking 无能为力。

🌰 例 2:函数未使用
// utils.js
export function foo() { return 1 }
export function bar() { return 2 }

// index.js
import { foo } from './utils.js'
console.log(foo())

打包结果:

  • foo ✅ 保留
  • bar ❌ 被摇掉

🌰 例 3:死代码删除
if (process.env.NODE_ENV === 'development') {
  console.log('dev only log')
}

构建时如果替换为 production,Tree Shaking + 压缩器(Terser)会直接删掉整个 if 块。


工具支持
  • Webpack:默认支持,但要配置 "sideEffects": false(否则担心副作用不敢删)。
  • Rollup:天生 ESM-first,Tree Shaking 效果最好。
  • Vite:基于 Rollup,天然支持 Tree Shaking。

使用建议

优先用 ESM 版本库(如 lodash-esdate-fns
写代码时避免副作用(比如模块 import 就执行了全局注册)
检查 bundle analyzer 看 Tree Shaking 是否生效


总结
  • Tree Shaking ≠ 压缩(Minify)。
    • Minify 是字符级别(删空格、变量混淆)。
    • Tree Shaking 是语义级别(删没用的函数/模块)。
  • 前者类似“把书缩成小字本”,后者类似“只带走你要看的章节”。

Tree Shaking 踩坑与优化清单
库层面的问题

** 坑 1:使用 CommonJS(CJS)库**

// 使用 lodash(CJS)
import _ from 'lodash'
const obj2 = _.cloneDeep(obj1)
  • 结果:整个 lodash (~500KB) 被打包。Tree Shaking 失效。
  • 原因:CJS 的 require 是动态的,打包工具无法静态分析。

✅ 解决:

  • 使用 ESM 版本库(如 lodash-es)。
  • 或者使用 babel-plugin-lodash 做按需加载。

** 坑 2:库声明了副作用(sideEffects)**

有些库的 package.json 没有正确声明 sideEffects
Webpack 就会担心“删了会不会影响全局” → 不敢摇掉。

✅ 解决:

  • 查看 package.json 是否有:
"sideEffects": false

"sideEffects": ["*.css"]
  • 如果没有,可以 fork 库或在项目层面声明。

** 坑 3:import 时触发副作用**

// index.js
import './polyfill.js'   // polyfill 内部改写全局对象

即使没显式使用 polyfill,Webpack 也不敢删,因为它可能改写全局。

✅ 解决:

  • 明确哪些文件有副作用,并在 sideEffects 配置里声明。

代码层面的问题

** 坑 4:整体导入而不是按需导入**

import * as utils from './utils.js'
console.log(utils.foo)
  • 结果:barbaz 也被打包。
  • 原因:Tree Shaking 只能删除完全未使用的导出,但 import * 让工具觉得可能用到全部。

✅ 解决:

  • 改成精确导入:
import { foo } from './utils.js'

** 坑 5:动态 import 失效**

const lib = import(someVariable)   // ❌ 变量不确定,无法静态分析

✅ 解决:

  • 动态 import 时,路径要是静态字符串
const Chart = () => import('chart.js')

** 坑 6:条件导入**

if (process.env.NODE_ENV === 'production') {
  import { heavyFn } from './debug-tools'
}
  • Tree Shaking 不会处理条件语句里的 import。

✅ 解决:

  • 用动态 import 或者插件替换环境变量后,再交给打包工具消除:
if (process.env.NODE_ENV === 'production') {
  const debug = await import('./debug-tools')
}

** 坑 7:函数引用间接化**

import { foo } from './utils'
const fn = foo
console.log(fn)
  • 这种没问题;但如果你写:
const name = 'foo'
utils[name]()

→ 工具无法静态分析具体用到哪个方法,可能整个模块保留。


工具层面的问题

** 坑 8:没开启生产模式**

  • Webpack/Vite 在开发模式(mode=development)不会开启完整优化。
  • Tree Shaking、压缩等都只在 production 下启用。

✅ 解决:

  • 构建时必须:
npm run build --mode production

** 坑 9:babel 转换导致破坏 ESM**

有些 Babel 配置(比如 @babel/preset-envmodules: commonjs)会把 ESM 转成 CJS → Tree Shaking 失效。

✅ 解决:

  • 保留 ES Module:
{
  "presets": [
    ["@babel/preset-env", { "modules": false }]
  ]
}

** 坑 10:工具插件顺序错误**

  • 有些优化插件(比如 babel-plugin-lodashrollup-plugin-commonjs)如果顺序不对,可能影响 Tree Shaking。

✅ 解决:

  • 查文档,保证执行顺序:先转成 ESM → 再摇树 → 再压缩。

最终总结清单
  • 库层面:CJS → ESM,正确 sideEffects,避免 import 副作用。
  • 代码层面:按需导入,避免动态/条件 import,别用间接调用。
  • 工具层面:确保生产模式,不要让 Babel 把 ESM 转 CJS,合理配置插件顺序。

代码分割(Code Splitting)

要解决什么问题?

历史背景回顾一下:

  • 早期 一个 bundle.js:所有代码合并到一起。
  • 问题:
    • 文件太大(几 MB),首页首屏加载慢。
    • 某些页面功能用户可能永远用不到,却也被打进首屏包。
    • 修改一点点代码 → 整个 bundle.js hash 变了 → 浏览器缓存全失效。

于是:
👉 我们需要 分割代码,把应用拆成多个更小的 chunk(代码块)。


核心思路

代码分割的本质:按需加载 + 缓存优化

  • 同步代码:打进入口(如 app.js)。
  • 第三方依赖:单独抽取(如 vendor.js)。
  • 路由/页面:懒加载(生成 chunk-xxx.js)。
  • 公共模块:抽取成共享 chunk(避免重复打包)。

Webpack 的代码分割

我们先聚焦 Webpack 的代码分割方式(因为 Webpack 是历史上工程化里“打包”的主角,几乎所有现代工具链的思路都受它启发)。

Webpack 本质是 静态打包器,它需要在编译阶段分析依赖关系,然后根据配置拆分。


1. 多入口分割(Entry Points Splitting)

👉 适用于 多页面应用(MPA)

// webpack.config.js
module.exports = {
  entry: {
    home: './src/home.js',
    about: './src/about.js'
  },
  output: {
    filename: '[name].bundle.js'
  }
}

打包结果:

dist/
  home.bundle.js   # home 页代码
  about.bundle.js  # about 页代码

📌 应用场景:老式多页面项目(不同页面完全独立)。


2. 动态导入(Dynamic Import)

👉 适用于 路由懒加载、按需加载

// 正常静态引入 → 打包进 app.js
import { foo } from './utils'

// 动态引入 → 单独生成 chunk-utils.js
const Chart = () => import('chart.js')

打包结果:

dist/
  app.js
  chunk-Chart.js

📌 应用场景:Vue/React 项目的路由懒加载、某些大体积库。


3. SplitChunksPlugin(第三方库/公共模块分割)

Webpack 内置的 optimization.splitChunks 专门用于 自动分割 vendor 和 commons

// webpack.config.js
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',  // 所有模块都参与分割
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor',
          chunks: 'all'
        }
      }
    }
  }
}

打包结果:

dist/
  app.js        # 业务代码
  vendor.js     # node_modules 依赖(vue、react、lodash…)

📌 应用场景:适合大型单页应用(SPA),把稳定依赖抽出来,缓存复用。


4. Commons Chunk(公共模块分割)

当某个文件被 多个入口引用 时,Webpack 可以自动抽出公共部分。

// webpack.config.js
optimization: {
  splitChunks: {
    minSize: 0,  // 默认 30KB,调小方便演示
      cacheGroups: {
      commons: {
        minChunks: 2,   // 至少被 2 个模块引用
          name: 'commons',
          chunks: 'all'
      }
    }
  }
}

打包结果:

dist/
  home.js
  about.js
  commons.js    # home 和 about 共同依赖的模块

📌 应用场景:多个页面/模块复用工具库时,避免重复打包。


5. 预取与预加载(Prefetch / Preload)

Webpack 支持 /* webpackPrefetch: true *//* webpackPreload: true */ 提示浏览器加载策略。

// 预取:浏览器空闲时下载
import(/* webpackPrefetch: true */ './math')

// 预加载:和主 bundle 并行加载
import(/* webpackPreload: true */ './math')

📌 应用场景

  • Prefetch → 用户很可能下一个页面要用到的资源。
  • Preload → 首屏渲染一定会用到,但可以并行提前请求。

小结

Webpack 的代码分割方式 = 手动切 + 自动切

  1. 入口分割(entry 配置)
  2. 动态导入(import())
  3. SplitChunksPlugin(vendor + commons 自动拆分)
  4. 预取/预加载(优化体验)

Vite 的代码分割
1. 路由懒加载(Dynamic Import)

和 Webpack 一样,用 import()

// Vue Router 示例
const Home = () => import('./pages/Home.vue')
const About = () => import('./pages/About.vue')

🔹 打包结果(vite build → rollup 分割):

dist/
  index.html
  assets/index.js      # 入口
  assets/About-xxx.js  # 异步 chunk
  assets/Home-xxx.js

📌 原理

  • 开发时(npm run dev) → 浏览器原生 ESM,动态请求 Home.vue 转换后的模块。
  • 打包时(npm run build) → Rollup 自动分 chunk。

2. 自动分割(Rollup 内置机制)

Vite 的底层是 Rollup,它会自动把 动态 import 的模块拆出来。
无需 SplitChunksPlugin 这么复杂的配置。

示例:

// src/main.js
import _ from 'lodash'

// src/pageA.js
import { foo } from './utils'

// src/pageB.js
import { bar } from './utils'

👉 打包结果:

assets/index.js     # main 入口
assets/vendor.js    # lodash 抽取
assets/utils.js     # pageA/pageB 共同依赖

📌 和 Webpack 对比

  • Webpack 需要 optimization.splitChunks 显式配置。
  • Vite/Rollup 默认会做 vendor 分离(node_modules 依赖)+ 公共依赖抽取

3. 手动分包(Manual Chunk)

Vite 提供 build.rollupOptions.output.manualChunks,可以自定义分包规则:

// vite.config.js
export default {
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vue: ['vue'],
          vendor: ['lodash', 'axios']
        }
      }
    }
  }
}

👉 打包结果:

assets/index.js
assets/vue.js
assets/vendor.js

📌 应用场景

  • 精细控制大包拆分。
  • 比如把 Vue 核心单独打成一个文件(缓存命中率更高)。

4. 预加载(Preload / Prefetch)

Vite 内置 <link rel="modulepreload">,自动为入口 chunk 生成依赖预加载。
你也可以手动配置路由级别的 /* @vite-ignore */ 或结合框架的 router.prefetch()

📌 和 Webpack 对比

  • Webpack 需要注释 /* webpackPrefetch */
  • Vite 自动插入 preload link,默认体验更丝滑。

Webpack vs Vite:代码分割思路对比
特性WebpackVite (Rollup)
默认分割需要配置 SplitChunksPluginRollup 自动分 chunk
动态导入import()
,需配置懒加载
import()
,天然支持
第三方库分割vendor.js
需手动配置
自动抽出 node_modules
公共依赖cacheGroups
规则复杂
Rollup 默认抽取
预取/预加载需加注释自动生成 <link rel="modulepreload">
手动分包cacheGroups
里配置复杂规则
manualChunks
配置直观简单

✅ 小结:

  • Webpack = 大而全,功能强,配置复杂(需要告诉它怎么切)。
  • Vite = 借助 原生 ESM + Rollup,默认就有较优的分割(少配置)。

和其他优化的关系
  • 和压缩(Minify):压缩是减小单个文件体积,分割是减少一次性加载的体积。
  • 和 Tree Shaking:Tree Shaking 去掉没用的代码,Code Splitting 解决文件分配问题。
  • 和懒加载(Lazy Load):动态 import 生成 chunk,本质就是懒加载的实现方式。

📌 总结一句:
Code Splitting = 给 bundle 做“拆包 + 按需配送”,让用户先吃一口小菜(app.js),后面需要再上大餐(chunk-xxx.js)。


4. 静态资源处理(Assets)

目的:让图片/字体等能在浏览器高效使用。
典型操作

  • 图片压缩(image-webpack-loader)
  • 小图转 base64 内联
  • 大图单独 hash 命名,便于缓存

背景与目的

在前端工程中,除了 JS / CSS / HTML 外,图片、字体、音视频等静态资源占据了很大体积。
问题:

  • 如果资源过大 → 页面加载慢。
  • 如果资源没处理好缓存 → 每次发布都要重新下载。
  • 如果资源没优化 → 移动端/弱网下卡顿严重。

所以需要:
✅ 压缩体积
✅ 优化加载方式
✅ 提高缓存利用率


常见处理方式

(1)图片压缩
  • 目的:减少文件体积。
  • 工具链
    • image-webpack-loader(Webpack 经典方案)
    • vite-imagetools(Vite 插件,基于 Rollup)
    • 底层依赖:imagemin 系列(支持 jpg/png/svg/webp 等格式)

👉 压缩策略:

  • 有损压缩(jpg/webp):牺牲画质换体积。
  • 无损压缩(png/svg):保证质量,去掉冗余信息。

(2)小图转 Base64 内联
  • 目的:减少 HTTP 请求数(雪碧图的替代方案)。
  • 做法:把小于某个阈值(如 8KB)的图片 → 转成 data:image/png;base64,... 内嵌到 CSS/JS。
  • 优点:减少请求开销。
  • 缺点:会增大 bundle 体积,不利于缓存。
  • Webpackurl-loader / Webpack5 内置 asset/inline
  • Vite:内置支持,小于 assetsInlineLimit(默认 4KB)自动 base64。

(3)大图单独打包(hash 命名)
  • 目的:利用浏览器缓存,避免每次构建都重新下载。
  • 做法:文件名加 content hash(如 banner.abc123.png)。
  • Webpackfile-loader / Webpack5 内置 asset/resource
  • Vite:默认支持 → 生成 banner.xxxxx.png

👉 优点:

  • 文件内容不变 → hash 不变 → 浏览器走缓存。
  • 内容变了 → hash 更新 → 浏览器强制拉新。

(4)现代图片格式支持
  • webp / avif:相比 jpg/png 体积可减少 20%~70%。
  • 构建工具可自动生成多版本:banner.png + banner.webp,前端 <picture> 标签按需加载。
  • 插件:
    • Webpack:image-webpack-loader + responsive-loader
    • Vite:vite-plugin-imagemin / vite-imagetools

(5)字体与图标优化
  • 使用 woff2(比 woffttf 更小)。
  • subset:只打包用到的字符(比如只保留中文 + 英文字母)。
  • 替代方案:用 IconFontSVG SpriteIcon 组件库

Webpack 静态资源处理方式

背景与演变

在早期的 Webpack(v4 及之前),处理图片/字体/媒体等静态资源,主要依赖 loader

  • file-loader:复制文件 → 输出目录 → 返回 url。
  • url-loader:小文件转 base64,大文件走 file-loader。
  • raw-loader:直接导入文件的源字符串。

但从 Webpack 5 开始,官方内置了 Asset Modules,可以替代这些 loader,让配置更简洁。


Webpack 5 的 Asset Modules

Webpack5 提供了 4 种内置类型:

  1. asset/resource
    • 类似 file-loader
    • 把文件输出到指定目录,返回 url。
    • 适合 大图 / 字体 / 视频
{
  test: /\.(png|jpg|gif)$/i,
  type: 'asset/resource',
  generator: {
    filename: 'images/[hash][ext][query]' // 自定义输出目录+命名
  }
}
  1. asset/inline
    • 类似 url-loader,直接转成 Base64 内联。
    • 适合 小图标、极小图片
{
  test: /\.svg$/i,
  type: 'asset/inline'
}
  1. asset/source
    • 类似 raw-loader,导入文件源码字符串。
    • 适合 SVG、Markdown 模板等
{
  test: /\.txt$/i,
  type: 'asset/source'
}
  1. asset
    • 自动选择:小于某阈值 → base64,超过阈值 → 单独文件。
    • 默认阈值 8KB,可以自定义:
{
  test: /\.(png|jpg)$/i,
  type: 'asset',
  parser: {
    dataUrlCondition: {
      maxSize: 4 * 1024 // 4KB 以下转 base64
    }
  }
}

👉 总结:Webpack5 一步到位,几乎可以抛弃 file-loaderurl-loader


常见静态资源处理场景
(1)图片处理
  • 压缩
    使用 image-webpack-loaderimagemin-webpack-plugin
{
  test: /\.(jpe?g|png|gif|svg)$/i,
  type: 'asset',
  use: [
    {
      loader: 'image-webpack-loader',
      options: {
        mozjpeg: { progressive: true, quality: 65 },
        optipng: { enabled: false },
        pngquant: { quality: [0.65, 0.90], speed: 4 },
        gifsicle: { interlaced: false },
        webp: { quality: 75 }
      }
    }
  ]
}
  • 小图内联 / 大图分离
    通过 type: 'asset' + parser.maxSize 控制。

(2)字体文件
  • 常见格式:woff2(推荐)、woffttfeot
  • 配置:
{
  test: /\.(woff2?|eot|ttf|otf)$/i,
  type: 'asset/resource',
  generator: {
    filename: 'fonts/[name].[hash:8][ext]'
  }
}
  • 优化:
    • 只保留必要字符(用 subset-font 工具)。
    • 优先使用 woff2

(3)媒体文件(音视频)
  • 格式:mp4 / webm / mp3 / ogg
  • 配置:
{
  test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)$/i,
  type: 'asset/resource',
  generator: {
    filename: 'media/[name].[hash:8][ext]'
  }
}
  • 优化:
    • 大文件建议走 CDN。
    • 视频可拆分成多码率 HLS(前端 <video> 自适应)。

Webpack 静态资源优化技巧
  1. 哈希命名 + 缓存优化
    • [contenthash] 保证资源未变时缓存有效。
    • [hash] 每次构建不同,不推荐用于生产缓存。
  2. 分目录管理
    • images/xxx.png
    • fonts/xxx.woff2
    • media/xxx.mp4
  3. 现代格式
    • 使用 webp/avif 替代 jpg/png
    • 配合 <picture>srcset
  4. 异步加载资源
    • 通过 import() 动态加载图片或 JSON。
    • 减少初始 bundle 体积。

小结
  • Webpack4 及之前 → 依赖 loader(file-loader/url-loader/raw-loader)。
  • Webpack5 → 内置 asset modules,简化配置。
  • 常见优化手段:压缩(image-webpack-loader)、小图内联、大图哈希命名、现代格式、字体子集化、媒体分离。

Webpack 静态资源优化最佳实践
// webpack.config.js
const path = require('path');

module.exports = {
  mode: 'production', // 启用优化(压缩、tree-shaking 等)
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'js/[name].[contenthash:8].js', // JS 按内容哈希
    clean: true, // 每次构建清理 dist
  },

  module: {
    rules: [
      /** ==================== 图片 ==================== */
      {
        test: /\.(png|jpe?g|gif|svg)$/i,
        type: 'asset', // 小图转 base64,大图生成文件
        parser: {
          dataUrlCondition: {
            maxSize: 4 * 1024, // 4KB 以下内联
          },
        },
        generator: {
          filename: 'images/[name].[contenthash:8][ext]', // hash 命名
        },
        use: [
          {
            loader: 'image-webpack-loader',
            options: {
              mozjpeg: { progressive: true, quality: 70 },
              pngquant: { quality: [0.65, 0.9], speed: 4 },
              gifsicle: { interlaced: false },
              webp: { quality: 75 }, // 转 webp
            },
          },
        ],
      },

      /** ==================== 字体 ==================== */
      {
        test: /\.(woff2?|eot|ttf|otf)$/i,
        type: 'asset/resource', // 总是输出独立文件
        generator: {
          filename: 'fonts/[name].[contenthash:8][ext]',
        },
      },

      /** ==================== 媒体文件(音视频) ==================== */
      {
        test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)$/i,
        type: 'asset/resource',
        generator: {
          filename: 'media/[name].[contenthash:8][ext]',
        },
      },

      /** ==================== CSS / SCSS ==================== */
      {
        test: /\.(css|scss)$/i,
        use: [
          'style-loader', // 或 MiniCssExtractPlugin.loader(推荐生产用)
          'css-loader',
          {
            loader: 'postcss-loader',
            options: {
              postcssOptions: {
                plugins: [
                  ['autoprefixer'],
                  ['cssnano', { preset: 'default' }], // 压缩 CSS
                ],
              },
            },
          },
          'sass-loader',
        ],
      },
    ],
  },

  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
        },
      },
    },
    runtimeChunk: 'single', // 提取 runtime,避免缓存失效
  },

  plugins: [
    // 如果用 MiniCssExtractPlugin
    // new MiniCssExtractPlugin({
    //   filename: 'css/[name].[contenthash:8].css',
    // }),
  ],
};

配置要点解析
  1. 图片优化
    • type: 'asset' → 小图(<4KB)自动转 base64,减少 HTTP 请求。
    • image-webpack-loader → 压缩 jpeg/png/gif/svg,支持输出 WebP。
    • filename: 'images/[name].[contenthash:8][ext]' → 避免缓存失效。
  2. 字体优化
    • woff2 优先,体积更小、兼容性好。
    • 单独存放在 fonts/,方便 CDN 分发。
  3. 媒体文件
    • 全部 asset/resource,走单独文件,避免打包进 JS。
    • 大文件可迁移到 CDN。
  4. CSS/SCSS
    • 使用 postcss-loader + autoprefixer → 自动加前缀,兼容性更好。
    • cssnano → 压缩 CSS。
    • MiniCssExtractPlugin(推荐生产) → 抽离 CSS 文件,避免 style 标签内联过多。
  5. 缓存优化
    • contenthash:8 → 只在内容变化时更新,减少重复下载。
    • splitChunks → 提取公共库(vendors.js)。
    • runtimeChunk: 'single' → runtime 独立,避免修改入口时所有文件缓存失效。

总结

这份配置清单解决了 四大问题

  1. 图片(小图内联 + 大图压缩 + hash 命名)。
  2. 字体(独立存放 + hash 缓存)。
  3. 媒体(独立存放 + CDN 友好)。
  4. CSS(压缩 + 抽离 + 自动前缀)。

最终效果:

  • 首屏资源小(小图内联、按需加载)。
  • 缓存利用率高(contenthash)。
  • 文件分目录管理清晰
  • 兼容现代格式(webp/woff2)

Vite静态资源处理方式

背景

Vite 有两种运行模式:

  • 开发模式(dev):基于浏览器原生 ES Module,静态资源按需加载,不打包。
  • 生产模式(build):调用 Rollup 打包,资源会 hash 命名并做优化。

所以:

  • 开发时几乎零等待,直接加载源文件。
  • 打包时利用 Rollup plugin,做更强的优化(hash、压缩、拆分)。

静态资源导入方式
(1)作为 URL 引入

.png.jpg.svg 等文件,可以直接 import,得到的是构建后的 URL:

import logoUrl from './logo.png'
document.querySelector('img').src = logoUrl
  • 开发环境:返回本地文件路径(/src/assets/logo.png)。
  • 构建后:复制到 dist/assets 并加 hash,例如:
dist/assets/logo.8d2f3a.png

(2)作为字符串引入(raw)
  • ?raw:直接返回文件内容(字符串)。
import txt from './note.txt?raw'
console.log(txt) // 文件源内容
  • 适合导入 SVG、Markdown、纯文本

(3)作为 Data URI 引入(base64 内联)
  • ?url:强制返回资源的 URL。
  • ?inline:强制内联(base64 编码)。
import smallIcon from './icon.svg?inline'
// smallIcon = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0i...'

👉 对比 Webpack,这里不需要区分 loader,直接通过 查询参数 控制。


(4)环境变量 + Public 目录
  • /public 下的文件不会被打包,而是原样拷贝到 dist。
  • 使用时直接通过 /xxx.png 访问。
    • 适合 CDN 链接、外部引用 的静态文件(如 favicon.ico)。

Vite 的优化策略(生产模式)

Vite 基于 Rollup,所以资源优化大部分依赖 Rollup 插件或 Vite 内置规则。

(1)图片优化
  • Vite 本身不自带压缩,但可以配合插件:
    • vite-plugin-imagemin → 用 imagemin 压缩 jpg/png/webp/svg。
    • vite-plugin-vue-images → 自动导入图片。

示例:

import viteImagemin from 'vite-plugin-imagemin'

export default {
  plugins: [
    viteImagemin({
      mozjpeg: { quality: 65 },
      pngquant: { quality: [0.65, 0.9] },
      webp: { quality: 75 }
    })
  ]
}

(2)字体 & 媒体文件

默认走 hash 命名,配置 build.assetsInlineLimit 可以控制是否内联:

export default {
  build: {
    assetsInlineLimit: 4096 // 4kb 以下转 base64
  }
}

常用场景:

  • 小图标 / 字体 → base64 内联。
  • 大图 / 视频 / 音频 → 输出文件并加 hash。

(3)资源缓存 & Hash
  • 生产构建时,Vite 会自动生成:
dist/assets/logo.8d2f3a.png
dist/assets/font.3a9d2f.woff2
  • 浏览器缓存策略:文件名变了才重新下载(和 Webpack contenthash 思路一致)。

(4)现代格式支持
  • Vite 支持 webp / avif,推荐用 <picture> 标签:
<picture>
  <source srcset="/logo.avif" type="image/avif" />
  <source srcset="/logo.webp" type="image/webp" />
  <img src="/logo.png" alt="logo" />
</picture>

对比 Webpack
特性Webpack(v5)Vite(Rollup)
配置方式asset/resource
asset/inline
?url
/ ?inline
/ ?raw
参数
小图内联parser.maxSizebuild.assetsInlineLimit
图片压缩image-webpack-loadervite-plugin-imagemin
public 目录CopyWebpackPlugin/public
直接拷贝
默认缓存策略[contenthash]自动 hash 文件名
开发时资源加载走 loader(需打包)原生 ESM,不打包,按需请求

👉 差异总结

  • Webpack → 强依赖 loader/plugin ,资源处理粒度更细,但配置复杂。
  • Vite → 借助 ES Module 原生支持 + Rollup ,直接用 query 参数控制,思路更轻量。

Vite 静态资源优化最佳实践
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import legacy from '@vitejs/plugin-legacy'
import viteImagemin from 'vite-plugin-imagemin'

export default defineConfig({
  plugins: [
    vue(),

    /** ========== 兼容性优化 ========== */
    legacy({
      targets: ['defaults', 'not IE 11'], // 自动 polyfill
    }),

    /** ========== 图片压缩 ========== */
    viteImagemin({
      gifsicle: { optimizationLevel: 7, interlaced: false },
      optipng: { optimizationLevel: 7 },
      mozjpeg: { quality: 70 },
      pngquant: { quality: [0.65, 0.9], speed: 4 },
      svgo: { plugins: [{ name: 'removeViewBox' }, { name: 'removeEmptyAttrs', active: false }] },
    }),
  ],

  build: {
    /** ========== 输出目录 & 缓存优化 ========== */
    outDir: 'dist',
    assetsDir: 'assets',
    sourcemap: false,
    minify: 'esbuild', // 比 terser 更快

    rollupOptions: {
      output: {
        chunkFileNames: 'js/[name]-[hash].js',
        entryFileNames: 'js/[name]-[hash].js',
        assetFileNames: ({ name }) => {
          if (/\.(gif|jpe?g|png|svg|webp)$/.test(name ?? '')) {
            return 'images/[name]-[hash][extname]'
          }
          if (/\.(woff2?|eot|ttf|otf)$/.test(name ?? '')) {
            return 'fonts/[name]-[hash][extname]'
          }
          if (/\.(mp4|webm|ogg|mp3|wav|flac|aac)$/.test(name ?? '')) {
            return 'media/[name]-[hash][extname]'
          }
          return 'assets/[name]-[hash][extname]'
        },
      },
    },

    /** ========== 代码分割优化 ========== */
    cssCodeSplit: true, // CSS 按页面分割
    rollupOptions: {
      output: {
        manualChunks: {
          vue: ['vue'],
          vendor: ['axios', 'lodash'], // 拆 vendor 包
        },
      },
    },

    /** ========== 浏览器缓存优化 ========== */
    brotliSize: false, // 关闭 brotli 计算(加快构建速度)
    assetsInlineLimit: 4096, // <4KB 转 base64
  },
})

配置要点解析
  1. 图片优化
    • vite-plugin-imagemin → 压缩 png/jpeg/gif/svg,支持 webp。
    • assetsInlineLimit: 4096 → 小图转 base64,大图独立文件。
  2. 字体 & 媒体文件
    • assetFileNames → 按目录分类(images/fonts/media),并加 hash。
    • 方便 CDN 缓存和管理。
  3. CSS 优化
    • cssCodeSplit: true → 按页面拆分 CSS,而不是一个大 bundle。
    • 内置 PostCSS + Autoprefixer,额外插件放 postcss.config.js 即可。
  4. 代码压缩
    • minify: 'esbuild' → Vite 默认,速度比 Terser 快 20 倍。
    • 产物小、构建快。
  5. 代码分割
    • manualChunks → 手动拆 vendor,避免业务代码和依赖耦合。
    • 首屏只加载必要模块,其余按需异步。
  6. 兼容性
    • @vitejs/plugin-legacy → 自动加 polyfill,兼容旧浏览器。


5. 输出产物(Output)

这一阶段就是:构建工具把前面处理好的 代码 + 资源,写入到磁盘上的最终目录(如 dist/),供浏览器或服务器使用。


整体脉络

自动化构建完整主线:

源码(.vue / .ts / .scss ...)
    ↓ 1. 代码转换(Transpile)
    ↓ 2. 资源合并(Bundle)
    ↓ 3. 优化(Minify / Tree Shaking / Code Splitting)
    ↓ 4. 静态资源处理(图片、字体、hash 命名)
    ↓ 5. 输出产物(Output)
最终产物:dist/ 目录

👉 Output 就是这条链路的终点。前面所有的工作,都是为了得到这一步的产物。


输出产物包含什么?

构建工具会把各种处理过的结果统一输出。常见有:

  1. JavaScript
    • 按分包策略生成:app.jsvendor.jschunk-xxx.js
    • 文件名里常常带 hash(如 app.abc123.js),用于 缓存优化
  2. CSS
    • .scss / .less / .css 打包过来的结果
    • 独立文件:style.abc123.css
  3. HTML
    • 一般是 index.html,里面自动注入 <script><link>,指向上面生成的 JS 和 CSS。
  4. 静态资源
    • 图片、字体、媒体文件。
    • 小文件可能被内联为 Base64,大文件被复制到 assets/ 并加上 hash 命名。

Webpack 的 Output 机制

Webpack 配置里有个 output 字段,控制产物写入方式:

// webpack.config.js
module.exports = {
  entry: './src/main.js',
  output: {
    path: path.resolve(__dirname, 'dist'),  // 输出目录
    filename: '[name].[contenthash].js',    // 输出 JS 名称
    clean: true                             // 构建前清空 dist/
  }
}

运行 webpacknpm run build 后,结果大概是:

dist/
  ├─ index.html       # 插入正确的 script/link
  ├─ app.1a2b3c.js
  ├─ vendor.9z8y7x.js
  ├─ style.4f5g6h.css
  └─ assets/
       ├─ logo.7h8j9k.png
       └─ font.3r2t1y.woff

Vite 的 Output 机制

Vite 内部打包也是用 Rollup,所以 output 规则由 Rollup 控制。

vite.config.js 里配置:

// vite.config.js
export default {
  build: {
    outDir: 'dist',   // 输出目录
    rollupOptions: {
      output: {
        entryFileNames: 'assets/[name].[hash].js',
        chunkFileNames: 'assets/[name].[hash].js',
        assetFileNames: 'assets/[name].[hash].[ext]'
      }
    }
  }
}

Vite 打包后产物:

dist/
  ├─ index.html
  ├─ assets/
       ├─ main.abc123.js
       ├─ vendor.def456.js
       ├─ index.ghi789.css
       └─ logo.klm000.png

开发者能做什么?

你作为前端工程师,在 Output 阶段能关注和优化的点有:

  • 文件命名规则
    • 用 hash 确保缓存更新
    • 合理拆分 app/vendor/chunk
  • 目录结构
    • 资源分到 assets/,CSS/JS 分开存放,利于部署和 CDN 缓存
  • HTML 注入
    • 让工具(HtmlWebpackPlugin / Vite 内置)自动插入资源,避免手动管理 <script>
  • 部署优化
    • 确保产物能直接丢到服务器或 CDN,用 nginx / apache 做静态托管

常见疑惑 & 解答

Q1:为什么文件名要加 hash?
👉 因为浏览器有缓存。如果 JS/CSS 更新但文件名没变,用户可能加载到旧版本。加上 [contenthash],文件内容变了,hash 就变,浏览器就会请求新文件。

Q2:为什么要拆分成 app.js、vendor.js?
👉 vendor(第三方库)更新频率低,可以长期缓存。app.js(业务代码)经常变。拆分能让用户更新更快,缓存更高效。

Q3:最后运行的是 index.html 还是 JS?
👉 用户访问网页时,首先加载的是 index.html,但 HTML 本身很轻,它只是入口,里面 <script src="app.js"> 会触发 JS 加载,逻辑才跑起来。


总结一句话
Output 就是把所有编译、打包、优化过的资源,按规则写到 dist/ 目录,供浏览器/服务器使用。

  • Webpack:通过 output 字段配置产物规则。
  • Vite:通过 Rollup 的 output 配置产物规则。
  • 你的关注点:命名、拆分、缓存、目录结构