Skip to content

Repository files navigation

DEMO一. pnpm 和npm的不同点

图为在Git bash中 分别创建两个不同的依赖(npm pnpm)创建的相同的vue文件

未加载成功 其显示出的pnpm与npm所依赖的node_modules大小几乎一致 原因是二者的vue项目目前都是空的 依赖包数量少 体积小 如果创建了n个npm项目 那么内存就会成为n倍 而同样的情况下 pnpm的所占内存不变

查询inode等node_modules 目录内容 都是输入 ls -la node_modules/

如果vue和pnpm系统分区相同 那么他们的inode是相同的 反之一定不相同

相同则说明确实证明是硬链接,不是复制 这极大地省下了巨大的内存空间


DEMO二.Vite 多环境配置与 mode 运行时验证

未加载成功 未加载成功

这两张图说明确实证明是硬链接,不是复制 这极大地省下了巨大的内存空间

证明pnpm强制的严格隔离性 lodash未申明未依赖 绝不安装

未加载成功

未在所属环境中 引入VITE_前缀的变量 在界面内直接引用 会报undefined

未加载成功 未加载成功

修改vite.config.js 以及为什么要修改

不修改它,Vite 就用默认配置:

1.端口 5173 2.不自动打开浏览器3.不生成 sourcemap 4.不区分环境加载变量(只能用默认的 development/production)

修改它,可以定制 Vite 的行为:

1.改端口2.自动打开浏览器3.生成 sourcemap 4.根据 command 和 mode 做条件判断5.配置代理、别名、插件等

改完之后他会在终端中显示体现 alt text

import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; import fs from 'fs'; import path from 'path';

// ============================================================ // 这部分代码在 Node 环境执行(终端里看输出) // ============================================================ console.log('\n========== Vite 配置文件加载(pnpm 环境)=========='); console.log('当前命令:', process.argv[2]); // 'serve' 或 'build' console.log('当前工作目录:', process.cwd());

// 验证 pnpm 的 .pnpm 目录是否存在 const pnpmDir = path.join(process.cwd(), 'node_modules', '.pnpm'); if (fs.existsSync(pnpmDir)) { console.log('✅ 检测到 pnpm 的 .pnpm 目录(严格隔离模式)');

// 读取 .pnpm 目录下的内容,展示 pnpm 的存储结构 const items = fs.readdirSync(pnpmDir).slice(0, 6); console.log(' .pnpm 目录内容(前6个):', items.join(', ')); } else { console.log('❌ 未检测到 .pnpm 目录,当前可能不是 pnpm 项目'); }

// 验证 node_modules 顶层只有声明过的依赖 const nodeModulesDir = path.join(process.cwd(), 'node_modules'); const topLevelDeps = fs.readdirSync(nodeModulesDir).filter((name) => !name.startsWith('.')); console.log(' node_modules 顶层依赖:', topLevelDeps.join(', ')); console.log('==================================================\n');

// ============================================================ // Vite 配置导出 // ============================================================ export default defineConfig(({ command, mode }) => { console.log('>> defineConfig 接收到的 command:', command); console.log('>> defineConfig 接收到的 mode:', mode); console.log('>> 将加载 .env.' + mode + ' 文件');

return { plugins: [vue()],

// 开发服务器配置
server: {
  port: mode === 'test' ? 3000 : 5173,
  open: true, // 自动打开浏览器
},

// 构建配置
build: {
  sourcemap: true, // 生成 sourcemap 便于调试
},

}; });

添加的代码部分作用

拓展工具:fs+path 是为了做路径处理、本地文件读写,绝大多数用来配置路径别名、本地 https 证书

console.log('当前命令:', process.argv[2]); // 'serve' 或 'build' process.argv[]是node函数内置数组,记录终端启动的整条命令

console.log('当前工作目录:', process.cwd()); //获取你执行命令的文件夹路径(项目根目录)

2、核心:检测是否为 pnpm 项目(最关键)

