diff --git a/.umirc.ts b/.umirc.ts index bfc9b219f8..2719858303 100644 --- a/.umirc.ts +++ b/.umirc.ts @@ -97,6 +97,11 @@ const navs = [ 'title.zh-CN': 'API 参考', path: '/api', }, + { + title: 'Schema components', + 'title.zh-CN': 'Schema 组件库', + path: '/components', + }, { title: 'GitHub', path: 'https://github.com/nocobase/nocobase', diff --git a/docs/en-US/development/directory-structure.md b/docs/en-US/development/app-ds.md similarity index 87% rename from docs/en-US/development/directory-structure.md rename to docs/en-US/development/app-ds.md index 770ab204cc..a725a8ef52 100644 --- a/docs/en-US/development/directory-structure.md +++ b/docs/en-US/development/app-ds.md @@ -1,6 +1,6 @@ # 项目目录结构 -无论是源码还是 `create-nocobase-app` 创建的应用,目录结构都是一样的,结构如下: +无论是 [Git 源码](/welcome/getting-started/installation/git-clone) 还是 [create-nocobase-app](/welcome/getting-started/installation/create-nocobase-app) 创建的 NocoBase 应用,目录结构都是一样的,结构如下: ```bash ├── my-nocobase-app diff --git a/docs/en-US/development/client/i18n.md b/docs/en-US/development/client/i18n.md new file mode 100644 index 0000000000..5ea350bc6c --- /dev/null +++ b/docs/en-US/development/client/i18n.md @@ -0,0 +1,140 @@ +# 国际化 + +客户端的国际化多语言基于 npm 包 [react-i18next](https://npmjs.com/package/react-i18next) 实现,在应用顶层提供了 `` 组件的包装,可以在任意位置直接使用相关的方法。 + +添加语言包: + +```tsx | pure +import { i18n } from '@nocobase/client'; + +i18n.addResources('zh-CN', 'test', { + Hello: '你好', + World: '世界', +}); +``` + +注:这里第二个参数填写的 `'test'` 是语言的命名空间,通常插件自己定义的语言资源都应该按自己插件包名创建特定的命名空间,以避免和其他语音资源冲突。NocoBase 中默认的命名空间是 `'client'`,大部分常用和基础的语言翻译都放置在此命名空间,当没有提供所需语言时,可在插件自身的命名空间内进行扩展定义。 + +在组件中调用翻译函数: + +```tsx | pure +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +export default function MyComponent() { + // 使用之前定义的命名空间 + const { t } = useTranslation('test'); + + return ( +
+

{t('World')}

+
+ ); +} +``` + +在 SchemaComponent 组件中可以直接使用模板方法 `'{{t()}}'`,模板中的翻译函数会自动被执行: + +```tsx | pure +import React from 'react'; +import { SchemaComponent } from '@nocobase/client'; + +export default function MySchemaComponent() { + return ( + + ); +} +``` + +在某些特殊情况下也需要以模板的方式定义多语言时,可以使用 NocoBase 内置的 `compile()` 方法编译为多语言结果: + +```tsx | pure +import React from 'react'; +import { useCompile } from '@nocobase/client'; + +const title = '{{t("Hello", { ns: "test" })}}'; + +export default function MyComponent() { + const { compile } = useCompile(); + + return ( +
{compile(title)}
+ ); +} +``` + +## 建议配置 + +以英文文案为 key,翻译为 value,这样的好处,即使多语言缺失,也会以英文显示,不会造成阅读障碍,如: + +```ts +i18n.addResources('zh-CN', 'my-plugin', { + 'Show dialog': '显示对话框', + 'Hide dialog': '隐藏对话框' +}); +``` + +为了更方便管理多语言文件,推荐在插件中创建一个 `locale` 文件夹,并把对应语言文件都放置在其中方便管理: + +```bash +|- /my-plugin + |- /src + |- /client + |- locale # 多语言文件夹 + |- zh-CN.ts + |- en-US.ts +``` + +## 示例 + +### 客户端组件多语言 + +例如订单状态的组件,根据不同值有不同的文本显示: + +```tsx | pure +import React from 'react'; +import { Select } from 'antd'; +import { i18n } from '@nocobase/client'; +import { useTranslation } from 'react-i18next'; + +i18n.addResources('zh-CN', 'sample-shop-i18n', { + Pending: '已下单', + Paid: '已支付', + Delivered: '已发货', + Received: '已签收' +}); + +const ORDER_STATUS_LIST = [ + { value: -1, label: 'Canceled (untranslated)' }, + { value: 0, label: 'Pending' }, + { value: 1, label: 'Paid' }, + { value: 2, label: 'Delivered' }, + { value: 3, label: 'Received' }, +] + +function OrderStatusSelect() { + const { t } = useTranslation('sample-shop-i18n'); + + return ( + + ); +} + +export default function () { + return ( + + ); +} +``` diff --git a/docs/en-US/development/client/index.md b/docs/en-US/development/client/index.md new file mode 100644 index 0000000000..eeffb54328 --- /dev/null +++ b/docs/en-US/development/client/index.md @@ -0,0 +1,73 @@ +# 概述 + +NocoBase 客户端的扩展大多以 Provider 的形式提供,无论是内置的 Provider 还是插件的主文件都是 Provider。 + +## 内置的 Providers + +- APIClientProvider +- I18nextProvider +- AntdConfigProvider +- RemoteRouteSwitchProvider +- SystemSettingsProvider +- PluginManagerProvider +- SchemaComponentProvider +- SchemaInitializerProvider +- BlockSchemaComponentProvider +- AntdSchemaComponentProvider +- DocumentTitleProvider +- ACLProvider + +## 客户端 Provider 模块的注册 + +静态的 Provider 通过 app.use() 注册,动态的 Provider 通过 dynamicImport 适配。 + +```tsx | pure +import React from 'react'; +import { Application } from '@nocobase/client'; + +const app = new Application({ + apiClient: { + baseURL: process.env.API_BASE_URL, + }, + dynamicImport: (name: string) => { + return import(`../plugins/${name}`); + }, +}); + +// 访问 /hello 页面时,显示 Hello world! +const HelloProvider = React.memo((props) => { + const location = useLocation(); + if (location.pathname === '/hello') { + return
Hello world!
+ } + return <>{props.children} +}); + +app.use(HelloProvider); +``` + +## 插件的客户端 + +初始化的空插件,服务端相关目录结构如下: + +```bash +|- /my-plugin + |- /src + |- /client + |- index.tsx + |- client.d.ts + |- client.js +``` + +`client/index.tsx` 内容如下: + +```tsx | pure +import React from 'react'; + +// 这是一个空的 Provider,只有 children 传递,并未提供自定义的 Context +export default React.memo((props) => { + return <>{props.children}; +}); +``` + +插件 pm.add 之后,会向 `packages/app/client/src/plugins` 目录写入 `my-plugin.ts` 文件 diff --git a/docs/en-US/development/guide/m.svg b/docs/en-US/development/client/m.svg similarity index 100% rename from docs/en-US/development/guide/m.svg rename to docs/en-US/development/client/m.svg diff --git a/docs/en-US/development/guide/settings-center.md b/docs/en-US/development/client/settings-center.md similarity index 90% rename from docs/en-US/development/guide/settings-center.md rename to docs/en-US/development/client/settings-center.md index 37286175c8..7639b793ec 100644 --- a/docs/en-US/development/guide/settings-center.md +++ b/docs/en-US/development/client/settings-center.md @@ -1,6 +1,6 @@ # 配置中心 - + ## 示例 diff --git a/docs/en-US/development/guide/settings-tab.jpg b/docs/en-US/development/client/settings-center/settings-tab.jpg similarity index 100% rename from docs/en-US/development/guide/settings-tab.jpg rename to docs/en-US/development/client/settings-center/settings-tab.jpg diff --git a/docs/en-US/development/client/test.md b/docs/en-US/development/client/test.md new file mode 100644 index 0000000000..452ea9f46d --- /dev/null +++ b/docs/en-US/development/client/test.md @@ -0,0 +1,41 @@ +# 测试 + +测试基于 [Jest](https://jestjs.io/) 测试框架。同时还包括了常用的 React 测试库,如 [@testing-library/react](https://testing-library.com/docs/react-testing-library/intro/) + +## 示例 + +```tsx | pure +import { render } from '@testing-library/react'; +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { RouteSwitch } from '../RouteSwitch'; +import { RouteSwitchProvider } from '../RouteSwitchProvider'; + +const Home = () =>

Home

; +const About = () =>

About

; + +describe('route-switch', () => { + it('case 1', () => { + const App = () => { + return ( + + + + + + ); + }; + const { container } = render(); + expect(container).toMatchSnapshot(); + }); +}); +``` \ No newline at end of file diff --git a/docs/en-US/development/guide/ui-router.md b/docs/en-US/development/client/ui-router.md similarity index 100% rename from docs/en-US/development/guide/ui-router.md rename to docs/en-US/development/client/ui-router.md diff --git a/docs/en-US/development/guide/ui-schema-designer/acl.md b/docs/en-US/development/client/ui-schema-designer/acl.md similarity index 100% rename from docs/en-US/development/guide/ui-schema-designer/acl.md rename to docs/en-US/development/client/ui-schema-designer/acl.md diff --git a/docs/en-US/development/guide/ui-schema-designer/block-provider.md b/docs/en-US/development/client/ui-schema-designer/block-provider.md similarity index 100% rename from docs/en-US/development/guide/ui-schema-designer/block-provider.md rename to docs/en-US/development/client/ui-schema-designer/block-provider.md diff --git a/docs/en-US/development/guide/ui-schema-designer/collection-manager.md b/docs/en-US/development/client/ui-schema-designer/collection-manager.md similarity index 100% rename from docs/en-US/development/guide/ui-schema-designer/collection-manager.md rename to docs/en-US/development/client/ui-schema-designer/collection-manager.md diff --git a/docs/en-US/development/guide/ui-schema-designer/component-library.md b/docs/en-US/development/client/ui-schema-designer/component-library.md similarity index 100% rename from docs/en-US/development/guide/ui-schema-designer/component-library.md rename to docs/en-US/development/client/ui-schema-designer/component-library.md diff --git a/docs/en-US/development/guide/ui-schema-designer/demo1.tsx b/docs/en-US/development/client/ui-schema-designer/demo1.tsx similarity index 100% rename from docs/en-US/development/guide/ui-schema-designer/demo1.tsx rename to docs/en-US/development/client/ui-schema-designer/demo1.tsx diff --git a/docs/en-US/development/guide/ui-schema-designer/designable.md b/docs/en-US/development/client/ui-schema-designer/designable.md similarity index 100% rename from docs/en-US/development/guide/ui-schema-designer/designable.md rename to docs/en-US/development/client/ui-schema-designer/designable.md diff --git a/docs/en-US/development/guide/ui-schema-designer/designable.png b/docs/en-US/development/client/ui-schema-designer/designable.png similarity index 100% rename from docs/en-US/development/guide/ui-schema-designer/designable.png rename to docs/en-US/development/client/ui-schema-designer/designable.png diff --git a/docs/en-US/development/guide/ui-schema-designer/extending-schema-components.md b/docs/en-US/development/client/ui-schema-designer/extending-schema-components.md similarity index 100% rename from docs/en-US/development/guide/ui-schema-designer/extending-schema-components.md rename to docs/en-US/development/client/ui-schema-designer/extending-schema-components.md diff --git a/docs/en-US/development/guide/ui-schema-designer/index.md b/docs/en-US/development/client/ui-schema-designer/index.md similarity index 100% rename from docs/en-US/development/guide/ui-schema-designer/index.md rename to docs/en-US/development/client/ui-schema-designer/index.md diff --git a/docs/en-US/development/guide/ui-schema-designer/insert-adjacent.md b/docs/en-US/development/client/ui-schema-designer/insert-adjacent.md similarity index 100% rename from docs/en-US/development/guide/ui-schema-designer/insert-adjacent.md rename to docs/en-US/development/client/ui-schema-designer/insert-adjacent.md diff --git a/docs/en-US/development/guide/ui-schema-designer/what-is-ui-schema.md b/docs/en-US/development/client/ui-schema-designer/what-is-ui-schema.md similarity index 100% rename from docs/en-US/development/guide/ui-schema-designer/what-is-ui-schema.md rename to docs/en-US/development/client/ui-schema-designer/what-is-ui-schema.md diff --git a/docs/en-US/development/guide/ui-schema-designer/x-designer.md b/docs/en-US/development/client/ui-schema-designer/x-designer.md similarity index 100% rename from docs/en-US/development/guide/ui-schema-designer/x-designer.md rename to docs/en-US/development/client/ui-schema-designer/x-designer.md diff --git a/docs/en-US/development/guide/ui-schema-designer/x-initializer.md b/docs/en-US/development/client/ui-schema-designer/x-initializer.md similarity index 100% rename from docs/en-US/development/guide/ui-schema-designer/x-initializer.md rename to docs/en-US/development/client/ui-schema-designer/x-initializer.md diff --git a/docs/en-US/development/guide/i18n.md b/docs/en-US/development/guide/i18n.md deleted file mode 100644 index 9c08448cd7..0000000000 --- a/docs/en-US/development/guide/i18n.md +++ /dev/null @@ -1,266 +0,0 @@ -# 国际化 - -## 基础概念 - -多语言国际化支持根据服务端和客户端分为两大部分,各自有相应的实现。 - -语言配置均为普通的 JSON 对象键值对,如果没有翻译文件(或省略编写)的话会直接输出键名的字符串。 - -### 服务端 - -服务端的多语言国际化基于 npm 包 [i18next](https://npmjs.com/package/i18next) 实现,在服务端应用初始化时会创建一个 i18next 实例,同时也会将此实例注入到请求上下文(`context`)中,以供在各处方便的使用。 - -在创建服务端 Application 实例时可以传入配置对应的初始化参数: - -```ts -import { Application } from '@nocobase/server'; - -const app = new Application({ - i18n: { - defaultNS: 'test', - resources: { - 'en-US': { - test: { - hello: 'Hello', - }, - }, - 'zh-CN': { - test: { - hello: '你好', - } - } - } - } -}); -``` - -或者在插件中对已存在的 app 实例添加语言数据至对应命名空间: - -```ts -app.i18n.addResources('zh-CN', 'test', { - Hello: '你好', - World: '世界', -}); - -app.i18n.addResources('en-US', 'test', { - Hello: 'Hello', - World: 'World', -}); -``` - -基于应用: - -```ts -app.i18n.t('World') // “世界”或“World” -``` - -基于请求: - -```ts -app.resource({ - name: 'test', - actions: { - async get(ctx, next) { - ctx.body = `${ctx.t('Hello')} ${ctx.t('World')}`; - await next(); - } - } -}); -``` - -通常服务端的多语言处理主要用于错误信息的输出。 - -### 客户端 - -客户端的多语言国际化基于 npm 包 [react-i18next](https://npmjs.com/package/react-i18next) 实现,在应用顶层提供了 `` 组件的包装,可以在任意位置直接使用相关的方法。 - -添加语言包: - -```tsx | pure -import { i18n } from '@nocobase/client'; - -i18n.addResources('zh-CN', 'test', { - Hello: '你好', - World: '世界', -}); -``` - -注:这里第二个参数填写的 `'test'` 是语言的命名空间,通常插件自己定义的语言资源都应该按自己插件包名创建特定的命名空间,以避免和其他语音资源冲突。NocoBase 中默认的命名空间是 `'client'`,大部分常用和基础的语言翻译都放置在此命名空间,当没有提供所需语言时,可在插件自身的命名空间内进行扩展定义。 - -在组件中调用翻译函数: - -```tsx | pure -import React from 'react'; -import { useTranslation } from 'react-i18next'; - -export default function MyComponent() { - // 使用之前定义的命名空间 - const { t } = useTranslation('test'); - - return ( -
-

{t('World')}

