前端工程化

48 阅读15分钟

前端工程化全流程

一、工程化基础认知

1.1 核心定义

前端工程化是通过工具链标准化、流程规范化、架构模块化、质量体系化的方式,解决前端开发中"协作低效、代码杂乱、部署繁琐、质量失控"等问题的系统性方案,核心目标是支撑中大型项目规模化发展,实现"提效、提质、降本"。

1.2 核心价值

  • 开发提效:自动化工具替代重复操作(如构建、格式化),模块化复用减少重复编码

  • 质量保障:通过代码规范、测试体系、监控告警提前规避线上问题

  • 协作顺畅:统一规范降低沟通成本,Monorepo/微前端支持跨团队协作

  • 可维护性:清晰架构与规范使项目易迭代,降低新人上手成本

1.3 核心环节

前端工程化涵盖"初始化→开发→构建→测试→部署→运维"全链路,关键环节包括:

  1. 模块化开发:代码拆分与复用(ES Module/CJS)

  2. 自动化构建:打包、压缩、优化(Vite/Webpack)

  3. 代码规范:质量校验与格式统一(ESLint/Prettier)

  4. 测试体系:单元测试、E2E测试(Jest/Cypress)

  5. CI/CD:持续集成与自动部署(GitHub Actions/Jenkins)

  6. 监控运维:线上错误与性能监控(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.xTree 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已安装且extendsplugin: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)