跳转到内容

前端工程与应用设计

前端工程的核心问题不是“选哪个框架”,而是怎样把界面、状态、数据和用户操作组织成可维护的系统。页面从浏览器渲染模型开始,往上叠加组件、路由、请求、缓存、构建和部署。框架会变化,分层和边界不会。

1. 框架到底解决了什么

跳转到“1. 框架到底解决了什么”

原生 DOM 当然可以写应用,但当状态越来越多时,手动同步数据和界面容易失控:

用户操作 → 修改数据 → 更新 DOM
↑ │
└── 其他操作 ┘

同一份数据可能在多个位置显示,网络返回和本地操作又可能交错。框架主要提供三件事:

  1. 声明式渲染:描述“界面应该是什么样”,而不是逐步修改 DOM。
  2. 组件化:把结构、样式和行为封装成有边界的单元。
  3. 状态到视图的同步:状态变化后,以可控方式重新计算和渲染。

React、Vue、Svelte、Solid 等实现不同,但都要处理组件、属性、状态、生命周期、事件和渲染调度。

组件可以理解为接收输入并返回界面描述的函数:

type ButtonProps = {
label: string;
disabled?: boolean;
onClick: () => void;
};
function Button({ label, disabled = false, onClick }: ButtonProps) {
return (
<button disabled={disabled} onClick={onClick}>
{label}
</button>
);
}

**属性(props)**是从父级传入的输入,**状态(state)**是组件内部会变化的数据。两者边界不清时,组件会出现大量同步逻辑。

好的组件边界通常满足:

  • 输入和输出清楚。
  • 不直接依赖外部环境,或把依赖通过参数传入。
  • 只负责一个可解释的界面职责。
  • 可单独测试,不要求启动整个应用。
  • 样式和可访问性在同一层解决,不把行为留给调用者猜测。

组件不是越小越好。把一个按钮拆成七个文件,如果它们总是一起变化,反而增加跳转成本。拆分依据是职责、复用和变化频率,不是行数。

3. 渲染模型:从声明到屏幕

跳转到“3. 渲染模型:从声明到屏幕”

不同框架的更新方式不同:

  • 虚拟 DOM先构造轻量的界面树,再与上一棵树比较,最后应用差异。
  • 细粒度响应式跟踪哪些状态被哪些视图读取,状态变化时只更新相关部分。
  • 编译式更新在构建阶段分析模板,把更新逻辑编译成更直接的代码。

这些方式都试图减少无效工作。真正影响性能的是:更新频率、每次更新涉及多少组件、是否触发同步布局,以及状态是否放置得过高层。

表单输入可以由应用状态控制,也可以由 DOM 自己维护。受控输入便于校验和联动,但每次输入都可能触发渲染;非受控输入更接近原生表单,适合简单或高性能场景。选择取决于是否需要即时同步,而不是框架偏好。

3.2 服务端渲染与客户端渲染

跳转到“3.2 服务端渲染与客户端渲染”
模式 HTML 从哪来 优势 注意
CSR 浏览器执行脚本生成 交互丰富、部署简单 首屏依赖 JS,SEO 和弱网体验可能差
SSR 服务端按请求生成 首屏内容更快可读 服务端负载、流式渲染、状态同步
SSG 构建时生成静态页面 便宜、稳定、适合内容站 内容更新需要重新构建
混合渲染 页面或组件分别选择 兼顾静态与动态 缓存、边界和调试更复杂

SSR 返回 HTML 后,客户端还需要“水合”(hydration),把事件和状态接上。水合不匹配可能导致闪烁、重复渲染或事件异常。不要默认 SSR 一定更快;它增加的是服务端与缓存复杂度,换来首屏和可索引性。

路由把 URL 映射到页面和状态。单页应用常用 History API 修改地址,不重新加载整个文档;服务端路由则由服务器根据路径返回不同响应。

路由设计要回答:

  • 哪些状态应放进 URL,能通过刷新、分享和后退恢复?
  • 哪些页面需要登录、权限或数据预加载?
  • 页面切换时哪些请求应取消,哪些缓存可复用?
  • 404、空状态和错误状态分别怎样展示?
  • 深层链接和浏览器前进后退是否正常工作?
/orders 订单列表
/orders/:id 订单详情
/orders/:id?tab=items 详情中的页签

