UmiJS SSR实战:从零搭建企业级服务端渲染应用(附完整配置流程)
在当今快节奏的数字化时代,企业级前端应用对性能、SEO和首屏渲染速度的要求越来越高。服务端渲染(SSR)技术作为解决这些痛点的关键方案,正逐渐成为现代Web开发的标配。而UmiJS作为蚂蚁金服开源的企业级前端框架,其内置的SSR能力让开发者能够轻松构建高性能的同构应用。
本文将带你从零开始,完整搭建一个基于UmiJS的SSR应用,涵盖项目初始化、配置优化、数据预取、性能调优以及与Dva状态管理和Ant Design组件库的深度集成。无论你是希望提升现有应用的SEO表现,还是需要为新产品选择技术方案,这篇实战指南都将为你提供清晰的路径。
1. 环境准备与项目初始化
在开始SSR之旅前,我们需要确保开发环境准备就绪。推荐使用Node.js 16+版本和npm 8+版本,以获得最佳的开发体验和性能表现。
首先创建项目目录并初始化Umi应用:
mkdir umi-ssr-enterprise && cd umi-ssr-enterprise
npx @umijs/create-umi-app
选择项目模板时,建议使用ant-design-pro模板,它已经预置了企业级应用所需的许多最佳实践:
? Pick template › - Use arrow-keys. Return to submit.
❯ ant-design-pro
app
block
plugin
安装完成后,项目结构应包含以下关键目录:
├── config/ # Umi配置
├── src/ # 源代码
│ ├── pages/ # 页面组件
│ ├── models/ # Dva模型
│ └── app.ts # 运行时配置
├── .umirc.ts # 主配置文件
└── package.json
重要版本选择:目前Umi v4已发布,但其SSR功能仍在完善中。对于生产环境,建议使用更稳定的Umi v3版本。可以通过以下命令明确指定版本:
npx @umijs/create-umi-app@3
2. SSR核心配置与基础优化
启用SSR功能只需在.umirc.ts中进行简单配置,但为了企业级应用的稳定运行,我们需要进行更细致的调优。
2.1 基础SSR配置
修改.umirc.ts文件,添加SSR相关配置:
import { defineConfig } from 'umi';
export default defineConfig({
ssr: {
// 启用服务端渲染
mode: 'stream', // 使用流式渲染提升性能
forceInitial: false, // 是否强制首次渲染使用SSR
removeWindowInitialProps: true, // 移除window.__initialProps__
devServerRender: true, // 开发模式下也启用SSR
},
nodeModulesTransform: {
type: 'none', // 提升编译速度
},
hash: true, // 开启文件哈希
dva: {
immer: true, // 启用Immer简化reducer编写
hmr: false, // SSR环境下关闭热更新
},
});
2.2 静态资源优化
SSR应用对静态资源的加载性能要求更高,Umi提供了多种优化手段:
export default defineConfig({
// ...其他配置
chunks: ['vendors', 'umi'], // 明确指定代码分割策略
chainWebpack(config) {
// 添加Webpack优化配置
config.merge({
optimization: {
splitChunks: {
chunks: 'all',
minSize: 30000,
minChunks: 3,
automaticNameDelimiter: '.',
cacheGroups: {
vendor: {
name: 'vendors',
test: /[\\/]node_modules[\\/]/,
priority: 10,
},
},
},
},
});
},
});
2.3 页面元信息管理
SEO优化是SSR的重要目标之一,我们可以通过@umijs/plugin-helmet管理页面标题和meta标签:
- 首先安装插件:
npm install @umijs/plugin-helmet --save-dev
- 在页面组件中使用:
import React from 'react';
import { Helmet } from 'umi';
const ProductPage = () => (
<>
<Helmet>
<title>产品中心 - 企业名称</title>
<meta name="keywords" content="产品,企业,解决方案" />
<meta name="description" content="企业产品中心页面" />
</Helmet>
{/* 页面内容 */}
</>
);
3. 数据预取与状态管理
SSR的核心优势之一是在服务端完成数据获取,直接将完整页面返回给客户端。Umi结合Dva提供了完善的数据预取方案。
3.1 页面级数据预取
Umi为每个页面组件提供了getInitialProps静态方法,用于在服务端获取数据:
// src/pages/products/index.tsx
import React from 'react';
import { connect } from 'dva';
const ProductsPage = ({ products, loading }) => {
if (loading) return <div>加载中...</div>;
return (
<div>
{products.map(product => (
<div key={product.id}>{product.name}</div>
))}
</div>
);
};
ProductsPage.getInitialProps = async ({ store, route }) => {
// 触发Dva model中的effect
await store.dispatch({
type: 'products/fetch',
payload: { pageSize: 10, current: 1 }
});
// 返回的数据会作为props传递给组件
return {
// 可以从store中获取数据
products: store.getState().products.list,
loading: store.getState().loading.effects['products/fetch']
};
};
export default connect(({ products, loading }) => ({
products: products.list,
loading: loading.effects['products/fetch'],
}))(ProductsPage);
3.2 Dva Model配置
在src/models/products.ts中定义数据模型:
import { queryProducts } from '@/services/api';
export default {
namespace: 'products',
state: {
list: [],
total: 0,
},
effects: {
*fetch({ payload }, { call, put }) {
const response = yield call(queryProducts, payload);
yield put({
type: 'save',
payload: response,
});
},
},
reducers: {
save(state, { payload }) {
return {
...state,
list: payload.data,
total: payload.total,
};
},
},
};
3.3 复杂数据预取策略
对于需要多个接口并行请求的场景,可以使用Promise.all优化请求效率:
HomePage.getInitialProps = async ({ store }) => {
const [productsRes, bannersRes] = await Promise.all([
store.dispatch({ type: 'products/fetch' }),
store.dispatch({ type: 'banners/fetch' }),
]);
return {
// 合并数据
};
};
4. 与Ant Design的深度集成
Ant Design是企业级UI设计的首选方案,但在SSR环境下需要特殊处理样式问题。
4.1 解决Antd v5的SSR样式问题
Antd v5采用CSS-in-JS方案,需要在服务端提前提取样式:
- 安装必要依赖:
npm install @ant-design/static-style-extract ts-node cross-env --save-dev
- 创建样式生成脚本
scripts/genAntdCss.ts:
import { extractStyle } from '@ant-design/static-style-extract';
import fs from 'fs';
const outputPath = './public/antd.min.css';
const css = extractStyle();
fs.writeFileSync(outputPath, css);
- 在
package.json中添加预处理命令:
{
"scripts": {
"predev": "ts-node --project tsconfig.node.json scripts/genAntdCss.ts",
"prebuild": "cross-env NODE_ENV=production ts-node --project tsconfig.node.json scripts/genAntdCss.ts"
}
}
- 在
app.ts中引入生成的样式文件:
import './public/antd.min.css';
4.2 按需加载优化
虽然我们已经提取了全局样式,但仍需配置组件按需加载以减少包体积:
// .umirc.ts
export default defineConfig({
extraBabelPlugins: [
[
'import',
{
libraryName: 'antd',
libraryDirectory: 'es',
style: true,
},
],
],
});
5. 性能优化与部署实践
企业级应用对性能有严格要求,下面介绍几种关键的优化手段。
5.1 流式渲染配置
Umi支持流式SSR,可以显著提升首字节到达时间(TTFB):
// .umirc.ts
export default defineConfig({
ssr: {
mode: 'stream', // 启用流式渲染
},
});
5.2 缓存策略
实现简单的页面级缓存可以大幅减轻服务器压力:
// 自定义服务端代码
import LRU from 'lru-cache';
const ssrCache = new LRU({
max: 100, // 缓存100个页面
maxAge: 1000 * 60 * 5, // 5分钟
});
app.use(async (ctx, next) => {
const hit = ssrCache.get(ctx.url);
if (hit) {
ctx.body = hit;
return;
}
const { html } = await render({ path: ctx.url });
ssrCache.set(ctx.url, html);
ctx.body = html;
});
5.3 生产环境部署
生产环境部署需要构建应用并启动Node服务:
- 构建命令:
npm run build
- 启动服务脚本
server.js:
const Koa = require('koa');
const path = require('path');
const serve = require('koa-static');
const app = new Koa();
// 静态资源服务
app.use(serve(path.join(__dirname, 'dist')));
// SSR渲染
let render;
app.use(async (ctx) => {
if (!render) {
render = require('./dist/umi.server');
}
ctx.set('Content-Type', 'text/html');
const { html } = await render({
path: ctx.url,
mode: 'stream',
});
ctx.body = html;
});
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
- 使用PM2管理进程:
npm install pm2 -g
pm2 start server.js -i max
6. 常见问题与解决方案
在实际企业级应用中,SSR会遇到各种边界情况。以下是几个典型问题的解决方案。
6.1 用户认证处理
SSR环境下需要特别注意认证状态的同步:
// app.ts
export function getInitialState() {
// 从cookie中获取认证信息
const token = getCookie('token');
return {
isLogin: !!token,
user: token ? jwtDecode(token) : null,
};
}
// 页面组件
UserPage.getInitialProps = async ({ store, ctx }) => {
if (!store.getState().user.isLogin && ctx?.req) {
// 服务端重定向
ctx.res.writeHead(302, { Location: '/login' });
ctx.res.end();
return {};
}
// 正常获取数据
};
6.2 错误边界处理
SSR环境下的错误需要特殊处理以避免页面崩溃:
// 创建错误边界组件
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, info) {
// 上报错误到监控系统
logErrorToService(error, info);
}
render() {
if (this.state.hasError) {
return <h1>页面加载出错,请刷新重试</h1>;
}
return this.props.children;
}
}
// 在app.ts中包裹路由
export function rootContainer(container) {
return <ErrorBoundary>{container}</ErrorBoundary>;
}
6.3 性能监控
集成APM工具监控SSR性能:
// 自定义服务端中间件
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const duration = Date.now() - start;
// 上报渲染耗时
metrics.timing('ssr.render_time', duration, {
path: ctx.path,
status: ctx.status,
});
// 添加Server-Timing头
ctx.set('Server-Timing', `render;dur=${duration}`);
});
7. 进阶优化技巧
对于大型企业应用,这些进阶技巧可以进一步提升SSR体验。
7.1 组件级数据预取
除了页面级getInitialProps,还可以实现组件级数据预取:
// 高阶组件实现
function withSSRData(fetchData) {
return (Component) => {
const WrappedComponent = (props) => <Component {...props} />;
WrappedComponent.getInitialProps = async (ctx) => {
const componentProps = Component.getInitialProps
? await Component.getInitialProps(ctx)
: {};
const data = await fetchData(ctx);
return {
...componentProps,
...data,
};
};
return WrappedComponent;
};
}
// 使用示例
const fetchUserData = () => ({ /* 获取数据 */ });
const UserProfileWithData = withSSRData(fetchUserData)(UserProfile);
7.2 渐进式注水(Hydration)
对于复杂页面,可以采用渐进式注水策略:
// 使用React.lazy和Suspense实现
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));
const ProductPage = () => (
<div>
{/* 关键内容直接渲染 */}
<ProductHeader />
{/* 非关键内容延迟注水 */}
<Suspense fallback={<div>加载中...</div>}>
<HeavyComponent />
</Suspense>
</div>
);
7.3 静态生成混合策略
结合SSG(Static Site Generation)和SSR实现混合渲染:
// .umirc.ts
export default defineConfig({
exportStatic: {
dynamicRoot: true, // 允许部分页面静态化
extraRoutePaths: async () => {
// 从API获取需要静态化的路由
const res = await fetch('https://api.example.com/static-routes');
return res.json();
},
},
ssr: {
// 非静态路由使用SSR
forceInitial: false,
},
});
8. 测试与调试
完善的测试策略是保证SSR应用稳定性的关键。
8.1 端到端测试
使用Cypress进行SSR特定的端到端测试:
// cypress/integration/ssr.spec.js
describe('SSR测试', () => {
it('应正确渲染页面内容', () => {
cy.request('/products') // 直接请求SSR页面
.its('body')
.should('include', '<div id="root">') // 检查SSR内容
.and('include', '产品列表'); // 检查数据预取
cy.visit('/products'); // 客户端导航
cy.contains('产品详情').click();
cy.url().should('include', '/products/'); // 检查客户端路由
});
});
8.2 服务端调试
在Node服务中集成诊断工具:
// server.js
const { createServer } = require('http');
const { renderToPipeableStream } = require('react-dom/server');
const { createElement } = require('react');
const { StaticRouter } = require('react-router-dom/server');
const server = createServer((req, res) => {
// 启用React的Profiler API
const { pipe, abort } = renderToPipeableStream(
createElement(
React.Profiler,
{ id: "SSR", onRender: (id, phase, actualTime) => {
console.log(`渲染耗时: ${actualTime}ms`);
}},
createElement(StaticRouter, { location: req.url })
),
{
onShellReady() {
res.setHeader('Content-type', 'text/html');
pipe(res);
},
onError(error) {
console.error('SSR错误:', error);
abort();
}
}
);
});
8.3 内存泄漏检测
SSR服务容易发生内存泄漏,需要定期检测:
const heapdump = require('heapdump');
const path = require('path');
// 每小时生成一次堆快照
setInterval(() => {
const filename = path.join(__dirname, 'heapdump', `${Date.now()}.heapsnapshot`);
heapdump.writeSnapshot(filename, (err) => {
if (err) console.error('堆快照失败:', err);
else console.log('堆快照已保存:', filename);
});
}, 60 * 60 * 1000);
9. 安全加固
企业级应用必须考虑各种安全风险。
9.1 XSS防护
虽然React默认会转义HTML,但在SSR环境下仍需额外防护:
// 安全中间件
app.use(async (ctx, next) => {
await next();
if (typeof ctx.body === 'string') {
// 添加安全头
ctx.set('X-XSS-Protection', '1; mode=block');
ctx.set('Content-Security-Policy', "default-src 'self'");
}
});
// 在Helmet中配置更严格的安全策略
<Helmet>
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline';" />
</Helmet>
9.2 CSRF防护
实现CSRF令牌验证:
// app.ts
export const request = {
prefix: '/api',
credentials: 'include',
errorConfig: {
adaptor: (resData) => ({
...resData,
success: resData.code === 200,
errorMessage: resData.message,
}),
},
middlewares: [
async (ctx, next) => {
// 添加CSRF令牌
const token = getCookie('csrfToken');
if (token) {
ctx.req.options.headers = {
...ctx.req.options.headers,
'X-CSRF-Token': token,
};
}
await next();
},
],
};
9.3 请求限流
防止恶意请求耗尽服务器资源:
const rateLimit = require('koa-ratelimit');
// 应用限流中间件
app.use(rateLimit({
driver: 'memory',
db: new Map(),
duration: 60000,
errorMessage: '请求过于频繁,请稍后再试',
id: (ctx) => ctx.ip,
headers: {
remaining: 'Rate-Limit-Remaining',
reset: 'Rate-Limit-Reset',
total: 'Rate-Limit-Total'
},
max: 100, // 每分钟100次请求
disableHeader: false,
}));
10. 国际化与本地化
企业级应用通常需要支持多语言,SSR环境下需要特殊处理。
10.1 服务端语言检测
基于请求头确定用户语言:
// app.ts
export function getInitialState() {
// 服务端通过ctx获取语言
if (typeof window === 'undefined' && ctx?.req) {
const lang = ctx.req.acceptsLanguages(['zh-CN', 'en-US']) || 'zh-CN';
return { locale: lang };
}
// 客户端从localStorage获取
return { locale: localStorage.getItem('locale') || 'zh-CN' };
}
// 页面组件
ProductPage.getInitialProps = async ({ store }) => {
const { locale } = store.getState();
await store.dispatch({
type: 'products/fetch',
payload: { lang: locale }
});
};
10.2 静态文本提取
优化多语言包的加载性能:
// .umirc.ts
export default defineConfig({
locale: {
default: 'zh-CN',
baseNavigator: true,
antd: true,
baseSeparator: '-',
// 按需加载语言包
dynamicImport: {
loading: '@/components/LocaleLoading',
},
},
});
10.3 服务端时区处理
正确处理日期时间的时区问题:
// 服务端中间件
app.use(async (ctx, next) => {
// 从请求头获取时区
const timezone = ctx.get('X-Timezone') || 'Asia/Shanghai';
// 设置全局时区
process.env.TZ = timezone;
await next();
});
// 客户端代码
new Date().toLocaleString('en-US', {
timeZone: 'America/New_York'
});
11. 微前端集成
大型企业可能采用微前端架构,SSR需要特殊适配。
11.1 子应用SSR配置
使用qiankun微前端框架时,子应用的SSR配置:
// 子应用.umirc.ts
export default defineConfig({
ssr: {
// 修改静态资源路径为绝对路径
modifyServerRenderOpts(memo) {
return {
...memo,
publicPath: 'https://cdn.example.com/sub-app/',
};
},
},
runtimePublicPath: true,
});
11.2 主应用集成
主应用集成SSR子应用的策略:
// 主应用server.js
app.use(async (ctx) => {
const { html: mainAppHtml } = await mainAppRender({ path: ctx.url });
// 并行获取子应用内容
const [subApp1, subApp2] = await Promise.all([
fetch('https://sub-app1.example.com/render?path=' + ctx.url),
fetch('https://sub-app2.example.com/render?path=' + ctx.url),
]);
// 合并HTML
const finalHtml = mainAppHtml
.replace('<!-- sub-app-1 -->', await subApp1.text())
.replace('<!-- sub-app-2 -->', await subApp2.text());
ctx.body = finalHtml;
});
11.3 样式隔离
确保微前端样式不会相互影响:
// 子应用配置
export default defineConfig({
ssr: {
// 为所有样式添加命名空间
modifyStyles(styles) {
return styles.map(style => ({
...style,
content: `.sub-app-1 ${style.content}`,
}));
},
},
});
12. 监控与告警
生产环境需要完善的监控体系来保证SSR服务的稳定性。
12.1 性能指标采集
使用Prometheus采集关键指标:
const client = require('prom-client');
// 定义指标
const ssrRenderTime = new client.Histogram({
name: 'ssr_render_time_seconds',
help: 'SSR渲染耗时',
buckets: [0.1, 0.5, 1, 2, 5],
});
// 中间件记录指标
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const duration = (Date.now() - start) / 1000;
ssrRenderTime.observe(duration, {
path: ctx.path,
status: ctx.status,
});
});
// 暴露metrics端点
app.use(async (ctx) => {
if (ctx.path === '/metrics') {
ctx.set('Content-Type', client.register.contentType);
ctx.body = await client.register.metrics();
}
});
12.2 错误日志集成
集中式错误日志收集:
// app.ts
export function onError(error: Error) {
// 上报错误到Sentry
Sentry.captureException(error);
// 开发环境打印堆栈
if (process.env.NODE_ENV === 'development') {
console.error(error);
}
}
// 服务端错误处理
app.on('error', (err) => {
Sentry.captureException(err);
});
12.3 健康检查
实现Kubernetes友好的健康检查:
// 健康检查端点
router.get('/health', (ctx) => {
// 检查数据库连接
const dbOk = checkDatabaseConnection();
// 检查内存使用
const memoryUsage = process.memoryUsage().rss / 1024 / 1024;
const memoryOk = memoryUsage < 500; // 不超过500MB
ctx.status = dbOk && memoryOk ? 200 : 503;
ctx.body = {
status: ctx.status === 200 ? 'OK' : 'Service Unavailable',
details: {
database: dbOk ? 'connected' : 'disconnected',
memory: `${memoryUsage.toFixed(2)}MB`,
},
};
});
13. 持续集成与部署
自动化流程是保证企业级应用稳定交付的关键。
13.1 CI/CD流程
典型的GitLab CI配置示例:
# .gitlab-ci.yml
stages:
- build
- test
- deploy
build:
stage: build
image: node:16
script:
- npm install
- npm run build
artifacts:
paths:
- dist/
- public/
test:
stage: test
image: cypress/base:16
script:
- npm run test:e2e
deploy:
stage: deploy
image: alpine
script:
- rsync -avz --delete dist/ user@server:/var/www/app/
- ssh user@server "pm2 reload app"
13.2 渐进式发布
使用Docker实现蓝绿部署:
# Dockerfile
FROM node:16-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
# 部署脚本
docker build -t app:ssr-v2 .
docker run -d -p 3001:3000 --name app-ssr-v2 app:ssr-v2
# 测试新版本
curl http://localhost:3001/health
# 切换流量
docker stop app-ssr-v1
docker rm app-ssr-v1
docker service update --image app:ssr-v2 app-ssr
13.3 回滚机制
确保可以快速回滚到稳定版本:
# 回滚到上一个版本
docker service rollback app-ssr
# 或者手动指定版本
docker service update --image app:ssr-v1 app-ssr
14. 未来演进方向
随着技术发展,SSR架构也在不断演进。值得关注的几个方向:
- React Server Components:React官方推出的服务端组件方案,可以与现有SSR结合使用
- Edge SSR:利用边缘计算节点实现就近渲染,降低延迟
- Islands Architecture:混合静态生成和动态SSR,提升性能
- Resumability:Qwik框架提出的可恢复性渲染概念,减少注水成本
在实际项目中,我们发现流式SSR结合CDN缓存能够提供最佳的用户体验。对于内容变化不频繁的页面,设置适当的缓存时间可以大幅减轻服务器压力。而对于个性化内容,则需要在边缘节点进行差异化渲染。
&spm=1001.2101.3001.5002&articleId=159705369&d=1&t=3&u=d190120023c24e9d8d017c08adbb9949)
1867

被折叠的 条评论
为什么被折叠?



