前端工程化进阶:从搭建完整项目脚手架到性能优化【技术类】
本文分享了一位新晋程序员的学习笔记,涵盖机器人科普、技术文章以及前端工程化实践。重点介绍了基于Vite+Vue3/React18的企业级脚手架搭建流程,包括路由配置、状态管理、请求封装等核心功能。详细讲解了如何通过ESLint+Prettier+husky实现代码规范闭环校验,以及使用Jest/Vitest进行自动化测试的方法。最后从打包优化、首屏加载、缓存策略三个方面提供了性能优化方案,形成完整
新晋码农一枚,小编会定期整理一些写的比较好的代码和知识点,作为自己的学习笔记,试着做一下批注和补充,转载或者参考他人文献会标明出处,非商用,如有侵权会删改!欢迎大家斧正和讨论!本章内容较多,可点击文章目录进行跳转!
小编整理和学习了机器人的相关知识,可作为扫盲使用,后续也会更新一些技术类的文章,大家共同交流学习!
您的点赞、关注、收藏就是对小编最大的动力!
机器人系列文章
机器人的“神经网络”:以太网技术如何重塑机器人内部通信?【技术类】
CRC校验:二进制除法的魔法如何守护你的数据安全?【技术类】
解密ROS:机器人时代的“安卓系统”,凭什么让开发者集体狂欢?【科普类】
边缘计算与云计算的协同发展:未来算力布局的核心逻辑【科普类】
前言
在前端技术飞速迭代的今天,“工程化”已不再是大厂专属的高端概念,而是中小团队提升开发效率、保障项目质量、降低维护成本的核心支撑。当项目规模从单人小demo升级为多人协作的企业级应用,零散的开发模式、混乱的代码规范、繁琐的手动部署,都会成为制约项目推进的瓶颈。2026年,随着Vite、Vue3、React18等工具与框架的成熟,前端工程化迎来了“轻量化、高效化、标准化”的新阶段,掌握从脚手架搭建到性能优化的全链路工程化能力,已成为前端开发者从“初级”迈向“进阶”的关键门槛。
本文聚焦前端工程化核心落地路径,从工程化组成架构拆解,到基于Vite+Vue3/React18的企业级脚手架搭建,再到代码规范、自动化测试、性能优化的实战落地,全程配套详细步骤与避坑指南,帮助前端工程师系统性提升工程化能力,让项目开发从“无序混乱”走向“标准化高效协同”。
一、认知升级:现代前端工程化的核心组成
前端工程化的本质,是通过工具链与规范体系,将前端开发的全流程(构建、编码、测试、部署)标准化、自动化,解决“多人协作一致性”“开发与生产环境差异”“项目可维护性”三大核心问题。现代前端工程化并非单一工具的应用,而是由构建工具、代码规范、自动化测试、部署流程四大核心模块构成的完整体系,各模块相互协同,形成闭环。
1.1 构建工具:工程化的核心引擎
构建工具是前端工程化的基石,负责将开发者编写的源代码(Vue/React组件、TypeScript、SCSS等)转换为浏览器可识别的静态资源,核心能力包括代码编译、模块打包、热更新、环境变量注入等。近年来,构建工具从Webpack的“全能型”向Vite、Turbopack的“轻量化、高速化”演进,解决了传统构建工具冷启动慢、热更新延迟高的痛点。
当前主流构建工具各有侧重:Webpack生态完善、适配场景广,适合复杂大型项目;Vite基于ESModule实现按需编译,冷启动速度较Webpack提升10倍以上,成为Vue3、React18项目的首选;Turbopack依托Rust语言,在大型项目中性能优势显著,但生态尚在完善中。选择构建工具的核心原则的是“适配项目规模+团队技术栈”,中小项目优先选Vite,复杂项目可选用Webpack搭配Vite做开发环境优化。
1.2 代码规范:多人协作的“统一语言”
多人协作开发中,代码风格不一致、语法错误、潜在bug等问题,会大幅增加代码评审成本与维护难度。代码规范体系通过“自动化校验+格式化”,强制约束编码行为,确保团队代码风格统一、质量可控。其核心包含两部分:一是代码语法与风格规范(如变量命名、缩进格式、语句规范),二是代码质量规范(如避免死循环、禁止未使用变量、规范函数复杂度)。
主流工具组合为“ESLint+Prettier”:ESLint负责语法校验与质量检测,可针对性配置Vue/React/TypeScript规则;Prettier专注代码格式化,统一缩进、引号、换行等风格,两者配合可实现“编码即规范”。同时,通过husky在Git提交环节添加钩子,可强制拦截不符合规范的代码,从源头保障代码质量。
1.3 自动化测试:保障项目稳定性的“护城河”
前端项目迭代过程中,新增功能或修改代码极易引发“回归bug”,手动测试效率低、覆盖范围有限,难以满足企业级项目的稳定性需求。自动化测试通过编写测试用例,自动验证代码功能正确性,覆盖单元测试、组件测试、E2E测试等场景,实现“代码变更即验证”,大幅降低回归bug风险。
前端自动化测试工具生态已趋于成熟:Jest是通用型测试框架,支持断言、模拟、覆盖率统计,适配Vue/React项目;Vue Test Utils与React Testing Library分别为Vue、React组件测试提供专用API,模拟组件渲染与交互;Vitest作为Vite生态的测试工具,兼容Jest API,测试速度更快,成为Vue3项目的优选。企业级项目通常采用“单元测试+组件测试”组合,覆盖率目标建议不低于70%。
1.4 部署流程:从开发到上线的“自动化通道”
传统前端部署依赖手动打包、上传服务器,流程繁琐且易因环境差异导致线上问题。现代前端部署流程通过CI/CD(持续集成/持续部署)工具,实现“代码提交→自动构建→自动测试→自动部署”全链路自动化,缩短迭代周期,降低人为失误。同时,结合环境隔离(开发、测试、预发布、生产)、版本控制、回滚机制,保障上线流程的稳定性与可控性。
主流CI/CD工具包括Jenkins、GitHub Actions、GitLab CI,其中GitHub Actions与GitLab CI无需本地部署,可直接与代码仓库联动,配置简单,适合中小团队;Jenkins自定义能力强,适合复杂的部署场景。部署目标可根据项目需求选择静态资源服务器(Nginx)、云存储(OSS)、Serverless平台(Vercel、Netlify)等。
二、实战搭建:基于Vite+Vue3/React18的企业级脚手架
脚手架是前端工程化的“落地载体”,一个完善的企业级脚手架需集成构建工具、路由、状态管理、请求封装、代码规范、测试框架等核心能力,支持开发者快速启动项目,无需重复搭建基础架构。本节分别以Vite+Vue3、Vite+React18为技术栈,详细讲解企业级脚手架的搭建步骤,覆盖核心功能配置。
2.1 Vite+Vue3脚手架搭建(企业级标准)
2.1.1 基础项目初始化
首先确保本地已安装Node.js(版本≥18.0.0,建议使用nvm管理Node版本),执行以下命令初始化Vite+Vue3项目:
# 初始化项目,选择Vue+TypeScript
npm create vite@latest vue3-enterprise-scaffold -- --template vue-ts
# 进入项目目录
cd vue3-enterprise-scaffold
# 安装依赖
npm install
# 启动开发服务器
npm run dev
初始化完成后,项目基础结构包含src(源代码目录)、public(静态资源目录)、vite.config.ts(Vite配置文件)、tsconfig.json(TypeScript配置文件)等核心文件。接下来逐步完善企业级所需功能模块。
2.1.2 核心依赖安装与配置
安装路由、状态管理、请求封装等核心依赖:
# 安装路由(Vue Router 4.x)、状态管理(Pinia)、请求库(Axios)
npm install vue-router@4 pinia axios
# 安装UI组件库(以Element Plus为例,按需引入)
npm install element-plus @element-plus/icons-vue
# 安装样式预处理器(SCSS)
npm install sass --save-dev
路由配置(src/router/index.ts)
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
// 引入布局组件
import MainLayout from '@/layouts/MainLayout.vue'
// 路由规则
const routes: RouteRecordRaw[] = [
{
path: '/',
component: MainLayout,
redirect: '/home',
children: [
{
path: 'home',
name: 'Home',
component: () => import('@/views/HomeView.vue'),
meta: { title: '首页', requiresAuth: false }
}
]
},
// 登录页(无布局)
{
path: '/login',
name: 'Login',
component: () => import('@/views/LoginView.vue'),
meta: { title: '登录', requiresAuth: false }
},
// 404页面
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/NotFoundView.vue'),
meta: { title: '页面不存在' }
}
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
})
// 路由守卫:设置页面标题、权限校验
router.beforeEach((to) => {
document.title = to.meta.title as string || 'Vue3企业级脚手架'
// 权限校验逻辑(示例:未登录跳转登录页)
const isLogin = localStorage.getItem('token')
if (to.meta.requiresAuth && !isLogin) {
return '/login'
}
})
export default router
Pinia状态管理配置(src/store/modules/userStore.ts)
import { defineStore } from 'pinia'
import axios from '@/utils/request'
interface UserState {
token: string | null
userInfo: {
username: string
role: string
} | null
}
export const useUserStore = defineStore('user', {
state: (): UserState => ({
token: localStorage.getItem('token'),
userInfo: JSON.parse(localStorage.getItem('userInfo') || 'null')
}),
actions: {
// 登录动作
async login(username: string, password: string) {
const res = await axios.post('/api/login', { username, password })
const { token, userInfo } = res.data
this.token = token
this.userInfo = userInfo
// 本地存储
localStorage.setItem('token', token)
localStorage.setItem('userInfo', JSON.stringify(userInfo))
return res.data
},
// 退出登录
logout() {
this.token = null
this.userInfo = null
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
}
}
})
Axios请求封装(src/utils/request.ts)
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import { ElMessage, ElLoading } from 'element-plus'
import { useUserStore } from '@/store/modules/userStore'
// 创建Axios实例
const service: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL, // 环境变量配置基础地址
timeout: 5000,
headers: {
'Content-Type': 'application/json;charset=utf-8'
}
})
// 加载动画实例
let loadingInstance: ReturnType<typeof ElLoading.service> | null = null
// 请求拦截器:添加Token、显示加载动画
service.interceptors.request.use(
(config: AxiosRequestConfig) => {
const userStore = useUserStore()
// 添加Token到请求头
if (userStore.token) {
config.headers = config.headers || {}
config.headers.Authorization = `Bearer ${userStore.token}`
}
// 显示加载动画(非GET请求)
if (config.method !== 'get') {
loadingInstance = ElLoading.service({
lock: true,
text: '加载中...',
background: 'rgba(0, 0, 0, 0.5)'
})
}
return config
},
(error) => {
loadingInstance?.close()
ElMessage.error('请求参数错误')
return Promise.reject(error)
}
)
// 响应拦截器:处理错误、隐藏加载动画
service.interceptors.response.use(
(response: AxiosResponse) => {
loadingInstance?.close()
const { code, message, data } = response.data
// 业务错误处理
if (code !== 200) {
ElMessage.error(message || '操作失败')
// Token过期:退出登录并跳转登录页
if (code === 401) {
const userStore = useUserStore()
userStore.logout()
window.location.href = '/login'
}
return Promise.reject(new Error(message || 'Error'))
}
return data
},
(error) => {
loadingInstance?.close()
// 网络错误处理
if (error.message.includes('timeout')) {
ElMessage.error('请求超时,请重试')
} else if (error.message.includes('404')) {
ElMessage.error('接口不存在')
} else {
ElMessage.error('网络异常,请检查网络连接')
}
return Promise.reject(error)
}
)
export default service
Vite配置优化(vite.config.ts)
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import path from 'path'
// Element Plus按需引入插件
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
// 加载环境变量
const env = loadEnv(mode, process.cwd())
return {
plugins: [
vue(),
vueJsx(),
// 自动导入Element Plus API
AutoImport({
resolvers: [ElementPlusResolver()],
dts: path.resolve(path.resolve(__dirname, 'src'), 'auto-imports.d.ts')
}),
// 自动导入Element Plus组件
Components({
resolvers: [ElementPlusResolver()],
dts: path.resolve(path.resolve(__dirname, 'src'), 'components.d.ts')
})
],
// 路径别名配置
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'@assets': path.resolve(__dirname, 'src/assets'),
'@components': path.resolve(__dirname, 'src/components')
}
},
// 开发服务器配置
server: {
host: '0.0.0.0',
port: Number(env.VITE_DEV_PORT) || 3000,
open: true, // 自动打开浏览器
proxy: {
// 接口代理,解决跨域问题
'/api': {
target: env.VITE_API_BASE_URL,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
// 构建配置
build: {
outDir: 'dist',
assetsDir: 'static',
sourcemap: mode === 'development', // 开发环境生成sourcemap
rollupOptions: {
// 打包优化:分割代码
output: {
chunkFileNames: 'static/js/[name]-[hash].js',
entryFileNames: 'static/js/[name]-[hash].js',
assetFileNames: 'static/[ext]/[name]-[hash].[ext]'
}
}
}
}
})
环境变量配置(.env.development/.env.production)
.env.development(开发环境):
NODE_ENV=development
VITE_DEV_PORT=3000
VITE_API_BASE_URL=http://localhost:8080/api
VITE_APP_TITLE=Vue3企业级脚手架(开发环境)
.env.production(生产环境):
NODE_ENV=production
VITE_API_BASE_URL=https://api.example.com
VITE_APP_TITLE=Vue3企业级脚手架(生产环境)
2.2 Vite+React18脚手架搭建(企业级标准)
2.2.1 基础项目初始化
# 初始化项目,选择React+TypeScript
npm create vite@latest react18-enterprise-scaffold -- --template react-ts
# 进入项目目录
cd react18-enterprise-scaffold
# 安装依赖
npm install
# 启动开发服务器
npm run dev
2.2.2 核心依赖安装与配置
路由配置(src/router/index.tsx)
import { createBrowserRouter, RouterProvider, Navigate } from 'react-router-dom'
import { lazy, Suspense } from 'react'
// 加载组件
import Loading from '@/components/Loading'
// 布局组件
import MainLayout from '@/layouts/MainLayout'
// 懒加载页面组件
const Home = lazy(() => import('@/views/Home'))
const Login = lazy(() => import('@/views/Login'))
const NotFound = lazy(() => import('@/views/NotFound'))
// 权限校验组件
const RequireAuth = ({ children }: { children: React.ReactNode }) => {
const token = localStorage.getItem('token')
if (!token) {
return <Navigate to="/login" replace />
}
return <>{children}</>
}
// 路由配置
const router = createBrowserRouter([
{
path: '/',
element: (
<RequireAuth>
<MainLayout />
</RequireAuth>
),
children: [
{
path: '/',
element: <Navigate to="/home" replace />
},
{
path: 'home',
element: (
<Suspense fallback={<Loading />}>
<Home />
</Suspense>
)
}
]
},
{
path: '/login',
element: (
<Suspense fallback={<Loading />}>
<Login />
</Suspense>
)
},
{
path: '*',
element: <NotFound />
}
])
// 路由提供者组件
export default function AppRouter() {
return <RouterProvider router={router} />
}
Redux状态管理配置(src/store/modules/userSlice.ts)
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'
import axios from '@/utils/request'
interface UserState {
token: string | null
userInfo: {
username: string
role: string
} | null
loading: boolean
error: string | null
}
// 初始状态
const initialState: UserState = {
token: localStorage.getItem('token'),
userInfo: JSON.parse(localStorage.getItem('userInfo') || 'null'),
loading: false,
error: null
}
// 异步登录动作
export const loginAsync = createAsyncThunk(
'user/login',
async ({ username, password }: { username: string; password: string }, { rejectWithValue }) => {
try {
const res = await axios.post('/api/login', { username, password })
return res.data
} catch (error: any) {
return rejectWithValue(error.response?.data?.message || '登录失败')
}
}
)
// 用户Slice
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
// 退出登录
logout: (state) => {
state.token = null
state.userInfo = null
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
},
clearError: (state) => {
state.error = null
}
},
extraReducers: (builder) => {
builder
.addCase(loginAsync.pending, (state) => {
state.loading = true
state.error = null
})
.addCase(loginAsync.fulfilled, (state, action: PayloadAction<{ token: string; userInfo: any }>) => {
state.loading = false
state.token = action.payload.token
state.userInfo = action.payload.userInfo
localStorage.setItem('token', action.payload.token)
localStorage.setItem('userInfo', JSON.stringify(action.payload.userInfo))
})
.addCase(loginAsync.rejected, (state, action) => {
state.loading = false
state.error = action.payload as string
})
}
})
export const { logout, clearError } = userSlice.actions
export default userSlice.reducer
全局Store配置(src/store/index.ts)
import { configureStore } from '@reduxjs/toolkit'
import userReducer from './modules/userSlice'
export const store = configureStore({
reducer: {
user: userReducer
// 后续可添加更多模块
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false // 允许非序列化值(如Date)
})
})
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
Vite配置、Axios封装与Vue3版本类似,可参考上文调整,核心差异在于框架专属插件与语法适配,此处不再赘述。脚手架搭建完成后,需集成代码规范、自动化测试模块,形成完整的工程化体系。
三、规范落地:ESLint+Prettier+husky打造闭环校验体系
代码规范的核心是“自动化强制执行”,而非依赖开发者自觉。本节通过ESLint+Prettier配置实现语法与风格校验,结合husky与lint-staged在Git提交环节拦截不规范代码,打造“编码→保存→提交”全链路规范校验体系。
3.1 基础依赖安装
# 安装ESLint及Vue/React适配插件
npm install eslint eslint-plugin-vue @typescript-eslint/eslint-plugin @typescript-eslint/parser --save-dev # Vue3项目
# npm install eslint eslint-plugin-react eslint-plugin-react-hooks @typescript-eslint/eslint-plugin @typescript-eslint/parser --save-dev # React18项目
# 安装Prettier及与ESLint兼容插件
npm install prettier eslint-config-prettier eslint-plugin-prettier --save-dev
# 安装husky与lint-staged(Git提交钩子)
npm install husky lint-staged --save-dev
# 初始化husky
npx husky install
在package.json中添加脚本,确保项目启动时自动启用husky:
"scripts": {
"prepare": "husky install"
}
3.2 ESLint配置(.eslintrc.js)
Vue3+TypeScript项目配置
module.exports = {
root: true,
env: {
browser: true,
es2021: true,
node: true
},
// 解析器配置
parser: 'vue-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser',
ecmaVersion: 'latest',
sourceType: 'module'
},
// 插件配置
plugins: ['vue', '@typescript-eslint', 'prettier'],
// 扩展配置:整合ESLint、Vue、Prettier规则
extends: [
'eslint:recommended',
'plugin:vue/vue3-essential',
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended' // 使Prettier规则优先级高于ESLint
],
// 自定义规则
rules: {
// 关闭console(生产环境)
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
// 关闭debugger
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
// TypeScript规则:允许any类型
'@typescript-eslint/no-explicit-any': 'off',
// Vue规则:允许单文件组件无setup语法糖
'vue/setup-compiler-macros': 'off',
// Prettier规则:将格式化错误视为错误
'prettier/prettier': 'error'
}
}
3.3 Prettier配置(.prettierrc.js)
module.exports = {
printWidth: 120, // 每行最大长度
tabWidth: 2, // 缩进宽度
useTabs: false, // 使用空格代替Tab
semi: true, // 语句末尾加分号
singleQuote: true, // 使用单引号
quoteProps: 'as-needed', // 仅在必要时为对象属性加引号
jsxSingleQuote: false, // JSX使用双引号
trailingComma: 'all', // 数组、对象末尾加逗号
bracketSpacing: true, // 对象字面量括号内加空格
bracketSameLine: false, // 标签闭合括号换行
arrowParens: 'always', // 箭头函数参数始终加括号
rangeStart: 0,
rangeEnd: Infinity,
requirePragma: false,
insertPragma: false,
proseWrap: 'preserve',
htmlWhitespaceSensitivity: 'css',
vueIndentScriptAndStyle: true, // Vue单文件组件中脚本和样式缩进
endOfLine: 'lf', // 行结束符使用LF
embeddedLanguageFormatting: 'auto'
}
3.4 Git提交钩子配置(husky+lint-staged)
配置lint-staged(package.json)
"lint-staged": {
"*.{js,jsx,ts,tsx,vue}": [
"eslint --fix", // 自动修复ESLint错误
"prettier --write" // 自动格式化
],
"*.{css,scss,less}": [
"prettier --write"
],
"*.{json,md}": [
"prettier --write"
]
}
添加husky钩子
# 添加pre-commit钩子:提交前执行lint-staged
npx husky add .husky/pre-commit "npx lint-staged"
# 添加commit-msg钩子:校验提交信息格式(可选,推荐)
npx husky add .husky/commit-msg "npx --no -- commitlint --edit $1"
# 安装commitlint(如需校验提交信息)
npm install @commitlint/cli @commitlint/config-conventional --save-dev
创建commitlint配置文件(commitlint.config.js):
module.exports = {
extends: ['@commitlint/config-conventional']
}
配置完成后,每次Git提交时,会先对暂存区文件执行ESLint修复与Prettier格式化,若存在无法自动修复的错误,将拦截提交并提示,从源头保障代码规范。
四、自动化测试入门:Jest+组件测试框架实战
自动化测试的核心目标是“验证功能正确性”与“预防回归bug”,前端测试优先覆盖核心业务逻辑与公共组件,本节以Vue3+Vitest、React18+Jest为例,讲解单元测试与组件测试的基础实现。
4.1 Vue3+Vitest测试配置
4.1.1 依赖安装
# 安装Vitest、Vue Test Utils、测试相关依赖
npm install vitest @vitest/ui @vue/test-utils jsdom --save-dev
# 安装测试覆盖率工具
npm install @vitest/coverage-v8 --save-dev
4.1.2 配置文件(vitest.config.ts)
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
},
test: {
environment: 'jsdom', // 模拟浏览器环境
globals: true, // 全局注入test、expect等API
setupFiles: ['./src/test/setup.ts'], // 测试初始化文件
coverage: {
provider: 'v8',
include: ['src/**/*.{vue,ts}'],
exclude: ['src/main.ts', 'src/router/**', 'src/store/**'],
reporter: ['text', 'html'] // 生成文本与HTML格式覆盖率报告
}
}
})
4.1.3 编写测试用例
组件测试(src/components/Button/Button.test.tsx)
import { mount } from '@vue/test-utils'
import Button from './Button.vue'
describe('Button组件', () => {
// 测试默认渲染
it('默认渲染正确的文本与样式', () => {
const wrapper = mount(Button, {
props: {
label: '测试按钮'
}
})
// 断言:按钮文本正确
expect(wrapper.text()).toBe('测试按钮')
// 断言:默认样式类存在
expect(wrapper.classes()).toContain('btn-default')
})
// 测试类型属性
it('不同type属性渲染不同样式', () => {
const wrapper = mount(Button, {
props: {
label: '主要按钮',
type: 'primary'
}
})
expect(wrapper.classes()).toContain('btn-primary')
})
// 测试点击事件
it('点击按钮触发click事件', async () => {
const mockClick = vi.fn()
const wrapper = mount(Button, {
props: {
label: '点击按钮',
onClick: mockClick
}
})
// 模拟点击
await wrapper.trigger('click')
// 断言:事件被调用
expect(mockClick).toHaveBeenCalled()
})
})
工具函数测试(src/utils/format.test.ts)
import { formatDate } from './format'
describe('formatDate函数', () => {
it('格式化日期为YYYY-MM-DD格式', () => {
const date = new Date('2026-01-01')
expect(formatDate(date)).toBe('2026-01-01')
})
it('处理空值返回空字符串', () => {
expect(formatDate(null)).toBe('')
expect(formatDate(undefined)).toBe('')
})
})
4.1.4 运行测试脚本(package.json)
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui", // 可视化测试界面
"test:coverage": "vitest run --coverage" // 生成覆盖率报告
}
4.2 React18+Jest测试配置
4.2.1 依赖安装
# 安装Jest、React Testing Library、测试相关依赖
npm install jest @testing-library/react @testing-library/jest-dom @testing-library/user-event --save-dev
# 安装TypeScript适配插件
npm install ts-jest @types/jest --save-dev
4.2.2 配置文件(jest.config.js)
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'\\.(css|scss)$': 'identity-obj-proxy' // 模拟样式文件
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
testMatch: ['<rootDir>/src/**/*.test.(ts|tsx|js|jsx)'],
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
coverageReporters: ['text', 'html'],
collectCoverageFrom: [
'src/**/*.{ts,tsx,js,jsx}',
'!src/main.tsx',
'!src/router/**',
'!src/store/**'
]
}
4.2.3 编写测试用例
组件测试(src/components/Button/Button.test.tsx)
import { render, screen, fireEvent } from '@testing-library/react'
import Button from './Button'
describe('Button组件', () => {
it('渲染正确的文本与样式', () => {
render(<Button label="测试按钮" />)
const buttonElement = screen.getByText('测试按钮')
// 断言:组件渲染成功
expect(buttonElement).toBeInTheDocument()
// 断言:默认样式存在
expect(buttonElement).toHaveClass('btn-default')
})
it('不同type属性渲染不同样式', () => {
render(<Button label="主要按钮" type="primary" />)
const buttonElement = screen.getByText('主要按钮')
expect(buttonElement).toHaveClass('btn-primary')
})
it('点击按钮触发onClick事件', () => {
const mockClick = jest.fn()
render(<Button label="点击按钮" onClick={mockClick} />)
const buttonElement = screen.getByText('点击按钮')
// 模拟点击
fireEvent.click(buttonElement)
// 断言:事件被调用
expect(mockClick).toHaveBeenCalledTimes(1)
})
})
运行测试脚本与Vue3项目类似,在package.json中配置脚本后执行即可。企业级项目中,建议优先覆盖公共组件、工具函数、核心业务逻辑,逐步提升测试覆盖率。
五、性能优化实战:从打包到上线的全链路优化
前端性能直接影响用户体验与业务转化,性能优化需贯穿“开发→打包→部署→运行”全链路。本节聚焦打包优化、首屏加载优化、缓存策略三大核心场景,提供可落地的优化方案与实战案例。
5.1 打包优化:减小资源体积,提升构建效率
5.1.1 代码分割与懒加载
通过代码分割将大文件拆分为多个小文件,结合路由懒加载、组件懒加载,实现“按需加载”,减少初始加载资源体积。Vite默认支持代码分割,可通过rollupOptions进一步优化:
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
// 按模块分割代码
manualChunks: {
vue: ['vue', 'vue-router', 'pinia'],
elementPlus: ['element-plus'],
axios: ['axios']
}
}
}
}
})
路由懒加载已在脚手架搭建环节实现,组件懒加载示例(Vue3):
<template>
<div>
<button @click="showModal = true">打开弹窗</button>
<Teleport to="body">
<Modal v-if="showModal" @close="showModal = false" />
</Teleport>
</div>
</template>
<script setup lang="ts">
import { ref, defineAsyncComponent } from 'vue'
// 懒加载组件
const Modal = defineAsyncComponent(() => import('@/components/Modal.vue'))
const showModal = ref(false)
</script>
5.1.2 依赖优化:剔除无用代码,替换轻量库
-
Tree Shaking:Vite默认开启Tree Shaking,剔除未使用的代码,需确保代码符合ESModule规范(避免使用CommonJS)。
-
按需引入UI组件库:如Element Plus、Ant Design,通过插件自动按需引入组件与样式,避免全量引入导致体积过大。
-
替换重依赖:将体积大的库替换为轻量替代方案,如用dayjs替换moment.js(体积缩小80%),用lodash-es替换lodash(支持Tree Shaking)。
5.1.3 资源压缩与优化
图片优化
安装vite-plugin-imagemin插件压缩图片:
npm install vite-plugin-imagemin --save-dev
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import imagemin from 'vite-plugin-imagemin'
export default defineConfig({
plugins: [
vue(),
imagemin({
gifsicle: {
optimizationLevel: 7,
interlaced: false
},
optipng: {
enabled: true
},
pngquant: {
quality: [0.6, 0.8]
},
mozjpeg: {
quality: 80
},
webp: {
quality: 80
}
})
]
})
同时,推荐使用WebP格式图片(体积较PNG小50%),通过picture标签适配不同浏览器:
<picture>
<source srcset="@assets/images/example.webp" type="image/webp">
<img src="@assets/images/example.png" alt="示例图片">
</picture>
CSS压缩与提取
Vite默认压缩CSS,可通过build.cssCodeSplit配置提取CSS为单独文件:
// vite.config.ts
export default defineConfig({
build: {
cssCodeSplit: true, // 提取CSS为单独文件
minify: 'terser' // 使用terser压缩JS/CSS
}
})
5.2 首屏加载优化:缩短白屏时间
5.2.1 预加载与预连接
通过link标签实现资源预加载、预连接,提升资源加载速度:
<!-- public/index.html -->
<head>
<!-- 预连接API服务器 -->
<link rel="preconnect" href="https://api.example.com">
<!-- 预加载核心JS文件 -->
<link rel="preload" href="/static/js/main-[hash].js" as="script">
<!-- 预加载字体文件 -->
<link rel="preload" href="/static/fonts/iconfont.woff2" as="font" type="font/woff2" crossorigin>
</head>
5.2.2 骨架屏与加载状态优化
首屏加载时显示骨架屏,替代白屏,提升用户感知体验。Vue3项目可通过组件实现骨架屏:
<template>
<div class="home-page">
<!-- 骨架屏 -->
<HomeSkeleton v-if="loading" />
<!-- 真实内容 -->
<div v-else>
<!-- 页面内容 -->
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import HomeSkeleton from './HomeSkeleton.vue'
import { fetchHomeData } from '@/api/home'
const loading = ref(true)
onMounted(async () => {
await fetchHomeData()
loading.value = false
})
</script>
5.2.3 服务端渲染(SSR)/静态站点生成(SSG)
对于首屏加载速度要求极高的项目(如官网、电商首页),可采用SSR或SSG。Vue3可通过Nuxt.js、React18可通过Next.js实现,将组件渲染为HTML字符串发送给浏览器,大幅缩短首屏加载时间。
5.3 缓存策略:减少重复请求,提升二次加载速度
5.3.1 HTTP缓存配置(Nginx)
通过Nginx配置HTTP缓存头,实现静态资源的浏览器缓存与协商缓存:
server {
listen 80;
server_name example.com;
root /usr/share/nginx/html;
# 静态资源缓存配置
location ~* \.(js|css|png|jpg|webp|woff2)$ {
# 强缓存:资源未过期直接使用本地缓存(7天)
expires 7d;
# 协商缓存:资源过期后验证ETag/Last-Modified
add_header Cache-Control "public, max-age=604800";
# 指纹文件(含hash)永久缓存
if ($request_filename ~* \.(js|css|png|jpg|webp|woff2)\.[0-9a-f]{8,}\.) {
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
}
}
# 入口文件(index.html)禁用强缓存,启用协商缓存
location = /index.html {
expires -1;
add_header Cache-Control "no-cache";
}
}
5.3.2 接口缓存策略
对于不常变化的接口数据,可通过本地存储(localStorage/sessionStorage)或请求缓存(Axios拦截器)实现缓存:
// src/utils/request.ts(Axios接口缓存示例)
import axios from 'axios'
// 接口缓存对象
const requestCache = new Map()
// 请求拦截器
service.interceptors.request.use((config) => {
// 开启缓存的接口,从缓存中获取数据
if (config.cache) {
const cacheKey = config.url + JSON.stringify(config.params)
const cacheData = requestCache.get(cacheKey)
if (cacheData) {
return Promise.resolve({ data: cacheData })
}
}
return config
})
// 响应拦截器
service.interceptors.response.use((response) => {
// 开启缓存的接口,存储数据到缓存
const config = response.config
if (config.cache) {
const cacheKey = config.url + JSON.stringify(config.params)
requestCache.set(cacheKey, response.data)
// 设置缓存过期时间(可选)
setTimeout(() => {
requestCache.delete(cacheKey)
}, config.cacheExpire || 300000) // 默认5分钟过期
}
return response
})

六、总结:前端工程化的进阶之路
前端工程化并非一成不变的标准,而是随着技术发展不断迭代的体系。从搭建标准化脚手架,到落地代码规范、自动化测试,再到全链路性能优化,每一步都是对“高效、稳定、可维护”开发目标的追求。对于前端工程师而言,工程化能力的提升,不仅是工具的熟练使用,更是对“全流程把控”思维的培养——既要关注编码细节,也要兼顾项目整体质量与用户体验。
本文讲解的工程化方案,已覆盖企业级项目的核心需求,开发者可根据项目规模、团队技术栈灵活调整。在实际开发中,建议逐步落地、持续优化:先搭建基础脚手架与代码规范,再引入自动化测试保障质量,最后通过性能优化提升用户体验。随着工程化体系的不断完善,团队开发效率与项目质量将实现质的飞跃,为业务发展提供坚实的技术支撑。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐

所有评论(0)