跳转到主文档

开发中后台

本章将介绍如何使用 Modern.js,进行中后台项目的开发。本章对应的代码仓库地址在这里查看

通过本章你可以了解到:

  • 如何创建一个中后台项目。
  • 如何为项目创建新入口。
  • 如何使用客户端路由。
  • 如何集成和使用开源组件库。
  • 如何开发和使用 BFF API。
  • 如何使用 Model 进行状态管理。
  • 如何使用测试功能。
在线预览

在 StackBlitz 中预览:

环境准备

开始之前,请参考【环境准备】部分,确保本地环境正常。

创建项目

使用 @modern-js/create 创建新项目,运行命令如下:

npx @modern-js/create@modern-1 middle-platform

middle-platform 为创建的项目名。

按照如下选择,生成项目:

? 请选择你想创建的工程类型: 应用
? 请选择开发语言: TS
? 请选择包管理工具: pnpm
? 是否需要支持以下类型应用: 不需要
? 是否需要调整默认配置: 否

开发调试

进入项目根目录, 之后执行 pnpm run dev 即可启动开发服务器:

# 进入项目根目录
cd middle-platform

# 启动开发服务器
pnpm run dev

浏览器中访问 http://localhost:8080,可以看到应用已经正常启动。

修改 src/App.tsx 会触发重新编译和热更新,浏览器中页面会自动展示对应变化。

IDE 支持

Modern.js 对 VS Code 等主流 IDE 提供了开箱即用的支持,具备 Lint 问题自动检测、自动修复,代码提交前的准入检查等功能特性,可以让代码开发更加高效和智能。详细介绍请参考【确认编程环境】。

创建入口

在 Modern.js 中,一个入口,经过构建后会生成一个对应的 HTML 文件。默认生成的项目只包含一个入口。现在,我们创建一个新入口,对应中后台应用的控制台模块,而原有入口对应中后台应用的落地页。

在项目根目录下,执行 pnpm run new,进行如下选择:

? 请选择你想要的操作: 创建工程元素
? 创建工程元素: 新建「应用入口」
? 填写入口名称:console
? 是否修改默认的应用入口配置:否

创建完成,项目的 src 目录下会有两个目录:

.
├── src/
│   ├── console/
│   │   └── App.tsx
│   ├── middle-platform/
│   │   └── App.tsx
│   ├── .eslintrc.json

其中,console/ 目录对应新建的入口,项目默认的入口(主入口)代码被移动到 middle-platform/ 目录下。

使用生成器将应用从单入口转换成多入口时,原本主入口的代码将会被移动到与当前应用 package.json 同名的目录下。

重新启动应用,控制台会输出不同入口对应的访问地址。默认情况下,主入口对应的访问地址为 {域名根路径},其他入口对应的访问地址为 {域名根路径}/{入口名称},如下所示:

App running at:

> Local:
console http://localhost:8080/console
middle-platform http://localhost:8080/
补充信息

如果需要修改入口名和访问地址的映射关系,可以配置【server.routes】。

我们对两个入口的代码做简单修改:

middle-platform/App.tsx
import React from 'react';

const App: React.FC = () => (
<div>
<div>This is a landing page. </div>
<a href="/console">Go to console</a>
</div>
);

export default App;
console/App.tsx
import React from 'react';

const App: React.FC = () => <div>Console</div>;

export default App;

现在,点击落地页上的链接,可以跳转到控制台入口对应的页面。

补充信息

关于入口的更多介绍,请参考 【添加应用入口】。

客户端路由

console 入口对应中后台应用的控制台模块,控制台模块一般会实现为一个复杂的 SPA 应用,所以需要使用客户端路由。默认生成的项目已经开启客户端路由功能,我们可以直接从 @modern-js/runtime/router 包引入路由相关组件。

console/App.tsx 的代码如下:

import React from 'react';
import { Route, Switch, Link} from '@modern-js/runtime/router';
import Dashboard from './dashboard';
import TableList from './tableList';

