京程一灯宣传官网面试手册
# 京程一灯宣传官网面试手册
# 使用方法
先读 项目介绍 和 你主要负责什么?,再按前端架构、图形渲染、性能与测试查阅实现。面试前可用 重点问题速查 定位答案,术语不熟悉时查看 技术术语速查。
# 事实边界
| 可以明确说 | 需要限定 | 不要声称 |
|---|---|---|
| 完成 React 静态站、五条路由、三语、3D 场景、响应式和浏览器测试实现 | 目标部署平台是 Cloudflare Pages;仓库有部署说明和产物检查,但域名接入仍受 GoDaddy 账号问题影响 | 已有大量真实用户、已经形成业务转化数据、经过真实流量压测 |
| WebGPU 海洋基于 FFT Ocean 上游实现并做了站点集成与改造 | 海洋算法不是从数学公式开始全部原创;应说明基于开源 OceanDemo,自己的工作是整合、隔离、体验与性能改造 | 独立发明 FFT 海洋算法、完全自研 Babylon 引擎 |
| 主站使用 Babylon 9,隔离海洋运行时使用 Babylon 5.x | 当前仓库同时保留源码、vendor 源和编译后的海洋资产,主站实际通过 iframe 加载预构建页面 | 所有海洋源码都由当前 Vite 主构建实时编译 |
| 有 AI 课程内容和 AI 全栈工程交付背景 | 当前站点本身没有模型 API、RAG、Agent、向量库或后端推理 | 这是一个完整 AI Agent 产品或具备线上大模型推理 |
| 有测试代码和历史 QA 记录 | 当前环境的可选原生依赖安装不完整时,Vite、Oxlint 和 Playwright 启动会失败,需要重新安装依赖后再验证 | 当前所有测试刚刚重新跑过并全部通过,如果你没有真实执行证据 |
最重要的诚实口径:这个项目能证明你具备复杂前端、图形渲染、浏览器兼容、工程化测试与静态部署能力;它不能单独证明后端数据库、分布式系统或大模型应用链路能力。AI 全栈面试时,应把它作为“体验层与交付层项目”,再用其他项目证明 AI 与后端能力。
# 技术栈与项目分工
| 技术 | 当前版本或形态 | 项目职责 | 面试重点 |
|---|---|---|---|
| JavaScript (opens new window) / JSX (opens new window) | ES Module | 页面、交互和状态主语言 | 闭包、事件、异步、清理、模块边界 |
| TypeScript (opens new window) | 5.9.3,允许 JS,strict: false | 海洋源码、着色器工具的类型表达 | 渐进迁移与类型边界,不要声称全项目严格 TS |
| React (opens new window) | 19.2.8 | 组件、状态、Effect、外部 Store 接入 | 渲染模型、Effect 清理、StrictMode |
| React DOM (opens new window) | 19.2.8 | createRoot 挂载 | 客户端渲染与 hydration 区别 |
| Vite (opens new window) | 8.2.2 | 开发、资源处理、生产构建 | 插件、静态资源、SPA 与构建边界 |
| Babylon.js (opens new window) | 主站 9.23;隔离海洋 5.x | 粒子场、WebGPU 海洋、PBR、模型与浮力 | WebGL/WebGPU、场景生命周期、版本隔离 |
| WGSL (opens new window) / WebGPU (opens new window) | Compute Shader | 波谱演化和 IFFT | GPU 通用计算、工作组、能力检测 |
| Cesium (opens new window) | 1.145 | 3D 地球、测地线航线、标签与交互 | 椭球坐标、懒加载、按需渲染 |
| GSAP (opens new window) / ScrollTrigger (opens new window) | 3.15 | 滚动进入、按钮磁吸、逐字动画 | 布局时机、清理、降级 |
| Motion (opens new window) | 13.2 | BlurText 动画 | 声明式动画与 reduced motion |
| Canvas 2D (opens new window) | 浏览器原生 | 海洋加载器的粒子球静态绘制 | DPR、重绘成本、合成动画 |
| React PageFlip (opens new window) | 2.0.3 | AI 发展史翻页书 | 受控页码、移动端交互 |
| Pretext | 0.0.8 | 标题按真实布局分行;见 其他视觉库 | ResizeObserver、文本布局 |
| Playwright (opens new window) | 1.63 | 三语、路由、移动端和异常场景回归 | 可观察行为、测试矩阵、失败证据 |
| Oxlint (opens new window) | 1.79 | 静态检查 | lint 与测试的职责区别 |
| Cloudflare Pages (opens new window) | 静态托管目标 | CDN、SPA 回退、静态资产发布 | 直接刷新、缓存和产物限制 |
# 已安装不等于核心使用
面试时不要把 package.json 中的每个依赖都说成“核心技术”。例如 liquid-glass-react 在依赖表中,但当前主要玻璃卡片是项目自己的 SVG filter 和 CSS 实现;它不应该被描述成整站架构核心。src/ocean 中还有一套可由 Vite 处理的海洋源码,但首页的实际调用链是 /ocean-demo/index.html 的隔离预构建运行时。准确区分“依赖存在”“源码存在”和“线上调用链正在使用”会显得更可信。
# 第一部分 项目介绍与架构
# 项目介绍
这是一个面向 AI 全栈课程展示和招生转化的三语官网,包含首页、课程大纲、公开课、招生和关于五条页面路径,支持中文、英文、日文。前端使用 React 和 Vite,结合 WebGPU FFT 海洋、Babylon.js 粒子场、Cesium 地球与滚动动画增强品牌展示,同时通过静态预览、渐进降级、响应式和浏览器测试保证内容可用。项目是静态 SPA,没有后端、数据库或大模型推理链路,目标部署平台是 Cloudflare Pages。
# 你主要负责什么?
建议回答:我负责的范围是需求拆分、前端架构、核心视觉模块集成、三语与响应式、浏览器测试和静态部署准备。开源 OceanDemo、Babylon、Cesium 和 React Bits 是第三方基础,我没有把它们包装成自研;我的主要工作是让这些能力在同一产品里具备清晰边界、降级、性能控制和可验证交付。
# 架构总览
浏览器
├─ React 19 应用
│ ├─ 手写 History API Router
│ ├─ URL + localStorage + useSyncExternalStore 国际化
│ ├─ 五个页面与共享 Header / Footer
│ ├─ GSAP / Motion / React Bits 动效
│ └─ HTML / CSS / 图片 / 字体 / 海报 / PDF
├─ Babylon.js 9
│ └─ 课程大纲技术图标粒子场
├─ Cesium 1.145 独立运行时
│ └─ 地球、测地线航线、点、标签与交互
└─ iframe 运行时隔离
└─ Babylon.js 5.x OceanDemo
├─ WebGPU Compute Shader
├─ 初始波谱与时间演化
├─ 二维 IFFT
├─ 海面几何与 PBR 材质
└─ 浮力、小船、天空与反射
构建与发布
Vite 8 → dist 静态资产 → 产物约束检查 → Cloudflare Pages
└─ /* → /index.html 200
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 页面结构
| 路径 | 页面职责 | 代表性技术 |
|---|---|---|
/ | 品牌首屏、课程价值、能力和转化入口 | WebGPU 海洋、Canvas Loader、响应式首屏 |
/curriculum | 技术课程图谱与漫画大纲 | Babylon.js 粒子、海报弹窗、下载与语言同步 |
/open-class | 公开课、AI 发展史与全球视野 | 翻页书、Liquid Glass 卡片、Cesium 地球 |
/admissions | 招生信息、优势、方向、流程与咨询 | 2.5D 图标、动态光场、交互 CTA |
/about | 品牌介绍、里程碑和价值观 | 卡片聚光、滚动进入动画 |
# 一次页面导航发生了什么
- 用户点击站内链接。
Router的文档级 click 监听只处理左键、未按修饰键、同源、非下载、非新窗口且属于白名单的链接。localizedPath自动保留或补上当前lang参数。history.pushState改变 URL,再主动派发popstate。- 路由状态更新,页面组件切换,滚动位置回到顶部。
pageMetadata根据当前页面和语言更新<html lang>、document.title和 meta description。- 浏览器前进后退会触发真实
popstate,旧的#/path地址会被replaceState迁移为干净路径。
# 一次语言切换发生了什么
- 首次加载先读 URL 的
lang;合法值为zh、ja、en。 - URL 没有合法语言时尝试读取 localStorage;访问被禁用或抛错时忽略。
- 两者都没有时使用中文。
setLocale用replaceState写回当前 URL,并尝试保存偏好。- 模块级 Store 通知所有订阅者,React 通过
useSyncExternalStore重新取快照。 - 普通文案通过
translate查 JSON;数组和普通对象由translateData递归处理;React 图标对象保持引用不变。 - 新导航继续携带语言,刷新和历史返回仍能恢复。
# 动态海洋启动链路
React 首屏挂载
↓
始终先准备静态海景图
↓ navigator.gpu 是否存在
不支持 ─────────────→ 直接静态预览 + 不支持提示
支持
↓
加载 /ocean-demo/index.html iframe
↓
OceanDemo 创建 WebGPUEngine、编译 WGSL、加载纹理和模型
↓
父页面轮询 iframe.scene 并检查 compute shader capability
↓
设置固定相机、天空、海天线同步、加载船模
↓
观察真实海面网格的有效 draw call
↓
双 requestAnimationFrame 等浏览器合成
↓
隐藏 Loader,显示实时海洋
30 秒仍未完成:显示静态预览,但后台继续编译
初始化失败:静态预览 + 失败提示 + 重试按钮
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Cesium 地球启动链路
CareerGlobe 挂载
↓ IntersectionObserver
进入视口附近才 start
↓
动态插入本地 Cesium.js 与 widgets.css
↓
创建 Viewer,关闭无关控件和默认连续循环
↓
加载日夜单图纹理、经纬网、点、标签和测地线航线
↓
等待 tilesLoaded 且屏幕中心确实可拾取到地球表面
↓
静态预览淡出,交互控件启用
↓
可见时自定义 45 FPS 更新航线头部和相机
2
3
4
5
6
7
8
9
10
11
12
13
14
15
你这个项目叫 AI 全栈,但没有调用大模型,是否名不副实?
建议回答:如果单看这个仓库,它是 AI 课程品牌的体验层项目,不是大模型应用,我不会把课程内容等同于 AI 技术实现。它主要证明复杂前端、图形、国际化、测试和部署能力。我的 AI 能力应由另一个包含模型、RAG 或 Agent 调用链的项目证明;在这份经历里我会准确标成“AI 教育官网”或“AI 全栈课程品牌站”。
# 第二部分 前端架构与工程化
# React 19 与组件设计
# React 在项目里解决什么
React 负责把五个页面、共享页头页脚、语言状态、弹窗、加载状态和复杂场景外壳组合起来。3D 引擎不是 React 的替代品:React 管理 DOM 和生命周期,Babylon/Cesium 管理 Canvas 内部的渲染世界,两者通过 ref、状态和 Effect 边界协作。
# 为什么大量使用 useRef
3D Viewer、Scene、动画帧 ID、Observer、计时器和指针控制器都不适合直接放进 React state。它们变化频繁,但变化本身不应触发组件重新渲染;useRef 可以保存跨渲染的可变引用,并在 Effect 清理中精确释放。
# Effect 的正确边界
- 创建浏览器外部资源,例如事件监听、Observer、GSAP 时间线、Babylon Engine、Cesium Viewer。
- 组件卸载或依赖变化时撤销同一批资源。
- 使用
disposed或cancelled标记,避免异步加载完成后更新已卸载组件。 - 取消
requestAnimationFrame、setTimeout、setInterval,断开ResizeObserver与IntersectionObserver。 - 销毁粒子系统、纹理、Scene、Engine 和 Viewer,避免 GPU 资源泄漏。
# StrictMode 会带来什么
开发环境中 StrictMode 会额外执行渲染和 Effect 的设置清理流程,帮助暴露副作用不对称的问题。3D 场景如果没有完整 cleanup,可能出现两个渲染循环、重复 Canvas、重复事件或显存不释放。这个项目的复杂组件都显式维护销毁函数,正是为了让生命周期可重入。
为什么不用 React state 保存 Cesium Viewer?
建议回答:Viewer 是可变的第三方运行时对象,不参与 JSX 计算,也不需要每次变化都触发 React 渲染。我把它保存在 Effect 闭包或 ref 中,由 React state 只保存 loading、ready、error 和 selected 这类真正影响界面的最小状态。这样能避免无意义重渲染,也明确了销毁责任。
`useEffect` 和 `useLayoutEffect` 在项目中怎样选?
建议回答:普通事件订阅、异步加载和 3D 初始化放在 `useEffect`,避免阻塞浏览器绘制。需要在浏览器显示前读取布局或安装会影响初始动画状态的逻辑,例如 GSAP 进入动画和轨道尺寸同步,使用 `useLayoutEffect`,减少首帧闪动。两者都必须返回对称清理函数。
React 负责 3D 场景里的每一帧吗?
建议回答:不负责。React 只负责创建容器、状态提示和交互控件,Babylon 或 Cesium 自己维护渲染循环。每帧位置更新直接发生在引擎对象上;如果每帧都 setState,会让 React 协调和 DOM 更新进入高频路径。只有用户可见的低频状态才回到 React。
为什么国际化状态使用 `useSyncExternalStore`?
建议回答:语言状态由模块级 Store、URL、localStorage 和 storage 事件共同驱动,不属于单个组件。`useSyncExternalStore` 提供标准的 subscribe 和 snapshot 接口,让多个页面读取一致快照,也为服务端快照预留了接口。这里也可以用 Context,但外部 Store 更直接地表达“浏览器外部状态源”。
为什么不能在 render 中创建 Babylon Engine?
建议回答:render 必须保持纯,可能重复执行或被中断。创建 Engine 会产生 Canvas、GPU 和事件副作用,重复 render 就可能重复创建。应该在 Effect 中创建,并返回完整 cleanup。
stale closure 在动画代码里会怎样出现?
建议回答:长期运行的 RAF 回调捕获初次渲染的 state,后来按钮更新 state,但回调仍读旧值。项目用 `rotatingRef` 同步最新是否旋转,让每帧读取 ref;React state 只负责按钮显示。也可以重建 Effect,但会增加场景重启成本。
为什么状态要最小化?
建议回答:可由其他状态或引擎对象推导的值不应重复保存,否则会出现两份事实不一致。地球只把 loading/ready/error、selected 和 rotating 放入 React,路线点、Primitive 和 Viewer 留在引擎内部。
# 手写 SPA 路由与 History API
# 为什么没有使用 React Router
项目只有五条固定一级路径,没有动态参数、嵌套路由、路由级数据加载和复杂权限守卫。手写路由只承担路径映射、站内链接拦截、前进后退、旧 Hash 迁移和语言参数保留,代码量可控。它是当前需求下的简单方案,不代表大型项目也应该手写路由。
# 直接刷新为什么可能 404
客户端跳转时,浏览器已经拿到 index.html,React 可以按 pathname 选择页面。直接访问 /about 时,请求先到静态服务器;如果服务器寻找真实的 /about 文件就会 404。所以 Nginx 需要 try_files ... /index.html,Cloudflare Pages 通过 _redirects 的 /* /index.html 200 做 SPA fallback。
# 当前路由方案的边界
- 只认
PAGE_PATHS白名单,不支持参数化路由。 - 页面组件是静态 import,没有路由级代码分割。
- 没有路由守卫、loader、action、错误边界和滚动位置恢复策略。
- document click 委托要小心修饰键、下载链接、Hash 链接、跨域链接和 target。
- 页面数量增长后应迁移到成熟路由库。
`pushState` 会自动触发 `popstate` 吗?
建议回答:不会。`pushState` 和 `replaceState` 本身不触发 `popstate`,浏览器前进后退才会触发。项目的 `navigate` 在 pushState 后主动派发一个 PopStateEvent,复用同一条路径更新逻辑。更大型的路由器通常有自己的 history 订阅层。
为什么旧 Hash 路由用 `replaceState` 迁移?
建议回答:旧书签 `/#/about` 仍然要可用,但用户进入后应该看到干净的 `/about`。用 replaceState 不会额外增加一条历史记录,用户按返回键不会在旧地址和新地址之间多退一步。
什么时候应该换成 React Router?
建议回答:出现嵌套路由、动态参数、权限守卫、路由级数据加载、错误页、代码分割或几十个页面时,成熟路由器的约定和测试价值会超过这几十行手写代码。当前方案的价值是需求简单,而不是重复造轮子本身。
事件委托的优缺点是什么?
建议回答:优点是一个 document listener 可以处理动态生成的站内链接,不必每个组件重复绑定;缺点是边界判断更复杂,必须尊重默认阻止、鼠标键、修饰键、target、download、Hash 和跨域。项目只对白名单内部路径接管,其余行为交还浏览器。
为什么不用现成路由库,是不是过度自研?
建议回答:当前只有五条静态一级路由,手写层约几十行并覆盖了真实需要的 History、旧 Hash 和语言保留,复杂度可控。但我不把它当通用框架;出现嵌套、参数、数据加载和守卫时会迁移到 React Router。判断标准是总复杂度,不是依赖越少越高级。
如果要把当前站点改成 SSR,最先改什么?
建议回答:先盘点所有模块顶层的 window、navigator、localStorage 和事件监听,把浏览器依赖移入 Effect 或能力适配层;语言状态改为请求级;路由和 metadata 交给 SSR 框架;3D 组件只在客户端动态加载。SSR 不会让 WebGPU 在服务器运行,它只让首屏 HTML 和 SEO 可预渲染。
# 三语国际化与状态一致性
# 国际化设计
中文原文直接作为 Key,英文与日文放在 messages.json。这种方式让中文页面开发直观,但也有缺点:修改中文措辞就等于修改 Key,可能导致其他语言查找失败。规模继续增长时,更稳妥的方式是使用稳定语义 Key,例如 home.hero.title,并让三种语言都进入资源文件。
# URL 为什么优先于 localStorage
URL 表达当前页面可分享的状态。别人打开 ?lang=en 应该得到英文,即使自己浏览器以前保存的是中文。localStorage 只代表设备偏好,不能覆盖明确链接意图。
# 为什么所有存储读写都包在 try/catch
隐私模式、浏览器策略或测试注入可能让 localStorage 访问抛异常。如果语言系统把存储当成硬依赖,整个页面会在启动阶段失败。项目把它当成可选增强:不可用时 URL 仍工作,最后回退中文。
为什么不直接用 `navigator.language`?
建议回答:浏览器语言只能作为推测,不能覆盖用户明确选择,也不保证内容团队支持完整地区变体。当前优先级是 URL、已保存选择、中文默认值,行为确定且便于测试。后续可以在首次访问且没有任何偏好时把 navigator.language 作为软提示。
模块级 Store 有什么风险?
建议回答:它在单页客户端中简单有效,但如果做 SSR,模块实例可能跨请求共享,必须把请求级状态隔离;如果热更新或多 Bundle 重复加载,也要注意是否出现多份 Store。当前是纯客户端静态站,所以这个边界可以接受。
怎样发现英文页面遗漏了中文?
建议回答:Playwright 在英文页面遍历 main 下的文本节点,排除语言切换器和脚本样式后,用 Unicode Han Script 正则寻找汉字,期望结果为空。它不能证明翻译质量,但能自动发现大量最常见的漏翻。
SPA 的 SEO 有什么问题?
建议回答:当前 title 和 description 在客户端切换,爬虫是否执行 JS 和如何处理不同 lang URL 不完全可控,也没有每种语言的静态 HTML、hreflang 和社交卡片。营销站如果重视自然搜索,应做 SSG/SSR、独立语言 URL、canonical、结构化数据和 sitemap。
# Vite、TypeScript 与资源管线
# Vite 在项目中的职责
- React JSX 转换与开发时快速更新。
- 将图片、字体、CSS 和 JavaScript 产出到
dist。 - 通过自定义插件把
.wgsl文件作为源码字符串导入。 - 把
.exr、.hdr、.glb识别为静态资源 URL。 - 使用
vite-plugin-static-copy复制 Cesium 运行时目录。 - 保留
public目录中的海洋预构建页面、WASM、模型、海报和 PDF。
# WGSL 为什么不能按普通资源 URL 处理
Babylon ComputeShader 需要的是着色器源代码字符串。普通资源导入通常返回构建后的 URL,因此项目插件在 transform 阶段读取 WGSL 内容,生成 export default "...shader source..."。这属于一个很小但有明确抽象价值的构建适配层。
# TypeScript 的真实使用边界
项目配置 allowJs: true、strict: false,React 页面主要是 JSX,海洋计算模块主要是 TypeScript。因此可以说“项目混合使用 JS/TS,并在图形计算模块获得类型帮助”,不能说“全项目完成严格 TypeScript 类型治理”。如果面试官问改进,应回答开启 strict、逐步补全组件 Props、事件类型和第三方运行时接口,而不是一次性强改全部文件。
为什么 Cesium 资源要复制 Workers、Assets 和 Widgets?
建议回答:Cesium 不只有一个 JavaScript 文件,运行时还会按相对路径加载 Web Worker、纹理与数据资源、控件样式和第三方模块。项目设置 `CESIUM_BASE_URL`,再把对应目录复制到相同发布结构,否则主脚本可能加载成功,但地球、地形处理或控件在运行时 404。
`public` 和源码 import 的资源有什么区别?
建议回答:源码 import 的资源会进入构建依赖图,通常获得 Hash 文件名和引用改写;public 资源按原路径复制,适合必须保持固定 URL 的预构建海洋页面、模型目录和下载文件。代价是缺少按 import 图自动发现与强缓存文件名,需要额外做存在性和缓存管理。
当前构建最大的工程问题是什么?
建议回答:现有 dist 显示主 JavaScript 大约 7 MiB,整个产物约 98 MiB,最大的单个 GLB 约 23.68 MiB,已经接近 Pages 的 25 MiB 单文件限制。下一步应做页面级动态 import、把 Babylon 粒子和 Cesium 进一步拆分、压缩或替换旧海洋大模型,并用 Bundle Analyzer 和真实网络瀑布图设性能预算。
为什么锁 Node 版本?
建议回答:构建工具和可选原生依赖对 Node ABI、平台和包管理器行为敏感。项目提供 `.node-version` 22.16.0,能减少本地与 CI 差异,但还需要在 CI 真正使用它并通过 `npm ci` 按锁文件安装。
`npm install` 和 `npm ci` 有什么区别?
建议回答:`npm ci` 按 lockfile 做干净、可重复安装,适合 CI;`npm install` 可能更新锁文件并增量调整 node_modules。当前缺可选原生绑定时,可靠修复通常是在确认没有本地依赖修改后重新做干净安装,而不是手工把某个二进制塞进去。
# 第三部分 图形渲染与交互
# Babylon.js、WebGL 与 WebGPU
# Babylon.js 是什么
Babylon.js 是浏览器 3D 引擎,封装场景树、相机、灯光、材质、纹理、模型、粒子、动画以及 WebGL/WebGPU 后端。项目没有直接用底层 API 从零管理所有 GPU 状态,而是在引擎抽象上实现两个不同任务:主站的图标粒子场,以及隔离运行的 FFT 海洋。
# WebGL 与 WebGPU 的区别
WebGL 基于 OpenGL ES 图形管线,浏览器支持范围更广,适合普通顶点、片元渲染。WebGPU 更接近现代 GPU API,除了渲染管线,还原生提供 Compute Shader、Storage Buffer/Texture 和更明确的资源绑定模型。FFT 海洋需要大量规则的并行计算,因此 WebGPU 计算着色器更合适;课程粒子场使用 Babylon 的普通 WebGL 能力即可。
# 为什么两个 Babylon 版本要隔离
上游 OceanDemo 使用 Babylon 5.x,主站依赖 Babylon 9.23。强行把旧 Demo 源码直接升级或让两个版本共享页面,会把引擎 API 差异、全局 BABYLON、Shader 注册、材质扩展和资源缓存耦合在一起。iframe 提供独立 Window、模块运行时、Canvas 和全局对象,父页面只通过明确的 contentWindow.scene 集成点配置相机、天空和船模。
这不是没有代价:iframe 增加了一层通信与可测试性成本,父页面访问子页面还依赖同源,构建和升级要维护两套产物。当前选择的原因是先降低版本冲突和迁移风险;长期方案是把 OceanDemo 升级到统一 Babylon 版本并形成稳定模块接口。
# 课程粒子场怎样工作
AdmissionsParticles 在 Canvas 上创建 Babylon Engine、Scene、ArcRotateCamera 和多层粒子系统。技术图标先绘制到动态纹理或加载图标纹理,再分布在三维空间。每帧根据时间、速度和范围更新位置,并把粒子尺寸换算成与相机深度相关的世界单位,避免靠近相机时图标无限放大。移动端调整相机半径与粒子密度,卸载时销毁粒子、纹理、场景和引擎。
Scene、Engine、Camera 和 Mesh 分别是什么?
建议回答:Engine 连接浏览器 Canvas 和 GPU,管理渲染后端;Scene 是一组相机、灯光、网格、材质和更新逻辑的容器;Camera 决定视图和投影;Mesh 提供几何顶点及其世界变换。项目卸载时先销毁场景内资源,再销毁 Engine,避免渲染循环和 GPU 对象残留。
为什么不能只检测 `navigator.gpu` 就认为海洋一定可用?
建议回答:`navigator.gpu` 只说明浏览器暴露了入口,不代表适配器、设备、所需纹理格式和计算着色器一定成功。项目还会检查 Babylon Engine 的 `supportComputeShaders`,监听 iframe 与场景失败,并以真实海面 draw call 作为就绪条件。能力检测必须分层,不能只看一个布尔值。
iframe 隔离与微前端有什么相似和不同?
建议回答:相似点是独立运行时和依赖隔离;不同点是这里没有团队级应用拆分、独立路由和部署治理,只是为了隔离一个旧 3D 运行时。父子页面同源,父页面仍直接读取子页面场景并做有限配置,所以它更像技术兼容层,不应包装成完整微前端架构。
为什么 3D 场景必须显式 dispose?
建议回答:JavaScript 对象被回收不等于 GPU 纹理、Buffer、Shader、Canvas 事件和动画帧立即释放。单页应用反复进出页面时,如果只移除 DOM,会持续占用显存和 CPU。项目在 cleanup 中取消渲染循环、断开监听,并 dispose 粒子、纹理、Scene、Engine 或 Cesium Viewer。
View Matrix 和 Projection Matrix 分别做什么?
建议回答:View Matrix 把世界坐标变换到以相机为基准的观察空间;Projection Matrix 把观察空间映射到裁剪空间,透视投影会产生近大远小。海天线同步就是用固定相机的 view×projection 把世界海平面点投到屏幕。
PBR 材质是什么?
建议回答:PBR 是基于物理的渲染模型,用粗糙度、金属度、法线、环境光和能量关系得到更一致的材质表现。海面还需要反射、折射、菲涅耳和波面法线。它追求可解释的一致近似,不等于真实流体光学全仿真。
Draw Call 为什么重要?
建议回答:每次 draw call 都有 CPU 到 GPU 的提交与状态管理成本,很多小对象会让 CPU 成为瓶颈。粒子系统、PolylineCollection 和 PointPrimitiveCollection 能批量管理相似对象。项目还利用 draw call 是否大于零判断海洋表面真正参与了渲染。
为什么不用 Three.js?
建议回答:项目已有可复用的 Babylon WebGPU OceanDemo,Babylon 对 Engine、Scene、ComputeShader、PBR、GUI 和资源生命周期的集成适合当前任务。Three.js 生态很强,但切换并不会自动减少复杂度。地理模块则选择 Cesium,因为椭球与测地线是它的专长。
iframe 直接读 `contentWindow.scene` 是不是很脆弱?
建议回答:是,它依赖同源和子页面内部对象名,属于集成债。当前用它快速隔离版本并控制风险,但长期应该在子页面暴露版本化的 `postMessage` 协议,只传状态和高层命令,不让父页面操作内部 Scene。文档和测试也应锁定协议契约。
# FFT 海洋与计算着色器
# 先用一句话理解
FFT 海洋不是逐个顶点随便加正弦波,而是在频域组合大量不同方向和尺度的波,再用二维逆 FFT 高效还原成每一帧的空间位移、斜率和泡沫数据。
# 核心流程
- 生成高斯噪声,为不同频率的波提供随机初始相位。
- 根据风速、风向、重力和波谱模型计算初始频域振幅。
- 按时间更新复数频谱,使波随色散关系演化。
- 对多个位移和导数纹理执行二维逆 FFT。
- 合并得到水平位移、垂直位移、法线相关数据和破碎趋势。
- 海面几何在顶点着色阶段读取位移纹理,材质读取法线、反射、折射与泡沫信息。
- 浮力模块在对象位置采样波面高度和方向,让浮标或船随浪运动。
# 为什么使用 FFT
假设海面网格为 N×N,如果对每个空间点遍历所有频率,直接离散傅里叶变换的成本非常高;FFT 把一维复杂度从 O(N²) 降到 O(N log N),二维通过先横向再纵向执行。GPU 可以把每一阶段的大量元素并行处理,适合实时视觉。
# Compute Shader 如何执行
WGSL 定义工作组大小和每个 invocation 处理的纹理坐标。JavaScript 创建 ComputeShader,绑定输入纹理、Storage Texture、Uniform Buffer 和预计算数据,再根据数据尺寸除以线程组尺寸计算 dispatch group 数。IFFT 横向和纵向使用 ping-pong 纹理交替读写,避免同一阶段同时读写同一资源产生冲突。
# 海面几何为什么分块
无限海面不可能真的创建无限网格。常见做法是以相机为中心使用多个不同分辨率和尺度的网格块,近处细、远处粗,并随相机调整位置,形成视觉上的无限延伸。项目的 OceanGeometry 管理分块、顶点数据、边缘过渡和相机关系。
# 船为什么能随浪动
项目没有再做一套昂贵的船体流体模拟,而是复用上游浮标的波面采样结果。原浮标根节点已经获得位置与旋转,船模被挂到新 carrier,逐帧复制浮标位置,再加入缓慢前进距离;旋转用 Quaternion Slerp 在船的基础朝向和波浪朝向之间插值,只保留约 62% 的波浪姿态,兼顾真实感与甲板可见性。
FFT 和 IFFT 的区别是什么?
建议回答:FFT 是高效计算离散傅里叶变换的一类算法,不只代表正向变换;IFFT 是从频域回到空间域的逆变换。海洋先在频域构造和演化波谱,再通过二维 IFFT 得到每个空间位置的位移与导数。
为什么需要 ping-pong 纹理?
建议回答:FFT 有多个阶段,当前阶段的输出是下一阶段的输入。交替使用两张纹理可以避免同一次 dispatch 在没有同步保证的情况下边读边覆盖同一资源,也减少每阶段重新分配纹理的成本。
海洋算法是你原创的吗?
建议回答:不是从零原创。底层 FFT Ocean 来自开源 OceanDemo,它又参考了 Unity FFT Ocean。我负责的是把它以可维护方式接入站点:隔离引擎版本、本地化远程运行时、重做首帧判定和降级、调整相机与天空、压缩船模、复用浮力、同步海天线,并验证响应式与失败路径。我会明确区分上游算法和自己的工程贡献。
为什么 iframe 的 load 事件不能代表海洋 ready?
建议回答:load 只代表文档及其同步依赖完成加载,WebGPU 设备初始化、WGSL 编译、纹理上传、模型加载和第一帧真正绘制可能还在后面。项目等待场景存在、计算能力可用,并观察海洋网格实际发生有效 draw call,再经过两帧合成才通知 Loader 结束。
30 秒超时为什么不直接判失败?
建议回答:低性能设备首次编译 WebGPU Shader 可能很慢,慢不等于失败。30 秒后先显示静态预览,保证用户可看到内容,但让 iframe 在后台继续准备;真正完成后自动切到动态海洋。只有能力明确缺失或初始化错误才进入失败状态。
如何避免船模穿浪?
建议回答:先复用同一波面采样,保证船和海浪处于同一事实源;再调整船体局部水线高度,使用四元数插值降低极端 pitch 和 roll,并在多个波相位观察甲板与浪峰关系。它是视觉浮力,不是完整刚体流体仿真,所以应描述为稳定的近似。
四元数为什么适合旋转插值?
建议回答:欧拉角直观但可能万向节锁,直接分量插值也会出现不自然路径。单位四元数能稳定表示三维旋转,Slerp 沿球面做平滑插值。项目用它在船的基础朝向和波浪姿态之间取折中。
为什么海洋不直接做视频背景?
建议回答:视频实现简单、兼容性和能耗更可控,很多营销站确实应该选视频。项目选择实时海洋是为了让船和波面持续一致、体现实时图形能力并支持场景改造;同时保留静态回退。若真实数据证明互动价值不高,我会把低端设备甚至全量切到高质量视频或图片。
你用了开源代码,项目价值在哪里?
建议回答:工程价值不在重复实现 FFT,而在正确选择、理解并整合上游:处理版本隔离、本地运行时、失败降级、真实 ready、视觉对齐、船模压缩、浮力复用、性能与许可证。面试时我会明确上游贡献和自己的改动,并能解释关键原理与边界。
# Cesium 三维地球与测地线
# Cesium 为什么适合这个模块
普通 Three.js/Babylon 当然也能画球,但 Cesium 原生提供 WGS84 椭球、经纬度到笛卡尔坐标转换、测地线插值、相机与地球表面拾取,以及为地理可视化设计的 Primitive。项目需要从北京到多个城市的全球航线,因此使用 Cesium 能减少地理数学和交互基础设施的重复实现。
# 航线怎样生成
每条路线用 EllipsoidGeodesic 连接北京和目的地,按 193 个采样点插值经纬度,再根据路线距离计算一个受上下限约束的最大高度,用 sin(πt) 让中点最高、两端回到地表。随后创建光晕线、主体线、移动尾迹、头部点和目的地标签。
# 为什么开启 requestRenderMode
Cesium 默认适合连续动态世界,会持续渲染。这个模块大部分内容静止,只在航线运动、自转、相机飞行、尺寸变化或资源加载时需要新帧。requestRenderMode 加自定义循环可以把控制权交给页面,在不可见或文档隐藏时停止推进,降低 CPU/GPU 消耗。
# ready 为什么还要检查地球表面
Viewer 创建成功不代表用户看到的地球已经贴图。项目等待至少几个 postRender、tilesLoaded,并从屏幕中心发射拾取射线确认确实命中 Globe,才把状态设为 ready。这避免静态预览过早消失,露出黑球或未完成纹理。
大圆航线和屏幕直线有什么区别?
建议回答:地球表面两点最短路径应沿球面或椭球测地线,不是把经纬度在线性平面上直接连线。Cesium 的 EllipsoidGeodesic 在 WGS84 椭球上插值,再把每个点转为三维坐标。项目额外加入高度形成可视弧线,但地面路径仍遵循测地线。
为什么要用 `IntersectionObserver` 懒加载地球?
建议回答:Cesium 脚本、Worker、纹理和 Viewer 初始化成本高,而地球位于页面下方。Observer 在接近视口时才启动,能减少首屏网络、解析和 GPU 竞争。它比在 scroll 回调中频繁读布局更适合可见性判断。
自定义 45 FPS 有什么取舍?
建议回答:航线头部和缓慢自转不要求 60 FPS,45 FPS 可以降低持续负载,又比 30 FPS 更平滑。代码按时间差推进而不是按帧数推进,并把 delta 上限限制为 0.1 秒,避免切回页面后一次跳很远。最终帧率仍应根据设备测量动态调整。
为什么要把 delta 限制到 0.1 秒?
建议回答:标签页后台暂停后,下一帧时间差可能很大。如果直接按完整 delta 推进,航线头和相机会突然跳跃。限制单帧最大步长可以让恢复更稳定;需要精确物理时还应使用固定时间步和累积器。
# 动画、Canvas 与视觉组件
# GSAP 与 ScrollTrigger
SiteMotion 对现有页面元素做滚入动画,不额外包裹固定页头或 3D Canvas。它根据选择器批量创建 tween,卡片使用位移、透明度、轻微旋转和缩放;动画只执行一次。gsap.matchMedia 只在用户没有减少动态偏好时启用,cleanup 时 revert 所有媒体上下文。
# 为什么焦点进入时强制完成动画
键盘用户可能通过 Tab 聚焦到仍然 opacity 0 的按钮。如果只考虑滚动视觉,元素存在于可访问树但肉眼不可见。项目监听 focusin,一旦焦点进入动画元素,就把对应 tween 推到完成状态,保证键盘焦点可见。
# Canvas Loader 的性能演进
加载器最初可以持续重绘粒子球,但此时海洋正在编译 Shader,两者会争抢主线程与 GPU。当前做法是 Canvas 只绘制一次粒子球,持续呼吸交给 CSS compositor 的 opacity/scale,进度每 100ms 左右低频更新。视觉上保持活动感,但不在最昂贵阶段制造另一条高频绘制链。
# 其他视觉库
- Motion 的
BlurText负责声明式逐字模糊进入,并读取 reduced motion。 - React PageFlip 提供翻页书;React state 记录当前页并同步左右控制。
- Pretext 根据真实宽度重新分行,
ResizeObserver在容器变化时更新。 - SpotlightCard 用指针位置写 CSS 变量,CSS 完成聚光与倾斜;触摸和 reduced motion 不启用。
- Simple Icons 与 Phosphor Icons 提供真实品牌和界面图标,避免用 emoji 代替产品图标。
为什么优先动画 transform 和 opacity?
建议回答:它们通常可以在合成阶段完成,避免频繁触发布局和大范围绘制。并不是所有 transform 都零成本,大面积图层仍会占显存,所以项目还要控制动画数量、模糊半径和同时运行的 3D 场景。
`requestAnimationFrame` 节流鼠标光场有什么意义?
建议回答:pointermove 频率可能高于屏幕刷新率,直接每次修改样式会产生重复工作。把最新坐标合并到下一次 animation frame,保证一帧最多更新一次,并与浏览器绘制节奏对齐。
reduced motion 应该怎样处理?
建议回答:不是简单把所有内容隐藏,而是保留信息和最终状态,关闭或缩短滚动、逐字、自动旋转和高频运动。项目通过 CSS 媒体查询、GSAP matchMedia、Motion 的 useReducedMotion 和 3D 组件里的 matchMedia 分层处理。
`requestAnimationFrame`、`setTimeout` 和 `setInterval` 怎样选择?
建议回答:与屏幕绘制同步的动画使用 requestAnimationFrame;一次延迟动作使用 setTimeout;固定轮询可用 setInterval,但要考虑漂移、重入和页面隐藏。项目对视觉帧使用 RAF,对 Loader 进度和 iframe 场景发现使用计时器,并在 cleanup 中全部取消。
`ResizeObserver` 与 window resize 有什么区别?
建议回答:window resize 只表示视口变化,元素可能因字体、侧栏、内容或父布局改变而变尺寸。ResizeObserver 观察具体元素,适合 Canvas、标题分行和 ScrollTrigger 刷新。回调里仍要避免同步反复改尺寸造成观察循环。
`image.decode()` 比只等 load 好在哪里?
建议回答:load 表示资源到达,decode 返回的 Promise 在图片可解码时完成,更适合在切换可见层前避免解码卡顿或空白。项目对海洋静态预览先 decode,再等绘制帧后通知 ready。
浏览器一帧大致经过哪些阶段?
建议回答:JavaScript 更新后,浏览器可能进行样式计算、布局、绘制和合成。并非每次都经过全部阶段;只改 transform/opacity 常可跳过布局和大部分绘制。Canvas/WebGL 还有自己的绘制命令和 GPU 管线,最终再与 DOM 图层合成。
# 第四部分 性能、响应式与无障碍
# 性能优化与渐进降级
# 性能预算要分四类看
| 类型 | 主要风险 | 项目措施 |
|---|---|---|
| 网络 | 大模型、纹理、Cesium 与主 JS | 本地压缩船模、WebP、接近视口加载、静态预览 |
| JavaScript | 解析执行、主线程事件、Bundle 大 | 低频状态、第三方运行时延迟加载、Observer |
| GPU | 多 Canvas、Shader 编译、像素填充 | 帧率上限、DPR 限制、不可见暂停、降级图 |
| 内存 | 纹理、Buffer、Viewer、事件泄漏 | Effect cleanup、dispose、取消动画帧与 Observer |
# 当前已经做的优化
- 船模型从约 24.8 MiB 压缩到约 3.2 MiB,贴图由 2048×2048 重采样到 512×512。
- Cesium 进入视口附近才初始化,并将连续默认循环改成按需渲染。
- 地球动画限为约 45 FPS,页面隐藏或模块不可见时停止推进。
- 海洋慢加载时先显示高质量静态预览,避免白屏。
- Loader Canvas 使用 DPR 1 并取消持续全屏重绘。
ResizeObserver只在尺寸变化时处理,事件卸载时移除。- 移动端减少粒子数、调整相机和布局,短横屏也有专项断点。
- 所有重动画考虑
prefers-reduced-motion。
# 当前仍然存在的问题
- 主 JavaScript Bundle 约 7 MiB,说明页面没有真正路由级拆包,Babylon 等依赖可能进入主包。
- 产物约 98 MiB,旧 OceanDemo 带有多个大 GLB、WASM、EXR 和 HDR。
- 最大单文件 23.68 MiB,只比 Cloudflare Pages 25 MiB 限制低约 1.32 MiB,安全余量小。
- 没有基于网络条件、设备内存、GPU 型号做更细的质量分级。
- 没有真实 RUM、Core Web Vitals 和错误采集,只能依赖本地 QA 与浏览器测试。
- 首页海洋 iframe 的跨运行时协调仍有轮询,可改成显式
postMessage协议。
# 如果让你继续优化
第一步不是盲目重写,而是采集 Bundle 构成、LCP、INP、CLS、内存和 GPU 帧时间。第二步做页面级 React.lazy 和动态 import,把 Babylon 粒子、Cesium 和 PageFlip 从首屏主包分开。第三步清理未使用依赖和重复海洋资产,把大模型转换为 meshopt/Draco、KTX2/Basis 纹理,并设置长期缓存。第四步按设备能力选择实时海洋、简化 WebGL 海面或静态图。最后接入 RUM,确认优化对真实设备有效。
懒加载是不是越晚越好?
建议回答:不是。太晚会让用户滚到模块时看到明显等待。项目给 IntersectionObserver 设置正 rootMargin,在进入视口前预热。具体距离要结合资源体积、网络速度和用户滚动速度测量。
静态预览是否掩盖了真实性能问题?
建议回答:它改善感知体验,但不能替代性能优化。必须分别测量预览出现时间、实时场景可交互时间、下载体积和失败率。项目把慢与失败分开,并保留状态提示;后续仍应压缩资源和做能力分级。
如何定位 3D 页面卡顿?
建议回答:先区分网络、主线程和 GPU:看瀑布图与资源解码,Performance 面板的脚本和布局,GPU 帧时间、draw call、三角形、纹理与 DPR。然后逐项关闭地球、粒子、阴影、后处理和高分辨率,找最敏感因素。只看平均 FPS 不够,还要看长任务和低端设备。
为什么不能只依靠 tree shaking 解决主包过大?
建议回答:Tree shaking 只能移除可静态判断的未使用导出,实际使用的 Babylon、动画和页面代码仍会进同一 Chunk,第三方包也可能有副作用。真正减少首屏下载还需要动态 import、路由拆包、替代重依赖和资源压缩。
DPR 为什么会显著影响 3D 性能?
建议回答:Canvas 内部像素数约等于 CSS 宽×高×DPR²。DPR 从 1 升到 2,不是两倍而是约四倍像素填充和部分后处理成本。项目限制地球 resolutionScale 和 Loader DPR,保证高分屏清晰度与性能平衡。
backdrop-filter 和大模糊为什么贵?
建议回答:它们需要对较大区域采样和离屏处理,动态背景下还可能频繁重算,叠加多个大半径模糊会增加 GPU 和显存压力。项目把玻璃和光晕控制在局部,并在重 3D 页面避免无限叠层;是否昂贵最终要通过 DevTools 测量。
98 MiB 的静态站还能叫性能优化吗?
建议回答:不能用“做过优化”掩盖总体积仍大。已有工作解决了首屏竞争、模型压缩、懒加载和运行时负载,但产物预算仍不理想,尤其旧海洋资产。我的结论是体验可用但还有明确性能债,下一阶段会用代码分割、资源去重、KTX2/meshopt 和按设备质量档位继续降低成本。
如何设计 3D 场景错误监控?
建议回答:记录阶段化事件:能力检测、脚本下载、设备创建、Shader 编译、资源加载、首个有效帧、运行时 renderError 和 fallback。附带匿名化的浏览器、GPU capability、耗时和版本,不采集敏感内容。监控 fallback 率、P50/P95 ready 时间和崩溃设备分布。
如何做 3D 功能灰度发布?
建议回答:用远程配置或边缘分流给稳定用户 ID 分桶,保留 static、video、realtime 三种策略。先内部和小流量,比较 LCP、INP、错误、fallback、停留与转化,出现异常能立即切回静态,不需要重新部署全部页面。
# 响应式、无障碍与内容合规
# 响应式策略
全站共享的最终断点集中在 Responsive.css,主要覆盖桌面、平板、760px 手机、320px 小屏和短横屏。不是只改变列数,还调整导航形态、3D 相机距离、粒子密度、标题分行、触摸目标、安全区和浮动元素位置。
# 无障碍措施
- 页面使用真实 heading、link、button,不用 div 模拟所有交互。
- 图标动画元素通常
aria-hidden,Canvas 和交互模块提供可读标签。 - 弹窗使用 dialog 语义,并提供关闭按钮和明确图片 alt。
- 菜单支持 Escape 关闭;按钮保留 focus 样式。
- 动画尊重 reduced motion;键盘焦点不会停在透明元素上。
- 地球目的地同时提供按钮列表,不要求用户只能拖拽 Canvas。
- 状态变化使用
role=status或aria-live。
# 当前无障碍不足
海报弹窗还应系统验证焦点陷阱、打开后的初始焦点、关闭后的焦点恢复和背景 inert;3D Canvas 的完整等价文本仍可以更丰富;颜色对比需要用自动工具和人工复核;真机读屏和键盘全流程尚未形成正式验收记录。
# 内容与素材合规
项目保留字体许可证、React Bits 许可证、OceanDemo 许可证、Babylon Runtime 许可证、船模来源和地球纹理来源。宣传文案做过广告法风险复查,但图片海报中的部分表述和讲师履历仍需要真实材料支持。技术面试中可以说“建立了素材来源和文案风险清单”,不能说“已经获得完整法律合规认证”。
响应式为什么不能只用一个 768px 断点?
建议回答:页面不仅有普通卡片,还有固定导航、长中英日标题、3D Canvas、翻页书和短横屏。设备宽度相同但高度、输入方式和语言长度不同。项目用内容实际破坏点决定断点,并对 320px 和短横屏做专项检查。
Canvas 内容怎样做无障碍?
建议回答:先判断它是装饰还是功能。装饰 Canvas 应从可访问树隐藏;功能 Canvas 需要可读名称,并在 DOM 中提供等价操作和信息,例如地球的目的地按钮与文字读数。不能指望读屏理解像素。
# 第五部分 测试、质量与静态部署
# Playwright 测试矩阵
当前测试代码定义 34 个用例:五条路径×三种语言×两种宽度共 30 个,再加四个交互与异常用例。核心检查包括:
html lang与页面标题正确。- 首页链接、页脚内容和页面结构存在。
- 英文 main 区域没有遗漏汉字。
- 字体 ready 后横向溢出不超过 2px。
- 监听未捕获 page error。
- 语言切换后导航、刷新、历史返回保持一致。
- 移动菜单与海报语言同步。
- 折叠图片支持 click 和键盘 Enter,不因 hover 意外改变。
- 非法语言回退;localStorage 抛错时页面仍可工作。
# 为什么浏览器测试不等于全部质量
测试使用 Chrome channel 和 reduced motion,不能代表 Safari、Firefox、Android GPU、iOS WebKit 与真实触摸设备。海洋 WebGPU 尤其依赖浏览器和驱动。自动化还没有覆盖每种 3D 失败、内存泄漏、视觉像素差异和网络弱网,因此项目保留了人工 QA 记录和明确边界。
# 产物检查
check-pages.mjs 递归统计 dist:文件数不能超过 20000,单文件不能超过 25 MiB,并检查 index.html、_redirects、关键海洋图、Cesium 主脚本和三语海报存在。当前已有 dist 检查结果是 845 个文件,最大文件约 23.68 MiB。
# 当前环境验证状态
本次整理文档时,单独运行已有 dist 的产物检查成功。生产构建、Oxlint 和需要启动 Vite preview 的 Playwright 未能重新执行,因为现有 node_modules 缺少 Rolldown 与 Oxlint 的 macOS 可选原生绑定。这个问题指向依赖安装状态,不代表源码测试一定失败;但在重新 npm ci 并实际跑通前,不能声称本次全量验证通过。
# Cloudflare Pages 部署
项目是纯静态站,不需要后端和环境密钥。部署产物是 dist,_redirects 处理 History 路由直接刷新。发布后应逐条检查五个路径的直接打开和刷新、三语切换、海报下载、二维码和 3D 场景。自定义域名尚受域名账号问题影响,不能把“具备部署配置”说成“正式域名已上线”。
单元测试、集成测试和 E2E 在这个项目怎么分?
建议回答:纯函数如翻译替换、海天线平滑和路径规范化适合单元测试;React 与 3D 外壳的状态切换适合集成测试;语言、导航、弹窗和刷新属于 Playwright E2E。当前 E2E 较完整,但单元和组件测试仍不足,是后续改进点。
为什么测试要等待 `document.fonts.ready` 再检查溢出?
建议回答:后备字体和最终字体的字宽不同。如果过早测量,测试可能在后备字体下通过,真实字体加载后却溢出,或反过来产生不稳定结果。等待字体完成能让布局断言更接近用户最终看到的状态。
生产构建失败但旧 dist 检查通过,说明什么?
建议回答:只能说明现有 dist 满足文件约束,不能证明当前源码能重新构建。旧产物可能来自之前正常的依赖环境。交付前必须修复依赖安装并重新 build,再对新 dist 执行检查,不能用旧产物替代源码可构建性。
为什么选择静态部署而不是服务器?
建议回答:当前业务没有登录、数据库和服务端动态内容,静态部署能减少运维面、密钥和攻击面,并利用 CDN 分发。代价是运行时 SEO、多语言预渲染和动态内容能力有限;需求变化后再引入 SSR 或 API,而不是为假想需求先上服务器。
Cloudflare Pages 缓存怎样设计?
建议回答:带内容 Hash 的构建资产可以长缓存并 immutable;`index.html` 应较短缓存或可重新验证,确保能指向最新 Hash;public 中固定文件名的海洋资产要么版本化 URL,要么谨慎设置缓存并提供更新策略。发布采用原子版本,避免 HTML 与资产不一致。
为什么没有后端监控和错误上报?
建议回答:当前交付范围是静态演示站,仓库里没有接入 RUM 或错误平台,所以我不会声称有线上可观测性。正式发布后至少应采集 Web Vitals、资源失败、WebGPU/Cesium 初始化阶段、设备能力和 JS error,并避免上传隐私数据。
# 第六部分 技术难点
# 难点一 WebGPU 海洋真实就绪与渐进降级
一句话结论
项目最难的不是把海洋 Demo 放进页面,而是定义“什么时候它真的可展示”,并让不支持、加载慢和初始化失败三种情况都有合理体验。
两分钟回答
最初很容易把 iframe 的 load 或 Scene 创建成功当成 ready,但实际会出现 Loader 消失后只有天空、黑帧或还没编译好的海面。根因是 WebGPU 设备初始化、WGSL 编译、纹理上传和第一帧有效绘制发生在文档 load 之后,而且不同设备差异很大。
我的处理分四层。第一层先看 navigator.gpu,没有入口直接静态图。第二层等 iframe 场景实例并检查 Babylon 的 compute shader capability,避免把诊断场景当成成功。第三层在相机和材质配置完成后监听渲染,只有海洋网格真的产生有效 draw call 才标记 ready。第四层再等两个 animation frame,让浏览器完成 iframe 显示与合成后才结束 Loader。
同时我把慢和失败分开:30 秒只是显示预览,后台继续准备;明确失败才停止实时场景并给重试按钮。这样兼顾了首屏可见性和高端设备效果。边界是现在父子通信仍有轮询,后续会改成 iframe 内部通过 postMessage 发布 booting/surface-ready/error 状态,并记录各阶段耗时。
可能追问
- 为什么不是固定 5 秒后显示?因为设备差异大,时间不等于状态。
- 为什么要双 RAF?第一帧提交 DOM 状态,下一帧更有机会看到已合成结果。
- 为什么不直接取消慢加载?慢设备可能只是首次 Shader 编译,取消会让用户永远无法看到实时场景。
# 海洋就绪判断中曾经出现什么问题?
建议回答:早期我曾把“场景对象创建”理解为“用户已经能看到海面”,导致 Loader 退出时露出天空或未完成帧。这个错误让我意识到异步图形系统要定义业务可见的 ready,而不是复用某个技术事件。后来我把 ready 拆成能力、场景、表面 draw call 和浏览器合成四个阶段,并把慢与失败分开。后续如果重做,我会从一开始就设计明确状态机和耗时埋点。
# 难点二 Babylon 5.x 与 9.x 的运行时隔离
一句话结论
我没有把旧海洋代码硬升级后直接塞进主包,而是先用 iframe 把版本风险限制在一个边界内,再通过小接口完成站点化改造。
两分钟回答
海洋上游基于 Babylon 5.x,主站粒子场已经使用 Babylon 9.x。两个版本在引擎 API、材质扩展、Shader 注册和全局对象上存在潜在冲突。如果一次性升级整个 OceanDemo,既要验证 FFT 计算正确性,又要验证所有画面和资源,改动面太大。
我选择把上游构建保留成同源 iframe。它拥有独立 Window、Canvas 和依赖,主站只负责等待 Scene、固定相机、调整天空、替换船模和接收就绪信号。这个方案把迁移风险和主站迭代解耦,同时保留了回退图。
我也会主动说明代价:两套 Babylon 增加下载体积,父页面直接访问子页面对象不是理想公共接口,同源限制也较强。长期演进是先补海洋视觉回归和性能基线,再逐步把上游升级到 Babylon 9,最终改成动态 import 的独立模块或 Worker/OffscreenCanvas 可控边界。
# 难点三 重 3D 体验与首屏性能之间的平衡
一句话结论
我的策略不是一刀切删除 3D,而是按用户可见性和设备能力安排加载顺序,让信息先可用,增强体验后到。
两分钟回答
首页有 WebGPU 海洋,课程页有 Babylon 粒子,公开课页有 Cesium 地球。如果它们同时进入首屏主链路,会争抢网络、JS 解析、GPU 和显存。我先把内容层与 3D 层分离,所有关键信息都用正常 DOM;海洋始终有静态预览,地球进入视口才加载,粒子和地球在卸载时完整释放。
运行时方面,地球关闭默认连续渲染并限制 45 FPS,页面隐藏或不可见时不推进;Loader Canvas 从持续重画改成一次绘制加 CSS 合成;移动端降低粒子量和相机负荷;所有自动运动响应 reduced motion。资源方面压缩船模,并用构建脚本检查 25 MiB 单文件上限。
验证上,除了看 FPS,我会看首屏静态内容出现、实时场景 ready、主线程长任务、资源瀑布和 GPU 占用。当前最大的遗留问题是主包和总体积仍大,所以不能回答“性能已经彻底解决”,而应说明下一步是代码分割和 3D 资源压缩。
# 难点四 三种语言、路由和资源状态一致性
一句话结论
难点不只是翻译文案,而是保证语言在分享链接、站内导航、刷新、历史返回、弹窗资源和存储不可用时都只有一份一致状态。
两分钟回答
我把 URL 作为明确意图,localStorage 作为偏好,模块 Store 作为运行时快照。首次进入按 URL、存储、中文默认的优先级解析;切换语言用 replaceState 更新当前 URL,不增加多余历史;站内路径统一通过 localizedPath 携带语言。浏览器前进后退和跨标签 storage 变化也会同步 Store。
课程海报不是只换按钮文字,还要切换实际图片、下载文件名和 dialog alt。Playwright 因此不仅断言某个标题,而是覆盖五页、三语、桌面和手机,并额外测试导航、刷新、回退、海报切换和 localStorage 抛异常。当前不足是中文句子作为 Key,不利于长期维护,规模扩大后会迁移到稳定语义 Key 和翻译完整性检查。
# 难点五 3D 海天线与 DOM 视觉层精确对齐
一句话结论
我没有把暖色遮罩的位置完全写死,而是把 3D 世界中的海平面远点投影到屏幕,再用低通式限制更新写给 CSS 变量。
两分钟回答
首页 DOM 上有文字、渐变和氛围层,iframe 里有独立 3D 相机。只用固定百分比对齐,在不同宽高比和相机矩阵下会偏离真实海天线。我的做法是取相机水平前向,在 y=0 海平面上选择很远的点,用相机自己的 view 和 projection matrix 投影到 viewport,得到屏幕 y,再更新父容器的 --hero-horizon-y。
这里还有一个坑:Scene 当前 transform matrix 可能在反射探针或上一帧状态,所以我显式用固定主相机的 view×projection 矩阵,而不是盲信 Scene matrix。为了避免每帧像素噪声让 CSS 层抖动,更新函数还会做范围限制和稳定处理。它体现的是跨 Canvas 和 DOM 坐标系的统一,而不只是调 CSS。
# 第七部分 项目复盘
# 需求变化时,怎样避免影响其他模块?
建议回答:我先区分内容、布局、视觉参数和底层能力。像课程文案与语言资源集中在 content 模块,路由和页面壳不需要跟着改;3D 场景通过独立组件和 iframe 隔离,调整天空或船模不会扩散到全部页面。变化如果涉及路由数量和 SEO,我会重新评估手写路由与 CSR 是否仍合适,而不是继续往原结构堆特殊判断。
# 怎样把视觉要求变成可检查的交付目标?
建议回答:我会把主观的“更有科技感”转换成可检查目标:构图、海天线位置、首屏文字对比、动效强度、移动端折行、加载等待和失败状态。每轮保留同视口截图与 QA 记录,先处理遮挡、溢出和交互阻断,再做颜色与氛围微调。对于宣传文案和素材授权,技术实现不能替代事实核验,我会把需要业务确认的项单独列出。
# 项目还有哪些不足,下一步先改什么?
建议回答:最大的遗憾是体验完成度高于产物治理完成度。当前仍存在主包大、旧海洋资产重复、单文件接近平台上限、CSR 多语言 SEO 不完整、缺少线上 RUM。这些不是一句“做了懒加载”就能掩盖的。我会优先补新鲜构建基线和性能预算,再做路由拆包、资源格式升级、设备质量分级和可观测性。
# 第八部分 重点问题速查
以下是项目的精选问题入口,不是另一份重复题库。面向交易类前端岗位,优先关注状态一致性、Canvas 生命周期、加载失败和性能控制。
| 问题与答案跳转 | 回答要点 |
|---|---|
| 项目介绍 | 三语官网、静态 SPA 与业务边界 |
| 你主要负责什么? | 第三方基础与个人工程贡献 |
| React 负责 3D 场景里的每一帧吗? | React 界面状态与引擎每帧更新分开 |
为什么国际化状态使用 useSyncExternalStore? | URL、存储与共享语言快照 |
| 为什么 iframe 的 load 事件不能代表海洋 ready? | 文档加载不等于真实画面就绪 |
| 30 秒超时为什么不直接判失败? | 慢加载、失败与降级分开处理 |
| 难点二 Babylon 5.x 与 9.x 的运行时隔离 | 隔离收益、集成代价与升级边界 |
| 为什么 3D 场景必须显式 dispose? | 清理渲染循环与 GPU 资源 |
| 如何定位 3D 页面卡顿? | 网络、主线程与 GPU 分别定位 |
| 响应式为什么不能只用一个 768px 断点? | 语言、输入方式和实际布局破坏点 |
| 单元测试、集成测试和 E2E 在这个项目怎么分? | 测试分层与尚未覆盖的范围 |
| 项目还有哪些不足,下一步先改什么? | 产物治理、SEO 与线上观测 |
# 附录 项目证据与术语速查
# 代码证据索引
| 主题 | 主要文件 | 可核对内容 |
|---|---|---|
| 应用入口 | src/main.jsx | React 19 createRoot、StrictMode、全局样式 |
| 路由 | src/Router.jsx、src/navigation.js | 五路由、History、链接委托、旧 Hash、metadata |
| 国际化 | src/useLocale.js、src/i18n.js、src/content/messages.json | 优先级、Store、递归翻译、存储异常 |
| 首页 | src/App.jsx | 海洋、Loader、内容和转化入口 |
| 海洋外壳 | src/OceanScene.jsx | iframe 隔离、就绪、相机、船、降级、重试 |
| 海洋就绪 | src/oceanReadiness.js | 表面有效渲染判定 |
| 海天线 | src/oceanHorizon.js | 屏幕位置更新稳定逻辑 |
| 海洋源码 | vendor/OceanDemo/src/scenes、src/ocean | 波谱、FFT、几何、材质、浮力、天空 |
| WebGPU 入口 | vendor/OceanDemo/src/index.ts | WebGPUEngine 和支持检测 |
| 粒子场 | src/AdmissionsParticles.jsx | Babylon 9、图标纹理、相机和清理 |
| Cesium | src/CareerGlobe.jsx | 动态加载、测地线、按需渲染、销毁 |
| 动画 | src/reactbits/SiteMotion.jsx、ScrollFloat.jsx、BlurText.jsx | GSAP、Motion、键盘与 reduced motion |
| 翻页书 | src/AIEvolutionBook.jsx | PageFlip 与页码状态 |
| 海报 | src/CoursePoster.jsx | dialog、三语资源、下载、键盘 |
| 构建 | vite.config.js | WGSL、Cesium copy、3D 资源类型 |
| 测试 | playwright.config.js、tests/browser/i18n.spec.js | 浏览器矩阵和异常路径 |
| 发布检查 | scripts/check-pages.mjs、public/_redirects | Pages 限制、关键资产、SPA fallback |
| 部署说明 | DEPLOYMENT.md | Cloudflare 流程、语言与域名边界 |
| 设计与响应式证据 | design-qa.md、responsive-qa.md | 多轮视觉、交互和视口验证 |
| 文案风险 | ad-copy-review.md | 已调整和仍需事实核验的内容 |
# 技术术语速查
| 术语 | 一句话理解与详细介绍 |
|---|---|
| SPA | Single-Page Application,单页应用;浏览器拿到页面入口后,由客户端切换内容详见:手写 SPA 路由与 History API |
| CSR | Client-Side Rendering,客户端渲染;本项目由浏览器执行 React 来生成界面详见:SPA 的 SEO 有什么问题? |
| SSR / SSG | Server-Side Rendering / Static Site Generation;分别在服务端请求时、构建时生成页面 HTML,是现有 SEO 问题的改进方向详见:如果要把当前站点改成 SSR,最先改什么? |
| WebGL / WebGPU | 浏览器 GPU API;WebGL 主要用于图形渲染,WebGPU 还提供通用计算管线详见:Babylon.js、WebGL 与 WebGPU |
| WGSL | WebGPU Shading Language,WebGPU 着色器语言;项目用它编写海洋计算着色器详见:WGSL 为什么不能按普通资源 URL 处理 |
| FFT / IFFT | Fast Fourier Transform / Inverse Fast Fourier Transform;海洋通过逆变换把频域波谱还原为空间位移详见:FFT 和 IFFT 的区别是什么? |
| Compute Shader | 计算着色器;用 GPU 并行处理波谱、纹理等数据,不只是绘制像素详见:Compute Shader 如何执行 |
| Ping-pong | 交替使用两份纹理,让上一阶段的输出成为下一阶段的输入详见:为什么需要 ping-pong 纹理? |
| Draw Call | 向 GPU 提交绘制任务;项目也用有效绘制判断海洋表面是否真正参与渲染详见:Draw Call 为什么重要? |
| DPR | Device Pixel Ratio,设备像素比;升高会增加画布内部像素数与渲染成本详见:DPR 为什么会显著影响 3D 性能? |
| RAF | requestAnimationFrame,浏览器帧回调;用于动画和与绘制节奏一致的更新详见:requestAnimationFrame、setTimeout 和 setInterval 怎样选择? |
| Dispose | 显式释放引擎、场景等资源;移除 DOM 不等于停止渲染或释放 GPU 资源详见:为什么 3D 场景必须显式 dispose? |
| Reduced motion | 用户的减少动态效果偏好;保留信息,关闭或减弱不必要的运动详见:reduced motion 应该怎样处理? |
| SPA fallback | 静态服务器把页面路径回退到 index.html,让客户端路由接管直接访问详见:直接刷新为什么可能 404 |
| RUM | Real User Monitoring,真实用户监测;用于观察真实设备的体验,当前项目尚未接入详见:为什么没有后端监控和错误上报? |
# 历史记录中的项目数据
以下沿用原手册记录,描述的是当时的代码、产物和 QA 基线,不代表本次重新测量或当前线上表现。
| 项目事实 | 数字 |
|---|---|
| 正式页面路径 | 5 条 |
| 支持语言 | 3 种:中文、日文、英文 |
| Playwright 测试代码定义 | 34 个用例 |
| 主要视口矩阵 | 1440px 与 390px;历史 QA 还检查 320/768/1024/1440 |
| Cesium 动画上限 | 约 45 FPS |
| 海洋慢加载预览阈值 | 30 秒 |
| Loader 最短展示 | 约 1.2 秒,完成推进约 560ms |
| 当前 dist 文件数 | 845 |
| 当前 dist 总体积 | 约 98 MiB |
| 主 JavaScript 产物 | 约 7 MiB |
| 最大单文件 | 约 23.68 MiB GLB |
| Pages 单文件检查上限 | 25 MiB |
| 压缩后船模 | 约 3.2 MiB,纹理 512×512 |
# 系统设计扩展(非现有实现)
以下保留原手册的扩展方案,不表示官网已经具备报名支付、AI 助手或百万访问量的生产验证。
# 如果要增加报名与支付
不要把表单直接提交到静态页面。应增加后端 API、身份与防滥用、数据库、订单状态机、支付平台 Webhook、幂等键和审计。浏览器创建订单,服务端生成支付请求;支付成功以签名 Webhook 为准,不信任前端跳转结果。个人信息最小化收集并设置保留期限。
# 如果要增加 AI 课程助手
前端只负责对话 UI 和流式展示;后端负责模型密钥、鉴权、限流、Prompt 版本、检索权限、日志脱敏、超时和费用。课程资料进入可追踪的 RAG 索引,回答带来源;工具调用经过服务器权限和参数校验。建立固定评测集,分别评估正确性、引用、安全、延迟和成本。这样才是这个站点从“AI 内容官网”升级到“AI 应用”的真实链路。
# 如果要做百万访问量
静态 HTML、JS 和媒体继续由 CDN;大模型和视频不要和主站产物混在同一发布路径。图片视频使用专门媒体服务和自适应格式。启用 RUM、错误采集和缓存命中率监控。3D 场景按设备、网络与实验分流,并提供低成本版本。报名与 AI API 独立扩展,使用限流、队列和降级,避免营销流量拖垮核心服务。
# 简历项目描述参考
以下内容应根据你本人真实职责删改,不要把参考答案直接当成个人经历。
- 负责 AI 全栈课程品牌官网从信息架构、React 组件、三语内容到 Cloudflare Pages 交付,完成首页、课程、公开课、招生与品牌五条业务路径。
- 集成 WebGPU 计算着色器与二维逆 FFT 动态海洋,使用 iframe 隔离 Babylon.js 5.x 与主站 9.x 运行时,设计真实首帧检测、静态预览、失败回退与重试机制。
- 使用 Babylon.js 实现可交互技术粒子场,使用 Cesium 实现椭球测地线全球航线,并通过视口懒加载、按需渲染、帧率上限和完整资源清理控制 GPU 成本。
- 设计 URL 与 localStorage 协同的中英日国际化状态,覆盖导航、历史、刷新、弹窗资源和无存储环境;通过 Playwright 建立 34 个浏览器用例的测试矩阵。
- 配置 Vite 的 WGSL 源码加载、Cesium 运行时资产复制与 Cloudflare SPA 回退,增加文件数、25 MiB 单文件限制和关键资产完整性检查。