eggjs + umi + ssr

之前写过一篇类似文章,本篇为重写

本篇教程讲 eggjs 怎么配合 umi 开启 ssr(服务端渲染)。

一、 创建 eggjs 工程

1
2
3
4
5
6
7
8
9
# 创建项目目录
mkdir ssr-with-eggjs && cd ssr-with-eggjs
# 初始化 eggjs 项目
npm init egg --type=simple
# 安装依赖
yarn
# 启动项目,测试下有没问题
yarn dev
open http://127.0.0.1:7001/

二、创建 umi 工程

1
2
3
4
5
6
7
8
9
# 创建 umi 项目目录
mkdir app/web && cd app/web
# 初始化 umi 项目
yarn create @umijs/umi-app
# 安装依赖
yarn
# 启动项目,测试下有没问题
yarn start
open http://127.0.0.1:8000

三、修改 umi 工程配置

.umirc.ts 文件新增如下配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { defineConfig } from 'umi';

export default defineConfig({
...
// 目前开启 dynamicImport 和 ssr,服务端渲染资源文件路径有问题
// dynamicImport: {},
ssr: {
// dev 模式 服务端渲染交给 eggjs 处理
devServerRender: false,
},
hash: true,
outputPath: '../public',
manifest: {
fileName: '../../config/manifest.json',
publicPath: '',
}
...
});

修改项目根目录 .gitignore 文件,添加如下内容

1
2
3
# umi
app/public
config/manifest.json

启动 umi 项目看有没问题

四、修改 eggjs 项目

安装 egg-view-assetsegg-view-nunjucks

1
yarn add egg-view-assets egg-view-nunjucks

config/plugin.js 中开启 egg-view-assetsegg-view-nunjucks 插件

1
2
3
4
5
6
7
8
9
10
11
12
13
'use strict';

/** @type Egg.EggPlugin */
module.exports = {
assets: {
enable: true,
package: 'egg-view-assets',
},
nunjucks: {
enable: true,
package: 'egg-view-nunjucks',
},
};

修改 config/config.default.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/* eslint valid-jsdoc: "off" */

'use strict';
const path = require('path');

/**
* @param {Egg.EggAppInfo} appInfo app info
* @param {Egg.EggAppConfig} appConfig config
*/
module.exports = (appInfo, appConfig = {}) => {
const assetsDir = (appConfig.assets && appConfig.assets.dir) || 'app/web';

/**
* built-in config
* @type {Egg.EggAppConfig}
**/
const config = (exports = {});

// use for cookie sign key, should change to your own and keep security
config.keys = appInfo.name + '_1636442650677_1894';

// add your middleware config here
config.middleware = [];

// add your user config here
const userConfig = {
// 开启 gzip
static: {
gzip: true,
},
// 配置 assets
assets: {
publicPath: '/public',
devServer: {
command: 'app/web/node_modules/umi/bin/umi.js dev',
env: {
APP_ROOT: path.join(appInfo.baseDir, assetsDir),
PORT: '{port}',
BROWSER: 'none',
ESLINT: 'none',
SOCKET_SERVER: 'http://127.0.0.1:{port}',
PUBLIC_PATH: 'http://127.0.0.1:{port}',
},
},
},
// 配置 view
view: {
mapping: {
'.html': 'nunjucks',
},
defaultViewEngine: 'nunjucks',
},
};

return {
...config,
...userConfig,
};
};

创建 config/config.local.js 文件,添加如下配置

1
2
3
4
5
6
7
8
9
10
11
12
13
'use strict';

module.exports = (appInfo) => {
const config = (exports = {});
config.assets = {
devServer: {
debug: true,
autoPort: true,
},
dynamicLocalIP: false,
};
return config;
};

创建 app/view/index.html,添加如下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
<title></title>
{{ helper.assets.getStyle('umi.css') | safe }}
</head>
<body>

<div id="root"></div>

