开发中后台
本章将介绍如何使用 Modern.js,进行中后台项目的开发。本章对应的代码仓库地址在这里查看。
通过本章你可以了解到:
- 如何创建一个中后台项目。
- 如何为项目创建新入口。
- 如何使用客户端路由。
- 如何集成和使用开源组件库。
- 如何开发和使用 BFF API。
- 如何使用 Model 进行状态管理。
- 如何使用测试功能。
环境准备
开始之前,请参考【环境准备】部分,确保本地环境正常。
创建项目
使用 @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
】。
我们对两个入口的代码做简单修改:
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;
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>
<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
两个文件夹下,代码如下:
import React from 'react';
const Dashboard: React.FC = () => <div>Dashboard Page</div>;
export default Dashboard;
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 文件,如下图所示:
我们可以使用 loadable,并根据路由划分,对代码进行分片。
Modern.js 对 loadable 提供了开箱即用的支持,可以直接从 '@modern-js/runtime/loadable' 导出函数,例如:
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
这个文件。
补充信息
关于 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,代码如下:
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
组件中的状态:
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,关键代码如下:
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 内置 Jest 、Testing Library 等测试库/框架,提供单元测试、组件/页面集成测试、业务模型 Model 测试等功能。默认情况下,src/
目录下文件名匹配规则 *.test.(t|j)sx?
的文件都会被识别为测试用例。
使用测试功能,需要先开启该功能。在项目根目录下,执行 pnpm run new
,进行如下选择:
? 请选择你想要的操作: 启用可选功能
? 启用可选功能: 启用「单元测试 / 集成测试」功能
页面集成测试
新建 src/middle-platform/__tests__/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
文件,作为主入口页面的测试用例,代码如下:
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】。