前端开发工具全景
涵盖语言、框架、构建工具、运行时、生态库的完整前端开发图谱 · 来源: 各项目官方文档 · GitHub
1. 前端开发生态全景图
前端开发涉及多层技术栈,从底层语言到上层框架、从开发工具到生产部署,110+ 工具/技术、24 大分类覆盖。图 1-1 以分类地图形式(按用途归类、无依赖连线)呈现生态全貌;后三张全景图分别从开发管线、技术分层、浏览器运行时给出流程与架构视角。除语言 / CSS / 动效 / 构建 / 框架 / 组件库 / 路由 / 状态 / 元框架 / 测试 / 代码质量 / 开发工具 / 运行时等经典分类外,还覆盖数据获取、数据校验、表单、组件文档、微前端、Monorepo、跨端桌面、Git 工作流、Mock、性能调试等第二梯队热门分类。
图 1-1: 前端开发技术生态全景(分类地图 · 2026)
1.1 生态补充分类 — 各分类流行工具速查
图 1-1 在原 14 大分类基础上,补充了 10 个第二梯队热门分类(含扩充现有分类的成员)。下表按分类列出补充工具的定位与适用场景。
| 分类 | 工具 | 是什么 / 解决什么问题 | 适用场景 |
|---|---|---|---|
| 数据获取 / 服务端状态 | TanStack Query · SWR · Apollo | 把「服务端数据」做成可缓存的客户端状态:自动缓存/失效/重试/乐观更新,替代手写 useEffect + fetch。 | 列表/详情需缓存与并发去重、轮询、无限滚动;GraphQL 项目用 Apollo。 |
| 数据校验 | Zod · Valibot · Yup | 运行时数据校验与 schema 定义,可与 TypeScript 类型联动(Zod 直接推导类型),表单提交前统一校验。 | 表单验证、API 响应/环境变量校验、前后端共享 schema。 |
| 表单 | React Hook Form · Formik · VeeValidate | 把「受控组件 + 手写 state」替换为表单状态管理:校验、提交、错误提示、字段联动。 | 复杂表单(多字段、条件字段、动态增删);React 用 RHF/Formik,Vue 用 VeeValidate。 |
| 文档 & 组件文档 | Storybook · VitePress · Docusaurus | Storybook 为组件库做可视化开发/文档/测试;VitePress 面向 Vue/Vite 快速建站;Docusaurus 面向 React 文档站。 | 团队组件库、开源库、技术博客与 API 文档站。 |
| 微前端 | qiankun · Module Federation · wujie | 把一个巨型应用拆成多个独立部署/独立技术栈的子应用,运行时组合成整体。 | 多团队并行、老系统渐进改造、中后台平台化。 |
| Monorepo | Turborepo · Nx | 多包仓库的编排工具:任务缓存、增量构建、依赖图、统一命令与版本管理。 | 多包/多应用同仓管理、共享代码库(如 pnpm workspace + Turborepo)。 |
| 跨端 / 桌面 | Electron · Tauri · Capacitor | 用 Web 技术做桌面应用(Electron 重、Tauri 轻)或移动端 App 壳(Capacitor 包 Web 到原生)。 | 桌面工具/客户端(VS Code、Notion 用 Electron);跨端 App。 |
| Git 工作流 | husky · commitlint · Changesets · semantic-release | 提交前钩子跑 lint/test(husky)、规范 commit 信息(commitlint)、版本号与 changelog 自动化(Changesets/semantic-release)。 | 团队 commit 规范、开源包版本发布、自动化发布流水线。 |
| Mock / 数据模拟 | MSW · json-server | 拦截网络请求返回模拟数据:MSW 在 Service Worker 层拦截(前后端同契约)、json-server 起一个假 REST API。 | 后端未就绪时前端先行开发、E2E 测试稳定化。 |
| 性能 & 调试 | Lighthouse · WebPageTest | 性能/SEO/无障碍审计:Lighthouse 内置于 Chrome DevTools;WebPageTest 多地域多设备实测。 | 上线前性能体检、Core Web Vitals 达标、竞品对比。 |
| CSS 生态补充 | UnoCSS · Emotion · vanilla-extract · Panda CSS | UnoCSS 为即时原子化引擎(Tailwind 的更快替代);Emotion 为 CSS-in-JS;vanilla-extract 编译期 CSS 模块;Panda CSS 类型安全原子类。 | 按团队偏好选型;追求性能/类型安全/可维护性时替换 Tailwind 或 styled-components。 |
| 状态管理补充 | Vuex · Recoil · Valtio | Vuex 为 Vue2 时代官方状态库(新项目用 Pinia);Recoil 为 React 原子状态;Valtio 基于 Proxy 的响应式状态。 | 维护存量 Vue2 项目(Vuex);React 细粒度状态(Recoil/Valtio)。 |
2. 开发管线全景
从编码到上线的完整前端开发管线:编码 → lint/格式化 → 版本控制 → CI/CD → 构建 → 部署 → 浏览器渲染。
图 2-1: 前端开发管线全景 — 从编码到上线
- SFC(Single File Component,单文件组件):Vue 的组件写法,一个
.vue文件同时包含<template>(HTML)、<script>(JS/TS)、<style>(CSS),不需要分离为三个文件。 - JSX(JavaScript XML):React 的语法扩展,在 JS 中直接写类似 HTML 的标签,编译后转为
createElement调用。 - TSX:TypeScript 版的 JSX,带类型检查。
- SSR(Server-Side Rendering,服务端渲染):在服务器把组件渲染为 HTML 字符串再下发,首屏更快、SEO 友好。
- SSG(Static Site Generation,静态站点生成):构建时预渲染所有页面为静态 HTML,无需服务器。
- ISR(Incremental Static Regeneration):Next.js 的概念,静态页面可按需增量重新生成。
- SPA(Single Page Application,单页应用):只有一个 HTML 页面,路由切换由 JS 在前端完成,不刷新浏览器。
- Tree Shaking(摇树优化):构建时删除未使用的代码,减小打包体积。
- lint-staged:只对 git 暂存区文件运行 lint/格式化,避免全量扫描。
- HMR(Hot Module Replacement,热模块替换):修改代码后浏览器局部更新,不丢失页面状态。
- 前端框架 vs 组件库 vs CSS 库:前端框架(Vue/React/Svelte)提供组件模型、响应式与渲染机制,本身不带现成 UI;组件库/UI 库(Element Plus/Ant Design/shadcn/ui)构建在某个前端框架之上,交付带结构、样式、行为的现成组件;CSS 库/方案(Tailwind/Sass/styled-components)只解决"怎么写样式",是组件库的底层依赖之一。
3. 技术分层全景
前端技术栈的六层架构:运行平台 → 语言/类型系统 → 构建工具 → 前端框架 & 状态管理 → 组件库 → 元框架 & 部署。补充说明新分类的归属:数据获取(TanStack Query/SWR/Apollo)、数据校验(Zod/Valibot/Yup)属于第 4 层的应用逻辑层;表单方案(React Hook Form/Formik/VeeValidate)属于第 5 层的 UI 方案层;Monorepo(Turborepo/Nx)属于第 3 层的工程化工具;微前端(qiankun/Module Federation)与桌面跨端(Electron/Tauri/Capacitor)属于第 6 层的交付形态。注意区分:前端框架(Vue/React)提供组件模型与响应式渲染,本身不带现成 UI;组件库(Element Plus/Ant Design)构建在框架之上,交付带样式、带行为的现成组件。
图 3-1: 前端技术分层全景 — 六层架构
组件库只是上层"成品",不是颜值上限。真正决定观感的是三层协作:
| 层级 | 代表 | 决定什么 | 权重 |
|---|---|---|---|
| 设计规范 | 色板 / 栅格 / 间距 / 圆角 / 字体层级 | 审美品位、视觉上限(源头) | 最高 |
| CSS 方案 | Tailwind / CSS Modules / CSS-in-JS | 样式如何组织、能否精细微调 | 高 |
| 组件库 / UI 库 | Element Plus / Ant Design / shadcn/ui | 默认好看的下限(及格线),提效 | 中 |
| 前端框架 | Vue / React / Svelte | 几乎不管颜值——只管逻辑与结构 | 低 |
一句话:组件库决定"最差能有多好看",CSS 方案决定"能不能精细掌控",设计规范决定"上限"。两个用同款 Ant Design 的团队,一个能做得精致、一个可以同样难看——差距在布局、色彩运用、内容密度与动效,这些组件库管不了。
案例演示:同款 Ant Design,颜值由谁决定?
三组演示都用同一份「卡片 + 按钮」组件代码,只改外层 CSS,直观看出每一层对观感的真实影响。
① 组件库只决定「下限」——直接堆叠 vs 设计规范精修
项目概览
今日访问 128,订单 32,转化 25%。
本周新增用户 45,较上周 +12%。
项目概览
<!-- ================= ① 同一份「卡片 + 按钮」组件代码 ================= -->
<!-- 组件库(Ant Design)只提供结构:div.card + button.btn,好看与否它不管 -->
<div class="antd-card">
<h3>项目概览</h3>
<p>今日访问 128,订单 32,转化 25%。</p>
<p>本周新增用户 45,较上周 +12%。</p>
<button class="antd-btn">查看详情</button>
<button class="antd-btn antd-btn-danger">导出报表</button>
</div>
<!-- ====== 左:直接堆叠(及格线)—— 组件库自带默认样式,仅此而已 ====== -->
.antd-card { padding: 0; } /* 无间距规范:文字贴边 */
h3 { margin: 0; } /* 无字体层级:标题与正文同样大 */
.antd-btn { background: #fff; } /* 无色彩规范:灰底白字直接怼 */
.antd-btn-danger { color: red; } /* 色彩乱用:红字突兀刺眼 */
<!-- ====== 右:设计规范精修(上限)—— 用间距/色板/层级覆盖组件库默认 ====== -->
.antd-card {
padding: 16px 20px; /* 间距规范:统一 8px 栅格 → 呼吸感 */
border-radius: 12px; /* 圆角规范:与全站一致 */
box-shadow: 0 4px 16px rgba(100,108,255,.12); /* 轻投影提质感 */
}
.antd-card h3 { font-size: 16px; font-weight: 600; color: #1a1a2e; } /* 字体层级 */
.antd-card p { font-size: 13px; color: #555; } /* 次级信息降权 */
.antd-btn { background: linear-gradient(135deg,#646cff,#bd34fe); color:#fff; border:none; }
.antd-btn-danger { color: #d1242f; } /* 危险色仅用于「危险操作」,不滥用 */
② CSS 方案决定「能不能精细掌控」——同样的设计稿,能否逐像素还原
响应式导航
- 首页
- 产品
- 定价
- 文档
响应式导航
- 首页
- 产品
- 定价
- 文档
md:flex gap-4 就能做到的响应式栅格,纯手写 CSS 也能做,但成本高、一致性差。CSS 方案决定精细掌控力。<!-- ============ ② 同样的设计稿:导航栏「hover 高亮 + 自适应」 ============ -->
<!-- 方案 A:纯手写 CSS —— 能做,但要自己管间距、断点、状态,且易写乱 -->
ul.nav { display: flex; }
ul.nav li { padding: 8px; }
@media (max-width: 600px) { ul.nav { flex-direction: column; } } /* 断点要自己写 */
ul.nav li:hover { background: #646cff; color: #fff; }
<!-- 方案 B:Tailwind —— 一条 class 全搞定,间距/断点/状态都是原子化的 -->
<!-- 对应上面的渲染效果:窄屏自动竖排、hover 高亮、间距 6px 精确可控 -->
<ul class="flex gap-1.5 md:flex-col">
<li class="flex-1 rounded-lg text-center py-2 bg-indigo-100 text-indigo-600
transition hover:bg-indigo-600 hover:text-white">首页</li>
...
</ul>
<!-- 同样用 CSS 方案(Tailwind / CSS Modules / CSS-in-JS),
差别只在「组织方式」:能否用变量统一间距、能否原子化复用、能否断点化 ——
这就是「高权重」的含义:方案决定你精细化调优的效率天花板。 -->
③ 前端框架「不管颜值」——换框架,观感零变化
<!-- ============ ③ 三种框架渲染同一个按钮,都不写 CSS ============ -->
<!-- Vue 3 写法 -->
<template><button>未加样式</button></template>
<!-- React 写法 -->
function Btn() { return <button>未加样式</button>; }
<!-- 原生 JS 写法 -->
document.body.innerHTML = '<button>未加样式</button>';
<!-- 结果:三者输出完全相同的原生 <button> 元素,浏览器统一套用默认外观。
框架只负责「渲染逻辑」,像素观感全归 CSS。要让按钮变好看,
得给 <button> 加 CSS(class/内联/样式表)—— 框架帮不上忙。 -->
4. 浏览器核心架构
浏览器是前端代码的终极运行环境。理解渲染引擎(Blink/WebKit/Gecko)、JS 引擎(V8/SpiderMonkey)、Event Loop 和 Web APIs 是深入前端开发的基础。
图 4-1: 浏览器核心架构 — 前端运行环境
HTML / CSS / JavaScript / TypeScript 前端核心技术体系
综合梳理:MDN Web Docs · WHATWG HTML Living Standard · W3C CSS WG · TC39 ECMA-262 · TypeScript
1. 概述 — 网页三层架构
一个网页由三门互补的语言构成,各司其职、分层解耦:
| 技术 | 职责 | 类比 | 语言类型 | 标准组织 |
|---|---|---|---|---|
| HTML (HyperText Markup Language) | 结构:定义内容和语义(标题、段落、图片、链接) | 房子的骨架 | 标记语言 | WHATWG(Living Standard,无版本号) |
| CSS (Cascading Style Sheets) | 表现:定义外观(颜色、字体、布局、动画) | 装修和外观 | 样式语言 | W3C CSS WG(模块化,各模块独立分级演进) |
| JavaScript | 行为:定义交互逻辑(响应点击、请求数据、修改页面) | 水电和电器 | 编程语言(唯一图灵完备) | Ecma TC39(ECMA-262,2015 起每年 6 月发一版) |
HTML 是宿主:CSS 通过
<link>/<style> 挂到 HTML 上,JS 通过 <script> 挂到 HTML 上。
浏览器把 HTML 解析成 DOM 树、把 CSS 解析成 CSSOM,JS 则通过 DOM/CSSOM API 在运行时读写这两棵树——这就是"动态网页"的本质。
① "HTML5" 只是历史名称,2019 年后唯一标准是 WHATWG 维护的 HTML Living Standard,持续滚动更新;
② CSS 没有"CSS4",CSS3 之后拆成独立模块(Selectors Level 4、Grid Level 2 等各自演进);
③ JavaScript 的标准名是 ECMAScript(ES2015/ES6 是分水岭,此后每年一版:ES2016 … ES2025+)。
第一部分:行为图(浏览器如何运作这三者)
2.1 时序图 — 页面加载全流程
从输入 URL 到页面可交互,三门语言依次登场:HTML 先到并开始解析,遇到 CSS 和 JS 再分别请求。注意两个关键阻塞点:同步 <script> 阻塞 HTML 解析;CSSOM 未就绪会阻塞 JS 执行(因为 JS 可能读样式)。
图 2.1-1: 页面加载时序图
专业术语
- DOM (Document Object Model)
- HTML 解析后生成的内存树结构,JS 通过它读写页面节点。浏览器解析 HTML 时逐步构建,遇同步 script 时暂停。
- CSSOM (CSS Object Model)
- CSS 解析后生成的对象模型,与 DOM 类似。CSSOM 未就绪时会阻塞 JS 执行(因为 JS 可能读取计算样式)。
- 阻塞解析 (Parser Blocking)
- 同步
<script>标签在下载和执行期间会暂停 HTML 解析,是页面加载性能的关键瓶颈。 - defer / async / module
- 三种异步加载策略:defer 并行下载、HTML 解析完后按顺序执行;async 下载完即执行、顺序不定;module 默认行为等同于 defer。
- DOMContentLoaded
- 在 HTML 解析完成、所有 defer 脚本执行完毕后触发,DOM 树已完整。比 load 事件早很多(load 需等所有图片等资源)。
- load 事件
- 页面所有资源(图片、CSS、JS、iframe 等)全部加载完毕后才触发,是最晚的页面事件。
案例演示:同步脚本为什么阻塞
<!DOCTYPE html>
<html>
<body>
<h1>页面加载时序演示</h1>
<!-- ① 同步脚本:HTML 解析暂停,等它下载 + 执行 -->
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
<p>这段 p 要等 lodash 加载执行完才能被解析到</p>
<!-- ② defer:并行下载,HTML 解析完后按序执行 -->
<script src="analytics.js" defer></script>
<!-- ③ async:下载完立即执行,不保证顺序 -->
<script src="widget.js" async></script>
<p>此 p 不受 defer/async 影响,正常解析</p>
</body>
</html>
// 测量关键时间点
window.addEventListener('DOMContentLoaded', () => {
console.log('DOM 树就绪(defer 脚本已执行)—', performance.now());
});
window.addEventListener('load', () => {
console.log('全部资源加载完毕 —', performance.now());
});
// DOMContentLoaded 通常远早于 load
| 脚本加载方式 | 下载时机 | 执行时机 | 是否阻塞解析 |
|---|---|---|---|
<script>(同步) | 解析到标签时 | 下载完立即执行 | 阻塞(下载+执行期间暂停解析) |
<script defer> | 并行下载 | HTML 解析完、DOMContentLoaded 前,按顺序 | 不阻塞 |
<script async> | 并行下载 | 下载完立即执行,顺序不定 | 执行时阻塞 |
<script type="module"> | 并行下载(含依赖图) | 默认 defer 行为 | 不阻塞 |
2.2 活动图 — 浏览器渲染流水线
浏览器把 HTML + CSS 变成屏幕像素的固定流水线:DOM → CSSOM → Render Tree → Layout → Paint → Composite。JS 修改页面后,会触发流水线的部分重跑——改几何属性触发 回流 Reflow(代价大),只改颜色等触发 重绘 Repaint,只改 transform/opacity 则仅需 合成 Composite(最便宜,动画首选)。
图 2.2-1: 渲染流水线活动图
专业术语
- Render Tree (渲染树)
- DOM 树与 CSSOM 合并后生成的树,只包含可见节点(排除
display:none、<head>等),是 Layout 的输入。 - Layout / Reflow (布局 / 回流)
- 计算每个可见元素的确切位置和尺寸。改
width、height、left、font-size等几何属性会触发回流,代价最大。 - Paint / Repaint (绘制 / 重绘)
- 将 Layout 结果转换为像素填充指令(颜色、阴影、文字等)。改
color、background等非几何属性仅触发重绘。 - Composite (合成)
- GPU 将多个层合并上屏的最终步骤。只改
transform和opacity时跳过后面的 Layout/Paint,仅触发合成,动画首选(60fps 无压力)。 - GPU 加速
- 将元素提升为独立的合成层(如
will-change: transform),使其动画在 GPU 上完成而不占用主线程。
案例演示:三种操作触发不同代价的渲染
// 准备:获取一个 div
const box = document.querySelector('.box');
// 【回流 Reflow】— 改了几何,代价最大
box.style.width = '300px'; // Layout → Paint → Composite
box.style.height = '200px';
box.style.left = '100px';
box.style.fontSize = '20px';
// 【重绘 Repaint】— 只改颜色,跳过 Layout
box.style.color = 'red';
box.style.background = '#eee';
box.style.boxShadow = '0 2px 8px rgba(0,0,0,0.3)';
// 【仅合成】— transform/opacity,GPU 处理,60fps 无压力
box.style.transform = 'translateX(100px) rotate(15deg)';
box.style.opacity = '0.8';
最佳实践:动画只用
transform + opacity,并加 will-change: transform 提升为合成层。// 坏 vs 好:动画对比
// 坏 — 每帧触发回流,卡顿
el.style.top = y + 'px'; el.style.left = x + 'px';
// 好 — 仅合成,丝滑 60fps
el.style.transform = `translate(${x}px, ${y}px)`;
2.3 活动图 — JS 事件循环 (Event Loop)
JS 是单线程语言,靠事件循环实现异步:调用栈清空后,先清空微任务队列(Promise.then、queueMicrotask),再看是否需要渲染,然后取一个宏任务(setTimeout、I/O、点击回调)循环往复。这是理解 async/await、页面卡顿、任务优先级的钥匙。
图 2.3-1: 事件循环活动图
专业术语
- Call Stack (调用栈)
- JS 引擎执行同步代码的后进先出栈。栈空时才从任务队列取任务执行。栈溢出(Stack Overflow)即调用栈过深。
- Macro Task (宏任务 / Task)
- 由宿主环境(浏览器/Node)发起的任务,如
setTimeout、setInterval、I/O、用户事件回调。每轮事件循环从宏任务队列取一个执行。 - Micro Task (微任务)
- 由 Promise 或
queueMicrotask产生的任务。每个宏任务执行完后、渲染之前,必须清空微任务队列。执行中新增的微任务也会在本轮被清空,可能饿死渲染。 - requestAnimationFrame (rAF)
- 浏览器在每帧渲染前调用的回调,执行时机在微任务之后、Layout 之前。适合做帧同步的动画逻辑,与屏幕刷新率(通常 60Hz ≈ 16.7ms/帧)对齐。
- 渲染时机
- 浏览器并非每轮事件循环都渲染,而是以约 16.7ms(60fps)为帧间隔。渲染发生在微任务清空之后、下一个宏任务之前。
案例演示:彻底搞懂宏/微任务顺序
// 打开控制台运行这段,观察输出顺序
console.log('== 开始 ==');
// 宏任务 1: script 本身(当前正在执行的就是第一个宏任务)
setTimeout(() => {
console.log('A setTimeout 0 — 宏任务');
Promise.resolve().then(() => console.log('B Promise 在 setTimeout 里 — 微任务'));
}, 0);
setTimeout(() => console.log('C setTimeout 0 第二个 — 下一个宏任务'), 0);
Promise.resolve().then(() => console.log('D Promise 1 — 当前宏任务的微任务'));
Promise.resolve().then(() => {
console.log('E Promise 2 — 当前宏任务的微任务');
queueMicrotask(() => console.log('F queueMicrotask 套娃 — 本微任务清空期内执行'));
});
requestAnimationFrame(() => console.log('G rAF — 渲染前回调'));
console.log('== 同步结束 ==');
开始 → 同步结束 → D → E → F → A → B → C → G(rAF 下一帧)规律:① 当前宏任务全部同步代码 → ② 微任务清空(含套娃) → ③ rAF 回调 → ④ 渲染 → ⑤ 下一个宏任务
// 经典面试题
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// 输出: 1 4 3 2 —— 微任务永远插队在下一个宏任务之前
2.4 时序图 — DOM 事件流(捕获 → 目标 → 冒泡)
用户点击一个按钮时,事件并不是直接派发给按钮:先从 window 自上而下捕获到目标,再自下而上冒泡回去。"事件委托"(把监听器挂在父元素上统一处理子元素事件)正是利用冒泡阶段。
图 2.4-1: 事件捕获与冒泡时序图
专业术语
- Capture Phase (捕获阶段)
- 事件从
window开始,沿 DOM 树自上而下传播到目标元素的阶段。addEventListener(type, fn, true)的第三个参数true在此阶段触发。 - Target Phase (目标阶段)
- 事件到达目标元素本身,触发绑定在目标上的监听器,不分捕获或冒泡。
- Bubble Phase (冒泡阶段)
- 事件从目标元素自下而上传播回
window。这是addEventListener的默认触发阶段,也是事件委托依赖的阶段。 - Event Delegation (事件委托)
- 将监听器挂在父元素上,利用事件冒泡统一处理所有子元素的事件。用
e.target判断实际触发者,避免为每个子元素单独绑定,性能好且支持动态添加的元素。 - stopPropagation
e.stopPropagation()阻止事件继续在 DOM 树中传播(不影响同一元素上的其他监听器);e.stopImmediatePropagation()更彻底,连同一元素的其他监听器也阻止。
案例演示:事件委托 & 捕获 vs 冒泡
<!-- HTML 结构 -->
<div id="outer">
<ul id="list">
<li>项目 1</li>
<li>项目 2</li>
<li>项目 3</li>
</ul>
<button id="add">添加项目</button>
</div>
<script>
// 事件委托:监听 ul,而不是每个 li
// 新增的 li 也会自动响应(因为事件冒泡到 ul)
document.getElementById('list').addEventListener('click', (e) => {
if (e.target.tagName === 'LI') {
console.log('点击了:', e.target.textContent, ' | 实际监听在:', e.currentTarget);
}
});
document.getElementById('add').addEventListener('click', () => {
const li = document.createElement('li');
li.textContent = '项目 ' + Date.now();
document.getElementById('list').appendChild(li); // 新 li 自动有 click 响应!
});
// 捕获 vs 冒泡对比
const outer = document.getElementById('outer');
outer.addEventListener('click', () => console.log('冒泡阶段触发 (默认)'));
outer.addEventListener('click', () => console.log('捕获阶段触发'), true); // true = 捕获
// 点击 li 时输出顺序:捕获阶段触发 → 冒泡阶段触发
</script>
场景:表格行点击、下拉菜单项、无限滚动列表等。
第二部分:结构图(三门语言各自由什么组成)
3.1 组件图 — 三者与浏览器引擎的关系
浏览器内部由多个引擎协作:渲染引擎(Blink/WebKit/Gecko)负责 HTML/CSS 解析与渲染,JS 引擎(V8/JavaScriptCore/SpiderMonkey)负责执行脚本。DOM(Document Object Model,文档对象模型)/ BOM(Browser Object Model,浏览器对象模型)API 不属于 JS 语言本身,而是浏览器提供的宿主能力——这也是 Node.js 里没有 document 的原因。
图 3.1-1: 浏览器引擎组件图
专业术语
- Rendering Engine (渲染引擎)
- 浏览器的核心子系统,负责 HTML 解析 (DOM)、CSS 解析 (CSSOM)、Layout、Paint 和 Composite。主流引擎:Blink (Chrome/Edge)、WebKit (Safari)、Gecko (Firefox)。
- JS Engine (JS 引擎)
- 独立的脚本执行环境,负责解析 JS 源码 → AST → 字节码 → 机器码的完整流程。主流引擎:V8 (Chrome/Node/Deno)、JavaScriptCore (Safari/Bun)、SpiderMonkey (Firefox)。
- Web API (宿主对象)
- 浏览器提供的 API,不属于 ECMAScript 语言规范。包括 DOM API (
document)、BOM API (window、fetch、setTimeout、localStorage) 等。Node.js 没有 DOM/BOM。 - Event Loop (事件循环)
- 协调 JS 单线程与异步操作的调度机制。负责管理调用栈、任务队列(宏/微任务)和渲染时机,使 JS 能在不阻塞主线程的情况下处理网络请求、定时器等异步操作。
- JIT (Just-In-Time Compilation)
- JS 引擎在运行时将热点代码编译为机器码的优化技术。V8 的 Ignition (解释器) + TurboFan (JIT 编译器) 是典型的两层编译架构。
案例演示:一次点击背后的各引擎协作
// 假设 HTML 中有 <button id="btn">点我</button>
// 步骤 1: JS 引擎 (V8) 执行这段脚本
const btn = document.querySelector('#btn'); // → 调用 DOM API
// 步骤 2: DOM API (Web API) 在渲染引擎中查找元素
// Blink 遍历 DOM 树找到 #btn,返回其引用
// 步骤 3: JS 通过 DOM API 挂载监听器
btn.addEventListener('click', async () => {
// 步骤 5: 用户点击 → Event Loop 调这个回调回 V8 执行
const res = await fetch('/api/data'); // → BOM API (fetch)
// 步骤 6: fetch 由网络线程处理,不阻塞 JS 主线程
// 请求完成后,Promise resolve → 微任务入队
// Event Loop 调度微任务 → V8 继续执行
const data = await res.json();
btn.textContent = data.label; // → 修改 DOM
// 步骤 7: 渲染引擎收到 DOM 变更 → 触发重绘/回流
// Blink: Layout → Paint → Composite → 屏幕更新
});
// 步骤 4: JS 执行完 → 调用栈清空 → 等待事件
// 步骤 5-7 循环发生在用户交互时
| 浏览器 | 渲染引擎 | JS 引擎 |
|---|---|---|
| Chrome / Edge / Opera | Blink | V8(Node.js/Deno 也用它) |
| Safari | WebKit | JavaScriptCore(Bun 用它) |
| Firefox | Gecko | SpiderMonkey |
3.2 HTML — 文档结构与 DOM 类图
3.2.1 文档骨架
<!DOCTYPE html> <!-- 声明标准模式,防止怪异模式 -->
<html lang="zh-CN">
<head> <!-- 元信息:不渲染 -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>页面标题</title>
<link rel="stylesheet" href="style.css"> <!-- 挂载 CSS -->
</head>
<body> <!-- 可见内容 -->
<h1>标题</h1>
<script src="app.js" defer></script> <!-- 挂载 JS -->
</body>
</html>
3.2.2 DOM 类图 — HTML 在内存中的运行时形态
HTML 文本被解析后,每个标签变成一个对象节点,全部继承自 Node;而 Node 又继承 EventTarget——所以任何节点都能 addEventListener。JS 操作页面,本质是操作这棵对象树。
图 3.2-1: DOM 节点类型类图
专业术语
- EventTarget
- DOM 的根类,提供事件监听能力(
addEventListener/removeEventListener/dispatchEvent)。window、XMLHttpRequest等非节点对象也继承它。 - Node
- DOM 树中所有节点的基类,提供父子兄弟关系操作(
parentNode、childNodes、appendChild、removeChild等)。 - Element
- 代表 HTML/SVG 标签元素,提供属性操作(
getAttribute、classList、innerHTML)。JS 操作页面时最常打交道的就是 Element。 - HTMLElement
- 所有 HTML 元素的基础类(
style、dataset、click())。每种具体标签(div、input、canvas)都有对应的子类,带有特定属性和方法。 - Document
- 代表整个 HTML 文档的根节点(
document),提供查询方法(querySelector、getElementById)和创建节点方法(createElement)。
案例演示:DOM 各级类的真实操作
// Document — 文档根节点
const doc = document;
console.log(doc.constructor.name); // "HTMLDocument" (继承 Document)
const body = doc.querySelector('body'); // Document 提供查询方法
const div = doc.createElement('div'); // Document 提供创建方法
// Element / HTMLElement — 标签元素
div.className = 'box'; // Element 属性
div.classList.add('active'); // Element 方法
div.style.background = '#eee'; // HTMLElement.style
div.dataset.id = '123'; // HTMLElement.dataset
div.innerHTML = '<span>hello</span>'; // Element.innerHTML
// Node — 所有节点的基类
body.appendChild(div); // Node.appendChild
console.log(div.parentNode === body); // Node.parentNode
console.log(div.childNodes.length); // Node.childNodes (含 Text 节点)
// EventTarget — 事件能力
div.addEventListener('click', (e) => {
console.log('事件目标:', e.target); // EventTarget 提供
console.log('节点类型:', e.target.nodeType); // 1=Element, 3=Text, 8=Comment
});
div (HTMLDivElement) → HTMLElement → Element → Node → EventTarget —— 所以一个 div 同时拥有事件能力、树操作、属性操作、样式操作。3.3 CSS — 选择器 / 盒模型 / 层叠
3.3.1 一条规则的解剖
/* 选择器 声明块 */
.card:hover { /* 类选择器 + 伪类 */
color: #333; /* 属性: 值; */
padding: 16px;
}
3.3.2 层叠 (Cascade) — CSS 的 "C"
多条规则命中同一元素时,按以下顺序裁决,这是 CSS 最核心的机制:
图 3.3-1: 层叠优先级判定活动图
专业术语
- Cascade (层叠)
- CSS 的 "C" 代表的核心机制:当多条规则命中同一元素时,按 "来源 → 特异性 → 书写顺序" 的优先级裁决最终生效的声明。
- Specificity (特异性 / 权重)
- 衡量选择器优先级的数值,记作 (ID数, 类数, 元素数)。按位比较,
(1,0,0) > (0,99,99)。行内 style 高于所有选择器,*通配符和继承值最低。 - !important
- 将单条声明的优先级提升到最高层级,覆盖一切(除非另一条也有
!important)。会逆转常规的层叠顺序,应尽量避免使用。 - 来源优先级 (Origin)
- CSS 声明的三层来源:用户代理样式(浏览器默认)< 用户样式 < 作者样式(开发者写的)。
!important存在时顺序反转 —— 用户的!important最高。 - 行内样式 (Inline Style)
- 直接写在 HTML 的
style=""属性中的声明,特异性高于所有外部/内部选择器。它是特异性计算中的一个独立层级。
案例演示:同元素多条规则,谁生效?
<!-- HTML -->
<div id="main" class="box content" style="color: purple;">
这段文字最终是什么颜色?
</div>
<!-- CSS -->
<style>
/* ① 来源: 作者样式(浏览器默认用户代理样式优先级最低,忽略) */
/* ② 特异性: (0,0,1) — 1 个元素选择器 */
div { color: red; }
/* ③ 特异性: (0,1,0) — 1 个类选择器 */
.box { color: green; }
.content { color: yellow; } // 与 .box 同特异性,后写覆盖 → yellow
/* ④ 特异性: (1,0,0) — 1 个 ID 选择器 */
#main { color: blue; }
/* ⑤ 行内 style 高于所有选择器 */
/* style="color: purple" → 这是最终生效的颜色! */
/* ⑥ !important 更高,但应避免 */
#main { color: orange !important; } // 加了 !important → orange 会覆盖 purple
</style>
(1,0,0) > (0,1,0) > (0,0,1) → ③ ID 选择器 blue 胜出,但④ 行内 style=purple 高于一切 → ⑤ 除非某条有 !important。最终生效:去掉行内样式 = blue;有行内样式 = purple;有 !important = orange。
| 特异性 (Specificity) | 示例 | 记作 (a,b,c) |
|---|---|---|
| ID 选择器 | #nav | (1,0,0) |
| 类 / 属性 / 伪类 | .btn [type=text] :hover | (0,1,0) |
| 元素 / 伪元素 | div ::before | (0,0,1) |
比较时按位比大小:(1,0,0) > (0,99,99)。行内 style="" 高于一切选择器,!important 再高一层(应避免使用)。
3.3.3 盒模型 (Box Model)
每个元素都是一个盒子:content + padding + border + margin。默认 width 只算 content(content-box),现代做法是全局改为 border-box 让 width 包含 padding 和 border,所见即所得:
*, *::before, *::after { box-sizing: border-box; } /* 现代 CSS 第一行 */
3.4 JS — 类型 / 原型链 / 模块化
3.4.1 类型系统
| 分类 | 类型 | 说明 |
|---|---|---|
| 原始类型(7 种,按值传递) | string number boolean undefined null symbol bigint | 不可变,存栈 |
| 对象类型(按引用传递) | Object(含 Array、Function、Date、Map、Set…) | 函数是一等公民,可赋值/传参/返回 |
3.4.2 原型链 — JS 面向对象的本质
JS 没有传统类继承,class 只是语法糖:每个对象有隐藏属性 [[Prototype]],读属性时沿链向上查找直到 null。这就是为什么随手创建的数组能调用 map()——方法在 Array.prototype 上。
图 3.4-1: 原型链类图
专业术语
- Prototype (原型)
- JS 中每个对象都有一个内部属性
[[Prototype]](可通过__proto__或Object.getPrototypeOf访问),指向它的原型对象。原型对象自身也可以有原型,形成链。 - Prototype Chain (原型链)
- JS 实现属性查找和继承的核心机制:读属性时先从对象自身查找,找不到就沿
[[Prototype]]链向上,直到null。这就是为什么所有数组都有map()、所有对象都有toString()。 - Constructor (构造函数)
- 带
prototype属性的函数对象(包括class)。当用new调用时,返回的新对象的[[Prototype]]会指向构造函数的prototype属性。 - Prototypal Inheritance (原型继承)
- JS 的继承模型(区别于 Java/C++ 的类继承):对象直接继承对象。ES6 的
class extends只是原型继承的语法糖,底层仍然是原型链。 - Object.prototype
- 几乎所有对象的原型链终点(再往上是
null)。它提供了toString()、hasOwnProperty()、valueOf()等共方法。
案例演示:亲手验证原型链查找
// 创建不同类型的数据,追踪 [[Prototype]] 链
const arr = [1, 2, 3];
const obj = { a: 1 };
const str = 'hello';
const fn = function() {};
const date = new Date();
// 验证原型链:所有对象最终都追溯到 Object.prototype
console.log(arr.__proto__ === Array.prototype); // true
console.log(arr.__proto__.__proto__ === Object.prototype); // true
console.log(arr.__proto__.__proto__.__proto__ === null); // true — 链终点
console.log(fn.__proto__ === Function.prototype); // true — 函数的直接原型
console.log(date.__proto__ === Date.prototype); // true
// 属性查找过程演示
arr.myMethod = '自身属性';
console.log(arr.myMethod); // '自身属性' — 第 1 步找到
console.log(arr.map); // ƒ map() — 自身没有,沿链找到 Array.prototype
console.log(arr.toString); // ƒ toString() — Array 没有,继续到 Object.prototype
console.log(arr.xyz); // undefined — 查遍全链都没有
arr.自身 → Array.prototype → Object.prototype → null,每一步找不到就继续向上。类比:原型链就像作用域链——变量先从当前作用域找,找不到往外层,直到全局。原型链就是 "属性的作用域链"。
// class 只是语法糖
class Dog {
constructor(name) { this.name = name; }
bark() { return `${this.name} says woof`; }
}
const d = new Dog('旺财');
console.log(d.__proto__ === Dog.prototype); // true — 和 function 原型一模一样
console.log(d.bark === Dog.prototype.bark); // true — 方法在 prototype 上,不在实例
console.log(Dog.prototype.__proto__ === Object.prototype); // true
3.4.3 模块化 — CJS vs ESM
ESM 是模块化语法规范(不是标准库):规定代码怎么拆文件、怎么 import/export、宿主怎么加载解析。它统一了浏览器与 Node 两个世界,取代了 Node 私有的 CommonJS:
| 维度 | CommonJS (CJS) | ES Modules (ESM) |
|---|---|---|
| 语法 | require() / module.exports | import / export |
| 标准 | Node.js 社区约定 | ECMA-262 语言标准(ES2015 引入) |
| 加载时机 | 运行时、同步、动态 | 编译期静态分析(支持 Tree Shaking),异步加载 |
| 浏览器支持 | 不支持(需打包器转换) | 原生支持 <script type="module"> |
| 值语义 | 导出值的拷贝 | 导出值的只读实时绑定 (live binding) |
| 现状 | Node 存量生态 | 唯一官方标准,Vite 等现代工具的基石 |
3.5 前端组件化演进
组件(Component)是前端 UI 领域的核心抽象:把一段可复用的 HTML + CSS + JS 封装成一个独立单元,对外只暴露 props(入参) 和 事件(出参)。本节梳理"组件"概念在前端的起源,并用具体代码演示三种主流实现方式。
3.5.1 历史时间线
| 时期 | 里程碑 | 意义 |
|---|---|---|
| 2011-2013 | Google 推动 Web Components 标准(Custom Elements、Shadow DOM、HTML Templates) | 浏览器原生组件化方案开始成形 |
| 2013 | React 开源发布 | 引入"组件 = 函数 + 状态"心智模型,声明式 UI 成为主流 |
| 2014 | Vue 1.x 发布 | 以 SFC(单文件组件)降低组件化门槛 |
| 2018+ | Web Components 获得所有主流浏览器原生支持 | 框架无关的跨平台组件标准成熟 |
3.5.2 三种实现方式 — 同一个计数器组件
方式一:Web Components 原生组件(Custom Elements + Shadow DOM)
<!-- HTML 中使用 -->
<my-counter initial="5"></my-counter>
<script>
class MyCounter extends HTMLElement {
connectedCallback() {
// 从属性读入参 (props)
this._count = Number(this.getAttribute('initial')) || 0;
// Shadow DOM:封装内部结构与样式,外部无法污染
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>button { font-size: 20px; } </style>
<button>-</button>
<span>${this._count}</span>
<button>+</button>`;
this.shadowRoot.querySelectorAll('button')[0]
.addEventListener('click', () => this.set(this._count - 1));
this.shadowRoot.querySelectorAll('button')[1]
.addEventListener('click', () => this.set(this._count + 1));
}
set(n) {
this._count = n;
this.shadowRoot.querySelector('span').textContent = n;
}
}
customElements.define('my-counter', MyCounter);
</script>
customElements.define() 注册新标签;② 组件名必须含连字符(my-counter)避免与原生标签冲突;③ attachShadow 实现样式与结构的真正封装(外部 CSS 无法穿透)。方式二:React 函数组件(组件 = 函数 + 状态)
// Counter.jsx
import { useState } from 'react';
export default function Counter({ initial = 0 }) { // props 入参
const [count, setCount] = useState(initial); // 内部状态
return (
<div className="counter">
<button onClick={() => setCount(c => c - 1)}>-</button>
<span>{count}</span>
<button onClick={() => setCount(c => c + 1)}>+</button>
</div>
);
}
// 使用:<Counter initial={5} />
props 是入参、useState 是内部状态;状态一变,函数重新执行、视图自动更新(声明式)。方式三:Vue 单文件组件(SFC)
<!-- Counter.vue -->
<template> <!-- 结构 -->
<div class="counter">
<button @click="count--">-</button>
<span>{{ count }}</span>
<button @click="count++">+</button>
</div>
</template>
<script setup> <!-- 逻辑 -->
import { ref } from 'vue';
const props = defineProps<{ initial: number }>(); // props 入参
const count = ref(props.initial ?? 0); // 内部状态
</script>
<style scoped> <!-- 样式 (scoped 自动隔离) -->
.counter { display: flex; gap: 8px; }
</style>
.vue 文件 = 模板 + 逻辑 + 样式三合一;scoped 关键字让样式自动按组件隔离,比原生 Shadow DOM 更省心。3.5.3 传统 vs 组件化 — 同一个用户卡片
传统方式:手动操作 DOM
// 每渲染一张卡片都要重复这段命令式代码
function renderUserCard(user) {
const card = document.createElement('div');
card.className = 'card';
const name = document.createElement('h3');
name.textContent = user.name; // 潜在 XSS,需转义
const btn = document.createElement('button');
btn.textContent = '关注';
let followed = false; // 状态散落在外部变量
btn.onclick = () => {
followed = !followed;
btn.textContent = followed ? '已关注' : '关注';
};
card.append(name, btn);
document.querySelector('#list').appendChild(card);
}
// 100 个用户 = 手动调 100 次,状态全靠自己记
组件化方式:声明 + 复用
// Vue 里写一次 UserCard,到处复用
<UserCard v-for="u in users" :user="u" />
// 关注状态是组件内部的事,外部无需关心
<script setup>
const props = defineProps<{ user: User }>();
const followed = ref(false);
</script>
appendChild。3.5.4 什么是状态(State)
状态就是随时间变化、并驱动界面显示的数据。组件化框架的核心工作,就是把"状态"和"视图"绑定:状态一变,视图自动重新渲染。
案例演示:状态 vs 普通变量
// 状态:会变化 + 驱动视图
const [count, setCount] = useState(0); // count 是状态
setCount(5); // 改状态 → 界面自动显示 5
// 普通变量:变了界面不知道
let count = 0; // 不是状态,改了也没人管
count = 5; // 界面毫无反应
典型状态:计数器
count、登录用户 userInfo、表单输入 inputValue、列表 list、加载中 loading。对照:状态驱动视图 vs 手动操作 DOM
// ❌ 传统命令式:改数据 + 手动同步界面(易漏、易错)
let count = 0;
count++;
document.querySelector('#num').textContent = count; // 忘了这行就不同步
// ✅ 状态驱动(React / Vue):只改状态,界面自动更新
const [count, setCount] = useState(0);
setCount(count + 1); // 浏览器自动重渲染,无需手动操作 DOM
这正是上一节"声明式"的底层机制:开发者只声明"数据长什么样",更新 DOM 的脏活交给框架。
组件不是框架发明的:软件工程的"组件化复用"思想可追溯到 1968 年 McIlroy 的"量产软件组件";前端只是在 2011-2014 年把它落到 UI 层。如今 Web Components 是浏览器原生标准,React/Vue 是框架层实现,三者殊途同归:都是"可复用、可组合、接口清晰的独立单元"。
第三部分:速查手册
4.1 HTML 语义化标签速查
| 分类 | 标签 | 用途 |
|---|---|---|
| 页面结构 | <header> <nav> <main> <article> <section> <aside> <footer> | 替代无意义的 <div>,利于 SEO 和无障碍 |
| 文本 | <h1>~<h6> <p> <strong> <em> <blockquote> <code> | 标题层级唯一 h1;strong/em 有语义,b/i 无 |
| 列表/表格 | <ul> <ol> <li> <dl> <table> <thead> <tbody> | 表格只用于数据,不用于布局 |
| 媒体 | <img> <picture> <video> <audio> <canvas> <svg> | img 必写 alt;懒加载用 loading="lazy" |
| 表单 | <form> <input> <label> <select> <textarea> <button> | label 用 for 关联 input;input 有 20+ 种 type |
| 交互(免 JS) | <details> <summary> <dialog> | 原生折叠面板和模态框 |
4.2 CSS 布局速查
| 方案 | 维度 | 适用场景 | 核心属性 |
|---|---|---|---|
| Flexbox | 一维(行或列) | 导航栏、居中、等分、工具栏 | display:flex justify-content align-items gap flex:1 |
| Grid | 二维(行+列) | 整页骨架、卡片墙、仪表盘 | display:grid grid-template-columns gap grid-area |
| 普通流 + position | — | 局部微调、悬浮元素 | position: relative/absolute/fixed/sticky |
/* 万能居中 */
.parent { display: flex; justify-content: center; align-items: center; }
/* 响应式卡片墙:自动换行,每列最少 240px */
.wall { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 16px; }
/* 圣杯布局 */
.page { display: grid; grid-template: "header header" auto "side main" 1fr "footer footer" auto / 220px 1fr; }
/* 响应式断点 */
@media (max-width: 800px) { .sidebar { display: none; } }
4.3 JS 核心语法速查
// 变量:const 优先,需要重新赋值才用 let,永远不用 var
const list = [1, 2, 3];
// 解构 & 展开
const { name, age = 18 } = user;
const merged = [...a, ...b];
// 箭头函数(无自己的 this)
const double = (x) => x * 2;
// 数组三板斧
list.map(x => x * 2).filter(x => x > 2).reduce((s, x) => s + x, 0);
// 异步:async/await 是 Promise 的语法糖
async function load() {
try {
const res = await fetch('/api/data');
if (!res.ok) throw new Error(res.status);
return await res.json();
} catch (e) {
console.error('请求失败', e);
}
}
// DOM 四件套:查、改、建、听
const el = document.querySelector('#app');
el.textContent = 'hello'; // 改文本(安全,不解析 HTML)
el.classList.toggle('active'); // 改样式:操作 class 而非行内 style
const li = document.createElement('li'); // 建节点
el.addEventListener('click', (e) => {}); // 监听事件
4.4 三者协作完整示例 — 计数器
一个文件展示三层分工:HTML 定义结构和挂载点,CSS 负责外观和状态样式,JS 监听事件并更新 DOM,浏览器自动重渲染。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<style>
/* CSS:表现层 */
.counter { display: flex; gap: 12px; align-items: center; font-size: 24px; }
button { padding: 8px 20px; border: none; border-radius: 8px;
background: #1572b6; color: #fff; cursor: pointer; }
button:hover { opacity: 0.85; }
.num.warn { color: #e34f26; font-weight: 700; } /* 状态样式由 JS 切换 */
</style>
</head>
<body>
<!-- HTML:结构层 -->
<div class="counter">
<button id="dec">-</button>
<span class="num" id="num">0</span>
<button id="inc">+</button>
</div>
<script>
// JS:行为层 —— 状态驱动视图
let count = 0;
const num = document.querySelector('#num');
function render() {
num.textContent = count;
num.classList.toggle('warn', count >= 10); // JS 只切 class,样式交给 CSS
}
document.querySelector('#inc').addEventListener('click', () => { count++; render(); });
document.querySelector('#dec').addEventListener('click', () => { count--; render(); });
</script>
</body>
</html>
4.5 常见坑与最佳实践
| 坑 | 原因 | 正确做法 |
|---|---|---|
0.1 + 0.2 !== 0.3 | IEEE 754 浮点数精度 | 金额用整数分存储,或 toFixed 显示 |
== 出现诡异结果 | 隐式类型转换('' == 0 为 true) | 永远用 === |
| 循环里 innerHTML 拼接卡顿 | 每次赋值都触发重新解析+回流 | 拼完一次性赋值,或用 DocumentFragment |
| 动画掉帧 | 动了 width/left 等触发回流 | 只动 transform / opacity(合成层) |
| 用户输入直接插入 innerHTML | XSS 注入 | 用 textContent,或严格转义 |
| 样式全写行内 / JS 里改 style | 结构表现行为耦合,难维护 | JS 只切换 class,样式集中在 CSS |
| div 套 div 到底 | 无语义,无障碍差 | 优先语义化标签(4.1 节) |
第四部分:CSS 系统深入
来源:MDN Web Docs · W3C CSS WG · CSS Editor's Drafts
CSS(Cascading Style Sheets,级联样式表)是一种描述 HTML/XML/SVG 文档呈现方式的声明式样式语言。它是开放 Web 三大核心技术之一(HTML 结构 + CSS 表现 + JS 行为),由 W3C CSS 工作组制定规范。自 CSS2.1 之后,CSS 不再有统一版本号,而是拆分为独立模块各自演进(如 Selectors Level 4、Color Level 5、Grid Level 2 等),通过年度 CSS Snapshot 记录各模块成熟度。
5.1 概述
CSS 的设计哲学是关注点分离:内容(HTML)与表现(CSS)解耦。其核心机制围绕三个关键词展开:
| 机制 | 含义 | 关键概念 |
|---|---|---|
| Cascade(层叠) | 多来源样式冲突时的裁决规则 | 来源、特异性、书写顺序、!important、@layer |
| Inheritance(继承) | 某些属性自动从父元素传递 | inherit / initial / unset / revert |
| Specificity(特异性) | 选择器优先级的量化指标 | ID > Class > Type (0,0,0 三元组) |
5.2 行为分析(系统做什么)
5.2.1 用例图:CSS 系统的参与者与功能
CSS 系统有多个参与者:设计师编写样式、开发者维护工具链、浏览器引擎解析渲染、最终用户阅读页面。
5.2.2 活动图:CSS 渲染流水线
浏览器将 CSS 文本转换为屏幕像素的完整流水线:解析 CSS → 构建 CSSOM → 合并 Render Tree → Layout → Paint → Composite。每一步都可能成为性能瓶颈。
5.2.3 活动图:层叠优先级判定
多条 CSS 规则命中同一元素时,浏览器按什么顺序裁决最终生效值?这是 CSS 的"C"——Cascade 的核心流程。
5.2.4 活动图:选择器匹配流程
浏览器如何为 DOM 中的每个元素找到匹配的 CSS 规则?注意浏览器是从右向左匹配的,以快速排除不匹配元素。
5.2.5 活动图:布局计算流程
当 Render Tree 构建完成后,浏览器计算每个元素的几何位置和尺寸——这就是 Layout(布局/回流)阶段。
5.2.6 序列图:动画与过渡时序
CSS 动画和过渡的触发、帧计算、合成渲染的完整时序。
5.3 结构分析(系统由什么组成)
5.3.1 包图:CSS 规范模块组织
CSS 规范按功能领域拆分为独立模块,各自有独立 Level 版本号。以下是主要模块的组织结构。
5.3.2 组件图:浏览器渲染引擎中的 CSS 子系统
CSS 引擎在浏览器渲染引擎(Blink/WebKit/Gecko)中的组件架构。
5.3.3 类图:CSSOM 核心对象模型
CSS Object Model(CSSOM)是 JS 操作样式的 API 层,是 CSS 在内存中的对象表示。
5.3.4 结构图:CSS 盒模型
每个元素在 CSS 中都是一个矩形盒子,由 content → padding → border → margin 四层嵌套组成。
5.3.5 部署图:不同媒介的渲染上下文
CSS 不仅服务于屏幕浏览器,还覆盖打印、语音、电子纸等多种媒介。
5.4 层叠机制详解
5.4.1 层叠总览
当多条 CSS 规则命中同一元素时,浏览器按以下 6 层优先级裁决(由低到高):
- 用户代理样式(浏览器默认)——如默认的
display: block、margin: 8px - 用户样式——用户在浏览器设置中自定义的样式
- 作者样式中的 @layer——按声明顺序,后声明的层优先级更高
- 作者样式中的无层级(normal)规则——特异性裁决
- 作者样式中的 !important——反转常规优先级
- 用户样式中的 !important / 用户代理 !important——最高优先级(无障碍保障)
5.4.2 特异性计算
特异性记作 (A, B, C) 三元组:
| A | ID 选择器数量 |
|---|---|
| B | 类选择器、属性选择器、伪类的数量 |
| C | 元素选择器、伪元素的数量 |
比较规则:先比 A,相同时比 B,再相同时比 C。注意 (1,0,0) > (0,999,999)——A 优先于 B 优先于 C。
特殊规则:
- 行内 style 属性 > 所有选择器(相当于 (1,0,0,0))
!important覆盖一切(但层叠顺序反转)- 通配符
*和组合器(> + ~ ||)不计入特异性 :is()/:not()取参数中最高特异性;:where()始终为 (0,0,0)
/* (0,0,1) */ p { color: blue; }
/* (0,1,0) */ .intro { color: green; }
/* (1,0,0) */ #hero { color: orange; }
/* 结果: #hero 最高优先级 */
查看源码
/* (0,0,1) → 1 个元素选择器 */
p { color: blue; }
/* (0,1,0) → 1 个类选择器 */
.intro { color: green; } /* 优先级高于 p */
/* (1,0,0) → 1 个 ID 选择器 */
#hero { color: orange; } /* 优先级高于 .intro */
5.4.3 继承机制
CSS 属性分为两类:
- 可继承属性(大多数排版属性):
color、font-*、text-*、line-height、visibility、cursor等 - 不可继承属性(大多数盒模型属性):
margin、padding、border、width、height、display、position等
四个通用关键字控制继承:inherit(强制继承)、initial(重置为初始值)、unset(可继承属性和 inherit 相同,不可继承属性和 initial 相同)、revert(回退到上一层来源的值)。
5.4.4 属性值处理流程
一个属性的最终值经历了 6 个阶段的转换:
- 声明值(Declared Value)——样式表中写了什么(可能多条声明命中同一元素)
- 层叠值(Cascaded Value)——层叠裁决后的赢家(含 !important 和 @layer)
- 指定值(Specified Value)——若无层叠值则取继承值或初始值
- 计算值(Computed Value)——相对单位(em、%)解析为绝对但不依赖于布局的值
- 使用值(Used Value)——布局计算后的最终值(如
width: auto计算为具体像素) - 实际值(Actual Value)——受限于设备精度(如浮点像素取整)
5.4.5 @layer 级联层
CSS Cascade Level 5 引入 @layer 规则,允许开发者显式分层管理样式优先级:
/* 声明层顺序——越后声明优先级越高 */
@layer reset, base, components, utilities;
@layer reset {
* { margin: 0; padding: 0; }
}
@layer base {
h1 { font-size: 2rem; }
}
@layer components {
.card { border: 1px solid #ddd; }
}
@layer utilities {
.mt-4 { margin-top: 1rem; } /* 优先级高于 reset/base/components */
}
查看源码
@layer low { .box { background: #fdd; } }
@layer high { .box { background: #d4edda; } }
/* 虽然 low 在前 high 在后,但 @layer 声明顺序决定了优先级 */
5.5 布局系统
5.5.1 格式化上下文
CSS 布局的基础是格式化上下文——一个区域内的元素按特定规则排列。主要有以下类型:
| 上下文类型 | 触发方式 | 作用 |
|---|---|---|
| BFC(块级格式化上下文) | display: flow-root / overflow: hidden / float / position: absolute | 独立块布局区域,防止外边距折叠、包含浮动 |
| IFC(行内格式化上下文) | 块容器内默认存在 | 行内级元素在水平方向排列 |
| Flex Formatting Context | display: flex / inline-flex | 弹性盒子布局 |
| Grid Formatting Context | display: grid / inline-grid | 二维网格布局 |
5.5.2 常规流布局(Flow Layout)
默认布局方式。块级元素垂直堆叠,行内元素水平排列。核心概念:
- 包含块(Containing Block):元素尺寸和位置的参考矩形
- 外边距折叠(Margin Collapsing):相邻块级元素的上下外边距会合并,取最大值
- 浮动(Float):使元素脱离常规流,向左/右移动,文字环绕
- 清除浮动(Clear):
clear: both或触发 BFC 来包含浮动元素
5.5.3 Flexbox 弹性布局
CSS Flexible Box Layout(Level 1)提供一维方向的灵活布局。核心模型:
- Flex 容器:
display: flex,定义主轴(flex-direction)和交叉轴 - 容器属性:
justify-content(主轴对齐)、align-items(交叉轴对齐)、flex-wrap(换行)、gap(间距) - 项目属性:
flex-grow(放大比例)、flex-shrink(缩小比例)、flex-basis(初始大小)、align-self(单独对齐)、order(排序) - 简写
flex: 1:等价于flex-grow: 1; flex-shrink: 1; flex-basis: 0%
5.5.4 Grid 网格布局
CSS Grid Layout(Level 1/2)提供二维网格布局能力,是目前最强的 CSS 布局系统:
- Grid 容器:
display: grid,用grid-template-columns/rows定义轨道 - 轨道函数:
fr(弹性份数)、minmax()、auto-fill / auto-fit、repeat() - 区域模板:
grid-template-areas用 ASCII 艺术定义布局区域 - 间距:
gap/row-gap/column-gap(替代了旧的 grid-gap) - Grid Level 2:引入
subgrid(子网格继承父网格轨道) - Grid Level 3:引入
masonry瀑布流布局
5.5.5 定位布局
CSS Positioned Layout 允许元素脱离常规流进行精确定位:
| 定位方式 | 参考系 | 是否脱离常规流 |
|---|---|---|
static(默认) | 常规流位置 | 否 |
relative | 自身原位置 | 否(保留原位置空间) |
absolute | 最近的定位祖先(非 static) | 是 |
fixed | 视口 | 是 |
sticky | 滚动容器(滚动到阈值后固定) | 否(切换) |
核心概念:
- 堆叠上下文(Stacking Context):由
z-index+position(非 static)创建,或由opacity < 1、transform、filter等触发的独立层叠空间 - 包含块:absolute 的包含块是最近的定位祖先的 padding-box;fixed 的包含块是视口
Flexbox (一维)
Grid (二维)
查看源码
/* Flexbox */
.flex { display: flex; gap: 8px; }
.flex .item:nth-child(2) { flex-grow: 1; }
/* Grid */
.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.grid .header { grid-column: 1 / 3; } /* 跨两列 */
5.6 动画系统
CSS 提供三层动画能力,从简单到复杂:
| 机制 | 触发方式 | 控制力 | 典型场景 |
|---|---|---|---|
| Transition(过渡) | 属性值变化时自动触发(如 :hover) | 简单(from → to) | 按钮悬停变色、菜单滑入 |
| Animation(动画) | 通过 @keyframes 定义,可自动播放 | 复杂(多关键帧、循环、反向) | 加载动画、无限循环动效 |
| Scroll-driven Animations | 滚动位置驱动时间线 | 与滚动绑定 | 滚动视差、进度指示器 |
关键性能原则:只动画 transform 和 opacity——它们只触发 Composite 阶段,不走 Layout/Paint,可保持 60fps。使用 will-change 提前提示浏览器创建合成层。
- 最便宜(仅 Composite):
transform、opacity- 中等(Paint + Composite):
color、background、box-shadow- 最贵(Layout + Paint + Composite):
width、height、top、left、margin
Hover 查看 Transition
@keyframes 自动播放
查看源码
/* Transition */
.btn { transition: all 0.3s; }
.btn:hover { background: #e34f26; transform: translateY(-2px); }
/* Animation */
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.3); opacity: 0.6; }
}
.dot { animation: pulse 1.5s ease-in-out infinite; }
5.7 选择器系统
选择器是 CSS 的寻址系统,决定样式应用到哪些元素。按规范演进分为多个 Level:
| Level | 新增功能 | 状态 |
|---|---|---|
| Level 3 (REC) | 属性选择器、伪类、组合器 | 全面支持 |
| Level 4 (WD) | :is() :where() :has() :not() 复合参数、:focus-visible、:focus-within、逻辑组合器 || | 主流浏览器已支持 |
| Level 5 (WD) | :state()、:open、:closed、自定义状态 | 实验阶段 |
选择器分类:
- 基本选择器:
*(通配符)、元素选择器、类选择器、ID 选择器 - 属性选择器:
[attr]、[attr=val]、[attr^=]、[attr$=]、[attr*=]、[attr~=]、[attr|=] - 伪类:状态类(
:hover、:focus、:checked)、结构类(:nth-child、:first-of-type)、逻辑类(:is、:where、:not、:has) - 伪元素:
::before、::after、::first-line、::first-letter、::selection、::marker - 组合器:
(后代)、>(子代)、+(相邻兄弟)、~(通用兄弟)
:has() 是"父选择器":Level 4 引入的 :has() 是革命性的——它可以基于后代元素反向选择父元素,例如 div:has(> img) 选择包含直接子 img 的 div。
:nth-child 斑马纹
- 第 1 行
- 第 2 行
- 第 3 行
- 第 4 行
- 第 5 行
查看源码
/* :has() — 父选择器 */
.card:has(img) { border-color: #1572b6; border-width: 2px; }
/* :nth-child — 斑马纹 */
li:nth-child(odd) { background: #e8f0fe; }
5.8 响应式设计
CSS 响应式设计的核心工具链:
- 媒体查询(Media Queries):
@media (min-width: 768px)基于视口宽度、设备特性(hover、pointer、prefers-color-scheme 等)设置断点 - 容器查询(Container Queries):
@container (min-width: 400px)基于父容器尺寸而非视口,是组件级响应式的革命 - 视口单位:
vw/vh/dvh(动态视口高度)/svh(小视口高度)/lvh(大视口高度) - 函数:
clamp()(限制值范围)、min()、max()、calc() - 逻辑属性:
margin-inline、padding-block等书写模式相关属性,天然支持多语言布局
/* 现代响应式组合拳 */
.card {
width: clamp(300px, 50%, 600px); /* 限制不超出范围 */
padding: clamp(1rem, 2vw, 3rem); /* 自适应内边距 */
font-size: clamp(1rem, 0.5rem + 1vw, 2rem); /* 流体排版 */
}
这段文字使用 font-size: clamp(14px, 1.5vw, 24px),
缩放浏览器宽度可以看到字号平滑变化,但不会超出 14px~24px 范围。
查看源码
.fluid-text {
font-size: clamp(14px, 1.5vw, 24px);
/* 最小14px,理想1.5%视口宽度,最大24px */
}
5.9 性能与优化
CSS 性能影响页面渲染的三个阶段:
| 阶段 | 触发条件 | 优化策略 |
|---|---|---|
| Layout(回流) | 改几何属性:width/height/top/left/font-size/display/position | 批量修改(用 requestAnimationFrame 或 documentFragment);用 transform 替代 top/left |
| Paint(重绘) | 改非几何属性:color/background/border/box-shadow/outline | 减少绘制区域;用 contain: paint 限制绘制范围 |
| Composite(合成) | 改 transform/opacity | 用 will-change 提升为合成层;避免过多层(层爆炸) |
关键改进:
- CSS Containment:
contain: layout style paint size隔离子树,告诉浏览器该元素内部变化不影响外部,跳过不必要的全局计算。等效于content-visibility: auto(跳过屏幕外元素的渲染) - 选择器性能:浏览器从右向左匹配选择器,最右边的选择器应尽可能精确;避免深层嵌套选择器
- 关键 CSS 内联:将首屏关键 CSS 内联到
<head>,延迟加载非关键 CSS - @import 避免:
@import是串行加载的阻塞资源,用多个<link>替代
性能优化代码示例
/* 跳过屏幕外元素渲染,减少初始 Layout/Paint 开销 */
.off-screen-section {
content-visibility: auto;
contain-intrinsic-size: 0 500px; /* 占位高度,防止滚动条跳动 */
}
/* 将动画元素提升为独立合成层 */
.animated {
will-change: transform;
transform: translateZ(0); /* 强制 GPU 合成 */
}
/* 隔离子树,告诉浏览器该区域独立 */
.widget {
contain: layout style paint;
}
5.10 生态工具链
CSS 周边工具链覆盖开发、构建、运行时的完整流程:
| 类别 | 工具 | 作用 |
|---|---|---|
| 预处理器 | Sass/SCSS, Less, Stylus | 变量、嵌套、Mixin、函数——编译为普通 CSS |
| 后处理器 | PostCSS + Autoprefixer | 插件化 CSS 转换(自动加厂商前缀、未来语法转译) |
| CSS 框架 | Tailwind CSS, Bootstrap, Bulma | 预设 utility class / 预置组件样式(样式层,非完整组件) |
| CSS-in-JS | styled-components, Emotion, CSS Modules | 组件作用域样式(React/Vue 生态常用) |
| 构建工具 | Vite, webpack (css-loader/style-loader) | CSS 模块打包、压缩、Tree Shaking |
| 优化工具 | PurgeCSS, cssnano, Lightning CSS | 移除未使用 CSS、压缩、极速转译 |
| Linting | Stylelint | CSS 代码规范检查 |
| 设计系统 | CSS 自定义属性(--*) | 原生变量体系,运行时动态切换主题 |
/* CSS 自定义属性——原生变量体系 */
:root {
--color-primary: #2563eb;
--spacing-unit: 0.25rem;
}
.btn {
background: var(--color-primary);
padding: calc(var(--spacing-unit) * 4) calc(var(--spacing-unit) * 8);
}
/* 暗色模式只需覆盖变量 */
[data-theme="dark"] {
--color-primary: #60a5fa;
}
主题演示区域
背景和文字颜色由 --demo-bg / --demo-fg 控制
查看源码
.box {
background: var(--demo-bg, #f0f2f5);
color: var(--demo-fg, #212529);
}
.box.dark { --demo-bg: #1a1a2e; --demo-fg: #e0e0e0; }
/* 切换类名即可换主题,无需重写所有属性 */
5.11 参考来源
第五部分:JavaScript 系统深入
6.1 语言定位
什么是 JavaScript
JavaScript 是一种动态类型原型继承垃圾回收一等函数的编程语言。标准名称为 ECMAScript(ECMA-262),由 TC39 委员会维护。运行于单线程事件循环模型之上,通过 JIT 编译实现高性能执行。
| 特性 | 说明 |
|---|---|
| 范式 | 多范式:命令式、函数式、面向对象、事件驱动 |
| 类型系统 | 动态弱类型,7 种原始类型 + Object |
| 继承模型 | 原型链(prototype chain),ES6 引入 class 语法糖 |
| 内存管理 | 自动垃圾回收(标记-清除 + 分代收集) |
| 并发模型 | 单线程事件循环 + 异步非阻塞 I/O |
| 标准规范 | ECMA-262(语言核心)、ECMA-402(国际化 API) |
| 主要引擎 | V8(Chrome/Node.js)、SpiderMonkey(Firefox)、JavaScriptCore(Safari) |
6.2 版本历史
6.3 生态全景
三层生态结构
| 层级 | 内容 | 关键项目 |
|---|---|---|
| 语言核心 | ECMAScript 规范定义的语法、类型、内置对象 | ECMA-262, ECMA-402 |
| 引擎层 | C++ 实现的 JS 虚拟机,负责解析/编译/执行 | V8, SpiderMonkey, JavaScriptCore |
| 运行时层 | 提供 I/O、网络、文件系统等宿主 API | Browser, Node.js, Deno, Bun, Cloudflare Workers |
6.4 行为图(系统做什么)
6.4.1 用例图 — 开发者与语言的交互全景
6.4.2 活动图 — 源码到可执行代码的解析流水线
闭包 & 作用域 Demo
闭包 — 函数记住其创建时的作用域
function createCounter(initial = 0) {
let count = initial; // 私有变量,外部不可直接访问
return {
increment: () => ++count,
decrement: () => --count,
getValue: () => count,
};
}
const c = createCounter(10);
console.log(c.increment()); // 11
console.log(c.increment()); // 12
console.log(c.decrement()); // 11
console.log(c.getValue()); // 11
// console.log(c.count); // undefined — count 被闭包保护
经典场景 — 防抖 & 节流
function debounce(fn, delay) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
function throttle(fn, interval) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime >= interval) {
lastTime = now;
fn.apply(this, args);
}
};
}
const onResize = debounce(() => console.log('resized'), 300);
window.addEventListener('resize', onResize);
var vs let/const 的闭包陷阱
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 输出: 3, 3, 3 — var 没有块级作用域
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 输出: 0, 1, 2 — let 每次迭代绑定新变量
6.4.3 活动图 — 事件循环(Event Loop)
事件循环可视化 Demo
理解 Event Loop 的执行顺序
运行下面代码,观察 Call Stack → Microtask → Macrotask 的执行顺序:
console.log('1. 同步代码开始');
setTimeout(() => console.log('5. setTimeout 宏任务'), 0);
Promise.resolve()
.then(() => console.log('3. Promise.then 微任务'))
.then(() => console.log('4. Promise.then 链式微任务'));
queueMicrotask(() => console.log('2. queueMicrotask 微任务'));
console.log('6. 同步代码结束');
// 输出顺序: 1 → 6 → 2 → 3 → 4 → 5
// 原因: 同步 > 所有微任务 > 一个宏任务 > 重复
嵌套微任务 & 宏任务
console.log('start');
setTimeout(() => {
console.log('macro 1');
Promise.resolve().then(() => console.log('micro inside macro'));
}, 0);
Promise.resolve().then(() => {
console.log('micro 1');
setTimeout(() => console.log('macro inside micro'), 0);
});
console.log('end');
// 输出: start → end → micro 1 → macro 1 → micro inside macro → macro inside micro
6.4.4 时序图 — V8 JIT 编译流水线
6.4.5 时序图 — 垃圾回收流程
6.4.6 状态机图 — Promise 生命周期
Promise / async-await Demo
Promise 基础
function fetchUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) resolve({ id, name: 'User' + id });
else reject(new Error('Invalid id'));
}, 100);
});
}
// 链式调用
fetchUser(1)
.then(user => {
console.log(user.name); // User1
return fetchUser(2);
})
.then(user => console.log(user.name))
.catch(err => console.error(err))
.finally(() => console.log('请求结束'));
// 静态方法
Promise.all([fetchUser(1), fetchUser(2)])
.then(([u1, u2]) => console.log(u1, u2))
.catch(err => console.error('任一失败则整体 reject', err));
Promise.allSettled([fetchUser(1), fetchUser(-1)])
.then(results => results.forEach(r => console.log(r.status)));
// { status: 'fulfilled', value: { id:1, name:'User1' } }
// { status: 'rejected', reason: Error('Invalid id') }
Promise.race([fetchUser(1), fetchUser(2)]).then(console.log); // 取最快
Promise.any([fetchUser(-1), fetchUser(2)]).then(console.log); // 取第一个 fulfilled
async / await
async function loadUsers() {
try {
const u1 = await fetchUser(1); // 等待,像同步写法
const u2 = await fetchUser(2); // 串行:总耗时 200ms
return [u1, u2];
} catch (err) {
console.error('加载失败', err);
return [];
}
}
// 并发 vs 串行
async function loadParallel() {
const [u1, u2] = await Promise.all([
fetchUser(1),
fetchUser(2)
]); // 并发:总耗时 100ms
return [u1, u2];
}
// 顶层 await (ESM 模块中可用)
// const data = await fetch('/api/data');
6.5 结构图(系统由什么组成)
6.5.1 组件图 — 运行环境整体架构
6.5.2 包图 — ECMAScript 规范模块划分
6.5.3 类图 — V8 引擎核心对象模型
6.5.4 类图 — 原型链体系
原型链 & 继承 Demo
原型链基础
const arr = [1, 2, 3];
console.log(arr.__proto__ === Array.prototype); // true
console.log(Array.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null
// arr → Array.prototype → Object.prototype → null
const fn = () => {};
console.log(fn.__proto__ === Function.prototype); // true
console.log(Function.prototype.__proto__ === Object.prototype); // true
// fn → Function.prototype → Object.prototype → null
构造函数 + 原型继承
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
return `${this.name} makes a sound`;
};
function Dog(name, breed) {
Animal.call(this, name); // 继承属性
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype); // 继承方法
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
return `${this.name} barks!`;
};
const d = new Dog('旺财', '金毛');
console.log(d.speak()); // "旺财 makes a sound"
console.log(d.bark()); // "旺财 barks!"
console.log(d instanceof Dog); // true
console.log(d instanceof Animal); // true
// ES6 class 语法糖
class Animal2 {
constructor(name) { this.name = name; }
speak() { return `${this.name} makes a sound`; }
}
class Dog2 extends Animal2 {
constructor(name, breed) {
super(name);
this.breed = breed;
}
bark() { return `${this.name} barks!`; }
}
// 底层仍然是原型链: Dog2.prototype → Animal2.prototype → Object.prototype → null
6.5.5 部署图 — 多运行时部署拓扑
6.6 核心语法速查
6.6.1 数据类型速查
| 类型 | 值 | typeof | 说明 |
|---|---|---|---|
Undefined | undefined | "undefined" | 变量未赋值、函数无返回值 |
Null | null | "object" (bug) | 空对象引用,原型链终点 |
Boolean | true / false | "boolean" | Falsy: 0, "", null, undefined, NaN, false |
Number | 42, 3.14, NaN, Infinity | "number" | IEEE 754 双精度, 0.1+0.2≠0.3 |
BigInt | 42n | "bigint" | 任意精度整数 |
String | 'hello', "world", \`tpl\` | "string" | 不可变,UTF-16 编码 |
Symbol | Symbol('id') | "symbol" | 唯一标识符,对象 key 用 |
Object | {}, [], function() {} | "object"/"function" | 引用类型,键值对集合 |
类型判断最佳实践
typeof 42 // "number"
typeof 'hello' // "string"
typeof undefined // "undefined"
typeof null // "object" — 历史 Bug!
typeof function(){} // "function"
typeof [] // "object"
Array.isArray([]) // true — 推荐
Object.prototype.toString.call([]) // "[object Array]"
Object.prototype.toString.call(null) // "[object Null]"
// 判断 null
val === null
// 判断 NaN (NaN ≠ NaN)
Number.isNaN(NaN) // true
isNaN('hello') // true (先转数字)
Number.isNaN('hello') // false (不转换)
6.6.2 运算符速查
| 类型 | 运算符 | 说明 |
|---|---|---|
| 算术 | + - * / % ** | 加法可做字符串拼接; ** 幂运算 |
| 比较 | == != === !== > < >= <= | === 严格相等(类型+值); == 会类型转换 |
| 逻辑 | && || ! ?? | ?? nullish: 仅 null/undefined 触发默认值 |
| 位运算 | & | ^ ~ << >> >>> | 操作 32 位有符号整数 |
| 三元 | c ? a : b | 条件表达式 |
| 可选链 | ?. | obj?.prop?.sub 遇到 null/undefined 短路返回 undefined |
| 展开 | ... | 展开数组/对象,或收集剩余参数 |
| 逗号 | , | 返回最后一个表达式的值 |
== vs === 典型坑
0 == false // true 0 === false // false '' == false // true '' === false // false null == undefined // true — 特殊规则 null === undefined // false NaN == NaN // false — 唯一不自等的值 [1,2] == '1,2' // true — 数组先转字符串
?? vs ||
0 || 'default' // 'default' — 0 被当作 falsy 0 ?? 'default' // 0 — 只有 null/undefined 触发 '' || 'default' // 'default' '' ?? 'default' // '' false || true // true false ?? true // false
6.6.3 Array 方法速查
| 分类 | 方法 | 说明 | 改原数组? |
|---|---|---|---|
| 遍历 (不产生新数组) | forEach(fn) | 遍历每个元素,无返回值 | 否 |
some(fn) | 有一个通过即返回 true | 否 | |
every(fn) | 全部通过才返回 true | 否 | |
find(fn) | 返回第一个通过的元素 | 否 | |
findLast(fn) | 返回最后一个通过的元素 (ES2023) | 否 | |
| 变换 (产生新数组) | map(fn) | 映射每个元素到新值 | 否 |
filter(fn) | 过滤出通过的元素 | 否 | |
flatMap(fn) | map 后自动 flatten 一层 | 否 | |
| 归约 | reduce(fn, init) | 累积计算;reduceRight 从右开始 | 否 |
| 增删改 (改原数组) | push/pop | 尾部添加/移除 | 是 |
unshift/shift | 头部添加/移除 | 是 | |
splice(i, n, ...) | 删除/插入/替换 | 是 | |
sort(fn) | 排序 (默认按字符串) | 是 | |
reverse() | 反转 | 是 | |
| 子数组 (不修改) | slice(s, e) | 截取子数组 | 否 |
concat(arr) | 合并数组 | 否 | |
includes(v) | 是否包含某个值 | 否 | |
indexOf(v) | 返回首次出现索引 | 否 | |
| ES6+ 新增 | flat(n) | 展平嵌套 n 层, Infinity 全展平 | 否 |
at(i) | 支持负数索引: arr.at(-1) 最后一个 | 否 | |
with(i, v) | 不可变替换: 返回新数组 (ES2023) | 否 | |
toReversed/Sorted/Spliced() | 不可变版的 reverse/sort/splice | 否 |
常用模式
const nums = [1, 2, 3, 4, 5]; nums.filter(n => n > 2).map(n => n * 2); // [6, 8, 10] nums.reduce((sum, n) => sum + n, 0); // 15 nums.flatMap(n => [n, n * 2]); // [1,2, 2,4, 3,6, 4,8, 5,10] // 去重 [...new Set([1,1,2,2,3])]; // [1, 2, 3] // 排序数字 (默认按字符串排序!) [10, 2, 1].sort(); // [1, 10, 2] - 错! [10, 2, 1].sort((a,b) => a - b); // [1, 2, 10] - 对
6.6.4 String 方法速查
| 方法 | 说明 | 示例 |
|---|---|---|
length | 字符串长度 (UTF-16 单元数) | '你好'.length → 2 |
charAt(i) / at(i) | 第 i 个字符; at 支持负数 | 'abc'.at(-1) → 'c' |
includes(s) | 是否包含子串 | 'hello'.includes('ll') → true |
startsWith(s) / endsWith(s) | 判断前缀/后缀 | 'hello'.startsWith('he') → true |
indexOf(s) / lastIndexOf(s) | 首次/末次出现索引, -1 表示未找到 | 'abca'.indexOf('a') → 0 |
slice(s, e) / substring(s, e) | 截取子串; slice 支持负数 | 'abc'.slice(1) → 'bc' |
toLowerCase() / toUpperCase() | 大小写转换 | |
trim() / trimStart() / trimEnd() | 去空格/首部/尾部 | |
replace(r, s) / replaceAll(r, s) | 替换首个/全部匹配 | 'a a'.replaceAll('a','b') → 'b b' |
split(sep) | 按分隔符拆分为数组 | 'a,b,c'.split(',') → ['a','b','c'] |
padStart(n,c) / padEnd(n,c) | 填充到指定长度 | '42'.padStart(5,'0') → '00042' |
match(re) / matchAll(re) | 正则匹配 |
模板字面量
const name = 'World';
const greeting = `Hello ${name}! Backtick \` and ${2 + 3}`;
// "Hello World! Backtick ` and 5"
const multiline = `第1行
第2行`; // 保留换行
function tag(strings, ...values) {
return strings.reduce((r,s,i) => r + s + (values[i]?.toUpperCase() || ''), '');
}
tag`hello ${'world'}`; // "hello WORLD"
6.6.5 Object 方法速查
| 方法 | 说明 |
|---|---|
Object.keys(obj) | 返回自身可枚举属性名数组 |
Object.values(obj) | 返回自身可枚举属性值数组 |
Object.entries(obj) | 返回 [key, value] 对数组 |
Object.fromEntries(arr) | 从 [key,value] 数组构造对象 (Map ↔ Object 互转) |
Object.assign(target, ...srcs) | 浅拷贝: 将源对象属性复制到目标 |
Object.create(proto) | 以 proto 为原型创建新对象 |
Object.defineProperty(obj, key, desc) | 精确定义属性 (可设 getter/setter/writable/enumerable) |
Object.freeze(obj) | 冻结对象: 不可增删改 (浅冻结) |
Object.seal(obj) | 密封对象: 不可增删, 可修改现有属性值 |
Object.hasOwn(obj, prop) | 是否自身属性 (比 hasOwnProperty 更安全) |
对象操作模式
// 浅拷贝 (3 种方式)
const copy = { ...obj };
const copy2 = Object.assign({}, obj);
const copy3 = structuredClone(obj); // 深拷贝! (ES2021, 支持循环引用)
// Map ↔ Object 互转
const m = new Map([['a', 1], ['b', 2]]);
const o = Object.fromEntries(m); // { a: 1, b: 2 }
const m2 = new Map(Object.entries(o)); // Map(2) { 'a' => 1, 'b' => 2 }
// 属性描述符
Object.defineProperty(obj, 'fullName', {
get() { return `${this.first} ${this.last}`; },
set(v) { [this.first, this.last] = v.split(' '); },
enumerable: true,
});
// 解构 + 默认值 + 重命名
const { name: userName = 'anonymous', age = 0 } = user;
const [first, second, ...rest] = array;
6.6.6 异步模式速查
| 场景 | 写法 | 说明 |
|---|---|---|
| 串行 | await a(); await b(); | 一个完成后再做下一个 |
| 并发 | await Promise.all([a(), b()]) | 同时开始,全部完成才继续 |
| 竞速 | await Promise.race([a(), b()]) | 任意一个 settle 即返回 |
| 任一成功 | await Promise.any([a(), b()]) | 第一个 fulfilled; 全 rejected 才报 AggregateError |
| 全结果 | await Promise.allSettled([a(), b()]) | 全部 settle 后返回 {status, value/reason}[] |
| 超时 | Promise.race([req, timeout(ms)]) | 手动创建 reject 的定时 Promise |
| 错误重试 | 见下方代码 | async/await + try/catch 循环 |
常用工具函数
// 延时
const sleep = ms => new Promise(r => setTimeout(r, ms));
// 超时
const withTimeout = (promise, ms) =>
Promise.race([promise, sleep(ms).then(() => { throw new Error('Timeout'); })]);
// 重试
async function retry(fn, times = 3, delay = 1000) {
for (let i = 0; i < times; i++) {
try { return await fn(); }
catch (err) {
if (i === times - 1) throw err;
await sleep(delay * (i + 1)); // 指数退避
}
}
}
// 并发限制 (最多同时 N 个)
async function concurrentLimit(tasks, limit = 3) {
const results = [], executing = [];
for (const task of tasks) {
const p = Promise.resolve().then(() => task()).then(r => results.push(r));
executing.push(p);
if (executing.length >= limit) {
await Promise.race(executing);
executing.splice(executing.findIndex(e => e === p), 1);
}
}
await Promise.all(executing);
return results;
}
6.6.7 模块化速查
| 方式 | 导出 | 导入 |
|---|---|---|
| ESM (ES2015+) |
export const x = 1;export default fn;export { a, b as c };
|
import { x } from './m.js';import fn from './m.js';import * as M from './m.js';const m = await import('./m.js'); (动态导入)
|
| CJS (Node.js 传统) |
module.exports = fn;exports.x = 1;
|
const fn = require('./m');const { x } = require('./m');
|
典型导出模式
// math.js — ESM 命名导出 (推荐)
export function add(a, b) { return a + b; }
export function sub(a, b) { return a - b; }
export const PI = 3.14159;
// app.js — 导入
import { add, sub, PI } from './math.js';
import * as math from './math.js'; // math.add(1, 2)
// config.js — 默认导出 (一个模块一个)
export default { apiUrl: '/api', debug: true };
import config from './config.js';
// re-export (聚合模块)
export { add, sub } from './math.js';
export { default as config } from './config.js';
export * from './math.js'; // 全部再导出
6.6.7.1 文件扩展名与模块类型判断
Node.js 通过文件扩展名和 package.json 的 "type" 字段决定 JS 文件按哪种模块系统处理:
| 扩展名 | 模块类型 | 说明 |
|---|---|---|
.mjs | ESM | 明确标识为 ES Module,始终使用 import/export 语法 |
.cjs | CommonJS | 明确标识为 CommonJS,始终使用 require/module.exports |
.js | 取决于 package.json | 若最近 package.json 有 "type": "module" → ESM;否则 → CJS(默认) |
package.json 配置
// package.json — 设置项目默认模块类型
{
"type": "module" // .js 文件默认按 ESM 处理
}
// 或
{
"type": "commonjs" // .js 文件默认按 CJS 处理(默认值,可省略)
}
典型互操作
// ESM 中导入 CJS (.cjs 或 .js 被识别为 CJS 的项目)
import foo from './legacy.cjs'; // 默认导入 (module.exports)
import { named } from './legacy.cjs'; // 命名导入可能不支持
import * as mod from './legacy.cjs'; // 命名空间导入 (推荐)
const mod = await import('./legacy.cjs'); // 动态导入
// CJS 中导入 ESM (.mjs 或 .js 被识别为 ESM 的项目)
// 只能用动态 import(),不能用 require()
const esmModule = await import('./esm-module.mjs');
// const esm = require('./esm-module.mjs'); // ❌ 报错: ERR_REQUIRE_ESM
6.6.8 常见坑 & 最佳实践
| 坑 | 原因 | 正确做法 |
|---|---|---|
0.1 + 0.2 !== 0.3 |
IEEE 754 浮点误差 | Math.abs(a - b) < Number.EPSILON或用整数运算: (1+2)/10 |
typeof null === 'object' |
历史 Bug,无法修复 | val === null 直接判断 |
NaN !== NaN |
IEEE 754 规定 NaN 不自等 | Number.isNaN(val) |
this 指向丢失 |
函数内 this 取决于调用方式 | 箭头函数捕获外层 this; 或 .bind(this) |
for in 遍历原型属性 |
for-in 会枚举原型链上可枚举属性 | for (const k of Object.keys(obj))或加 hasOwnProperty 判断 |
sort() 按字符串排序 |
默认比较的是字符串形式 | [10,2,1].sort((a,b) => a - b) |
new Array(n) 稀疏数组 |
只有 length,没有元素 | Array.from({ length: n }, (_,i) => i) |
var 变量提升 & 函数作用域 |
var 没有块级作用域 | 始终用 const (不变) 或 let (可变) |
parseInt 不带基数 |
parseInt('08') 在老引擎当作八进制 |
parseInt('08', 10) |
| 数组/对象是引用 | const a = [1]; const b = a; 共享引用 |
浅拷贝: [...a]/{...obj}; 深拷贝: structuredClone |
现代最佳实践清单
// 1. 始终用 const/let,永远不用 var // 2. === 代替 ==,除非明确需要类型转换 // 3. 使用可选链 ?. 和空值合并 ?? 安全访问 // 4. 箭头函数用于回调; 普通函数/full function 声明用于方法 // 5. 异步操作用 async/await,避免回调地狱 // 6. try/catch 包裹 await,不要让 unhandled rejection 逃逸 // 7. 解构赋值提高可读性 // 8. 模板字面量代替 + 拼接 // 9. for...of 遍历数组,for...in 只用于对象(且加 hasOwnProperty) // 10. 启用严格模式: 文件开头 'use strict'; 或 ESM 自动严格
6.7 前端开发速查
6.7.1 DOM 操作速查
| 操作 | API |
|---|---|
| 按 ID 获取 | document.getElementById('hcj-id') |
| 按选择器获取 | document.querySelector('.class') / querySelectorAll |
| 创建元素 | document.createElement('div') |
| 创建文本 | document.createTextNode('text') |
| 插入子节点 | parent.appendChild(child) / parent.insertBefore(new, ref) |
| 移除节点 | el.remove() / parent.removeChild(child) |
| 替换节点 | parent.replaceChild(new, old) |
| 克隆节点 | el.cloneNode(true) (true=深拷贝) |
| 获取/设置 HTML | el.innerHTML / el.outerHTML |
| 获取/设置文本 | el.textContent / el.innerText |
| 属性操作 | el.getAttribute('href') / setAttribute / removeAttribute |
| data-* 属性 | el.dataset.key |
| class 操作 | el.classList.add/remove/toggle/contains('cls') |
| 样式操作 | el.style.color = 'red'; 读取计算样式: getComputedStyle(el).color |
DOM 遍历
el.parentElement // 父元素
el.children // 子元素 HTMLCollection
el.firstElementChild // 第一个子元素
el.lastElementChild
el.previousElementSibling / nextElementSibling
el.closest('.container') // 向上查找最近的匹配祖先
高效的 DOM 批量更新
// Fragment — 减少回流
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
fragment.appendChild(li);
}
ul.appendChild(fragment); // 一次性插入,只触发一次回流
6.7.2 事件处理速查
| 操作 | API |
|---|---|
| 绑定事件 | el.addEventListener('click', handler) |
| 解绑事件 | el.removeEventListener('click', handler) |
| 一次性绑定 | el.addEventListener('click', fn, { once: true }) |
| 被动监听 (提升滚动性能) | el.addEventListener('scroll', fn, { passive: true }) |
| 阻止默认行为 | event.preventDefault() |
| 阻止冒泡 | event.stopPropagation() |
| 事件委托 | 在父元素监听,通过 event.target 判断来源 |
| 自定义事件 | el.dispatchEvent(new CustomEvent('name', { detail: {...} })) |
常用事件类型
// 鼠标: click, dblclick, mousedown, mouseup, mousemove, mouseenter, mouseleave // 键盘: keydown, keyup, keypress (已废弃) // 表单: submit, change, input, focus, blur // 文档: DOMContentLoaded, load, beforeunload // 视口: scroll, resize // 触摸: touchstart, touchmove, touchend
事件委托模式
// 在父容器监听,避免给每个子元素都绑事件
document.getElementById('list').addEventListener('click', (e) => {
if (e.target.matches('.item')) {
console.log('clicked:', e.target.textContent);
}
// 更精确: e.target.closest('.item')
});
6.7.3 Fetch / 网络请求速查
| 操作 | 代码 |
|---|---|
| GET | fetch('/api/data').then(r => r.json()) |
| POST JSON | fetch('/api/data', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(data) }) |
| POST FormData | fetch('/upload', { method:'POST', body: new FormData(form) }) |
| 超时控制 | Promise.race([fetch(url), timeout(5000)]) |
| 中止请求 | const ctrl = new AbortController(); fetch(url, { signal: ctrl.signal }); ctrl.abort(); |
| 流式读取 | const reader = response.body.getReader(); |
async/await + 错误处理
async function getData(url) {
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
console.error('fetch failed:', err);
throw err;
}
}
请求拦截器模式
async function fetchWithRetry(url, options = {}, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const res = await fetch(url, options);
if (res.ok) return await res.json();
if (res.status >= 500 && i < retries - 1) continue; // 服务端错误重试
throw new Error(`HTTP ${res.status}`);
} catch (err) {
if (i === retries - 1) throw err;
await new Promise(r => setTimeout(r, 1000 * (i + 1)));
}
}
}
6.7.4 浏览器存储速查
| 存储类型 | 容量 | 持久性 | 作用域 | API |
|---|---|---|---|---|
localStorage | ~5MB | 永久保留 | 同源 | getItem/setItem/removeItem/clear |
sessionStorage | ~5MB | 标签页关闭即清 | 同源同标签 | 同上 |
cookie | ~4KB | 可设过期时间 | 同源+路径 | document.cookie |
IndexedDB | > 50MB | 永久保留 | 同源 | 异步事务式 NoSQL |
Cache API | 弹性 | 永久保留 | 同源 | Service Worker 缓存 |
// localStorage (同步,只能存字符串)
localStorage.setItem('key', JSON.stringify(obj));
const data = JSON.parse(localStorage.getItem('key'));
localStorage.removeItem('key');
// sessionStorage API 相同
sessionStorage.setItem('token', 'abc123');
// cookie 操作
document.cookie = 'name=value; max-age=3600; path=/; SameSite=Lax';
// 读取: document.cookie 返回所有 cookie 字符串
// IndexedDB 简单封装
function openDB(name, version = 1) {
return new Promise((resolve, reject) => {
const req = indexedDB.open(name, version);
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
req.onupgradeneeded = (e) => {
e.target.result.createObjectStore('store', { keyPath: 'id' });
};
});
}
6.7.5 定时器 & 动画速查
| API | 说明 | 最小间隔 |
|---|---|---|
setTimeout(fn, ms) | 延迟执行一次 | ~4ms (嵌套 >5 层) |
setInterval(fn, ms) | 重复执行 | ~4ms |
clearTimeout/clearInterval | 取消定时器 | |
requestAnimationFrame(fn) | 下一帧渲染前执行 | ~16.7ms (60fps) |
requestIdleCallback(fn) | 浏览器空闲时执行 | 不固定 |
setImmediate(fn) | Node.js 专用,下一个 tick |
raf 动画循环
function animate() {
// 更新动画状态
el.style.transform = `translateX(${x}px)`;
x += 2;
if (x < 300) requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
定时器 + setInterval 的坑
// 坏: 如果 fn 执行时间 > 间隔,会堆积回调
setInterval(fn, 100);
// 好: 用 setTimeout 递归模拟精确间隔
function safeInterval(fn, ms) {
let stopped = false;
async function loop() {
if (stopped) return;
const start = Date.now();
await fn();
const elapsed = Date.now() - start;
setTimeout(loop, Math.max(0, ms - elapsed));
}
setTimeout(loop, ms);
return () => { stopped = true; }; // 返回停止函数
}
6.7.6 BOM (浏览器对象模型) 速查
| 对象 | 常用属性/方法 | 说明 |
|---|---|---|
window | innerWidth / innerHeight | 视口尺寸 (含滚动条) |
scrollX / scrollY | 页面滚动偏移 | |
scrollTo(x, y) / scrollBy(dx, dy) | 滚动; 平滑: { behavior: 'smooth' } | |
open(url) / close() | 打开/关闭窗口 (受弹窗拦截器限制) | |
navigator | userAgent | UA 字符串 (不可靠,推荐 feature detection) |
language / languages | 浏览器语言偏好 | |
onLine | 是否联网 (不可靠) | |
clipboard.writeText(s) | 写入剪贴板 (需用户手势或 HTTPS) | |
screen | width / height | 屏幕分辨率 |
availWidth / availHeight | 可用区域 (减去任务栏) | |
location | href | 完整 URL; 赋值即跳转 |
protocol / host / hostname / port / pathname / search / hash | URL 各组成部分 | |
reload() | 刷新页面 | |
replace(url) | 替换当前历史记录 (不可后退) | |
assign(url) | 跳转 (可后退) | |
history | back() / forward() / go(n) | 历史导航 |
pushState(state, '', url) | 新增一条历史记录 (SPA 路由核心) | |
replaceState(state, '', url) | 替换当前历史记录 | |
window.onpopstate | 前进/后退时触发 | |
| 弹窗 | alert('msg') | 同步阻塞,不推荐生产使用 |
confirm('msg') | 返回 true/false | |
prompt('msg', 'default') | 返回输入字符串或 null |
SPA 路由基础
// 切换到 /page2,不刷新页面
history.pushState({ page: 2 }, '', '/page2');
// 监听浏览器前进/后退
window.addEventListener('popstate', (e) => {
console.log('location changed to:', location.pathname);
console.log('state:', e.state); // { page: 2 }
});
// URL 解析
const url = new URL('https://example.com/page?a=1&b=2#section');
url.searchParams.get('a'); // '1'
url.searchParams.set('c', '3'); // 修改参数
url.searchParams.toString(); // 'a=1&b=2&c=3'
url.hash; // '#section'
6.8 TC39 提案流程
五阶段流程(Stage 0-4)
| Stage | 名称 | 含义 |
|---|---|---|
| 0 | Strawman | 想法阶段,可以提交到 TC39 讨论 |
| 1 | Proposal | 正式提案,需要 champion 推动,描述问题和解决方案 |
| 2 | Draft | 初稿,有精确语法和语义的规范文本 |
| 3 | Candidate | 候选,规范完成,等待浏览器实现和反馈 |
| 4 | Finished | 完成,通过 Test262 测试,正式成为 ECMAScript 标准 |
6.9 参考资源
- ECMA-262 规范:tc39.es/ecma262
- ECMA-402 国际化:tc39.es/ecma402
- MDN JavaScript:developer.mozilla.org
- V8 引擎文档:v8.dev/docs
- TC39 Proposals:github.com/tc39/proposals
- Node.js:nodejs.org
- Deno:deno.com
- HTML Event Loop Spec:html.spec.whatwg.org
第六部分:TypeScript 系统
TypeScript 是微软开发的静态类型、强类型编程语言,构建于 JavaScript 之上,由 Anders Hejlsberg(C# 之父)主导设计,2012 年首次发布。
TypeScript 是 JavaScript 的超集:所有合法的 JS 代码都是合法的 TS 代码。TypeScript 编译器将 TS 代码编译为纯 JavaScript,且类型注解在编译后被完全擦除,不影响运行时行为。其类型系统为结构化类型系统(Structural Type System),即"鸭子类型"。
| 层面 | 核心内容 |
|---|---|
| 词法/语法 | JS 超集语法 + 类型注解(: Type)、接口、泛型、枚举、装饰器、JSX |
| 类型系统 | 结构化类型、类型推断、控制流分析、类型窄化、泛型、条件类型、映射类型、模板字面量类型 |
| 编译管线 | Scanner → Parser → Binder → Checker → Emitter(类型擦除 + downleveling) |
| 语言服务 | tsserver:代码补全、错误诊断、快速修复、重构、导航;Language Service API |
| 配置系统 | tsconfig.json:target、module、strict、paths、project references 等 100+ 编译选项 |
| 生态 | DefinitelyTyped(@types/*)、tsc CLI、ts-node、Babel/Webpack 集成、ESLint 规则 |
7.1 概述
TypeScript 的设计目标(来自官方 wiki)包括:
- 对 JavaScript 程序的静态类型检查,提供可选的类型系统
- 编译输出符合规范的、可读的 JavaScript
- 兼容现有 JavaScript 库 — 通过声明文件(.d.ts)描述类型
- 不引入运行时开销 — 类型在编译后被擦除,输出纯 JS
- 提供一流的编辑工具支持 — tsserver、Language Service API
TypeScript 7.0 于 2026 年发布,核心编译器正逐步迁移至 Go 语言实现(typescript-go)以实现原生性能。当前 npm 版本(typescript 包)仍为 TypeScript 实现。
7.2 行为图
7.2.1 用例图 — 使用场景
7.2.2 活动图 — 编译流程
7.2.3 时序图 — 编辑器交互(tsserver)
7.2.4 时序图 — 类型检查与推断
7.3 结构图
7.3.1 组件图 — 编译器架构
7.3.2 包图 — 核心模块组织
7.3.3 类图 — 类型系统层次
7.4 使用手册
7.4.1 安装与配置
# 全局安装编译器
npm install -g typescript
# 项目本地安装
npm install -D typescript
# 初始化 tsconfig.json
tsc --init
# 编译
tsc # 使用 tsconfig.json
tsc src/index.ts # 编译单个文件
tsc --watch # 监视模式
核心编译选项:
{
"compilerOptions": {
"target": "ES2022", // 输出 JS 版本
"module": "NodeNext", // 模块系统
"strict": true, // 严格模式(推荐)
"outDir": "./dist", // 输出目录
"rootDir": "./src", // 源码根目录
"declaration": true, // 生成 .d.ts
"sourceMap": true, // 生成 source map
"esModuleInterop": true, // CJS/ESM 互操作
"skipLibCheck": true // 跳过声明文件类型检查
}
}
7.4.2 基础类型
// 基本类型注解
let isDone: boolean = false;
let count: number = 42;
let name: string = "TypeScript";
let list: number[] = [1, 2, 3];
let tuple: [string, number] = ["hello", 10];
// 联合类型
type Status = "idle" | "loading" | "success" | "error";
// 枚举
enum Color { Red, Green, Blue }
enum Direction { Up = "UP", Down = "DOWN" }
// 特殊类型
let notSure: unknown = 4; // 安全版的 any
let unusable: void = undefined;
function error(): never { throw new Error(); }
// 类型推断(自动推导类型)
let msg = "hello"; // msg: string
7.4.3 接口与类型别名
// Interface(首选,支持声明合并)
interface User {
readonly id: number;
name: string;
email?: string; // 可选属性
[key: string]: unknown; // 索引签名
}
// Type Alias(需要联合/交叉/元组等特性时使用)
type Point = { x: number; y: number };
type ID = string | number;
type PartialUser = Partial<User>;
// 接口继承
interface Admin extends User {
role: "admin" | "superadmin";
permissions: string[];
}
// 交叉类型
type ColoredPoint = Point & { color: string };
7.4.4 函数类型
// 函数声明
function add(a: number, b: number): number {
return a + b;
}
// 函数类型表达式
type MathFn = (x: number, y: number) => number;
const multiply: MathFn = (x, y) => x * y;
// 可选参数与默认值
function greet(name: string, greeting?: string): string {
return `${greeting || "Hello"}, ${name}`;
}
// 剩余参数
function sum(...nums: number[]): number {
return nums.reduce((a, b) => a + b, 0);
}
// 函数重载
function makeDate(timestamp: number): Date;
function makeDate(y: number, m: number, d: number): Date;
function makeDate(yOrTimestamp: number, m?: number, d?: number): Date {
if (m !== undefined && d !== undefined) {
return new Date(yOrTimestamp, m, d);
}
return new Date(yOrTimestamp);
}
// 泛型函数
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
7.4.5 泛型
// 泛型接口
interface Repository<T> {
getById(id: string): T;
getAll(): T[];
create(item: Omit<T, "id">): T;
}
// 泛型约束(extends)
interface HasLength { length: number; }
function logLength<T extends HasLength>(arg: T): T {
console.log(arg.length);
return arg;
}
// keyof + 泛型约束
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
// 泛型默认值
function create<T = string>(value?: T): T[] {
return value ? [value] : [];
}
// 条件类型 + infer
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type UnpackPromise<T> = T extends Promise<infer U> ? U : T;
7.4.6 高级类型操作
// 映射类型
type Readonly<T> = { readonly [P in keyof T]: T[P] };
type Optional<T> = { [P in keyof T]?: T[P] };
// 条件类型
type IsString<T> = T extends string ? true : false;
type NonNullable<T> = T extends null | undefined ? never : T;
// 模板字面量类型
type EventName<T extends string> = `on${Capitalize<T>}`;
type MouseEvents = EventName<"click" | "hover" | "drag">;
// "onClick" | "onHover" | "onDrag"
// 常用内置 Utility Types
type Todo = { title: string; desc: string; done: boolean };
type PartialTodo = Partial<Todo>; // 全部可选
type RequiredTodo = Required<Todo>; // 全部必填
type ReadonlyTodo = Readonly<Todo>; // 全部只读
type Picked = Pick<Todo, "title" | "done">; // 选取
type Omitted = Omit<Todo, "desc">; // 排除
type FromUnion = Extract<"a"|"b"|"c", "a"|"c">; // "a"|"c"
type Excluded = Exclude<"a"|"b"|"c", "a">; // "b"|"c"
type Params = Parameters<typeof add>; // 参数元组类型
7.4.7 模块系统
// ES Module(推荐)
export interface Config { debug: boolean }
export const VERSION = "1.0";
export default class App {}
import App, { Config, VERSION } from "./app";
// CommonJS 互操作
import fs = require("fs"); // CJS 导入
export = myLib; // CJS 导出
// 动态导入(返回 Promise)
const module = await import("./dynamic-module");
// 类型导入(仅编译时,不产生运行时导入)
import type { User } from "./types";
import { type User, createUser } from "./user";
7.4.8 声明文件
// .d.ts — 为 JS 库提供类型描述
declare module "my-lib" {
export function doSomething(x: number): string;
export const version: string;
}
// 全局声明
declare const APP_VERSION: string;
declare function initialize(config: Config): void;
// 扩展已有模块的类型(Declaration Merging)
interface String {
customMethod(): void;
}
// 扩展第三方模块
declare module "express" {
interface Request {
user?: User;
}
}
7.4.9 tsconfig 核心配置
{
"compilerOptions": {
/* 严格模式(推荐全部开启) */
"strict": true, // 开启所有严格检查
"noImplicitAny": true, // 禁止隐式 any
"strictNullChecks": true, // null/undefined 严格检查
"strictFunctionTypes": true, // 函数类型严格逆变
"noUnusedLocals": true, // 禁止未使用的局部变量
"noUnusedParameters": true, // 禁止未使用的参数
"exactOptionalPropertyTypes": true, // 精确可选属性
/* 模块解析 */
"moduleResolution": "bundler", // bundler/node16/nodenext
"baseUrl": ".", // 非相对模块导入基准
"paths": { "@/*": ["./src/*"] }, // 路径别名
/* 输出控制 */
"noEmit": true, // 仅类型检查,不产生输出
"noEmitOnError": true, // 类型错误时不产生输出
"declarationMap": true, // 声明文件 source map
/* 其他 */
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true, // 允许导入 JSON
"isolatedModules": true // 确保单文件编译安全
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"],
"references": [ // 项目引用(monorepo)
{ "path": "./packages/core" },
{ "path": "./packages/utils" }
]
}
7.4.10 工具链
| 工具 | 作用 |
|---|---|
tsc | TypeScript 官方编译器 |
ts-node | 直接执行 TypeScript 文件(开发用) |
tsx | 更快的 ts-node 替代(基于 esbuild) |
tsup | 基于 esbuild 的 TS 打包工具 |
vite | 内置 TS 支持的构建工具 |
esbuild / swc | 高速 TS 转译(不做类型检查) |
ts-jest / vitest | 支持 TS 的测试框架 |
tsc --noEmit | 仅类型检查(CI 使用) |
dts-bundle-generator | 合并 .d.ts 声明文件 |
Vue.js 生态系统系统拆解
Vue.js 是一个渐进式 JavaScript 框架,用于构建用户界面。Vue 3 由三大核心子系统组成:响应式系统(基于 Proxy 的依赖追踪)、编译器(模板→渲染函数)和渲染器(VDOM diff/patch)。生态包含 Vue Router(路由)、Pinia(状态管理)、Vite(构建工具)等。
Vue 同时对模板进行编译时静态分析 和 运行时 VDOM 更新。编译器留下优化提示(patch flags、静态提升、树展平),运行时据此走快速路径跳过静态内容。这使得 Vue 在保持声明式模板开发体验的同时,获得了接近手写 vanilla JS 的性能。
1. 系统概览
1.1 渲染管线总览
模板 → 编译 → 渲染函数 → VNode 树 → 挂载 → 真实 DOM
↑ │
└── 补丁(diff + 更新) ←── 响应式效应触发
1.2 技术栈一览
| 子系统 | 仓库 | 核心职责 |
|---|---|---|
| Vue Core | vuejs/core | 响应式、编译器、渲染器、组件系统 |
| Vue Router | vuejs/router | 客户端路由,URL ↔ 组件映射 |
| Pinia | vuejs/pinia | 全局状态管理(替代 Vuex) |
| Vite | vitejs/vite | 开发服务器 + 生产构建(Rolldown + Oxc) |
2. 行为图
先梳理 Vue.js 系统"做什么"——从整体用例到关键动态流程。
2.1 用例图
从开发者视角描述 Vue.js 生态提供的核心功能:创建应用、构建组件、配置路由、管理状态、构建部署。
2.2 活动图 — 渲染管线
描述从写模板到浏览器 DOM 更新的完整渲染流程。带泳道区分编译时和运行时两个阶段。
2.3 状态机图 — 组件生命周期
Vue 组件从创建、挂载、更新到卸载的完整生命周期状态迁移。
2.4 序列图 — 响应式更新链路
当开发者修改 ref 值后,从依赖追踪到 VDOM diff 再到 DOM 更新的完整时序。
3. 结构图
从宏观架构到内部细节,逐步拆解 Vue.js 系统的静态组成。
3.1 组件图 — 生态全景
展示 Vue.js 生态系统各组件之间的依赖和接口关系。
3.2 包图 — 核心包结构
Vue 3 core 内部的模块划分及依赖关系。
3.3 类图 — 核心 API
响应式系统、组件系统和路由系统的核心 API 及类型关系。
3.4 部署图
Vue 应用的开发、构建和上线部署拓扑。
4. 使用手册
<script setup lang="ts">(组合式 API + TypeScript),是当前 Vue 3 推荐的主流写法。
4.1 快速起步:从零到第一个页面
项目初始化
npm create vite@latest my-app -- --template vue-ts cd my-app npm install npm run dev # 打开 http://localhost:5173
生成的标准目录结构:
my-app/
├── index.html # Vite 入口(根 div#app)
├── vite.config.ts # Vite 配置
├── tsconfig.json
├── package.json
└── src/
├── main.ts # 应用入口:createApp(...).mount('#app')
├── App.vue # 根组件
├── components/ # 公共组件
├── views/ # 页面级组件
├── router/ # 路由配置(手动添加)
├── stores/ # Pinia store(手动添加)
├── api/ # HTTP 请求封装(手动添加)
└── assets/ # 静态资源(图片、CSS)
入口文件 src/main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import './assets/main.css'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')
index.html 唯一要改的地方
<body> <div id="app"></div> <script type="module" src="/src/main.ts"></script> </body>
4.2 核心开发模式
4.2.1 ref vs reactive:什么时候用什么
| 场景 | 用 | 原因 |
|---|---|---|
| 基础类型(string / number / boolean) | ref() | reactive 不支持原始值 |
| 对象/数组(本地状态) | ref() | 可整体替换,解构不丢失响应性 |
| 表单对象(字段多且固定结构) | reactive() | 模板中免 .value,读写像普通对象 |
toRefs)、可整体替换、不存在 reactive 的限制(不可替换、不可解构丢失响应性)。仅在明确需要模板中省略 .value 时使用 reactive。
// ref:模板/script 里统一 .value 访问(模板中自动解包顶层 ref)
const count = ref(0)
const user = ref({ name: 'Brian', role: 'CEO' })
// 读取:count.value 修改:count.value++
// 替换整个对象:user.value = newUser ✅
// 解构后仍响应:const { name } = toRefs(user.value) ✅
// reactive:模板中直接用,但限制多
const form = reactive({ username: '', password: '' })
// 不可替换:form = newForm ❌(断开响应性)
// 不可解构:const { username } = form ❌(失去响应性)
4.2.2 computed:派生状态自动推导
const items = ref([{ price: 10, qty: 2 }, { price: 30, qty: 1 }])
const total = computed(() =>
items.value.reduce((sum, i) => sum + i.price * i.qty, 0)
)
// total 是只读 ref,依赖变化时自动重算、有缓存
4.2.3 watch vs watchEffect:何时用哪个
// watch:需要"前后值对比"时用
watch(count, (newVal, oldVal) => {
if (newVal > 100) alert('超出上限')
})
// watchEffect:不在乎旧值,自动追踪依赖
watchEffect(() => {
localStorage.setItem('count', String(count.value))
})
// 自动追踪 count.value,立即执行一次,依赖变自动重跑
4.2.4 v-model 双向绑定
<!-- 等价于 :value="text" @input="text = $event.target.value" --> <input v-model="text" /> <!-- 修饰符:.lazy(change 触发).number(转数字).trim --> <input v-model.lazy.trim="text" />
4.2.5 v-if vs v-show
| v-if | v-show | |
|---|---|---|
| 机制 | DOM 创建/销毁 | CSS display:none 切换 |
| 初始开销 | 低(不渲染) | 高(总是渲染) |
| 切换开销 | 高(重建 DOM) | 低(仅改样式) |
| 适用 | 条件很少变化 | 频繁切换(tab、弹窗) |
4.2.6 v-for 必须带 key
<!-- bad: 没有 key,diff 可能错位 -->
<li v-for="item in list">{{ item.name }}</li>
<!-- good: 用唯一 id 做 key,不用 index -->
<li v-for="item in list" :key="item.id">{{ item.name }}</li>
4.3 组件实战:父子通信
场景:父组件 → 子组件(Props)
// ── 子组件 ChildCard.vue ──
<script setup lang="ts">
interface Props {
title: string
count?: number // ? = 可选
}
const props = withDefaults(defineProps<Props>(), {
count: 0 // 默认值
})
</script>
<template>
<div class="card">
<h3>{{ props.title }}</h3>
<span>{{ props.count }}</span>
</div>
</template>
// ── 父组件 ──
<ChildCard title="总览" :count="total" />
场景:子组件 → 父组件(Emits)
// ── 子组件 ConfirmDialog.vue ──
<script setup lang="ts">
const emit = defineEmits<{
confirm: [id: number]
cancel: []
}>()
</script>
<template>
<button @click="emit('confirm', 42)">确定</button>
<button @click="emit('cancel')">取消</button>
</template>
// ── 父组件 ──
<ConfirmDialog @confirm="handleConfirm" @cancel="show = false" />
场景:插槽(Slot)—— 父向子传递模板内容
// ── 子组件 Layout.vue ──
<template>
<header>
<slot name="header" />
</header>
<main>
<slot /> <!-- 默认插槽 -->
</main>
</template>
// ── 父组件 ──
<Layout>
<template #header>
<h1>页面标题</h1>
</template>
<p>这是主体内容</p>
</Layout>
场景:跨层级传递(provide / inject)
// ── 祖先组件 ──
import { provide, ref } from 'vue'
const theme = ref('light')
provide('theme', theme) // 也可 provide readonly(theme)
// ── 深层子组件 ──
import { inject } from 'vue'
const theme = inject('theme') // 类型自动推导
4.4 路由实战
典型项目结构
src/router/index.ts # 路由配置 src/views/ # 页面组件(LoginView.vue, HomeView.vue) src/layouts/ # 布局组件(含 <RouterView />)
路由配置(含懒加载 + 鉴权 + 404)
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
component: () => import('@/layouts/DefaultLayout.vue'),
children: [
{ path: '', name: 'home', component: () => import('@/views/HomeView.vue') },
{
path: 'admin',
meta: { requiresAuth: true, role: 'admin' },
component: () => import('@/views/AdminView.vue'),
},
],
},
{ path: '/login', component: () => import('@/views/LoginView.vue') },
{ path: '/:pathMatch(.*)*', redirect: '/' }, // 404 fallback
],
})
// 全局前置守卫:鉴权
router.beforeEach((to) => {
const token = localStorage.getItem('token')
if (to.meta.requiresAuth && !token) return '/login'
})
页面中获取路由参数、跳转
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
// 读当前路由
console.log(route.path) // /user/123
console.log(route.params.id) // 123 (path: '/user/:id')
console.log(route.query.search) // 'vue' (?search=vue)
// 跳转
router.push({ name: 'home' })
router.push({ path: '/user/42', query: { tab: 'profile' } })
router.replace('/login') // 替换历史记录
router.go(-1) // 后退
4.5 状态管理实战(Pinia)
组合式风格(推荐,写法与组件 setup 一致)
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { apiLogin, apiGetUserInfo } from '@/api/user'
export const useUserStore = defineStore('user', () => {
const token = ref(localStorage.getItem('token') || '')
const userInfo = ref(null)
const isLoggedIn = computed(() => !!token.value)
async function login(username: string, password: string) {
const res = await apiLogin({ username, password })
token.value = res.token
localStorage.setItem('token', res.token)
}
async function fetchUserInfo() {
userInfo.value = await apiGetUserInfo()
}
function logout() {
token.value = ''
userInfo.value = null
localStorage.removeItem('token')
}
return { token, userInfo, isLoggedIn, login, fetchUserInfo, logout }
})
组件中使用
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// 读状态
console.log(userStore.isLoggedIn) // computed,模板中直接 {{ userStore.isLoggedIn }}
// 调 action
await userStore.login('brian', 'xxx')
// 批量修改
userStore.$patch({ token: 'newtoken' })
4.6 逻辑复用:Composable 函数
Composable 是 Vue 3 的代码复用核心机制——把有状态逻辑封装成可复用的函数。组件间共享逻辑,不再是 mixin 的"来源不明",而是显式 import。
示例:封装鼠标位置追踪
// composables/useMouse.ts
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(e: MouseEvent) {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
// 组件中使用
const { x, y } = useMouse()
示例:封装异步数据请求
// composables/useFetch.ts
import { ref, watchEffect } from 'vue'
export function useFetch<T>(url: string) {
const data = ref<T | null>(null)
const loading = ref(false)
const error = ref<string | null>(null)
async function execute() {
loading.value = true; error.value = null
try {
data.value = await fetch(url).then(r => r.json())
} catch (e) {
error.value = (e as Error).message
} finally {
loading.value = false
}
}
execute()
return { data, loading, error }
}
// 组件中使用
const { data: users, loading, error } = useFetch<User[]>('/api/users')
4.7 常见踩坑与最佳实践
坑 1:reactive 整体替换会断开响应性
const state = reactive({ count: 0 })
state = { count: 1 } // ❌ 断开了!
// ✅ 方案一:用 ref 替代
const state = ref({ count: 0 })
state.value = { count: 1 }
// ✅ 方案二:用 Object.assign 就地更新
Object.assign(state, { count: 1 })
坑 2:reactive 解构后丢失响应性
const state = reactive({ a: 1, b: 2 })
const { a, b } = state // ❌ a, b 失去响应
// ✅ 用 toRefs
const { a, b } = toRefs(state) // a, b 现在是 Ref<number>
坑 3:模板 ref 在 v-if 内部获取不到
// ❌ v-if 为 false 时 ref 是 null <input v-if="show" ref="inputRef" /> // ✅ 用 v-show(DOM 始终存在但隐藏) <input v-show="show" ref="inputRef" /> // ✅ 或用 nextTick 在显示后访问 show.value = true await nextTick() inputRef.value?.focus()
坑 4:watch 默认不深度监听
const obj = ref({ a: { b: 1 } })
// ❌ 浅监听:obj.a.b = 2 不会触发
watch(obj, callback)
// ✅ 加 { deep: true }
watch(obj, callback, { deep: true })
// ✅ 或用 computed 返回具体值来精确监听
watch(() => obj.value.a.b, callback)
坑 5:异步组件加载状态
import { defineAsyncComponent } from 'vue'
const HeavyChart = defineAsyncComponent({
loader: () => import('./HeavyChart.vue'),
loadingComponent: LoadingSpinner, // 加载中显示
errorComponent: ErrorFallback, // 加载失败显示
delay: 200, // 延迟 200ms 才显示 loading
timeout: 10000, // 10s 超时
})
最佳实践速查
- 状态首选 ref:统一
.value访问模式,避免 reactive 的限制 - computed 优于 watch:能推导的不要侦听。watch 仅用于副作用(localStorage、发请求、操作 DOM)
- v-for 必须带 key:用唯一 ID,不要用 index
- 路由懒加载:
() => import()按页面拆分 chunk,首屏更快 - TypeScript:组件 props/emits 用泛型:
defineProps<{...}>()、defineEmits<{...}>() - 环境变量:
import.meta.env.VITE_API_BASE(Vite 前缀 VITE_ 才暴露给客户端) - 清理副作用:watch / watchEffect 返回的 stop 函数在 onUnmounted 时调用,或用
onWatcherCleanup() - 生产优化:Vite build 默认 tree-shaking、CSS code-split、async chunk 预加载,一般不需要额外配置
Vite 常用配置速查
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
resolve: { alias: { '@': '/src' } },
server: { port: 3000, proxy: { '/api': 'http://localhost:8080' } },
build: { outDir: 'dist', cssCodeSplit: true },
})
Vite 系统架构梳理
来源: vitejs.dev · github.com/vitejs/vite · 版本: v8.1.5 · 采用 Rust 统一工具链 (Oxc + Rolldown)
1. 系统概述
Vite(法语 "快",发音 /viːt/)是一个下一代前端构建工具,由两大部分组成:
- 开发服务器(Dev Server):基于原生 ES 模块 (ESM),提供极速冷启动和毫秒级 HMR
- 生产构建(Build):使用 Rust 原生工具链 Rolldown + Oxc 进行打包优化
Vite 的核心理念:开发时用原生 ESM 按需编译,生产时用 Rust 工具链打包优化。传统 bundler 需要先打包整个应用才能启动 dev server,Vite 则将依赖预打包后在浏览器中按需加载源码。
Vite v6+ 采用了统一的 Rust 工具链:Oxc(解析/转换/压缩)+ Rolldown(打包),替代了原先的 esbuild(dev 编译)+ Rollup(生产打包)双管线架构,消除了一致性问题并大幅提升性能。
第一部分:行为图(系统"做什么")
2.1 用例图 — 功能全景
Vite 面向三类角色的功能全景:开发者(日常开发)、插件作者(扩展系统)、框架作者(基于 Vite 构建应用框架)。
图 2.1-1: Vite 用例图
2.2 活动图 — Dev Server 启动流程
Vite 开发服务器启动的核心路径:扫描依赖 → 预打包 → 启动 HTTP + WebSocket → 就绪。整个流程通常在 1 秒内完成。
图 2.2-1: Dev Server 冷启动活动图
2.3 活动图 — 生产构建流程
Vite 的生产构建使用 Rolldown 作为打包引擎,Oxc 负责解析/转换/压缩,输出针对现代浏览器的优化产物。
图 2.3-1: 生产构建活动图
2.4 时序图 — 生产构建全流程
vite build 命令的执行是一个多阶段流水线:Rolldown 解析入口 → Oxc 逐模块转换 → Tree Shaking → Code Splitting → 压缩输出。展示完整的构建链:
图 2.4-1: 生产构建时序图
2.5 时序图 — HMR 热更新流程
HMR 是 Vite 开发体验的核心。当文件变更时,Vite 仅需 一次 HTTP 请求即可完成模块热替换,无需整页刷新。
图 2.4-1: HMR 热模块替换时序图
2.6 时序图 — Dev Server 请求处理管线
开发服务器的请求处理管线展示了 Vite 如何将源码(JSX/TS/CSS)转换为浏览器可执行的 ES 模块。
图 2.5-1: Dev Server 源码请求处理时序图
2.7 状态机图 — Dev Server 生命周期
图 2.6-1: Dev Server 生命周期状态机
第二部分:结构图(系统"由什么组成")
3.1 组件图 — 核心架构
Vite 核心由多个相互协作的子系统组成。开发模式下,所有源码通过 Plugin Container 的 resolveId → load → transform 管线处理后返回浏览器;生产模式下,Rolldown 统一处理全量打包。
图 3.1-1: Vite 核心组件架构图
3.2 包图 — Monorepo 结构
Vite 采用 pnpm monorepo 管理多个子包,包含核心 CLI、脚手架工具、官方插件和兼容层。
图 3.2-1: Vite Monorepo 包结构
3.3 类图 — 核心类关系
Vite 的 JavaScript API 围绕几个关键类构建,展示了插件系统、模块图和运行环境的抽象。
图 3.3-1: Vite 核心类图
3.4 部署图 — 环境拓扑
Vite 6+ 引入了 Environment API,允许框架定义多个运行时环境(client/ssr/edge/worker),每个环境拥有独立的模块解析和执行规则。
图 3.4-1: Vite 多环境部署拓扑
3.5 统一工具链演进
Vite 的工具链经历了重要的演进过程。Vite 原始架构依赖两套工具:开发用 esbuild(Go 语言编译),生产用 Rollup(JS 打包)。Vite 6+ 通过 Rolldown(Rust 打包器)和 Oxc(Rust 工具链)统一为单一线程,消除双管线不一致问题。
图 3.5-1: 工具链演进对比
- 开发和生产使用同一打包引擎 (Rolldown),行为完全一致
- Oxc 提供 Rust 原生速度的解析、转换、压缩
- Lightning CSS (Rust) 替代 PostCSS 进行 CSS 压缩
- 插件 API 基于 Rollup 规范,生态兼容性高
- 未来方向:Full Bundle Mode(开发模式也打包,减少大型项目的请求数)
第三部分:用户使用手册
4.1 项目创建
脚手架创建(推荐)
使用 create-vite 交互式创建项目,选择框架和语言:
npm create vite@latest # npm
yarn create vite # yarn
pnpm create vite # pnpm
bun create vite # bun
交互提示中选择项目名和模板。也可以直接指定模板参数跳过交互:
npm create vite@latest my-app -- --template react-ts
| 模板预设 | 说明 |
|---|---|
vanilla / vanilla-ts | 纯 JS/TS,无框架 |
vue / vue-ts | Vue 3 + 官方 Vite 插件 |
react / react-ts | React + SWC/官方插件 |
svelte / svelte-ts | Svelte |
solid / solid-ts | SolidJS |
lit / lit-ts | Lit (Web Components) |
手动安装
在已有项目中安装 Vite 作为开发依赖:
npm install -D vite
然后在项目根目录创建 index.html:
<!doctype html>
<html lang="en">
<head><meta charset="UTF-8" /></head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
4.2 项目结构
Vite 项目的核心原则:index.html 是入口,不是藏在 public/ 里的附属品。
my-vite-app/
├── index.html ← 应用入口(必须位于项目根目录)
├── package.json
├── vite.config.js ← Vite 配置文件
├── public/ ← 不经处理的静态资源(直接复制到 dist)
│ └── favicon.ico
├── src/
│ ├── main.js ← 应用入口 JS
│ ├── App.jsx
│ ├── components/
│ │ └── Header.jsx
│ ├── assets/ ← 会被处理/内联的静态资源
│ │ └── logo.svg
│ └── styles/
│ └── global.css
└── tsconfig.json ← TypeScript 配置(可选)
index.html必须在项目根目录(root),Vite 将其视为源码和模块图的一部分public/目录下的文件不会被处理,原样复制到dist/- 源码中的绝对路径以 root 为基准解析
- 支持多页面应用:只需在根目录放多个
.html文件
4.3 开发命令
package.json 脚本
脚手架创建的默认 npm scripts:
{
"scripts": {
"dev": "vite", // 启动开发服务器 (别名: vite dev, vite serve)
"build": "vite build", // 生产构建
"preview": "vite preview" // 本地预览构建产物
}
}
常用 CLI 选项
| 选项 | 说明 |
|---|---|
--port 3000 | 指定端口(默认 5173) |
--open | 自动在浏览器中打开 |
--host | 暴露到局域网(如手机测试) |
--force | 强制重新预打包依赖 |
--mode production | 指定运行模式(影响 env 变量加载) |
--outDir dist | 指定输出目录 (build 命令) |
--watch | 监听文件变化自动重新构建 (build 命令) |
--ssr entry.js | SSR 构建 (build 命令) |
查看完整 CLI 选项:npx vite --help
4.4 关键功能
TypeScript
Vite 开箱支持 .ts 文件导入,但只做 转译(transpile),不做类型检查。
// src/main.ts — 直接导入,不需要特殊配置
import { sayHello } from './utils'
console.log(sayHello('World'))
tsconfig.json 中必须设置
"isolatedModules": true,因为 Vite 使用 Oxc 单文件转译。类型检查建议在 IDE 中完成,或在构建时单独运行 tsc --noEmit。
CSS 处理
支持 .css、.scss、.less、.styl 直接导入:
import './style.css' // 自动注入到页面
import styles from './App.module.css' // CSS Modules
import cssString from './vars.css?inline' // 作为字符串导入
CSS Modules:引用 .module.css 后缀自动启用,支持类名映射:
import classes from './Button.module.css'
// classes.primary → 编译后的哈希类名
预处理器安装后即用:
npm add -D sass-embedded # .scss / .sass
npm add -D less # .less
npm add -D stylus # .styl
静态资源
导入资源返回最终部署 URL:
import imgUrl from './hero.png' // → /assets/hero.hash.png
import rawText from './data.txt?raw' // → 文本内容
import logo from './logo.svg?url' // → 显式获取 URL
小文件(默认 <4KB)自动内联为 base64。
环境变量
在源码中通过 import.meta.env 访问:
// 使用 .env 文件定义变量
// .env — 所有模式通用
// .env.development — dev 模式
// .env.production — build 模式
// 源码中访问
console.log(import.meta.env.VITE_API_URL) // 必须以 VITE_ 前缀
console.log(import.meta.env.PROD) // 内置布尔值
console.log(import.meta.env.DEV)
console.log(import.meta.env.BASE_URL)
Glob 批量导入
// 懒加载
const modules = import.meta.glob('./pages/*.tsx')
for (const path in modules) {
modules[path]().then(mod => { /* ... */ })
}
// 静态批量导入
const modules = import.meta.glob('./components/*.tsx', { eager: true })
Web Workers
// 推荐写法(标准方式)
const worker = new Worker(new URL('./worker.js', import.meta.url))
// 查询参数写法
import MyWorker from './worker.js?worker'
const worker = new MyWorker()
4.5 配置基础
Vite 配置文件名为 vite.config.js(或 .ts/.mts),位于项目根目录:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
// 公共基础路径(部署在子路径下时修改)
base: '/',
// 开发服务器配置
server: {
port: 3000,
open: true,
proxy: {
'/api': 'http://localhost:8080' // API 代理
}
},
// 构建配置
build: {
outDir: 'dist',
assetsDir: 'assets',
sourcemap: false,
},
// 路径别名
resolve: {
alias: {
'@': '/src',
},
},
// CSS 配置
css: {
modules: {
localsConvention: 'camelCase',
},
},
})
defineConfig 也支持接收一个函数,第一个参数 { mode, command } 可以区分 dev/build 环境:
export default defineConfig(({ command, mode }) => ({
plugins: [...],
define: {
__DEV__: command === 'serve',
__MODE__: JSON.stringify(mode),
},
}))
4.6 框架集成
React
npm install -D @vitejs/plugin-react
npm install react react-dom
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
})
Vue
npm install -D @vitejs/plugin-vue
npm install vue
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
})
Svelte
npm install -D @sveltejs/vite-plugin-svelte
npm install svelte
// vite.config.js
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
export default defineConfig({
plugins: [svelte()],
})
后端框架集成
Vite 也常被 Laravel、Ruby on Rails 等后端框架用作前端资产管线:
// Laravel 示例 — 使用 middleware mode
import { createServer } from 'vite'
const vite = await createServer({
server: { middlewareMode: true },
})
// 挂载到 Laravel 的 HTTP 服务器上
4.7 常见问题
修改配置后不生效
修改 vite.config.js 后,dev server 会自动重启。如果没有,尝试手动 Ctrl+C 后重新 npm run dev。
导入 CJS 依赖报错
某些 CommonJS 依赖可能需要显式加入预打包:
// vite.config.js
export default defineConfig({
optimizeDeps: {
include: ['some-cjs-package'],
},
})
浏览器控制台大量 404 请求
检查是否有未预打包的依赖被浏览器按 ESM 请求。可用 vite-plugin-inspect 查看模块解析链路。
HMR 不工作
- 确认文件在 Vite 的监听范围内(root 下的源码)
- 检查是否是
node_modules中的文件(依赖文件不会 HMR) - 尝试用
--force重启 dev server
生产构建产物过大
- 检查
build.rolldownOptions.output.codeSplitting配置 - 确认 Tree Shaking 正常工作(避免 barrel export)
- 使用
rolldown-plugin-visualizer分析 bundle 组成
与 Webpack/Rspack 区别
| 特性 | Vite | Webpack |
|---|---|---|
| 开发模式 | 原生 ESM 按需编译 | 全部打包后启动 |
| 冷启动速度 | <1s | 数十秒(大型项目) |
| HMR 速度 | 毫秒级 | 秒级 |
| 打包引擎 | Rolldown (Rust) | Webpack (JS) |
| 配置复杂度 | 低,开箱即用 | 高,需大量配置 |
| 插件生态 | Rolldown/Rollup 兼容 | 极其丰富 |
第四部分:生产部署
5.1 部署总览
Vite 的生产部署由两步组成:vite build 生成优化产物 + 将 dist/ 部署到生产环境。
生产构建命令
vite build # 标准构建
vite build --watch # 监听模式(持续构建)
vite build --mode staging # 指定模式加载 .env.staging
vite build --outDir prod-dist # 自定义输出目录
vite build --ssr src/entry-server.ts # SSR 构建
产物目录结构
dist/
├── index.html ← 入口 HTML(含 modulepreload/prefetch 指令)
├── assets/
│ ├── index-abc123.js ← 主入口 JS(带 hash)
│ ├── vendor-xyz789.js ← 拆分的第三方包(若启用 code splitting)
│ ├── chunk-*.js ← 懒加载路由拆分
│ └── index-def456.css ← CSS(带 hash)
├── images/ ← 大资源(默认 >4KB 的不内联)
│ └── hero-hash.png
└── .vite/
└── manifest.json ← 资源映射表(SSR 用)
build: {
outDir: 'dist', // 输出目录
assetsDir: 'assets', // 资源子目录
assetsInlineLimit: 4096, // 小于此值(bytes)的资源内联 base64
sourcemap: false, // 是否生成 sourcemap('hidden' 隐藏引用)
minify: 'oxc', // 压缩引擎: 'oxc' | 'esbuild' | false
cssMinify: 'lightningcss', // CSS 压缩: 'lightningcss' | 'esbuild'
target: 'esnext', // 浏览器目标(影响降级策略)
// Rolldown 扩展选项
rollupOptions: {
output: {
manualChunks: { // 手动控制代码分割
vendor: ['react', 'react-dom'],
utils: ['./src/utils'],
},
},
},
// 产物文件大小警告阈值
chunkSizeWarningLimit: 500, // KB
},
部署前本地预览
部署前强烈建议用 vite preview 在本地验证产物:
npx vite preview # 默认 port 4173
npx vite preview --port 8080 # 指定端口
npx vite preview --host 0.0.0.0 # 暴露到局域网
vite preview 仅用于本地预览构建产物,不适合直接用作生产服务器。生产环境应使用 Nginx、Caddy 等专业 Web 服务器。
5.2 静态托管(Nginx/CDN)
SPA/MPA 最常见的部署方式:vite build → 上传 dist/ 到静态服务器。
Nginx 配置(SPA)
server {
listen 80;
server_name example.com;
root /var/www/my-app/dist;
index index.html;
# 静态资源缓存(带 hash 的文件永不过期)
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# SPA fallback:所有非文件请求回退到 index.html
location / {
try_files $uri $uri/ /index.html;
}
# gzip 压缩
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml;
gzip_min_length 1000;
}
Nginx 配置(MPA 多页面)
server {
root /var/www/my-app/dist;
# 多页面:每个 .html 对应一个路由
location / {
try_files $uri $uri.html $uri/ =404;
}
}
部署到子路径
如果应用部署在 https://example.com/my-app/ 而非根路径,需设置 base:
// vite.config.js
export default defineConfig({
base: '/my-app/', // 以 / 开头和结尾
})
Nginx 对应配置:
location /my-app/ {
alias /var/www/my-app/dist/;
try_files $uri $uri/ /my-app/index.html;
}
CDN 部署
将 dist/assets/ 上传到 CDN(OSS/S3/CloudFront),其他文件部署到 Web 服务器:
// vite.config.js — 将资源引用指向 CDN
export default defineConfig({
base: 'https://cdn.example.com/',
})
5.3 SSR 部署
Vite 内置 SSR 支持,服务端渲染部署分为两步:构建 client + server bundle。
构建配置
{
"scripts": {
"build:client": "vite build --outDir dist/client",
"build:server": "vite build --ssr src/entry-server.js --outDir dist/server",
"build": "npm run build:client && npm run build:server"
}
}
SSR 服务端入口示例
// server.js (Node.js / Express)
import express from 'express'
import { createServer as createViteServer } from 'vite'
const app = express()
// 生产模式:直接加载构建产物
if (process.env.NODE_ENV === 'production') {
app.use(express.static('dist/client'))
// 加载 SSR manifest 用于资源注入
const manifest = await import('./dist/client/.vite/manifest.json')
const { render } = await import('./dist/server/entry-server.js')
// HTML 渲染逻辑...
}
// 开发模式:创建 Vite dev server(middleware mode)
if (process.env.NODE_ENV === 'development') {
const vite = await createViteServer({
server: { middlewareMode: true },
appType: 'custom',
})
app.use(vite.middlewares)
}
Edge Runtime 部署(Cloudflare Workers)
Vite 的 Environment API 原生支持 Edge/Worker 环境,无需 Node.js:
// vite.config.js — 定义 edge 环境
export default defineConfig({
environments: {
client: {
build: { outDir: 'dist/client' },
},
ssr: {
build: { outDir: 'dist/worker' },
resolve: { conditions: ['worker'] },
consumer: 'server',
},
},
})
5.4 Docker 部署
多阶段构建 Dockerfile(静态 SPA)
# Stage 1: 构建
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: 生产 Nginx
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
多阶段构建 Dockerfile(SSR Node.js)
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json .
COPY --from=builder /app/server.js .
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "server.js"]
docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "80:80"
environment:
- VITE_API_URL=https://api.example.com
restart: unless-stopped
5.5 CI/CD 集成
GitHub Actions
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- run: npm ci
- run: npm run build
- run: npm run build:ssr # 如果是 SSR
# 部署到静态托管(示例:Vercel / Netlify / OSS)
- name: Deploy to OSS
uses: some-oss-action@v1
with:
source: dist/
bucket: my-bucket
endpoint: oss-cn-shenzhen.aliyuncs.com
环境变量管理
Vite 的 env 机制在构建时静态替换(不是在运行时读取)。CI/CD 中通过不同 .env 文件区分环境:
# .env.production — 生产构建自动加载(vite build 默认 mode=production)
VITE_API_URL=https://api.prod.example.com
VITE_GA_ID=UA-XXXXXXXX-X
# .env.staging — 预发环境(vite build --mode staging)
VITE_API_URL=https://api.staging.example.com
# .env.preview — 预览模式(vite preview)
VITE_API_URL=https://api.preview.example.com
VITE_* 变量会内联到客户端代码中,绝不要在其中存放密钥、token 等敏感信息。服务端密钥应放在后端环境变量中,Vite 通过 server.proxy 转发 API 请求即可避免暴露。
构建性能优化(CI 中)
- 使用
npm ci而非npm install确保依赖版本一致且更快 - 缓存
node_modules/.vite可加速 dependenices 预打包 - 大型 monorepo 使用 Turborepo 或 Nx 增量构建
- 利用 Vite/Rolldown 的原生 Rust 速度,通常无需额外缓存策略
动效 & 动画系统
从 CSS 原生动画到 JS 动效库生态:渲染管线 · 技术分层 · 框架集成 · 选型指南
1. 概述
动效是前端「颜值金字塔」中决定观感的重要一环。本章聚焦 JS 动效库生态,与前面 HTML/CSS/JS/T 章的 CSS 基础动画(Transition / @keyframes / Scroll-driven)互补:那里讲「原生 CSS 怎么做」,这里讲「用库怎么做、选哪个、性能怎么保证」。
动效的本质:在每一帧(约 16.7ms)读取当前时间 → 计算插值 → 更新样式属性,驱动浏览器重绘。决定流畅度的关键是是否触发了布局/绘制,而非库本身。
- CSS 动画:简单交互、声明式,"不值得写 JS" 就用它。
- JS 动效库:需要时间轴、编排、缓动、交互联动时用(页面滚动、拖拽、编排式动画)。
- 3D / 复杂图形:Three.js / WebGL / Lottie。
1.1 动画 vs 视频 — 先分清两条轴
「界面里明明也有视频,凭什么说视频不是动效?」——视频确实是界面的组成部分,但它与动画属于两条不同的分类轴,容易混淆,先拆开:
组成轴:界面由什么构成
- 结构:HTML 元素与语义
- 样式:CSS 外观与布局
- 行为:JS 交互逻辑
- 媒体资源:图片 / 视频 / 音频(通过
<video>/<img>嵌入)
在这条轴上,视频是「内容素材」,和图片、文字同级。
机制轴:元素怎么被呈现
- 静态呈现:画面不动
- 动效(动画):代码/声明式实时计算,逐帧渲染
- 播放(视频):解码预录帧序列,按时间线回放
在这条轴上,动画是「表现/行为机制」,与视频的实现路径完全不同。
<video> 加淡入动画、随滚动缩放。本文「动效」章节只讲机制轴的"代码驱动怎么动",不否认视频作为内容存在于界面中。
| 维度 | 动画(Animation) | 视频(Video) |
|---|---|---|
| 本质 | 代码 / 声明式规则实时计算生成,逐帧渲染 | 预录制像素帧文件(MP4 / WebM 等),解码回放 |
| 内容 | 可参数化、可随状态变化(CSS / GSAP / rAF 驱动) | 固定不可变 |
| 交互 | 可响应 hover / 滚动 / 拖拽,可中断、可编排 | 只能播放 / 暂停 / 跳转 |
| 体积 | 近乎 0(纯代码 / CSS) | 大,需托管 + CDN + 带宽 |
| 清晰度 | 矢量缩放不糊 | 超过原始分辨率会糊 |
| 性能 | transform / opacity 走 GPU 合成,可保 60fps | 解码耗 CPU / GPU,帧率固定 |
| 典型用途 | UI 微交互、转场、加载、视差 | 实拍、录屏、叙事性内容、真实画面 |
- Lottie:AE 导出的 JSON 动画。由设计工具产出、以文件形式"播放",但运行时是矢量实时渲染,本质属动画。
- 透明通道视频:带 alpha 的 MOV / WebM,文件形式回放、内容为动效——实现上仍属「解码播放」派。
- 判断口诀:运行时计算 = 动画;解码回放 = 视频。二者可叠加(视频上叠动画、透明视频抠像进页面)。
第一部分:行为图(动效如何工作)
图 2-1: 动效渲染管线时序图 — 一帧动画从 JS 到屏幕
transform 和 opacity。它们只触发 Composite(合成)阶段,不回 Layout/Paint,能稳定跑满 60fps。动画 width / height / top / left / margin 会反复触发布局,最昂贵。
第二部分:结构图(技术分层)
图 3-1: 动效技术分层 — 从原生到 3D
| 层次 | 代表 | 适用场景 | 控制力 |
|---|---|---|---|
| 浏览器原生 | CSS transition / @keyframes、Web Animations API、rAF | 简单动效、微交互、滚动视差 | 低(声明式为主) |
| 动效库 | GSAP、Motion (Framer Motion)、Motion One、Anime.js、VueUse Motion | 编排队列、时间轴、缓动曲线、复杂交互动画 | 高(时间轴/缓动全掌控) |
| 框架集成/组件 | Framer Motion (React)、@vueuse/motion、Transition/TransitionGroup、AutoAnimate | 声明式地给组件加过渡、列表进入/离开 | 中(贴近框架语法) |
| 扩展/3D | Three.js / WebGL / Canvas 2D / Lottie(SVG) | 3D、游戏、数据可视化、导入 AE 动画 | 极高(底层图形) |
第三部分:选型手册
图 4-1: 动效选型决策
4.2 主流动效库指南
| 库 | 技术栈 | 核心优势 | 典型场景 |
|---|---|---|---|
| GSAP | 框架无关 | 业界最强时间轴(Timeline)、从容编排、性能好、生态全 | 落地页、复杂时间轴、跨元素编排 |
| Motion (Framer Motion) | React / 通用 | 声明式、Spring 物理、AnimatePresence(进出场) | React 组件微交互、页面过渡 |
| Motion One | 通用(轻量) | 基于 Web Animations API,体积小、性能优 | 追求极致体积的动效 |
| VueUse Motion | Vue 3 | 组合式 API 编写动效、与 Vue 响应式协同 | Vue 项目的声明式动效 |
| Anime.js | 框架无关 | 体积小、API 简洁、支持 SVG | 中小型动画、SVG 动效 |
| Three.js | WebGL | 3D 场景、相机、材质、渲染 | 3D 展示、数据可视化、游戏 |
| Lottie | SVG/Canvas | 导入 After Effects 动画 JSON,保真度高 | 复杂品牌动画、图标动画 |
4.3 性能与最佳实践
- 只动
transform/opacity:触发合成、避免布局抖动,是 60fps 的第一原则。 - 用
will-change提前建合成层:但要克制,过多合成层反而占内存(移动端尤甚)。 - 避免隐式动画 / 布局属性:动画
top/left/width/height会反复 Layout,改用translate/scale。 - 尊重
prefers-reduced-motion:减少动效或降级,兼顾可访问性(无障碍)。 - 用
requestAnimationFrame驱动:别用setInterval做动画,rAF 与屏幕刷新同步、节能。 - 大量元素用
transform-origin居中,配合backface-visibility提升合成性能。 - 用性能面板验证:DevTools Performance 录一段,确认没有 Layout/Paint 瓶颈。