在传统 CSR 页面中,用 JavaScript 监听视口并切换响应式类名是一种直观且实用的方案。项目早期引入 ResponsiveContainer,并不是因为 CSS 无法实现响应式,而是希望把分散在业务中的断点判断收拢到一个组件中。
本文介绍一种更稳健的响应式架构:把样式匹配交还给 CSS Media Query,通过共享 Less Mixin 集中维护断点;JavaScript 只处理确实依赖设备类型的业务行为。
背景:为什么最初要使用 ResponsiveContainer
在没有统一封装时,每个业务模块都可能自行编写 Media Query,或者在组件中直接判断 window.innerWidth。这样很容易出现几个问题:
- 相同设备类型在不同页面使用不同断点。
1240px、904px、600px等数字散落在业务代码中。- 业务同学需要记住每个断点的具体数值和覆盖范围,开发与评审时都要反复确认边界,心智负担较重。
- 修改一条断点规则时,需要逐个排查所有使用方。
- JavaScript 行为和 CSS 样式各自维护一套设备判断,语义难以统一。
ResponsiveContainer 正是为了解决这些问题而出现。它通过 react-responsive 的 useMediaQuery 在运行时判断视口宽度,再向容器注入四种语义类名:
- PC
- 固定宽度平板
- 弹性宽度平板
- 移动端
业务样式只依赖父级语义类,不直接感知具体像素。同时,组件通过 onScreenChange 把当前设备类型通知给业务,让埋点、交互逻辑和第三方组件参数也能复用同一套判断。
在纯 CSR 阶段,这个设计有明显收益:
- 断点集中定义,避免具体数值散落在业务中。
- 业务使用 PC、Tablet、Mobile 等布局语义,无需记忆每个断点的具体数值和范围。
- 样式和 JavaScript 行为能够复用同一个设备判断结果。
- 调整断点时只需修改组件,而不必逐个修改业务源码。
因此,问题并不在于这个组件从一开始就是错误的,而在于页面渲染模式发生变化后,它原本依赖的前提不再成立。
缺陷:运行时响应式不适合决定 SSG 首屏
ResponsiveContainer 的判断依赖浏览器视口,但 SSG 构建阶段和 SSR 服务端渲染阶段都不存在真实的 window。服务端只能输出一个默认结果,无法知道访问者最终使用的是 PC、平板还是手机。
如果服务端生成的初始语义类名与客户端实际宽度不一致,页面需要等到 React hydration 和媒体查询 Hook 执行后才能修正。这个过程会带来:
- 首屏短暂使用错误的宽度、间距或排列方式。
- hydration 后语义类名变化,引发布局闪动。
- 元素位置发生偏移,增加 CLS。
- 如果业务根据设备类型裁剪 DOM,可能产生 hydration 不一致。
- 慢网、低性能设备或 JavaScript 加载失败时,错误布局会持续更久。
除了首屏问题,组件模式还引入了额外耦合:
- 样式必须依赖容器生成的父级类名,组件结构与 CSS 选择器被绑定在一起。
- 只需要响应式样式的节点也必须增加组件包裹和运行时媒体监听。
onScreenChange容易被用于控制纯样式或首屏 DOM,使样式职责逐渐进入 JavaScript。- 项目中如果还存在 Context、Hook 或工具函数的另一套断点,规则会逐渐分叉。
这些缺陷的共同根因是:组件把两类职责合并了。它既负责视觉样式,又负责 JavaScript 行为;而视觉响应其实可以由浏览器在首次绘制前直接完成。
真正需要保留的不是 ResponsiveContainer 这个运行时载体,而是它提供的两个核心能力:
- 集中维护断点。
- 让业务使用语义化设备类型。
新方案会保留这两项能力,只把实现从 React 运行时组件迁移到 CSS 构建时 Mixin。
核心原则:CSS 管样式,JavaScript 管行为
浏览器在首次绘制前就能根据真实视口匹配 Media Query,不需要等待 React 启动。因此,新方案需要保留语义化断点和集中维护能力,但将视口匹配交给 CSS。
职责可以按下面的规则划分:
- 宽度、间距、排列、显隐等视觉差异由 CSS 处理。
- 埋点、第三方组件参数、非关键交互等行为差异由 JavaScript 处理。
- 首屏关键 DOM 尽量保持同构,通过 CSS 控制展示方式。
这不是简单地把一个组件改成几个 Media Query,而是重新划清样式与运行时逻辑的边界。
用 Less Mixin 建立语义化断点契约
可以在共享样式目录中新增一个可显式导入的 responsive.less。相比在构建层隐式注入全局 Mixin,显式导入更容易追踪依赖,也不会无条件污染所有 Less 文件。
假设现有布局包含四种断点语义:
| 语义 | 视口范围 | Mixin |
|---|---|---|
| PC | 大于或等于 1240px | .screen-pc |
| 固定宽度平板 | 904px 至 1239.99px | .screen-tablet-fixed |
| 弹性宽度平板 | 600px 至 903.99px | .screen-tablet-elastic |
| 移动端 | 小于或等于 599.99px | .screen-mobile |
对应的 Less 定义如下:
.screen-pc(@rules) {
@media (min-width: 1240px) {
@rules();
}
}
.screen-tablet-fixed(@rules) {
@media (min-width: 904px) and (max-width: 1239.99px) {
@rules();
}
}
.screen-tablet-elastic(@rules) {
@media (min-width: 600px) and (max-width: 903.99px) {
@rules();
}
}
.screen-mobile(@rules) {
@media (max-width: 599.99px) {
@rules();
}
}
这里有两个重要约束。
第一,四个范围必须互斥且完整覆盖目标视口,避免同一个边界被两条规则同时命中,或落入无人处理的空档。
第二,命名表达布局语义,不要把具体像素写进 Mixin 名称。业务关心的是“移动端布局”,而不是“宽度小于 600px 的布局”。未来断点变化时,业务源码无需跟着改名。
业务样式如何迁移
旧实现通常依赖响应式容器注入的父级类名:
.site-pc-responsive {
.content {
width: 1200px;
}
}
.site-mobile-responsive {
.content {
width: 100%;
}
}
迁移后,在稳定的外层容器中调用语义化 Mixin:
@import "path/to/responsive.less";
.container{
.site-pc-responsive({
.content {
width: 1200px;
}
});
.site-moblie-responsive({
.content {
width: 1200px;
}
});
}
保留外层容器不仅是为了组织样式,也是为了维持迁移前的选择器优先级。旧规则展开后是 .site-pc-responsive .content,包含两个类选择器;如果直接使用 Mixin 包裹 .content,生成的响应式规则只剩一个类选择器,优先级会降低,在存在其他竞争规则时可能无法生效。增加 .container 后,生成的 .container .content 与旧规则具有相同的优先级,可以避免迁移后样式被其他规则覆盖。
推荐采用“移动端基础样式 + 按需覆盖”的方式,而不是机械地为每个选择器调用四个 Mixin。只有不同设备确实存在差异时才声明规则,可以显著减少重复 CSS。
这次迁移也不意味着所有响应式容器都能直接删除。如果组件还负责普通 div 属性、布局、埋点或其他职责,需要先将这些职责迁移到合适的业务节点。只有当它唯一的作用是注入响应式父级类时,才应该移除组件和多余 DOM。
如何处理原有的屏幕变化回调
Less Mixin 只能解决样式问题,无法向 JavaScript 返回当前断点。迁移前,应检索所有屏幕变化回调,并按实际用途分类。
纯样式用途
如果回调只用于修改 className、内联样式或控制展示隐藏,应改为 CSS Mixin,不再使用 JavaScript。
业务行为用途
如果回调用于埋点、非关键交互或第三方组件参数,可以迁移到统一的 useBreakpoint Hook。
首屏关键 DOM
如果回调决定 SSG 首屏是否渲染某段关键 DOM,应优先改成同构 DOM,再用 CSS 控制显示方式。否则,即使换成新的 Hook,仍然可能出现 hydration 不一致或首屏跳变。
换句话说,Hook 是行为能力的兜底,不应重新成为首屏样式的控制中心。
避免 CSS 与 TypeScript 的断点规则分叉
当 CSS 和 JavaScript 都需要了解断点时,最危险的问题是两套定义逐渐不一致。例如样式认为 PC 从 1240px 开始,而某个 Context 或 Hook 仍然使用 1200px。此时同一视口可能同时处于两种设备状态,问题通常只会在特定宽度下出现,很难排查。
理想做法是维护一份 TypeScript 或 JSON 配置,在构建阶段生成 Less 变量和 Hook 配置。如果暂时不适合引入生成流程,至少应增加一致性测试,保证两端的断点边界相同。
还需要注意:Less Mixin 会在构建阶段展开成 Media Query。共享定义修改后,所有受影响的业务包都必须重新构建并发布;已经部署的静态产物不会自动获得新规则。断点变更因此也需要纳入发布流程和影响面评估。
总结
一个稳定的 SSG 响应式方案,不应该依赖 hydration 之后才能获得正确布局。
使用共享 Less Mixin,可以同时获得三项收益:
- 浏览器在首次绘制前完成断点匹配,减少首屏错误和布局跳变。
- 业务继续使用语义化断点,不直接依赖具体像素。
- 断点规则集中维护,调整时不必逐个修改业务样式。
更重要的是,这次重构建立了一条清晰的工程边界:CSS 负责视觉响应,JavaScript 负责业务行为。只要守住这条边界,响应式系统就能在 SSG、SSR 和 CSR 场景中保持一致、可预测,也更容易长期维护。