const App: React.FC = () => {
return (
<div>
<div>
<Link to="/">Dashboard</Link> &nbsp;
<Link to="/table">Table</Link>
</div>
<Switch>
<Route path="/" exact={true}>
<Dashboard/>
</Route>
<Route path="/table">
<TableList/>
</Route>
</Switch>
</div>
);
};

export default App;

console/App.tsxDashboardTableList 两个组件,分别定义在 console/dashboardconsole/tableList 两个文件夹下,代码如下:

console/dashboard/index.tsx
import React from 'react';

const Dashboard: React.FC = () => <div>Dashboard Page</div>;

export default Dashboard;
console/tableList/index.tsx
import React from 'react';

const TableList: React.FC = () => <div>TableList Page</div>;

export default TableList;

此时,点击页面上的两个链接,浏览器地址栏的 URL 发生变化,页面渲染的组件也随之更改,说明客户端路由可以正常工作。

补充信息

console/App.tsx 中客户端路由的使用方式在 Modern.js 中称为自控式路由,Modern.js 还支持约定式路由,关于路由的详细介绍,请参考【添加客户端路由】。

代码分片

当前代码在构建后,会把所有路由用到的组件都打包到一个 JS 文件中。打开浏览器开发者工具的 Network 窗口, console.js 对应所有路由组件打包后的 JS 文件,如下图所示:

code-split-1

我们可以使用 loadable,并根据路由划分,对代码进行分片。

Modern.js 对 loadable 提供了开箱即用的支持,可以直接从 '@modern-js/runtime/loadable' 导出函数,例如:

console/App.tsx
import loadable from '@modern-js/runtime/loadable'

const Dashboard = loadable(() => import('./dashboard'));
const TableList = loadable(() => import('./tableList'));

const App: React.FC = () => {
// ...
};

export default App;

此时,切换不同路由,会按需加载对应路由所需要的组件代码。如下图所示:

code-split-2

当访问 /console 路由时,会加载 src_console_dashboard_index_tsx.js 这个文件;当访问 /console/table 路由时,会加载 src_console_tableList_index_tsx.js 这个文件。

补充信息

关于 loadable 的更多用法,请参考【loadable API】。

集成组件库

中后台项目通常会集成第三方组件库,以提高组件开发效率。这里,我们以 Ant Design 为例,介绍组件库的集成方式。

首先需要安装组件库依赖:

pnpm add antd

然后,在需要使用 Ant Design 的入口文件中引入组件库的样式,这里我们在 /console/App.tsx 中引入:

import 'antd/dist/antd.css';

这样,我们就可以在任意组件中使用 Ant Design 的组件了。我们在 TableList 组件中使用 Table 组件:

import React from 'react';
import { Table } from 'antd';

const TableList: React.FC = () => {
const columns = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
},
{
title: 'Age',
dataIndex: 'age',
key: 'age',
},
{
title: 'Country',
dataIndex: 'country',
key: 'country',
},
];

const data = [
{
key: '1',
name: 'John Brown',
age: 32,
country: 'America',
},
{
key: '2',
name: 'Jim Green',
age: 42,
country: 'England',
},
{
key: '3',
name: 'Ming Li',
age: 30,
country: 'China',
},
];

return (
<div>
<Table columns={columns} dataSource={data} />
</div>
);
};

export default TableList;

此时,访问 http://localhost:8080/console/table,可以看到页面上会渲染出 Table 组件。

按需加载组件样式

直接 import 'antd/dist/antd.css' 会将组件库包含的所有组件的样式都引入进来。我们可以借助 babel-plugin-import 插件,实现组件样式的按需加载。

Modern.js 已经内置了 babel-plugin-import 插件,但因为 Ant Design 使用 Less 编写组件样式,我们还需要开启 Less 支持。

在项目根目录下,执行 pnpm run new,进行如下选择:

? 请选择你想要的操作: 启用可选功能
? 启用可选功能: 启用 Less 支持

