跳转到主文档

开发 UI 组件

本章将介绍如何使用 Modern.js 进行 UI 组件项目的开发。

本章对应的代码仓库地址:独立项目场景Monorepo 场景

在 Modern.js 中,UI 组件项目是指基于 React 开发组件类型可复用模块的项目。

通过本章你可以了解到:

  • 如何创建一个 UI 组件项目。
  • 如何在 UI 组件项目中进行测试。
  • 如何为 UI 组件项目开启 Storybook 功能并使用它进行调试。
  • 如何开发 UI 组件样式。
  • 如何在 UI 组件项目中使用运行时 API
  • 如何发布 UI 组件项目。
  • 在 Monorepo 中,UI 组件项目与应用项目如何联调。
在线预览

在 StackBlitz 中预览:

环境准备

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

创建项目

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

npx @modern-js/create components-project

components-project 为创建项目的目录名称。

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

? 请选择你想创建的工程类型 模块
? 请填写项目名称 components
? 请选择开发语言 TS
? 请选择包管理工具 pnpm
? 是否需要调整默认配置? 否

项目名称为 package.json 中的 "name" 字段值。

修改默认文件和代码

  1. src/index.ts 文件重命名为 src/index.tsx,以支持 JSX 语法。
  2. 将文件代码替换为:
export default function () {
return <div>This is a UI Component</div>;
}

修改默认测试文件和代码

  1. tests/index.test.ts 文件重命名为 tests/index.test.tsx,以支持 JSX 语法。
  2. 将测试代码替换为:
import { render, screen } from '@modern-js/runtime/testing';

import Component from '@/index';

describe('默认值 cases', () => {
test('Rendered', () => {
render(<Component />);
expect(screen.getByText('This is a UI Component')).toBeInTheDocument();
});
});

新增 styles/ 目录

在项目根目录创建 styles/ 目录,该目录用于存放独立样式文件。

到此为止,一个组件项目创建成功。

测试

项目创建成功之后,我们可以使用 Modern.js 提供的 test 命令对替换的组件代码进行测试,命令如下:

pnpm run test

component-test-result

IDE

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

添加组件代码

接下来添加真正的组件代码。这里以一个 TableList 组件举例。

准备工作

这里基于 Ant Design 组件库进行 UI 组件的开发。你也可以使用其他组件库,或不使用任何组件库。

  1. 安装 Ant Design 依赖:pnpm add antd

  2. 由于需要使用 Antd 的 Less 样式,需要先开启 Modern.js 对 Less 的支持:

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

新增 TableList 组件

修改 src/index.tsx 文件,增加以下代码:

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

export 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 className="table-list table-theme">
<Table columns={columns} dataSource={data} />
</div>
);
};

新增 TableList 组件样式

创建 src/tableList.less 文件,并增加以下内容:

.table-list {
border: 1px solid #ccc;
padding: 5px;
}

这里为表格的父容器增加了内边距以及边框

然后更新 src/tableList.tsx 文件。将 tableList.less 样式文件导入到 src/tableList.tsx 文件中:

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

import './tableList.less';

// ...

新增 styles/tableTheme.less 文件

创建 styles/tableTheme.less 样式文件,并增加以下内容:

.table-theme {
background: beige;
}

启用 Storybook 调试

在添加完组件代码后,可以使用 Modern.js 提供的 Storybook 功能,查看效果并进行调试。

在项目根目录下,执行 pnpm run new,可以开启 Storybook 功能:

? 请选择你想要的操作 启用可选功能
? 启用可选功能 启用「Storybook」

启用成功后,会自动创建 stories/ 目录。

然后更改默认的 Story 代码:

import { TableList } from '@/index';
import '@styles/tableTheme.less';

export const YourStory = () => <TableList />;

export default {
title: 'Your Stories',
};

执行 pnpm run dev 查看运行结果:

调试 storybook

开发组件样式

在开发 TableList 组件的过程中,通过三种方式为组件添加样式。接下来介绍这三种方式。

使用组件库样式

在开发 TableList 组件过程中,使用 Ant Design 组件库中的 Table 组件,基于 Table 组件提供基础的样式进行二次开发。像这类第三方 UI 组件库中包含一些可复用的样式,使用这类组件库提供的组件在开发中可以节省很多时间。

开发内置样式

在 Modern.js 中,位于 src 目录下的样式称为组件的内置样式。

关于更多内置样式的内容,请参考【什么是内置样式】章节。

在实现 TableList 组件的过程中,src/tableList.less 样式文件就是 TableList 组件的内置样式。在 Modern.js 中推荐内置样式在组件代码中引用,例如 tableList.tsx 文件中有如下一段代码:

import './tableList.less';

Modern.js 对于在组件代码中引用内置样式文件的情况,在构建组件代码的过程中会进行一些特殊的处理,更多内容请参考【内置样式与独立样式的构建产物】。