筛选条件、分页和页签通常适合放进 URL;临时弹窗和输入焦点未必需要。嵌套路由能表达布局复用,但不要把整个应用嵌套成几十层不可理解的树。

按路由或大型功能拆分代码,可以减少首屏下载。懒加载不是免费午餐:用户第一次进入页面时会额外等待,预加载策略、加载占位和失败重试都要设计。

5. 状态管理:先分类,再选工具

跳转到“5. 状态管理:先分类,再选工具”

“全局状态”常被滥用。实际状态可以分成几类:

状态类型 例子 适合放在哪里
局部 UI 状态 弹窗开关、输入值 组件内部
跨组件 UI 状态 主题、侧边栏、选中项 局部共享上下文或小型 store
URL 状态 页面、筛选、分页、页签 路由
服务端数据 用户、订单、列表 请求层缓存与 query 管理
表单状态 字段值、错误、脏状态 表单库或组件内
派生状态 总价、过滤结果、是否为空 根据源状态计算,不重复存储

最危险的是把同一事实存多份:例如用户资料同时存在全局 store、局部 state 和 URL 中,更新时只改了一处。优先确定单一事实来源,再决定缓存和派生。

5.1 服务端状态不是全局变量

跳转到“5.1 服务端状态不是全局变量”

服务端数据会过期、可能被其他客户端修改,也需要分页、重试和失效策略。它与“弹窗是否打开”不是同类问题。请求层通常需要:

  • 请求去重与缓存键。
  • 加载、成功、空数据和错误状态。
  • 过期时间与重新获取。
  • 用户操作后的局部失效。
  • 取消、超时和竞态处理。

两个请求乱序返回时,旧请求可能覆盖新结果。用请求 ID、取消信号或版本比较保证只有当前结果能更新界面。

不要只按“components / services / utils”切分所有代码。随着项目变大,一个业务功能会同时包含页面、组件、请求、类型和测试。更耐用的结构是按领域或功能聚合:

src/
app/
router.ts
providers.tsx
features/
orders/
api.ts
model.ts
OrderList.tsx
OrderDetail.tsx
OrderList.test.tsx
users/
...
shared/
ui/
lib/
config/

原则:

  • 业务功能内部可以高内聚,对外暴露少量入口。
  • shared 只放真正跨功能复用的代码,避免变成杂物间。
  • API 类型、校验和错误结构靠近调用方。
  • 页面负责组合,不负责所有业务细节。
  • 依赖方向尽量单向,低层工具不要反过来导入页面。

7. 构建工具:转译、打包与产物

跳转到“7. 构建工具:转译、打包与产物”

浏览器只需要能执行的 HTML、CSS、JavaScript 和资源文件。现代前端源码可能包含 TypeScript、JSX、CSS Modules、图片导入和新语法,因此需要构建链。

阶段 做什么
转译 TypeScript/JSX/新语法变成目标浏览器能执行的代码
解析依赖 找出模块之间谁依赖谁
打包/拆包 合并或拆分产物,控制加载体积
资源处理 图片、字体、CSS、哈希文件名
优化 压缩、Tree Shaking、代码分割
生成产物 输出可直接部署的文件和 manifest

Vite 在开发时利用浏览器原生模块和快速转换,生产构建通常交给更完整的打包流程;Webpack 的 loader 负责转换文件,plugin 负责扩展构建生命周期。工具不同,但都要回答同样的问题:入口是什么,输出在哪里,环境变量怎样注入,缓存怎样失效。

构建时注入到前端的变量会进入浏览器产物,任何人都能看到。API 密钥、数据库密码和服务端令牌不能放在前端环境变量里。公开配置可以注入,秘密必须留在服务端或受控代理。

Source Map 把压缩后的代码映射回源码,方便调试。公开生产 Source Map 可能泄露源码结构和内部路径;可以只上传到错误监控服务,或限制访问。

常见指标:

  • LCP(Largest Contentful Paint):最大内容元素何时可见,反映加载体验。
  • INP(Interaction to Next Paint):交互后界面多久响应。
  • CLS(Cumulative Layout Shift):页面内容是否突然跳动。

优化按阶段拆:

