从 NestJS 全栈开发到 SSR & SSR 核心原理
笔试题(6 题)
1. NestJS 核心概念
NestJS 的核心概念:模块、控制器、服务、依赖注入、装饰器。它们如何协同工作?
【作答】:
模块 (Module):
- 是应用的基本组织单位,使用 @Module() 装饰器定义
- 每个模块封装相关的功能(控制器、服务、导入、导出)
- 通过 imports 导入其他模块,exports 导出供其他模块使用
- 提供依赖注入的容器作用域
控制器 (Controller):
- 负责处理 HTTP 请求,使用 @Controller() 装饰器定义
- 通过路由装饰器(@Get、@Post 等)定义路由处理函数
- 接收请求参数,调用服务层处理业务逻辑,返回响应
- 不包含业务逻辑,只负责请求/响应的处理
服务 (Service/Provider):
- 封装业务逻辑,使用 @Injectable() 装饰器定义
- 可被注入到控制器或其他服务中
- 提供可复用的业务功能,保持代码解耦
- 可以是类、值、工厂函数等多种形式
依赖注入 (DI):
- NestJS 的核心机制,基于 IoC(控制反转)容器
- 通过构造函数参数自动注入依赖
- 支持单例、请求作用域、瞬态等多种生命周期
- 提高代码可测试性和可维护性
装饰器 (Decorator):
- TypeScript/JavaScript 的元编程特性
- NestJS 大量使用装饰器进行元数据标记
- 如 @Module()、@Controller()、@Injectable()、@Get() 等
- 在编译时和运行时提供元信息,实现依赖注入和路由映射
协同工作流程:
- 应用启动时,NestJS 扫描所有模块,构建依赖图
- 创建 IoC 容器,实例化所有 Provider(服务)
- 根据装饰器元数据,将路由映射到控制器方法
- 请求到达时,路由系统找到对应的控制器方法
- 控制器通过构造函数注入所需的服务
- 控制器调用服务方法处理业务逻辑
- 返回响应给客户端 整个过程实现了关注点分离:路由 → 控制器 → 服务 → 数据层
2. NestJS vs Express/Koa
NestJS 相比 Express/Koa 的优势是什么?适用场景有什么不同?
【作答】:
NestJS 优势:
- 架构清晰:基于模块化设计,强制分层(Controller-Service-Repository)
- 依赖注入:内置 IoC 容器,便于测试和模块解耦
- TypeScript 原生支持:完整的类型安全和 IDE 智能提示
- 企业级特性:内置认证、验证、日志、配置管理等
- 可扩展性:支持微服务、GraphQL、WebSocket 等
- 学习曲线:借鉴 Angular 架构,适合大型团队协作
Express/Koa 优势:
- 轻量灵活:核心功能精简,按需添加中间件
- 学习成本低:API 简单直观,上手快
- 生态丰富:中间件和插件数量庞大
- 性能优秀:框架开销小,适合高性能场景
- 自由度高:不强制架构模式,灵活组织代码
- 社区成熟:文档和案例丰富
适用场景对比: NestJS 适合:
- 大型企业级应用,需要严格的架构规范
- 团队协作项目,需要统一的代码风格
- 需要复杂业务逻辑和模块化设计的项目
- 微服务架构,需要统一的框架标准
- 需要 GraphQL、WebSocket 等高级特性
Express/Koa 适合:
- 中小型项目,快速开发原型
- 需要极致性能的场景(如 API 网关)
- 团队熟悉底层原理,需要高度定制
- 简单的 RESTful API 服务
- 学习和教学场景
3. SSR vs CSR vs SSG
对比 SSR(服务端渲染)/ CSR(客户端渲染)/ SSG(静态生成)的原理、优缺点、适用场景。
【作答】:
SSR (Server-Side Rendering): 原理:
- 服务器接收到请求后,在服务端执行 JavaScript 代码
- 渲染完整的 HTML 页面(包含数据和 DOM 结构)
- 将渲染好的 HTML 发送给客户端
- 客户端接收后显示页面,然后进行 hydration(水合)激活交互
优点:
- SEO 友好:搜索引擎可以直接抓取完整的 HTML 内容
- 首屏加载快:用户立即看到内容,无需等待 JS 执行
- 对低性能设备友好:渲染工作在服务端完成
- 更好的社交分享:OG 标签等元数据完整
缺点:
- 服务器压力大:每次请求都需要渲染
- TTFB(首字节时间)可能较长:需要等待服务端渲染完成
- 开发复杂度高:需要处理同构代码、环境差异
- 服务器成本高:需要 Node.js 服务器资源
适用场景:
- 内容为主的应用(博客、新闻、电商详情页)
- SEO 要求高的页面
- 首屏性能要求高的场景
- 需要服务端数据预取的页面
CSR (Client-Side Rendering): 原理:
- 服务器返回空的 HTML 骨架和 JavaScript bundle
- 浏览器下载并执行 JavaScript
- JS 代码在客户端请求数据、渲染 DOM
- 所有交互和路由都在客户端完成
优点:
- 服务器压力小:只提供静态文件和 API
- 交互体验好:页面切换无需刷新,SPA 体验
- 开发简单:只需关注客户端代码
- 可充分利用 CDN:静态资源缓存效果好
缺点:
- SEO 不友好:搜索引擎难以抓取动态内容
- 首屏白屏时间长:需要等待 JS 下载和执行
- 对低性能设备不友好:渲染在客户端完成
- 初始加载体积大:需要下载完整的 JS bundle
适用场景:
- 后台管理系统、Dashboard
- 交互复杂的应用(如在线编辑器)
- SEO 要求不高的应用
- 需要离线能力的 PWA 应用
SSG (Static Site Generation): 原理:
- 在构建时(build time)预渲染所有页面为静态 HTML
- 将生成的 HTML 文件部署到 CDN 或静态服务器
- 用户请求时直接返回预渲染的 HTML
- 可以配合 ISR(增量静态再生)实现部分页面动态更新
优点:
- 性能最优:直接返回静态 HTML,响应极快
- 服务器成本低:无需 Node.js 运行时
- SEO 完美:完整的 HTML 内容
- 安全性高:无服务端代码执行
缺点:
- 构建时间长:需要预渲染所有页面
- 不适合频繁更新的内容:需要重新构建
- 无法个性化:所有用户看到相同内容
- 动态数据需要额外处理(如 ISR、API 调用)
适用场景:
- 博客、文档网站
- 营销页面、落地页
- 内容相对固定的网站
- 需要极致性能的场景
4. SSR 核心流程
SSR 的核心流程是什么?从请求到返回 HTML 经历哪些步骤?数据预取、hydration 如何工作?
【作答】:
核心流程:
- 用户请求 → 服务器接收 HTTP 请求
- 路由匹配 → 根据 URL 匹配对应的路由组件
- 数据预取 → 在服务端获取页面所需数据(API 调用、数据库查询)
- 服务端渲染 → 使用 React/Vue 等框架在服务端渲染组件为 HTML
- HTML 返回 → 将完整的 HTML(包含初始数据)发送给客户端
- 客户端接收 → 浏览器接收 HTML 并立即显示
- JS Bundle 加载 → 下载并执行客户端 JavaScript
- Hydration → 客户端 JS 接管页面,激活交互功能
- 后续交互 → 用户操作触发客户端路由和数据更新
数据预取 (Data Fetching):
- 在服务端渲染前,需要获取页面所需的所有数据
- 方式 1:在路由组件中定义静态方法(如 getServerSideProps、load)
- 方式 2:在服务端路由处理函数中直接调用数据获取逻辑
- 方式 3:使用数据获取库(如 React Query、SWR)的服务端适配
- 数据获取完成后,将数据作为 props 传递给组件
- 数据通常序列化到 HTML 中(如 window.INITIAL_STATE),供客户端 hydration 使用
- 避免客户端再次请求相同数据,减少闪烁和加载状态
服务端渲染:
- 使用 ReactDOMServer.renderToString() 或 renderToStream() 等方法
- 在 Node.js 环境中执行组件代码,生成 HTML 字符串
- 组件在服务端执行时,所有生命周期和 hooks 都会执行
- 需要确保组件在服务端和客户端渲染结果一致(避免 hydration mismatch)
- 生成的 HTML 包含完整的 DOM 结构,可以直接被搜索引擎索引
- 可以配合流式渲染(Streaming)逐步发送 HTML,提升 TTFB
Hydration:
- 客户端 JavaScript 下载完成后,React/Vue 等框架会"激活"静态 HTML
- 框架会对比服务端渲染的 HTML 和客户端首次渲染的结果
- 如果一致,则"水合"成功,绑定事件监听器,启用交互
- 如果不一致,会出现 hydration mismatch 错误
- Hydration 后,后续的路由切换和数据更新都在客户端完成
- 这是一个"接管"过程:从静态 HTML 转为可交互的 SPA
- 性能优化:可以只对关键组件进行 hydration(Partial Hydration)
5. SSR 同构问题
SSR 同构代码需要注意哪些问题?window/document 不存在、Node.js API 差异、副作用处理?
【作答】:
环境差异处理:
- 使用 typeof window !== 'undefined' 判断客户端环境
- 使用 process**.env.NODE_ENV 或 process**.browser 判断服务端
- 创建统一的 isServer/isClient 工具函数
- 对于只在客户端运行的代码,使用动态导入(dynamic import)
- 使用条件渲染:只在客户端渲染特定组件
- 避免在模块顶层使用浏览器 API,延迟到运行时检查
全局对象问题:
- window/document 不存在:使用条件判断或 polyfill if (typeof window !== 'undefined') { window.localStorage.setItem('key', 'value'); }
- navigator/location 等:同样需要环境判断
- 使用 isomorphic-fetch、node-fetch 等跨环境库
- 对于必须使用浏览器 API 的代码,使用 useEffect(React)或 onMounted(Vue)
- 服务端渲染时提供默认值或跳过相关逻辑
副作用控制:
- 避免在组件顶层执行副作用(如直接调用 API、修改 DOM)
- 使用 useEffect/onMounted 等生命周期钩子包裹副作用
- 服务端渲染时,某些副作用应该跳过(如事件监听、定时器)
- 确保副作用代码在服务端和客户端执行结果一致
- 使用 cleanup 函数清理副作用,避免内存泄漏
- 对于全局状态(如 Redux),确保服务端和客户端初始化一致
第三方库兼容:
- 检查库是否支持 SSR(查看文档或源码)
- 使用支持同构的库(如 axios、isomorphic-fetch)
- 对于不支持的库,使用动态导入或条件加载
- 使用 webpack 的 externals 或 resolve.alias 处理 Node.js 模块
- 对于包含浏览器 API 的库,使用 dynamic import 延迟加载
- 使用 babel-plugin-transform-require-ignore 忽略某些模块
- 常见问题库:chart.js、three.js 等需要特殊处理
6. SSR 性能优化
SSR 性能优化策略有哪些?缓存、流式渲染、部分 Hydration、组件级缓存?
【作答】:
页面/组件缓存:
- 页面级缓存:对相同 URL 的请求缓存渲染结果(Redis、内存缓存)
- 组件级缓存:缓存无状态或数据变化少的组件渲染结果
- 缓存策略:TTL(过期时间)、LRU(最近最少使用)、基于数据版本
- 缓存键设计:URL + 用户身份 + 数据版本号
- 缓存失效:数据更新时清除相关缓存
- 使用 React.memo、Vue 的 computed 减少不必要的重渲染
- 对于静态内容,使用 SSG 或长期缓存
流式渲染 (Streaming):
- 使用 ReactDOMServer.renderToStream() 替代 renderToString()
- 逐步发送 HTML,而不是等待全部渲染完成
- 提升 TTFB(首字节时间),用户更快看到内容
- 使用 Suspense 边界控制流式渲染的粒度
- 关键内容优先发送,非关键内容延迟发送
- 配合 HTTP/2 Server Push 推送关键资源
- 注意:需要确保 HTML 结构完整,避免浏览器解析错误
部分 Hydration (Partial Hydration):
- 只对需要交互的组件进行 hydration,静态内容保持静态
- 减少客户端 JavaScript 执行量,提升性能
- 使用 React 18 的 Selective Hydration 特性
- 将页面分为可交互区域和静态区域
- 静态区域不进行 hydration,减少内存占用
- 延迟 hydration:非关键组件延迟到用户交互时再激活
- 配合代码分割,按需加载和 hydration
其他优化策略:
- 代码分割:使用动态导入,按路由或组件分割代码
- 预加载:使用 预加载关键资源
- 预取:使用 预取可能访问的资源
- 压缩优化:HTML、CSS、JS 压缩,图片优化
- CDN 加速:静态资源部署到 CDN
- HTTP/2:使用 HTTP/2 多路复用提升加载速度
- 服务端性能:优化数据库查询、API 响应时间
- 监控指标:监控 TTFB、FCP、LCP 等性能指标
- 降级策略:服务端渲染失败时降级到客户端渲染
- 边缘计算:使用 Edge Functions(如 Vercel Edge、Cloudflare Workers)就近渲染
面试题(4 题)
1. NestJS 全栈项目架构
用 NestJS 做全栈项目,你如何设计架构?前后端如何组织?API 设计、认证鉴权、错误处理?
【作答】:
2
3
4
2. SSR 方案选型
给一个电商项目做 SSR,你会如何选型(Next.js/Nuxt.js/自研)?需要考虑哪些因素?
【作答】:
2
3
4
3. SSR 性能问题排查
SSR 项目出现性能问题(TTFB 过长),你的排查思路是什么?如何定位瓶颈?
【作答】:
2
3
4
4. SSR 实践经验
讲一个你做过的 SSR 项目:为什么选 SSR?遇到的核心难点?如何解决?最终效果如何?
【作答】:
2
3
4
