Bootstrap

umijs 服务端渲染(SSR) 指南

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

配置

    1. 配置 .umirc.js
import { defineConfig } from 'umi';

export default defineConfig({
  nodeModulesTransform: {
    type: 'none',
  },
  routes: [
    { path: '/', component: '@/pages/index' },
  ],
  fastRefresh: {},
  /** 开启ssr */
  ssr: {}
});

    1. 查看是否开启成功

20240606112924

如上图所示,直接返回了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,通过以下步骤使用:

    1. 配置.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目录即可

;