服务端渲染SSR之UmiJS预渲染

UmiJS 服务端渲染

本文主要介绍 UmiJS 的预渲染功能。

一、什么是服务端渲染?

服务端渲染(Server-Side Rendering),是指由 服务端 完成页面的 HTML 结构拼接的页面处理技术,发送到浏览器,然后为其绑定状态与事件,成为完全可交互页面的过程。

二、CSR & SSR

CSR:Client Side Rendering 客户端渲染,流程如下:

CSR

SSR:Server Side Rendering 服务端渲染,流程如下:

CSR

三、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 中内置了预渲染功能,不再通过安装额外插件使用,同时开启 ssrexportStatic,在 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 方法时,除了以上两个固定参数外,还会获取到 titlestore 参数。

关于 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 详解
  • 基于浏览器客户端的流式渲染技术难点

你可能感兴趣的:(服务端渲染SSR之UmiJS预渲染)