前端开发实践简略版
一、工具与框架选型
工具库
- lodash-es:深拷贝 (
cloneDeep)、防抖 (debounce)、节流 (throttle)、对象合并 (merge) 等高频工具函数,优先使用 ES Module 版本以支持 Tree Shaking。避免手动实现深拷贝等底层逻辑。 - dayjs:替代 Moment.js,体积仅 2KB,提供链式 API 和插件机制(相对时间、时区、本地化)。所有时间展示统一走 dayjs 格式化,禁止
new Date()拼字符串。 - vueuse / react-use / ahooks:优先使用社区 Hooks 库解决窗口事件 (
useEventListener)、倒计时 (useCountDown)、滚动 (useScroll)、防抖 (useDebounceFn)、全屏 (useFullscreen)、网络状态 (useNetwork) 等复用逻辑,减少手写addEventListener和手动清理。
组件库
- Vue 生态:Element Plus 为 Vue 3 默认首选,覆盖表单、表格、弹窗、步骤条等 90% 业务场景。特殊场景(树形穿梭框、复杂表单联动)可辅以 Ant Design Vue。
- React 生态:Ant Design 覆盖中后台核心场景。Radix UI + shadcn/ui 组合适用于需要高度自定义样式的场景(如面向 C 端的营销页面),提供无样式的无障碍基础组件。
- 组件库选型对照:
| 维度 | 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 端 |
状态管理
- Pinia(Vue):Vue 3 官方推荐,支持 Composition API 风格、TypeScript 类型推导、devtools 集成。每个 Store 文件只关注一个业务领域(
useUserStore、useCartStore),禁止将全局状态塞入单 Store。 - Zustand(React):极简状态管理,通过
create创建 store,无需 Provider 包裹,支持中间件(immer、persist、devtools)。适合中大型 React 应用替代 Redux。 - TanStack Query(通用):服务端状态唯一正解,自动管理请求缓存、过期、重新获取、乐观更新。原则:UI 状态用 Pinia/Zustand,服务端数据用 TanStack Query,不要混为一谈。
HTTP 客户端
- Axios(推荐):拦截器机制统一处理 Token 注入、超时控制、响应统一封装(
response.data解构)、请求去重、全局错误处理。每个项目必须封装 Request 实例,禁止直接axios.get()裸调。 - ofetch:轻量替代方案,基于 Fetch API,体积更小,适合没有拦截器需求的简单场景。使用时需手动封装超时和错误处理逻辑。
| 特性 | Axios | ofetch |
|---|---|---|
| 拦截器 | 支持(请求/响应) | 不支持 |
| 超时控制 | 内置 | 需 AbortController |
| 请求取消 | CancelToken / AbortController | AbortController |
| 体积 | ~14KB | ~2KB |
构建工具
- Vite(首选):开发模式基于 ESM 原生模块,冷启动 < 1s,HMR 热更新毫秒级。生产构建使用 Rollup,配置
build.rollupOptions进行分包优化、CDN 外置。新项目统一 Vite,禁止再新建 Webpack 项目。 - Webpack:存量项目维护场景保留,遇到构建速度瓶颈时考虑渐进迁移至 Vite(通过
vite-plugin-webpack桥接或从非核心模块逐步替换)。
测试框架
- 单元测试:Vite 项目使用 Vitest(零配置、兼容 Jest API、HMR 热更新测试)。React 结合 React Testing Library,Vue 结合 Vue Test Utils + @vue/test-utils。
- E2E 测试:Playwright 跨浏览器支持 Chromium / Firefox / WebKit,支持自动等待、截图对比、网络拦截、Mobile 模拟。核心流程(登录、下单、支付)必写 E2E 用例。
二、代码规范与工程化
- ESLint + Prettier:ESLint 负责代码质量(未使用变量、类型检查),Prettier 负责代码格式(缩进、分号、引号)。使用
eslint-config-prettier关闭冲突规则。统一在 CI/CD 中执行eslint --max-warnings 0拦截问题代码。 - Husky + lint-staged:
pre-commit钩子只对暂存文件执行 lint 和格式化(lint-staged),确保提交的代码始终符合规范。commit-msg钩子校验 Conventional Commits 格式(type(scope): description)。 - TypeScript 严格模式:开启
strict: true,禁止any逃生(使用unknown+ 类型守卫替代)。API 返回数据必须定义接口类型,禁止as any强制断言,给后端返回数据增加一层类型校验(详见防御式编程)。
三、HTTP 与通信实践
- 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)
}
)
- 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
}
}
)
-
请求去重与防抖:同一接口在短时间内被重复调用(如搜索框快速输入、Tab 快速切换),使用 Axios CancelToken 或 AbortController 取消前一次请求,或使用
debounce延迟发起请求。 -
接口超时兜底:每个请求必须配置超时(建议默认 10s,文件上传 60s)。超时后展示友好的 loading/error 状态,而非白屏。对于非关键接口,超时可降级为展示兜底数据或容错 UI。
四、状态管理与数据流
-
服务端状态与 UI 状态分离:从 API 获取的数据使用 TanStack Query 管理,自动处理缓存、过期、重新获取;UI 状态(弹窗开关、当前 Tab、表单选中行)使用 Pinia / Zustand 管理。禁止将 API 数据同时存入 Pinia/Zustand 和 TanStack Query,造成双源不一致。
-
状态提升与下钻:多组件共享的状态提升到最近公共父组件或 Store 中;只在一个组件内部使用的状态保留在
ref/useState中,不要随意放入全局 Store。记住:全局 Store 不是垃圾桶。
五、防御式编程(前端专属)
- 永远不要信任后端返回的数据:后端可能字段缺失、类型不对、返回
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 : []
- 组件 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) => ...
-
使用可选链 (
?.) 和空值合并 (??):对象深层属性读取使用a?.b?.c ?? defaultValue,替代冗余的if (a && a.b)守卫。注意区分||(遇到空字符串会兜底)和??(仅在 null/undefined 时兜底)的差异。 -
图片加载失败兜底:所有
<img>标签或el-image/Image组件必须配置 fallback 占位图或 error 状态 UI,防止裂图展示。
<!-- Vue -->
<el-image :src="url" :fallback="placeholderImg" />
<!-- React -->
<Image src={url} fallback={placeholderImg} />
- 接口失败兜底:所有异步请求必须有
.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
}
六、用户体验与交互反馈
-
前端交互反馈原则(对标 Java 版第 34 条):
- 看得见变化不提示:页面局部刷新、列表排序变更等用户可感知的变化,不需要弹 Toast。
- 看不见/重要/异步必提示:数据后台提交(如导出报表)、后台异步刷新等用户感知不到的操作,必须有明确反馈(Loading + 成功/失败提示)。
- 失败必提示,成功看感知:操作失败(保存失败、提交失败、网络断开)必须弹窗或 Toast 明确告知用户。操作成功如果用户能直接看到结果变化(如表单关闭后列表新增一行),则不需要额外提示;如果看不到结果(后台异步任务提交),则需要提示"提交成功"。
-
按钮防重提交:表单提交、订单创建等关键操作,提交后按钮立即变为
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) }
}
- 加载状态覆盖:页面任何有数据获取的区块都必须有 loading 态(骨架屏优于 Spin),且 loading 态在异步操作完毕前不可移除。空数据态展示占位提示("暂无数据")。错误态允许用户点击重试。
七、性能优化
- 大列表虚拟滚动:展示 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) | 内置虚拟化,开箱即用 |
-
KeepAlive / 路由缓存:列表页跳转详情页返回后,应保持之前的滚动位置和搜索条件。Vue 使用
<KeepAlive>+onActivated;React 使用useRef保存状态 +sessionStorage持久化滚动位置。 -
图片懒加载:长页面中的图片使用懒加载(
v-lazy/lazyload库或IntersectionObserver),避免一次性加载大量图片资源阻塞渲染。 -
组件按需加载:路由级别使用
defineAsyncComponent/React.lazy()+Suspense进行懒加载,降低首屏 JS 体积。Modal/Drawer 等弹窗组件内部的路由或大型表单也按需加载。
八、安全实践
-
XSS 防御:用户输入展示在页面时必须转义。使用
v-html/dangerouslySetInnerHTML之前,必须确保内容经过了 DOMPurify 或相似库净化。富文本编辑器的内容展示同样需要净化。 -
CSRF 防御:前后端分离项目依赖 Token 验证(JWT 或 Session-Cookie)天然防 CSRF。如果使用 Cookie 做鉴权,确保设置
SameSite=Strict、HttpOnly、Secure标记。 -
前端鉴权不可绕过:永远不要认为"隐藏了按钮"就安全了。 按钮级别的权限控制在后端也必须验证。前端仅做 UI 层面的展示控制(无权限按钮隐藏 / 菜单过滤),真正的鉴权和越权检查必须依赖后端接口返回的数据。
九、监控与可观测性
- 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%
})
-
Web Vitals 监控:收集 LCP(最大内容绘制)、FID / INP(首次输入延迟/交互到下次绘制)、CLS(累计布局偏移)三大核心指标。通过
web-vitals库上报到 Sentry 或自建监控系统,及时发现性能退化。 -
错误边界: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 实践
- 方案选型对照:
| 方案 | 原理 | 适用场景 | 优缺点 |
|---|---|---|---|
| UnoCSS | 按需生成原子化 CSS | 新项目、定制化设计 | 极小产物体积,高度灵活,学习曲线 |
| Tailwind CSS | JIT 引擎按需生成 | 中大型定制项目 | 生态丰富,但类名冗长 |
| CSS Modules | 编译期生成唯一类名 | 已存在项目过渡 | 隔离性好,无运行时开销 |
| Scoped CSS | Vue 组件 scope attribute | Vue 项目默认方案 | 简单可靠,但权重略高 |
- 统一采用方案:Vue 新项目使用 UnoCSS + Scoped CSS 组合(UnoCSS 负责布局/间距/颜色等原子样式,Scoped CSS 负责组件独有样式)。CSS Modules 仅用于存量 React 项目改造过渡。
十一、部署与发布
- 构建产物输出: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'],
},
},
},
}
-
环境变量管理:通过
.env.development/.env.production管理不同环境配置,使用import.meta.env.VITE_*注入。禁止在代码中硬编码 API 域名或密钥。 -
CI/CD 流水线要点:
- 构建前执行
npm audit检查高危依赖 - 并行执行 Lint + 单元测试 + TypeScript 类型检查
- 构建产物上传 OSS/CDN,确保 hash 文件名长期缓存
- 部署后通过 Playwright E2E 冒烟测试验证核心流程可用
- 构建前执行
十二、团队协作与日常工作
-
Commit 规范:使用 Conventional Commits,格式
type(scope): description。当次提交只包含一个业务改动,不混入无关修改。 -
Code Review 要点:
- 是否有使用
any逃生?能否用unknown+ 类型守卫替代? - API 返回数据是否做了防御式校验?
- 组件 Props 是否提供了默认值?
- 异步请求是否有错误兜底?
- 是否有状态管理滥用(API 数据存入全局 Store 而非 TanStack Query)?
- 是否有性能隐患(大数据列表无虚拟滚动、图片无懒加载)?
- 是否有使用
-
代码注释原则:自文档化优先 —— 好的命名胜过一段注释。只在以下三种情况写注释:
- 业务逻辑晦涩(如复杂的状态机流转)
- 存在性能考量或魔数(如超时时间设为 3870ms 有特殊原因)
- 跨团队 API 的输入输出规范
本简略版提炼自前端开发实践系列文档(01-工程基础 ~ 05-专项实践),对标 JAVA 开发实践/z-开发实践简略版.md 风格。 每个实践点的详细原理、方案对比表格、完整代码示例和踩坑记录,请参考对应章节的正文文档。