CC 咖啡猫的工作空间 Coding Space

前端开发实践简略版

一、工具与框架选型

工具库

  1. lodash-es:深拷贝 (cloneDeep)、防抖 (debounce)、节流 (throttle)、对象合并 (merge) 等高频工具函数,优先使用 ES Module 版本以支持 Tree Shaking。避免手动实现深拷贝等底层逻辑。
  2. dayjs:替代 Moment.js,体积仅 2KB,提供链式 API 和插件机制(相对时间、时区、本地化)。所有时间展示统一走 dayjs 格式化,禁止 new Date() 拼字符串。
  3. vueuse / react-use / ahooks:优先使用社区 Hooks 库解决窗口事件 (useEventListener)、倒计时 (useCountDown)、滚动 (useScroll)、防抖 (useDebounceFn)、全屏 (useFullscreen)、网络状态 (useNetwork) 等复用逻辑,减少手写 addEventListener 和手动清理。

组件库

  1. Vue 生态:Element Plus 为 Vue 3 默认首选,覆盖表单、表格、弹窗、步骤条等 90% 业务场景。特殊场景(树形穿梭框、复杂表单联动)可辅以 Ant Design Vue。
  2. React 生态:Ant Design 覆盖中后台核心场景。Radix UI + shadcn/ui 组合适用于需要高度自定义样式的场景(如面向 C 端的营销页面),提供无样式的无障碍基础组件。
  3. 组件库选型对照
维度 Element Plus Ant Design Ant Design Vue Radix UI + shadcn/ui
框架绑定 Vue 3 React Vue 3 框架无关
样式定制 CSS Variables + SCSS CSS-in-JS (token) Less Tailwind CSS
无障碍 部分支持 较完善 部分支持 原生 WCAG 支持
适用场景 中后台 Vue 项目 中后台 React 项目 Vue 项目偏好 Ant 风格 自定义设计系统 / C 端

状态管理

  1. Pinia(Vue):Vue 3 官方推荐,支持 Composition API 风格、TypeScript 类型推导、devtools 集成。每个 Store 文件只关注一个业务领域(useUserStoreuseCartStore),禁止将全局状态塞入单 Store。
  2. Zustand(React):极简状态管理,通过 create 创建 store,无需 Provider 包裹,支持中间件(immer、persist、devtools)。适合中大型 React 应用替代 Redux。
  3. TanStack Query(通用):服务端状态唯一正解,自动管理请求缓存、过期、重新获取、乐观更新。原则:UI 状态用 Pinia/Zustand,服务端数据用 TanStack Query,不要混为一谈。

HTTP 客户端

  1. Axios(推荐):拦截器机制统一处理 Token 注入、超时控制、响应统一封装(response.data 解构)、请求去重、全局错误处理。每个项目必须封装 Request 实例,禁止直接 axios.get() 裸调。
  2. ofetch:轻量替代方案,基于 Fetch API,体积更小,适合没有拦截器需求的简单场景。使用时需手动封装超时和错误处理逻辑。
特性 Axios ofetch
拦截器 支持(请求/响应) 不支持
超时控制 内置 需 AbortController
请求取消 CancelToken / AbortController AbortController
体积 ~14KB ~2KB

构建工具

  1. Vite(首选):开发模式基于 ESM 原生模块,冷启动 < 1s,HMR 热更新毫秒级。生产构建使用 Rollup,配置 build.rollupOptions 进行分包优化、CDN 外置。新项目统一 Vite,禁止再新建 Webpack 项目。
  2. Webpack:存量项目维护场景保留,遇到构建速度瓶颈时考虑渐进迁移至 Vite(通过 vite-plugin-webpack 桥接或从非核心模块逐步替换)。

测试框架

  1. 单元测试:Vite 项目使用 Vitest(零配置、兼容 Jest API、HMR 热更新测试)。React 结合 React Testing Library,Vue 结合 Vue Test Utils + @vue/test-utils。
  2. E2E 测试:Playwright 跨浏览器支持 Chromium / Firefox / WebKit,支持自动等待、截图对比、网络拦截、Mobile 模拟。核心流程(登录、下单、支付)必写 E2E 用例。

二、代码规范与工程化

  1. ESLint + Prettier:ESLint 负责代码质量(未使用变量、类型检查),Prettier 负责代码格式(缩进、分号、引号)。使用 eslint-config-prettier 关闭冲突规则。统一在 CI/CD 中执行 eslint --max-warnings 0 拦截问题代码。
  2. Husky + lint-stagedpre-commit 钩子只对暂存文件执行 lint 和格式化(lint-staged),确保提交的代码始终符合规范。commit-msg 钩子校验 Conventional Commits 格式(type(scope): description)。
  3. TypeScript 严格模式:开启 strict: true,禁止 any 逃生(使用 unknown + 类型守卫替代)。API 返回数据必须定义接口类型,禁止 as any 强制断言,给后端返回数据增加一层类型校验(详见防御式编程)。