开发独立样式

在 Modern.js 中,位于 styles 目录下的样式称为组件的独立样式。

关于更多独立样式的内容,请参考【什么是独立样式】章节。

在实现 TableList 组件的过程中,styles/tableTheme.less 样式文件就是 TableList 组件的独立样式。独立样式的使用方式类似 Ant Design 组件库中组件样式的使用方式,分为:

  • 通过 babel-plugin-import 按需加载组件的独立样式文件产物。
  • 手动导入组件的独立样式文件产物。

关于 Ant Design 的按需加载,可以参考 Ant Design 官网【按需加载】章节内容。

转换为组件库

目前项目只包含一个组件。如果要创建组件库项目,只需要重新组织项目的目录结构,增加更多的组件即可。例如将当前 src 目录结构更改为:

.
├── src/
│ └── index.tsx
| └── tableList
| ├── tableList.less
| └── index.tsx
| └── avatar
| └── index.tsx
| ...

其中 src/tableList/index.tsx 包含了 TableList 组件的代码。

新建 src/avatar/index.tsx 文件,并添加组件代码:

import { Avatar } from 'antd';

export default ({ name }: {name: string}) => {
return <Avatar>{name}</Avatar>
}

修改 src/index.tsx 文件,将所有组件导出:

export { TableList } from './tableList';
export { default as Avatar } from './avatar'

此时组件项目就变成了组件库项目。

使用 Runtime API

Modern.js 通过 @modern-js/runtime 模块提供运行时 API。下面通过创建状态管理层,即 Model 层,演示如何在 UI 组件项目中使用 Modern.js Runtime API。接下来将新建一个 Model 对象,调用网络 API 动态获取数据,并提供给 TableList 组件使用。

首先,在 TableList 组件同一层级的目录下面创建 ./tableListModel.tsx 文件,定义 Model 对象,代码如下:

import { model } from '@modern-js/runtime/model';

type State = {
data: {
key: string;
name: string;
age: number;
country: string;
}[];
};

export default model<State>('tableList').define({
state: {
data: [],
},
actions: {
load: {
fulfilled(state, payload) {
return { data: payload };
},
}
},
effects: {
async load() {
const data = await (await fetch('https://lf3-static.bytednsdoc.com/obj/eden-cn/beeh7uvzhq/users.json')).json();
return data;
},
},
});

然后,在 TableList 组件文件中导入 tableListModel.tsx 文件,并使用导入的 tableListModel 对象:

import type React from 'react';
import { useEffect } from 'react';
import { Table } from 'antd';
import { useModel } from '@modern-js/runtime/model';
import tableListModel from './tableListModel';

import './tableList.less';

export 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}, {load}] = useModel(tableListModel);

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

return (
<div className='table-list table-theme'>
<Table columns={columns} dataSource={data} />
</div>
);
};
补充信息

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

接着新增 modern.config.js 文件并添加配置 runtime.state 开启支持 Model 功能:

modern.config.js
export default defineConfig({
runtime: {
state: true,
},
};

最后重新启动 Storybook 调试程序观察效果,可以看到 TableList 组件上展示的数据内容是从接口返回的动态数据:

component-with-model

发布

组件功能开发完成后,可对 UI 组件项目进行发布。

发布分以下四个步骤:

  1. 添加 changeset

    执行 pnpm run change,根据提示选择升级的版本,并填写变更信息。

填写变更信息

  1. 升级对应版本号,并生成 changelog

    执行 pnpm run bump,该命令会根据上述生成的 changeset 自动更新版本号和 CHANGELOG 信息,检查信息无误后提交。

  2. 发布

    执行 pnpm run release, 发布该 UI 组件包。

  3. 推送 tags

    发布完成之后执行 git push --follow-tags,推送当前发布对应生成的 git tag。

迁移到 Monorepo

在团队协作开发中,也会存在使用 Monorepo 进行项目开发的情况。接下来讲一下如何将组件项目在 monorepo 中的一些使用方式。

创建 Monorepo

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

npx @modern-js/create monorepo

monorepo 即为创建的 Monorepo 目录名称,又是项目的名称。

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

? 请选择你想创建的工程类型 Monorepo
? 请选择包管理工具 pnpm

生成的项目目录结构如下:

.
├── .changeset
│ └── config.json
├── .editorconfig
├── .gitignore
├── .npmrc
├── .nvmrc
├── .pnpmfile.cjs
├── .vscode
│ ├── extensions.json
│ └── settings.json
├── README.md
├── apps
│ └── .gitkeep
├── features
│ └── .gitkeep
├── monorepo.code-workspace
├── package.json
├── packages
│ └── .gitkeep
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── tsconfig.json

迁移组件项目

这里只需要将之前创建的组件项目复制到 packages 目录下面,并删除 .change/.vscode/.git/ 目录及 .editorconfig.gitignore.nvmrc.npmrc 文件。

.
├── packages/
│ └── components/
│ ├── README.md
│ ├── modern.config.js
│ ├── package.json
│ ├── src
│ │ ├── avatar
│ │ │ └── index.tsx
│ │ ├── index.ts
│ │ ├── modern-app-env.d.ts
│ │ └── tableList
│ │ ├── index.tsx
│ │ ├── tableListModel.tsx
│ │ └── tableList.less
│ ├── stories
│ │ ├── index.stories.tsx
│ │ └── tsconfig.json
│ ├── styles
│ │ └── tableTheme.less
│ ├── tests
│ │ ├── index.test.tsx
│ │ ├── modern-app-env.d.ts
│ │ └── tsconfig.json
│ └── tsconfig.json
│ ...

创建子项目

Modern.js 支持 Monorepo 工程方案的管理,可以在 Monorepo 项目下通过 new 命令创建不同类型的子项目。例如在刚刚创建的 Monorepo 项目根目录下执行:

pnpm run new

在刚刚创建的 Monorepo 项目根目录下执行 pnpm run new,然后分别选择创建 「应用」,「模块(内部)」项目:

? 请选择你想创建的工程类型 应用
? 请填写子项目名称 app
? 请填写子项目目录名称 app
? 请选择开发语言 TS
? 是否需要支持以下类型应用 不需要
? 是否需要调整默认配置? 否
? 请选择你想创建的工程类型 模块(内部)
? 请填写子项目名称 internal-lib
? 请填写子项目目录名称 internal-lib
? 请选择开发语言 TS
? 是否需要调整默认配置? 否

在应用项目中使用组件以及内部模块

接下来在 app 项目中通过以下方式把组件项目加到依赖中:

cd ./apps/app
pnpm add components

此时可以观察到 app 项目的 package.json 内容更新如下:

{
"dependencies": {
"@modern-js/runtime": "^1",
"components": "workspace:^0.1.0",
"react": "^17.0.1",
"react-dom": "^17.0.1"
}
}

接下来导入内部模块 internal-lib,由于内部模块并不需要进行发布,因此通过如下方式添加到项目中:

内部模块是指不需要发布到 npm 上的项目,它们只提供源码给应用项目使用,应用项目会将它们打包到构建产物中。

cd ./apps/app
pnpm add internal-lib -D

此时可以观察到 app 项目的 package.json 内容更新如下:

{
"devDependencies": {
"@modern-js/app-tools": "^1",
"@modern-js/plugin-jarvis": "^1",
"@types/jest": "^27",
"@types/node": "^14",
"@types/react": "^17",
"@types/react-dom": "^17",
"internal-lib": "workspace:^0.1.0",
"typescript": "^4"
},
}

此时在 app 项目下的 src/App.tsx 文件引用 componentsTableList 组件以及 internal-lib 模块,并使用它们:

import { Switch, Route } from '@modern-js/runtime/router';
import { TableList } from 'components';
import sayHelloWorld from 'internal-lib';

import './App.css';

const App = () => (
<Switch>
<Route exact={true} path="/">
<div className="container-box">
<main>
{/* //... */}
</main>
<TableList />
{sayHelloWorld()}
<footer className="footer">
<a href="#" target="_blank" rel="noopener noreferrer">
Powered by Modern.JS
</a>
</footer>
</div>
</Route>
<Route path="*">
<div>404</div>
</Route>
</Switch>
);

export default App;

然后我们在 apps/app 目录下执行 dev 命令,可以看到组件被正确渲染出来:

app-tools

批量发布

我们可以在 Monorepo 中再创建一个 UI 组件项目(以创建 Button 组件为例):

在 Monorepo 项目根目录下执行:

pnpm run new
? 请选择你想创建的工程类型 模块
? 请填写子项目名称 component2
? 请填写子项目目录名称 compomemt2
? 请选择开发语言 TS
? 是否需要调整默认配置? 否

删除 packages/component2/src/index.ts 文件,新建 packages/component2/src/index.tsx 文件,增加 Button 组件:

import Button from 'antd/es/button';

export default function () {
return <Button />;
}

开发完成后可以在 Monorepo 的场景下对所有 UI 组件库做批量发布:

添加 changeset

在 Monorepo 根目录执行 pnpm run change,根据提示选择发布的包(注意这里只选择 UI 组件包名)和升级的版本,并填写变更信息。

monorepo 添加 changeset

升级发布包对应版本号,并生成 changelog

执行 pnpm run bump,该命令会根据上述生成的 changeset 自动更新版本号和 CHANGELOG 信息,检查信息无误后提交。

发布

执行 pnpm run release, 发布对应的多个 UI 组件包。

推送 tags

发布完成之后执行 git push --follow-tags,推送当前发布对应生成的 Git Tag