umijs 服务端渲染(SSR) 指南
Umi 是什么?
Umi,中文可发音为乌米,是可扩展的企业级前端应用框架。Umi 以路由为基础的,同时支持配置式路由和约定式路由,保证路由的功能完备,并以此进行功能扩展。然后配以生命周期完善的插件体系,覆盖从源码到构建产物的每个生命周期,支持各种功能扩展和业务需求。
Umi 是蚂蚁集团的底层前端框架,已直接或间接地服务了 3000+ 应用,包括 java、node、H5 无线、离线(Hybrid)应用、纯前端 assets 应用、CMS 应用等。他已经很好地服务了我们的内部用户,同时希望他也能服务好外部用户。
选择umijs v3版本
umijs v4版本已经发布,但是ssr功能还不完善,所以建议使用v3版本。
https://v3.umijs.org/zh-CN/docs/ssr
创建项目
mkdir umi-ssr-demo
cd umi-ssr-demo
npx @umijs/create-umi-app
npm i
npm run start
配置
-
- 配置
.umirc.js
- 配置
import { defineConfig } from 'umi';
export default defineConfig({
nodeModulesTransform: {
type: 'none',
},
routes: [
{ path: '/', component: '@/pages/index' },
],
fastRefresh: {},
/** 开启ssr */
ssr: {}
});
-
- 查看是否开启成功
如上图所示,直接返回了dom,不止一个根节点,这就算成功了
页面title、meta
每个页面都可以配置title和meta,通过配置<Helmet>
组件即可
import React from 'react';
import { Helmet } from 'umi';
export default () => {
return (
<>
{/* 可自定义需不需要编码 */}
<Helmet>
<title>Hello Umi Bar Title</title>
<meta
name="keywords"
content="Hello Umi Bar Title"
/>
<meta
name="description"
content="Hello Umi Bar Title"
/>
</Helmet>
</>
);
};
获取接口数据
可以直接使用 umi-request
像spa应用一样获取接口数据,但是服务端渲染的,我们需要再服务器返回前就把数据获取好
每个页面有getInitialProps方法,在这个方法里可以获取数据,只有页面才有,组件是没有的
import React from 'react';
const Home = (props) => {
const { data } = props;
return (
{/* <div>Hello World</div> */}
<div>{data.title}</div>
)
}
Home.getInitialProps = (async (ctx) => {
return Promise.resolve({
data: {
title: 'Hello World',
}
})
})
当一个页面请求多个接口时,可以使用Promise.all()合并数据,能使请求速度变快,多个请求同时发出,而不是链式获取
import React from 'react';
const Home = (props) => {
const { data } = props;
return (
{/* <div>Hello World</div> */}
<div>{data.title}</div>
)
}
Home.getInitialProps = (async (ctx) => {
const [res1, res2] = await Promise.all([
/** */
])
return {
res1,
res2
}
})
当数据量大了之后,会导致直接卡住,比如分页接口,调整了pageSize参数过大,可能导致页面卡死,可以将接口分片成多个请求,然后合并数据
export const useFiberRequest = async (fn, parames) => {
const { page = 1, page_size = 10 } = parames?.pagination || {};
const fiberSize = 10;
const promiseList: any[] = [];
for (let p = 1; p <= page_size / fiberSize; p++) {
const pagination = {
page: (page - 1) * (page_size / fiberSize) + p,
page_size: fiberSize,
};
promiseList.push(
fn({
...parames,
pagination,
}),
);
}
const resList: any[] = await Promise.all(promiseList);
const list = resList?.map((item) => item?.data?.list);
const total = resList?.[0]?.data?.total || 0;
return {
data: {
list: list?.flat(),
total,
},
};
};
富文本
只有 div 标签 dangerouslySetInnerHTML 属性才能被 SSR 渲染,正常的写法应该是:
- <p dangerouslySetInnerHTML={{ __html: '<p>Hello</p>' }} />
+ <div dangerouslySetInnerHTML={{ __html: '<p>Hello</p>' }} />
与 dva 结合使用
已内置 dva,通过以下步骤使用:
-
- 配置
.umirc.ts
开启
- 配置
export default {
dva: {}
}
使用antd v5
由于antd v5是css in js的形式,所以样式是异步导入,导致ssr渲染时会先渲染出接口,然后闪一下,才加载出样式
我们可以把antd v5的样式提前加载,在ssr渲染时,先渲染出样式,然后再渲染出结构
- 安装包
npm i -D @ant-design/static-style-extract ts-node cross-env
- 生成antd v5 样式脚本
// src/scripts/genAntdCss.tsx
import { extractStyle } from '@ant-design/static-style-extract';
import fs from 'fs';
const outputPath = './public/css/antd.min.css';
// 1. default theme
const css = extractStyle();
// 2. With custom theme
// const css = extractStyle(withTheme);
fs.writeFileSync(outputPath, css);
console.log(`🎉 Antd CSS generated at ${outputPath}`);
- 在package.json中添加脚本
{
"scripts": {
"predev": "ts-node --project ./tsconfig.node.json ./scripts/genAntdCss.tsx",
"prebuild": "cross-env NODE_ENV=production ts-node --project ./tsconfig.node.json ./scripts/genAntdCss.tsx",
}
}
- 在app.tsx中引入
import '../public/css/antd.min.css';
部署
需要再起一个node服务,然后将ssr生成的dist目录作为静态资源目录
var Koa = require('koa'),
logger = require('koa-logger'),
json = require('koa-json'),
views = require('koa-views'),
onerror = require('koa-onerror');
const { extname } = require('path');
const app = new Koa();
// error handler
onerror(app);
let render;
app.use(async (ctx, next) => {
const req = ctx.req;
const res = ctx.res;
const ext = extname(ctx.request.path);
if (ext) {
await next();
return;
}
// 或者从 CDN 上下载到 server 端
// const serverPath = await downloadServerBundle('http://cdn.com/bar/umi.server.js');
if (!render) {
render = require('./dist/umi.server');
}
res.setHeader('Content-Type', 'text/html');
const context = {};
const { html, error, rootContainer } = await render({
// 有需要可带上 query
path: req.url,
context,
// 可自定义 html 模板
// htmlTemplate: defaultHtml,
// 启用流式渲染
// mode: 'stream',
// html 片段静态标记(适用于静态站点生成)
// staticMarkup: false,
// 扩展 getInitialProps 在服务端渲染中的参数
// getInitialPropsCtx: {},
// manifest,正常情况下不需要
});
if (error) {
console.log('----------------服务端报错-------------------', error);
ctx.throw(500, error);
}
ctx.body = html;
});
// global middlewares
app.use(
views('views', {
root: __dirname + '/views',
default: 'jade',
}),
);
app.use(require('koa-bodyparser')());
app.use(json());
app.use(logger());
app.use(function* (next) {
var start = new Date();
yield next;
var ms = new Date() - start;
console.log('%s %s - %s', this.method, this.url, ms);
});
app.use(require('koa-static')(__dirname + '/dist'));
// error-handling
app.on('error', (err, ctx) => {
console.error('server error', err, ctx);
});
module.exports = app;
运行node服务,将ssr生成的dist目录替换服务中的dist目录即可