const pnpmDir = path.join(process.cwd(), 'node_modules', '.pnpm'); if (fs.existsSync(pnpmDir)) { //fs.existsSync:Node 文件 API,判断这个文件夹是否存在 // 找到node_modules/.pnpm,判定是pnpm const items = fs.readdirSync(pnpmDir).slice(0,6); console.log('✅ 检测到 pnpm 的 .pnpm 目录(严格隔离模式)'); console.log(' .pnpm 目录内容(前6个):', items.join(',')); } else { console.log('❌ 未检测到 .pnpm 目录,当前可能不是 pnpm 项目'); }

const nodeModulesDir = path.join(process.cwd(), 'node_modules'); // 拼接路径:获取当前命令行所在根目录下 node_modules 文件夹的绝对路径

// 读取 node_modules 整个目录下所有文件/文件夹名称 // filter 过滤规则:保留「不以英文点 . 开头」的内容,剔除 .pnpm、.DS_Store 这类隐藏文件/隐藏目录 const topLevelDeps = fs.readdirSync(nodeModulesDir).filter(name => !name.startsWith('.'));

// 将过滤后的顶层依赖数组用英文逗号拼接为字符串,打印到终端 console.log(' node_modules 顶层依赖:', topLevelDeps.join(','));

返回Vite最终配置对象

return { // 注册vue插件,让Vite支持解析.vue单文件组件 plugins: [vue()],

开发服务相关配置,只在pnpm dev生效

server: {
  port: mode === 'test' ? 3000 : 5173, // 固定本地开发端口为5173,不使用随机端口
  <!-- defineConfig 回调写法优势:可以根据mode/command做动态配置 -->
  open: true, // 启动服务后自动打开默认浏览器访问项目地址
},

打包构建配置,只在pnpm build打包时生效

build.sourcemap: true 生成 .map 文件,方便调试时看到源码位置(工程化实战中,这用于线上报错定位)

检测开发环境 pnpm dev

alt text

检测自定义 staging 环境 pnpm build -- --mode staging

alt text

检测生产环境 pnpm build -- --mode test pnpm preview

alt text

需要切回开发模式的话 直接pnpm dev即可 毫不影响 正是工程化“构建与交付分离”的核心设计思想


DEMO3、ESLint + Prettier + Husky + lint-staged 全流程卡点实战 (直接在Demo2内操作)

在项目根目录执行:

第一步: 安装 ESLint 及相关 Vue 3 规则集

pnpm add -D eslint eslint-plugin-vue @vue/eslint-config-typescript @typescript-eslint/eslint-plugin @typescript-eslint/parser 包的作用:

安装 Prettier 及相关插件

pnpm add -D prettier eslint-config-prettier eslint-plugin-prettier

安装 Husky 和 lint-staged

pnpm add -D husky lint-staged

第二步:配置ESLint cjs文件

比如说:.eslintrc.cjs

为什么在ESModule 项目中要使用CommonJS的写法? :如果起名为xxxx.js Node会把它当成ESM ESM不可以直接写module.exports 语法报错 所以说要用xxx.cjs 强制走CommonJS 写法固定成熟 几乎不会出现解析BUG 稳定性更高

第三步:创建 Prettier 配置文件

项目根目录创建 .prettierrc.cjs:

第四步:创建 Prettier 忽略文件

在该文件中声明的文件/目录 Prettier 会跳过这些目录/文件,不进行格式化。 一般就是dist/ node*modules/ *.log _.lock _.yaml _.yml _.html _.svg 这些文件 总而言之 就是Prettier 只管手写的业务源码(ts、js、vue、css 等);

第五步:在 package.json 中添加 scripts 和 lint-staged 配置

打开 package.json,在 "scripts" 中添加以下命令,并在根节点添加 "lint-staged" 字段:

具体操作:只改 3 个地方

  1. 在 "scripts" 里添加 5 个新命令(原有的 dev、build、preview 不要动) json { "scripts": { "lint": "eslint . --ext .vue,.js,.ts --fix", // ← 新增 "format": "prettier --write .", // ← 新增 "lint:check": "eslint . --ext .vue,.js,.ts", // ← 新增 "format:check": "prettier --check .", // ← 新增 "prepare": "husky" // ← 新增 //"prepare": "husky" 的作用是让 pnpm install 时自动初始化 Husky,这样团队成员克隆项目后只需 pnpm install,Git Hooks 就会自动配置好 } }
  2. 在 package.json 的根节点(和 "scripts" 平级)添加 "lint-staged" 字段 json { "name": "demo2-env", "private": true, "version": "0.0.0", "scripts": { /_ ... / }, "lint-staged": { // ← 整个字段都是新增的 ".{js,ts,vue}": [ "eslint --fix", "prettier --write" ], ".{css,scss}": [ "prettier --write" ], ".{json,md}": [ "prettier --write" ] }, "dependencies": { /_ ... / }, "devDependencies": { / ... */ } // ← 新增的 devDependencies 也在这里 }
  3. 在 "devDependencies" 中添加新包的列表(原有的 @vitejs/plugin-vue、vite、vue 等都要保留) json { "devDependencies": { "typescript": "^5.5.3", "vue-eslint-parser": "^9.4.3" } }

第六步:初始化 Husky 在Git bash终端中输入

必须是"prepare": "husky install" 否则钩子无法初始化!!

Husky 9.x 的初始化方式和使用 package.json 中的 "prepare": "husky" 脚本自动触发。为了让 Husky 立即生效,手动执行一次: pnpm run prepare

但需要在当前文件夹内部申明 Git Husky若无Git(git init)存在 则无法初始化

这会在项目根目录创建 .husky/ 文件夹。然后手动创建 pre-commit 钩子文件:

一键生成钩子pre-commit :npx husky hook add pre-commit "npx lint-staged"

查看 .husky/pre-commit 文件内容,确保它是这样的: #!/bin/sh . "$(dirname "$0")/_/husky.sh" //里面没有\

pnpm lint-staged

这个文件的作用: 当你执行 git commit 时,Git 会先执行这个脚本。脚本调用 pnpm lint-staged,而 lint-staged 只会检查你本次 git add 的那些文件。

第七步:验证安装是否成功

验证 1:ESLint 能正常运行 pnpm lint 验证 2:Prettier 能正常运行 pnpm format 验证 3:lint-staged 能正常运行 git add src/App.vue //先 add 一个文件到暂存区 pnpm lint-staged alt text 发现代码报错 原因是 eslint版本过新 不支持原本的.eslintrc.cjs文件 需要改用eslint.config.js

解决方案:

方案一:(最简单推荐:降级 ESLint 到 8.x,适配你现有的.eslintrc.cjs) 新版本 ESLint 改动极大,大量规则写法全部重写,新手最稳妥是降级回稳定的 v8 版本,兼容你现在所有配置: 卸载现有高版本 eslint 全套 powershell pnpm remove eslint eslint-plugin-vue @vue/eslint-config-typescript @typescript-eslint/eslint-plugin @typescript-eslint/parser 固定安装 8 系列稳定版 powershell pnpm add -D eslint@8 eslint-plugin-vue @vue/eslint-config-typescript @typescript-eslint/eslint-plugin @typescript-eslint/parser 再次执行 powershell pnpm lint .eslintrc.cjs 可以正常被识别,不用改配置代码。 方案二:(保留 ESLint 10,全面迁移新版配置) 需要做 3 件事: 把根目录的 .eslintrc.cjs 重命名为 eslint.config.js 把配置语法从 CommonJS module.exports 改成 ESM export default,重写整套规则格式(语法变动非常多) 适配插件导入写法,大部分旧规则名失效需要替换 pnpm add -D @eslint/js typescript-eslint eslint-config-prettier //10代需要添加的扁平包 缺点:改动量大,你整套 Vue+TS 规则都要重写,不适合现阶段。

ESLint 9/10 是大版本破坏性更新,抛弃传统 .eslintrc 所有格式,强制全新扁平化配置

alt text 运行正常 但是一堆报错 是因为他扫描了一堆本不该扫描的文档/文件 可以改为"lint": "eslint src --ext .vue,.js,.ts --fix", alt text 可以发现 改完不扫描无关文档/文件 就不会报错了

alt text Prettier正常运行

alt text Lint-staged正常运行

第八步:故意写一段“坏代码”(验证卡点)

如:故意用双引号,故意不加分号等

第九步:提交坏代码(观察被拦截)

#将坏代码添加到暂存区 git add src/App.vue

#尝试提交 git commit -m "test: 故意提交坏代码验证 ESLint 拦截" 而后就会看到Husky的日志 报错 阻止提交

ESLint 和 Prettier 谁先执行?

lint-staged 配置里从左到右依次执行:["eslint --fix", "prettier --write"]。先 ESLint 修代码风格,再 Prettier 格式化,避免相互覆盖

--fix 修不了的问题怎么办?

自动修复失败时,lint-staged 会终止提交,你需要手动修复后再 add 提交。这是故意的——宁可不让提交,也不能让坏代码进仓库。


DEMO4 Vite 分包策略与路由懒加载效果验证

第一步:在现有项目里创建 3 个页面 Home User Admin

第二步:安装并配置路由

pnpm add vue-router 在router/index.ts里加上 // src/router/index.ts import { createRouter, createWebHistory } from 'vue-router';

// ⭐ 关键:所有页面都用 () => import() 实现懒加载 const routes = [ { path: '/', name: 'Home', component: () => import('@/views/Home.vue'), }, { path: '/user', name: 'User', component: () => import('@/views/User.vue'), }, { path: '/admin', name: 'Admin', component: () => import('@/views/Admin.vue'), }, ];

const router = createRouter({ history: createWebHistory(), routes, });

export default router;

在这段代码中 用component:()=>import('文件地址')实现路由的懒加载

第三步:修改(添加几行代码) src/main.ts 注册路由

import router from './router';

app.use(router);

第四步:修改 src/App.vue 显示路由出口

🌍 Vite 分包验证 Demo

当前环境:{{ mode }}

//import.meta.env.MODE无法在非js/ts我机组生效 此处现在替换为了mode 同时在下方声明
<nav style="margin: 20px 0; padding: 12px; background: #f5f5f5; border-radius: 6px;">
  <router-link to="/" style="margin-right: 20px;">🏠 首页</router-link>
  <router-link to="/user" style="margin-right: 20px;">👤 用户中心</router-link>
  <router-link to="/admin">🔐 管理后台</router-link>
</nav>

<hr />

<!-- 路由出口:当前页面的内容会渲染在这里 -->
<router-view />
<script setup> console.log('App.vue 加载了(主入口)'); const mode =import.meta.env.MODE </script>

第五步:修改 vite.config.ts 配置分包

这一步是关键——让 Vite 在打包时主动拆分代码: 修改vite.config.ts 保留所有原有配置,只新增 resolve 和 build.rollupOptions

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

resolve: { // 配置路径别名,让 @ 指向 src/ alias: { '@': path.resolve(__dirname, 'src'), }, },

build: { // 生成 sourcemap,便于调试时定位源码位置 sourcemap: true, rollupOptions: { output: {

分包拆分规范:手动分包:把 Vue 核心库单独打成一个 vendor 包 !!!!此处最新版Rollup要求使用函数形式而非对象形式

manualChunks(id) { // 所有以 'vue' 开头的包(vue、vue-router 等)都打包到 vue-vendor 中 if (id.includes('vue') || id.includes('vue-router')) { return 'vue-vendor' } }, // 每个 chunk 的文件名格式,[name] 会自动取 manualChunks 里定义的 key

chunk里的[hash]值的作用:每次构建会生成唯一的hash值,重新构建会生成一个新的hash值,浏览器就会认为他变了,会重新强制加载一次缓存;如果hash值不变,则浏览器就只会一直读缓存里的,不会重复下载

chunkFileNames: 'assets/[name]-[hash].js', }, }, }, });

第六步:在终端一次性验证所有场景

在开发环境下: pnpm dev : 不做完整打包,按需加载文件,启动,热更新极快

alt text 可以看出:所有 .vue 组件是通过 ?import 请求分别加载的(因为 Vite 在开发模式下不做打包合并) alt text 再次观察:打开其他界面时(切换路由),浏览器network面板可以看见该界面的动态加载

在生产环境下: pnpm build

1.读取环境变量2.解析入口3.构建依赖图谱4.插件转换(把输入的.vue,.ts,.less转换为.js,.css 5.摇树优化6.代码拆分7.压缩混淆8.输出dist/目录+清单(manifest) alt text

在预览环境下: pnpm preview(最重要的一步)

alt text 明显的发现Network面板少了不少加载的东西 没有出现 User-xxx.js 或 Admin-xxx.js。说明它们没有被加载,实现了“按需加载” 然后点击导航栏的“用户中心”,观察 Network 面板,或者也可以观察console面板 发现这时出现了我们点击对应的路由的名字 alt text

这就是路由懒加载的效果——用户不访问的页面,代码不会提前加载

如果失去分包规范,即失去manualChunks的功能 会出现:文件重复下载

manualChunks的功能:分包规范,Rollup 原生提供的手动分包配置项,Vue 核心库只加载一次,后续页面直接读浏览器缓存,减少重复下载,这个文件的 hash 只在这些库的版本号变化时才会改变——你改业务代码不会让 vendor 的 hash 变化,用户不需要重新下载 85KB 的 Vue 核心库,只下载被改动的业务 chunk

该步骤核心:分包的核心目标就是把首页必须加载的 JS 压缩到最小


DEMO5:基于 fs + inquirer 的自动化代码生成器

第一步:安装依赖

用 Node.js 脚本,通过命令行交互询问用户,然后自动创建 view、api、store 三个目录下的文件骨架 inquirer 从 v9 开始是纯 ESM 包,如果项目是 ESM("type": "module"),直接 import 即可。但我们这里用 CommonJS 方式写脚本(方便在终端直接运行),所以使用 inquirer@8.2.6(最后一个支持 CommonJS 的版本) 最新版本: pnpm add -D inquirer @types/inquirer 支持cjs写法的8.2.6版本: pnpm add -D inquirer@8.2.6

第二步:创建脚本文件

在项目根目录下创建scripts文件夹,然后创建 generate.ts 解决了:日常开发每个新业务模块都要手动新建 3 个文件、重复写基础模板,效率低且格式不统一: 不用反复复制粘贴 Vue、API、Pinia 样板代码 强制统一全项目代码格式、命名规范(大驼峰接口 / 小驼峰文件) 自带 TS 类型模板,不用从零手写 interface 内置文件覆盖保护,防止误删已有代码,支持--force强制覆盖

运行使用方式: 常规唤起交互 ts-node generate.ts

第三步:在 package.json 中添加脚本命令

"gen": "node scripts/generate.js"

第四步:创建必要的目录结构

在 src/ 下创建 api/、stores/modules/、views/ 目录(如果还没有的话): //mkdir = make directory -p 父目录不存在时自动逐级创建;文件夹已经存在也不会报错,静默跳过

mkdir -p src/api mkdir -p src/stores/modules mkdir -p src/views

同时需要创建 src/utils/request.ts(API 模板里引用了它): // src/utils/request.ts import axios from 'axios';

const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 10000, });

// 响应拦截器:直接返回 data request.interceptors.response.use( (response) => response.data, (error) => Promise.reject(error) );

export default request;

第六步:执行脚本生成模块

pnpm gen alt text

第七步:验证生成的文件

在src/views内部新生成了一个新的文件夹product 里面有新生成的index.vue 以及在api/中生成的product.ts

第八步:将生成的路由注册到 Vue Router

打开 src/router/index.ts,在 routes 数组中新增一个路由: // ========== 新增:自动生成的 product 路由 ========== { path: '/product', name: 'Product', component: () => import('@/views/product/index.vue'), },

第九步:在首页添加导航链接

打开views/Home.vue 在导航区添加 /product 的链接 去产品页

第十步:验证完整流程

pnpm dev

真正作用:大量的模块只需要在终端输入pnpm gen输入多次 输入多次模块名,就可以省去大量时间。并且这个脚本“目录分层规范”变成了一条自动化生产线。 新人入职第一天跑一遍 pnpm gen product,生成的文件结构跟团队老员工的一模一样,不需要有人口头告诉他“你要在 api 下建文件、还要按这个格式写”————让机器帮你执行规范,而不是靠人脑记


DEMO 6 : GitHub Actions CI 流水线

git push 代码到 GitHub 时,GitHub 自动拉取代码 → 安装依赖 → 代码校验(ESLint) → 执行测试(Vitest) → 打包构建。任何一步失败,流水线标红,阻止代码合并。

第一步:在项目中安装 Vitest(测试框架)

pnpm add -D vitest @vue/test-utils jsdom @vitest/coverage-v8 : vitest:测试运行器(Vite 原生集成,启动极快) @vue/test-utils:Vue 组件测试工具库,用于挂载和交互 Vue 组件 jsdom:在 Node 环境里模拟浏览器 DOM API(如 document、window),让组件测试能在 Node 中运行 @vitest/coverage-v8:测试覆盖率统计工具(V8 引擎原生支持)

第二步:写一个简单的单元测试(验证用)

// src/utils/__tests__/math.test.ts import { describe, it, expect } from 'vitest';

// 一个简单的加法函数(直接写在测试里,不用单独建文件) function add(a: number, b: number): number { return a + b; }

describe('add 函数', () => { it('1 + 2 应该等于 3', () => { expect(add(1, 2)).toBe(3); });

it('-1 + 1 应该等于 0', () => { expect(add(-1, 1)).toBe(0); }); });

第三步:在 package.json 中添加测试命令(加两行就行)

"test": "vitest run", "test:coverage": "vitest run --coverage"

第四步:本地验证测试能跑通

pnpm test

预期结果: alt text 如果显示 Test Files 1 passed,说明测试跑通了。如果没有输出或报错,检查是否安装了 vitest,以及 tests 目录是否在 src/utils/ 下

第五步:在 GitHub 上创建仓库并推送代码

这一步先确认你已经把代码推到 GitHub 了: git remote -v 然后把代码推送上去,确保 GitHub 仓库里有你的代码。

第六步:创建 GitHub Actions 流水线配置文件 !!一定要在总文件的根目录配置 否则检测不出来!!

创建 .github/workflows/ci.yml:

name: CI 流水线

on: push: branches: [main] pull_request: branches: [main]

jobs: build-and-test: # 运行环境:最新 Ubuntu runs-on: ubuntu-latest

steps:

  • name: 检出代码 uses: actions/checkout@v4
  • name: 安装 pnpm uses: pnpm/action-setup@v3 with: version: 8
  • name: 安装 Node.js uses: actions/setup-node@v4 with: node-version: '18' cache: 'pnpm'
  • name: 安装依赖 run: pnpm install
  • name: 代码规范检查 run: pnpm lint
  • name: 单元测试 run: pnpm test
  • name: 构建项目 run: pnpm build
提交并推送: git add .github/workflows/ci.yml git commit -m "feat: 添加 GitHub Actions CI 流水线" git push

第七步:观察流水线执行结果

打开你的 GitHub 仓库页面

点击 Actions 标签页

你会看到 CI 流水线 正在运行(黄色圆点表示运行中)

点击进去,可以看到每一步的执行日志 alt text alt text报错详细说明 流水线标红 ❌,阻止了这次提交的合并(如果你配置了分支保护规则,这个 PR 会被自动标记为不可合并)


Demo 7:Playwright 端到端(E2E)测试(对应导图中的 E2E 部分)

验证: 用 Playwright 自动打开浏览器,模拟用户访问首页 → 点击“用户页” → 检查页面是否正常。

对应你导图中的: Vitest, Playwright E2E 测试 自动执行冒烟测试,验证页面可以正常访问,接口 500 无报错

第一步:安装 Playwright

pnpm add -D @playwright/test npx playwright install:npx playwright install 会下载 Chromium、Firefox 和 WebKit 三个浏览器内核,用于在不同浏览器中运行测试,下载时间可能较长 可以只安装 Chromium 节省时间:npx playwright install Chromium 也可以安装edge:Playwright 里 Edge 命名为 msedge :npx playwright install msedge

第二步:创建 E2E 测试文件

创建 e2e/homepage.spec.ts: // e2e/homepage.spec.ts import { test, expect } from '@playwright/test';

test.describe('首页冒烟测试', () => { test('首页应该正常加载', async ({ page }) => { // 访问首页 await page.goto('http://localhost:5173/');

// 检查页面标题是否包含 "Vite 分包验证 Demo"
await expect(page.locator('h1')).toContainText('Vite 分包验证');

});

test('点击用户页链接应该跳转到 /user', async ({ page }) => { await page.goto('http://localhost:5173/');

// 点击 "用户中心" 链接
await page.click('text=用户中心');

// 验证 URL 变成了 /user
await expect(page).toHaveURL(/.*\/user/);

// 验证页面出现了 "用户中心" 标题
await expect(page.locator('h1')).toContainText('用户中心');

}); });

第三步:在 package.json 中添加 E2E 测试命令

{ "scripts": { "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui" } }

第四步:本地验证 E2E 测试(注意: Playwright 需要开发服务器正在运行。开两个终端:)

终端 1(启动开发服务器):

pnpm dev

终端 2(运行 E2E 测试):

pnpm test:e2e 预期: Running 2 tests using 1 worker

✓ e2e/homepage.spec.ts:4:7 › 首页冒烟测试 › 首页应该正常加载 (2.1s) ✓ e2e/homepage.spec.ts:10:7 › 首页冒烟测试 › 点击用户页链接应该跳转到 /user (1.5s)

2 passed (4.2s)

第五步:把 E2E 测试加入 CI 流水线

修改 .github/workflows/ci.yml,在 build 步骤之后添加 E2E 测试步骤:

  • name: 构建项目 run: pnpm build
  • name: 安装 Playwright 浏览器 run: npx playwright install --with-deps msedge

  • name: 运行 E2E 测试 run: | pnpm preview & npx wait-on http://localhost:4173 pnpm test:e2e env: CI: true

第六步:提交并推送,观察流水线

git add . git commit -m "feat: 添加 Playwright E2E 测试并集成到 CI" git push

打开 GitHub Actions,会看到流水线中新增了 “安装 Playwright 浏览器” 和 “运行 E2E 测试” 两个步骤,全部通过后流水线变绿。


Demo 8:性能监控 SDK(对应导图中的“运行时监控层”)

监控LCP、CLS、FID 等 Web Vitals 指标,并上报到监控平台。 LCP → 页面主要内容加载完成 FID → 用户首次交互响应延迟 CLS → 页面视觉稳定性 采集LCP/CLS/FID/FCP/TTFB五大前端性能指标,打印日志,可对接接口上报后端

第一步:创建性能监控脚本

创建 src/utils/performanceReporter.ts:

// src/utils/performanceReporter.ts // 定义性能指标上报的数据TS类型约束 interface ReportData { // 当前页面完整URL地址 url: string; // 最大内容绘制,可选,单位ms lcp?: number; // 累积布局偏移,可选 cls?: number; // 首次输入延迟,可选(FID已废弃,浏览器主推INP) fid?: number; // 首次内容绘制,可选,ms fcp?: number; // 首字节时间,服务器返回首个字节耗时,ms ttfb?: number; // 性能采集时间戳(毫秒时间戳) timestamp: number; }

/**

  • 页面核心Web Vitals性能采集函数
  • 采集LCP/CLS/FID/FCP/TTFB五大前端性能指标,打印日志,可对接接口上报后端 */ export function reportWebVitals() { // 初始化性能上报对象,基础字段:当前页面地址、采集时间戳 const report: ReportData = { url: window.location.href, timestamp: Date.now(), };

// ====================== 1. 监听 LCP 最大内容绘制 ====================== // PerformanceObserver:浏览器原生性能监听API,异步捕获性能条目 new PerformanceObserver((list) => { // 获取本次回调所有性能条目数组 const entries = list.getEntries(); // LCP会多次触发,取最后一次(最终最大元素) const lastEntry = entries[entries.length - 1]; if (lastEntry) { // 赋值LCP耗时:页面开始加载到最大可视元素渲染完成的毫秒数 report.lcp = lastEntry.startTime; console.log('[性能监控] LCP:', report.lcp); // 可放开注释,把完整指标上报后端接口 // sendToServer(report); } // 监听的性能类型:largest-contentful-paint 专门对应LCP指标 }).observe({ entryTypes: ['largest-contentful-paint'] });

// ====================== 2. 监听 CLS 累积布局偏移 ====================== // 用来累加所有布局偏移值 let clsValue = 0; new PerformanceObserver((list) => { // 遍历本次所有布局偏移记录 for (const entry of list.getEntries() as any[]) { // hadRecentInput:用户近期是否有点击/输入操作 // 规范要求:用户主动交互引发的布局偏移不计入CLS,只算意外抖动 if (!entry.hadRecentInput) { clsValue += entry.value; } } // 把累加后的总偏移存入上报对象 report.cls = clsValue; console.log('[性能监控] CLS:', report.cls); // 监听页面布局变动事件 }).observe({ entryTypes: ['layout-shift'] });

// ====================== 3. 监听 FID 首次输入延迟 ====================== new PerformanceObserver((list) => { const entries = list.getEntries(); // FID只取页面第一次用户交互的记录 const firstEntry = entries[0] as any; if (firstEntry) { // 计算延迟:浏览器收到用户输入时间 → 浏览器真正开始处理输入的时间差 report.fid = firstEntry.processingStart - firstEntry.startTime; console.log('[性能监控] FID:', report.fid); } // first-input:浏览器首次用户输入行为(点击、按键等) }).observe({ entryTypes: ['first-input'] });

// ====================== 4. 监听 FCP 首次内容绘制 ====================== new PerformanceObserver((list) => { const entries = list.getEntries(); const firstEntry = entries[0]; if (firstEntry) { // startTime:从页面初始化到页面第一次渲染出文字/图片等内容的耗时 report.fcp = firstEntry.startTime; console.log('[性能监控] FCP:', report.fcp); } // paint 绘制类指标,包含 FCP / LCP 等绘制事件 }).observe({ entryTypes: ['paint'] });

// ====================== 5. 监听 TTFB 首字节时间 ====================== new PerformanceObserver((list) => { const entries = list.getEntries(); // 遍历导航性能条目 for (const entry of entries) { // 筛选页面导航类型的性能数据 if (entry.entryType === 'navigation') { // 类型断言成导航详细性能对象 const navEntry = entry as PerformanceNavigationTiming; // TTFB = 浏览器发起请求开始 到 接收到服务器第一个字节的间隔 report.ttfb = navEntry.responseStart - navEntry.requestStart; console.log('[性能监控] TTFB:', report.ttfb); } } // 监听页面导航(页面刷新、跳转)全链路耗时 }).observe({ entryTypes: ['navigation'] }); }

第二步:在 main.ts 中启用性能监控

第三步:验证(用 Lighthouse 跑分对比)

打开 Chrome DevTools → Lighthouse 面板 点击 Analyze page load(分析页面加载) 运行完成后,你会看到 LCP、CLS、FID 的官方评分 对比脚本里 console.log 打印出来的数值,应该基本一致

但是我们前面ESLint把console全部都给Error了 所以现在需要去把他关闭掉才会让她再次显示

在eslint里把eslintrc.cjs里的rule里的 no-console的内容 改成 'off' (必须是单引号)


Demo 9:自动化部署脚本

这个demo验证: 写一个 Node.js 脚本,模拟 Jenkins/GitLab CI 的部署流程——备份当前版本 → 上传新版本 → 刷新 Nginx → 验证页面可访问(在本地模拟,不真的操作服务器)。

第一步:创建部署脚本

创建 scripts/deploy.ts: #!/usr/bin/env ts-node // scripts/deploy.ts import _ as fs from 'fs'; import _ as path from 'path'; import { execSync } from 'child_process'; import { fileURLToPath } from 'url';

const **filename = fileURLToPath(import.meta.url); const **dirname = path.dirname(__filename);

const DIST_DIR = path.join(**dirname, '../dist'); const BACKUP_DIR = path.join(**dirname, '../backups');

// 1. 校验dist文件夹存在 if (!fs.existsSync(DIST_DIR)) { console.error('❌ dist 目录不存在,请先执行 pnpm build'); process.exit(1); }

// 2. 创建备份目录(递归创建) if (!fs.existsSync(BACKUP_DIR)) { fs.mkdirSync(BACKUP_DIR, { recursive: true }); }

// 3. 生成时间戳版本号,替换冒号小数点避免路径非法字符 const version = new Date().toISOString().replace(/[:.]/g, '-'); const backupPath = path.join(BACKUP_DIR, dist-${version});

console.log(📦 备份当前版本到: ${backupPath}); fs.cpSync(DIST_DIR, backupPath, { recursive: true });

// 4. 复制产物到部署目录 const DEPLOY_DIR = path.join(__dirname, '../deploy'); if (!fs.existsSync(DEPLOY_DIR)) { fs.mkdirSync(DEPLOY_DIR, { recursive: true }); }

console.log('📤 上传新版本到部署目录...'); fs.cpSync(DIST_DIR, DEPLOY_DIR, { recursive: true });

// 5. 模拟重载Nginx console.log('🔄 模拟重载 Nginx...');

// 6. 冒烟测试 console.log('🧪 执行冒烟测试...'); try { const result = execSync('curl -s -o /dev/null -w "%{http_code}" http://localhost:5173', { timeout: 5000, }).toString().trim();

if (result === '200') { console.log('✅ 冒烟测试通过!首页可访问'); } else { console.log(⚠️ 冒烟测试异常:HTTP ${result}); } } catch (error) { console.log('⚠️ 冒烟测试失败(开发服务器未启动,请先 pnpm dev)'); }

console.log(\n🎉 部署完成!版本号: ${version}); console.log(📌 备份位置: ${backupPath}); console.log(📌 回滚命令: cp -r ${backupPath}/* ${DIST_DIR}/);

第二步:在 package.json 中添加部署命令

{ "scripts": { "deploy": "pnpm build && node scripts/deploy.ts" } }

deploy文件夹

相当于:生产服务器上正在运行的 Web 服务根目录(比如 Nginx 指向的 /usr/share/nginx/html)。

里面的内容:assets/ 目录下存放着 index-yMkJyNDY...(入口 JS)和 vue-vendor-Ys7F...(第三方库 JS)。

怎么生成的:当执行 pnpm deploy 时,脚本会把构建好的 dist/ 文件夹完整复制一份到这里。如果服务器部署,运维就会把这里的内容挂载给用户访问。

backup文件夹

2.(操作2) Backups 文件夹(历史版本快照) 相当于:生产环境上的“版本历史仓库”。每次你执行 pnpm deploy,脚本都会先把当前的 deploy 文件夹压缩打包(以时间戳命名),放进 Backups 里。

为什么要有它:为了 Demo 10(版本回滚)。如果新的 deploy 版本出了严重 Bug,你可以执行 pnpm rollback,脚本会从 Backups 里挑一个旧版本,直接覆盖回 deploy,实现“秒级回滚”。

这两个文件夹作用:deploy 是正在运行的现场,Backups 是时间机器。你不需要真的买一台云服务器,就在本机把“持续交付(CD)”的核心流程亲手跑了一遍

为什么文件名全是乱码?(DEMO4

这是 Vite 构建时加的 Hash(哈希值)。 index-yMkJyNDY.js 里的 yMkJyNDY 是根据 index.js 的文件内容算出来的。内容变了,Hash 就变。 作用:强制浏览器刷新缓存。如果你改了一行代码,新版本的 index-新Hash.js 会被加载,而旧的 index-旧Hash.js 会留在 Backups 里,不会被浏览器误用

第三步:验证

pnpm deploy

预计输出结果:

未加载成功


Demo 10:版本回滚机制( 写一个回滚脚本,列出所有备份版本,选择其中一个快速恢复,模拟线上出问题后秒级回滚)

第一步:创建回滚脚本 scripts/rollback.ts

#!/usr/bin/env ts-node // scripts/rollback.ts

import _ as fs from 'fs'; import _ as path from 'path'; import * as readline from 'readline'; import { fileURLToPath } from 'url';

const **filename = fileURLToPath(import.meta.url); const **dirname = path.dirname(__filename);

const DIST_DIR = path.join(**dirname, '../dist'); const BACKUP_DIR = path.join(**dirname, '../backups');

// 1. 检查备份目录 if (!fs.existsSync(BACKUP_DIR)) { console.error('❌ 没有找到备份目录'); process.exit(1); }

// 2. 列出所有备份版本 const backups = fs.readdirSync(BACKUP_DIR) // 过滤:只保留以 dist- 开头的备份文件夹,排除无关文件 .filter(name => name.startsWith('dist-')) // 按字符串升序排序(时间戳命名,旧版本靠前) .sort() // 数组倒序翻转,最新的备份排在最前面 .reverse();

if (backups.length === 0) { console.error('❌ 没有找到任何备份版本'); process.exit(1); // 终止node进程,返回异常退出码 }

// 打印提示文案 console.log('📋 可用的备份版本:'); // 遍历所有备份,按序号打印给用户看 backups.forEach((name, index) => { console.log( ${index + 1}. ${name}); });

// ========== 开始交互式命令行输入 ========== // 引入readline模块,创建命令行交互实例,读取键盘输入、控制台输出 const rl = readline.createInterface({ input: process.stdin, // 输入来源:终端键盘 output: process.stdout, // 输出位置:终端控制台 });

// 弹出问题,等待用户输入版本数字,回车后执行回调函数 rl.question('\n请选择要回滚到的版本编号(输入数字):', (answer) => { // 用户输入是字符串,转数字;用户看到的序号从1开始,数组下标从0开始,所以-1 const index = parseInt(answer.trim()) - 1;

// 合法性校验:不是数字 / 下标小于0 / 下标超过备份总数 → 输入非法 if (isNaN(index) || index < 0 || index >= backups.length) { console.error('❌ 无效的选择'); rl.close(); // 关闭命令行交互 process.exit(1); // 异常退出 }

// 根据合法下标拿到选中的备份文件夹名称 const selectedVersion = backups[index]; // 拼接得到该备份文件夹的完整绝对路径 const backupPath = path.join(BACKUP_DIR, selectedVersion);

// 二次风险提醒:告知用户要回滚的版本,提示dist会被全覆盖 console.log(\n⚠️ 即将回滚到: ${selectedVersion}); console.log( 当前 dist 将被完全替换);

// 再次弹窗确认,询问是否真的执行回滚 rl.question('确认继续?(y/N):', (confirm) => { // 用户输入不是y/Y,判定取消回滚 if (confirm.toLowerCase() !== 'y') { console.log('👋 已取消回滚'); rl.close(); return; // 终止后续逻辑 }

// 开始执行回滚操作
console.log(`🔄 正在回滚到 ${selectedVersion}...`);

// 如果当前dist文件夹存在
if (fs.existsSync(DIST_DIR)) {
  // 强制递归删除整个dist目录,recursive删文件夹、force无视权限/只读强制删除
  fs.rmSync(DIST_DIR, { recursive: true, force: true });
}
// 将选中的备份文件夹完整复制覆盖到dist
fs.cpSync(backupPath, DIST_DIR, { recursive: true });

// 回滚成功提示
console.log(`✅ 回滚完成!当前版本: ${selectedVersion}`);
rl.close(); // 关闭命令行交互,脚本正常结束

}); });

第二步:在 package.json 中添加回滚命令

{ "scripts": { "rollback": "node scripts/rollback.js" } }

第三步:验证(先创建几个备份版本(多次执行 pnpm deploy),然后执行回滚:)

pnpm rollback

效果如图所示: 未成功加载


Demo 11:CDN 加速模拟

配置 Vite 的 base 选项,让构建产物的所有资源路径指向 CDN 地址,而不是相对路径。然后用 http-server 模拟 CDN 服务器,观察资源加载变化

第一步:修改 vite.config.ts 配置 CDN 地址

base: mode === 'production' ? 'https://cdn.example.com/demo2-env/' // 生产环境走 CDN : '/', // 开发环境走相对路径

第二步:构建并观察产物路径变化

pnpm build 打开 dist/index.html,会看到:未成功加载 原本的绝对路径 变为了相对路径

第三步:用 http-server 模拟 CDN 服务器 (pnpm add -D http-server)

在 package.json 中添加模拟 CDN 的命令: "serve:cdn": "http-server dist --port 8080 --cors"(scripts下加)

第四步:验证 CDN 加载效果

在一个终端启动 CDN 服务器:pnpm serve:cdn(此时 dist/ 下的资源可以通过 http://localhost:8080/assets/xxx.js 访问) 在另一个终端启动预览:pnpm preview(预览服务器会加载 dist/index.html,里面的资源 URL 指向 https://cdn.example.com/...,但因为 CDN 地址是假的,资源会 404) 未成功加载就像这样404

那怎么验证 CDN 生效了? 将 vite.config.ts 中的 base 改为 http://localhost:8080/(指向本地 CDN 模拟服务器),重新构建,再预览,此时资源就能正常加载了: 更进一步: 如果你有一个真实的 CDN 域名(比如阿里云 OSS、腾讯云 COS),可以把构建产物上传上去,然后把 base 改成真实的 CDN 域名,就完成了生产环境的 CDN 部署。


Demo12:请求池与并发控制

验证: 模拟在一个页面里同时发起 100 个接口请求,但通过请求池控制,同一时刻最多只发 6 个(浏览器同域名并发限制),剩下的排队等待,防止浏览器崩溃或页面卡死。

第一步:创建请求池工具

创建 src/utils/requestPool.ts:

// src/utils/requestPool.ts

/**

- 请求池:控制并发请求数量,防止浏览器因并发过多崩溃

- 浏览器对同一域名的并发连接数有限制(HTTP/1.1 约 6 个 HTTP/2大得多)

*/ export class RequestPool { private limit: number; private running: number; private queue: Array<() => void>;

constructor(limit = 6) { this.limit = limit; // 最大并发数 this.running = 0; // 当前正在运行的请求数 this.queue = []; // 等待队列 }

/**

  • 执行请求

  • @param fn 返回 Promise 的请求函数(如 () => axios.get('/api'))

  • @returns 请求结果 */ async request(fn: () => Promise): Promise { // 1. 如果当前并发数已达上限,进入排队等待 if (this.running >= this.limit) { await new Promise((resolve) => { this.queue.push(resolve); }); }

    // 2. 拿到执行名额,开始执行 this.running++; console.log([请求池] 当前并发数: ${this.running}/${this.limit});

    try { // 3. 执行真实请求 const result = await fn(); return result; } finally { // 4. 请求结束(无论成功/失败),释放名额 this.running--; console.log([请求池] 释放并发,剩余: ${this.running}/${this.limit});

    // 5. 如果队列里还有等待的任务,唤醒下一个
    if (this.queue.length > 0) {
      const next = this.queue.shift();
      if (next) {
        next(); // 唤醒等待的 request 调用
      }
    }
    

    }

} }

// 导出单例(整个应用共享一个请求池) export const requestPool = new RequestPool(6);

第二步:在 Vue 组件中模拟并发请求

🌍 请求池并发控制 Demo

<!-- 按钮:点击触发批量发起20个请求 -->
<button @click="sendRequests" style="padding: 12px 24px; font-size: 16px; cursor: pointer;">
  🚀 发送 20 个并发请求
</button>
<!-- 清空日志按钮 -->
<button @click="clearLogs" style="padding: 12px 24px; font-size: 16px; cursor: pointer; margin-left: 12px;">
  🗑️ 清空日志
</button>

<!-- 黑色日志面板,模拟终端控制台 -->
<div style="margin-top: 20px; padding: 16px; background: #1e1e1e; color: #d4d4d4; border-radius: 8px; max-height: 400px; overflow-y: auto; font-size: 13px;">
  <!-- 循环渲染每一条日志,index作为key(简易日志场景可用) -->
  <div v-for="(log, index) in logs" :key="index" style="padding: 2px 0; border-bottom: 1px solid #333;">
    {{ log }}
  </div>
</div>
<script setup lang="ts"> import { ref } from 'vue'; // 导入我们实现好的全局请求池单例 import { requestPool } from '@/utils/requestPool'; // 响应式数组:存放页面上展示的所有日志文本 const logs = ref([]); /** * ✅ 就是你问的 addLog 函数! * 作用:统一新增一条日志,自动带上当前时间,推入logs数组 * 数组更新后页面自动渲染这条日志 */ const addLog = (msg: string) => { logs.value.push(`[${new Date().toLocaleTimeString()}] ${msg}`); }; // 点击按钮触发的主函数:一次性发起20个模拟请求 const sendRequests = () => { logs.value = []; // 先清空旧日志 addLog('🚀 开始发送 20 个并发请求...'); addLog(`📌 最大并发数: 6,超出部分将排队等待`); // 收集所有请求Promise,最后统一监听全部完成 const promises: Promise[] = []; // 循环创建20个请求任务 for (let i = 1; i <= 20; i++) { // 请求函数:交给请求池调度的任务 const requestFn = () => { // 模拟接口耗时:1000 ~ 3000ms 随机延迟 const delay = 1000 + Math.random() * 2000; // 模拟异步接口 return new Promise<{ id: number; delay: number }>((resolve) => { setTimeout(() => { resolve({ id: i, delay }); }, delay); }).then((result) => { // 请求完成后打印日志到页面 addLog(`✅ 请求 #${result.id} 完成 (耗时 ${result.delay.toFixed(0)}ms)`); }); }; // 交给请求池管控执行(核心!限制最多同时跑6个) const promise = requestPool.request(requestFn); promises.push(promise); } // 等待20个请求全部执行完毕,打印结束提示 Promise.all(promises).then(() => { addLog('🎉 所有请求已完成!'); }); }; // 清空日志数组,页面日志随之消失 const clearLogs = () => { logs.value = []; }; </script>

第三步:验证

启动项目:pnpm dev

打开页面,点击 "发送 20 个并发请求"

观察控制台输出和页面日志面板

预期结果:

未加载成功

1.控制台显示 当前并发数: 1/6 → 2/6 → ... → 6/6

2.当并发数达到 6 后,第 7 个请求会等待,不会立即发起

3.一个请求完成后,并发数降为 5,下一个等待的请求立即顶上来

4.最终 20 个请求全部完成,整个过程并发数始终不超过 6 注意:这个依旧是FIFO 而不是SJF 不是按发起顺序先后完成,因为每个请求设置了【随机耗时】 设置了const delay=1000+Math.random()*2000

如果不用请求池会发生什么? 20 个请求同时发起,浏览器的网络调度器会把它们全部排队,但由于没有“主动控制并发”的逻辑,20 个请求会瞬间占满所有可用连接,页面可能变得响应缓慢。


Demo 13:指数退避重试(限制最大重试次数,杜绝无限死循环)

“指数退避”就是:重试的间隔越来越长。比如:第一次失败等 500ms 重试,第二次失败等 1000ms 重试,第三次失败等 2000ms 重试。既给了服务器恢复的时间,又不会造成“重试风暴”——如果所有客户端都在 500ms 后同时重试,服务器可能刚恢复又被冲垮。指数退避让重试请求分散到不同的时间点,错峰重试,降低了集群雪崩的风险。

验证: 模拟接口请求失败(返回 500 错误或超时),自动按 500ms → 1000ms → 2000ms 的间隔重试,最多重试 3 次

第一步:创建重试工具

创建 src/utils/retry.ts: // src/utils/retry.ts

// 1. 定义一个类型守卫,检查错误是否具有类似 Axios 错误的特征 function isAxiosError(error: unknown): error is { response?: { status: number }; code?: string; message?: string; } { return typeof error === 'object' && error !== null; }

export interface RetryOptions { maxRetries?: number; // 最大重试次数,默认 3 baseDelay?: number; // 基础延迟(毫秒),默认 500 maxDelay?: number; // 最大延迟上限(毫秒),默认 10000 // ⭐ 这里把 any 改成了 unknown shouldRetry?: (error: unknown) => boolean; }

/**

  • 指数退避重试 */ export async function retryWithBackoff( fn: () => Promise, options: RetryOptions = {} ): Promise { const { maxRetries = 3, baseDelay = 500, maxDelay = 10000, // 默认的重试判断逻辑 shouldRetry = (error: unknown) => { // 如果不是对象类型,直接不重试 if (!isAxiosError(error)) return false;

      // 只有 5xx 错误才重试(服务端错误)
      if (error.response?.status && error.response.status >= 500) return true;
      // 网络超时错误(Axios 的 ECONNABORTED)
      if (error.code === 'ECONNABORTED') return true;
      // 其他包含 timeout 关键字的错误
      if (error.message?.includes('timeout')) return true;
    
      return false;
    },
    

    } = options;

// ⭐ lastError 也改为 unknown let lastError: unknown = null;

for (let attempt = 1; attempt <= maxRetries; attempt++) { try { if (attempt > 1) { console.log([重试] 第 ${attempt - 1} 次重试...); } return await fn(); } catch (error) { lastError = error;

  // 判断是否需要重试
  if (!shouldRetry(error)) {
    console.log('[重试] 错误类型不支持重试,直接抛出');
    throw error;
  }

  if (attempt === maxRetries) {
    console.log(`[重试] 已达最大重试次数 ${maxRetries},停止重试`);
    throw error;
  }

  // 计算退避延迟:2^(attempt-1) * baseDelay
  const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
  console.log(`[重试] ${delay}ms 后重试 (尝试 ${attempt}/${maxRetries})`);

  await new Promise((resolve) => setTimeout(resolve, delay));
}

}

throw lastError; } any在eslintd的recommand模式里是默认被判断为error的 可以把any换成unknown 他比他的类型更强 注意使用unknown的时候要加一个类型守卫 不然无法使用 // 1.声明 function isAxiosError(error: unknown): error is { response?: { status: number }; code?: string; message?: string; } { return typeof error === 'object' && error !== null; } // 2. 在重试逻辑里使用它 catch (error) { // 这里 error 是 unknown(未知包裹)

// ⭐ 类型守卫在这里发挥作用!
// 我们调用 isAxiosError,如果返回 true,那么在这个 if 块内部,
// TypeScript 就知道 error 不再是“未知包裹”,而是“拆开后的具体对象”
if (isAxiosError(error)) {

  // ✅ 这里可以安全地访问 response.status 了!
  // 因为 isAxiosError 保证了 error 是一个对象,并且有 response 和 code 属性
  if (error.response?.status >= 500) {
    // 服务端错误,决定重试...
  }

  if (error.code === 'ECONNABORTED') {
    // 超时错误,决定重试...
  }
}

其他逻辑.....

第二步:创建测试 API(模拟失败)

(error as any).response = { status: 500 };如果报错说不让使用any 可以声明 (error as Error & { response: { status: number } }).response = { status: 500 }; 表明:这是一个 Error,同时还有一个 response 属性” 创建 src/api/testApi.ts:

第三步:在 App.vue 中添加测试按钮

保持原有内容不变 新增一点内容 其实就是加了个按钮 !!!但要注意@click要在style之后 不然会被warning

🔄 测试重试 (前2次失败,第3次成功) 🔄 重置状态

在script部分里 import { fetchWithRetry, resetAttemptCount } from '@/api/testApi';const testRetry = async () => { addLog('🔁 开始测试重试...'); try { const result = await fetchWithRetry(); addLog(✅ 请求成功: ${result.data}); } catch (error) { addLog(❌ 请求失败: ${(error as Error).message}); } };

const resetTest = () => { resetAttemptCount(); addLog('🔄 已重置计数器'); };

第四步:验证

点击 "测试重试",观察控制台输出:

第 1 次调用失败 → [重试] 500ms 后重试

第 2 次调用失败 → [重试] 1000ms 后重试

第 3 次调用成功 → 请求成功!

点击 "重置状态" 后再次测试,会重复上述过程 如图: 未能加载成功

这一步是在浏览器console控制台里输出的 不是界面的输出处输出的 界面的输出处不会输出这些内容!!

About

A note about whole Front-end Engineering.A lots demos of Frontend Architecture Setup

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages