React服务端渲染报错:Invariant Violation: Invalid tag: <!doctype html> 如何解决?
解决React SSR中的两个问题:Invalid Tag错误与数据传递失败
先来说你遇到的第一个错误——Invariant Violation: Invalid tag: <!doctype html>,这个问题其实很常见,根源是你误解了ReactDOMServer.renderToString的作用:它只能用来渲染React元素,而<!doctype html>是HTML文档的声明,不属于React能识别的组件/标签范畴。
修复Invalid Tag错误
之前你可能尝试直接把整个HTML文档字符串传给renderToString,这肯定会报错。正确的做法是拆分两步:
- 用
renderToString单独渲染你的根组件(比如<App />),得到组件对应的HTML字符串。 - 把这个字符串插入到预先写好的完整HTML模板里(包含doctype、head、body等结构)。
举个具体的代码示例:
// 服务端代码 import React from 'react'; import ReactDOMServer from 'react-dom/server'; import App from './App'; // 1. 渲染React根组件 const appHtml = ReactDOMServer.renderToString(<App />); // 2. 拼接完整的HTML文档 const fullHtml = `<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Your App</title> </head> <body> <div id="root">${appHtml}</div> <!-- 别忘了引入客户端React脚本,用于hydrate激活交互 --> <script src="/client-bundle.js"></script> </body> </html>`; // 发送给浏览器 res.send(fullHtml);
这样就不会触发Invalid Tag的错误了,因为React只负责渲染你的App组件,剩下的HTML结构由你自己拼接。
解决数据未传递到App的问题
这个问题通常和SSR场景下的服务端-客户端数据同步有关,下面是几个排查和修复的关键点:
1. 确保服务端正确传递props给App
首先检查服务端渲染<App />时,是否真的把数据作为props传进去了。比如:
// 服务端:假设你有要传递的初始数据 const initialData = { posts: [...], user: {...} }; // 一定要把数据作为props传给App const appHtml = ReactDOMServer.renderToString(<App initialData={initialData} />);
可以在服务端用console.log(initialData)确认数据是否存在,有没有为空或者未定义的情况。
2. 同步客户端与服务端的初始数据
SSR的核心是服务端先渲染出带数据的HTML,客户端再"hydrate"(激活)这个HTML,让它变成可交互的React应用。如果客户端的App没有拿到和服务端一样的初始数据,就会出现数据不显示或者页面闪烁的问题。
解决方法是把服务端的初始数据挂载到全局window对象上,让客户端可以读取:
// 服务端拼接HTML时,添加这行代码 const fullHtml = `<!doctype html> <html> <!-- ... 其他结构 ... --> <body> <div id="root">${appHtml}</div> <!-- 把初始数据注入到全局,注意要转义避免XSS --> <script>window.__INITIAL_DATA__ = ${JSON.stringify(initialData).replace(/</g, '\\u003c')}</script> <script src="/client-bundle.js"></script> </body> </html>`;
然后在客户端hydrate时,把这个全局数据传给App:
// 客户端代码 import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; ReactDOM.hydrate( <App initialData={window.__INITIAL_DATA__} />, document.getElementById('root') );
这样服务端和客户端的App用的是同一套初始数据,就能保证渲染结果一致。
3. 检查App组件是否正确接收props
最后,确认你的App组件确实在使用传入的props。比如用函数组件的话:
function App({ initialData }) { // 可以在这里打印props,确认是否接收到数据 console.log('Initial data received:', initialData); return ( <div className="app-container"> {/* 使用initialData渲染内容 */} {initialData?.posts?.map(post => ( <div key={post.id} className="post-item"> <h3>{post.title}</h3> <p>{post.content}</p> </div> ))} </div> ); }
如果是类组件,要通过this.props.initialData来访问。
按照上面的步骤,应该能同时解决这两个问题。如果还有疑问,可以检查一下是否有拼写错误(比如props名称写错),或者服务端数据是否在序列化时出了问题(比如JSON.stringify不能处理循环引用的数据)。
内容的提问来源于stack exchange,提问作者Nikita Vlasenko

