HUYA-直播m站&视频站重构
一、项目背景与概述
1.1 项目起源
虎牙直播移动端 M 站(mobile-ssr)和视频站(huya_video)原先均基于 PHP + Smarty 模板引擎 构建。随着业务快速迭代,老架构暴露出一系列问题:
| 痛点 | 具体表现 |
|---|---|
| 开发效率低 | PHP 模板与前端逻辑耦合严重,前后端无法独立迭代 |
| 维护成本高 | 代码年久失修,Smarty 模板嵌套层级深,新人上手困难 |
| 性能瓶颈 | PHP 渲染链路长,首屏时间不可控,无法做精细化性能优化 |
| 技术栈陈旧 | 无法使用现代前端生态(组件化、TypeScript、工程化工具链) |
| SEO 与体验矛盾 | CSR 方案无法兼顾 SEO,纯 PHP 渲染又无法提供流畅的 SPA 体验 |
基于以上问题,团队决定将两个站点全面重构为 React + TypeScript + SSR 架构,在保证 SEO 的同时,大幅提升开发效率和用户体验。两个项目共享同一套 SSR 底层框架 @hnf-next/light,实现架构统一。
1.2 两个子项目定位
| 项目 | 定位 | 核心页面 | 访问域名 |
|---|---|---|---|
| mobile-ssr | 虎牙 M 站(移动端直播门户) | 首页、直播间、分类列表、搜索 | m.huya.com |
| huya_video | 虎牙视频站(PC 端视频门户) | 首页、视频播放页、频道页、搜索、排行榜 | v.huya.com |
两个项目共享同一套 SSR 底层框架 @hnf-next/light,但在业务逻辑、组件设计和运行时架构上各有特色。
二、技术栈总览
┌─────────────────────────────────────────────────────────────┐
│ 技术栈全景图 │
├──────────────┬──────────────────┬───────────────────────────┤
│ 层级 │ mobile-ssr │ huya_video │
├──────────────┼──────────────────┼───────────────────────────┤
│ UI 框架 │ React 17 + TS 4.2 │
│ 状态管理 │ Redux 4 + Redux Thunk + React-Redux 7 │
│ 路由 │ React Router DOM 5 + 文件路由 │
│ SSR 框架 │ @hnf-next/light (基于 Koa 2) │
│ 构建工具 │ Webpack 5 + Babel 7 │
│ 样式方案 │ SCSS + PostCSS + Autoprefixer │
│ RPC 通信 │ TAF/TARS (@tars/rpc + @leaf-tars-node/rpc) │
│ HTTP 客户端 │ Axios + JSONP Adapter │
│ 监控 & 追踪 │ Sentry 6 + YA Report │
│ 部署平台 │ LEAF Serverless Platform │
│ CDN │ msstatic.com (生产) / test-hd.huya.com (测试)│
├──────────────┼──────────────────┼───────────────────────────┤
│ 业务特有 │ @huyafed/web- │ LibLoader 动态加载 │
│ │ signal-sdk │ SharedArrayBuffer Token │
│ │ (WebSocket 信令) │ (视频编解码能力) │
│ │ @huyafed/wechat │ 弹幕系统 / 赛事系统 │
│ │ share (微信分享) │ │
│ │ HYPlayer (直播 │ 视频播放器 (VOD) │
│ │ 播放器 FLV/HLS) │ │
└──────────────┴──────────────────┴───────────────────────────┘
三、整体架构设计
3.1 SSR 渲染架构
两个项目均采用基于 @hnf-next/light 的服务端渲染架构,核心流程如下:
3.2 页面文件约定(文件路由协议)
每个页面文件遵循统一的导出约定:
// 1. 默认导出:React 组件(必须)
export default (props: PageProps) => <PageView {...props} />;
// 2. 服务端数据获取(SSR 阶段执行)
export const getServerProps = async (leafReq, opt) => {
const data = await fetchFromTAF();
return { ...data };
};
// 3. HTML Head 注入(Meta、TDK、样式、脚本)
export const setHeader = async (leafReq, opt) => {
return ['<title>...</title>', '<meta ...>', '<script>...</script>'];
};
// 4. Body 尾部注入(统计脚本、首屏打点)
export const setBodyTag = async () => {
return ['<script>window.performanceInfo.firstScreenTime=Date.now()</script>'];
};
// 5. 响应拦截/重写(用于 UA 检测、重定向)
export const rewriteResponse = async (leafReq) => {
if (isMobile(leafReq)) return { statusCode: 302, headers: { location: mobileUrl } };
};
3.3 数据流架构
四、mobile-ssr(M 站)详细分析
4.1 项目结构
mobile-ssr/
├── pages/ # 页面入口(文件路由)
│ ├── index.tsx # 首页:推荐直播 + 轮播 + 游戏分类
│ ├── [room].tsx # 直播间:动态路由(房间号/靓号)
│ ├── g/[id].tsx # 分类列表页
│ ├── l/[id].tsx # 直播列表页
│ ├── search.tsx # 搜索页
│ └── error.tsx # 错误页
├── components/
│ ├── common/ # 通用组件(Header, Footer, Nav, Slider)
│ ├── room/ # 直播间组件
│ ├── game/ # 游戏分类组件
│ ├── search/ # 搜索组件
│ ├── tag/ # 标签组件
│ └── base/ # 基础组件(Link, OpenApp, Card)
├── common/
│ ├── server/sevices/ # TAF RPC 服务层
│ ├── client/modules/ # 客户端交互模块
│ ├── store/ # Redux 状态管理
│ ├── types/ # TypeScript 类型定义
│ ├── utils.ts # 工具函数
│ └── constant.ts # 环境常量
├── serverless/ # Serverless 函数
├── jce/ # JCE 协议文件
├── tafstruct/ # TAF 结构体定义
├── build/ # 构建脚本 & Webpack 配置
└── api/ # API 端点
└── healthCheck.ts
4.2 核心业务:直播间实现
直播间是 M 站最核心、最复杂的页面,涉及视频流播放、实时弹幕、礼物系统、信令通信等多个子系统。
4.2.1 直播间类型区分
4.2.2 直播间初始化流程
4.2.3 视频播放方案
// 流媒体协议选择策略
class Video {
init() {
if (supportFLV()) {
this.streamType = 'flv'; // 优先 FLV(低延迟)
} else {
this.streamType = 'hls'; // 降级 HLS(兼容性好)
}
}
}
// 播放质量监控
// - NO_PICTURE 检测: 10s 超时无画面 → 上报黑屏率
// - PLAY_CARTON 检测: 每 20s 统计一次卡顿次数
// - 码率 & 线路追踪: 按 webPriorityRate 选择最优线路
4.2.4 弹幕系统设计
弹幕系统采用 对象池 + CSS Transform 动画 方案:
- 最多 10 条弹幕轨道,动态分配空闲轨道
- 等待队列上限 200 条,超出丢弃旧消息
- 根据队列积压量动态调整弹幕滚动速度
- 分辨率自适应字体大小
- DOM 节点复用(对象池模式),减少 GC 压力
- CSS Transform 硬件加速,保持 60fps 渲染
4.3 Redux 状态架构
4.4 移动端特有处理
| 能力 | 实现方式 |
|---|---|
| UA 检测重定向 | rewriteResponse 中检测桌面 UA → 302 跳转 www.huya.com |
| App 唤起 | @huyafed/openapp 组件,支持 Universal Link + Scheme |
| 微信分享 | @huyafed/wechatshare,配置分享标题、图片、链接 |
| 平台标识透传 | URL 白名单参数 platForm, autoOpenApp, hideDownApp 等 |
| 竖屏适配 | 颜值直播间使用 r=270 图片旋转 + 竖屏播放器布局 |
五、huya_video(视频站)详细分析
5.1 项目结构
huya_video/
├── pages/
│ ├── index.tsx # 首页:Banner + 推荐视频 + 站点地图
│ ├── play/[videoId].tsx # 视频播放页(核心)
│ ├── search.tsx # 搜索页(多类型:视频/播客/综合)
│ ├── g/[channel].tsx # 频道页
│ ├── cat/[category].tsx # 分类页
│ ├── rank/[channel].tsx # 排行榜
│ └── error.tsx # 错误页
├── components/
│ ├── common/ # Header, Pagination, Loading, Error, VideoList
│ ├── home/ # Banner, Sidebar, Recommender, Sitemap
│ ├── play/ # Player, Info, Match, DanmuList,
│ │ VideoCollection, RelativeVideo
│ ├── search/ # 多维搜索组件
│ ├── channel/ # 频道视图
│ └── rank/ # 排行榜视图
├── common/
│ ├── server/sevices/ # TAF 服务层
│ ├── client/
│ │ ├── utils/ # report, sentry, createAxios
│ │ ├── modules/ # 各页面客户端交互逻辑
│ │ ├── styles/ # SCSS 样式
│ │ ├── services/ # JSONP 服务
│ │ └── plugins/ # YA Report 插件
│ ├── types/ # TS 类型
│ ├── jce/ # JCE 协议消息类
│ └── utils/
│ ├── function.ts # 通用工具
│ └── constant.ts # 环境配置 & HTML 注入
├── build/ # 构建系统
└── api/
└── healthCheck.ts
5.2 核心业务:视频播放页
视频播放页是视频站最复杂的页面,承载视频播放、弹幕、赛事信息、合集推荐等功能。
5.2.1 播放页 SSR 数据获取
5.2.2 播放器加载策略
播放器关键配置:
const options = {
channelId: videoData.channelId,
auto_play: 1,
vid: videoData.vid,
no_danmu: 0, // 开启弹幕
from: 'vhuyaweb',
enableH265: true, // 支持 H.265 编码
source: 'play'
};
- 通过
<meta http-equiv="origin-trial">注入 Origin Trial Token - 用于启用 SharedArrayBuffer,提升视频编解码性能(H.265 解码性能提升 30%+)
- 不同环境(本地/测试/生产)配置不同的 Token
5.2.3 播放质量全链路监控
5.3 图片 CDN 处理(双 OSS 方案)
视频站支持两种 OSS 图片处理服务,根据 URL 自动匹配:
function createScreenshot(url: string, options: { w: number, h: number, r?: number }) {
if (isAliyunOSS(url)) {
// 阿里云 OSS 图片处理
return `${url}?x-oss-process=image/resize,limit_0,m_fill/w_${w}/h_${h}/sharpen,80/quality,q_90`;
} else {
// 虎牙自建图片服务
return `${url}?imageview/4/0/w/${w}/h/${h}/blur/1`;
}
}
5.4 搜索系统
搜索页支持多维度搜索:
| 搜索类型 | 组件 | 说明 |
|---|---|---|
| 综合搜索 | GeneralResult | 默认搜索,聚合所有类型结果 |
| 视频搜索 | VideoResult | 筛选视频类型结果 |
| 播客搜索 | PodcastResult | 筛选播客类型结果 |
支持排序方式:相关度 / 播放量 / 发布时间,通过 URL 参数 type 和 order 控制。
5.5 SEO 优化策略
// 动态 TDK 生成(以频道页为例)
setHeader = async (leafReq, opt) => {
const channelName = opt.serverProps.channelInfo.name;
return [
`<title>${channelName}精彩视频_虎牙视频</title>`,
`<meta name="keywords" content="${channelName},${channelName}视频,虎牙视频">`,
`<meta name="description" content="虎牙视频${channelName}频道...">`,
// XSS 过滤
filterXss(dynamicContent)
];
};
六、TAF/TARS RPC 通信层详解
6.1 架构总览
6.2 tafRequest 通用封装
interface RequestParams {
requestObj: string; // 服务标识: 'HUYASZ.HuyaVideoWebServer.HuyaVideoWebObj'
iface: string; // 接口方法: 'getRecommendList'
proxyConstructor: Function; // JCE 代理构造函数
timeout?: number; // 超时时间 (默认 3000ms)
req: Function; // JCE Request 类
rsp: Function; // JCE Response 类
data: object; // 请求参数
}
// 请求头统一封装
function getVideoReqHeader(): VideoReqHeader {
return {
lUid: getCookieVal('yyuid'), // 用户 ID
sGuid: getCookieVal('guid'), // 设备唯一标识
sCookie: getCookie(), // 完整 Cookie
sUserIp: getClientIP(leafReq), // 真实 IP (多级代理提取)
sHuYaUA: 'webh5&1.0.0&huyavideo' // 客户端标识
};
}
// 错误处理: 不抛异常,返回空响应对象
try {
return await tafCall(params);
} catch (e) {
logBiz(e);
return new params.rsp(); // 优雅降级
}
任何 TAF RPC 调用失败都不会导致页面白屏——catch 后返回空响应对象 new params.rsp(),页面用默认值渲染。默认 3000ms 超时,避免单个慢服务拖垮整个 SSR 响应。大型赛事期间后端流量激增时,这种容错机制尤为关键。
6.3 JCE 协议文件
项目通过 .jce 文件定义 RPC 接口和数据结构,编译为 JavaScript 类:
jce/
├── HuyaVideoWebServerProxy.js # 视频站 Web 接口代理
├── HuyaVideoBaseServerProxy.js # 视频站基础接口代理
├── WebLiveRoomServerProxy.js # 直播间接口代理 (M 站)
└── WebLiveConfigServerProxy.js # 直播配置接口代理 (M 站)
七、构建与部署体系
7.1 Webpack 构建策略
7.2 代码分割策略
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
libs: {
test: /react|redux|prop-types/, // 框架代码分离
priority: 10,
chunks: 'initial',
name: 'vendor'
}
}
}
}
// 产出结构:
// vendor.[hash].js → React/Redux 框架 (长期缓存)
// [page].[hash].js → 页面级代码
// [page].[hash].css → 页面级样式
7.3 静态资源处理
7.4 manifest.json 映射
{
"pages/index": {
"js": ["vendor.a1b2c3.js", "pages/index.d4e5f6.js"],
"css": ["pages/index.g7h8i9.css"]
},
"pages/play/:videoId": {
"js": ["vendor.a1b2c3.js", "pages/play/_videoId_.j0k1l2.js"],
"css": ["pages/play/_videoId_.m3n4o5.css"]
}
}
LEAF 平台根据 manifest.json 在 SSR 时自动注入对应页面的 JS/CSS 资源。
八、监控与数据体系
8.1 错误监控(Sentry)
Sentry.init({
dsn: 'https://xxx@fems.huya.com/368',
sampleRate: 1, // 100% 采样
integrations: [new BrowserTracing()], // 浏览器性能追踪
initialScope: {
tags: { url: location.href },
user: { id: getCookieVal('yyuid') }
},
debug: !isProduction
});
8.2 数据上报(YA Report)
// 初始化
initReport({
pro: isProduction ? 'huyasp' : 'huyasp_test',
eid: 'hysp/videoplay/pv/web', // 事件 ID
eid_desc: '展示/虎牙视频/播放页', // 事件描述
rso: getURLParam('rso') || getURLParam('from'), // 来源追踪
});
// 点击事件上报
reportClickEvent({
eid: 'hysp/videoplay/click/share',
eid_desc: '点击/视频播放/分享按钮'
});
8.3 性能监控
九、技术亮点总结
两个日活百万级站点(M 站 + 视频站)从 PHP + Smarty 全面迁移到 React SSR,共 15+ 页面全部完成迁移,整个过程零线上故障。视频播放页首帧时间从 3.2s 降低到 1.8s,播放器可用性从 99.5% 提升到 99.95%。
1. PHP → React SSR 逐页平滑迁移——零故障流量切换
两个日活百万级站点从 PHP + Smarty 全面迁移到 React SSR,最大的挑战不是"如何写新代码",而是"如何在不中断线上服务的前提下逐步替换"。我们采用了逐页迁移 + Nginx 流量路由的灰度策略:
- 页面级粒度控制:通过 Nginx
location规则,将已迁移完成的 URL Path 指向新的 Node.js SSR 服务,未迁移的路径继续走 PHP 渲染。比如先迁移首页/,观察一周数据无异常后再迁移直播间/[room],最后迁移搜索页等低优先级页面 - 双栈并行:迁移期间 PHP 服务和 Node SSR 服务同时运行,Nginx 做反向代理分流。任何页面出问题可以秒级回滚——只需修改 Nginx 规则重新指回 PHP
- 数据层兼容:新旧页面共享同一套 TAF 后端服务,不需要后端做任何改造。SSR 服务端通过
@tars/rpc直接调用与 PHP 相同的 TARS 接口 - SEO 无缝衔接:由于两套方案都是服务端渲染出完整 HTML,搜索引擎爬虫在迁移过程中感知不到任何变化,页面收录量和排名零波动
面试表述:这不是一个简单的"重写项目",而是一次在高流量线上环境下的渐进式架构升级。就像给飞行中的飞机换引擎——每次只换一个页面,每个页面都有灰度验证和秒级回滚能力。最终两个站点共 15+ 页面全部迁移完成,整个过程零线上故障。
2. 统一 SSR 框架协议——类 Next.js 的自研文件路由系统
两个项目共享同一套 @hnf-next/light SSR 框架,它的核心设计是约定大于配置的文件路由协议,每个页面文件统一导出 5 个生命周期钩子:
export default Component → React 页面组件(必须)
export getServerProps → 服务端数据获取(SSR 阶段)
export setHeader → HTML Head 注入(TDK/Meta/脚本)
export setBodyTag → Body 尾部注入(打点脚本)
export rewriteResponse → 响应拦截/重写(UA 重定向等)
- 动态路由:
pages/[room].tsx→/12345,pages/play/[videoId].tsx→/play/67890.html,框架自动解析路径参数注入leafReq.params - 服务端/客户端分离:
getServerProps只在 Node.js 端执行,不会打包到客户端 bundle 中,TAF RPC 调用代码完全不暴露给浏览器 - 统一数据注水:
getServerProps返回值自动序列化到window.__INITIAL_STATE__,客户端 React Hydration 时自动从中恢复 Redux Store
这套设计让两个项目的开发者遵循完全一致的开发范式——新人看过一个页面的代码就能开发任意页面,跨项目支援也没有学习成本。
面试表述:我们实际上在 Next.js 大规模普及之前就实现了类似的文件路由 + SSR 数据获取约定。这套框架让两个不同业务的站点在架构层面完全统一,团队协作效率提升了约 40%。
3. TAF/TARS RPC 通信层封装——优雅降级 + 全链路类型安全
SSR 服务端需要调用 10+ 个 TARS 后端微服务获取数据,我们封装了一层通用的 tafRequest 来统一处理所有 RPC 调用:
- JCE 二进制序列化:请求参数和响应数据通过 JCE(Java Compact Encoding)二进制协议编解码,比 JSON 更紧凑高效。每个服务都有对应的
.jce定义文件,编译生成 JS Proxy 类 - 统一请求头:所有 RPC 调用自动注入
VideoReqHeader(包含 UID、GUID、Cookie、真实 IP、客户端标识),从 leafReq 中提取真实用户信息,穿透多级代理 - 优雅降级策略:任何 TAF 调用失败都不会导致页面白屏——catch 后返回
new params.rsp()(空响应对象),页面用默认值渲染。这保证了即使某个后端服务短暂不可用,页面仍然可以正常 SSR 输出 - 超时控制:默认 3000ms 超时,避免单个慢服务拖垮整个 SSR 响应时间
- 并行请求:
getServerProps内使用Promise.all并行调用多个 TAF 服务(如视频详情 + 频道推荐 + 赛事信息 + 合集数据),将串行变并行,SSR 数据获取耗时取决于最慢的那个服务而非所有服务的总和
面试表述:这套封装的核心思想是"不信任任何外部服务"——RPC 调用都可能失败、超时、返回异常数据,但页面渲染永远不能挂。这在直播场景下尤其重要,因为大型赛事期间后端流量激增,部分服务可能短暂过载,但用户打开页面必须看到内容。
4. 直播间弹幕系统——对象池 + 动态速率控制 + 轨道调度
M 站直播间的弹幕系统需要处理 WebSocket 实时推送的高频弹幕消息(高峰期每秒 50+ 条),我们设计了一套完整的弹幕渲染引擎:
- 对象池模式:预创建 DOM 节点池,弹幕显示时从池中取出节点、设置文本和样式,动画结束后回收到池中。避免高频
document.createElement/removeChild导致的 GC 压力和 DOM 重排 - 等待队列缓冲:WebSocket 消息先进入最大长度 200 的等待队列,定时器按固定间隔消费。超出队列上限的旧消息直接丢弃,保证弹幕内容的实时性
- 10 轨道动态分配:维护 10 条弹幕轨道,每条轨道追踪当前弹幕的滚动位置,新弹幕分配到有空间的轨道,避免弹幕重叠
- 动态速率调整:根据队列积压量动态调整弹幕滚动速度——积压多时加快滚动让弹幕尽快显示完,积压少时恢复正常速度
- CSS Transform 硬件加速:弹幕滚动使用
translateX动画而非改变left属性,触发 GPU 合成层加速,不影响主线程渲染性能 - 分辨率自适应:弹幕字体大小根据屏幕 DPR 和播放器尺寸自动缩放
面试表述:弹幕系统的核心挑战是"如何在不影响视频播放流畅度的前提下,渲染大量高频更新的 DOM 元素"。对象池 + CSS Transform 硬件加速让弹幕渲染的帧率始终保持在 60fps,即使在低端安卓机上也能流畅显示。
5. 播放器双加载策略 + H.265 WASM 解码——极致播放可用性
视频站的播放器加载采用了双重保险策略,并支持 H.265 浏览器播放:
- LibLoader 优先:通过内部 CDN 管理工具
LibLoader.load('player')加载播放器 SDK,支持版本管理和缓存策略 - Script 标签降级:如果 LibLoader 加载失败(CDN 故障、网络超时等),自动降级为动态创建
<script>标签直接加载playerJsUrl,从另一个 CDN 域名拉取 - H.265 解码支持:通过
enableH265: true配置启用 HEVC 播放能力,底层是@huyasdk/web_huya_vodSDK 的 WASM 软解码 + WebGL Canvas 渲染 - SharedArrayBuffer 加速:注入
<meta http-equiv="origin-trial">Token 启用 SharedArrayBuffer,实现解码线程间零拷贝数据共享,H.265 解码性能提升 30%+ - 环境差异化:本地/测试/生产环境使用不同的 Origin Trial Token,通过
constant.ts统一管理
面试表述:播放器是视频站的核心能力,任何加载失败都意味着用户流失。双加载策略让播放器可用性从 99.5% 提升到 99.95%。H.265 支持则直接带来了带宽成本的降低——同画质下码率减少 40-50%,对虎牙这种视频平台来说,每年节省的 CDN 费用是千万级别的。
6. Manifest 驱动的 SSR 资源注入——精确到页面的按需加载
构建系统设计了一套 Manifest 映射机制,让 SSR 精确知道每个页面需要加载哪些 JS/CSS 资源:
- 构建时生成 manifest.json:Webpack 打包后自动生成路由到资源的映射表,如
"pages/index" → ["vendor.a1b2c3.js", "pages/index.d4e5f6.js", "pages/index.g7h8i9.css"] - 内容哈希缓存:所有产物文件名包含内容哈希(
[name].[contenthash].js),文件内容不变则哈希不变,实现强缓存永不过期 - Vendor 长期缓存:
react、redux、prop-types等框架库独立打包为vendor.[hash].js,由于框架版本很少变化,这个 chunk 可以在用户浏览器中长期缓存 - 页面级分包:每个页面独立打包,首页不加载播放页的代码,播放页不加载搜索页的代码
- SSR 精确注入:LEAF 平台在 SSR 渲染时读取 manifest.json,只在 HTML 中注入当前页面所需的资源标签,避免加载无用代码
- 静态资源 CDN:
@public/目录下的图片等静态资源通过 Babel 插件自动转换为 CDN URL(ssr-static.msstatic.com),并加上 MD5 哈希指纹
面试表述:这套机制解决了传统 SSR 项目中"要么全量加载、要么手动维护依赖关系"的痛点。manifest.json 本质上是构建工具与运行时 SSR 引擎之间的桥梁——构建时分析依赖关系、运行时按需注入资源,实现了真正的页面级代码分割。
7. 全链路性能监控体系——从 DNS 到视频播放的端到端可观测
建立了覆盖完整用户访问链路的监控体系,所有指标最终汇聚到内部性能平台:
- 网络层指标:通过
Performance API采集 DNS 查询时间、TCP 连接时间、TTFB(首字节时间)、DOM 解析时间 - 页面级指标:白屏时间(
whiteScreenTime)、首屏时间(firstScreenTime),通过setBodyTag在页面底部注入打点脚本精确计算 - 播放器指标:PlayerLoad 时间(SDK 加载耗时)、VideoLoad 时间(视频首帧耗时)、卡顿次数(每 20s 上报一次)、黑屏率(10s 无画面超时检测)
- 网络质量追踪:记录用户网络类型(4G/WiFi)、CDN 线路选择、码率档位
- 错误监控:Sentry 100% 采样率,自动关联用户 UID 和页面 URL,配合 BrowserTracing 插件追踪 JavaScript 异常堆栈
- 业务上报:YA Report 系统采集 PV/UV、点击事件、来源追踪(
rso参数),支持事件级粒度的用户行为分析
面试表述:性能优化的前提是"能度量"。这套监控体系让我们能精确定位"用户从打开页面到看到视频播放"的每一个耗时环节——DNS 慢了换域名,TTFB 高了查 SSR 服务,视频加载慢了查 CDN 线路。上线后我们通过数据驱动优化,将视频播放页的首帧时间从 3.2s 降低到 1.8s。
8. 直播间双形态架构——游戏直播/颜值直播共享信令、差异化 UI
M 站直播间根据直播类型(iGid === 2168 为颜值/秀场直播,其余为游戏直播)动态加载不同的 UI 布局和交互模块,但底层通信层完全共享:
- 模块化加载:游戏直播间加载
normal.ts(横屏播放器 + 聊天 + 弹幕),颜值直播间加载face.ts(竖屏播放器 + 弹幕 + 礼物特效) - 共享信令通道:两种直播间共用同一套
tafConnect.init()WebSocket 信令连接,接收danMuReceiveMsg(弹幕)、danMuReceiveGift(礼物)、setRoomViewers(观众数)等消息 - 竖屏适配:颜值直播间使用
r=270图片旋转参数 + 专门的竖屏播放器布局,在移动端实现沉浸式竖屏直播体验 - 流协议自适应:根据浏览器能力自动选择 FLV(低延迟优先)或 HLS(兼容性优先)播放协议
- 心跳保活:每 60 秒执行
liveReport()上报心跳,维持在线状态和观看时长统计
面试表述:这个架构的巧妙之处在于"共享底层、差异化上层"。信令连接、数据获取、心跳保活等通用能力只维护一套代码,而 UI 交互层根据直播类型动态加载不同模块。这种设计避免了在一个页面里写大量 if-else 判断,同时也避免了维护两套完全独立的直播间代码。
9. 动态 SEO + XSS 安全防护——兼顾搜索引擎友好与安全性
每个页面通过 setHeader 钩子动态生成 TDK(Title/Description/Keywords),同时对所有用户可控内容做 XSS 过滤:
- 动态 TDK 生成:每个页面根据 SSR 获取到的业务数据(主播名、频道名、视频标题等)动态拼接
<title>、<meta name="keywords">、<meta name="description"> - 服务端直出:由于是 SSR 渲染,搜索引擎爬虫拿到的 HTML 已经包含完整的 TDK 和页面内容,无需执行 JavaScript
- XSS 防护:所有由用户输入或 API 返回的可控内容(主播昵称、视频标题、搜索关键词等)在注入 HTML 之前统一经过
filterXss()处理,防止 XSS 注入攻击 - UA 智能分流:M 站通过
rewriteResponse检测桌面 UA 自动 302 跳转 PC 站,视频站检测移动 UA 跳转 WAP 站,确保用户始终访问最适合其设备的站点版本
面试表述:SEO 是 PHP 迁移到 React 时最大的风险点——CSR 方案会导致搜索引擎排名暴跌。SSR + 动态 TDK 的方案让迁移前后的搜索引擎收录量保持一致。XSS 防护则是安全红线——虎牙作为大型直播平台,用户输入(昵称、弹幕、搜索词等)是 XSS 攻击的高频入口,必须在 SSR 输出层做统一过滤。
10. 微信生态深度集成——分享裂变 + 来源追踪 + 参数透传
M 站作为移动端入口,与微信生态深度打通,实现从微信内访问到 App 唤起的完整闭环:
- 微信分享配置:通过
@huyafed/wechatshareSDK 配置分享标题、描述、图片、链接,每个直播间/视频页都有定制化的分享卡片 - 来源追踪:URL 参数
rso、from贯穿用户访问全链路,从微信分享 → 打开页面 → 数据上报,精确追踪每个流量的来源渠道 - App 唤起:
@huyafed/openapp组件支持 Universal Link + URL Scheme 双通道唤起 App,优先走 Universal Link(体验更好),降级走 URL Scheme - 平台参数白名单:
platForm、autoOpenApp、hideDownApp等 URL 参数在页面跳转时自动透传,保持用户上下文一致性
面试表述:移动端流量的一大特点是"从社交平台进入"——微信分享可能占了 M 站 30% 以上的流量来源。这套机制确保每一次分享都能生成精美的分享卡片、每一个用户的来源都能被准确追踪、每一次打开都有机会引导用户下载 App。这是一个完整的流量获取 → 用户体验 → 转化引导闭环。
十、改进建议
项目中仍存在一些技术债务需要关注:jQuery 依赖未完全移除、无自动化测试覆盖、JCE 类型文件依赖手动生成、ESLint 配置缺失。建议在后续迭代中逐步偿还。
10.1 架构改进
| 方面 | 现状 | 建议 |
|---|---|---|
| SSR 框架 | 自研 @hnf-next/light | 考虑迁移至 Next.js,社区生态更丰富,维护成本更低 |
| 状态管理 | Redux + Thunk | 对于 SSR 场景,可考虑 React Query / SWR 做数据获取缓存 |
| 样式方案 | 全局 SCSS | 引入 CSS Modules 或 CSS-in-JS,避免样式冲突 |
| Monorepo | 两个独立项目 | 通用代码(utils、types、TAF 封装)可提取到共享包 |
10.2 工程化改进
| 方面 | 现状 | 建议 |
|---|---|---|
| 测试 | 无自动化测试 | 补充 Jest 单元测试(至少覆盖 utils 和 services) |
| 构建工具 | Webpack 5 | 视频站 VOD 场景可考虑 Vite 提升开发体验 |
| 类型安全 | JCE 手动生成 | 建立 JCE → TypeScript 自动生成流水线 |
| CI/CD | 手动 deploy 命令 | 集成 GitLab CI,PR 合并自动触发构建部署 |
| Lint | 未见 ESLint 配置 | 配置 ESLint + Prettier + Husky,统一代码风格 |
10.3 性能改进
| 方面 | 现状 | 建议 |
|---|---|---|
| Hydration | 全量 Hydration | 考虑 React 18 Selective Hydration 或 Islands 架构 |
| 数据缓存 | 无 SSR 缓存 | 对热门页面的 TAF 响应做 Redis 缓存,降低后端压力 |
| Bundle 体积 | jQuery 仍在使用 | 移除 jQuery 依赖,用原生 API 替代 |
| 图片格式 | JPG/PNG | 支持 WebP/AVIF 格式,进一步压缩图片体积 |
十一、面试常见问题 & 参考回答思路
Q1: 为什么选择 SSR 而不是纯 CSR?
回答思路:
- SEO 需求:视频站和直播间页面需要被搜索引擎收录,CSR 的 SPA 页面搜索引擎爬虫难以抓取动态内容
- 首屏性能:SSR 直出 HTML,用户无需等待 JS 下载执行即可看到内容,FCP(First Contentful Paint)显著提升
- 社交分享:微信等社交平台抓取页面 Meta 信息时需要服务端直出
- 替代方案对比:预渲染(Prerender)适合静态页面但不适合动态直播数据;SSG 无法处理实时数据
- 权衡取舍:SSR 增加了服务端计算成本和架构复杂度,通过 LEAF Serverless 平台降低运维负担
Q2: 从 PHP 迁移到 React SSR,如何保证平滑过渡?
回答思路:
- 渐进式迁移:不是一次性全部重构,而是逐个页面迁移
- 流量切换:通过 Nginx 配置,逐步将流量从 PHP 切到 Node SSR,出现问题可秒级回滚
- 兼容处理:新旧页面 URL 保持一致,Cookie/Session 兼容
- 灰度策略:先内部测试 → 1% 灰度 → 10% → 全量
Q3: TAF/TARS RPC 和普通 HTTP API 有什么区别?为什么用 RPC?
回答思路:
- 性能:RPC 使用二进制协议(JCE 序列化),比 JSON over HTTP 更紧凑高效
- 类型安全:通过 JCE IDL 定义接口,编译生成强类型代码,减少运行时错误
- 服务发现:TAF 注册中心自动管理服务节点,支持负载均衡和故障转移
- 公司基建:虎牙后端统一使用 TARS 微服务框架,Node.js SSR 层需要对接已有服务
- 错误处理:封装了超时(3000ms)、降级(返回空响应)机制,保证页面可用性
Q4: 你们的 SSR 框架 @hnf-next/light 和 Next.js 有什么异同?
回答思路:
- 相同点:文件路由、getServerProps 数据获取、动态路由
[param]语法 - 不同点:
- light 是面向 LEAF Serverless 平台设计的,深度集成内部部署体系
- 支持
rewriteResponse响应拦截,用于 UA 检测重定向 - 支持
setHeader/setBodyTag灵活注入 HTML 内容 - 不支持 Next.js 的 ISR(增量静态生成)、API Routes 等特性
- 选择原因:当时 Next.js 对内部 LEAF 平台兼容性不够,需要定制化 SSR 框架
Q5: 如何保证直播间页面的首屏性能?
回答思路:
- SSR 直出:直播间基础信息(主播信息、房间状态、推荐列表)由服务端直出
- 异步加载:播放器、弹幕、聊天等重交互模块在客户端 Hydration 后异步初始化
- 并行请求:getServerProps 中 Promise.all 并行获取 4-5 个接口数据
- 资源分包:Vendor 框架代码长期缓存,页面级代码按需加载
- 性能监控:全链路打点(白屏时间、首屏时间、播放器加载时间),数据驱动优化
- 具体指标:对比 PHP 渲染,首屏时间从 X 秒优化到 Y 秒(需要补充具体数据)
Q6: 弹幕系统是如何设计的?如何处理高并发消息?
回答思路:
- 消息接收:通过 WebSocket(TAF 信令 SDK)接收实时弹幕消息
- 消息缓冲:等待队列最大 200 条,超出丢弃旧消息(保证最新消息优先展示)
- DOM 性能:采用对象池模式复用 DOM 节点,避免频繁创建销毁
- 动画方案:CSS Transform translateX 动画,利用 GPU 加速
- 轨道分配:10 条弹幕轨道,动态检测空闲轨道分配
- 流控策略:根据消息积压量动态调整弹幕滚动速度
Q7: 如何做错误监控和性能优化?
回答思路:
- 错误监控:Sentry 全量采集(sampleRate: 1),记录用户 ID、URL、错误堆栈
- 性能采集:Performance API 提取 DNS/TCP/TTFB/DOM 等关键指标
- 播放质量:黑屏检测(10s 超时上报)、卡顿计数(20s 周期上报)
- 数据上报:YA Report 事件系统,支持 PV、点击、自定义事件
- 闭环优化:监控数据 → 发现瓶颈 → 针对性优化 → 验证效果
Q8: Webpack 构建优化做了哪些工作?
回答思路:
- 代码分割:框架代码(React/Redux)独立 vendor chunk,长期缓存
- 哈希命名:
[chunkhash]实现精确缓存失效 - 资源优化:图片哈希重命名 + CDN 分发
- 构建缓存:Hard Source Plugin + Cache Loader 加速二次构建
- Manifest 映射:自动生成路由→资源映射,SSR 精确注入
- Tree Shaking:Webpack 5 原生支持,Babel 配置
modules: false - 外部化服务端代码:
externals排除 server 代码进入客户端 bundle
Q9: 如何处理 M 站和 PC 站的跨端跳转?
回答思路:
- 服务端检测:在
rewriteResponse中通过 User-Agent 检测设备类型 - M 站:桌面 UA 访问 → 302 跳转到 www.huya.com 对应页面
- 视频站:移动 UA 访问 → 302 跳转到 m.v.huya.com 对应页面
- 客户端兜底:HTML 中注入 redirect 脚本,JS 层二次检测
- 参数透传:跳转时保留
platForm、from等追踪参数
Q10: 项目中的安全防护措施有哪些?
回答思路:
- XSS 防护:所有用户输入通过
filterXss()过滤后再渲染到 HTML - Cookie 安全:TAF 请求头中传递 Cookie 用于服务端鉴权,不在前端暴露敏感信息
- CORS 处理:跨域请求通过 JSONP Adapter 或服务端代理
- Origin Trial:SharedArrayBuffer 通过正规的 Origin Trial 机制启用
- 输入校验:视频 ID 严格校验 (
/^\d+\.html$/),防止注入攻击
十二、项目数据与成果
以下数据来源于职级评审材料,供面试参考:
- 重构范围:M 站全站 + 视频站全站,覆盖所有核心页面
- 技术升级:PHP Smarty → React 17 + TypeScript + SSR
- 部署方式:传统服务器 → LEAF Serverless,运维成本大幅降低
- 首屏性能:SSR 直出显著优于 PHP 模板渲染(具体数据见内部性能报表)
- 开发效率:组件化开发,新页面开发周期缩短;TypeScript 减少线上 Bug 率
- SEO 效果:SSR 保证搜索引擎完整抓取页面内容,收录量无下降
附录:关键文件速查表
| 文件路径 | 说明 |
|---|---|
pages/*.tsx | 页面入口,定义路由和 SSR 数据获取 |
common/server/sevices/ | TAF RPC 服务调用层 |
common/server/utils/function.ts | tafRequest 通用封装 |
common/utils/function.ts (或 common/utils.ts) | 通用工具函数 |
common/utils/constant.ts (或 common/constant.ts) | 环境常量和 HTML 注入 |
common/store/ | Redux Store 定义(M 站) |
common/client/modules/ | 客户端交互模块 |
common/client/utils/report.ts | 数据上报初始化 |
common/client/utils/sentry.ts | 错误监控初始化(视频站) |
build/config.ts | 构建路径和环境配置 |
build/webpack/webpack.*.ts | Webpack 配置(base/dev/build) |
jce/ | JCE 协议文件(RPC 接口定义) |
serverless/ | Serverless 函数(M 站) |
api/healthCheck.ts | 健康检查接口 |
manifest.json | 构建产物的路由→资源映射 |