UmiJS 服务端渲染
本文主要介绍 UmiJS 的预渲染功能。
一、什么是服务端渲染?
服务端渲染(Server-Side Rendering),是指由 服务端 完成页面的 HTML 结构拼接的页面处理技术,发送到浏览器,然后为其绑定状态与事件,成为完全可交互页面的过程。
二、CSR & SSR
CSR:Client Side Rendering 客户端渲染,流程如下:
SSR:Server Side Rendering 服务端渲染,流程如下:
三、SSR 的优缺点及使用场景
3.1 优点
- 更快的首屏加载速度:无需等待 JS 完成下载且执行才显示内容,更快地看到完整渲染的页面,有更好的用户体验。
- 更友好的 SEO:
- 爬虫可以直接抓取渲染之后的页面,CSR 首次返回的 HTML 文件中,root 节点为空,不包含内容;而 SSR 返回渲染之后的 HTML 片段,内容完整,能更好地被爬虫分析与索引
3.2 缺点
- 对服务器性能消耗较高
- 项目复杂度变高,多了一个 node 中间层
- 需要考虑 SSR 及其的运维、申请、扩容,增加了运维成本
3.3 UmiJS 预渲染
服务端渲染,首先得有后端服务器(一般是 Node.js)才可以使用,而没有后端服务器的情况下,可以使用预渲染。
预渲染与服务端渲染唯一的不同点在于 渲染时机,服务端渲染的时机是在用户访问时执行渲染(即实时渲染,数据一般是最新的),预渲染的时机是在项目构建时,当用户访问时,数据不一定是最新的(如果数据没有实时性,可以直接考虑预渲染)。
预渲染在构建时执行渲染,将渲染后的 HTML 片段生成静态 HTML 文件。无需使用 web 服务器实时动态编译 HTML,适用于 静态站点生成。
四、Umi 服务端渲染
Umi3 在 SSR 上做了大量优化及开发体验的提升,具有以下特性:
- 开箱即用:内置 SSR,一键开启,可在
umi dev
中预览,方便调试开发。 - 服务端框架无关:Umi 不耦合服务端框架(如 Egg.js、Express、Koa),无论是哪种框架或者 Serverless 模式,都可以非常简单的进行集成。
- 支持应用和页面级数据预获取
- 支持按需加载:开启
dynamicImport
(按需加载)后,Umi 3 会根据不同路由加载对应的资源文件(css/js)。 - 内置预渲染功能:Umi 3 中内置了预渲染功能,不再通过安装额外插件使用,同时开启
ssr
和exportStatic
,在umi build
构建时会编译出渲染后的 HTML。 - 支持渲染降级:优先使用 SSR,如果服务端渲染失败,自动降级为客户端渲染(CSR),不影响正常业务流程。
- 支持流式渲染:
ssr: { mode: 'stream' }
即可开启流式渲染,流式 SSR 较正常 SSR 有更少的 TTFB(发送页面请求到接收到应用数据第一个字节所花费的毫秒数) 时间。 - 兼容客户端动态加载:可同时使用 SSR 和 dynamicImport。
- SSR 功能插件化:可通过提供的 API 来自定义 SSR 功能。
4.1 启用服务端渲染
默认情况下,服务端渲染时关闭的,可通过配置开启:
export default {
ssr: {
// 开发模式下的服务端渲染,默认为 true
devServerRender: false,
},
};
4.2 数据预获取
服务端渲染的数据获取方式与 SPA(单页应用) 有所不同,为了让客户端和服务端都能获取到同一份数据,Umi 提供了页面级数据的预获取。
页面级数据获取 - 使用
每个页面可能有单独的数据预获取逻辑,这里我们会获取页面组件上的 getInitialProps
静态方法,执行后将结果注入到该页面组件的 props
中,如:
// pages/index.tsx 函数组件
import { IGetInitialProps } from "umi";
import React from "react";
const Home = (props) => {
const { data } = props;
return {data.title};
};
Home.getInitialProps = (async (ctx) => {
return Promise.resolve({
data: {
title: "Hello World!",
},
});
}) as IGetInitialProps;
export default Home;
// pages/index.tsx 类组件
import { IGetInitialProps } from "umi";
import React from "react";
class Home extends React.Component {
static getInitialProps = (async (ctx) => {
return Promise.resolve({
data: {
title: "Hello World",
},
});
}) as IGetInitialProps;
render() {
const { data } = props;
return {data.title};
}
}
export default Home;
getInitialProps
有几个固定参数:
-
match
:与客户端页面 props 中的match
保持一致,保存当前路由的相关数据 -
isServer
:是否为服务端在执行该方法 -
route
:当前路由对象 -
history
:history 对象
扩展 ctx 参数
为了结合数据流框架,我们提供了 modifyGetInitialPropsCtx
方法,由插件或应用来扩展 ctx
参数,以 dva
为例:
// plugin-dva/runtime.ts
export const ssr = {
modifyGetInitialPropsCtx: async (ctx) => {
ctx.store = getApp()._store;
},
};
然后在页面中,可以获取到 store
:
// pages/index.tsx
const Home = () => ;
Home.getInitialProps = async (ctx) => {
const state = ctx.store.getState();
return state;
};
export default Home;
同时也可以在自身应用中进行扩展:
// app.ts
export const ssr = {
modifyGetInitialPropsCtx: async (ctx) => {
ctx.title = "params";
return ctx;
},
};
同时可以使用 getInitialPropsCtx
将服务端参数扩展到 ctx
中,例如:
app.use(async (req, res) => {
// 或者从 CDN 上下载到 server 端
const render = require("./dist/umi.server");
res.setHeader("Content-Type", "text/html");
const context = {};
const { html, error, rootContainer } = await render({
path: req.url,
query: {},
context,
getInitialPropsCtx: {
req,
},
});
});
在使用的时候,就有 req
对象,不过需要注意的是,只在服务端执行时才有此参数:
Page.getInitialProps = async (ctx) => {
if (ctx.isServer) {
// console.log(ctx.req);
}
return {};
};
则在执行 getInitialProps
方法时,除了以上两个固定参数外,还会获取到 title
和 store
参数。
关于 getInitialProps
执行逻辑和时机,需要注意:
- 开启 ssr,且执行成功
- 未开启
forceInitial
,首屏不触发getInitialProps
,切换页面时会执行请求,和客户端渲染逻辑保持一致。 - 开启
forceInitial
,无论是首屏还是页面切换,都会触发getInitialProps
,目的是始终以客户端请求的数据为准。(有用在静态页面站点的实时数据请求上)
- 未开启
- 未开启 ssr 时,只要页面中有
getInitialProps
静态方法,则会执行该方法。
4.3 部署
执行 umi build
,除了正常的 umi.js
外,会多一个服务端文件:umi.server.js
(相当于服务端入口文件)。然后在后端框架中,引用该文件:
// Express
app.use(async (req, res) => {
const 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,正常情况下不需要
});
// support stream content
if (content instanceof Stream) {
html.pice(res);
html.on("end", function () {
res.end();
});
} else {
res.send(res);
}
});
render
方法参数和返回值如下:
// 参数:
{
// 渲染页面路由,支持 `base` 和带 query 的路由,通过 umi 配置
path: string;
// 可选,初始化数据,传到 getInitialProps 方法的参数中
initialData?: object;
// 自定义 html 模板
htmlTemplate?: string;
// 页面内容挂载节点,与 htmlTemplate 配合使用,默认为 root
mountElementId?: string;
// 上下文数据,可用来标记服务端渲染页面时的状态
context?: object;
// ${protocol}://${host} 扩展 location 对象
origin?: string;
}
// 返回值:
{
// html 内容,服务端渲染错误后,会返回原始 html
html?: string | Stream;
// 挂载节点中的渲染内容(ssr渲染实际上只是渲染挂载节点中的内容),同时你也可以用该值来拼接自定义模板
rootContainer: string | Stream;
// 错误对象,服务端渲染错误后,值不为 null
error?: Error;
}
4.4 动态加载、流式渲染、预渲染
完美兼容客户端动态加载,配置如下:
// .umirc.ts
export default {
ssr: {},
// 开启动态加载,使用动态加载后,启动和构建会自动开启 manifest 配置,并在 dist 目录中生成 `asset-manifest.json` 做资源映射,并自动将页面对应的资源注入到 HTML 中,避免开启动态加载后,页面首屏闪烁问题。
dynamicImport: {},
// 开启流式渲染功能
mode: "stream",
// 开启预渲染,默认情况下,预渲染后会删除 umi.server.js 服务端入口文件,可通过 `RM_SERVER_FILE=none` 来保留 `umi.server.js`
exportStatic: {
// 预渲染动态路由:默认情况下,预渲染不会渲染动态路由里的所有页面,如果需要渲染动态路由中的页面,可通过配置 `extraRoutePaths`
extraRoutePaths: async () => {
// const result = await request('https://your-api/news/list');
return Promise.resolve(["/news/1", "/news/2"]);
},
},
};
4.5 页面标题渲染
@umijs/preset-react
插件集中已内置对标题的渲染,通过以下步骤使用:
// pages/bar.tsx
import React from "react";
import { Helmet } from "umi";
export default (props) => {
return (
{/* 可自定义需不需要编码 */}
Hello Umi Bar Title
);
};
4.6 与 dva 结合使用
@umijs/preset-react
插件集中已内置 dva
export default {
ssr: {},
// 开启dva,并在 modules 目录下创建 dva model
dva: {},
};
这时候 getInitialProps(ctx)
中的 ctx
就会有 store
属性,可执行 dispatch
,并返回初始化数据。
Page.getInitialProps = async (ctx) => {
const { store } = ctx;
store.dispatch({
type: "bar/getData",
});
return store.getState();
};
4.7 包大小分析
Umi 同时支持对服务端和客户端包大小的分析
# 服务端包大小分析
$ ANALYZE_SSR=1 umi build
# 客户端包大小分析
$ ANALYZE=1 umi build
参考资料
- UmiJS-SSR
- 掘金-react ssr 详解
- 基于浏览器客户端的流式渲染技术难点