三、HTTP 与通信实践

  1. Request 统一封装:每个项目只维护一个 Request 实例,拦截器处理:
// Axios 统一封装示例
const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE, timeout: 10000 })

// 请求拦截器:Token 注入
request.interceptors.request.use((config) => {
  const token = useAuthStore.getState().token
  if (token) config.headers.Authorization = `Bearer ${token}`
  return config
})

// 响应拦截器:统一解构 + 错误处理
request.interceptors.response.use(
  (res) => res.data,
  (error) => {
    if (error.response?.status === 401) {
      // 清除登录态,跳转登录页
      useAuthStore.getState().logout()
      window.location.href = '/login'
    }
    return Promise.reject(error)
  }
)
  1. Token 刷新机制:Access Token 过期时,通过响应拦截器自动调用 Refresh Token 接口刷新,并创建一个 Promise 队列,让后续并发请求复用同一个刷新 Promise,避免并发刷新问题(Token 穿刺/Stampede)。
let isRefreshing = false
let pendingQueue: Array<{ resolve: Function; reject: Function }> = []

request.interceptors.response.use(
  (res) => res,
  async (error) => {
    if (error.response?.status !== 401) return Promise.reject(error)
    const config = error.config
    if (config._retry) return Promise.reject(error) // 防止循环
    if (isRefreshing) {
      return new Promise((resolve, reject) => {
        pendingQueue.push({ resolve, reject })
      }).then((token) => {
        config.headers.Authorization = `Bearer ${token}`
        return request(config)
      })
    }
    config._retry = true
    isRefreshing = true
    try {
      const { accessToken } = await refreshTokenApi()
      useAuthStore.getState().setToken(accessToken)
      pendingQueue.forEach((p) => p.resolve(accessToken))
      pendingQueue = []
      config.headers.Authorization = `Bearer ${accessToken}`
      return request(config)
    } catch (e) {
      pendingQueue.forEach((p) => p.reject(e))
      pendingQueue = []
      useAuthStore.getState().logout()
      window.location.href = '/login'
      return Promise.reject(e)
    } finally {
      isRefreshing = false
    }
  }
)
  1. 请求去重与防抖:同一接口在短时间内被重复调用(如搜索框快速输入、Tab 快速切换),使用 Axios CancelToken 或 AbortController 取消前一次请求,或使用 debounce 延迟发起请求。

  2. 接口超时兜底:每个请求必须配置超时(建议默认 10s,文件上传 60s)。超时后展示友好的 loading/error 状态,而非白屏。对于非关键接口,超时可降级为展示兜底数据或容错 UI。

四、状态管理与数据流

  1. 服务端状态与 UI 状态分离:从 API 获取的数据使用 TanStack Query 管理,自动处理缓存、过期、重新获取;UI 状态(弹窗开关、当前 Tab、表单选中行)使用 Pinia / Zustand 管理。禁止将 API 数据同时存入 Pinia/Zustand 和 TanStack Query,造成双源不一致。

  2. 状态提升与下钻:多组件共享的状态提升到最近公共父组件或 Store 中;只在一个组件内部使用的状态保留在 ref / useState 中,不要随意放入全局 Store。记住:全局 Store 不是垃圾桶。

五、防御式编程(前端专属)

  1. 永远不要信任后端返回的数据:后端可能字段缺失、类型不对、返回 null 而非空数组。在消费 API 数据前必须做类型校验和空值处理。
// ❌ 错误:假设后端一定返回 user.list
const list = res.user.list.map(...)  // 如果 res.user 为 null,直接 Crash

// ✅ 正确:逐层防御
const list = res?.user?.list ?? []
const safeList = Array.isArray(list) ? list : []
  1. 组件 Props 默认值兜底:所有非必填 Props 必须提供默认值,数值型考虑 0 兜底,字符串考虑空字符串兜底,数组使用 () => [] 避免引用共享。
// Vue
defineProps<{
  data?: TableRow[]
  loading?: boolean
  emptyText?: string
}>()
withDefaults(defineProps<{
  data?: TableRow[]
  loading?: boolean
  emptyText?: string
}>(), {
  data: () => [],
  loading: false,
  emptyText: '暂无数据',
})