然后,我们删除 /console/App.tsx 中引入 Ant Design 组件库样式的代码。

重新访问 http://localhost:8080/console/table,可以看到页面上依然可以渲染出带有样式的 Table 组件。查看浏览器开发者工具的 Network 标签,会发现此时加载的 Ant Design 的 CSS 文件体积也变小了。

另外,开启 Less 支持后,我们也可以在单独的样式文件中使用 Less 语法。

补充信息

关于组件样式的更多用法,请参考【CSS 开发方案】。

一体化 BFF

当前 Table 组件使用的数据是静态数据,我们现在希望能通过服务端 API 动态获取数据。服务端 API 地址为:https://lf3-static.bytednsdoc.com/obj/eden-cn/beeh7uvzhq/users.json

但这个 API 并不是专门为当前项目提供的,部署也是在一个独立的域名下。

通常情况下,项目需要创建一个和当前项目部署在同一域名下的专属 API,并在这个 API 内部去调用原始数据获取,并进行裁剪聚合等。在前端,这样的需求通常使用 BFF 层来实现。

Modern.js 提供了开箱即用的 BFF 能力,支持和前端代码共同开发、调试、部署。

如果已经具备了为前端项目专门开发的、部署在同域下的 API,则不需要再创建 BFF 层,前端代码直接调用 API 即可。

首先,需要开启 BFF 功能,在项目根目录下,执行 pnpm run new,进行如下选择:

? 请选择你想要的操作: 启用可选功能
? 启用可选功能: 启用「BFF」功能
? 请选择 BFF 类型:函数写法
?请选择运行时框架:Express

执行完成后,项目中新增了 api/ 目录,添加在 api/users.ts 文件,实现对获取数据 API 的调用(需要先安装 axios 依赖):

import axios from 'axios';

export default async () => {
const res = await axios.get<
{ key: string; name: string; age: number; country: string }[]
>('https://lf3-static.bytednsdoc.com/obj/eden-cn/beeh7uvzhq/users.json');
return res.data;
};

重新执行 dev 命令,我们已经可以访问 http://localhost:8080/api/users,并成功获取用户数据。

下面,我们来修改 /console/tableList/index.tsx,我们可以在组件代码中通过 axios 调用 API 获取数据,但是 Modern.js 提供了一种更加简洁的方式,可以像使用函数一样来调用 API,关键代码如下:

import users from '@api/users'

interface User {
key: string;
name: string;
age: number;
country: string
}

const TableList: React.FC = () => {
const [data, setData] = useState<User[]>([]);

useEffect(() => {
const load = async () => {
const _data = await users();
setData(_data);
}

load();
}, [])

//...
}

通过 import users from '@api/users' 直接引入 users 函数,调用 users 函数起到了和调用 http://localhost:8080/api/users API 同样的作用,这就是 Modern.js 一体化 BFF 的功能。

补充信息

更多信息,请参考【一体化 BFF】。

Mock 数据

在 Modern.js 中使用 Mock 功能,只需要在 config/mock/index.t(j)s 导出一个包含所有 Mock API 的对象,对象的属性由请求 Method 和 URL 组成,对应的值可以为 Object、Array、Function 类型的数据。

现在我们创建一个 mockUser API,代码如下:

config/mock/index.ts
export default {
'GET /api/mockUsers': [
{ key: '1', name: 'Mock Name 1', age: 32, country: 'America' },
{ key: '2', name: 'Mock Name 2', age: 42, country: 'England' },
{ key: '3', name: 'Mock Name 3', age: 30, country: 'China' },
],
};

访问 http://localhost:8080/api/mockUsers 即可获取 Mock API 的数据。

注意

Mock API 的优先级高于 BFF API。即,当 Mock API 和 BFF API 重名时,返回 Mock API 的数据。

补充信息

更多信息,请参考【调试代理和 Mock】。

使用 Model