-
- ); -} -``` - -在 SchemaComponent 组件中可以直接使用模板方法 `'{{t()}}'`,模板中的翻译函数会自动被执行: - -```tsx | pure -import React from 'react'; -import { SchemaComponent } from '@nocobase/client'; - -export default function MySchemaComponent() { - return ( - - ); -} -``` - -在某些特殊情况下也需要以模板的方式定义多语言时,可以使用 NocoBase 内置的 `compile()` 方法编译为多语言结果: - -```tsx | pure -import React from 'react'; -import { useCompile } from '@nocobase/client'; - -const title = '{{t("Hello", { ns: "test" })}}'; - -export default function MyComponent() { - const { compile } = useCompile(); - - return ( -
{compile(title)}
- ); -} -``` - -### 建议配置 - -当添加语言资源时,推荐在 JSON 配置模板中将语言串的键名设置为默认语言,更方便统一处理,且可以省去默认语言的翻译。例如以英语为默认语言: - -```ts -i18n.addResources('zh-CN', 'your-namespace', { - 'Show dialog': '显示对话框', - 'Hide dialog': '隐藏对话框' -}); -``` - -语言内容如果比较多,推荐在插件中创建一个 `locals` 目录,并把对应语言文件都放置在其中方便管理: - -``` -- server -| - locals -| | - zh-CN.ts -| | - en-US.ts -| | - ... -| - index.ts -``` - -## 示例 - -### 服务端错误提示 - -例如用户在店铺对某个商品下单时,如果商品的库存不够,或者未上架,那么下单接口被调用时,应该返回相应的错误。 - -```ts -const namespace = 'shop'; - -export default class ShopPlugin extends Plugin { - async load() { - this.app.i18n.addResources('zh-CN', namespace, { - 'No such product': '商品不存在', - 'Product not on sale': '商品已下架', - 'Out of stock': '库存不足', - }); - - this.app.resource({ - name: 'orders', - actions: { - async create(ctx, next) { - const productRepo = ctx.db.getRepository('products'); - const product = await productRepo.findOne({ - filterByTk: ctx.action.params.values.productId - }); - - if (!product) { - return ctx.throw(404, ctx.t('No such product')); - } - - if (!product.enabled) { - return ctx.throw(400, ctx.t('Product not on sale')); - } - - if (!product.inventory) { - return ctx.throw(400, ctx.t('Out of stock')); - } - - const orderRepo = ctx.db.getRepository('orders'); - ctx.body = await orderRepo.create({ - values: { - productId: product.id, - quantity: 1, - totalPrice: product.price, - userId: ctx.state.currentUser.id - } - }); - - next(); - } - } - }); - } -} -``` - -### 客户端组件多语言 - -例如订单状态的组件,根据不同值有不同的文本显示: - -```tsx -import React from 'react'; -import { Select } from 'antd'; -import { i18n } from '@nocobase/client'; -import { useTranslation } from 'react-i18next'; - -i18n.addResources('zh-CN', '@nocobase/plugin-sample-shop-i18n', { - Pending: '已下单', - Paid: '已支付', - Delivered: '已发货', - Received: '已签收' -}); - -const ORDER_STATUS_LIST = [ - { value: -1, label: 'Canceled (untranslated)' }, - { value: 0, label: 'Pending' }, - { value: 1, label: 'Paid' }, - { value: 2, label: 'Delivered' }, - { value: 3, label: 'Received' }, -] - -function OrderStatusSelect() { - const { t } = useTranslation('@nocobase/plugin-sample-shop-i18n'); - - return ( - - ); -} - -export default function () { - return ( - - ); -} -``` diff --git a/docs/en-US/development/guide/index.md b/docs/en-US/development/guide/index.md deleted file mode 100644 index fa1ec74e4a..0000000000 --- a/docs/en-US/development/guide/index.md +++ /dev/null @@ -1,71 +0,0 @@ -# 概述 - -## Web Server - -- Collection & Field -- Resource & Action -- Middleware -- Events & Hooks - -**Samples** - -- samples/shop-modeling -- samples/shop-actions -- samples/ratelimit -- samples/model-hooks - -## UI Schema Designer - -平台最核心的功能,用于可视化配置页面。 - -- 扩展各种供 UI 设计器使用的 Schema 组件 -- 各种 x-designer 和 x-initializer - -**Samples** - -- samples/custom-block -- samples/custom-action -- samples/custom-field -- samples/custom-x-designer -- samples/custom-x-initializer - -## UI Router - -用于自定义页面,包括: - -- routes config -- route components - -**Samples** - -- samples/custom-page - -## Settings Center - -由插件提供的系统级配置的能力 - -- plugin settings tabs - -**Samples** - -- samples/custom-settings-center-page - -## I18n - -国际化 - -- I18n - -**Samples** - -- samples/shop-i18n - -## Devtools - -- Commands -- Migrations - -**Samples** - -- samples/custom-command -- samples/custom-migration diff --git a/docs/en-US/development/guide/migration.md b/docs/en-US/development/guide/migration.md deleted file mode 100644 index ad54e6fd26..0000000000 --- a/docs/en-US/development/guide/migration.md +++ /dev/null @@ -1,219 +0,0 @@ -# 数据库迁移 - -应用在业务发展或版本升级过程中,某些情况会需要修改数据库表或字段等信息,为保证安全无冲突且可回溯的解决数据库变更,通常的做法是使用数据库迁移的方式完成。 - -## 介绍 - -Nocobase 基于 npm 包 [Umzug](https://www.npmjs.com/package/umzug) 处理数据库迁移。并将相关功能集成在命令行的子命令 `nocobase migrator` 中,大部分操作通过该命令处理。 - -### 仅增加表或字段无需迁移脚本 - -通常如果只是增加数据表或增加字段,可以不使用数据库迁移脚本,而是直接修改数据表定义(`collection`)的内容即可。例如文章表定义(`collections/posts.ts`)初始状态: - -```ts -export default { - name: 'posts', - fields: [ - { - name: 'title', - type: 'string', - } - ] -} -``` - -当需要增加一个分类字段时,直接修改原来的表结构定义文件内容: - -```ts -export default { - name: 'posts', - fields: [ - { - name: 'title', - type: 'string', - }, - { - name: 'category', - type: 'belongsTo' - } - ] -} -``` - -当新版本代码在环境中调用升级命令时,新的字段会以 Sequelize 中 sync 的逻辑自动同步到数据库中,完成表结构变更。 - -### 创建迁移文件 - -如果表结构的变更涉及到字段类型变更、索引调整等,需要人工创建迁移脚本文件: - -```bash -yarn nocobase migrator create --name change-some-field.ts --folder path/to/migrations -``` - -该命令会在 `path/to/migrations` 目录中创建一个基于时间戳的迁移脚本文件 `YYYY.MM.DDTHH.mm.ss.change-some-field.ts`,内容如下: - -```ts -import { Migration } from '@nocobase/server'; - -export default class MyMigration extends Migration { - async up() { - // TODO - } - - async down() { - // TODO - } -} -``` - -脚本导出的主类相关 API 可以参考 [`Migration` 类](/api/server/migration)。 - -### 数据库操作内容 - -`up()`、`down()` 是一对互逆操作,升级时会调用 `up()`,降级时会调用 `down()`。大部分情况我们主要考虑升级操作。 - -在升级中我们有几种方式进行数据库变更: - -```ts -import { Migration } from '@nocobase/server'; - -export default class MyMigration extends Migration { - async up() { - // 1. 针对自行管理的静态数据表,调用 Sequelize 提供的 queryInterface 实例上的方法 - await this.queryInterface.changeColumn('posts', 'title', { - type: DataTypes.STRING, - unique: true // 添加索引 - }); - - // 2. 针对被 collection-manager 插件管理的动态数据表,调用插件提供的 collections / fields 表的数据仓库方法 - await this.db.getRepository('fields').update({ - values: { - collectionName: 'posts', - name: 'title', - type: 'string', - unique: true // 添加索引 - } - }); - } -} -``` - -### 数据变更 - -除了表结构变更,也可以在迁移过程中导入需要的数据,或对数据进行调整: - -```ts -import { Migration } from '@nocobase/server'; - -export default class MyMigration extends Migration { - async up() { - await this.sequelize.transaction(async transaction => { - const defaultCategory = await this.db.getRepository('categories').create({ - values: { - title: '默认分类' - }, - transaction - }); - - await this.db.getRepository('posts').update({ - filter: { - categoryId: null - }, - values: { - categoryId: defaultCategory.id - }, - transaction - }); - }); - } -} -``` - -在一个脚本中有多项数据库操作时,建议使用事务保证所有操作都成功才算迁移完成。 - -### 执行升级 - -迁移脚本准备好以后,在项目目录下执行对应的升级命令即可完成数据库变更: - -```bash -yarn nocobase upgrade -``` - -根据数据库迁移的机制,迁移脚本执行成功后也会被记录在数据库的升级记录表中,只有第一次执行有效,之后的多次重复执行都会被忽略。 - -## 示例 - -### 修改主键字段类型 - -假设订单表一开始使用数字类型,但后期希望改成可以包含字母的字符串类型,我们可以在迁移文件中填写: - -```ts -import { Migration } from '@nocobase/server'; - -export default class MyMigration extends Migration { - async up() { - await this.sequelize.transaction(async transaction => { - await this.queryInterface.changeColumn('orders', 'id', { - type: DataTypes.STRING - }, { - transaction - }); - }); - } -} -``` - -注:修改字段类型只有在未加入新类型数据之前可以进行逆向降级操作,否则需要自行备份数据并对数据进行特定处理。 - -另外,直接修改数据表主键 `id` 的类型在某些数据库中会提示错误(SQLite 正常,PostgreSQL 失败)。这时候需要把相关操作分步执行: - -1. 创建一个新的字符串类型字段 `id_new;` -2. 复制原有表 `id` 的数据到新的字段; -3. 移除原有 `id` 主键约束; -4. 将原有 `id` 列改名为不使用的列名 `id_old`; -5. 将新的 `id_new` 列改名为 `id`; -6. 对新的 `id` 列增加主键约束; - -```ts -import { Migration } from '@nocobase/server'; - -export default class MyMigration extends Migration { - async up() { - await this.sequelize.transaction(async transaction => { - await this.queryInterface.addColumn('orders', 'id_new', { - type: DataTypes.STRING - }, { transaction }); - - const PendingOrderModel = this.sequelize.define('orders', { - id_new: DataTypes.STRING - }); - - await PendingOrderModel.update({ - id_new: col('id') - }, { - where: { - id: { [Op.not]: null } - }, - transaction - }); - - await this.queryInterface.removeConstraint('orders', 'orders_pkey', { transaction }); - - await this.queryInterface.renameColumn('orders', 'id', 'id_old', { transaction }); - - await this.queryInterface.renameColumn('orders', 'id_new', 'id', { transaction }); - - await this.queryInterface.addConstraint('orders', { - type: 'PRIMARY KEY', - name: 'orders_pkey', - fields: ['id'], - transaction - }); - }); - } -} -``` - -通常修改列类型在已存在数据量较大的表里操作时也建议用新列代替旧列的方式,性能会更好。其他更多细节可以参考 [Sequelize 的 `queryInterface` API](https://sequelize.org/api/v6/class/src/dialects/abstract/query-interface.js),以及各个数据库引擎的细节。 - -注:在执行升级命令后,应用启动之前请确保变更的表结构能够对应上 collections 中定义的内容,以免不一致导致错误。 diff --git a/docs/en-US/development/index.md b/docs/en-US/development/index.md index ec9544ba74..77377521af 100644 --- a/docs/en-US/development/index.md +++ b/docs/en-US/development/index.md @@ -1,16 +1,24 @@ # 介绍 -NocoBase 采用微内核架构,各类功能以插件形式扩展,所以微内核架构也叫插件化架构,由内核和插件两部分组成。内核提供了最小功能的 WEB 服务器,还提供了各种插件化接口;插件是按功能划分的各种独立模块,通过接口适配,具有可插拔的特点。插件化的设计降低了模块之间的耦合度,提高了复用率。随着插件库的不断扩充,常见的场景只需要组合插件即可完成基础搭建。例如 NocoBase 的无代码平台,就是由各种插件组合起来。 +NocoBase 采用微内核架构,各类功能以插件形式扩展,前后端分离,提供了各种插件化接口,插件按功能模块划分,具有可插拔的特点。 - + + +插件化的设计降低了模块之间的耦合度,提高了复用率。随着插件库的不断扩充,常见的场景只需要组合插件即可完成基础搭建。例如 NocoBase 的无代码平台,就是由各种插件组合起来。 + + ## 插件管理器 NocoBase 提供了强大的插件管理器用于管理插件,插件管理器的流程如下: - + -开发可以通过 CLI 的方式管理插件: +无代码用户可以通过界面管理本地插件的激活和禁用: + + + +开发也可以通过 CLI 的方式管理完整的插件流程: ```bash # 创建插件 @@ -25,10 +33,6 @@ yarn pm disable hello yarn pm remove hello ``` -无代码用户也可以通过插件管理器界面激活、禁用、删除已添加的本地插件: - - - 更多插件示例,查看 [packages/samples](https://github.com/nocobase/nocobase/tree/main/packages/samples)。 ## 扩展能力 @@ -39,4 +43,20 @@ yarn pm remove hello - 也可以是用于增强或限制 HTTP API 的过滤器、校验器、访问限制等 - 也可以是更底层的数据表、迁移、事件、命令行等功能的增强 -不仅如此,更多扩展介绍请查看 [扩展指南 - 概述](/development/guide) 章节。 \ No newline at end of file + +各模块分布: + +- Server + - Collections & Fields:主要用于系统表配置,业务表建议在「配置中心 - 数据表配置」里配置 + - Resources & Actions:主要用于扩展 Action API + - Middleware:中间件 + - Events:事件 + - I18n:服务端国际化 + - Commands:自定义命令行 + - Migrations:迁移脚本 +- Client + - UI Schema Designer:页面设计器 + - UI Router:有自定义页面需求时 + - Settings Center:为插件提供配置页面 + - I18n:客户端国际化 + diff --git a/docs/en-US/development/index/app-flow.svg b/docs/en-US/development/index/app-flow.svg new file mode 100644 index 0000000000..e167cda39b --- /dev/null +++ b/docs/en-US/development/index/app-flow.svg @@ -0,0 +1 @@ +
beforeLoad
loop: plugin.beforeLoad()
load/reload
loop: plugin.load()
afterLoad
Reinstall?
beforeInstall
install
afterInstall
beforeUpgrade
upgrade
afterUpgrade
Restart?
beforeStart
start
afterStart
beforeStop
stop
afterStop
beforeDestroy
destroy
afterDestroy
\ No newline at end of file diff --git a/docs/en-US/development/guide/pm-built-in.jpg b/docs/en-US/development/index/pm-built-in.jpg similarity index 100% rename from docs/en-US/development/guide/pm-built-in.jpg rename to docs/en-US/development/index/pm-built-in.jpg diff --git a/docs/en-US/development/index/pm-flow.svg b/docs/en-US/development/index/pm-flow.svg new file mode 100644 index 0000000000..23a738cfcd --- /dev/null +++ b/docs/en-US/development/index/pm-flow.svg @@ -0,0 +1 @@ +
Local
pm.create
Marketplace
pm.publish
NPM registry
Extracting client files
pm.add
plugin.afterAdd()
app/client plugins
pm.enable
plugin.install()
plugin.afterEnable()
pm.disable
plugin.afterDisable()
pm.remove
plugin.remove()
pm.upgrade
\ No newline at end of file diff --git a/docs/en-US/development/pm-ui.jpg b/docs/en-US/development/index/pm-ui.jpg similarity index 100% rename from docs/en-US/development/pm-ui.jpg rename to docs/en-US/development/index/pm-ui.jpg diff --git a/docs/en-US/development/life-cycle.md b/docs/en-US/development/life-cycle.md new file mode 100644 index 0000000000..d6daab561b --- /dev/null +++ b/docs/en-US/development/life-cycle.md @@ -0,0 +1,41 @@ +# 生命周期 + +## 应用的生命周期 + + + +## 插件的生命周期 + + + +## 插件的生命周期方法 + +```ts +import { InstallOptions, Plugin } from '@nocobase/server'; + +export class MyPlugin extends Plugin { + afterAdd() { + // 插件 pm.add 注册进来之后,主要用于放置 app.beforeLoad 的事件。 + } + beforeLoad() { + // 所有插件执行 load 之前,一般用于注册类和事件监听 + } + async load() { + // 加载配置 + } + async install(options?: InstallOptions) { + // 安装逻辑 + } + async afterEnable() { + // 激活之后 + } + async afterDisable() { + // 禁用之后 + } + async remove() { + // 删除逻辑 + } +} + +export default MyPlugin; +``` diff --git a/docs/en-US/development/nocobase-cli.md b/docs/en-US/development/nocobase-cli.md deleted file mode 100644 index bb1c0402a1..0000000000 --- a/docs/en-US/development/nocobase-cli.md +++ /dev/null @@ -1,310 +0,0 @@ ---- -order: 2 ---- - -# NocoBase CLI - -NocoBase CLI 旨在帮助你开发、构建和部署 NocoBase 应用。 - - - -NocoBase CLI 支持 ts-node 和 node 两种运行模式 - -- ts-node 模式(默认):用于开发环境,支持实时编译,但是响应较慢 -- node 模式:用于生产环境,响应迅速,但需要先执行 `yarn nocobase build` 将全部源码进行编译 - - - -## 使用说明 - -```bash -$ yarn nocobase -h - -Usage: nocobase [command] [options] - -Options: - -h, --help - -Commands: - create-plugin 创建插件脚手架 - console - db:auth 校验数据库是否连接成功 - db:sync 通过 collections 配置生成相关数据表和字段 - install 安装 - start 生产环境启动应用 - build 编译打包 - clean 删除编译之后的文件 - dev 启动应用,用于开发环境,支持实时编译 - doc 文档开发 - test 测试 - umi - upgrade 升级 - help -``` - -## 在脚手架里应用 - -应用脚手架 `package.json` 里的 `scripts` 如下: - -```json -{ - "scripts": { - "dev": "nocobase dev", - "start": "nocobase start", - "clean": "nocobase clean", - "build": "nocobase build", - "test": "nocobase test", - "postinstall": "nocobase umi generate tmp" - } -} -``` - -## 命令行扩展 - -NocoBase CLI 基于 [commander](https://github.com/tj/commander.js) 构建,你可以自由扩展命令,扩展的 command 可以写在 `app/server/index.ts` 里: - -```ts -const app = new Application(config); - -app.command('hello').action(() => {}); -``` - -或者,写在插件里: - -```ts -class MyPlugin extends Plugin { - beforeLoad() { - this.app.command('hello').action(() => {}); - } -} -``` - -终端运行 - -```bash -$ yarn nocobase hello -``` - -## 内置命令行 - -按使用频率排序 - -### `dev` - -开发环境下,启动应用,代码实时编译。 - - -NocoBase 未安装时,会自动安装(参考 install 命令) - - -```bash -Usage: nocobase dev [options] - -Options: - -p, --port [port] - --client - --server - -h, --help -``` - -示例 - -```bash -# 启动应用,用于开发环境,实时编译 -yarn nocobase dev -# 只启动服务端 -yarn nocobase dev --server -# 只启动客户端 -yarn nocobase dev --client -``` - -### `start` - -生产环境下,启动应用,代码需要 yarn build。 - - - -- NocoBase 未安装时,会自动安装(参考 install 命令) -- 源码有修改时,需要重新打包(参考 build 命令) - - - -```bash -$ yarn nocobase start -h - -Usage: nocobase start [options] - -Options: - -p, --port - -s, --silent - -h, --help -``` - -示例 - -```bash -# 启动应用,用于生产环境, -yarn nocobase start -``` - -### `install` - -安装 - -```bash -$ yarn nocobase install -h - -Usage: nocobase install [options] - -Options: - -f, --force - -c, --clean - -s, --silent - -l, --lang [lang] - -e, --root-email - -p, --root-password - -n, --root-nickname [rootNickname] - -h, --help -``` - -示例 - -```bash -# 初始安装 -yarn nocobase install -l zh-CN -e admin@nocobase.com -p admin123 -# 删除 NocoBase 的所有数据表,并重新安装 -yarn nocobase install -f -l zh-CN -e admin@nocobase.com -p admin123 -# 清空数据库,并重新安装 -yarn nocobase install -c -l zh-CN -e admin@nocobase.com -p admin123 -``` - - - -`-f/--force` 和 `-c/--clean` 的区别 -- `-f/--force` 删除 NocoBase 的数据表 -- `-c/--clean` 清空数据库,所有数据表都会被删除 - - - -### `upgrade` - -升级 - -```bash -yarn nocobase upgrade -``` - -### `test` - -jest 测试,支持所有 [jest-cli](https://jestjs.io/docs/cli) 的 options,除此之外还扩展了 `-c, --db-clean` 的支持。 - -```bash -$ yarn nocobase test -h - -Usage: nocobase test [options] - -Options: - -c, --db-clean 运行所有测试前清空数据库 - -h, --help -``` - -示例 - -```bash -# 运行所有测试文件 -yarn nocobase test -# 运行指定文件夹下所有测试文件 -yarn nocobase test packages/core/server -# 运行指定文件里的所有测试 -yarn nocobase test packages/core/database/src/__tests__/database.test.ts - -# 运行测试前,清空数据库 -yarn nocobase test -c -yarn nocobase test packages/core/server -c -``` - -### `build` - -代码部署到生产环境前,需要将源码编译打包,如果代码有修改,也需要重新构建。 - -```bash -# 所有包 -yarn nocobase build -# 指定包 -yarn nocobase build app/server app/client -``` - -### `clean` - -删除编译之后的文件 - -```bash -yarn clean -# 等同于 -yarn rimraf -rf packages/*/*/{lib,esm,es,dist} -``` - -### `doc` - -文档开发 - -```bash -# 启动文档 -yarn doc --lang=zh-CN # 等同于 yarn doc dev -# 构建文档,默认输出到 ./docs/dist/ 目录下 -yarn doc build -# 查看 dist 输出的文档最终效果 -yarn doc serve --lang=zh-CN -``` - -### `db:auth` - -校验数据库是否连接成功 - -```bash -$ yarn nocobase db:auth -h - -Usage: nocobase db:auth [options] - -Options: - -r, --retry [retry] 重试次数 - -h, --help -``` - -### `db:sync` - -通过 collections 配置生成数据表和字段 - -```bash -$ yarn nocobase db:sync -h - -Usage: nocobase db:sync [options] - -Options: - -f, --force - -h, --help display help for command -``` - -### `umi` - -`app/client` 基于 [umi](https://umijs.org/) 构建,可以通过 `nocobase umi` 来执行其他相关命令。 - -```bash -# 生成开发环境所需的 .umi 缓存 -yarn nocobase umi generate tmp -``` - -### `help` - -帮助命令,也可以用 option 参数,`-h` 和 `--help` - -```bash -# 查看所有 cli -yarn nocobase help -# 也可以用 -h -yarn nocobase -h -# 或者 --help -yarn nocobase --help -# 查看 db:sync 命令的 option -yarn nocobase db:sync -h -``` diff --git a/docs/en-US/development/plugin-ds.md b/docs/en-US/development/plugin-ds.md new file mode 100644 index 0000000000..b1dc993b52 --- /dev/null +++ b/docs/en-US/development/plugin-ds.md @@ -0,0 +1,17 @@ +# 插件目录结构 + +可以通过 `yarn pm create my-plugin` 快速创建一个空插件,目录结构如下: + +```bash +|- /my-plugin + |- /src + |- /client # 插件客户端代码 + |- /server # 插件服务端代码 + |- client.d.ts + |- client.js + |- package.json # 插件包信息 + |- server.d.ts + |- server.js +``` + +`/src/server` 的教程参考 [服务端](./server) 章节,`/src/client` 的教程参考 [客户端](./client) 章节。 diff --git a/docs/zh-CN/development/guide/collections-fields.md b/docs/en-US/development/server/collections-fields.md similarity index 100% rename from docs/zh-CN/development/guide/collections-fields.md rename to docs/en-US/development/server/collections-fields.md diff --git a/docs/zh-CN/development/guide/commands.md b/docs/en-US/development/server/commands.md similarity index 86% rename from docs/zh-CN/development/guide/commands.md rename to docs/en-US/development/server/commands.md index 5334d3b8db..3edfcab05e 100644 --- a/docs/zh-CN/development/guide/commands.md +++ b/docs/en-US/development/server/commands.md @@ -1,7 +1,5 @@ # 命令行 -## 简介 - NocoBase Server Application 除了用作 WEB 服务器以外,也是个强大可扩展的 CLI 工具。 新建一个 `app.js` 文件,代码如下: @@ -24,20 +22,24 @@ node app.js start # 启动 为了更好的开发、构建和部署 NocoBase 应用,NocoBase 内置了许多命令,详情查看 [NocoBase CLI](/api/cli) 章节。 -## 自定义 Command +## 如何自定义 Command? NocoBase CLI 的设计思想与 [Laravel Artisan](https://laravel.com/docs/9.x/artisan) 非常相似,都是可扩展的。NocoBase CLI 基于 [commander](https://www.npmjs.com/package/commander) 实现,可以这样扩展 Command: ```ts -app - .command('echo') - .option('-v, --version'); - .action(async ([options]) => { - console.log('Hello World!'); - if (options.version) { - console.log('Current version:', app.getVersion()); - } - }); +export class MyPlugin extends Plugin { + load() { + this.app + .command('echo') + .option('-v, --version'); + .action(async ([options]) => { + console.log('Hello World!'); + if (options.version) { + console.log('Current version:', app.getVersion()); + } + }); + } +} ``` 这个方法定义了以下命令: @@ -47,7 +49,7 @@ yarn nocobase echo # Hello World! yarn nocobase echo -v # Hello World! -# Current version: 0.7.4-alpha.7 +# Current version: 0.8.0-alpha.1 ``` 更多 API 细节可参考 [Application.command()](/api/server/application#command) 部分。 diff --git a/docs/zh-CN/development/guide/events.md b/docs/en-US/development/server/events.md similarity index 60% rename from docs/zh-CN/development/guide/events.md rename to docs/en-US/development/server/events.md index 5c20293043..2b16a96b3d 100644 --- a/docs/zh-CN/development/guide/events.md +++ b/docs/en-US/development/server/events.md @@ -1,66 +1,58 @@ # 事件 -事件在很多插件化可扩展的框架和系统中都有应用,比如著名的 Wordpress,是比较广泛的对生命周期支持扩展的机制。 +NocoBase 在应用、插件、数据库的生命周期中提供了非常多的事件监听,这些方法只有在触发了事件之后才会执行。 -## 基础概念 +## 如何添加事件监听? -NocoBase 在应用生命周期中提供了一些钩子,以便在运行中的一些特殊时期根据需要进行扩展开发。 - -### 数据库事件 - -主要通过 `db.on()` 的方法定义,大部分事件兼容 Sequelize 原生的事件类型。例如需要在某张数据表创建一条数据后做一些事情时,可以使用 `.afterCreate` 事件: +事件的注册一般放于 afterAdd 或 beforeLoad 中 ```ts -// posts 表创建数据完成时触发 -db.on('posts.afterCreate', async (post, options) => { - console.log(post); -}); +export class MyPlugin extends Plugin { + // 插件添加进来之后,有没有激活都执行 afterAdd() + afterAdd() { + this.app.on(); + this.db.on(); + } + + // 只有插件激活之后才会执行 beforeLoad() + beforeLoad() { + this.app.on(); + this.db.on(); + } +} ``` -由于 Sequelize 默认的单条数据创建成功触发的时间点上并未完成与该条数据所有关联数据的处理,所以 NocoBase 针对默认封装的 Repository 数据仓库类完成数据创建和更新操作时,扩展了几个额外的事件,代表关联数据被一并操作完成: +### `db.on` -```ts -// 已创建且已根据创建数据完成关联数据创建或更新完成时触发 -db.on('posts.afterCreateWithAssociations', async (post, options) => { - console.log(post); -}); -``` +数据库相关事件与 Collection 配置、Repository 的 CRUD 相关,包括: -与 Sequelize 同样的也可以针对全局的数据处理都定义特定的事件: +- 'beforeSync' / 'afterSync' +- 'beforeValidate' / 'afterValidate' +- 'beforeCreate' / 'afterCreate' +- 'beforeUpdate' / 'afterUpdate' +- 'beforeSave' / 'afterSave' +- 'beforeDestroy' / 'afterDestroy' +- 'afterCreateWithAssociations' +- 'afterUpdateWithAssociations' +- 'afterSaveWithAssociations' +- 'beforeDefineCollection' +- 'afterDefineCollection' +- 'beforeRemoveCollection' / 'afterRemoveCollection -```ts -// 每张表创建数据完成都触发 -db.on('beforeCreate', async (model, options) => { - console.log(model); -}); -``` +更多详情参考 [Database API](/api/database#内置事件)。 -针对特殊的生命周期比如定义数据表等,NocoBase 也扩展了相应事件: +### `app.on()` -```ts -// 定义任意数据表之前触发 -db.on('beforeDefineCollection', (collection) => { - collection.options.tableName = 'somePrefix' + collection.options.tableName; -}); -``` +app 的事件与应用的生命周期相关,相关事件有: -其他所有可用的数据库事件类型可以参考 [Database API](/api/database#on)。 +- 'beforeLoad' / 'afterLoad' +- 'beforeInstall' / 'afterInstall' +- 'beforeUpgrade' / 'afterUpgrade' +- 'beforeStart' / 'afterStart' +- 'beforeStop' / 'afterStop' +- 'beforeDestroy' / 'afterDestroy' -### 应用级事件 - -在某些特殊需求时,会需要在应用的外层生命周期中定义事件进行扩展,比如当应用启动前做一些准备操作,当应用停止前做一些清理操作等: - -```ts -app.on('beforeStart', async () => { - console.log('app is starting...'); -}); - -app.on('beforeStop', async () => { - console.log('app is stopping...'); -}); -``` - -其他所有可用的应用级事件类型可以参考 [Application API](/api/server/application#事件)。 +更多详情参考 [Application API](/api/server/application#事件)。 ## 示例 @@ -72,7 +64,7 @@ app.on('beforeStop', async () => { ```ts class ShopPlugin extends Plugin { - load() { + beforeLoad() { this.db.on('orders.afterCreate', async (order, options) => { const product = await order.getProduct({ transaction: options.transaction diff --git a/docs/en-US/development/server/i18n.md b/docs/en-US/development/server/i18n.md new file mode 100644 index 0000000000..80fa6398fd --- /dev/null +++ b/docs/en-US/development/server/i18n.md @@ -0,0 +1,134 @@ +# 国际化 + +NocoBase 国际化基于 [i18next](https://npmjs.com/package/i18next) 实现。 + +## 如何注册多语言包? + +```ts +export class MyPlugin extends Plugin { + load() { + this.app.i18n.addResources('zh-CN', 'test', { + Hello: '你好', + World: '世界', + }); + this.app.i18n.addResources('en-US', 'test', { + Hello: 'Hello', + World: 'World', + }); + } +} +``` + +## 两个 i18n 实例 + +### app.i18n + +全局 i18n 实例,一般用于 CLI 中国。 + +```ts +app.i18n.t('World') // “世界”或“World” +``` + +### ctx.i18n + +全局 i18n 的 cloneInstance,每个请求的 context 完全独立,通常用于根据客户端语言响应多语言信息。 + +```ts +app.use(async (ctx, next) => { + ctx.body = `${ctx.i18n.t('Hello')} ${ctx.i18n.t('World')}`; + await next(); +}); +``` + +客户端请求参数可以放在 query string 里 + +```bash +GET /?locale=en-US HTTP/1.1 +Host: localhost:13000 +``` + +或放在 request headers 里 + +```bash +GET / HTTP/1.1 +Host: localhost:13000 +X-Locale: en-US +``` + +## 建议配置 + +以英文文案为 key,翻译为 value,这样的好处,即使多语言缺失,也会以英文显示,不会造成阅读障碍,如: + +```ts +i18n.addResources('zh-CN', 'your-namespace', { + 'Show dialog': '显示对话框', + 'Hide dialog': '隐藏对话框' +}); +``` + +为了更方便管理多语言文件,推荐在插件中创建一个 `locale` 文件夹,并把对应语言文件都放置在其中方便管理: + +```bash +|- /my-plugin + |- /src + |- /server + |- locale # 多语言文件夹 + |- zh-CN.ts + |- en-US.ts +``` + +## 示例 + +### 服务端错误提示 + +例如用户在店铺对某个商品下单时,如果商品的库存不够,或者未上架,那么下单接口被调用时,应该返回相应的错误。 + +```ts +const namespace = 'shop'; + +export default class ShopPlugin extends Plugin { + async load() { + this.app.i18n.addResources('zh-CN', namespace, { + 'No such product': '商品不存在', + 'Product not on sale': '商品已下架', + 'Out of stock': '库存不足', + }); + + this.app.resource({ + name: 'orders', + actions: { + async create(ctx, next) { + const productRepo = ctx.db.getRepository('products'); + const product = await productRepo.findOne({ + filterByTk: ctx.action.params.values.productId + }); + + if (!product) { + return ctx.throw(404, ctx.t('No such product')); + } + + if (!product.enabled) { + return ctx.throw(400, ctx.t('Product not on sale')); + } + + if (!product.inventory) { + return ctx.throw(400, ctx.t('Out of stock')); + } + + const orderRepo = ctx.db.getRepository('orders'); + ctx.body = await orderRepo.create({ + values: { + productId: product.id, + quantity: 1, + totalPrice: product.price, + userId: ctx.state.currentUser.id + } + }); + + next(); + } + } + }); + } +} +``` diff --git a/docs/en-US/development/server/index.md b/docs/en-US/development/server/index.md new file mode 100644 index 0000000000..988b108554 --- /dev/null +++ b/docs/en-US/development/server/index.md @@ -0,0 +1,75 @@ +# 概述 + +初始化的空插件,服务端相关目录结构如下: + +```bash +|- /my-plugin + |- /src + |- /server # 插件服务端代码 + |- plugin.ts # 插件类 + |- index.ts # 服务端入口 + |- server.d.ts + |- server.js +``` + +`plugin.ts` 提供了插件生命周期的各种方法的调用 + +```ts +import { InstallOptions, Plugin } from '@nocobase/server'; + +export class MyPlugin extends Plugin { + afterAdd() { + // 插件 pm.add 注册进来之后,主要用于放置 app beforeLoad 事件的监听 + } + beforeLoad() { + // 自定义类或方法 + this.db.registerFields(); + this.db.registerFields(); + this.db.registerFields(); + this.db.registerFields(); + this.db.registerFieldTypes() + this.db.registerModels() + this.db.registerRepositories() + this.db.registerOperators() + // 事件监听 + this.app.on(); + this.db.on(); + } + async load() { + // 定义 collection + this.db.collection(); + // 导入 collection + this.db.import(); + this.db.addMigrations(); + + // 定义 resource + this.resourcer.define(); + // resource action + this.resourcer.registerActions(); + + // 注册 middleware + this.resourcer.use(); + this.acl.use(); + this.app.use(); + + // 自定义多语言包 + this.app.i18n.addResources(); + // 自定义命令行 + this.app.command(); + } + async install(options?: InstallOptions) { + // 安装逻辑 + } + async afterEnable() { + // 激活之后 + } + async afterDisable() { + // 禁用之后 + } + async remove() { + // 删除逻辑 + } +} + +export default MyPlugin; +``` diff --git a/docs/en-US/development/guide/middleware.md b/docs/en-US/development/server/middleware.md similarity index 92% rename from docs/en-US/development/guide/middleware.md rename to docs/en-US/development/server/middleware.md index fbb4ecc9f2..560c2cbef1 100644 --- a/docs/en-US/development/guide/middleware.md +++ b/docs/en-US/development/server/middleware.md @@ -1,6 +1,20 @@ # 中间件 -## 添加方法 +## 如何注册中间件? + +中间件的注册方法一般写在 load 方法里 + +```ts +export class MyPlugin extends Plugin { + load() { + this.app.acl.use(); + this.app.resourcer.use(); + this.app.use(); + } +} +``` + +说明: 1. `app.acl.use()` 添加资源权限级中间件,在权限判断之前执行 2. `app.resourcer.use()` 添加资源级中间件,只有请求已定义的 resource 时才执行 @@ -141,8 +155,6 @@ app.resourcer.use(async (ctx, next) => { 待补充 -## 完整示例 - -待补充 +## 示例 - [samples/ratelimit](https://github.com/nocobase/nocobase/blob/main/packages/samples/ratelimit/) IP rate-limiting diff --git a/docs/en-US/development/server/migration.md b/docs/en-US/development/server/migration.md new file mode 100644 index 0000000000..1ec0a81397 --- /dev/null +++ b/docs/en-US/development/server/migration.md @@ -0,0 +1,50 @@ +# 数据库迁移 + +插件在更新迭代过程中,可能会出现某些不兼容的改动,这些不兼容的升级脚本可以通过编写 migration 文件来处理。 + +## 如何添加迁移文件? + +```ts +export class MyPlugin extends Plugin { + load() { + // 加载单个 Migration 文件 + this.db.addMigration(); + // 加载多个 Migration 文件 + this.db.addMigrations(); + } +} +``` + +API 参考: + +- [db.addMigration()](/api/database#addmigration) +- [db.addMigrations()](/api/database#addmigrations) + +## 什么时候执行? + +```bash +# app 升级时,会执行 migrator.up() 和 db.sync() +yarn nocobase upgrade +# 单独触发 migration +yarn nocobase migrator up +``` + +## 什么时候需要写 migration 文件? + +一般用于升级过程中,存于数据库的系统配置的更新。如果只是 collection 配置的变更,无需配置 migration,直接执行 `yarn nocobase db:sync` 就可以同步给数据库了。 + +## Migration 文件 + +```ts +import { Migration } from '@nocobase/server'; + +export default class CustomMigration extends Migration { + async up() { + // + } + + async down() { + // + } +} +``` diff --git a/docs/en-US/development/guide/resources-actions.md b/docs/en-US/development/server/resources-actions.md similarity index 100% rename from docs/en-US/development/guide/resources-actions.md rename to docs/en-US/development/server/resources-actions.md diff --git a/docs/en-US/development/server/test.md b/docs/en-US/development/server/test.md new file mode 100644 index 0000000000..aedff9d325 --- /dev/null +++ b/docs/en-US/development/server/test.md @@ -0,0 +1,162 @@ +# 测试 + +测试基于 [Jest](https://jestjs.io/) 测试框架。为了方便的编写测试,提供了 `mockDatabase()` 和 `mockServer()` 用于数据库和服务端应用的测试。 + +## `mockDatabase()` + +默认提供一种完全隔离的 db 测试环境 + +```ts +import { mockDatabase } from '@nocobase/test'; + +describe('my db suite', () => { + let db; + + beforeEach(async () => { + db = mockDatabase(); + db.collection({ + name: 'posts', + fields: [ + { + type: 'string', + name: 'title', + } + ] + }); + await db.sync(); + }); + + afterEach(async () => { + await db.close(); + }); + + test('my case', async () => { + const repository = db.getRepository('posts'); + const post = await repository.create({ + values: { + title: 'hello' + } + }); + + expect(post.get('title')).toEqual('hello'); + }); +}); +``` + +## `mockServer()` + +提供模拟的服务端应用实例,对应的 app.db 为 `mockDatabase()` 实例,同时还提供了便捷的 `app.agent()` 用于测试 HTTP API,针对 NocoBase 的 Resource Action 还封装了 `app.agent().resource()` 用于测试资源的 Action。 + +```ts +import { mockServer } from '@nocobase/test'; + +class MyPlugin extends Plugin { + +} + +describe('my suite', () => { + let app; + let agent; + + beforeEach(async () => { + app = mockServer(); + agent = app.agent(); + // 添加待注册的插件 + app.plugin(MyPlugin, { name: 'my-plugin' }); + // 加载配置 + app.load(); + // 清空数据库并安装 + app.install({ clean: true }); + }); + + afterEach(async () => { + await app.destroy(); + }); + + test('my case', async () => { + await agent.resource('posts').create({ + values: { + title: 'hello' + } + }); + await agent.get('/users:check').set({ Authorization: 'Bearer abc' }); + }); +}); +``` + +## 示例 + +我们以之前在 [资源与操作](development/guide/resources-actions) 章节的功能为例,来写一个插件的测试: + +```ts +import { mockServer } from '@nocobase/test'; +import Plugin from '../../src/server'; + +describe('shop actions', () => { + let app; + let agent; + let db; + + beforeEach(async () => { + app = mockServer(); + app.plugin(Plugin); + agent = app.agent(); + db = app.db; + + await app.load(); + await db.sync(); + }); + + afterEach(async () => { + await app.destroy(); + }); + + test('product order case', async () => { + const { body: product } = await agent.resource('products').create({ + values: { + title: 'iPhone 14 Pro', + price: 7999, + enabled: true, + inventory: 1 + } + }); + expect(product.data.price).toEqual(7999); + + const { body: order } = await agent.resource('orders').create({ + values: { + productId: product.data.id + } + }); + expect(order.data.totalPrice).toEqual(7999); + expect(order.data.status).toEqual(0); + + const { body: deliveredOrder } = await agent.resource('orders').deliver({ + filterByTk: order.data.id, + values: { + provider: 'SF', + trackingNumber: '123456789' + } + }); + expect(deliveredOrder.data.status).toBe(2); + expect(deliveredOrder.data.delivery.trackingNumber).toBe('123456789'); + }); +}); +``` + +编写完成后,在命令行中允许测试命令: + +```bash +yarn test packages/samples/shop-actions +``` + +该测试将验证: + +1. 商品可以创建成功; +2. 订单可以创建成功; +3. 订单可以发货成功; + +当然这只是个最基本的例子,从业务上来说并不完善,但作为示例已经可以说明整个测试的流程。 + +## 小结 + +本章涉及的示例代码整合在对应的包 [packages/samples/shop-actions](https://github.com/nocobase/nocobase/tree/main/packages/samples/shop-actions) 中,可以直接在本地运行,查看效果。 diff --git a/docs/en-US/manual/quick-start/the-first-app.md b/docs/en-US/manual/quick-start/the-first-app.md index 6226bf1234..89e358da46 100755 --- a/docs/en-US/manual/quick-start/the-first-app.md +++ b/docs/en-US/manual/quick-start/the-first-app.md @@ -52,18 +52,7 @@ In this case, for relational fields, if you are not familiar with the concepts o In the graphical interface, you can visualize the relationship between the various collections. (Note: Graph-collection plugin is not yet open source) - -🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎 - -🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎 - -🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎 - -🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎 - -🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎 - -🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎 +![graph-collection.jpg](./the-first-app/graph-collection.jpg) Once the data collections and fields are created, we start making the interface. diff --git a/docs/en-US/manual/quick-start/the-first-app/graph-collection.jpg b/docs/en-US/manual/quick-start/the-first-app/graph-collection.jpg new file mode 100644 index 0000000000..d2488b29f7 Binary files /dev/null and b/docs/en-US/manual/quick-start/the-first-app/graph-collection.jpg differ diff --git a/docs/en-US/welcome/community/v08-changelog.md b/docs/en-US/welcome/community/v08-changelog.md new file mode 100644 index 0000000000..3e1cf544a9 --- /dev/null +++ b/docs/en-US/welcome/community/v08-changelog.md @@ -0,0 +1,216 @@ +# v0.8 的一些变化 + +## 界面右上角的调整 + +- 界面配置 +- 插件管理器 +- 配置中心 +- 个人中心 + + + +## 全新的插件管理器 + +v0.8 提供了强大的插件管理器用于无代码的方式管理插件。 + +### 插件管理器流程 + + + +### 插件管理器界面 + +目前主要用于本地插件的禁用、激活和删除。内置插件不能删除,插件市场敬请期待。 + + + +### 插件管理器命令行 + +除了可以在后台界面激活、禁用插件,开发也可以通过命令行更完整的管理插件。 + +```bash +# 创建插件 +yarn pm create hello +# 注册插件 +yarn pm add hello +# 激活插件 +yarn pm enable hello +# 禁用插件 +yarn pm disable hello +# 删除插件 +yarn pm remove hello +``` + +备注:插件的发布和升级会在陆续的版本里更新并支持 + +```bash +# 发布插件 +yarn pm publish hello +# 升级插件 +yarn pm upgrade hello +``` + +更多插件示例,查看 [packages/samples](https://github.com/nocobase/nocobase/tree/main/packages/samples)。 + +## 插件的变化 + +### 插件目录结构 + +```bash +|- /hello + |- /src + |- /client # 插件客户端代码 + |- /server # 插件服务端代码 + |- client.d.ts + |- client.js + |- package.json # 插件包信息 + |- server.d.ts + |- server.js +``` + +### 插件名规范 + +NocoBase 插件也是 NPM 包,插件名和 NPM 包名的对应规则为 `${PLUGIN_PACKAGE_PREFIX}-${pluginName}`。 + +`PLUGIN_PACKAGE_PREFIX` 为插件包前缀,可以在 .env 里自定义,[点此查看 PLUGIN_PACKAGE_PREFIX 说明](/api/env#plugin_package_prefix)。 + +例如,有一名为 `my-nocobase-app` 的项目,新增了 `hello` 插件,包名为 `@my-nocobase-app/plugin-hello`。 + +PLUGIN_PACKAGE_PREFIX 配置如下: + +```bash +PLUGIN_PACKAGE_PREFIX=@nocobase/plugin-,@nocobase/preset-,@my-nocobase-app/plugin- +``` + +插件名和包名的对应关系为: + +- `users` 插件包名为 `@nocobase/plugin-users` +- `nocobase` 插件包名为 `@nocobase/preset-nocobase` +- `hello` 插件包名为 `@my-nocobase-app/plugin-hello` + +### 插件的生命周期 + +提供了更完整的插件生命周期方法 + +```ts +import { InstallOptions, Plugin } from '@nocobase/server'; + +export class HelloPlugin extends Plugin { + afterAdd() { + // 插件通过 pm.add 添加之后 + } + + beforeLoad() { + // 所有插件执行 load 之前,一般用于注册类和事件监听 + } + + async load() { + // 加载配置 + } + + async install(options?: InstallOptions) { + // 安装逻辑 + } + + async afterEnable() { + // 激活之后 + } + + async afterDisable() { + // 禁用之后 + } + + async remove() { + // 删除逻辑 + } +} + +export default HelloPlugin; +``` + +### 插件的前后端入口 + +插件的生命周期由服务端控制 + +```ts +import { Application } from '@nocobase/server'; + +const app = new Application({ + // ... +}); + +class MyPlugin extends Plugin { + afterAdd() {} + beforeLoad() {} + load() {} + install() {} + afterEnable() {} + afterDisable() {} + remove() {} +} + +app.plugin(MyPlugin, { name: 'my-plugin' }); +``` + +插件的客户端以 Context.Provider 形式存在(类似于服务端的 Middleware) + +```tsx | pure +import React from 'react'; +import { Application } from '@nocobase/client'; + +const app = new Application({ + apiClient: { + baseURL: process.env.API_BASE_URL, + }, + dynamicImport: (name: string) => { + return import(`../plugins/${name}`); + }, +}); + +// 访问 /hello 页面时,显示 Hello world! +const HelloProvider = React.memo((props) => { + const location = useLocation(); + if (location.pathname === '/hello') { + return
Hello world!
+ } + return <>{props.children} +}); + +app.use(HelloProvider); +``` + +## 自定义的业务代码 + +v0.7 的插件并不完整,自定义的业务代码可能分散在 `packages/app/client` 和 `packages/app/server` 里,不利于升级、维护。v0.8 推荐以插件包的形式整理,并使用 `yarn pm` 来管理插件。 + +## 提供了更完整的文档 + +- **欢迎**:快速了解 NocoBase +- **用户使用手册**:进一步了解 NocoBase 平台提供的核心功能 +- **插件开发教程**:进阶深入插件开发 +- **API 参考**:插件开发过程中,查阅各 API 用法 +- **客户端组件库**(正在准备中):提供 NocoBase 各组件的示例和用法 + +备注:文档还有很多细节待补充,也会根据大家进一步反馈,继续调整。 + +## 提供了更多插件示例 + +- [command](https://github.com/nocobase/nocobase/tree/develop/packages/samples/command "command") +- [custom-block](https://github.com/nocobase/nocobase/tree/develop/packages/samples/custom-block "custom-block") +- [custom-page](https://github.com/nocobase/nocobase/tree/develop/packages/samples/custom-page "custom-page") +- [custom-signup-page](https://github.com/nocobase/nocobase/tree/develop/packages/samples/custom-signup-page "custom-signup-page") +- [hello](https://github.com/nocobase/nocobase/tree/develop/packages/samples/hello "hello") +- [ratelimit](https://github.com/nocobase/nocobase/tree/develop/packages/samples/ratelimit "ratelimit") +- [shop-actions](https://github.com/nocobase/nocobase/tree/develop/packages/samples/shop-actions "shop-actions") +- [shop-events](https://github.com/nocobase/nocobase/tree/develop/packages/samples/shop-events "shop-events") +- [shop-i18n](https://github.com/nocobase/nocobase/tree/develop/packages/samples/shop-i18n "shop-i18n") +- [shop-modeling](https://github.com/nocobase/nocobase/tree/develop/packages/samples/shop-modeling "shop-modeling") + +## 其他新特性和功能 + +### 导入 + +### 批量更新 & 编辑 + +### 图形化数据表配置 + +### 工作流支持查看执行历史 diff --git a/docs/en-US/development/pm-flow.svg b/docs/en-US/welcome/community/v08-changelog/pm-flow.svg similarity index 100% rename from docs/en-US/development/pm-flow.svg rename to docs/en-US/welcome/community/v08-changelog/pm-flow.svg diff --git a/docs/zh-CN/development/pm-ui.jpg b/docs/en-US/welcome/community/v08-changelog/pm-ui.jpg similarity index 100% rename from docs/zh-CN/development/pm-ui.jpg rename to docs/en-US/welcome/community/v08-changelog/pm-ui.jpg diff --git a/docs/en-US/welcome/community/v08-changelog/topright.jpg b/docs/en-US/welcome/community/v08-changelog/topright.jpg new file mode 100644 index 0000000000..8ce5109730 Binary files /dev/null and b/docs/en-US/welcome/community/v08-changelog/topright.jpg differ diff --git a/docs/menus.ts b/docs/menus.ts index 060311f3ff..45f90cbd9e 100644 --- a/docs/menus.ts +++ b/docs/menus.ts @@ -40,6 +40,15 @@ export default { }, ], }, + { + title: 'Release', + 'title.zh-CN': '版本发布', + type: 'group', + children: [ + '/welcome/release/index', + '/welcome/release/v08-changelog', + ], + }, { title: 'Community', 'title.zh-CN': '社区', @@ -48,6 +57,7 @@ export default { '/welcome/community/contributing', // '/welcome/community/faq', '/welcome/community/translations', + '/welcome/community/thanks', ], }, @@ -94,140 +104,68 @@ export default { children: [ '/development/index', '/development/your-fisrt-plugin', - '/development/learning-guide', - ], - }, - { - title: '约束规范', - 'title.zh-CN': '约束规范', - type: 'group', - children: [ '/development/app-ds', '/development/plugin-ds', + '/development/life-cycle', + // '/development/learning-guide', ], }, { - title: 'Extension Guides', - 'title.zh-CN': '扩展指南', + title: 'Server', + 'title.zh-CN': '服务端', type: 'group', children: [ - '/development/guide/index', - '/development/guide/collections-fields', - '/development/guide/resources-actions', - '/development/guide/middleware', - '/development/guide/commands', - '/development/guide/events', - '/development/guide/i18n', - '/development/guide/migration', + '/development/server/index', + '/development/server/collections-fields', + '/development/server/resources-actions', + '/development/server/middleware', + '/development/server/commands', + '/development/server/events', + '/development/server/i18n', + '/development/server/migration', + '/development/server/test', + ], + }, + { + title: 'Client', + 'title.zh-CN': '客户端', + type: 'group', + children: [ + '/development/client/index', { title: 'UI 设计器', type: 'subMenu', children: [ - // '/development/guide/ui-schema-designer/index', - '/development/guide/ui-schema-designer/what-is-ui-schema', - '/development/guide/ui-schema-designer/extending-schema-components', - // '/development/guide/ui-schema-designer/insert-adjacent', - '/development/guide/ui-schema-designer/designable', - '/development/guide/ui-schema-designer/component-library', - // '/development/guide/ui-schema-designer/collection-manager', - // '/development/guide/ui-schema-designer/acl', - '/development/guide/ui-schema-designer/x-designer', - '/development/guide/ui-schema-designer/x-initializer', + // '/development/client/ui-schema-designer/index', + '/development/client/ui-schema-designer/what-is-ui-schema', + '/development/client/ui-schema-designer/extending-schema-components', + // '/development/client/ui-schema-designer/insert-adjacent', + '/development/client/ui-schema-designer/designable', + '/development/client/ui-schema-designer/component-library', + // '/development/client/ui-schema-designer/collection-manager', + // '/development/client/ui-schema-designer/acl', + '/development/client/ui-schema-designer/x-designer', + '/development/client/ui-schema-designer/x-initializer', ], }, - '/development/guide/ui-router', - '/development/guide/settings-center', - ], - }, - { - title: 'HTTP API', - type: 'group', - children: ['/development/http-api/index', '/development/http-api/rest-api'], - }, - { - title: 'Others', - 'title.zh-CN': '其他', - type: 'group', - children: [ - '/development/others/testing', - // '/development/pre-release/build', + '/development/client/ui-router', + '/development/client/settings-center', + '/development/client/i18n', + '/development/client/test', ], }, ], - // { - // title: 'Development', - // 'title.zh-CN': '开发指南', - // type: 'group', - // children: [ - // '/development/directory-structure', - // '/development/env', - // '/development/nocobase-cli', - // { - // title: 'HTTP API', - // 'title.zh-CN': 'HTTP API', - // type: 'subMenu', - // children: [ - // '/development/http-api/index', - // '/development/http-api/rest-api', - // '/development/http-api/action-api', - // '/development/http-api/javascript-sdk', - // '/development/http-api/filter-operators', - // ], - // }, - // '/development/javascript-sdk', - // { - // title: 'Plugin development', - // 'title.zh-CN': '插件开发', - // type: 'subMenu', - // children: [ - // '/development/plugin-development/index', - // { - // title: 'Server', - // 'title.zh-CN': 'Server', - // type: 'subMenu', - // children: [ - // '/development/plugin-development/server/overview', - // '/development/plugin-development/server/database', - // '/development/plugin-development/server/resourcer', - // '/development/plugin-development/server/middleware', - // '/development/plugin-development/server/acl', - // '/development/plugin-development/server/events', - // '/development/plugin-development/server/i18n', - // '/development/plugin-development/server/cli', - // '/development/plugin-development/server/app-manager', - // '/development/plugin-development/server/plugin-manager', - // ], - // }, - // { - // title: 'Client', - // 'title.zh-CN': 'Client', - // type: 'subMenu', - // children: [ - // '/development/plugin-development/client/overview', - // { - // title: 'Providers', - // 'title.zh-CN': 'Providers', - // type: 'subMenu', - // children: [ - // '/development/plugin-development/client/providers/acl', - // '/development/plugin-development/client/providers/antd', - // '/development/plugin-development/client/providers/api-client', - // '/development/plugin-development/client/providers/collection-manager', - // '/development/plugin-development/client/providers/i18n', - // '/development/plugin-development/client/providers/route-switch', - // '/development/plugin-development/client/providers/schema-component', - // '/development/plugin-development/client/providers/schema-initializer', - // ], - // }, - // ], - // }, - // ], - // }, - // ], - // }, '/api': [ '/api/index', '/api/env', + { + title: 'HTTP API', + type: 'subMenu', + children: [ + '/api/http/index', + '/api/http/rest-api', + ], + }, { title: '@nocobase/server', type: 'subMenu', diff --git a/docs/zh-CN/api/http/index.md b/docs/zh-CN/api/http/index.md new file mode 100644 index 0000000000..a3117cf49b --- /dev/null +++ b/docs/zh-CN/api/http/index.md @@ -0,0 +1,301 @@ +# 概述 + +NocoBase 的 HTTP API 基于 Resource & Action 设计,是 REST API 的超集,操作不局限于增删改查,在 NocoBase 里,Resource Action 可以任意的扩展。 + +## 资源 Resource + +在 NocoBase 里,资源(resource)有两种表达方式: + +- `` +- `.` + + + +- collection 是所有抽象数据的集合 +- association 为 collection 的关联数据 +- resource 包括 collection 和 collection.association 两类 + + + +### 示例 + +- `posts` 文章 +- `posts.user` 文章用户 +- `posts.tags` 文章标签 + +## 操作 Action + +以 `:` 的方式表示资源操作 + +- `:` +- `.:` + +内置的全局操作,可用于 collection 或 association + +- `create` +- `get` +- `list` +- `update` +- `destroy` +- `move` + +内置的关联操作,仅用于 association + +- `set` +- `add` +- `remove` +- `toggle` + +### 示例 + +- `posts:create` 创建文章 +- `posts.user:get` 查看文章用户 +- `posts.tags:add` 附加文章标签(将现有的标签与文章关联) + +## 请求 URL + +```bash + /api/: + /api/:/ + /api///: + /api///:/ +``` + +### 示例 + +posts 资源 + +```bash +POST /api/posts:create +GET /api/posts:list +GET /api/posts:get/1 +POST /api/posts:update/1 +POST /api/posts:destroy/1 +``` + +posts.comments 资源 + +```bash +POST /api/posts/1/comments:create +GET /api/posts/1/comments:list +GET /api/posts/1/comments:get/1 +POST /api/posts/1/comments:update/1 +POST /api/posts/1/comments:destroy/1 +``` + +posts.tags 资源 + +```bash +POST /api/posts/1/tags:create +GET /api/posts/1/tags:get +GET /api/posts/1/tags:list +POST /api/posts/1/tags:update +POST /api/posts/1/tags:destroy +POST /api/posts/1/tags:add +GET /api/posts/1/tags:remove +``` + +## 资源定位 + +- collection 资源,通过 `collectionIndex` 定位到待处理的数据,`collectionIndex` 必须唯一 +- association 资源,通过 `collectionIndex` 和 `associationIndex` 联合定位待处理的数据,`associationIndex` 可能不是唯一的,但是 `collectionIndex` 和 `associationIndex` 的联合索引必须唯一 + +查看 association 资源详情时,请求的 URL 需要同时提供 `` 和 ``,`` 并不多余,因为 `` 可能不是唯一的。 + +例如 `tables.fields` 表示数据表的字段 + +```bash +GET /api/tables/table1/fields/title +GET /api/tables/table2/fields/title +``` + +table1 和 table2 都有 title 字段,title 在 table1 里是唯一的,但是其他表也可能有 title 字段 + +## 请求参数 + +请求的参数可以放在 Request 的 headers、parameters(query string)、body(GET 请求没有 body) 里。 + +几个特殊的 Parameters 请求参数 + +- `filter` 数据过滤,用于查询相关操作里; +- `filterByTk` 根据 tk 字段字过滤,用于指定详情数据的操作里; +- `sort` 排序,用于查询相关操作里。 +- `fields` 输出哪些数据,用于查询相关操作里; +- `appends` 附加关系字段,用于查询相关操作里; +- `except` 排除哪些字段(不输出),用于查询相关操作里; +- `whitelist` 字段白名单,用于数据的创建和更新相关操作里; +- `blacklist` 字段黑名单,用于数据的创建和更新相关操作里; + +### filter + +数据过滤 + +```bash +# simple +GET /api/posts?filter[status]=publish +# 推荐使用 json string 的格式,需要 encodeURIComponent 编码 +GET /api/posts?filter={"status":"published"} + +# filter operators +GET /api/posts?filter[status.$eq]=publish +GET /api/posts?filter={"status.$eq":"published"} + +# $and +GET /api/posts?filter={"$and": [{"status.$eq":"published"}, {"title.$includes":"a"}]} +# $or +GET /api/posts?filter={"$or": [{"status.$eq":"pending"}, {"status.$eq":"draft"}]} + +# association field +GET /api/posts?filter[user.email.$includes]=gmail +GET /api/posts?filter={"user.email.$includes":"gmail"} +``` + +[点此查看更多关于 filter operators 的内容](http-api/filter-operators) + +### filterByTk + +根据 tk 字段过滤,默认情况: + +- collection 资源,tk 为数据表的主键; +- association 资源,tk 为 association 的 targetKey 字段。 + +```bash +GET /api/posts:get?filterByTk=1&fields=name,title&appends=tags +``` + +### sort + +排序。降序时,字段前面加上减号 `-`。 + +```bash +# createAt 字段升序 +GET /api/posts:get?sort=createdAt +# createAt 字段降序 +GET /api/posts:get?sort=-createdAt +# 多个字段联合排序,createAt 字段降序、title A-Z 升序 +GET /api/posts:get?sort=-createdAt,title +``` + +### fields + +输出哪些数据 + +```bash +GET /api/posts:list?fields=name,title + +Response 200 (application/json) +{ + "data": [ + { + "name": "", + "title": "" + } + ], + "meta": {} +} +``` + +### appends + +附加关系字段 + +### except + +排除哪些字段(不输出),用于查询相关操作里; + +### whitelist + +白名单 + +```bash +POST /api/posts:create?whitelist=title + +{ + "title": "My first post", + "date": "2022-05-19" # date 字段会被过滤掉,不会写入数据库 +} +``` + +### blacklist + +黑名单 + +```bash +POST /api/posts:create?blacklist=date + +{ + "title": "My first post", + "date": "2022-05-19" # date 字段会被过滤掉,不会写入数据库 +} +``` + +## 请求响应 + +响应的格式 + +```ts +type ResponseResult = { + data?: any; // 主体数据 + meta?: any; // 附加数据 + errors?: ResponseError[]; // 报错 +}; + +type ResponseError = { + code?: string; + message: string; +}; +``` + +### 示例 + +查看列表 + +```bash +GET /api/posts:list + +Response 200 (application/json) + +{ + data: [ + { + id: 1 + } + ], + meta: { + count: 1 + page: 1, + pageSize: 1, + totalPage: 1 + }, +} +``` + +查看详情 + +```bash +GET /api/posts:get/1 + +Response 200 (application/json) + +{ + data: { + id: 1 + }, +} +``` + +报错 + +```bash +POST /api/posts:create + +Response 400 (application/json) + +{ + errors: [ + { + message: 'name must be required', + }, + ], +} +``` \ No newline at end of file diff --git a/docs/zh-CN/api/http/rest-api.md b/docs/zh-CN/api/http/rest-api.md new file mode 100644 index 0000000000..2d7fd86d5c --- /dev/null +++ b/docs/zh-CN/api/http/rest-api.md @@ -0,0 +1,184 @@ +# REST API + +NocoBase 的 HTTP API 是 REST API 的超集,标准的 CRUD API 也支持 RESTful 风格。 + +## Collection 资源 + +--- + +### 创建 collection + +HTTP API + +```bash +POST /api/:create + +{} # JSON body +``` + +REST API + +```bash +POST /api/ + +{} # JSON body +``` + +### 查看 collection 列表 + +HTTP API + +```bash +GET /api/:list +``` + +REST API + +```bash +GET /api/ +``` + +### 查看 collection 详情 + +HTTP API + +```bash +GET /api/:get?filterByTk= +GET /api/:get/ +``` + +REST API + +```bash +GET /api// +``` + +### 更新 collection + +HTTP API + +```bash +POST /api/:update?filterByTk= + +{} # JSON body + +# 或者 +POST /api/:update/ + +{} # JSON body +``` + +REST API + +```bash +PUT /api// + +{} # JSON body +``` + +### 删除 collection + +HTTP API + +```bash +POST /api/:destroy?filterByTk= +# 或者 +POST /api/:destroy/ +``` + +REST API + +```bash +DELETE /api// +``` + +## Association 资源 + +--- + +### 创建 Association + +HTTP API + +```bash +POST /api///:create + +{} # JSON body +``` + +REST API + +```bash +POST /api/// + +{} # JSON body +``` + +### 查看 Association 列表 + +HTTP API + +```bash +GET /api///:list +``` + +REST API + +```bash +GET /api/// +``` + +### 查看 Association 详情 + +HTTP API + +```bash +GET /api///:get?filterByTk= +# 或者 +GET /api///:get/ +``` + +REST API + +```bash +GET /api///:get/ +``` + +### 更新 Association + +HTTP API + +```bash +POST /api///:update?filterByTk= + +{} # JSON body + +# 或者 +POST /api///:update/ + +{} # JSON body +``` + +REST API + +```bash +PUT /api///:update/ + +{} # JSON 数据 +``` + +### 删除 Association + +HTTP API + +```bash +POST /api///:destroy?filterByTk= +# 或者 +POST /api///:destroy/ +``` + +REST API + +```bash +DELETE /api//// +``` diff --git a/docs/zh-CN/development/directory-structure.md b/docs/zh-CN/development/app-ds.md similarity index 87% rename from docs/zh-CN/development/directory-structure.md rename to docs/zh-CN/development/app-ds.md index 770ab204cc..a725a8ef52 100644 --- a/docs/zh-CN/development/directory-structure.md +++ b/docs/zh-CN/development/app-ds.md @@ -1,6 +1,6 @@ # 项目目录结构 -无论是源码还是 `create-nocobase-app` 创建的应用,目录结构都是一样的,结构如下: +无论是 [Git 源码](/welcome/getting-started/installation/git-clone) 还是 [create-nocobase-app](/welcome/getting-started/installation/create-nocobase-app) 创建的 NocoBase 应用,目录结构都是一样的,结构如下: ```bash ├── my-nocobase-app diff --git a/docs/zh-CN/development/client/i18n.md b/docs/zh-CN/development/client/i18n.md new file mode 100644 index 0000000000..5ea350bc6c --- /dev/null +++ b/docs/zh-CN/development/client/i18n.md @@ -0,0 +1,140 @@ +# 国际化 + +客户端的国际化多语言基于 npm 包 [react-i18next](https://npmjs.com/package/react-i18next) 实现,在应用顶层提供了 `` 组件的包装,可以在任意位置直接使用相关的方法。 + +添加语言包: + +```tsx | pure +import { i18n } from '@nocobase/client'; + +i18n.addResources('zh-CN', 'test', { + Hello: '你好', + World: '世界', +}); +``` + +注:这里第二个参数填写的 `'test'` 是语言的命名空间,通常插件自己定义的语言资源都应该按自己插件包名创建特定的命名空间,以避免和其他语音资源冲突。NocoBase 中默认的命名空间是 `'client'`,大部分常用和基础的语言翻译都放置在此命名空间,当没有提供所需语言时,可在插件自身的命名空间内进行扩展定义。 + +在组件中调用翻译函数: + +```tsx | pure +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +export default function MyComponent() { + // 使用之前定义的命名空间 + const { t } = useTranslation('test'); + + return ( +
+