// React
interface Props {
  data?: TableRow[]
  loading?: boolean
  emptyText?: string
}
const Table = ({ data = [], loading = false, emptyText = '暂无数据' }: Props) => ...
  1. 使用可选链 (?.) 和空值合并 (??):对象深层属性读取使用 a?.b?.c ?? defaultValue,替代冗余的 if (a && a.b) 守卫。注意区分 ||(遇到空字符串会兜底)和 ??(仅在 null/undefined 时兜底)的差异。

  2. 图片加载失败兜底:所有 <img> 标签或 el-image / Image 组件必须配置 fallback 占位图或 error 状态 UI,防止裂图展示。

<!-- Vue -->
<el-image :src="url" :fallback="placeholderImg" />
<!-- React -->
<Image src={url} fallback={placeholderImg} />
  1. 接口失败兜底:所有异步请求必须有 .catchtry/catch 兜底,展示错误 UI 或 Toast 提示。从不信任请求一定成功。
// ❌ 错误:await 无兜底
const data = await api.getList()

// ✅ 正确:try/catch 兜底 + 用户可感知
try {
  const data = await api.getList()
  state.data = data
} catch {
  state.data = []
  ElMessage.error('加载失败,请重试') // Vue
  // message.error('加载失败,请重试') // React
}

六、用户体验与交互反馈

  1. 前端交互反馈原则(对标 Java 版第 34 条)

    • 看得见变化不提示:页面局部刷新、列表排序变更等用户可感知的变化,不需要弹 Toast。
    • 看不见/重要/异步必提示:数据后台提交(如导出报表)、后台异步刷新等用户感知不到的操作,必须有明确反馈(Loading + 成功/失败提示)。
    • 失败必提示,成功看感知:操作失败(保存失败、提交失败、网络断开)必须弹窗或 Toast 明确告知用户。操作成功如果用户能直接看到结果变化(如表单关闭后列表新增一行),则不需要额外提示;如果看不到结果(后台异步任务提交),则需要提示"提交成功"。
  2. 按钮防重提交:表单提交、订单创建等关键操作,提交后按钮立即变为 loading / disabled 状态,防止用户多次点击产生重复请求。

// Vue
const submitting = ref(false)
const handleSubmit = async () => {
  if (submitting.value) return
  submitting.value = true
  try { await submitApi() } finally { submitting.value = false }
}

// React
const [submitting, setSubmitting] = useState(false)
const handleSubmit = async () => {
  if (submitting) return
  setSubmitting(true)
  try { await submitApi() } finally { setSubmitting(false) }
}
  1. 加载状态覆盖:页面任何有数据获取的区块都必须有 loading 态(骨架屏优于 Spin),且 loading 态在异步操作完毕前不可移除。空数据态展示占位提示("暂无数据")。错误态允许用户点击重试。

七、性能优化

  1. 大列表虚拟滚动:展示 1000+ 条数据的列表/表格时,必须使用虚拟滚动(vue-virtual-scroller、react-window、element-plus 虚拟化表格 el-table-v2),避免 DOM 节点过多导致卡顿。
框架 特点
vue-virtual-scroller Vue 支持列表、网格、无限滚动
react-window React 轻量,支持固定/可变高度
@tanstack/virtual 框架无关 底层虚拟化库,可自行封装
el-table-v2 Vue (Element Plus) 内置虚拟化,开箱即用
  1. KeepAlive / 路由缓存:列表页跳转详情页返回后,应保持之前的滚动位置和搜索条件。Vue 使用 <KeepAlive> + onActivated;React 使用 useRef 保存状态 + sessionStorage 持久化滚动位置。

  2. 图片懒加载:长页面中的图片使用懒加载(v-lazy / lazyload 库或 IntersectionObserver),避免一次性加载大量图片资源阻塞渲染。

  3. 组件按需加载:路由级别使用 defineAsyncComponent / React.lazy() + Suspense 进行懒加载,降低首屏 JS 体积。Modal/Drawer 等弹窗组件内部的路由或大型表单也按需加载。

八、安全实践

  1. XSS 防御:用户输入展示在页面时必须转义。使用 v-html / dangerouslySetInnerHTML 之前,必须确保内容经过了 DOMPurify 或相似库净化。富文本编辑器的内容展示同样需要净化。

  2. CSRF 防御:前后端分离项目依赖 Token 验证(JWT 或 Session-Cookie)天然防 CSRF。如果使用 Cookie 做鉴权,确保设置 SameSite=StrictHttpOnlySecure 标记。

  3. 前端鉴权不可绕过永远不要认为"隐藏了按钮"就安全了。 按钮级别的权限控制在后端也必须验证。前端仅做 UI 层面的展示控制(无权限按钮隐藏 / 菜单过滤),真正的鉴权和越权检查必须依赖后端接口返回的数据。