阶段 问题 常用手段
加载 资源太多、太慢、阻塞渲染 压缩、拆包、懒加载、缓存、CDN
渲染 DOM 太大、布局复杂、图片未定尺寸 虚拟列表、占位尺寸、减少嵌套
交互 主线程长任务、重复计算 拆分任务、Worker、缓存派生值
动画 每帧触发布局或重绘 transform/opacity、requestAnimationFrame

图片要设置宽高或比例,避免加载后推倒布局;字体要考虑 font-display、子集化和布局差异。性能指标要分设备、网络和地区观察,不能只测开发机。

上千条记录全部渲染成 DOM 会消耗内存和布局时间。虚拟列表只渲染视口附近的项目,但会增加滚动、可变高度和焦点管理的复杂度。数据量不大时,普通列表更简单可靠。

方式 方向 适合
轮询 客户端定时请求 简单、低频、容忍延迟
长轮询 服务端保持请求直到有数据 兼容性好,但连接管理复杂
SSE 服务端单向推送 通知、进度、流式文本
WebSocket 双向长连接 聊天、协作、游戏
WebRTC 点对点音视频和数据 实时媒体,需要信令与网络协商

实时连接必须处理断线重连、重复消息、顺序、心跳、认证过期和页面隐藏。SSE/WebSocket 只负责传输,业务仍需消息 ID、幂等和恢复策略。

流式结果要能区分“部分成功”和“最终成功”。用户关闭页面或取消操作时,客户端应发送取消信号,服务端也应停止无意义的后台工作。

可访问性不是额外功能,而是正确使用 Web 平台:

  • 用语义元素表达标题、按钮、链接和表单。
  • 所有交互都能用键盘完成,并有清楚的焦点样式。
  • 图片有合适的替代文本,装饰图可标记为空。
  • 表单错误同时提供文字说明,不只依赖颜色。
  • 弹窗打开后管理焦点,关闭后把焦点还给触发元素。
  • 动态内容更新时,用合适的 live region 告诉辅助技术。

ARIA 可以在原生语义不足时补充信息,但“加了很多 aria-*”不等于可访问。错误的 ARIA 比没有更糟。

国际化包括翻译、日期/数字格式、复数规则、文字方向、长度变化和时区。使用 Intl API 处理本地化格式,不要手写所有语言规则。布局要允许德语长词、阿拉伯语从右到左和中文无空格断行。

  • 不把未转义的用户内容插入 innerHTML。
  • 对 URL、重定向和第三方脚本做白名单限制。
  • 敏感操作在服务端鉴权,前端隐藏按钮不算权限。
  • Cookie 根据场景设置 HttpOnly、Secure 和 SameSite。
  • 使用 CSP、依赖审计和最小权限降低 XSS 与供应链风险。
  • 第三方组件和统计脚本可能读取页面数据,必须评估来源与必要性。

前端测试大致分三层:

  • 单元测试:格式化、计算、状态转换、校验函数。
  • 组件/集成测试:用户操作、表单、请求 mock、错误状态。
  • 端到端测试:真实浏览器中走关键路径、登录、支付、权限。

不要只断言 DOM class 名,用户能看到的行为更值得测试。视觉回归可以发现布局问题,但截图差异噪声较大,应选择稳定页面和关键断点。

上线前确认:

  1. 路由刷新和 404 回退是否适配部署平台。
  2. 静态资源是否使用正确 base path 和缓存头。
  3. 环境变量是否只包含公开值。
  4. 错误监控能否定位到版本和 Source Map。
  5. 首屏、弱网、移动端和键盘操作是否验证。
  6. 旧版本缓存的 Service Worker 是否能安全更新。
  • 组件输入输出是否清楚,是否避免重复状态。
  • 路由是否覆盖刷新、深链、权限和错误页。
  • 服务端数据是否有缓存、失效、取消和竞态处理。
  • 项目结构是否按变化边界组织,而不是只有技术层目录。
  • 构建产物是否可复现,前端环境变量是否全部视为公开。
  • 性能是否由真实指标驱动,是否检查了主线程和布局。
  • 实时连接是否处理断线、重复、顺序和取消。
  • 语义、键盘、焦点、对比度和替代文本是否检查。
  • 安全边界是否在服务端,而不是只靠前端隐藏。
  • 是否测试了正常、空、加载、错误和权限不足状态。