一、七条技能
text
HTML / CSS / JavaScript
↓ 形成浏览器可执行的页面
React / Vue
↓ 用组件和状态描述 UI
Redux / Zustand / Pinia
↓ 管理跨组件、跨模块或高频状态
Next.js / Vite / Webpack
↓ 负责开发、编译、分包、SSR 与构建产物
浏览器 / HTTP / 缓存 / 渲染
↓ 决定首屏与交互性能
WebView / JSBridge / SDK
↓ 把 H5 接入客户端能力
DevTools / Whistle / ADB / 日志
↓ 证明问题发生在哪一层
测试 / 发布 / 监控 / 复盘
↓ 保证变更可交付、可验证、可沉淀
Agent / Skill / Harness
↓ 放大上述工程能力,但不能替代判断与验证
二、用户操作视角
text
用户操作
→ DOM 事件或路由
→ React/Vue 业务逻辑
→ Store 状态变化
→ 浏览器或 Native 网络请求
→ 后端响应
→ 数据清洗与状态派生
→ 组件重新渲染
→ 图片/字体等资源加载
→ 浏览器布局、绘制、合成
→ 用户最终看到结果
三、HTML5 / CSS3 / JavaScript
3.1 主线
HTML 描述内容结构,CSS 负责布局和视觉,JavaScript 负责状态、行为和异步;框架最终仍然落到 DOM、CSSOM、事件与浏览器渲染流水线。
3.2 HTML 语义化
语义化不是"少写 div",而是让元素表达内容角色:
- 页面地标:
header、nav、main、aside、footer; - 独立内容:
article; - 有主题的一组内容:
section,通常应有标题; - 操作使用
button,导航使用a,不要用可点击的div冒充; - 表单输入用
label关联控件,图片提供合适的alt; - 标题按信息层级组织,不因字号需要乱跳级。
收益是可访问性、SEO、键盘操作、默认交互语义和可维护性。需要强调:语义化不等于自动获得好 SEO;内容质量、渲染方式、元数据和可抓取性同样重要。App WebView 内部页面通常不以 SEO 为核心目标,但语义化仍有可访问性和维护价值。
3.3 盒模型、布局与响应式
盒模型:
text
content → padding → border → margin
content-box:声明宽度只计算 content;border-box:声明宽度包含 content、padding、border,业务布局通常更直观;- 全局常见设置:
*, *::before, *::after { box-sizing: border-box; }。
Flex 与 Grid:
- Flex 是一维布局,适合导航、工具栏、单方向列表与对齐;
- Grid 是二维布局,适合同时控制行列的页面骨架或卡片网格;
min-width: 0是 Flex 子项文本省略经常遗漏的点;flex: 1常等价于1 1 0%,不是简单的"宽度占满";- 收缩权重与
flex-shrink × flex-basis有关。
响应式要同时考虑:
- 视口:正确设置 viewport;
- 布局:Flex/Grid、百分比、
minmax(); - 尺寸:
rem、vw、dvh与必要的 px; - 断点:媒体查询或容器查询;
- 内容:长文本、国际化、图片比例与安全区;
- 输入方式:触摸热区、hover 不可用、键盘与无障碍;
- WebView:状态栏、安全区、软键盘、横竖屏和不同内核版本。
3.4 BFC 与层叠上下文
- BFC 主要影响块级布局、浮动、外边距合并;
- stacking context 决定 z 轴上的分组与比较范围;
z-index: 99999仍可能被遮挡,因为后代无法越过祖先层叠上下文。
案例:低代码组件弹窗被宿主组件遮挡。静态代码中弹窗已经 position: fixed; z-index: 999,运行时沿祖先链发现宿主的 position + z-index 创建了层叠上下文。最终通过 Portal 把弹窗挂到 document.body,脱离原层叠上下文。这个案例体现的是 CSS 原理、组件宿主边界和运行时取证,而不是"把 z-index 调大"。
3.5 JavaScript 主干
Event Loop 标准答法:
JavaScript 执行当前同步调用栈;栈清空后先持续清空微任务队列,再由事件循环选择下一个 Task,浏览器会在合适时机执行渲染。Promise 回调属于微任务,setTimeout 回调属于 Task。渲染不是简单地固定发生在每个宏任务之后,而由浏览器调度。
竞态要点:Promise 解决"等待异步结果",不自动保证"最后意图获胜"。排行榜项目中,用户快速切换时间和地区时,旧请求可能后返回并覆盖新结果。需要把用户意图编码进请求:
js
let latestRequestId = 0;
async function loadList(filters) {
const requestId = ++latestRequestId;
const data = await fetchList(filters);
if (requestId !== latestRequestId) return;
render(data);
}
如果底层支持 AbortController,还可以取消旧请求以节省资源;即便取消失败,响应提交前仍应校验 requestId 或筛选上下文。
四、React / Redux / Zustand / Next.js / Vue 3
4.1 React:从状态到 DOM 的完整链路
一句话:React 让开发者声明 UI = f(state);更新状态后,React 调度 render,生成新的元素树并协调差异,最后在 commit 阶段修改 DOM 和执行相关副作用。
要能区分:
- render 阶段:计算 UI,可被打断或重做,不应产生外部副作用;
- commit 阶段:提交 DOM 变更;
useEffect:在 commit 后调度,通常不会阻塞浏览器绘制,适合订阅、请求等;具体是否在绘制前后执行会受交互更新等因素影响,不应用它做必须在首帧前完成的布局修正;useLayoutEffect:DOM 提交后、浏览器绘制前同步执行,适合必须先测量再修正的布局;- 每次函数组件执行都有独立 props/state 快照,回调会通过闭包捕获某一轮快照;
- setState 是请求更新,不是原地修改当前 render 中的变量;
key表示列表项的稳定身份,不是消除 warning 的装饰。
性能 API 要说清"缓存什么"和"何时值得":
React.memo:在父组件更新时,基于 props 比较决定是否跳过子组件 render;useMemo:缓存一次计算结果或引用;useCallback:缓存函数引用;- 它们都有比较和内存成本,只有计算较重、引用稳定性影响 memo 子组件、或依赖链确实需要时才使用。
项目案例:时间轴的 currentTime 高频变化,时间轴头部只订阅 canUndo/canRedo;轨道项通过 Zustand selector、React.memo 和稳定 props 隔离渲染,拖拽位置则由 react-spring 直接更新 transform,避免每帧都走 React render/diff/commit。
4.2 Redux:可预测的集中状态
Redux 核心不是"全局变量",而是:
text
dispatch(action)
→ reducer(previousState, action)
→ newState
→ selector 读取切片
→ 订阅组件更新
适合状态关系复杂、更新需要追踪、多人协作需要明确约束的场景。避免把所有局部 UI 状态都塞入 Redux。
项目案例:首页在 getServerSideProps 中取得用户、会员和配置数据,服务端 dispatch 到 Redux;next-redux-wrapper 将快照写入 __NEXT_DATA__,浏览器恢复 store,reducer 处理 HYDRATE 合并服务端状态,再由 Provider 提供给组件树。一次性页面数据走 pageProps,需要响应式共享的数据走 Redux。
追问:为什么 reducer 要保持纯函数和不可变更新?
相同输入得到相同输出,便于回放、测试和调试;新引用也让订阅层能通过引用比较判断变化。直接修改旧对象会让变化边界不清晰,可能导致组件不更新或历史状态被污染。
4.3 Zustand:外部 Store 与精准订阅
Zustand store 可以理解为 React 外部的状态容器:内部保存状态并维护订阅者,React Hook 通过 selector 订阅切片,非 React 类可用 getState/setState。
- selector 返回基本值时,默认用
Object.is比较新旧结果; - selector 每次返回新对象时,即使字段相同也会被认为变化;
- 需要
useShallow、自定义 equality function,或分别订阅基本字段,才能获得浅比较效果; - 不应把
Object.is叫"浅比较"。
项目案例: Controller/Manager 是非 React 类,通过 store 的 getState/setState 与 UI 协作;组件只订阅所需字段,避免 60Hz 播放时间更新带动无关工具栏重渲染。这也解释了为什么选 Zustand:API 小、外部访问自然、适合连接 React UI 与命令/控制器对象。
4.4 Redux、Zustand、Context 怎么选
| 方案 | 更适合 | 主要风险 |
|---|---|---|
| 组件 state | 单组件或局部子树 | 提升过高会让更新范围扩大 |
| Context | 低频、跨层级依赖,如主题/用户环境 | Provider value 变化会使消费者更新,拆分不当易扩大影响 |
| Zustand | 轻量共享状态、外部类、高频切片订阅 | store 边界随意、selector 返回值不稳定 |
| Redux | 复杂业务状态、严格事件流、可追踪更新、SSR 生态 | 样板与建模成本,不应承载所有瞬时 UI 状态 |
回答选型时先说数据的所有者、生命周期、更新频率和消费范围,不要先站框架阵营。
4.5 Next.js:渲染策略与 Hydration
必须分清:
- CSR:服务端给壳,客户端加载 JS 后请求数据并渲染;
- SSR:每次请求在服务端生成 HTML;
- SSG:构建期生成 HTML;
- ISR:静态页面按策略增量再生成;
- Hydration:客户端 React 在服务端已有 DOM 上恢复组件状态和事件能力。
Hydration mismatch 的本质:服务端首次输出与客户端首次 render 不一致。常见原因包括 Date.now()、Math.random()、只在浏览器存在的 API、时区/语言差异和首轮读取 localStorage。
项目案例:使用 Next.js Pages Router。请求到达自定义 server 后匹配页面,getServerSideProps 读取 header/query 和平台配置,并发请求上游,初始化 Redux,服务端生成 HTML;浏览器加载 _next 的 JS/CSS 后 hydration。项目还有 SSR 直出、客户端渐进渲染和 CSR 降级路径。
4.6 Vue 3:与 React 对照理解
Vue 3 的核心是响应式依赖追踪:读取响应式值时收集依赖,写入时触发相关 effect/组件更新。Composition API 用 ref/reactive/computed/watch 组织逻辑。
与 React 的差异不要背成优劣:
- React 更强调每次 render 的快照和显式依赖;
- Vue 通过 Proxy/Ref 做细粒度依赖追踪,模板由编译器提供优化信息;
- React Hook 依赖调用顺序,Vue Composition API 没有相同限制;
- Pinia 与 Zustand 都轻量,但 Pinia 深度融入 Vue 响应式、DevTools 和插件体系。
项目案例:使用 Vue 3 + Vite + Pinia,把上传、处理中、完成、异常等状态集中在 task store;异步 action 串联上传、推理和结果获取,组件根据状态显示骨架屏、结果或错误。使用 URL.createObjectURL 做本地预览,文件变化或组件销毁时 revokeObjectURL 防止泄漏。
五、Vite / Webpack / 发布上线
5.1 一句话主线
开发工具解决模块解析、语法转换、依赖预构建和热更新;生产构建把模块图转换为可缓存、可分包、可部署的产物;发布流程保证 HTML、JS/CSS 资源、服务容器和环境配置来自同一版本。
5.2 Webpack 原理
text
entry
→ 解析模块与依赖
→ Loader 转换非标准模块或源码
→ 形成 Module Graph
→ 按入口和动态导入形成 Chunk Graph
→ 优化、Tree Shaking、压缩
→ emit assets
- Loader 面向单个模块内容转换,例如 Babel、CSS、图片;
- Plugin 监听构建生命周期 hooks,影响打包、优化、资源生成等更广流程;
- Module 是源码依赖单元,Chunk 是打包过程中的代码块,Bundle/asset 是最终文件;
- 动态
import()是常见分包边界; - Tree Shaking 依赖 ESM 的静态结构、副作用标记和构建器优化,不是"用了 import 就一定删干净"。
5.3 Vite 原理
开发阶段:
text
浏览器原生 ESM 请求
→ Vite 按 URL 解析模块
→ 按需转换源码
→ 依赖预构建
→ 返回模块
→ 文件变化后通过 HMR 更新受影响模块
生产阶段:当前项目材料中的 Vite 版本以 Rollup 生成优化产物。
Vite 快的原因不是"完全不打包":开发时不先把整个业务打成一个 bundle,并对依赖预构建;生产仍然要构建、分包和压缩。
项目证据:配置了路径 alias、FFmpeg 预构建排除、构建 target、manualChunks、生产移除调试语句、全局常量和开发代理;FFmpeg 独立分包避免进入首屏主 chunk。
5.4 Babel、SWC 与 Polyfill
- Babel/SWC 可以把 JSX、TypeScript 或新语法转换为目标 JavaScript;
- 语法转换不等于补齐运行时 API;旧环境缺少 Promise、Array.from 等仍可能需要 polyfill;
- Browserslist/target 描述目标环境,决定转译和兼容策略;
- 项目选择
esnext是明确放弃旧浏览器兼容以换取更小产物和更快构建,只适合环境受控的内部平台。
5.5 发布链路:必须讲清版本一致性
text
本地开发与自测
→ push 代码
→ CI/容器内安装依赖并执行 Next SSR build
→ escheck 等产物兼容检查
→ 同步 _next 静态资源并生成静态资源 tag
→ 产出 SSR 容器镜像
→ K8s 预发布部署
→ 发布与容器匹配的静态资源 tag
→ Whistle 配置域名/Host 验收
→ 测试流程与线上发布
关键风险:容器 tag 与静态资源 tag 不一致,会导致 HTML 引用的 _next hash 资源不存在或版本错配。验收不能只看首页打开,还要检查资源 404、接口环境、登录态、缓存、端内 WebView 和降级路径。
六、浏览器渲染与性能优化
6.1 URL 到页面
text
URL 解析
→ 缓存与 DNS
→ 建立连接(TCP/TLS 或 QUIC)
→ HTTP 请求/响应
→ HTML 流式解析生成 DOM
→ CSS 解析生成 CSSOM
→ 样式计算与渲染树
→ Layout
→ Paint
→ Composite
→ JS 框架 hydration/交互
注意:解析 HTML 时遇到普通同步 script 可能阻塞;defer 在解析后按顺序执行,async 下载完即执行、顺序不保证。CSS 通常不阻塞 DOM 构建,但会影响渲染,也可能间接阻塞依赖样式信息的脚本。
6.2 Layout、Paint、Composite
- 改变几何尺寸或位置可能触发布局;
- 颜色、阴影等可能只需绘制;
transform/opacity在满足分层条件时可主要由合成阶段处理;- 强制同步布局常来自"写样式后立刻读
offsetWidth/getBoundingClientRect"的读写交错; will-change不是越多越好,图层会消耗内存。
拖拽用 transform 和动画值直接更新,目标就是避开高频 React 更新及昂贵布局;应使用 Performance 和 React Profiler 证明,而不是仅凭"感觉顺滑"。
6.3 首屏优化要按指标拆
先明确优化对象:
- TTFB:服务端响应速度;
- FCP:首次内容出现;
- LCP:最大内容元素完成渲染;
- CLS:布局稳定性;
- INP:交互响应;
- JS 执行与 hydration 成本。
优化手段与问题对应:
| 问题 | 方案 |
|---|---|
| HTML 到得晚 | CDN/缓存、服务端优化、SSR 上游并发与超时 |
| 主包太大 | 路由/组件分包、Tree Shaking、按需引入、移出重型依赖 |
| 关键资源晚 | 正确使用 preload、preconnect、fetchpriority,优化请求优先级 |
| 非关键资源抢带宽 | lazy、动态 import、低优先级加载 |
| 图片拖慢 LCP | 尺寸和格式优化、响应式图片、首屏图不要懒加载 |
| hydration 太重 | 减少首屏客户端 JS、延迟非关键交互、合理 SSR/SSG |
| 页面跳动 | 预留图片/广告尺寸、稳定字体与骨架结构 |
6.4 lazy、preload、prefetch 的边界
loading="lazy":元素接近视口再加载,适合非首屏图片/iframe;- 动态
import():执行到边界时加载代码 chunk; preload:当前页面很快必需的高优先级资源;滥用会抢占关键带宽;prefetch:未来可能使用、低优先级获取;preconnect:提前建立跨域连接;- 首屏 LCP 图片通常不应 lazy,应让浏览器尽早发现并正确设置尺寸/优先级。
项目案例:对非首屏弹窗/模块做动态加载并分阶段渲染; 用组件库按需引入、路由拆分和本地 Blob URL 预览;把 FFmpeg 单独分包。只有实际配置过 preload/prefetch 才说"做过资源预加载"。
6.5 性能验证闭环
text
建立基线
→ Performance/Network/React Profiler 定位瓶颈
→ 提出单一假设
→ 修改
→ 相同设备、网络和操作复测
→ 对比指标与火焰图
→ 检查功能和内存回归
项目中可讲:时间轴观察 commit 次数、Frames、Long Task、heap;会员中心用网络限速、首帧录像、资源 onLoad 与状态日志验证白屏/闪烁时序。
七、前后端联调与 DevTools / Whistle / ADB 排障
7.1 通用排障框架
text
1. 定义现象:谁、在哪个环境、什么操作、期望与实际是什么
2. 稳定复现:记录版本、账号、网络、设备和时间
3. 画链路:操作 → 请求 → 响应 → 状态 → DOM → 资源 → 可见结果
4. 分层取证:Network、Console、React/Vue DevTools、Performance、Bridge/客户端日志
5. 提出最小假设并用 Mock/断点/日志证伪
6. 修复后在原条件和边界条件下回归
7. 记录根因、边界、证据和预防措施
7.2 三个工具各自解决什么
- DevTools:DOM/样式、浏览器发出的请求、JS 断点、性能、内存、存储;
- Whistle:作为代理观察或改写请求响应、Host 映射、Mock、Rewrite、弱网与异常场景;HTTPS 解密依赖调试设备主动信任 CA;
- ADB:开发机到 Android 设备的调试通道,可看设备、日志、shell、端口转发并辅助 WebView 调试。ADB 本身不是抓包工具。
必须先判断请求由谁发出:浏览器 XHR/fetch 可在 Network 中看到;通过 CMD 186 走 Native 网络通道时,浏览器 Network 可能没有记录,需要 Bridge 日志、客户端日志或代理抓包。
7.5 接口联调基本功
要主动核对:
- method、URL、query/body、Content-Type;
- token/cookie/header 与签名;
- HTTP 状态和业务 code 的区别;
- 空数据、字段缺失、类型漂移、超时和重复提交;
- 幂等、重试、取消与竞态;
- CORS 只由服务端响应许可,开发代理是开发期规避同源限制,不等于生产 CORS 已解决;
- 以服务端权威状态判断支付成功,不能用"点击过按钮"代替。
八、WebView / JSBridge / 客户端命令号
8.1 一句话主线
WebView 是 App 内嵌的 Web 运行环境;JSBridge 是 H5 与 Native 约定的方法、参数、回调和传输通道;命令号是协议中的能力标识,不是前端随意生成的编号。
8.2 调用链
text
H5 business code
→ mobileCall(cmd, params, callback)
→ SDK 序列化 cmd / jsonStr / callbackName
→ 注入对象、自定义 Scheme 或 postMessage
→ Native 拦截并按 cmd 路由
→ 执行登录、支付、播放等原生能力
→ Native 按 callbackName 回调 H5
→ SDK 解码、解析、清理 callback
→ Promise/callback 回到业务层
项目中的 Android 新通道会构造 jsbridge:// Scheme 并用隐藏 iframe 触发;旧通道可能调用 external.superCall;iframe 内页面先 postMessage 给父页面转发。当前仓库只能证明 H5 SDK 侧,不能据此声称实现过 Native 拦截器。
8.3 稳健接入要处理什么
- 端内/端外,Android/iOS;
- 客户端版本与命令支持矩阵;
- 参数序列化与字段差异;
- callbackId 唯一性、一次性清理和重复回调;
- 超时、取消、页面卸载和降级;
- iframe 消息的 origin/source 校验;
- Native 收到命令不等于业务成功,最终状态应以明确回调或服务端结果为准;
- 不把内部 SDK 能力说成个人从零实现。
8.4 项目案例:MV 播放与活动支付
MV 播放:先用命令号获取网络状态,再通过 Request/CMD 186 或 XHR 获取播放地址,按客户端版本选择播放命令;没有可靠结果回调的命令通过超时降级到 H5 页面。排障时不能只盯浏览器 Network。
活动支付:页面 EventEmitter 打开确认弹窗,VOO SDK 发出支付事件,支付组件调用 Native,成功/失败事件回到页面后再刷新用户、商品或装扮状态。页面事件、SDK 事件和 Native 结果必须分层,点击购买不代表支付成功。
九、Agent / Skill / Harness 与 AI Coding
9.1 概念关系
- LLM:根据上下文生成下一步输出;
- Tool:有明确输入输出的外部能力;
- Agent Loop:模型决策、调用工具、观察结果并继续的循环;
- Skill:可复用的程序性知识/SOP,说明何时做什么、读哪些参考、完成条件是什么;
- References/Knowledge:字段、规则、事实、例外与示例;
- Hook:在工具调用前后观察、记录、提醒或阻断;
- Checkpoint:持久化可恢复状态;
- Harness:把模型、上下文、工具、权限、状态、验证、观测和人机协作组合起来的运行环境。
一句话区分:Prompt 告诉模型"这次怎么做";Skill 固化可复用流程;Tool 真正执行能力;Harness 保证整个执行系统能受控运行。
9.3 AI Coding 的正确闭环
text
任务契约
→ 提供最小必要项目上下文
→ 让 AI 先追踪真实数据流
→ 人确认方案与风险
→ 小步实现
→ lint / typecheck / test / build
→ Mock、日志或运行时验证
→ 审查 diff 与行为边界
→ 写回稳定知识和故障复盘
AI 输出需要检查:
- 是否引用了真实文件、接口和字段;
- 是否扩大需求或改动无关代码;
- 是否破坏状态不变量、错误处理和兼容性;
- 是否编造测试结果;
- 是否在关键副作用前请求确认;
- 是否用可观测证据证明最终行为。
十、工程质量、可维护代码与组件复用
10.1 质量不是"写了测试"四个字
text
需求澄清
→ 状态与边界建模
→ 类型/接口契约
→ 小职责组件和纯数据转换
→ 错误、超时、取消、幂等与降级
→ 静态检查、单元/集成/E2E
→ 构建和兼容检查
→ 预发布/真机/灰度验证
→ 监控、回滚与复盘
10.2 状态建模优于布尔堆叠
多个布尔值容易产生非法组合:loading=true、success=true、error=true。优先用可辨识状态:
ts
type TaskState =
| { status: 'idle' }
| { status: 'uploading'; progress: number }
| { status: 'processing'; taskId: string }
| { status: 'success'; result: Result }
| { status: 'error'; message: string; retryable: boolean };
10.3 组件复用的判断标准
可复用组件应有:
- 单一、稳定的业务职责;
- 明确 props/事件契约;
- 数据与视图解耦,原始接口字段先转为 view model;
- 样式和宿主边界明确;
- loading/empty/error/disabled 等状态完整;
- 可配置但不做"万能组件";
- 关键交互可测试、可观测
10.4 错误分层
- 网络层:断网、DNS、超时、连接失败;
- HTTP 层:4xx/5xx;
- 协议层:响应结构不合法、字段缺失;
- 业务层:库存不足、次数耗尽、未登录;
- UI 层:渲染异常、资源加载失败;
- 跨端层:Bridge 不存在、版本不支持、回调超时;
- Agent 层:工具失败、流程偏离、证据不足、输出未通过校验。
不同层的重试和提示不同。支付/提交类操作重试前必须考虑幂等,不能把所有错误都自动重试。