utquidem
Version:
The meta-framework suite designed from scratch for frontend-focused modern web development.
578 lines (407 loc) • 18.5 kB
Markdown
---
sidebar_position: 3
---
# 开发中后台
本章将介绍如何使用 Modern.js,进行中后台项目的开发。本章对应的代码仓库地址在[这里查看](https://github.com/modern-js-dev/modern-js-examples/tree/main/quick-start/middle-platform)。
通过本章你可以了解到:
- 如何创建一个中后台项目。
- 如何为项目创建新入口。
- 如何使用客户端路由。
- 如何集成和使用开源组件库。
- 如何开发和使用 BFF API。
- 如何使用 Model 进行状态管理。
- 如何使用测试功能。
## 环境准备
import EnvPrepare from '@site/docs/components/env-prepare.md';
<EnvPrepare />
## 创建项目
使用 `@modern-js/create` 创建新项目,运行命令如下:
```bash
npx @modern-js/create middle-platform
```
:::info 注
`middle-platform` 为创建的项目名。
:::
按照如下选择,生成项目:
```bash
? 请选择你想创建的工程类型: 应用
? 请选择开发语言: TS
? 请选择包管理工具: pnpm
? 是否需要支持以下类型应用: 不需要
? 是否需要调整默认配置: 否
```
## 开发调试
进入项目根目录, 之后执行 `pnpm run dev` 即可启动开发服务器:
```bash
# 进入项目根目录
cd middle-platform
# 启动开发服务器
pnpm run dev
```
浏览器中访问 `http://localhost:8080`,可以看到应用已经正常启动。
修改 `src/App.tsx` 会触发重新编译和热更新,浏览器中页面会自动展示对应变化。
### Unbundled 开发模式
import DevUnbundle from '@site/docs/components/dev-unbundle.md'
<DevUnbundle/>
:::info 注
Unbundled 模式暂不支持在 Windows 平台使用,支持即将上线。
:::
### IDE 支持
import DevIDE from '@site/docs/components/dev-ide.md'
<DevIDE/>
## 创建入口
在 Modern.js 中,一个[入口](/docs/guides/tutorials/c07-app-entry/7.1-intro),经过构建后会生成一个对应的 HTML 文件。默认生成的项目只包含一个入口。现在,我们创建一个新入口,对应中后台应用的**控制台模块**,而原有入口对应中后台应用的落地页。
在项目根目录下,执行 `pnpm run new`,进行如下选择:
```bash
? 请选择你想要的操作: 创建工程元素
? 创建工程元素: 新建「应用入口」
? 填写入口名称:console
? 是否修改默认的应用入口配置:否
```
创建完成,项目的 `src` 目录下会有两个目录:
```md
.
├── src/
│ ├── console/
│ │ └── App.tsx
│ ├── middle-platform/
│ │ └── App.tsx
│ ├── .eslintrc.json
```
其中,`console/` 目录对应新建的入口,项目默认的入口(主入口)代码被移动到 `middle-platform/` 目录下。
:::info 注
使用生成器将应用从单入口转换成多入口时,原本主入口的代码将会被移动到与当前应用 package.json 同名的目录下。
:::
重新启动应用,控制台会输出不同入口对应的访问地址。默认情况下,主入口对应的访问地址为 **{域名根路径}**,其他入口对应的访问地址为 **{域名根路径}/{入口名称}**,如下所示:
```md
App running at:
> Local:
console http://localhost:8080/console
middle-platform http://localhost:8080/
```
:::info 补充信息
如果需要修改入口名和访问地址的映射关系,可以配置【[`server.routes`](/docs/apis/config/server/routes)】。
:::
我们对两个入口的代码做简单修改:
```js title="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;
```
```js title="console/App.tsx"
import React from 'react';
const App: React.FC = () => <div>Console</div>;
export default App;
```
现在,点击落地页上的链接,可以跳转到控制台入口对应的页面。
:::info 补充信息
关于入口的更多介绍,请参考 【[添加应用入口](/docs/guides/tutorials/c07-app-entry/7.1-intro)】。
:::
## 客户端路由
`console` 入口对应中后台应用的控制台模块,控制台模块一般会实现为一个复杂的 SPA 应用,所以需要使用客户端路由。默认生成的项目已经开启客户端路由功能,我们可以直接从 `@modern-js/runtime/router` 包引入路由相关组件。
`console/App.tsx` 的代码如下:
```js {2,10-11,13-20}
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>
<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.tsx` 中 `Dashboard` 和 `TableList` 两个组件,分别定义在 `console/dashboard` 和 `console/tableList` 两个文件夹下,代码如下:
```js title="console/dashboard/index.tsx"
import React from 'react';
const Dashboard: React.FC = () => <div>Dashboard Page</div>;
export default Dashboard;
```
```js title="console/tableList/index.tsx"
import React from 'react';
const TableList: React.FC = () => <div>TableList Page</div>;
export default TableList;
```
此时,点击页面上的两个链接,浏览器地址栏的 URL 发生变化,页面渲染的组件也随之更改,说明客户端路由可以正常工作。
:::info 补充信息
`console/App.tsx` 中客户端路由的使用方式在 Modern.js 中称为**自控式路由**,Modern.js 还支持**约定式路由**,关于路由的详细介绍,请参考【[添加客户端路由](/docs/guides/tutorials/c08-client-side-routing/8.1-code-based-routing)】。
:::
### 代码分片
当前代码在构建后,会把所有路由用到的组件都打包到一个 JS 文件中。打开浏览器开发者工具的 Network 窗口, `console.js` 对应所有路由组件打包后的 JS 文件,如下图所示:

我们可以使用 loadable,并根据路由划分,对代码进行分片。
Modern.js 对 loadable 提供了开箱即用的支持,可以直接从 '@modern-js/runtime/loadable' 导出函数,例如:
```js title="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;
```
此时,切换不同路由,会按需加载对应路由所需要的组件代码。如下图所示:

当访问 `/console` 路由时,会加载 `src_console_dashboard_index_tsx.js` 这个文件;当访问 `/console/table` 路由时,会加载 `src_console_tableList_index_tsx.js` 这个文件。
:::info 补充信息
关于 `loadable` 的更多用法,请参考【[loadable API](/docs/apis/runtime/utility/loadable/loadable_)】。
:::
## 集成组件库
中后台项目通常会集成第三方组件库,以提高组件开发效率。这里,我们以 [Ant Design](https://ant.design) 为例,介绍组件库的集成方式。
首先需要安装组件库依赖:
```bash
pnpm add antd
```
然后,在需要使用 Ant Design 的入口文件中引入组件库的样式,这里我们在 `/console/App.tsx` 中引入:
```js
import 'antd/dist/antd.css';
```
这样,我们就可以在任意组件中使用 Ant Design 的组件了。我们在 `TableList` 组件中使用 `Table` 组件:
```js
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](https://github.com/ant-design/babel-plugin-import) 插件,实现组件样式的按需加载。
Modern.js 已经内置了 babel-plugin-import 插件,但因为 Ant Design 使用 Less 编写组件样式,我们还需要开启 Less 支持。
在项目根目录下,执行 `pnpm run new`,进行如下选择:
```bash
? 请选择你想要的操作: 启用可选功能
? 启用可选功能: 启用 Less 支持
```
然后,我们删除 `/console/App.tsx` 中引入 Ant Design 组件库样式的代码。
重新访问 `http://localhost:8080/console/table`,可以看到页面上依然可以渲染出带有样式的 `Table` 组件。查看浏览器开发者工具的 Network 标签,会发现此时加载的 Ant Design 的 CSS 文件体积也变小了。
另外,开启 Less 支持后,我们也可以在单独的样式文件中使用 Less 语法。
:::info 补充信息
关于组件样式的更多用法,请参考【[CSS 开发方案](/docs/guides/usages/css/css-in-js)】。
:::
## 一体化 BFF
当前 Table 组件使用的数据是静态数据,我们现在希望能通过服务端 API 动态获取数据。服务端 API 地址为:<https://lf3-static.bytednsdoc.com/obj/eden-cn/beeh7uvzhq/users.json>。
但这个 API 并不是专门为当前项目提供的,部署也是在一个独立的域名下。
通常情况下,项目需要创建一个和当前项目部署在同一域名下的专属 API,并在这个 API 内部去调用原始数据获取,并进行裁剪聚合等。在前端,这样的需求通常使用 BFF 层来实现。
Modern.js 提供了开箱即用的 BFF 能力,支持和前端代码共同开发、调试、部署。
:::info 注
如果已经具备了为前端项目专门开发的、部署在同域下的 API,则不需要再创建 BFF 层,前端代码直接调用 API 即可。
:::
首先,需要开启 BFF 功能,在项目根目录下,执行 `pnpm run new`,进行如下选择:
import LaunchBFFChoices from '@site/docs/components/launch-bff-choices.md';
<LaunchBFFChoices />
执行完成后,项目中新增了 `api/` 目录,添加在 `api/users.ts` 文件,实现对获取数据 API 的调用(需要先安装 axios 依赖):
```js
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,关键代码如下:
```js {1,15}
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 的功能。
:::info 补充信息
更多信息,请参考【[一体化 BFF](/docs/guides/features/server-side/bff/function)】。
:::
### Mock 数据
在 Modern.js 中使用 Mock 功能,只需要在 `config/mock/index.t(j)s` 导出一个包含所有 Mock API 的对象,对象的属性由请求 Method 和 URL 组成,对应的值可以为 Object、Array、Function 类型的数据。
现在我们创建一个 `mockUser` API,代码如下:
```js title="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 的数据。
:::caution 注意
Mock API 的优先级高于 BFF API。即,当 Mock API 和 BFF API 重名时,返回 Mock API 的数据。
:::
:::info 补充信息
更多信息,请参考【[调试代理和 Mock](/docs/guides/usages/debug/proxy-and-mock)】。
:::
## 使用 Model
中后台项目往往涉及较复杂的状态管理逻辑,此时可以使用专门的状态管理解决方案,Modern.js 已经集成了主流的状态管理解决方案。下面,我们将 `TableList` 组件中的状态管理逻辑移到单独的状态管理层,即 Model 层。
Model 相关 API 由 `@modern-js/runtime/model` 导出,其中,最常用的 API 是 `model`,用于创建 Model 对象。
我们新建 `console/tableList/models/tableList.ts` 文件,用于管理 `TableList` 组件中的状态:
```js title="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` 对应组件中需要使用的状态,`actions` 和 `effects` 对应状态的读取和修改逻辑。
接下来,我们重构 `console/tableList/index.tsx` 的代码:通过 `tableList.ts` 创建的 Model 对象,获取组件所需要的状态。这里,主要用到 `@modern-js/runtime/model` 提供的 `useModel` API,关键代码如下:
```js title="console/tableList/index.tsx" {3-4,7,10}
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;
```
:::info 补充信息
关于 Model 的详细介绍,请参考【[添加业务模型](/docs/guides/tutorials/c10-model/10.1-application-architecture)】。
:::
## 定制 Web Server
Modern.js 除了支持**一体化 BFF**等基本服务端能力,还支持通过定制 Web Server,实现更复杂的服务端需求,例如用户鉴权等功能。 关于这部分内容,请参考【[定制 Web Server](/docs/guides/features/server-side/web/web-server)】。
## 微前端
当中后台项目越来越复杂后,我们还可以把项目拆分成微前端项目,详细内容请参考【[开发微前端](/docs/start/micro-frontend)】。
## 测试
Modern.js 内置 [Jest](https://jestjs.io/) 、[Testing Library](https://testing-library.com/) 等测试库/框架,提供单元测试、组件/页面集成测试、业务模型 Model 测试等功能。默认情况下,`src/` 目录下文件名匹配规则 `*.test.(t|j)sx?` 的文件都会被识别为测试用例。
使用测试功能,需要先开启该功能。在项目根目录下,执行 `pnpm run new`,进行如下选择:
```bash
? 请选择你想要的操作: 启用可选功能
? 启用可选功能: 启用「单元测试 / 集成测试」功能
```
### 页面集成测试
新建 `src/middle-platform/__tests__/App.test.tsx` 文件,作为主入口页面的测试用例:
```js title="App.test.tsx" {1,6,7}
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` 文件,作为主入口页面的测试用例,代码如下:
```js title="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`,会运行项目下的所有测试用例。
:::info 补充信息
更多用法,请参考【[Testing API](/docs/apis/runtime/testing/render)】、【[测试 Model](/docs/guides/features/runtime/model/test-model)】。
:::
## 部署
import Deploy from '@site/docs/components/deploy.md';
<Deploy/>