<script>
window.routerBase = '/';
window.resourceBaseUrl = '{{ helper.assets.resourceBase }}';
</script>
{{ helper.assets.getScript('umi.js') | safe }}
</body>
</html>

修改 router.js 文件

1
2
3
4
5
module.exports = app => {
const { router, controller } = app;
// 所有请求都走 controller.home.index
router.get('*', controller.home.index);
};

添加 mime

1
yarn add mime

修改 app/controller/home.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
'use strict';

const Controller = require('egg').Controller;
const mime = require('mime');

class HomeController extends Controller {
constructor(ctx) {
super(ctx);
this.serverRender = require('../public/umi.server');
}
async index() {
const { ctx } = this;

// 先走 eggjs 的 view 渲染
const htmlTemplate = await ctx.view.render('index.html');

// 将 html 模板传到服务端渲染函数中
const { html, error } = await this.serverRender({
path: ctx.url,
getInitialPropsCtx: {},
htmlTemplate,
});

if (error) {
ctx.logger.error(
'[SSR ERROR] 渲染报错,切换至客户端渲染',
error,
ctx.url
);
}
ctx.type = mime.getType(ctx.url);
ctx.status = 200;
ctx.body = html;
}
}

module.exports = HomeController;

创建 app/web/.env,添加如下环境变量

1
2
# build 时不生成 html
HTML=none

eggjs 项目安装 cross-env

1
yarn add cross-env

在根目录的 package.json 添加如下配置

1
2
3
4
5
{
"scripts": {
"build": "cross-env APP_ROOT=app/web app/web/node_modules/umi/bin/umi.js build",
}
}

至此,项目配置基本已经完成,下面介绍怎么启动开发环境和生产环境

开发环境

1
2
// 启动开发环境
yarn dev

生产环境

启动

1
2
3
4
// 构建
yarn build
// 启动
yarn start

关闭

1
yarn stop

总结

以上是根据 umi 官方 srr + egg 总结出来的主要步骤。不过要注意,目前 umi 工程配置 不能开启 dynamicImport ,不然服务端渲染时会出错。

项目地址

按上面配置完后,发现 yarn dev 启动后会报 too many open files 错误,看了下报错日志,是 eggjsweb 项目的 node_modules 文件夹也加入了 watcher,导致大量文件句柄,目前解决方法是将 web 项目依赖都移到项目根目录。以下是解决步骤

1、 将 app/web/package.json 中依赖都移到根目录的 package.json

现在根目录 package.json 新增依赖如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"dependencies": {
"@ant-design/pro-layout": "^6.5.0",
"react": "17.x",
"react-dom": "17.x",
"umi": "^3.5.20"
},
"devDependencies": {
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@umijs/preset-react": "1.x",
"@umijs/test": "^3.5.20",
"lint-staged": "^10.0.7",
"prettier": "^2.2.0",
"typescript": "^4.1.2",
"yorkie": "^2.0.0"
}
}

2、 将 app/web/package.json 中脚本移到根目录的 package.json,根目录 package.json 新增如下脚本:

1
2
3
4
5
6
7
8
9
{
"scripts": {
"build": "cross-env APP_ROOT=app/web umi build",
"start-web": "cross-env APP_ROOT=app/web umi dev",
"postinstall-web": "cross-env APP_ROOT=app/web umi generate tmp",
"test-web": "cross-env APP_ROOT=app/web umi-test",
"test:coverage-web": "cross-env APP_ROOT=app/web umi-test --coverage"
}
}

现在可以把 app/web/package.json 文件删了

3、 修改 config/config.default.json

1
2
3
4
5
6
assets: {
devServer: {
// 改这一行
command: 'umi dev',
},
},

4、 重新安装依赖

1
yarn

以上就是相关解决方案,现在 yarn dev 启动开发环境,就不会报错了。本来想找找有什么配置,能让 eggjs 直接忽略 node_modules,不过没找到。