为什么需要前端工程化

十年前写前端,一个 HTML 文件加一个 CSS 文件加一个 JS 文件就是全部。现在的前端项目动辄几百个组件、几十个第三方依赖、TypeScript 要编译、Less/Sass 要预处理、图片要压缩、代码要打包。这些事情手动做不完,也不该手动做——这就是前端工程化要解决的问题:把重复的构建、打包、优化工作交给工具,开发者只写业务代码。

一个现代前端项目的工具链通常包括:包管理器(npm/pnpm)、构建工具(Vite/Webpack)、语言转译(TypeScript/Babel)、代码规范(ESLint/Prettier)、测试(Vitest)。这些工具互相配合,让"写代码"和"上线"之间的距离尽量短。

工程化的目标不是堆工具,而是让团队里任何人 clone 下项目、装依赖、跑一条命令,就能在本地复现和线上一致的开发环境。
本文核心知识点
  • Vite基于浏览器原生 ESM 的开发服务器,冷启动毫秒级。
  • 分包把第三方依赖和业务代码拆成不同 chunk,利用浏览器缓存。
  • Tree Shaking自动剔除没被用到的导出,减小产物体积。
  • 缓存策略文件名带内容 hash,配合长缓存实现增量更新。
前端工程化示意:蓝紫渐变背景左侧多个模块方块通过发光管线汇入中央发光核心,右侧齿轮组代表构建工具链
构建工具是流水线:源代码从左边进,优化压缩后的产物从右边出。

Vite 为什么比 Webpack 快

用过老 Webpack 项目的人都知道,项目一大,启动要等几十秒甚至几分钟,改一行代码热更新也要等好几秒。Vite 解决这个问题的思路完全不同:开发环境下不打包,而是让浏览器自己去加载模块。

原理是这样的:现代浏览器都原生支持 ES Module,浏览器请求哪个文件,Vite 就实时编译哪个文件返回。项目再大,启动时间也只是"把服务器起来"那一瞬间——因为它不需要先把所有模块打成一个包。热更新也一样:改了哪个文件,浏览器只重新请求那一个文件,速度和文件大小成正比,和项目整体大小无关。

维度Webpack(传统)Vite(现代)
启动方式先打包整个项目再启动起服务器,按需编译
冷启动大项目几十秒毫秒级
热更新重新打包改动模块及依赖浏览器直接请求改的文件
生产构建Webpack 本身底层用 Rollup
配置复杂度plugin / loader 概念多开箱即用,约定优于配置

生产环境 Vite 仍然要把代码打包成浏览器能高效加载的静态文件,这一步底层用的是 Rollup——一个以产物体积小著称的打包器。所以 Vite 是"开发用 ESM 极速体验,生产用 Rollup 最优产物"的组合。

从零初始化一个 Vite 项目

官方推荐用 npm create 命令初始化,选对应模板即可:

# 初始化一个 Vue 项目(React / Vanilla 同理换模板名)
npm create vite@latest my-app -- --template vue

cd my-app
npm install
npm run dev      # 启动开发服务器,默认 http://localhost:5173

项目结构非常简洁:index.html 是入口(注意 Vite 的入口在根目录而不是 public),src/ 放所有源代码,vite.config.js 是配置文件。和老工具最大的区别是:不用写一堆 loader 配置,开箱就支持 TypeScript、JSX、CSS 预处理——需要什么装什么插件,不需要就在配置里加一行。

// vite.config.js —— 一个最小配置
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  server: {
    port: 5173,
    open: true,      // 启动自动开浏览器
  },
})

路径别名、环境变量与代理配置

项目写深了,../../components/Button 这种相对路径会让人崩溃。Vite 内置了路径别名,配合 TypeScript 配置就能用 @/components/Button:

// vite.config.js
import { defineConfig } from 'vite'
import { fileURLToPath } from 'node:url'

export default defineConfig({
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
  server: {
    proxy: {
      // 开发环境把 /api 开头的请求转发到后端
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
      },
    },
  },
})

环境变量放在项目根目录的 .env 文件里,以 VITE_ 开头的变量会被注入到前端代码中,通过 import.meta.env.VITE_XXX 读取。分环境用 .env.development 和 .env.production,开发和构建时自动加载对应的文件。

# .env.development
VITE_API_BASE=/api

# .env.production
VITE_API_BASE=https://api.example.com

// 代码里使用
const base = import.meta.env.VITE_API_BASE

构建产物分包与 Tree Shaking

npm run build 之后,产物默认放在 dist/ 目录。打开一看,往往有一个几百 KB 的 js 文件——这是因为所有第三方依赖和业务代码被打到一起了。合理的做法是手动分包:把不常变的第三方库拆成独立 chunk,用户下次访问时浏览器直接用缓存。

// vite.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'vue-vendor': ['vue', 'vue-router', 'pinia'],
          'utils-vendor': ['lodash-es', 'axios'],
        },
      },
    },
  },
})

这样打包后会生成 vue-vendor.abc123.js、utils-vendor.def456.js 等独立文件。业务代码更新时,只要依赖版本没变,这些 vendor chunk 的 hash 就不变,浏览器直接用本地缓存——这是首屏加载速度提升最明显的一个优化。

Tree Shaking 是默认开启的:如果你的工具库用 ES Module 导出(比如 lodash-es 而不是 lodash),构建时会自动把没被 import 的函数从产物里剔除。举个例子,import 了 lodash-es 的 debounce,最终产物里只会有 debounce 相关的代码,不会把整个 lodash 打进来。