{t('World')}

+
+ ); +} +``` + +在 SchemaComponent 组件中可以直接使用模板方法 `'{{t()}}'`,模板中的翻译函数会自动被执行: + +```tsx | pure +import React from 'react'; +import { SchemaComponent } from '@nocobase/client'; + +export default function MySchemaComponent() { + return ( + + ); +} +``` + +在某些特殊情况下也需要以模板的方式定义多语言时,可以使用 NocoBase 内置的 `compile()` 方法编译为多语言结果: + +```tsx | pure +import React from 'react'; +import { useCompile } from '@nocobase/client'; + +const title = '{{t("Hello", { ns: "test" })}}'; + +export default function MyComponent() { + const { compile } = useCompile(); + + return ( +
{compile(title)}
+ ); +} +``` + +## 建议配置 + +以英文文案为 key,翻译为 value,这样的好处,即使多语言缺失,也会以英文显示,不会造成阅读障碍,如: + +```ts +i18n.addResources('zh-CN', 'my-plugin', { + 'Show dialog': '显示对话框', + 'Hide dialog': '隐藏对话框' +}); +``` + +为了更方便管理多语言文件,推荐在插件中创建一个 `locale` 文件夹,并把对应语言文件都放置在其中方便管理: + +```bash +|- /my-plugin + |- /src + |- /client + |- locale # 多语言文件夹 + |- zh-CN.ts + |- en-US.ts +``` + +## 示例 + +### 客户端组件多语言 + +例如订单状态的组件,根据不同值有不同的文本显示: + +```tsx | pure +import React from 'react'; +import { Select } from 'antd'; +import { i18n } from '@nocobase/client'; +import { useTranslation } from 'react-i18next'; + +i18n.addResources('zh-CN', 'sample-shop-i18n', { + Pending: '已下单', + Paid: '已支付', + Delivered: '已发货', + Received: '已签收' +}); + +const ORDER_STATUS_LIST = [ + { value: -1, label: 'Canceled (untranslated)' }, + { value: 0, label: 'Pending' }, + { value: 1, label: 'Paid' }, + { value: 2, label: 'Delivered' }, + { value: 3, label: 'Received' }, +] + +function OrderStatusSelect() { + const { t } = useTranslation('sample-shop-i18n'); + + return ( + + ); +} + +export default function () { + return ( + + ); +} +``` diff --git a/docs/zh-CN/development/client/index.md b/docs/zh-CN/development/client/index.md new file mode 100644 index 0000000000..eeffb54328 --- /dev/null +++ b/docs/zh-CN/development/client/index.md @@ -0,0 +1,73 @@ +# 概述 + +NocoBase 客户端的扩展大多以 Provider 的形式提供,无论是内置的 Provider 还是插件的主文件都是 Provider。 + +## 内置的 Providers + +- APIClientProvider +- I18nextProvider +- AntdConfigProvider +- RemoteRouteSwitchProvider +- SystemSettingsProvider +- PluginManagerProvider +- SchemaComponentProvider +- SchemaInitializerProvider +- BlockSchemaComponentProvider +- AntdSchemaComponentProvider +- DocumentTitleProvider +- ACLProvider + +## 客户端 Provider 模块的注册 + +静态的 Provider 通过 app.use() 注册,动态的 Provider 通过 dynamicImport 适配。 + +```tsx | pure +import React from 'react'; +import { Application } from '@nocobase/client'; + +const app = new Application({ + apiClient: { + baseURL: process.env.API_BASE_URL, + }, + dynamicImport: (name: string) => { + return import(`../plugins/${name}`); + }, +}); + +// 访问 /hello 页面时,显示 Hello world! +const HelloProvider = React.memo((props) => { + const location = useLocation(); + if (location.pathname === '/hello') { + return
Hello world!
+ } + return <>{props.children} +}); + +app.use(HelloProvider); +``` + +## 插件的客户端 + +初始化的空插件,服务端相关目录结构如下: + +```bash +|- /my-plugin + |- /src + |- /client + |- index.tsx + |- client.d.ts + |- client.js +``` + +`client/index.tsx` 内容如下: + +```tsx | pure +import React from 'react'; + +// 这是一个空的 Provider,只有 children 传递,并未提供自定义的 Context +export default React.memo((props) => { + return <>{props.children}; +}); +``` + +插件 pm.add 之后,会向 `packages/app/client/src/plugins` 目录写入 `my-plugin.ts` 文件 diff --git a/docs/zh-CN/development/guide/m.svg b/docs/zh-CN/development/client/m.svg similarity index 100% rename from docs/zh-CN/development/guide/m.svg rename to docs/zh-CN/development/client/m.svg diff --git a/docs/zh-CN/development/guide/settings-center.md b/docs/zh-CN/development/client/settings-center.md similarity index 90% rename from docs/zh-CN/development/guide/settings-center.md rename to docs/zh-CN/development/client/settings-center.md index 37286175c8..7639b793ec 100644 --- a/docs/zh-CN/development/guide/settings-center.md +++ b/docs/zh-CN/development/client/settings-center.md @@ -1,6 +1,6 @@ # 配置中心 - + ## 示例 diff --git a/docs/zh-CN/development/guide/settings-tab.jpg b/docs/zh-CN/development/client/settings-center/settings-tab.jpg similarity index 100% rename from docs/zh-CN/development/guide/settings-tab.jpg rename to docs/zh-CN/development/client/settings-center/settings-tab.jpg diff --git a/docs/zh-CN/development/client/test.md b/docs/zh-CN/development/client/test.md new file mode 100644 index 0000000000..452ea9f46d --- /dev/null +++ b/docs/zh-CN/development/client/test.md @@ -0,0 +1,41 @@ +# 测试 + +测试基于 [Jest](https://jestjs.io/) 测试框架。同时还包括了常用的 React 测试库,如 [@testing-library/react](https://testing-library.com/docs/react-testing-library/intro/) + +## 示例 + +```tsx | pure +import { render } from '@testing-library/react'; +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { RouteSwitch } from '../RouteSwitch'; +import { RouteSwitchProvider } from '../RouteSwitchProvider'; + +const Home = () =>

