前端工程化全流程
一、工程化基础认知
1.1 核心定义
前端工程化是通过工具链标准化、流程规范化、架构模块化、质量体系化的方式,解决前端开发中"协作低效、代码杂乱、部署繁琐、质量失控"等问题的系统性方案,核心目标是支撑中大型项目规模化发展,实现"提效、提质、降本"。
1.2 核心价值
-
开发提效:自动化工具替代重复操作(如构建、格式化),模块化复用减少重复编码
-
质量保障:通过代码规范、测试体系、监控告警提前规避线上问题
-
协作顺畅:统一规范降低沟通成本,Monorepo/微前端支持跨团队协作
-
可维护性:清晰架构与规范使项目易迭代,降低新人上手成本
1.3 核心环节
前端工程化涵盖"初始化→开发→构建→测试→部署→运维"全链路,关键环节包括:
-
模块化开发:代码拆分与复用(ES Module/CJS)
-
自动化构建:打包、压缩、优化(Vite/Webpack)
-
代码规范:质量校验与格式统一(ESLint/Prettier)
-
测试体系:单元测试、E2E测试(Jest/Cypress)
-
CI/CD:持续集成与自动部署(GitHub Actions/Jenkins)
-
监控运维:线上错误与性能监控(Sentry/Lighthouse)
二、技术栈选型指南
2.1 核心框架选型
| 框架 | 核心优势 | 适用场景 | 配套技术 |
|---|---|---|---|
| Vue 3 | 组合式API提升可维护性,响应式性能优化,学习成本低 | 中中小型项目、后台管理系统、快速迭代产品 | TypeScript、Pinia、Vue Router、Vite |
| React 18 | 生态丰富,组件化能力强,Server Components优化首屏 | 复杂交互项目、大型应用、跨端开发(React Native) | TypeScript、Redux Toolkit、React Router、Vite |
| Angular 18 | 强架构约束,内置完整解决方案(路由/表单/HTTP) | 企业级大型项目、团队协作规范要求高的场景 | TypeScript、Angular CLI、RxJS |
| 建议:无论选择哪种框架,均配套TypeScript提升类型安全,减少运行时错误。 |
2.2 构建工具选型
| 工具 | 核心优势 | 适用场景 | 性能亮点 |
|---|---|---|---|
| Vite 7.x | 基于ESModule冷启动,开发体验佳,配置简洁 | Vue/React中大型项目、现代浏览器场景 | Rolldown打包器使构建时间减少40%,支持边缘端构建 |
| Webpack 5.x | 生态成熟,loader/plugin丰富,适配复杂场景 | 多页面应用、需深度定制构建流程的项目 | 持久化缓存提升二次构建效率,Module Federation支持模块共享 |
| Rollup 4.x | Tree Shaking能力强,输出体积小 | 类库开发、组件库打包场景 | 对ESModule支持友好,打包产物更精简 |
2.3 包管理工具选型
-
pnpm(推荐):原生支持Monorepo的Workspace,通过内容寻址存储节省磁盘空间,严格依赖解析避免幽灵依赖,适合中大型多包项目
-
yarn:缓存机制完善,适合中型项目,yarn workspaces支持多包管理
-
npm:生态基础,配置简单,适合小型项目或快速原型开发
三、项目初始化规范
3.1 脚手架工具选择
-
Vue项目:优先Vite(
npm create vite@latest),替代传统Vue CLI,启动速度提升3-5倍 -
React项目:简单场景用Create React App(
npx create-react-app),复杂场景用Vite -
Angular项目:官方Angular CLI(
ng new),内置完整工程化配置 -
自定义脚手架:大型团队可基于Yeoman开发专属脚手架,集成团队规范与模板
3.2 目录结构设计
3.2.1 通用目录结构(中大型项目)
src/
├── api/ # 接口请求封装(按业务模块拆分,如api/user.js)
├── assets/ # 静态资源(分类存放:images/styles/fonts/icons)
├── components/ # 通用组件(拆分为base基础组件和business业务组件)
│ ├── base/ # 原子组件:Button、Input、Table等
│ └── business/# 业务组件:UserCard、OrderList等
├── views/ # 页面级组件(按路由模块拆分,内含私有组件)
│ ├── Home/
│ │ ├── components/ # 页面私有组件
│ │ ├── Home.vue
│ │ └── Home.api.js # 页面专属接口
├── router/ # 路由配置(含路由守卫、动态路由)
├── store/ # 状态管理(按模块拆分,如store/modules/user.js)
├── utils/ # 工具函数(分类存放:format/validate/request等)
├── config/ # 配置文件(环境变量、常量、枚举等)
├── hooks/ # 自定义Hook(React/Vue 3,封装复用逻辑)
├── styles/ # 全局样式(变量、重置样式、混合器、主题)
├── types/ # TypeScript类型定义(全局类型、接口类型)
├── filters/ # 过滤器(Vue项目专用,格式化展示数据)
└── main.ts # 入口文件(初始化配置:路由、状态管理、全局组件)
# 根目录配置文件
├── .env.development # 开发环境变量
├── .env.production # 生产环境变量
├── .eslintrc.js # ESLint配置
├── .prettierrc # Prettier配置
├── vite.config.ts # 构建配置
└── package.json # 依赖与脚本配置
3.2.2 目录适配调整
-
小型项目:合并同类目录(如api与utils合并,components不区分base和business)
-
Monorepo项目:按业务域拆分packages目录(参考4.5.2节)
-
跨端项目:新增platform目录区分端相关逻辑(如platform/h5、platform/app)
3.3 环境配置规范
3.3.1 多环境区分
至少区分4类环境,避免硬编码配置:
| 环境类型 | 配置文件 | 核心用途 | 示例配置 |
|---|---|---|---|
| 开发环境 | .env.development | 本地开发,对接开发服务器 | VITE_API_URL=http://localhost:3000/api |
| 测试环境 | .env.test | 测试验证,对接测试服务器 | VITE_API_URL=test.example.com/api |
| 预发环境 | .env.staging | 上线前验证,对接生产镜像服务器 | VITE_API_URL=staging.example.com/api |
| 生产环境 | .env.production | 线上运行,对接生产服务器 | VITE_API_URL=api.example.com |
3.3.2 环境变量使用规范
-
前缀约束:Vite项目环境变量必须以
VITE_为前缀,确保客户端可访问且避免敏感信息暴露 -
封装访问:统一封装环境变量访问,避免直接使用
import.meta.env
// src/utils/env.js
/** 环境变量封装,统一管理 */
export const ENV = {
// 接口基础地址
apiUrl: import.meta.env.VITE_API_URL,
// 环境标识
env: import.meta.env.VITE_ENV || import.meta.env.NODE_ENV,
// 是否为生产环境
isProd: import.meta.env.VITE_ENV === 'production',
// 应用版本(从package.json读取)
version: import.meta.env.VITE_APP_VERSION
};
// 使用示例
import { ENV } from '@/utils/env';
axios.defaults.baseURL = ENV.apiUrl;
四、核心工具链配置实操
4.1 代码规范工具链(ESLint + Prettier + Husky)
4.1.1 依赖安装
# 核心依赖
npm install eslint prettier eslint-config-prettier eslint-plugin-prettier -D
# 框架适配依赖(根据框架选择)
# Vue项目
npm install eslint-plugin-vue @vue/eslint-config-typescript -D
# React项目
npm install eslint-plugin-react eslint-plugin-react-hooks @typescript-eslint/parser -D
# Git Hook依赖
npm install husky lint-staged -D
4.1.2 配置文件编写
ESLint配置(.eslintrc.js)- Vue+TS项目示例
module.exports = {
// 运行环境
env: {
browser: true,
es2021: true,
node: true
},
// 继承规则集(plugin:prettier/recommended必须放最后解决冲突)
extends: [
'eslint:recommended',
'plugin:vue/vue3-recommended',
'@vue/eslint-config-typescript',
'plugin:prettier/recommended'
],
// 解析器配置
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
},
// 自定义规则(优先级高于继承规则)
rules: {
// Prettier规则(以ESLint错误形式展示)
'prettier/prettier': ['error', { singleQuote: true, trailingComma: 'all' }],
// Vue规则
'vue/multi-word-component-names': 'off', // 关闭组件名多单词约束
'vue/no-v-model-argument': 'off',
// TypeScript规则
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
// 通用规则
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
}
};
Prettier配置(.prettierrc)
{
"printWidth": 120, // 每行最大字符数
"tabWidth": 2, // 缩进宽度
"useTabs": false, // 不使用制表符缩进
"semi": true, // 语句末尾加分号
"singleQuote": true, // 使用单引号
"trailingComma": "all", // 所有对象/数组末尾加逗号
"bracketSpacing": true, // 对象字面量加空格({ foo: bar })
"arrowParens": "always", // 箭头函数参数必须加括号
"endOfLine": "lf" // 换行符格式(LF)
}
Git Hook配置(提交前校验)
# 初始化husky(生成.husky目录)
npx husky init
{
// package.json中添加lint-staged配置
"lint-staged": {
"*.{js,jsx,ts,tsx,vue}": ["eslint --fix", "prettier --write"],
"*.{css,scss,less}": ["stylelint --fix", "prettier --write"],
"*.{json,md}": ["prettier --write"]
}
}
# 修改.husky/pre-commit文件
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged
VS Code自动格式化配置(.vscode/settings.json)
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true, // 保存时自动格式化
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true // 保存时自动修复ESLint错误
},
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact", "vue"],
"stylelint.validate": ["css", "scss", "less", "vue"]
}
4.1.3 常见问题解决
-
ESLint与Prettier冲突:确保
eslint-config-prettier已安装且extends中plugin:prettier/recommended放最后 -
保存不自动格式化:检查VS Code是否安装ESLint和Prettier插件,确保工作区配置正确
-
特定文件忽略校验:创建
.eslintignore和.prettierignore,添加忽略路径(如node_modules、dist)
4.2 构建工具配置(以Vite为例)
4.2.1 基础配置(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';
import { visualizer } from 'rollup-plugin-visualizer'; // 构建体积分析
import viteCompression from 'vite-plugin-compression'; // Gzip压缩
import eslintPlugin from 'vite-plugin-eslint'; // 开发时ESLint校验
// 定义路径别名函数
const resolve = (dir: string) => path.resolve(__dirname, dir);
export default defineConfig(({ mode }) => {
// 加载对应环境的环境变量
const env = loadEnv(mode, process.cwd());
return {
// 基础路径(生产环境部署子路径时配置)
base: env.VITE_BASE_URL || '/',
// 插件配置
plugins: [
vue(), // Vue支持
vueJsx(), // JSX支持
eslintPlugin(), // 开发时实时ESLint校验
// 生产环境开启Gzip压缩
env.NODE_ENV === 'production' && viteCompression({
algorithm: 'gzip',
threshold: 10240, // 大于10KB的文件才压缩
deleteOriginFile: false // 不删除原文件
}),
// 生产环境生成构建体积分析报告
env.NODE_ENV === 'production' && visualizer({
open: true,
filename: 'dist/visualizer.html'
})
].filter(Boolean), // 过滤掉false的插件
// 解析配置
resolve: {
// 路径别名
alias: {
'@': resolve('src'),
'@api': resolve('src/api'),
'@components': resolve('src/components')
},
// 省略文件后缀
extensions: ['.ts', '.tsx', '.vue', '.js', '.jsx', '.json']
},
// 开发服务器配置
server: {
host: '0.0.0.0', // 允许外部访问
port: Number(env.VITE_PORT) || 3000, // 端口
open: true, // 自动打开浏览器
// 接口代理(解决跨域)
proxy: {
'/api': {
target: env.VITE_API_URL,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
// 构建配置
build: {
outDir: 'dist', // 输出目录
assetsDir: 'static', // 静态资源目录
sourcemap: env.NODE_ENV === 'development', // 开发环境生成sourcemap
// 产物优化
rollupOptions: {
// 代码分割(按模块拆分chunk,优化缓存)
output: {
manualChunks: {
// 第三方库单独拆分
vue: ['vue', 'vue-router', 'pinia'],
utils: ['axios', 'lodash-es', 'dayjs'],
// 业务模块拆分
home: ['@/views/Home'],
user: ['@/views/User']
},
// 静态资源命名规则
assetFileNames: {
js: 'static/js/[name]-[hash].js',
css: 'static/css/[name]-[hash].css',
images: 'static/images/[name]-[hash].[ext]'
}
}
},
// 关闭生产环境console
minify: 'terser',
terserOptions: {
compress: {
drop_console: env.NODE_ENV === 'production',
drop_debugger: env.NODE_ENV === 'production'
}
}
},
// CSS配置
css: {
// 预处理器配置
preprocessorOptions: {
scss: {
// 全局引入SCSS变量和混合器
additionalData: `@import "@/styles/variables.scss"; @import "@/styles/mixins.scss";`
}
},
// 开启CSS Modules
modules: {
scopeBehaviour: 'local',
// 生成的类名格式
generateScopedName: '[name]-[local]-[hash:base64:4]'
}
}
};
});
4.2.2 构建优化关键指标
核心优化目标:生产环境构建时间<3分钟,单个chunk体积<500KB(第三方库单独拆分),Gzip压缩后体积降低60%以上,首屏加载时间<3秒。
4.3 测试体系搭建
4.3.1 测试分层设计
| 测试类型 | 核心工具 | 测试范围 | 覆盖率目标 |
|---|---|---|---|
| 单元测试 | Jest + Vue Test Utils/Testing Library | 工具函数、组件、Hook、状态管理逻辑 | 核心业务模块≥80%,工具函数≥90% |
| 组件测试 | Storybook + Cypress | 组件渲染、交互逻辑、视觉一致性 | 通用组件≥90% |
| 端到端测试(E2E) | Cypress/Playwright | 核心业务流程(登录、下单、支付等) | 关键流程100%覆盖 |
| 性能测试 | Lighthouse + Sentry性能监控 | 首屏加载、交互响应、资源加载 | Lighthouse评分≥90分 |
4.3.2 单元测试实操(Vue组件示例)
# 安装依赖
npm install jest @vue/test-utils@next vue-jest@next ts-jest @types/jest -D
// jest.config.js配置
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
// 模块映射(对应Vite的别名)
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
},
// 转换配置
transform: {
'^.+\\.vue$': '@vue/vue3-jest',
'^.+\\.tsx?$': 'ts-jest'
},
// 测试文件匹配规则
testMatch: ['<rootDir>/src/**/*.test.{js,ts,jsx,tsx}'],
// 模块文件扩展名
moduleFileExtensions: ['vue', 'ts', 'tsx', 'js', 'jsx', 'json', 'node']
};
// src/components/base/Button/Button.test.ts
import { mount } from '@vue/test-utils';
import Button from './Button.vue';
describe('Button组件', () => {
// 测试默认渲染
test('默认样式与文本渲染正确', () => {
const wrapper = mount(Button, {
props: { text: '默认按钮' }
});
expect(wrapper.text()).toBe('默认按钮');
expect(wrapper.classes()).toContain('btn-default');
});
// 测试不同类型
test('不同type属性渲染对应样式', () => {
const primaryWrapper = mount(Button, {
props: { text: '主要按钮', type: 'primary' }
});
expect(primaryWrapper.classes()).toContain('btn-primary');
});
// 测试点击事件
test('点击触发onClick事件', async () => {
const mockClick = jest.fn();
const wrapper = mount(Button, {
props: { text: '点击按钮', onClick: mockClick }
});
await wrapper.trigger('click');
expect(mockClick).toHaveBeenCalledTimes(1);
});
// 测试禁用状态
test('disabled状态下不可点击', async () => {
const mockClick = jest.fn();
const wrapper = mount(Button, {
props: { text: '禁用按钮', disabled: true, onClick: mockClick }
});
expect(wrapper.attributes('disabled')).toBe('true');
await wrapper.trigger('click');
expect(mockClick).not.toHaveBeenCalled();
});
});
// src/components/base/Button/Button.test.ts
import { mount } from '@vue/test-utils';
import Button from './Button.vue';
describe('Button组件', () => {
// 测试默认渲染
test('默认样式与文本渲染正确', () => {
const wrapper = mount(Button, {
props: { text: '默认按钮' }
});
expect(wrapper.text()).toBe('默认按钮');
expect(wrapper.classes()).toContain('btn-default');
});
// 测试不同类型
test('不同type属性渲染对应样式', () => {
const primaryWrapper = mount(Button, {
props: { text: '主要按钮', type: 'primary' }
});
expect(primaryWrapper.classes()).toContain('btn-primary');
});
// 测试点击事件
test('点击触发onClick事件', async () => {
const mockClick = jest.fn();
const wrapper = mount(Button, {
props: { text: '点击按钮', onClick: mockClick }
});
await wrapper.trigger('click');
expect(mockClick).toHaveBeenCalledTimes(1);
});
// 测试禁用状态
test('disabled状态下不可点击', async () => {
const mockClick = jest.fn();
const wrapper = mount(Button, {
props: { text: '禁用按钮', disabled: true, onClick: mockClick }
});
expect(wrapper.attributes('disabled')).toBe('true');
await wrapper.trigger('click');
expect(mockClick).not.toHaveBeenCalled();
});
});
五、架构设计核心实践
5.1 模块化与组件化设计
5.1.1 原子设计模式
采用"原子→分子→有机体→模板→页面"的分层设计,确保组件复用性与可维护性:
| 层级 | 定义 | 示例 |
|---|---|---|
| 原子组件 | 不可拆分的基础组件,封装基础样式与交互 | Button、Input、Icon、Checkbox |
| 分子组件 | 由原子组件组合而成,具备简单业务能力 | SearchBar(Input+Button+Icon)、Select(Input+Dropdown) |
| 有机体组件 | 由分子/原子组件组合,构成页面独立区块 | UserForm、TablePagination、CardList |
| 模板 | 页面布局框架,定义组件排列规则 | MainLayout(Header+Sidebar+Content+Footer) |
| 页面 | 基于模板填充业务组件,实现完整功能 | UserListPage、OrderDetailPage |
5.1.2 组件开发规范
// Vue原子组件示例(src/components/base/Button/Button.vue)
<template>
<button
:class="btnClass"
:disabled="disabled"
@click="$emit('click')"
:type="type"
>
<Icon v-if="icon" :name="icon" class="btn-icon" />
<span v-if="text" class="btn-text">{{ text }}</span>
</button>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import Icon from '../Icon/Icon.vue';
// Props定义(含类型校验与默认值)
const props = defineProps<{
text?: string;
icon?: string;
type?: 'primary' | 'default' | 'danger' | 'text';
disabled?: boolean;
buttonType?: 'button' | 'submit' | 'reset';
}>();
// 计算属性(动态类名)
const btnClass = computed(() => [
'btn',
`btn-${props.type || 'default'}`,
{ 'btn-disabled': props.disabled },
{ 'btn-has-icon': props.icon }
]);
// 事件定义
defineEmits(['click']);
</script>
<style scoped lang="scss">
.btn {
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
&.btn-disabled {
cursor: not-allowed;
opacity: 0.6;
}
// 不同类型样式
&.btn-primary {
background: #1890ff;
color: #fff;
}
// 其他类型样式...
}</style>
5.2 状态管理实践
5.2.1 状态分类与选型
核心原则:避免过度使用全局状态,优先选择"最小作用域"管理方案
| 状态类型 | 管理方案 | 适用场景 |
|---|---|---|
| 组件内状态 | Vue:ref/reactive;React:useState | 单个组件内部使用(如表单输入值、弹窗显示隐藏) |
| 跨组件状态(父子/兄弟) | Vue:provide/inject;React:Context+useReducer | 组件树中多层级共享(如主题配置、权限信息) |
| 全局状态(多页面共享) | Vue:Pinia;React:Redux Toolkit | 用户信息、全局配置、跨页面数据共享 |
| 服务端状态 | VueQuery/SWR | 接口数据缓存、刷新、轮询(如列表数据、详情数据) |
5.2.2 Pinia全局状态管理示例(Vue 3)
// src/store/modules/userStore.ts
import { defineStore } from 'pinia';
import { loginApi, getUserInfoApi, logoutApi } from '@/api/user';
import { setToken, removeToken, getToken } from '@/utils/auth';
// 定义状态类型
interface UserState {
userInfo: Nullable<{
id: string;
name: string;
role: string;
avatar: string;
}>;
token: string;
loading: boolean;
}
// 创建Store(按业务模块拆分)
export const useUserStore = defineStore('user', {
// 状态初始化(从本地存储恢复)
state: (): UserState => ({
userInfo: null,
token: getToken() || '',
loading: false
}),
// 计算属性(缓存派生数据)
getters: {
// 是否为管理员
isAdmin: (state) => state.userInfo?.role === 'admin',
// 是否已登录
isLogin: (state) => !!state.token && !!state.userInfo
},
// 异步动作(封装业务逻辑)
actions: {
// 登录动作
async loginAction(params: { username: string; password: string }) {
this.loading = true;
try {
const { token } = await loginApi(params);
this.token = token;
setToken(token); // 持久化存储token
await this.getUserInfoAction(); // 登录后获取用户信息
return true;
} catch (error) {
console.error('登录失败:', error);
return false;
} finally {
this.loading = false;
}
},
// 获取用户信息
async getUserInfoAction() {
const res = await getUserInfoApi();
this.userInfo = res.data;
},
// 退出登录
async logoutAction() {
await logoutApi();
// 清除状态
this.token = '';
this.userInfo = null;
removeToken(); // 清除本地存储
}
}
});
5.3 路由管理最佳实践
5.3.1 路由配置与权限控制(Vue Router)
// src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import { useUserStore } from '@/store/modules/userStore';
import MainLayout from '@/components/layout/MainLayout.vue';
// 1. 静态路由(无需权限)
const staticRoutes: RouteRecordRaw[] = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login/Login.vue'),
meta: { hidden: true } // 侧边栏不显示
},
{
path: '/404',
component: () => import('@/views/Error/404.vue'),
meta: { hidden: true }
}
];
// 2. 动态路由(需权限控制)
export const dynamicRoutes: RouteRecordRaw[] = [
{
path: '/',
component: MainLayout,
redirect: '/dashboard',
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard/Dashboard.vue'),
meta: {
title: '首页',
icon: 'dashboard',
roles: ['admin', 'editor'] // 可访问角色
}
}
]
},
// 管理员专属路由
{
path: '/system',
component: MainLayout,
meta: {
title: '系统管理',
icon: 'setting',
roles: ['admin'] // 仅管理员可访问
},
children: [
{
path: 'user-manage',
name: 'UserManage',
component: () => import('@/views/System/UserManage.vue'),
meta: { title: '用户管理', roles: ['admin'] }
}
]
},
// 404页面必须放最后
{ path: '/:pathMatch(.*)*', redirect: '/404', meta: { hidden: true } }
];
// 3. 创建路由实例
const router = createRouter({
history: createWebHistory(import.meta.env.VITE_BASE_URL),
routes: staticRoutes // 初始只加载静态路由
});
// 4. 路由守卫(权限控制核心)
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore();
const hasToken = userStore.token;
// 未登录且不是去登录页:跳转登录
if (!hasToken && to.name !== 'Login') {
next({ name: 'Login', query: { redirect: to.fullPath } });
return;
}
// 已登录且去登录页:跳转首页
if (hasToken && to.name === 'Login') {
next({ name: 'Dashboard' });
return;
}
// 已登录且有用户信息:权限校验
if (userStore.userInfo) {
// 无需权限的路由直接放行
if (!to.meta.roles) {
next();
return;
}
// 校验角色权限
const hasPermission = (to.meta.roles as string[]).includes(userStore.userInfo.role);
hasPermission ? next() : next('/404');
return;
}
// 已登录但无用户信息:获取用户信息后再跳转
if (hasToken && !userStore.userInfo) {
try {
await userStore.getUserInfoAction();
// 动态添加路由
dynamicRoutes.forEach(route => router.addRoute(route));
// 重新跳转当前路由(触发权限校验)
next({ ...to, replace: true });
} catch (error) {
// 获取信息失败:清除token并跳转登录
await userStore.logoutAction();
next({ name: 'Login' });
}
}
});
export default router;
六、性能优化体系
6.1 构建优化
-
代码分割:通过Vite/Webpack的manualChunks拆分第三方库与业务代码,实现按需加载
-
Tree Shaking:开启生产环境Tree Shaking(需使用ES Module),移除未使用代码
-
压缩优化:开启Gzip/Brotli压缩(Vite用vite-plugin-compression),减少传输体积
-
依赖优化:使用体积更小的替代库(如lodash-es替代lodash),剔除无用依赖
6.2 资源优化
-
图片优化:使用webp/avif格式,配置图片懒加载;小图片转base64(Vite内置支持)
-
字体优化:使用font-display: swap避免字体加载阻塞;提取关键字体子集
-
静态资源CDN:第三方库(Vue/React)与静态资源(图片/字体)部署至CDN,减少服务器压力
6.3 渲染优化
-
首屏加载优化:
-
路由懒加载(动态import)
-
预加载关键资源()
-
服务端渲染(SSR)/静态站点生成(SSG):Next.js/Nuxt.js提升首屏渲染速度
运行时优化:
-
Vue:使用v-show替代频繁切换的v-if;通过defineProps定义props减少响应式开销
-
React:使用React.memo/useMemo/useCallback避免不必要的重渲染;列表使用虚拟列表(react-window)