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 文件只关注一个业务领域(useUserStore、useCartStore),禁止将全局状态塞入单 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-staged:pre-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. 接口失败兜底:所有异步请求必须有 .catch 或 try/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=Strict、HttpOnly、Secure 标记。

  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 风格。 每个实践点的详细原理、方案对比表格、完整代码示例和踩坑记录,请参考对应章节的正文文档。