UNPKG

utquidem

Version:

The meta-framework suite designed from scratch for frontend-focused modern web development.

578 lines (407 loc) 18.5 kB
--- 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> &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.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 文件,如下图所示: ![code-split-1](https://lf3-static.bytednsdoc.com/obj/eden-cn/aphqeh7uhohpquloj/modern-js/start/code-split-1.png) 我们可以使用 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; ``` 此时,切换不同路由,会按需加载对应路由所需要的组件代码。如下图所示: ![code-split-2](https://lf3-static.bytednsdoc.com/obj/eden-cn/aphqeh7uhohpquloj/modern-js/start/code-split-2.png) 当访问 `/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 的对象,对象的属性由请求 MethodURL 组成,对应的值可以为 ObjectArrayFunction 类型的数据。 现在我们创建一个 `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/>