九、监控与可观测性

  1. Sentry 集成:生产环境必须接入 Sentry,捕获前端 JavaScript 运行时错误、Promise 未处理异常、资源加载失败。配置 SourceMap 上传(仅内网或有权限的 CI 环境),方便定位源码位置。
import * as Sentry from '@sentry/vue'  // @sentry/react
Sentry.init({
  dsn: import.meta.env.VITE_SENTRY_DSN,
  environment: import.meta.env.VITE_APP_ENV,
  release: __APP_VERSION__,
  tracesSampleRate: 0.2, // 采样率 20%
})
  1. Web Vitals 监控:收集 LCP(最大内容绘制)、FID / INP(首次输入延迟/交互到下次绘制)、CLS(累计布局偏移)三大核心指标。通过 web-vitals 库上报到 Sentry 或自建监控系统,及时发现性能退化。

  2. 错误边界:React 使用 Error Boundary(componentDidCatch / getDerivedStateFromError),Vue 使用 onErrorCaptured 或全局 errorHandler,将崩溃范围控制在单个组件内,避免整个页面白屏。

// React Error Boundary
class ErrorBoundary extends React.Component<Props, { hasError: boolean }> {
  state = { hasError: false }
  static getDerivedStateFromError() { return { hasError: true } }
  componentDidCatch(error: Error) { Sentry.captureException(error) }
  render() {
    if (this.state.hasError) return <FallbackUI />
    return this.props.children
  }
}

// Vue 全局
app.config.errorHandler = (err, instance, info) => {
  Sentry.captureException(err)
  ElNotification.error({ title: '页面发生异常', message: '请联系管理员' })
}

十、CSS 实践

  1. 方案选型对照
方案 原理 适用场景 优缺点
UnoCSS 按需生成原子化 CSS 新项目、定制化设计 极小产物体积,高度灵活,学习曲线
Tailwind CSS JIT 引擎按需生成 中大型定制项目 生态丰富,但类名冗长
CSS Modules 编译期生成唯一类名 已存在项目过渡 隔离性好,无运行时开销
Scoped CSS Vue 组件 scope attribute Vue 项目默认方案 简单可靠,但权重略高
  1. 统一采用方案:Vue 新项目使用 UnoCSS + Scoped CSS 组合(UnoCSS 负责布局/间距/颜色等原子样式,Scoped CSS 负责组件独有样式)。CSS Modules 仅用于存量 React 项目改造过渡。

十一、部署与发布

  1. 构建产物输出:Vite 构建输出至 dist/,目录按 assets/(JS/CSS/字体)和页面入口 HTML 分离。启用 build.rollupOptions.output.manualChunks 分包,将第三方库(Element Plus、Ant Design、ECharts)拆为独立 chunk,利用 CDN 缓存长期不变内容。
// vite.config.ts
build: {
  rollupOptions: {
    output: {
      manualChunks: {
        'element-plus': ['element-plus'],
        'echarts': ['echarts'],
        'vendor': ['vue', 'vue-router', 'pinia'],
      },
    },
  },
}
  1. 环境变量管理:通过 .env.development / .env.production 管理不同环境配置,使用 import.meta.env.VITE_* 注入。禁止在代码中硬编码 API 域名或密钥。

  2. CI/CD 流水线要点

    • 构建前执行 npm audit 检查高危依赖
    • 并行执行 Lint + 单元测试 + TypeScript 类型检查
    • 构建产物上传 OSS/CDN,确保 hash 文件名长期缓存
    • 部署后通过 Playwright E2E 冒烟测试验证核心流程可用

十二、团队协作与日常工作

  1. Commit 规范:使用 Conventional Commits,格式 type(scope): description。当次提交只包含一个业务改动,不混入无关修改。

  2. Code Review 要点

    • 是否有使用 any 逃生?能否用 unknown + 类型守卫替代?
    • API 返回数据是否做了防御式校验?
    • 组件 Props 是否提供了默认值?
    • 异步请求是否有错误兜底?
    • 是否有状态管理滥用(API 数据存入全局 Store 而非 TanStack Query)?
    • 是否有性能隐患(大数据列表无虚拟滚动、图片无懒加载)?
  3. 代码注释原则自文档化优先 —— 好的命名胜过一段注释。只在以下三种情况写注释:

    • 业务逻辑晦涩(如复杂的状态机流转)
    • 存在性能考量或魔数(如超时时间设为 3870ms 有特殊原因)
    • 跨团队 API 的输入输出规范

本简略版提炼自前端开发实践系列文档(01-工程基础 ~ 05-专项实践),对标 JAVA 开发实践/z-开发实践简略版.md 风格。 每个实践点的详细原理、方案对比表格、完整代码示例和踩坑记录,请参考对应章节的正文文档。