如何将Create React App TypeScript项目改造为SSR实现动态Meta标签?
基于TypeScript Create React App改造SSR实现动态Meta标签方案
现有代码存在的核心问题
readIndexHtml方法使用异步回调的fs.readFile,同步返回时永远拿到undefined,Meta替换逻辑完全失效- 缺少服务端TypeScript编译运行依赖,
package.json中未安装express等服务端核心依赖 - 未适配服务端路由与
react-helmet的服务端提取逻辑,无法做到页面对应Meta自动生成 public/index.html中未预留Meta占位符与SSR内容注入占位符
需新增的依赖包
生产依赖
npm install express react-helmet-async
开发依赖
npm install -D @types/express ts-node cross-env concurrently
配置调整
1. 新增服务端TS配置文件server/tsconfig.json
{ "compilerOptions": { "target": "ES2020", "module": "commonjs", "lib": ["dom", "ES2020"], "jsx": "react-jsx", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "outDir": "../dist-server" }, "include": ["./**/*"] }
2. 修改package.json脚本配置
替换原有scripts字段:
"scripts": { "start:client": "react-scripts start", "start:server": "ts-node --project server/tsconfig.json server/server.ts", "dev": "concurrently \"npm run start:client\" \"npm run start:server\"", "build:client": "react-scripts build", "build:server": "tsc --project server/tsconfig.json", "build": "npm run build:client && npm run build:server", "start": "cross-env NODE_ENV=production node dist-server/server.js", "test": "react-scripts test", "eject": "react-scripts eject" }
3. 修改public/index.html添加占位符
在head标签内添加:
<meta name="description" content="__META_DESCRIPTION__"> <meta property="og:title" content="__META_OG_TITLE__"> <meta property="og:description" content="__META_OG_DESCRIPTION__">
在root div内添加占位符:
<div id="root">__SSR_CONTENT__</div>
代码修改
1. 修改server/server.ts
import express from "express"; import React from "react"; import { renderToString } from "react-dom/server"; import { StaticRouter } from "react-router-dom"; import { HelmetProvider, Helmet } from "react-helmet-async"; import App from '../src/App' import * as path from 'path'; import * as fs from 'fs' import { getArticle } from "../src/api"; const app = express(); const PORT = process.env.PORT || 3006; const isProd = process.env.NODE_ENV === 'production'; const publicPath = isProd ? path.resolve(__dirname, '../build') : path.resolve(__dirname, '../public'); const indexPath = path.resolve(publicPath, 'index.html') app.use(express.static(publicPath, { index: false })); // 同步读取html模板,避免异步回调问题 const indexHtml = fs.readFileSync(indexPath, 'utf8'); app.get('*', async (req, res) => { try { const helmetContext = {}; // 渲染组件为字符串 const appHtml = renderToString( <HelmetProvider context={helmetContext}> <StaticRouter location={req.url}> <App /> </StaticRouter> </HelmetProvider> ); const { helmet } = helmetContext as { helmet: Helmet }; // 替换占位符 let html = indexHtml .replace('__SSR_CONTENT__', appHtml) // 如果要更灵活,也可以直接把helmet的所有head内容注入 .replace('</head>', `${helmet.title.toString()}${helmet.meta.toString()}${helmet.link.toString()}</head>`); // 针对文章页单独处理动态Meta也可以保留原有逻辑 if (req.path.startsWith('/article/')) { const id = req.params.id; const articleRes = await getArticle(parseInt(id)); html = html .replace('__META_OG_TITLE__', articleRes.data.heading) .replace('__META_OG_DESCRIPTION__', articleRes.data.shortDescription) .replace('__META_DESCRIPTION__', articleRes.data.shortDescription); } res.send(html); } catch (error) { console.log('渲染出错', error); res.status(500).send('服务器错误'); } }) app.listen(PORT, () => { console.log(`server is listening on ${PORT}`); });
2. 修改src/index.tsx添加HelmetProvider
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; import { BrowserRouter } from 'react-router-dom'; import { HelmetProvider } from 'react-helmet-async'; ReactDOM.hydrate( <React.StrictMode> <HelmetProvider> <BrowserRouter> <App /> </BrowserRouter> </HelmetProvider> </React.StrictMode>, document.getElementById('root') );
3. 页面组件中使用Helmet定义Meta
以文章页src/pages/article/index.tsx为例:
import { Helmet } from 'react-helmet-async'; const Article = () => { return ( <> <Helmet> <title>文章标题</title> <meta name="description" content="文章描述" /> <meta property="og:title" content="文章OG标题" /> <meta property="og:description" content="文章OG描述" /> </Helmet> {/* 页面其他内容 */} </> ) }
运行方式
- 开发环境执行
npm run dev,同时启动前端开发服务和后端SSR服务 - 生产环境执行
npm run build编译前后端代码,再执行npm start启动生产SSR服务
内容的提问来源于stack exchange,提问作者anoop francis
相关产品推荐
相关产品推荐