中后台项目往往涉及较复杂的状态管理逻辑,此时可以使用专门的状态管理解决方案,Modern.js 已经集成了主流的状态管理解决方案。下面,我们将 TableList 组件中的状态管理逻辑移到单独的状态管理层,即 Model 层。

Model 相关 API 由 @modern-js/runtime/model 导出,其中,最常用的 API 是 model,用于创建 Model 对象。

我们新建 console/tableList/models/tableList.ts 文件,用于管理 TableList 组件中的状态:

console/tableList/models/tableList.ts
import { model } from '@modern-js/runtime/model';
import users from '@api/users'

type State = {
// ...
};

export default model<State>('tableList').define({
state: {
data: [],
},
actions: {
load: {
// effects 中的 load 函数 执行成功后,fulfilled 会被调用
fulfilled(state, payload) {
state.data = payload;
},
}
},
effects: {
// 获取用户列表数据的副作用,内部会调用 actions 中的 load 对象的不同方法
async load() {
const data = await users();
return data;
},
},
});

将这一个 Model 对象命名为 tableList,其中 state 对应组件中需要使用的状态,actionseffects 对应状态的读取和修改逻辑。

接下来,我们重构 console/tableList/index.tsx 的代码:通过 tableList.ts 创建的 Model 对象,获取组件所需要的状态。这里,主要用到 @modern-js/runtime/model 提供的 useModel API,关键代码如下:

console/tableList/index.tsx
import React, { useEffect } from 'react';
import { Table } from 'antd';
import { useModel } from '@modern-js/runtime/model';
import tableListModel from './models/tableList';

const TableList: React.FC = () => {
const [{ data }, { load }] = useModel(tableListModel);

useEffect(() => {
load();
}, []);

// ...
};

export default TableList;

补充信息

关于 Model 的详细介绍,请参考【添加业务模型】。

定制 Web Server

Modern.js 除了支持一体化 BFF等基本服务端能力,还支持通过定制 Web Server,实现更复杂的服务端需求,例如用户鉴权等功能。 关于这部分内容,请参考【定制 Web Server】。

微前端

当中后台项目越来越复杂后,我们还可以把项目拆分成微前端项目,详细内容请参考【开发微前端】。

测试

Modern.js 内置 JestTesting Library 等测试库/框架,提供单元测试、组件/页面集成测试、业务模型 Model 测试等功能。默认情况下,src/ 目录下文件名匹配规则 *.test.(t|j)sx? 的文件都会被识别为测试用例。

使用测试功能,需要先开启该功能。在项目根目录下,执行 pnpm run new,进行如下选择:

? 请选择你想要的操作: 启用可选功能
? 启用可选功能: 启用「单元测试 / 集成测试」功能

页面集成测试

新建 src/middle-platform/__tests__/App.test.tsx 文件,作为主入口页面的测试用例:

App.test.tsx
import { renderApp } from '@modern-js/runtime/testing';
import App from '../App';

describe('main entry', () => {
it('should have contents', () => {
const { getByText } = renderApp(<App />);
expect(getByText('This is a landing page.')).toBeInTheDocument();
});
});

renderApp@modern-js/runtime/testing 提供的用于测试页面的 API。执行 pnpm run test,会运行项目下的所有测试用例。

Model 测试

新建 src/console/tableList/models/tableList.test.ts 文件,作为主入口页面的测试用例,代码如下:

tableList.test.tsx
import { createStore } from '@modern-js/runtime/testing';
import tableListModel from './tableList';

jest.mock('@api/users', () => [
{ key: 1, name: 'modernjs', age: 12, country: 'China' },
]);

describe('test model', () => {
it('basic usage', async () => {
const store = createStore();

const [state, { load }] = store.use(tableListModel);

expect(state.data).toEqual([]);

await load();

expect(store.use(tableListModel)[0].data.length).toEqual(1);
});
});

通过 @modern-js/runtime/testing 中的 createStore,可以创建测试 Model 时所需的 store。执行 pnpm run test,会运行项目下的所有测试用例。

补充信息

更多用法,请参考【Testing API】、【测试 Model】。