Home

; +const About = () =>

About

; + +describe('route-switch', () => { + it('case 1', () => { + const App = () => { + return ( + + + + + + ); + }; + const { container } = render(); + expect(container).toMatchSnapshot(); + }); +}); +``` \ No newline at end of file diff --git a/docs/zh-CN/development/guide/ui-router.md b/docs/zh-CN/development/client/ui-router.md similarity index 100% rename from docs/zh-CN/development/guide/ui-router.md rename to docs/zh-CN/development/client/ui-router.md diff --git a/docs/zh-CN/development/guide/ui-schema-designer/acl.md b/docs/zh-CN/development/client/ui-schema-designer/acl.md similarity index 100% rename from docs/zh-CN/development/guide/ui-schema-designer/acl.md rename to docs/zh-CN/development/client/ui-schema-designer/acl.md diff --git a/docs/zh-CN/development/guide/ui-schema-designer/block-provider.md b/docs/zh-CN/development/client/ui-schema-designer/block-provider.md similarity index 100% rename from docs/zh-CN/development/guide/ui-schema-designer/block-provider.md rename to docs/zh-CN/development/client/ui-schema-designer/block-provider.md diff --git a/docs/zh-CN/development/guide/ui-schema-designer/collection-manager.md b/docs/zh-CN/development/client/ui-schema-designer/collection-manager.md similarity index 100% rename from docs/zh-CN/development/guide/ui-schema-designer/collection-manager.md rename to docs/zh-CN/development/client/ui-schema-designer/collection-manager.md diff --git a/docs/zh-CN/development/guide/ui-schema-designer/component-library.md b/docs/zh-CN/development/client/ui-schema-designer/component-library.md similarity index 100% rename from docs/zh-CN/development/guide/ui-schema-designer/component-library.md rename to docs/zh-CN/development/client/ui-schema-designer/component-library.md diff --git a/docs/zh-CN/development/guide/ui-schema-designer/demo1.tsx b/docs/zh-CN/development/client/ui-schema-designer/demo1.tsx similarity index 100% rename from docs/zh-CN/development/guide/ui-schema-designer/demo1.tsx rename to docs/zh-CN/development/client/ui-schema-designer/demo1.tsx diff --git a/docs/zh-CN/development/guide/ui-schema-designer/designable.md b/docs/zh-CN/development/client/ui-schema-designer/designable.md similarity index 100% rename from docs/zh-CN/development/guide/ui-schema-designer/designable.md rename to docs/zh-CN/development/client/ui-schema-designer/designable.md diff --git a/docs/zh-CN/development/guide/ui-schema-designer/designable.png b/docs/zh-CN/development/client/ui-schema-designer/designable.png similarity index 100% rename from docs/zh-CN/development/guide/ui-schema-designer/designable.png rename to docs/zh-CN/development/client/ui-schema-designer/designable.png diff --git a/docs/zh-CN/development/guide/ui-schema-designer/extending-schema-components.md b/docs/zh-CN/development/client/ui-schema-designer/extending-schema-components.md similarity index 100% rename from docs/zh-CN/development/guide/ui-schema-designer/extending-schema-components.md rename to docs/zh-CN/development/client/ui-schema-designer/extending-schema-components.md diff --git a/docs/zh-CN/development/guide/ui-schema-designer/index.md b/docs/zh-CN/development/client/ui-schema-designer/index.md similarity index 100% rename from docs/zh-CN/development/guide/ui-schema-designer/index.md rename to docs/zh-CN/development/client/ui-schema-designer/index.md diff --git a/docs/zh-CN/development/guide/ui-schema-designer/insert-adjacent.md b/docs/zh-CN/development/client/ui-schema-designer/insert-adjacent.md similarity index 100% rename from docs/zh-CN/development/guide/ui-schema-designer/insert-adjacent.md rename to docs/zh-CN/development/client/ui-schema-designer/insert-adjacent.md diff --git a/docs/zh-CN/development/guide/ui-schema-designer/what-is-ui-schema.md b/docs/zh-CN/development/client/ui-schema-designer/what-is-ui-schema.md similarity index 100% rename from docs/zh-CN/development/guide/ui-schema-designer/what-is-ui-schema.md rename to docs/zh-CN/development/client/ui-schema-designer/what-is-ui-schema.md diff --git a/docs/zh-CN/development/guide/ui-schema-designer/x-designer.md b/docs/zh-CN/development/client/ui-schema-designer/x-designer.md similarity index 100% rename from docs/zh-CN/development/guide/ui-schema-designer/x-designer.md rename to docs/zh-CN/development/client/ui-schema-designer/x-designer.md diff --git a/docs/zh-CN/development/guide/ui-schema-designer/x-initializer.md b/docs/zh-CN/development/client/ui-schema-designer/x-initializer.md similarity index 100% rename from docs/zh-CN/development/guide/ui-schema-designer/x-initializer.md rename to docs/zh-CN/development/client/ui-schema-designer/x-initializer.md diff --git a/docs/zh-CN/development/guide/i18n.md b/docs/zh-CN/development/guide/i18n.md deleted file mode 100644 index 9c08448cd7..0000000000 --- a/docs/zh-CN/development/guide/i18n.md +++ /dev/null @@ -1,266 +0,0 @@ -# 国际化 - -## 基础概念 - -多语言国际化支持根据服务端和客户端分为两大部分,各自有相应的实现。 - -语言配置均为普通的 JSON 对象键值对,如果没有翻译文件(或省略编写)的话会直接输出键名的字符串。 - -### 服务端 - -服务端的多语言国际化基于 npm 包 [i18next](https://npmjs.com/package/i18next) 实现,在服务端应用初始化时会创建一个 i18next 实例,同时也会将此实例注入到请求上下文(`context`)中,以供在各处方便的使用。 - -在创建服务端 Application 实例时可以传入配置对应的初始化参数: - -```ts -import { Application } from '@nocobase/server'; - -const app = new Application({ - i18n: { - defaultNS: 'test', - resources: { - 'en-US': { - test: { - hello: 'Hello', - }, - }, - 'zh-CN': { - test: { - hello: '你好', - } - } - } - } -}); -``` - -或者在插件中对已存在的 app 实例添加语言数据至对应命名空间: - -```ts -app.i18n.addResources('zh-CN', 'test', { - Hello: '你好', - World: '世界', -}); - -app.i18n.addResources('en-US', 'test', { - Hello: 'Hello', - World: 'World', -}); -``` - -基于应用: - -```ts -app.i18n.t('World') // “世界”或“World” -``` - -基于请求: - -```ts -app.resource({ - name: 'test', - actions: { - async get(ctx, next) { - ctx.body = `${ctx.t('Hello')} ${ctx.t('World')}`; - await next(); - } - } -}); -``` - -通常服务端的多语言处理主要用于错误信息的输出。 - -### 客户端 - -客户端的多语言国际化基于 npm 包 [react-i18next](https://npmjs.com/package/react-i18next) 实现,在应用顶层提供了 `` 组件的包装,可以在任意位置直接使用相关的方法。 - -添加语言包: - -```tsx | pure -import { i18n } from '@nocobase/client'; - -i18n.addResources('zh-CN', 'test', { - Hello: '你好', - World: '世界', -}); -``` - -注:这里第二个参数填写的 `'test'` 是语言的命名空间,通常插件自己定义的语言资源都应该按自己插件包名创建特定的命名空间,以避免和其他语音资源冲突。NocoBase 中默认的命名空间是 `'client'`,大部分常用和基础的语言翻译都放置在此命名空间,当没有提供所需语言时,可在插件自身的命名空间内进行扩展定义。 - -在组件中调用翻译函数: - -```tsx | pure -import React from 'react'; -import { useTranslation } from 'react-i18next'; - -export default function MyComponent() { - // 使用之前定义的命名空间 - const { t } = useTranslation('test'); - - return ( -
-

