browserrouter依赖浏览器api导致服务端报错,必须替换为staticrouter(v4/v5)或createstatichandler+createstaticrouter(v6.4+),并传入req.url;禁用usenavigate等客户端hook,确保路由顺序、数据预加载与dom结构一致以支持正确hydration。

前端路由用 BrowserRouter 会导致服务端渲染失败
服务端没有 window 和 history 对象,BrowserRouter 依赖浏览器 API,一运行就报错 ReferenceError: window is not defined。必须换掉。
正确做法是:服务端用 StaticRouter(React Router v4/v5)或 Router + createStaticHandler(v6.4+),并传入当前请求的 url:
-
StaticRouter需显式传location或context,例如:<staticrouter location="{req.url}">...</staticrouter> - v6.4+ 推荐用
createStaticHandler+createStaticRouter,它能自动处理重定向、数据加载失败等状态,比手动 match 更可靠 - 千万别在服务端组件里调用
useNavigate或useLocation—— 这些 Hook 只在客户端有效
后端兜底路由必须放在所有 API 和静态资源之后
Express 或 Go 的路由顺序决定行为优先级。如果 app.get('*') 或 http.ServeFile 前置,API 请求和 JS/CSS 文件全被拦截,返回 index.html,导致接口 404 或资源加载失败。
典型错误顺序:
app.get('*', serveIndex) // ❌ 错了,它会吃掉 /api/users
正确顺序应为:
- 先注册所有
/api/xxx路由 - 再托管静态资源(如
express.static('dist')) - 最后加兜底:
app.get('*', (req, res) => res.sendFile(path.join(__dirname, 'dist', 'index.html')))
Go 的 net/http 同理:静态文件处理器要早于 fallback handler,否则 http.FileServer 拦不到真实文件路径。
matchRoutes 不等于“能渲染”,还要检查 data 加载是否完成
只靠 matchRoutes(routes, url) 匹配到组件,不代表页面能正确 SSR。很多组件依赖异步数据(比如 useEffect 里发请求),服务端没执行这些逻辑,结果 HTML 是空的或缺内容。
必须配合预取机制:
- 每个路由配置里定义
loader(v6.4+)或getData方法(自定义约定) - 服务端拿到匹配结果后,逐个调用 loader,
await Promise.all(loads) - 把数据序列化注入 HTML,例如塞进
window.__INITIAL_STATE__,客户端 hydration 时读取 - 若某个 loader 抛错,不能直接渲染,得返回 404 或错误页 —— 否则客户端 hydration 会 mismatch
Vue/React 模板中 <!--vue-ssr-outlet--> 或 root 节点必须严格一致
服务端生成的 HTML 字符串,要能被客户端准确 hydrate,前提是 DOM 结构完全一致。常见断裂点:
- 服务端模板里漏写
<!--vue-ssr-outlet-->注释节点,或 React 中<div id="root"></div>缺失 - 构建工具(如 webpack)开启
removeComments: true,把<!--vue-ssr-outlet-->删了,导致服务端无法注入内容 - 客户端入口多渲染了一层 wrapper div,比如
<app><provider>...</provider></app>,而服务端只渲染了<provider>...</provider>
验证方式很简单:用 renderToString 输出 HTML,复制到浏览器 DevTools 里手动比对结构层级和 class 名是否完全相同。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!