{t('World')}

-
- ); -} -``` - -在 SchemaComponent 组件中可以直接使用模板方法 `'{{t()}}'`,模板中的翻译函数会自动被执行: - -```tsx | pure -import React from 'react'; -import { SchemaComponent } from '@nocobase/client'; - -export default function MySchemaComponent() { - return ( - - ); -} -``` - -在某些特殊情况下也需要以模板的方式定义多语言时,可以使用 NocoBase 内置的 `compile()` 方法编译为多语言结果: - -```tsx | pure -import React from 'react'; -import { useCompile } from '@nocobase/client'; - -const title = '{{t("Hello", { ns: "test" })}}'; - -export default function MyComponent() { - const { compile } = useCompile(); - - return ( -
{compile(title)}
- ); -} -``` - -### 建议配置 - -当添加语言资源时,推荐在 JSON 配置模板中将语言串的键名设置为默认语言,更方便统一处理,且可以省去默认语言的翻译。例如以英语为默认语言: - -```ts -i18n.addResources('zh-CN', 'your-namespace', { - 'Show dialog': '显示对话框', - 'Hide dialog': '隐藏对话框' -}); -``` - -语言内容如果比较多,推荐在插件中创建一个 `locals` 目录,并把对应语言文件都放置在其中方便管理: - -``` -- server -| - locals -| | - zh-CN.ts -| | - en-US.ts -| | - ... -| - index.ts -``` - -## 示例 - -### 服务端错误提示 - -例如用户在店铺对某个商品下单时,如果商品的库存不够,或者未上架,那么下单接口被调用时,应该返回相应的错误。 - -```ts -const namespace = 'shop'; - -export default class ShopPlugin extends Plugin { - async load() { - this.app.i18n.addResources('zh-CN', namespace, { - 'No such product': '商品不存在', - 'Product not on sale': '商品已下架', - 'Out of stock': '库存不足', - }); - - this.app.resource({ - name: 'orders', - actions: { - async create(ctx, next) { - const productRepo = ctx.db.getRepository('products'); - const product = await productRepo.findOne({ - filterByTk: ctx.action.params.values.productId - }); - - if (!product) { - return ctx.throw(404, ctx.t('No such product')); - } - - if (!product.enabled) { - return ctx.throw(400, ctx.t('Product not on sale')); - } - - if (!product.inventory) { - return ctx.throw(400, ctx.t('Out of stock')); - } - - const orderRepo = ctx.db.getRepository('orders'); - ctx.body = await orderRepo.create({ - values: { - productId: product.id, - quantity: 1, - totalPrice: product.price, - userId: ctx.state.currentUser.id - } - }); - - next(); - } - } - }); - } -} -``` - -### 客户端组件多语言 - -例如订单状态的组件,根据不同值有不同的文本显示: - -```tsx -import React from 'react'; -import { Select } from 'antd'; -import { i18n } from '@nocobase/client'; -import { useTranslation } from 'react-i18next'; - -i18n.addResources('zh-CN', '@nocobase/plugin-sample-shop-i18n', { - Pending: '已下单', - Paid: '已支付', - Delivered: '已发货', - Received: '已签收' -}); - -const ORDER_STATUS_LIST = [ - { value: -1, label: 'Canceled (untranslated)' }, - { value: 0, label: 'Pending' }, - { value: 1, label: 'Paid' }, - { value: 2, label: 'Delivered' }, - { value: 3, label: 'Received' }, -] - -function OrderStatusSelect() { - const { t } = useTranslation('@nocobase/plugin-sample-shop-i18n'); - - return ( - - ); -} - -export default function () { - return ( - - ); -} -``` diff --git a/docs/zh-CN/development/guide/index.md b/docs/zh-CN/development/guide/index.md deleted file mode 100644 index fa1ec74e4a..0000000000 --- a/docs/zh-CN/development/guide/index.md +++ /dev/null @@ -1,71 +0,0 @@ -# 概述 - -## Web Server - -- Collection & Field -- Resource & Action -- Middleware -- Events & Hooks - -**Samples** - -- samples/shop-modeling -- samples/shop-actions -- samples/ratelimit -- samples/model-hooks - -## UI Schema Designer - -平台最核心的功能,用于可视化配置页面。 - -- 扩展各种供 UI 设计器使用的 Schema 组件 -- 各种 x-designer 和 x-initializer - -**Samples** - -- samples/custom-block -- samples/custom-action -- samples/custom-field -- samples/custom-x-designer -- samples/custom-x-initializer - -## UI Router - -用于自定义页面,包括: - -- routes config -- route components - -**Samples** - -- samples/custom-page - -## Settings Center - -由插件提供的系统级配置的能力 - -- plugin settings tabs - -**Samples** - -- samples/custom-settings-center-page - -## I18n - -国际化 - -- I18n - -**Samples** - -- samples/shop-i18n - -## Devtools - -- Commands -- Migrations - -**Samples** - -- samples/custom-command -- samples/custom-migration diff --git a/docs/zh-CN/development/guide/migration.md b/docs/zh-CN/development/guide/migration.md deleted file mode 100644 index ad54e6fd26..0000000000 --- a/docs/zh-CN/development/guide/migration.md +++ /dev/null @@ -1,219 +0,0 @@ -# 数据库迁移 - -应用在业务发展或版本升级过程中,某些情况会需要修改数据库表或字段等信息,为保证安全无冲突且可回溯的解决数据库变更,通常的做法是使用数据库迁移的方式完成。 - -## 介绍 - -Nocobase 基于 npm 包 [Umzug](https://www.npmjs.com/package/umzug) 处理数据库迁移。并将相关功能集成在命令行的子命令 `nocobase migrator` 中,大部分操作通过该命令处理。 - -### 仅增加表或字段无需迁移脚本 - -通常如果只是增加数据表或增加字段,可以不使用数据库迁移脚本,而是直接修改数据表定义(`collection`)的内容即可。例如文章表定义(`collections/posts.ts`)初始状态: - -```ts -export default { - name: 'posts', - fields: [ - { - name: 'title', - type: 'string', - } - ] -} -``` - -当需要增加一个分类字段时,直接修改原来的表结构定义文件内容: - -```ts -export default { - name: 'posts', - fields: [ - { - name: 'title', - type: 'string', - }, - { - name: 'category', - type: 'belongsTo' - } - ] -} -``` - -当新版本代码在环境中调用升级命令时,新的字段会以 Sequelize 中 sync 的逻辑自动同步到数据库中,完成表结构变更。 - -### 创建迁移文件 - -如果表结构的变更涉及到字段类型变更、索引调整等,需要人工创建迁移脚本文件: - -```bash -yarn nocobase migrator create --name change-some-field.ts --folder path/to/migrations -``` - -该命令会在 `path/to/migrations` 目录中创建一个基于时间戳的迁移脚本文件 `YYYY.MM.DDTHH.mm.ss.change-some-field.ts`,内容如下: - -```ts -import { Migration } from '@nocobase/server'; - -export default class MyMigration extends Migration { - async up() { - // TODO - } - - async down() { - // TODO - } -} -``` - -脚本导出的主类相关 API 可以参考 [`Migration` 类](/api/server/migration)。 - -### 数据库操作内容 - -`up()`、`down()` 是一对互逆操作,升级时会调用 `up()`,降级时会调用 `down()`。大部分情况我们主要考虑升级操作。 - -在升级中我们有几种方式进行数据库变更: - -```ts -import { Migration } from '@nocobase/server'; - -export default class MyMigration extends Migration { - async up() { - // 1. 针对自行管理的静态数据表,调用 Sequelize 提供的 queryInterface 实例上的方法 - await this.queryInterface.changeColumn('posts', 'title', { - type: DataTypes.STRING, - unique: true // 添加索引 - }); - - // 2. 针对被 collection-manager 插件管理的动态数据表,调用插件提供的 collections / fields 表的数据仓库方法 - await this.db.getRepository('fields').update({ - values: { - collectionName: 'posts', - name: 'title', - type: 'string', - unique: true // 添加索引 - } - }); - } -} -``` - -### 数据变更 - -除了表结构变更,也可以在迁移过程中导入需要的数据,或对数据进行调整: - -```ts -import { Migration } from '@nocobase/server'; - -export default class MyMigration extends Migration { - async up() { - await this.sequelize.transaction(async transaction => { - const defaultCategory = await this.db.getRepository('categories').create({ - values: { - title: '默认分类' - }, - transaction - }); - - await this.db.getRepository('posts').update({ - filter: { - categoryId: null - }, - values: { - categoryId: defaultCategory.id - }, - transaction - }); - }); - } -} -``` - -在一个脚本中有多项数据库操作时,建议使用事务保证所有操作都成功才算迁移完成。 - -### 执行升级 - -迁移脚本准备好以后,在项目目录下执行对应的升级命令即可完成数据库变更: - -```bash -yarn nocobase upgrade -``` - -根据数据库迁移的机制,迁移脚本执行成功后也会被记录在数据库的升级记录表中,只有第一次执行有效,之后的多次重复执行都会被忽略。 - -## 示例 - -### 修改主键字段类型 - -假设订单表一开始使用数字类型,但后期希望改成可以包含字母的字符串类型,我们可以在迁移文件中填写: - -```ts -import { Migration } from '@nocobase/server'; - -export default class MyMigration extends Migration { - async up() { - await this.sequelize.transaction(async transaction => { - await this.queryInterface.changeColumn('orders', 'id', { - type: DataTypes.STRING - }, { - transaction - }); - }); - } -} -``` - -注:修改字段类型只有在未加入新类型数据之前可以进行逆向降级操作,否则需要自行备份数据并对数据进行特定处理。 - -另外,直接修改数据表主键 `id` 的类型在某些数据库中会提示错误(SQLite 正常,PostgreSQL 失败)。这时候需要把相关操作分步执行: - -1. 创建一个新的字符串类型字段 `id_new;` -2. 复制原有表 `id` 的数据到新的字段; -3. 移除原有 `id` 主键约束; -4. 将原有 `id` 列改名为不使用的列名 `id_old`; -5. 将新的 `id_new` 列改名为 `id`; -6. 对新的 `id` 列增加主键约束; - -```ts -import { Migration } from '@nocobase/server'; - -export default class MyMigration extends Migration { - async up() { - await this.sequelize.transaction(async transaction => { - await this.queryInterface.addColumn('orders', 'id_new', { - type: DataTypes.STRING - }, { transaction }); - - const PendingOrderModel = this.sequelize.define('orders', { - id_new: DataTypes.STRING - }); - - await PendingOrderModel.update({ - id_new: col('id') - }, { - where: { - id: { [Op.not]: null } - }, - transaction - }); - - await this.queryInterface.removeConstraint('orders', 'orders_pkey', { transaction }); - - await this.queryInterface.renameColumn('orders', 'id', 'id_old', { transaction }); - - await this.queryInterface.renameColumn('orders', 'id_new', 'id', { transaction }); - - await this.queryInterface.addConstraint('orders', { - type: 'PRIMARY KEY', - name: 'orders_pkey', - fields: ['id'], - transaction - }); - }); - } -} -``` - -通常修改列类型在已存在数据量较大的表里操作时也建议用新列代替旧列的方式,性能会更好。其他更多细节可以参考 [Sequelize 的 `queryInterface` API](https://sequelize.org/api/v6/class/src/dialects/abstract/query-interface.js),以及各个数据库引擎的细节。 - -注:在执行升级命令后,应用启动之前请确保变更的表结构能够对应上 collections 中定义的内容,以免不一致导致错误。 diff --git a/docs/zh-CN/development/index.md b/docs/zh-CN/development/index.md index ec9544ba74..77377521af 100644 --- a/docs/zh-CN/development/index.md +++ b/docs/zh-CN/development/index.md @@ -1,16 +1,24 @@ # 介绍 -NocoBase 采用微内核架构,各类功能以插件形式扩展,所以微内核架构也叫插件化架构,由内核和插件两部分组成。内核提供了最小功能的 WEB 服务器,还提供了各种插件化接口;插件是按功能划分的各种独立模块,通过接口适配,具有可插拔的特点。插件化的设计降低了模块之间的耦合度,提高了复用率。随着插件库的不断扩充,常见的场景只需要组合插件即可完成基础搭建。例如 NocoBase 的无代码平台,就是由各种插件组合起来。 +NocoBase 采用微内核架构,各类功能以插件形式扩展,前后端分离,提供了各种插件化接口,插件按功能模块划分,具有可插拔的特点。 - + + +插件化的设计降低了模块之间的耦合度,提高了复用率。随着插件库的不断扩充,常见的场景只需要组合插件即可完成基础搭建。例如 NocoBase 的无代码平台,就是由各种插件组合起来。 + + ## 插件管理器 NocoBase 提供了强大的插件管理器用于管理插件,插件管理器的流程如下: - + -开发可以通过 CLI 的方式管理插件: +无代码用户可以通过界面管理本地插件的激活和禁用: + + + +开发也可以通过 CLI 的方式管理完整的插件流程: ```bash # 创建插件 @@ -25,10 +33,6 @@ yarn pm disable hello yarn pm remove hello ``` -无代码用户也可以通过插件管理器界面激活、禁用、删除已添加的本地插件: - - - 更多插件示例,查看 [packages/samples](https://github.com/nocobase/nocobase/tree/main/packages/samples)。 ## 扩展能力 @@ -39,4 +43,20 @@ yarn pm remove hello - 也可以是用于增强或限制 HTTP API 的过滤器、校验器、访问限制等 - 也可以是更底层的数据表、迁移、事件、命令行等功能的增强 -不仅如此,更多扩展介绍请查看 [扩展指南 - 概述](/development/guide) 章节。 \ No newline at end of file + +各模块分布: + +- Server + - Collections & Fields:主要用于系统表配置,业务表建议在「配置中心 - 数据表配置」里配置 + - Resources & Actions:主要用于扩展 Action API + - Middleware:中间件 + - Events:事件 + - I18n:服务端国际化 + - Commands:自定义命令行 + - Migrations:迁移脚本 +- Client + - UI Schema Designer:页面设计器 + - UI Router:有自定义页面需求时 + - Settings Center:为插件提供配置页面 + - I18n:客户端国际化 + diff --git a/docs/zh-CN/development/index/app-flow.svg b/docs/zh-CN/development/index/app-flow.svg new file mode 100644 index 0000000000..e167cda39b --- /dev/null +++ b/docs/zh-CN/development/index/app-flow.svg @@ -0,0 +1 @@ +
beforeLoad
loop: plugin.beforeLoad()
load/reload
loop: plugin.load()
afterLoad
Reinstall?
beforeInstall
install
afterInstall
beforeUpgrade
upgrade
afterUpgrade
Restart?
beforeStart
start
afterStart
beforeStop
stop
afterStop
beforeDestroy
destroy
afterDestroy
\ No newline at end of file diff --git a/docs/zh-CN/development/guide/pm-built-in.jpg b/docs/zh-CN/development/index/pm-built-in.jpg similarity index 100% rename from docs/zh-CN/development/guide/pm-built-in.jpg rename to docs/zh-CN/development/index/pm-built-in.jpg diff --git a/docs/zh-CN/development/index/pm-flow.svg b/docs/zh-CN/development/index/pm-flow.svg new file mode 100644 index 0000000000..23a738cfcd --- /dev/null +++ b/docs/zh-CN/development/index/pm-flow.svg @@ -0,0 +1 @@ +
Local
pm.create
Marketplace
pm.publish
NPM registry
Extracting client files
pm.add
plugin.afterAdd()
app/client plugins
pm.enable
plugin.install()
plugin.afterEnable()
pm.disable
plugin.afterDisable()
pm.remove
plugin.remove()
pm.upgrade
\ No newline at end of file diff --git a/docs/zh-CN/development/index/pm-ui.jpg b/docs/zh-CN/development/index/pm-ui.jpg new file mode 100644 index 0000000000..4c8fdd3c1a Binary files /dev/null and b/docs/zh-CN/development/index/pm-ui.jpg differ diff --git a/docs/zh-CN/development/life-cycle.md b/docs/zh-CN/development/life-cycle.md new file mode 100644 index 0000000000..d6daab561b --- /dev/null +++ b/docs/zh-CN/development/life-cycle.md @@ -0,0 +1,41 @@ +# 生命周期 + +## 应用的生命周期 + + + +## 插件的生命周期 + + + +## 插件的生命周期方法 + +```ts +import { InstallOptions, Plugin } from '@nocobase/server'; + +export class MyPlugin extends Plugin { + afterAdd() { + // 插件 pm.add 注册进来之后,主要用于放置 app.beforeLoad 的事件。 + } + beforeLoad() { + // 所有插件执行 load 之前,一般用于注册类和事件监听 + } + async load() { + // 加载配置 + } + async install(options?: InstallOptions) { + // 安装逻辑 + } + async afterEnable() { + // 激活之后 + } + async afterDisable() { + // 禁用之后 + } + async remove() { + // 删除逻辑 + } +} + +export default MyPlugin; +``` diff --git a/docs/zh-CN/development/nocobase-cli.md b/docs/zh-CN/development/nocobase-cli.md deleted file mode 100644 index bb1c0402a1..0000000000 --- a/docs/zh-CN/development/nocobase-cli.md +++ /dev/null @@ -1,310 +0,0 @@ ---- -order: 2 ---- - -# NocoBase CLI - -NocoBase CLI 旨在帮助你开发、构建和部署 NocoBase 应用。 - - - -NocoBase CLI 支持 ts-node 和 node 两种运行模式 - -- ts-node 模式(默认):用于开发环境,支持实时编译,但是响应较慢 -- node 模式:用于生产环境,响应迅速,但需要先执行 `yarn nocobase build` 将全部源码进行编译 - - - -## 使用说明 - -```bash -$ yarn nocobase -h - -Usage: nocobase [command] [options] - -Options: - -h, --help - -Commands: - create-plugin 创建插件脚手架 - console - db:auth 校验数据库是否连接成功 - db:sync 通过 collections 配置生成相关数据表和字段 - install 安装 - start 生产环境启动应用 - build 编译打包 - clean 删除编译之后的文件 - dev 启动应用,用于开发环境,支持实时编译 - doc 文档开发 - test 测试 - umi - upgrade 升级 - help -``` - -## 在脚手架里应用 - -应用脚手架 `package.json` 里的 `scripts` 如下: - -```json -{ - "scripts": { - "dev": "nocobase dev", - "start": "nocobase start", - "clean": "nocobase clean", - "build": "nocobase build", - "test": "nocobase test", - "postinstall": "nocobase umi generate tmp" - } -} -``` - -## 命令行扩展 - -NocoBase CLI 基于 [commander](https://github.com/tj/commander.js) 构建,你可以自由扩展命令,扩展的 command 可以写在 `app/server/index.ts` 里: - -```ts -const app = new Application(config); - -app.command('hello').action(() => {}); -``` - -或者,写在插件里: - -```ts -class MyPlugin extends Plugin { - beforeLoad() { - this.app.command('hello').action(() => {}); - } -} -``` - -终端运行 - -```bash -$ yarn nocobase hello -``` - -## 内置命令行 - -按使用频率排序 - -### `dev` - -开发环境下,启动应用,代码实时编译。 - - -NocoBase 未安装时,会自动安装(参考 install 命令) - - -```bash -Usage: nocobase dev [options] - -Options: - -p, --port [port] - --client - --server - -h, --help -``` - -示例 - -```bash -# 启动应用,用于开发环境,实时编译 -yarn nocobase dev -# 只启动服务端 -yarn nocobase dev --server -# 只启动客户端 -yarn nocobase dev --client -``` - -### `start` - -生产环境下,启动应用,代码需要 yarn build。 - - - -- NocoBase 未安装时,会自动安装(参考 install 命令) -- 源码有修改时,需要重新打包(参考 build 命令) - - - -```bash -$ yarn nocobase start -h - -Usage: nocobase start [options] - -Options: - -p, --port - -s, --silent - -h, --help -``` - -示例 - -```bash -# 启动应用,用于生产环境, -yarn nocobase start -``` - -### `install` - -安装 - -```bash -$ yarn nocobase install -h - -Usage: nocobase install [options] - -Options: - -f, --force - -c, --clean - -s, --silent - -l, --lang [lang] - -e, --root-email - -p, --root-password - -n, --root-nickname [rootNickname] - -h, --help -``` - -示例 - -```bash -# 初始安装 -yarn nocobase install -l zh-CN -e admin@nocobase.com -p admin123 -# 删除 NocoBase 的所有数据表,并重新安装 -yarn nocobase install -f -l zh-CN -e admin@nocobase.com -p admin123 -# 清空数据库,并重新安装 -yarn nocobase install -c -l zh-CN -e admin@nocobase.com -p admin123 -``` - - - -`-f/--force` 和 `-c/--clean` 的区别 -- `-f/--force` 删除 NocoBase 的数据表 -- `-c/--clean` 清空数据库,所有数据表都会被删除 - - - -### `upgrade` - -升级 - -```bash -yarn nocobase upgrade -``` - -### `test` - -jest 测试,支持所有 [jest-cli](https://jestjs.io/docs/cli) 的 options,除此之外还扩展了 `-c, --db-clean` 的支持。 - -```bash -$ yarn nocobase test -h - -Usage: nocobase test [options] - -Options: - -c, --db-clean 运行所有测试前清空数据库 - -h, --help -``` - -示例 - -```bash -# 运行所有测试文件 -yarn nocobase test -# 运行指定文件夹下所有测试文件 -yarn nocobase test packages/core/server -# 运行指定文件里的所有测试 -yarn nocobase test packages/core/database/src/__tests__/database.test.ts - -# 运行测试前,清空数据库 -yarn nocobase test -c -yarn nocobase test packages/core/server -c -``` - -### `build` - -代码部署到生产环境前,需要将源码编译打包,如果代码有修改,也需要重新构建。 - -```bash -# 所有包 -yarn nocobase build -# 指定包 -yarn nocobase build app/server app/client -``` - -### `clean` - -删除编译之后的文件 - -```bash -yarn clean -# 等同于 -yarn rimraf -rf packages/*/*/{lib,esm,es,dist} -``` - -### `doc` - -文档开发 - -```bash -# 启动文档 -yarn doc --lang=zh-CN # 等同于 yarn doc dev -# 构建文档,默认输出到 ./docs/dist/ 目录下 -yarn doc build -# 查看 dist 输出的文档最终效果 -yarn doc serve --lang=zh-CN -``` - -### `db:auth` - -校验数据库是否连接成功 - -```bash -$ yarn nocobase db:auth -h - -Usage: nocobase db:auth [options] - -Options: - -r, --retry [retry] 重试次数 - -h, --help -``` - -### `db:sync` - -通过 collections 配置生成数据表和字段 - -```bash -$ yarn nocobase db:sync -h - -Usage: nocobase db:sync [options] - -Options: - -f, --force - -h, --help display help for command -``` - -### `umi` - -`app/client` 基于 [umi](https://umijs.org/) 构建,可以通过 `nocobase umi` 来执行其他相关命令。 - -```bash -# 生成开发环境所需的 .umi 缓存 -yarn nocobase umi generate tmp -``` - -### `help` - -帮助命令,也可以用 option 参数,`-h` 和 `--help` - -```bash -# 查看所有 cli -yarn nocobase help -# 也可以用 -h -yarn nocobase -h -# 或者 --help -yarn nocobase --help -# 查看 db:sync 命令的 option -yarn nocobase db:sync -h -``` diff --git a/docs/zh-CN/development/plugin-ds.md b/docs/zh-CN/development/plugin-ds.md new file mode 100644 index 0000000000..b1dc993b52 --- /dev/null +++ b/docs/zh-CN/development/plugin-ds.md @@ -0,0 +1,17 @@ +# 插件目录结构 + +可以通过 `yarn pm create my-plugin` 快速创建一个空插件,目录结构如下: + +```bash +|- /my-plugin + |- /src + |- /client # 插件客户端代码 + |- /server # 插件服务端代码 + |- client.d.ts + |- client.js + |- package.json # 插件包信息 + |- server.d.ts + |- server.js +``` + +`/src/server` 的教程参考 [服务端](./server) 章节,`/src/client` 的教程参考 [客户端](./client) 章节。 diff --git a/docs/en-US/development/guide/collections-fields.md b/docs/zh-CN/development/server/collections-fields.md similarity index 100% rename from docs/en-US/development/guide/collections-fields.md rename to docs/zh-CN/development/server/collections-fields.md diff --git a/docs/en-US/development/guide/commands.md b/docs/zh-CN/development/server/commands.md similarity index 86% rename from docs/en-US/development/guide/commands.md rename to docs/zh-CN/development/server/commands.md index 5334d3b8db..3edfcab05e 100644 --- a/docs/en-US/development/guide/commands.md +++ b/docs/zh-CN/development/server/commands.md @@ -1,7 +1,5 @@ # 命令行 -## 简介 - NocoBase Server Application 除了用作 WEB 服务器以外,也是个强大可扩展的 CLI 工具。 新建一个 `app.js` 文件,代码如下: @@ -24,20 +22,24 @@ node app.js start # 启动 为了更好的开发、构建和部署 NocoBase 应用,NocoBase 内置了许多命令,详情查看 [NocoBase CLI](/api/cli) 章节。 -## 自定义 Command +## 如何自定义 Command? NocoBase CLI 的设计思想与 [Laravel Artisan](https://laravel.com/docs/9.x/artisan) 非常相似,都是可扩展的。NocoBase CLI 基于 [commander](https://www.npmjs.com/package/commander) 实现,可以这样扩展 Command: ```ts -app - .command('echo') - .option('-v, --version'); - .action(async ([options]) => { - console.log('Hello World!'); - if (options.version) { - console.log('Current version:', app.getVersion()); - } - }); +export class MyPlugin extends Plugin { + load() { + this.app + .command('echo') + .option('-v, --version'); + .action(async ([options]) => { + console.log('Hello World!'); + if (options.version) { + console.log('Current version:', app.getVersion()); + } + }); + } +} ``` 这个方法定义了以下命令: @@ -47,7 +49,7 @@ yarn nocobase echo # Hello World! yarn nocobase echo -v # Hello World! -# Current version: 0.7.4-alpha.7 +# Current version: 0.8.0-alpha.1 ``` 更多 API 细节可参考 [Application.command()](/api/server/application#command) 部分。 diff --git a/docs/en-US/development/guide/events.md b/docs/zh-CN/development/server/events.md similarity index 60% rename from docs/en-US/development/guide/events.md rename to docs/zh-CN/development/server/events.md index 5c20293043..2b16a96b3d 100644 --- a/docs/en-US/development/guide/events.md +++ b/docs/zh-CN/development/server/events.md @@ -1,66 +1,58 @@ # 事件 -事件在很多插件化可扩展的框架和系统中都有应用,比如著名的 Wordpress,是比较广泛的对生命周期支持扩展的机制。 +NocoBase 在应用、插件、数据库的生命周期中提供了非常多的事件监听,这些方法只有在触发了事件之后才会执行。 -## 基础概念 +## 如何添加事件监听? -NocoBase 在应用生命周期中提供了一些钩子,以便在运行中的一些特殊时期根据需要进行扩展开发。 - -### 数据库事件 - -主要通过 `db.on()` 的方法定义,大部分事件兼容 Sequelize 原生的事件类型。例如需要在某张数据表创建一条数据后做一些事情时,可以使用 `.afterCreate` 事件: +事件的注册一般放于 afterAdd 或 beforeLoad 中 ```ts -// posts 表创建数据完成时触发 -db.on('posts.afterCreate', async (post, options) => { - console.log(post); -}); +export class MyPlugin extends Plugin { + // 插件添加进来之后,有没有激活都执行 afterAdd() + afterAdd() { + this.app.on(); + this.db.on(); + } + + // 只有插件激活之后才会执行 beforeLoad() + beforeLoad() { + this.app.on(); + this.db.on(); + } +} ``` -由于 Sequelize 默认的单条数据创建成功触发的时间点上并未完成与该条数据所有关联数据的处理,所以 NocoBase 针对默认封装的 Repository 数据仓库类完成数据创建和更新操作时,扩展了几个额外的事件,代表关联数据被一并操作完成: +### `db.on` -```ts -// 已创建且已根据创建数据完成关联数据创建或更新完成时触发 -db.on('posts.afterCreateWithAssociations', async (post, options) => { - console.log(post); -}); -``` +数据库相关事件与 Collection 配置、Repository 的 CRUD 相关,包括: -与 Sequelize 同样的也可以针对全局的数据处理都定义特定的事件: +- 'beforeSync' / 'afterSync' +- 'beforeValidate' / 'afterValidate' +- 'beforeCreate' / 'afterCreate' +- 'beforeUpdate' / 'afterUpdate' +- 'beforeSave' / 'afterSave' +- 'beforeDestroy' / 'afterDestroy' +- 'afterCreateWithAssociations' +- 'afterUpdateWithAssociations' +- 'afterSaveWithAssociations' +- 'beforeDefineCollection' +- 'afterDefineCollection' +- 'beforeRemoveCollection' / 'afterRemoveCollection -```ts -// 每张表创建数据完成都触发 -db.on('beforeCreate', async (model, options) => { - console.log(model); -}); -``` +更多详情参考 [Database API](/api/database#内置事件)。 -针对特殊的生命周期比如定义数据表等,NocoBase 也扩展了相应事件: +### `app.on()` -```ts -// 定义任意数据表之前触发 -db.on('beforeDefineCollection', (collection) => { - collection.options.tableName = 'somePrefix' + collection.options.tableName; -}); -``` +app 的事件与应用的生命周期相关,相关事件有: -其他所有可用的数据库事件类型可以参考 [Database API](/api/database#on)。 +- 'beforeLoad' / 'afterLoad' +- 'beforeInstall' / 'afterInstall' +- 'beforeUpgrade' / 'afterUpgrade' +- 'beforeStart' / 'afterStart' +- 'beforeStop' / 'afterStop' +- 'beforeDestroy' / 'afterDestroy' -### 应用级事件 - -在某些特殊需求时,会需要在应用的外层生命周期中定义事件进行扩展,比如当应用启动前做一些准备操作,当应用停止前做一些清理操作等: - -```ts -app.on('beforeStart', async () => { - console.log('app is starting...'); -}); - -app.on('beforeStop', async () => { - console.log('app is stopping...'); -}); -``` - -其他所有可用的应用级事件类型可以参考 [Application API](/api/server/application#事件)。 +更多详情参考 [Application API](/api/server/application#事件)。 ## 示例 @@ -72,7 +64,7 @@ app.on('beforeStop', async () => { ```ts class ShopPlugin extends Plugin { - load() { + beforeLoad() { this.db.on('orders.afterCreate', async (order, options) => { const product = await order.getProduct({ transaction: options.transaction diff --git a/docs/zh-CN/development/server/i18n.md b/docs/zh-CN/development/server/i18n.md new file mode 100644 index 0000000000..80fa6398fd --- /dev/null +++ b/docs/zh-CN/development/server/i18n.md @@ -0,0 +1,134 @@ +# 国际化 + +NocoBase 国际化基于 [i18next](https://npmjs.com/package/i18next) 实现。 + +## 如何注册多语言包? + +```ts +export class MyPlugin extends Plugin { + load() { + this.app.i18n.addResources('zh-CN', 'test', { + Hello: '你好', + World: '世界', + }); + this.app.i18n.addResources('en-US', 'test', { + Hello: 'Hello', + World: 'World', + }); + } +} +``` + +## 两个 i18n 实例 + +### app.i18n + +全局 i18n 实例,一般用于 CLI 中国。 + +```ts +app.i18n.t('World') // “世界”或“World” +``` + +### ctx.i18n + +全局 i18n 的 cloneInstance,每个请求的 context 完全独立,通常用于根据客户端语言响应多语言信息。 + +```ts +app.use(async (ctx, next) => { + ctx.body = `${ctx.i18n.t('Hello')} ${ctx.i18n.t('World')}`; + await next(); +}); +``` + +客户端请求参数可以放在 query string 里 + +```bash +GET /?locale=en-US HTTP/1.1 +Host: localhost:13000 +``` + +或放在 request headers 里 + +```bash +GET / HTTP/1.1 +Host: localhost:13000 +X-Locale: en-US +``` + +## 建议配置 + +以英文文案为 key,翻译为 value,这样的好处,即使多语言缺失,也会以英文显示,不会造成阅读障碍,如: + +```ts +i18n.addResources('zh-CN', 'your-namespace', { + 'Show dialog': '显示对话框', + 'Hide dialog': '隐藏对话框' +}); +``` + +为了更方便管理多语言文件,推荐在插件中创建一个 `locale` 文件夹,并把对应语言文件都放置在其中方便管理: + +```bash +|- /my-plugin + |- /src + |- /server + |- locale # 多语言文件夹 + |- zh-CN.ts + |- en-US.ts +``` + +## 示例 + +### 服务端错误提示 + +例如用户在店铺对某个商品下单时,如果商品的库存不够,或者未上架,那么下单接口被调用时,应该返回相应的错误。 + +```ts +const namespace = 'shop'; + +export default class ShopPlugin extends Plugin { + async load() { + this.app.i18n.addResources('zh-CN', namespace, { + 'No such product': '商品不存在', + 'Product not on sale': '商品已下架', + 'Out of stock': '库存不足', + }); + + this.app.resource({ + name: 'orders', + actions: { + async create(ctx, next) { + const productRepo = ctx.db.getRepository('products'); + const product = await productRepo.findOne({ + filterByTk: ctx.action.params.values.productId + }); + + if (!product) { + return ctx.throw(404, ctx.t('No such product')); + } + + if (!product.enabled) { + return ctx.throw(400, ctx.t('Product not on sale')); + } + + if (!product.inventory) { + return ctx.throw(400, ctx.t('Out of stock')); + } + + const orderRepo = ctx.db.getRepository('orders'); + ctx.body = await orderRepo.create({ + values: { + productId: product.id, + quantity: 1, + totalPrice: product.price, + userId: ctx.state.currentUser.id + } + }); + + next(); + } + } + }); + } +} +``` diff --git a/docs/zh-CN/development/server/index.md b/docs/zh-CN/development/server/index.md new file mode 100644 index 0000000000..988b108554 --- /dev/null +++ b/docs/zh-CN/development/server/index.md @@ -0,0 +1,75 @@ +# 概述 + +初始化的空插件,服务端相关目录结构如下: + +```bash +|- /my-plugin + |- /src + |- /server # 插件服务端代码 + |- plugin.ts # 插件类 + |- index.ts # 服务端入口 + |- server.d.ts + |- server.js +``` + +`plugin.ts` 提供了插件生命周期的各种方法的调用 + +```ts +import { InstallOptions, Plugin } from '@nocobase/server'; + +export class MyPlugin extends Plugin { + afterAdd() { + // 插件 pm.add 注册进来之后,主要用于放置 app beforeLoad 事件的监听 + } + beforeLoad() { + // 自定义类或方法 + this.db.registerFields(); + this.db.registerFields(); + this.db.registerFields(); + this.db.registerFields(); + this.db.registerFieldTypes() + this.db.registerModels() + this.db.registerRepositories() + this.db.registerOperators() + // 事件监听 + this.app.on(); + this.db.on(); + } + async load() { + // 定义 collection + this.db.collection(); + // 导入 collection + this.db.import(); + this.db.addMigrations(); + + // 定义 resource + this.resourcer.define(); + // resource action + this.resourcer.registerActions(); + + // 注册 middleware + this.resourcer.use(); + this.acl.use(); + this.app.use(); + + // 自定义多语言包 + this.app.i18n.addResources(); + // 自定义命令行 + this.app.command(); + } + async install(options?: InstallOptions) { + // 安装逻辑 + } + async afterEnable() { + // 激活之后 + } + async afterDisable() { + // 禁用之后 + } + async remove() { + // 删除逻辑 + } +} + +export default MyPlugin; +``` diff --git a/docs/zh-CN/development/guide/middleware.md b/docs/zh-CN/development/server/middleware.md similarity index 92% rename from docs/zh-CN/development/guide/middleware.md rename to docs/zh-CN/development/server/middleware.md index fbb4ecc9f2..560c2cbef1 100644 --- a/docs/zh-CN/development/guide/middleware.md +++ b/docs/zh-CN/development/server/middleware.md @@ -1,6 +1,20 @@ # 中间件 -## 添加方法 +## 如何注册中间件? + +中间件的注册方法一般写在 load 方法里 + +```ts +export class MyPlugin extends Plugin { + load() { + this.app.acl.use(); + this.app.resourcer.use(); + this.app.use(); + } +} +``` + +说明: 1. `app.acl.use()` 添加资源权限级中间件,在权限判断之前执行 2. `app.resourcer.use()` 添加资源级中间件,只有请求已定义的 resource 时才执行 @@ -141,8 +155,6 @@ app.resourcer.use(async (ctx, next) => { 待补充 -## 完整示例 - -待补充 +## 示例 - [samples/ratelimit](https://github.com/nocobase/nocobase/blob/main/packages/samples/ratelimit/) IP rate-limiting diff --git a/docs/zh-CN/development/server/migration.md b/docs/zh-CN/development/server/migration.md new file mode 100644 index 0000000000..1ec0a81397 --- /dev/null +++ b/docs/zh-CN/development/server/migration.md @@ -0,0 +1,50 @@ +# 数据库迁移 + +插件在更新迭代过程中,可能会出现某些不兼容的改动,这些不兼容的升级脚本可以通过编写 migration 文件来处理。 + +## 如何添加迁移文件? + +```ts +export class MyPlugin extends Plugin { + load() { + // 加载单个 Migration 文件 + this.db.addMigration(); + // 加载多个 Migration 文件 + this.db.addMigrations(); + } +} +``` + +API 参考: + +- [db.addMigration()](/api/database#addmigration) +- [db.addMigrations()](/api/database#addmigrations) + +## 什么时候执行? + +```bash +# app 升级时,会执行 migrator.up() 和 db.sync() +yarn nocobase upgrade +# 单独触发 migration +yarn nocobase migrator up +``` + +## 什么时候需要写 migration 文件? + +一般用于升级过程中,存于数据库的系统配置的更新。如果只是 collection 配置的变更,无需配置 migration,直接执行 `yarn nocobase db:sync` 就可以同步给数据库了。 + +## Migration 文件 + +```ts +import { Migration } from '@nocobase/server'; + +export default class CustomMigration extends Migration { + async up() { + // + } + + async down() { + // + } +} +``` diff --git a/docs/zh-CN/development/guide/resources-actions.md b/docs/zh-CN/development/server/resources-actions.md similarity index 100% rename from docs/zh-CN/development/guide/resources-actions.md rename to docs/zh-CN/development/server/resources-actions.md diff --git a/docs/zh-CN/development/server/test.md b/docs/zh-CN/development/server/test.md new file mode 100644 index 0000000000..aedff9d325 --- /dev/null +++ b/docs/zh-CN/development/server/test.md @@ -0,0 +1,162 @@ +# 测试 + +测试基于 [Jest](https://jestjs.io/) 测试框架。为了方便的编写测试,提供了 `mockDatabase()` 和 `mockServer()` 用于数据库和服务端应用的测试。 + +## `mockDatabase()` + +默认提供一种完全隔离的 db 测试环境 + +```ts +import { mockDatabase } from '@nocobase/test'; + +describe('my db suite', () => { + let db; + + beforeEach(async () => { + db = mockDatabase(); + db.collection({ + name: 'posts', + fields: [ + { + type: 'string', + name: 'title', + } + ] + }); + await db.sync(); + }); + + afterEach(async () => { + await db.close(); + }); + + test('my case', async () => { + const repository = db.getRepository('posts'); + const post = await repository.create({ + values: { + title: 'hello' + } + }); + + expect(post.get('title')).toEqual('hello'); + }); +}); +``` + +## `mockServer()` + +提供模拟的服务端应用实例,对应的 app.db 为 `mockDatabase()` 实例,同时还提供了便捷的 `app.agent()` 用于测试 HTTP API,针对 NocoBase 的 Resource Action 还封装了 `app.agent().resource()` 用于测试资源的 Action。 + +```ts +import { mockServer } from '@nocobase/test'; + +class MyPlugin extends Plugin { + +} + +describe('my suite', () => { + let app; + let agent; + + beforeEach(async () => { + app = mockServer(); + agent = app.agent(); + // 添加待注册的插件 + app.plugin(MyPlugin, { name: 'my-plugin' }); + // 加载配置 + app.load(); + // 清空数据库并安装 + app.install({ clean: true }); + }); + + afterEach(async () => { + await app.destroy(); + }); + + test('my case', async () => { + await agent.resource('posts').create({ + values: { + title: 'hello' + } + }); + await agent.get('/users:check').set({ Authorization: 'Bearer abc' }); + }); +}); +``` + +## 示例 + +我们以之前在 [资源与操作](development/guide/resources-actions) 章节的功能为例,来写一个插件的测试: + +```ts +import { mockServer } from '@nocobase/test'; +import Plugin from '../../src/server'; + +describe('shop actions', () => { + let app; + let agent; + let db; + + beforeEach(async () => { + app = mockServer(); + app.plugin(Plugin); + agent = app.agent(); + db = app.db; + + await app.load(); + await db.sync(); + }); + + afterEach(async () => { + await app.destroy(); + }); + + test('product order case', async () => { + const { body: product } = await agent.resource('products').create({ + values: { + title: 'iPhone 14 Pro', + price: 7999, + enabled: true, + inventory: 1 + } + }); + expect(product.data.price).toEqual(7999); + + const { body: order } = await agent.resource('orders').create({ + values: { + productId: product.data.id + } + }); + expect(order.data.totalPrice).toEqual(7999); + expect(order.data.status).toEqual(0); + + const { body: deliveredOrder } = await agent.resource('orders').deliver({ + filterByTk: order.data.id, + values: { + provider: 'SF', + trackingNumber: '123456789' + } + }); + expect(deliveredOrder.data.status).toBe(2); + expect(deliveredOrder.data.delivery.trackingNumber).toBe('123456789'); + }); +}); +``` + +编写完成后,在命令行中允许测试命令: + +```bash +yarn test packages/samples/shop-actions +``` + +该测试将验证: + +1. 商品可以创建成功; +2. 订单可以创建成功; +3. 订单可以发货成功; + +当然这只是个最基本的例子,从业务上来说并不完善,但作为示例已经可以说明整个测试的流程。 + +## 小结 + +本章涉及的示例代码整合在对应的包 [packages/samples/shop-actions](https://github.com/nocobase/nocobase/tree/main/packages/samples/shop-actions) 中,可以直接在本地运行,查看效果。 diff --git a/docs/zh-CN/manual/quick-start/the-first-app.md b/docs/zh-CN/manual/quick-start/the-first-app.md index f3b0e16c25..af30f0bbff 100755 --- a/docs/zh-CN/manual/quick-start/the-first-app.md +++ b/docs/zh-CN/manual/quick-start/the-first-app.md @@ -56,17 +56,8 @@ 在图形化的数据表里,可以很直观的看出各个表之间的关系。(注:Graph-collection 插件暂未开源) -🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎 +![graph-collection.jpg](./the-first-app/graph-collection.jpg) -🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎 - -🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎 - -🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎 - -🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎 - -🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎 将数据表和字段创建完成后,我们开始制作界面。 diff --git a/docs/zh-CN/manual/quick-start/the-first-app/graph-collection.jpg b/docs/zh-CN/manual/quick-start/the-first-app/graph-collection.jpg new file mode 100644 index 0000000000..d2488b29f7 Binary files /dev/null and b/docs/zh-CN/manual/quick-start/the-first-app/graph-collection.jpg differ diff --git a/docs/zh-CN/welcome/getting-started/deployment.md b/docs/zh-CN/welcome/getting-started/deployment.md deleted file mode 100644 index 7ae584f53f..0000000000 --- a/docs/zh-CN/welcome/getting-started/deployment.md +++ /dev/null @@ -1,2 +0,0 @@ -# 部署 - diff --git a/docs/zh-CN/welcome/release/index.md b/docs/zh-CN/welcome/release/index.md new file mode 100644 index 0000000000..1ab49709f1 --- /dev/null +++ b/docs/zh-CN/welcome/release/index.md @@ -0,0 +1 @@ +# 更新日志 \ No newline at end of file diff --git a/docs/zh-CN/welcome/release/v08-changelog.md b/docs/zh-CN/welcome/release/v08-changelog.md new file mode 100644 index 0000000000..c32fc777d2 --- /dev/null +++ b/docs/zh-CN/welcome/release/v08-changelog.md @@ -0,0 +1,216 @@ +# v0.8:插件管理器和文档 + +从 v0.8 开始,NocoBase 开始提供可用的插件管理器和开发文档。以下是 v0.8 的主要变化。 + +## 界面右上角的调整 + +- 界面配置 +- 插件管理器 +- 配置中心 +- 个人中心 + + + +## 全新的插件管理器 + +v0.8 提供了强大的插件管理器用于无代码的方式管理插件。 + +### 插件管理器流程 + + + +### 插件管理器界面 + +目前主要用于本地插件的禁用、激活和删除。内置插件不能删除,插件市场敬请期待。 + + + +### 插件管理器命令行 + +除了可以在无代码界面激活、禁用插件,也可以通过命令行更完整的管理插件。 + +```bash +# 创建插件 +yarn pm create hello +# 注册插件 +yarn pm add hello +# 激活插件 +yarn pm enable hello +# 禁用插件 +yarn pm disable hello +# 删除插件 +yarn pm remove hello +``` + +备注:插件的发布和升级会在后续的版本里支持。 + +```bash +# 发布插件 +yarn pm publish hello +# 升级插件 +yarn pm upgrade hello +``` + +更多插件示例,查看 [packages/samples](https://github.com/nocobase/nocobase/tree/main/packages/samples)。 + +## 插件的变化 + +### 插件目录结构 + +```bash +|- /hello + |- /src + |- /client # 插件客户端代码 + |- /server # 插件服务端代码 + |- client.d.ts + |- client.js + |- package.json # 插件包信息 + |- server.d.ts + |- server.js +``` + +### 插件名称规范 + +NocoBase 插件也是 NPM 包,插件名和 NPM 包名的对应规则为 `${PLUGIN_PACKAGE_PREFIX}-${pluginName}`。 + +`PLUGIN_PACKAGE_PREFIX` 为插件包前缀,可以在 .env 里自定义,[点此查看 PLUGIN_PACKAGE_PREFIX 说明](/api/env#plugin_package_prefix)。 + +例如,有一名为 `my-nocobase-app` 的项目,新增了 `hello` 插件,包名为 `@my-nocobase-app/plugin-hello`。 + +PLUGIN_PACKAGE_PREFIX 配置如下: + +```bash +PLUGIN_PACKAGE_PREFIX=@nocobase/plugin-,@nocobase/preset-,@my-nocobase-app/plugin- +``` + +插件名和包名的对应关系为: + +- `users` 插件包名为 `@nocobase/plugin-users` +- `nocobase` 插件包名为 `@nocobase/preset-nocobase` +- `hello` 插件包名为 `@my-nocobase-app/plugin-hello` + +### 插件的生命周期 + +v0.8 提供了更完整的插件生命周期方法 + +```ts +import { InstallOptions, Plugin } from '@nocobase/server'; + +export class HelloPlugin extends Plugin { + afterAdd() { + // 插件通过 pm.add 添加之后 + } + + beforeLoad() { + // 所有插件执行 load 之前,一般用于注册类和事件监听 + } + + async load() { + // 加载配置 + } + + async install(options?: InstallOptions) { + // 安装逻辑 + } + + async afterEnable() { + // 激活之后 + } + + async afterDisable() { + // 禁用之后 + } + + async remove() { + // 删除逻辑 + } +} + +export default HelloPlugin; +``` + +### 插件的前后端入口 + +插件的生命周期由服务端控制 + +```ts +import { Application } from '@nocobase/server'; + +const app = new Application({ + // ... +}); + +class MyPlugin extends Plugin { + afterAdd() {} + beforeLoad() {} + load() {} + install() {} + afterEnable() {} + afterDisable() {} + remove() {} +} + +app.plugin(MyPlugin, { name: 'my-plugin' }); +``` + +插件的客户端以 Context.Provider 形式存在(类似于服务端的 Middleware) + +```tsx | pure +import React from 'react'; +import { Application } from '@nocobase/client'; + +const app = new Application({ + apiClient: { + baseURL: process.env.API_BASE_URL, + }, + dynamicImport: (name: string) => { + return import(`../plugins/${name}`); + }, +}); + +// 访问 /hello 页面时,显示 Hello world! +const HelloProvider = React.memo((props) => { + const location = useLocation(); + if (location.pathname === '/hello') { + return
Hello world!
+ } + return <>{props.children} +}); + +app.use(HelloProvider); +``` + +## 自定义的业务代码 + +v0.7 的插件并不完整,自定义的业务代码可能分散在 `packages/app/client` 和 `packages/app/server` 里,不利于升级、维护。v0.8 推荐以插件包的形式整理,并使用 `yarn pm` 来管理插件。 + +## 提供了更完整的文档 + +- **欢迎**:快速了解 NocoBase +- **用户使用手册**:进一步了解 NocoBase 平台提供的核心功能 +- **插件开发教程**:进阶深入插件开发 +- **API 参考**:插件开发过程中,查阅各 API 用法 +- **客户端组件库**(正在准备中):提供 NocoBase 各组件的示例和用法 + +备注:文档还有很多细节待补充,也会根据大家进一步反馈,继续调整。 + +## 提供了更多插件示例 + +- [command](https://github.com/nocobase/nocobase/tree/develop/packages/samples/command "command") +- [custom-block](https://github.com/nocobase/nocobase/tree/develop/packages/samples/custom-block "custom-block") +- [custom-page](https://github.com/nocobase/nocobase/tree/develop/packages/samples/custom-page "custom-page") +- [custom-signup-page](https://github.com/nocobase/nocobase/tree/develop/packages/samples/custom-signup-page "custom-signup-page") +- [hello](https://github.com/nocobase/nocobase/tree/develop/packages/samples/hello "hello") +- [ratelimit](https://github.com/nocobase/nocobase/tree/develop/packages/samples/ratelimit "ratelimit") +- [shop-actions](https://github.com/nocobase/nocobase/tree/develop/packages/samples/shop-actions "shop-actions") +- [shop-events](https://github.com/nocobase/nocobase/tree/develop/packages/samples/shop-events "shop-events") +- [shop-i18n](https://github.com/nocobase/nocobase/tree/develop/packages/samples/shop-i18n "shop-i18n") +- [shop-modeling](https://github.com/nocobase/nocobase/tree/develop/packages/samples/shop-modeling "shop-modeling") + +## 其他新特性和功能 + +- 导入 +- 批量更新 & 编辑 +- 图形化数据表配置 +- 工作流支持查看执行历史 +- JSON 字段 diff --git a/docs/zh-CN/development/pm-flow.svg b/docs/zh-CN/welcome/release/v08-changelog/pm-flow.svg similarity index 100% rename from docs/zh-CN/development/pm-flow.svg rename to docs/zh-CN/welcome/release/v08-changelog/pm-flow.svg diff --git a/docs/zh-CN/welcome/release/v08-changelog/pm-ui.jpg b/docs/zh-CN/welcome/release/v08-changelog/pm-ui.jpg new file mode 100644 index 0000000000..4c8fdd3c1a Binary files /dev/null and b/docs/zh-CN/welcome/release/v08-changelog/pm-ui.jpg differ diff --git a/docs/zh-CN/welcome/release/v08-changelog/topright.jpg b/docs/zh-CN/welcome/release/v08-changelog/topright.jpg new file mode 100644 index 0000000000..8ce5109730 Binary files /dev/null and b/docs/zh-CN/welcome/release/v08-changelog/topright.jpg differ