diff --git a/.dumi/theme/builtins/Redirect.tsx b/.dumi/theme/builtins/Redirect.tsx new file mode 100644 index 0000000000..9b2201ac06 --- /dev/null +++ b/.dumi/theme/builtins/Redirect.tsx @@ -0,0 +1,5 @@ +import React, { Redirect } from 'umi'; + +export default (props: any) => { + return +}; \ No newline at end of file diff --git a/.dumi/theme/layout.tsx b/.dumi/theme/layout.tsx index 49b2740617..93ccfcabd3 100755 --- a/.dumi/theme/layout.tsx +++ b/.dumi/theme/layout.tsx @@ -79,6 +79,9 @@ const Layout: React.FC = ({ children, location }) => { setMenuCollapsed(true); }} > +
+ {isCN ? '文档还在建设中,与实际代码存在差异' : 'The documentation is still under construction and differs somewhat from the actual code.'} +
} diff --git a/.dumi/theme/style/layout.less b/.dumi/theme/style/layout.less index 0cfec6bd6d..223deac51b 100755 --- a/.dumi/theme/style/layout.less +++ b/.dumi/theme/style/layout.less @@ -19,9 +19,12 @@ body { padding: 16px (@s-content-margin + @s-toc-width) 50px @s-menu-width + @s-content-margin; @media @mobile { - padding-top: 66px !important; + padding-top: 98px !important; padding-left: 16px !important; padding-right: 16px !important; + .tip { + top: 50px !important; + } } &[data-gapless='true'] { diff --git a/.dumi/theme/style/markdown.less b/.dumi/theme/style/markdown.less index 803b7d66d8..cae5e91565 100755 --- a/.dumi/theme/style/markdown.less +++ b/.dumi/theme/style/markdown.less @@ -3,7 +3,8 @@ .markdown { color: @c-text; font-size: 15px; - line-height: 1.60625; + // line-height: 1.60625; + line-height: 2; &:not(:first-child):empty { min-height: 32px; diff --git a/.github/workflows/vercel.yml b/.github/workflows/vercel.yml index d24a35bdff..39f019e76a 100644 --- a/.github/workflows/vercel.yml +++ b/.github/workflows/vercel.yml @@ -4,6 +4,7 @@ on: push: branches: - develop + - feature/docs jobs: deploy: diff --git a/.umirc.ts b/.umirc.ts index ceb18e6dcf..aef216ce2d 100644 --- a/.umirc.ts +++ b/.umirc.ts @@ -16,21 +16,51 @@ export default defineConfig({ 'pathRewrite': { '^/api/': '/api/' }, }, }, + resolve: { + includes: ['docs'], + }, + styles: [ + ` + .__dumi-default-navbar-logo { + height: 100%; + width: 100px; + } + .__dumi-default-layout .__dumi-default-navbar { + box-shadow: 0 0 3px rgb(60 72 88 / 15%); + } + .__dumi-default-layout[data-site-mode='true'] { + padding-top: 150px !important; + } + @media only screen and (max-width: 767px) { + .__dumi-default-layout[data-site-mode="true"] { + padding-top: 128px !important; + } + } + @media only screen and (min-width: 768px) { + .__dumi-default-menu[data-mode='site'] { + top: 100px !important; + } + .__dumi-default-layout[data-site-mode='true'] .__dumi-default-layout-toc { + top: 150px !important; + } + } + ` + ], // mfsu: {}, // ssr: {}, // exportStatic: {}, - mode: 'doc', - logo: 'https://www.nocobase.com/dist/images/logo.png', + mode: 'site', + logo: 'https://www.nocobase.com/images/logo.png', navs: { 'en-US': [ null, { title: 'GitHub', path: 'https://github.com/nocobase/nocobase' }, - { title: 'Changelog', path: 'https://github.com/nocobase/nocobase/releases' }, + // { title: 'Changelog', path: 'https://github.com/nocobase/nocobase/releases' }, ], 'zh-CN': [ null, { title: 'GitHub', path: 'https://github.com/nocobase/nocobase' }, - { title: '更新日志', path: 'https://github.com/nocobase/nocobase/releases' }, + // { title: '更新日志', path: 'https://github.com/nocobase/nocobase/releases' }, ], }, // more config: https://d.umijs.org/config diff --git a/docs/api/action.md b/docs/api/action.md new file mode 100644 index 0000000000..9b2348e23c --- /dev/null +++ b/docs/api/action.md @@ -0,0 +1,10 @@ +--- +toc: menu +--- + +# Action + +## action.params + +## action.mergeParams() + diff --git a/docs/api/application.md b/docs/api/application.md new file mode 100644 index 0000000000..5e1fcc812c --- /dev/null +++ b/docs/api/application.md @@ -0,0 +1,80 @@ +--- +nav: + title: API + order: 3 +toc: menu +--- + +# Application + +## app.db + +数据库实例 + +## app.resourcer + +资源实例 + +## app.pm + +插件管理器 + +## app.i18n + +国际化 + +## app.constructor() + +构造器 + +## app.use() + +中间件 + +## app.on() + +事件 + +## app.emit() + +## app.emitAsync() + +## app.collection() + +等同于 app.db.collection() + +## app.actions() + +等同于 app.resourcer.registerActions() + +## app.resource() + +等同于 app.resourcer.define() + +## app.parse() + +等同于 app.cli.parse() + +## app.load() + +加载配置 + +## app.init() + +初始化 + +## app.start() + +启动应用 + +## app.stop() + +停止应用 + +## app.command() + +等同于 app.cli.command() + +## app.plugin() + +等同于 app.pm.add() \ No newline at end of file diff --git a/docs/api/cli.md b/docs/api/cli.md new file mode 100644 index 0000000000..ce60864309 --- /dev/null +++ b/docs/api/cli.md @@ -0,0 +1,13 @@ +--- +toc: menu +--- + +# CLI + +## init +## start +## db:sync +## pm:download +## pm:enable +## pm:disable +## pm:remove diff --git a/docs/api/client.md b/docs/api/client.md new file mode 100644 index 0000000000..e845d83561 --- /dev/null +++ b/docs/api/client.md @@ -0,0 +1,16 @@ +--- +toc: menu +--- + +# Client + +## APIClient + +## createRouteSwitch + +## createCollectionField + +## createSchemaComponent + +## i18n + diff --git a/docs/api/collection.md b/docs/api/collection.md new file mode 100644 index 0000000000..5a71d32dc7 --- /dev/null +++ b/docs/api/collection.md @@ -0,0 +1,17 @@ +--- +toc: menu +--- + +# Collection + +## collection.constructor() +## collection.mergeOptions() +## collection.sync() +## collection.hasField() +## collection.getField() +## collection.addField() +## collection.setFields() +## collection.mergeField() +## collection.removeField() +## collection.forEachField() +## collection.findField() \ No newline at end of file diff --git a/docs/api/context.md b/docs/api/context.md new file mode 100644 index 0000000000..82b41dd76c --- /dev/null +++ b/docs/api/context.md @@ -0,0 +1,16 @@ +--- +toc: menu +--- + +# Context + +## ctx.db + +## ctx.resourcer + +## ctx.action + +## ctx.i18n + +## ctx.t() + diff --git a/docs/api/database.md b/docs/api/database.md new file mode 100644 index 0000000000..06dc10c1a0 --- /dev/null +++ b/docs/api/database.md @@ -0,0 +1,18 @@ +--- +toc: menu +--- + +# Database + +## db.constructor() +## db.collection() +## db.sync() +## db.close() +## db.registerFieldTypes() +## db.import() +## db.on() +## db.emitAsync() +## db.emit() +## db.registerModels() +## db.registerRepositories() +## db.registerOperators() diff --git a/docs/api/field-types.md b/docs/api/field-types.md new file mode 100644 index 0000000000..7408d98152 --- /dev/null +++ b/docs/api/field-types.md @@ -0,0 +1,30 @@ +--- +toc: menu +--- + +# Field Types + +## Field - abstract +## RelationField - abstract +## HasOneField +## HasManyField +## BelongsToField +## BelongsToManyField +## StringField +## TextField +## IntegerField +## FloatField +## DoubleField +## RealField +## DecimalField +## DateField +## TimeField +## JsonField +## JsonbField +## VirtualField +## SortField +## PasswordField +## RadioField +## UidField +## CreatedByField +## UpdatedByField diff --git a/docs/api/operators.md b/docs/api/operators.md new file mode 100644 index 0000000000..3888e2c401 --- /dev/null +++ b/docs/api/operators.md @@ -0,0 +1,50 @@ +--- +toc: menu +--- + +# Operators + +## string +- includes +- notIncludes +- eq +- ne +- null +- notNull +## number +- eq +- ne +- gt +- gte +- lt +- lte +- between +- null +- notNull +## select +- eq +- ne +- in +- notIn +- null +- notNull +## multipleSelect +- match +- notMatch +- anyOf +- noneOf +- null +- notNull +## date +- dateOn +- dateNotOn +- dateBefore +- dateAfter +- dateNotBefore +- dateNotAfter +- null +- notNull +## association +- fieldName.$name +- exists +- notExists \ No newline at end of file diff --git a/docs/api/plugin-manager.md b/docs/api/plugin-manager.md new file mode 100644 index 0000000000..f3d7c64b9e --- /dev/null +++ b/docs/api/plugin-manager.md @@ -0,0 +1,19 @@ +--- +toc: menu +--- + +# PluginManager + +## pm.constructor() + +## pm.forEach() + +## pm.load() + +## pm.download() + +## pm.enable() + +## pm.disable() + +## pm.remove() \ No newline at end of file diff --git a/docs/api/plugin.md b/docs/api/plugin.md new file mode 100644 index 0000000000..6c47a9ef93 --- /dev/null +++ b/docs/api/plugin.md @@ -0,0 +1,11 @@ +--- +toc: menu +--- + +# Plugin + +## plugin.constructor() +## plugin.enable() +## plugin.disable() +## plugin.upgrade() +## plugin.remove() \ No newline at end of file diff --git a/docs/api/repository.md b/docs/api/repository.md new file mode 100644 index 0000000000..79b0aead63 --- /dev/null +++ b/docs/api/repository.md @@ -0,0 +1,370 @@ +--- +toc: menu +--- + +# Repository + +## `repository.findMany()` + +查询数据,返回数组。无数据时为空数组,不返回 count。如果需要,请使用 [repository.paginate()](#repositorypaginate) 方法。 + +##### Definition + +```ts +interface findMany { + (options: FindManyOptions): Promise +} + +interface FindManyOptions extends Sequelize.FindOptions { + // 数据过滤 + filter?: FilterOptions; + // 输出结果显示哪些字段 + fields?: string[]; + // 输出结果不显示哪些字段 + expect?: string[]; + // 附加字段,用于控制关系字段的输出 + appends?: string[]; + // 排序,字段前面加上 “-” 表示降序 + sort?: string[]; +} + +// 待补充 +type FilterOptions = any; +``` + +##### Examples + +###### 全览 + +```ts +repository.findMany({ + // 过滤 + filter: { + $and: [{ a: 5 }, { b: 6 }], // (a = 5) AND (b = 6) + $or: [{ a: 5 }, { b: 6 }], // (a = 5) OR (b = 6) + someAttribute: { + // Basics + $eq: 3, // = 3 + $ne: 20, // != 20 + $is: null, // IS NULL + $not: true, // IS NOT TRUE + $gt: 6, // > 6 + $gte: 6, + }, + // 支持使用逗号间隔 + 'someAttribute.$eq': 3, + // 内嵌的,一般是关系数据 + nested: { + someAttribute: {}, + }, + // 同上,也支持使用逗号间隔 + 'nested.someAttribute': { + // 同上 + }, + }, + // 字段白名单 + fields: [], + // 附加字段,主要用于附加关系字段 + appends: [], + // 字段黑名单 + expect: [], + // 排序 + sort: ['-createdAt', 'updatedAt'], +}); +``` + +###### filter 参数示例说明 + +以文章和标签为例,文章和标签的 collection 如下: + +```ts +const Tag = db.collection({ + name: 'tags', + fields: [ + { type: 'string', name: 'name' }, + ], +}); + +const Post = db.collection({ + name: 'posts', + fields: [ + { type: 'string', name: 'name' }, + { type: 'text', name: 'content' }, + { type: 'belongsToMany', name: 'tags' }, + ], +}); +``` + +最简单的筛选过滤 + +```ts +await Post.repository.findMany({ + filter: { + 'name': 'post1', + }, +}); +``` + +支持多种 Operators,以 `$` 开头。[更多内容,查阅 Operators 章节](operators.md) + +```ts +await Post.repository.findMany({ + filter: { + 'name.$includes': 'post1', + // 等同于 + name: { + $includes: 'post1', + }, + }, +}); +``` + +支持关系字段过滤,可以使用 dot 来表示层级结构 + +```ts +await Post.repository.findMany({ + filter: { + 'tags.name': 'tag1', + // 等同于 + tags: { + name: 'tag1', + }, + }, +}); +``` + +多个同一关系字段的过滤可以写在一起 + +```ts +await Post.repository.findMany({ + filter: { + 'tags': { + 'name.$includes': 'tag1', + 'createdAt.$dateOn': '2020-10-28', + }, + }, +}); +``` + +同时也支持 and、or 逻辑运算符 + +```ts +await Post.repository.findMany({ + filter: { + $and: [ + // 一个 Object 只写一个条件 + { name: 'post1' }, + // 支持关系字段(非常重要) + { 'tags.name.$includes': 'tag1' }, + { 'tags.name.$includes': 'tag2' }, + ], + }, +}); +``` + +###### sort 参数示例说明 + +指定一组数据的排序,倒序时在字段前加上减号 `-` + +```ts +await Post.repository.findMany({ + // 创建日期倒序 + sort: ['-createdAt'], +}); +``` + +可以设置多个排序规则 + +```ts +await Post.repository.findMany({ + // 创建日期倒序,ID 正序 + sort: ['-createdAt', 'id'], +}); +``` + +也可以是关系表的字段 + +```ts +await Post.repository.findMany({ + // 标签名正序,文章创建日期倒序 + sort: ['tags.name', '-createdAt'], +}); +``` + +###### fields 参数示例说明 + +- `fields` 显示哪些字段 +- `expect` 不显示哪些字段 +- `appends` 附加哪些字段 + +如果并未指定 fields,输出所有 Attributes,Associations 字段并不输出 + +```ts +await Post.repository.findMany(); +// [{ id, name, content, createdAt, updatedAt }] +``` + +只输出指定字段时,可以用 fields + +```ts +await Post.repository.findMany({ + fields: ['name'], +}); +// [{ name }] +``` + +当 fields 里有关系字段时,按默认情况输出 + +```ts +await Post.repository.findMany({ + fields: ['id', 'name', 'tags'], +}); +// +// [{ id, name, createdAt, updatedAt, tags: [{ id, name, createdAt, updatedAt }] }] + +``` + +可以只输出关系数据的某个字段 + +```ts +await Post.repository.findMany({ + fields: ['id', 'name', 'tags.name'], +}); +// [{ id, name, tags: [{ name }] }] +``` + +排除某些字段时,可以使用 expect + +```ts +await Post.repository.findMany({ + expect: ['content'], +}); +// [{ id, name, createdAt, updatedAt }] +``` + +Attributes 不变,只附加 Associations 进来时,可以使用 appends + +```ts +await Post.repository.findMany({ + appends: ['tags'], +}); +// [{ id, name, content, createdAt, updatedAt, tags: [{ id, name, createdAt, updatedAt }] }] +``` + +如果某个字段只用在 filter 里,但并没有出现在 fields 里,不应该被输出 + +```ts +await Post.repository.findMany({ + filter: { + 'tags.name': 'tag1', + }, +}); +// 输出所有的 Attributes,但不输出 tags +// [{ id, name, content, createdAt, updatedAt }] +``` + +如果某个字段只用在 sort 里,但并没有出现在 fields 里,也不应该被输出 + +```ts +await Post.repository.findMany({ + sort: ['-tags.createdAt'] +}); +// 输出所有的 Attributes,但不输出 tags +// [{ id, name, content, createdAt, updatedAt }] +``` + +## `repository.paginate()` + +按分页查询数据,并返回所有符合的数据总数。 + +##### Definition + +```ts +interface paginate { + (options: PaginateOptions): Promise<[ M[], number ]> +} + +interface PaginateOptions extends Sequelize.FindAndCountOptions { + // 数据过滤 + filter?: FilterOptions; + // 输出结果显示哪些字段 + fields?: string[]; + // 输出结果不显示哪些字段 + expect?: string[]; + // 附加字段,用于控制关系字段的输出 + appends?: string[]; + // 排序,字段前面加上 “-” 表示降序 + sort?: string[]; + // 当前页,默认为 1 + page?: number; + // 当前页最大数量,默认为 20 + pageSize?: number; +} +``` + +##### Examples + +大部分参数与 [repository.findMany()](#repositoryfindmany) 一致,所以这里只列举 page 和 pageSize 的例子。 + +不填写参数时,默认 page=1,pageSize=20。 + +```ts +repository.paginate(); +// [[{ id, name, content, createdAt, updatedAt }], 50] + +const [models, count] = await repository.paginate(); +``` + +## `repository.findOne()` + +##### Definition + +```ts +interface findOne { + (options: FindOneOptions): Promise<[ M[], number ]> +} + +interface FindOneOptions extends FindManyOptions { + // 数据过滤 + filter?: FilterOptions; + // 输出结果显示哪些字段 + fields?: string[]; + // 输出结果不显示哪些字段 + expect?: string[]; + // 附加字段,用于控制关系字段的输出 + appends?: string[]; + // 排序,字段前面加上 “-” 表示降序 + sort?: string[]; + // 通过 pk 过滤 + filterByPk?: number | string; +} +``` + +##### Examples + +大部分参数与 [repository.findMany()](#repositoryfindmany) 一致。这里只列举 filterByPk 的例子。 + +```ts +await repository.findOne({ + filterByPk: 1, + // 等同于 + filter: { + [Model.primaryKeyAttribute]: 1, + } +}); +``` + +## repository.create() +## repository.update() +## repository.destroy() +## repository.relation().of() + +### findMany() +### findOne() +### create() +### update() +### destroy() +### set() +### add() +### remove() +### toggle() diff --git a/docs/api/resourcer.md b/docs/api/resourcer.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/api/rest-api.md b/docs/api/rest-api.md new file mode 100644 index 0000000000..5a972f42f2 --- /dev/null +++ b/docs/api/rest-api.md @@ -0,0 +1 @@ +# REST API diff --git a/docs/components/collection-fields/attachment.md b/docs/components/collection-fields/attachment.md new file mode 100644 index 0000000000..67c539ad61 --- /dev/null +++ b/docs/components/collection-fields/attachment.md @@ -0,0 +1,6 @@ +--- +group: + title: Collection Fields + path: /components/collection-fields + order: 3 +--- \ No newline at end of file diff --git a/docs/components/collection-fields/attachment.zh-CN.md b/docs/components/collection-fields/attachment.zh-CN.md new file mode 100644 index 0000000000..61e2e596e9 --- /dev/null +++ b/docs/components/collection-fields/attachment.zh-CN.md @@ -0,0 +1,6 @@ +--- +group: + title: 字段组件 + path: /zh-CN/components/collection-fields + order: 3 +--- diff --git a/docs/components/collection-fields/checkbox-group.md b/docs/components/collection-fields/checkbox-group.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/collection-fields/checkbox.md b/docs/components/collection-fields/checkbox.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/collection-fields/china-region.md b/docs/components/collection-fields/china-region.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/collection-fields/created-at.md b/docs/components/collection-fields/created-at.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/collection-fields/created-by.md b/docs/components/collection-fields/created-by.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/collection-fields/datetime.md b/docs/components/collection-fields/datetime.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/collection-fields/email.md b/docs/components/collection-fields/email.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/collection-fields/icon.md b/docs/components/collection-fields/icon.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/collection-fields/link-to.md b/docs/components/collection-fields/link-to.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/collection-fields/markdown.md b/docs/components/collection-fields/markdown.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/collection-fields/multiple-select.md b/docs/components/collection-fields/multiple-select.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/collection-fields/number.md b/docs/components/collection-fields/number.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/collection-fields/password.md b/docs/components/collection-fields/password.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/collection-fields/percent.md b/docs/components/collection-fields/percent.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/collection-fields/phone.md b/docs/components/collection-fields/phone.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/collection-fields/radio-group.md b/docs/components/collection-fields/radio-group.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/collection-fields/select.md b/docs/components/collection-fields/select.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/collection-fields/string.md b/docs/components/collection-fields/string.md new file mode 100644 index 0000000000..f1535c8bd4 --- /dev/null +++ b/docs/components/collection-fields/string.md @@ -0,0 +1,4 @@ +--- +group: + path: /components/collection-fields +--- diff --git a/docs/components/collection-fields/sub-table.md b/docs/components/collection-fields/sub-table.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/collection-fields/textarea.md b/docs/components/collection-fields/textarea.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/collection-fields/time.md b/docs/components/collection-fields/time.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/collection-fields/updated-at.md b/docs/components/collection-fields/updated-at.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/collection-fields/updated-by.md b/docs/components/collection-fields/updated-by.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/index.md b/docs/components/index.md new file mode 100644 index 0000000000..84a1506a08 --- /dev/null +++ b/docs/components/index.md @@ -0,0 +1,15 @@ +--- +title: Overview +order: 0 +nav: + title: Components + order: 4 +--- + +# Components + +NocoBase 的客户端组件总共有三类: + +- 通过 createRouteSwitch 创建的路由组件,如 Layou、Page +- 通过 createCollectionField 创建的字段组件,用于扩展字段 +- 通过 createSchemaComponent 创建的 JSON Schema 组件,可以是任意东西,比如表格、表单、日历、看板等。Schema Component 可用于 Route Component 或 Collection Field 中。 diff --git a/docs/components/index.zh-CN.md b/docs/components/index.zh-CN.md new file mode 100644 index 0000000000..04d255efcd --- /dev/null +++ b/docs/components/index.zh-CN.md @@ -0,0 +1,15 @@ +--- +title: 概述 +order: 0 +nav: + title: 组件 + order: 4 +--- + +NocoBase 的客户端组件总共有三类: + +- 通过 createRouteSwitch 创建的路由组件,如 Layou、Page +- 通过 createCollectionField 创建的字段组件,用于扩展字段 +- 通过 createSchemaComponent 创建的 JSON Schema 组件,可以是任意东西,比如表格、表单、日历、看板等。 + Schema Component 可用于 Route Component 或 Collection Field 中。 + diff --git a/docs/components/route-switch/admin-layout.md b/docs/components/route-switch/admin-layout.md new file mode 100644 index 0000000000..4717682675 --- /dev/null +++ b/docs/components/route-switch/admin-layout.md @@ -0,0 +1,10 @@ +--- +order: 3 +title: AdminLayout +group: + title: Route Components + path: /components/route-components + order: 1 +--- + +# AdminLayout diff --git a/docs/components/route-switch/admin-layout.zh-CN.md b/docs/components/route-switch/admin-layout.zh-CN.md new file mode 100644 index 0000000000..dab9435841 --- /dev/null +++ b/docs/components/route-switch/admin-layout.zh-CN.md @@ -0,0 +1,10 @@ +--- +order: 3 +title: AdminLayout +group: + title: 路由组件 + path: /zh-CN/components/route-components + order: 1 +--- + +# AdminLayout diff --git a/docs/components/route-switch/auth-layout.md b/docs/components/route-switch/auth-layout.md new file mode 100644 index 0000000000..6e50db51d6 --- /dev/null +++ b/docs/components/route-switch/auth-layout.md @@ -0,0 +1,9 @@ +--- +order: 2 +title: AuthLayout +group: + path: /components/route-components + order: 1 +--- + +# AuthLayout diff --git a/docs/components/schema-components/action.md b/docs/components/schema-components/action.md new file mode 100644 index 0000000000..b70a903083 --- /dev/null +++ b/docs/components/schema-components/action.md @@ -0,0 +1,8 @@ +--- +group: + title: Schema Components + path: /components/schema-components + order: 2 +--- + +# Action diff --git a/docs/components/schema-components/action.zh-CN.md b/docs/components/schema-components/action.zh-CN.md new file mode 100644 index 0000000000..bf26f163d5 --- /dev/null +++ b/docs/components/schema-components/action.zh-CN.md @@ -0,0 +1,8 @@ +--- +group: + title: Schema 组件 + path: /zh-CN/components/schema-components + order: 2 +--- + +# Action diff --git a/docs/components/schema-components/add-new.md b/docs/components/schema-components/add-new.md new file mode 100644 index 0000000000..8a00f4e122 --- /dev/null +++ b/docs/components/schema-components/add-new.md @@ -0,0 +1 @@ +# AddNew diff --git a/docs/components/schema-components/block-item.md b/docs/components/schema-components/block-item.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/schema-components/calendar.md b/docs/components/schema-components/calendar.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/schema-components/card-item.md b/docs/components/schema-components/card-item.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/schema-components/cascader.md b/docs/components/schema-components/cascader.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/schema-components/chart.md b/docs/components/schema-components/chart.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/schema-components/checkbox.md b/docs/components/schema-components/checkbox.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/schema-components/collection.md b/docs/components/schema-components/collection.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/schema-components/color-select.md b/docs/components/schema-components/color-select.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/schema-components/date-picker.md b/docs/components/schema-components/date-picker.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/schema-components/filter.md b/docs/components/schema-components/filter.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/schema-components/form-item.md b/docs/components/schema-components/form-item.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/schema-components/form.md b/docs/components/schema-components/form.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/schema-components/grid.md b/docs/components/schema-components/grid.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/schema-components/icon-picker.md b/docs/components/schema-components/icon-picker.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/schema-components/input-number.md b/docs/components/schema-components/input-number.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/schema-components/input.md b/docs/components/schema-components/input.md new file mode 100644 index 0000000000..7ca09eea21 --- /dev/null +++ b/docs/components/schema-components/input.md @@ -0,0 +1,5 @@ +--- +title: Input +group: + path: /components/schema-components +--- diff --git a/docs/components/schema-components/kanban.md b/docs/components/schema-components/kanban.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/schema-components/list-picker.md b/docs/components/schema-components/list-picker.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/schema-components/markdown.md b/docs/components/schema-components/markdown.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/schema-components/menu.md b/docs/components/schema-components/menu.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/schema-components/password.md b/docs/components/schema-components/password.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/schema-components/radio.md b/docs/components/schema-components/radio.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/schema-components/select.md b/docs/components/schema-components/select.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/schema-components/table.md b/docs/components/schema-components/table.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/schema-components/tabs.md b/docs/components/schema-components/tabs.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/schema-components/time-picker.md b/docs/components/schema-components/time-picker.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/schema-components/tree-select.md b/docs/components/schema-components/tree-select.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/components/schema-components/upload.md b/docs/components/schema-components/upload.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/examples/index.md b/docs/examples/index.md new file mode 100644 index 0000000000..843a2f2cb8 --- /dev/null +++ b/docs/examples/index.md @@ -0,0 +1,8 @@ +--- +sidemenu: false +nav: + title: Examples + order: 99 +--- + +Coming soon... diff --git a/docs/examples/index.zh-CN.md b/docs/examples/index.zh-CN.md new file mode 100644 index 0000000000..420497907a --- /dev/null +++ b/docs/examples/index.zh-CN.md @@ -0,0 +1,8 @@ +--- +sidemenu: false +nav: + title: 示例 + order: 99 +--- + +待补充... diff --git a/docs/guide/basic/client-components.md b/docs/guide/basic/client-components.md new file mode 100644 index 0000000000..f4a55869dc --- /dev/null +++ b/docs/guide/basic/client-components.md @@ -0,0 +1,324 @@ +--- +order: 2 +--- + +# Client Components + +为了让更多非开发人员也能参与进来,NocoBase 提供了配套的客户端 —— 无代码的可视化界面。客户端界面非常灵活,由不同组件构成,分为了三类: + +- 通过 createRouteSwitch 创建的路由组件,如 Layout、Page +- 通过 createCollectionField 创建的字段组件,用于扩展字段 +- 通过 createSchemaComponent 创建的 JSON Schema 组件,可以是任意东西,比如表格、表单、日历、看板等 + +[更多组件内容,查看组件章节](#) + +## 组件树结构 + +界面是由组件构成的组件树,结构如下: + +
+// 布局
+
+  // 页面
+  
+    // 栅格
+    
+      // 区块,以表格为例
+      
+        // 配置工具栏
+        
+        // 操作栏
+        
+          // 操作
+          
+          
+        
+        // 内容区
+        
+          
+            // 表格列的字段
+            
+          
+          
+            
+          
+        
+      
+
+ +
+
+
+ +注:以上例子只为表达组件树的结构和组件之间的关系,实际代码并不如此。 + +接下来,我们来详细的介绍各部分的概念。 + +## 布局和页面 + +页面是可以通过地址访问的网页,不同页面之间可能具有相同的页眉、页脚和导航,通常我们会把这些公共的内容放在布局组件里。例如,初始化的 NocoBase 提供了两个布局组件,如图所示: + +图 + +- AuthLayout:无需登录就能访问,一般用于嵌入登录、注册、忘记密码等页面。 +- AdminLayout:需要登录,管理后台的所有页面。 + +布局和页面组件通过 createRouteSwitch 注册,更多扩展内容点此查看。 + +## 页面内容排版 + +对开发者来说,页面内容的编写是自由的,不过为了方便对页面内容进行排版,提供了两种排版方式: + +### 简易的上下结构 + +
+
+  
+  
+  
+
+
+ +例子如下: + +```js +// 示例 +``` + +### 可拖拽的栅格 + +
+
+  
+    
+      
+        
+      
+      
+        
+      
+    
+    
+      
+        
+      
+      
+        
+      
+    
+  
+
+
+ +栅格组件 Grid 基于行(Grid.Row)和列(Grid.Col)来定义区块的外部框架。例子如下: + +```js +// 示例 +``` + +## AddNew + +AddNew 是页面可视化配置最重要的操作按钮,更多关于 AddNew 的内容点此查看 + +## 区块 - Block + +区块一般放在页面里,可以是任意东西,包括文字、附件、表格、表单、日历、看板等等。一个完整的区块由三部分组成: + +- 内容区 Content,区块的主体 +- 操作栏 ActionBar,可以放置各种操作按钮,用于操作区块数据(可选) +- 配置工具栏 DesignableBar,操作区块配置的按钮(可选) + +以表格区块为例,组件结构如下: + +
+
+  
+  
+  
+
+
+ +具体形态: + +```js +//示例(这里放上一个表示区块结构的示例) +``` + +区块有几种类型: + +- 数据类型,用于展示数据表的数据,如表格、日历、看板、表单、详情等。 +- 多媒体,用于丰富页面内容,如文本段、附件等。暂时只有一个简易的 Markdown。 +- 图表,用于展示数据统计。 +- 模板,可直接将某些成品模板化,直接应用到页面上。 + +区块可以任意扩展,如何扩展查看 createSchemaComponent 章节。 + +## 操作栏 - ActionBar + +操作栏是一系列操作的集合,一般用于区块内部。用户发出操作指令,程序做出改变,并将结果响应在区块内容区。 + +例如: + +表格,内容区是一个表格,操作区会放置一些操作按钮,如筛选、新增、删除、导出等 + +```js +// 示例(放一个简易的表格,把操作栏重点突出一下) +``` + +详情,内容区是详情数据,操作区会放置编辑、导出等按钮 + +```js +// 示例(放一个简易的详情,把操作栏重点突出一下) +``` + +不同的区块,操作栏的按钮可能不同。操作栏的按钮也是可以自定义的,具体内容查看操作章节。 + +## 操作 - Action + +操作是封装的一段指令,一般需要用户参与。 + +例如: + +- 删除数据,需要用户选中待删除数据,再触发删除指令 +- 筛选数据,需要用户填写筛选项,再触发筛选指令 +- 新增数据,需要用户填写数据之后提交,触发新增操作指令 +- 查看详情,用户点击操作按钮,弹窗查看详情或当前窗口打开详情页查看 + +最简单的操作,只需要绑定一段指令即可,简单来说就是指定一段函数,无需传参。组件结构如下: + +
+
+
+ +大部分的操作指令需要用户提供参数,如新增数据操作,需要用户填写数据,填写数据一般需要弹出表单,用户填写完数据,点击提交,才触发操作指令。组件结构如下: + +
+
+  {/* 这是个弹窗表单,内置提交按钮,点击提交触发操作指令,具体代码省略 */}
+  
+
+
+ +一个完整的操作大概分为两步: + +- 为 Action 绑定一段指令 +- 如果指令需要用户提供参数,需要提供交互界面,目前内置的有: + - Action.Drawer:抽屉 + - Action.Modal:对话框 + - Action.Popover:气泡 + +操作是 NocoBase 里非常重要的一个概念,更多详情点此查看 + +## 配置工具栏 - DesignableBar + +所有的 Schema Component 都可以绑定自己的配置工具栏(DesignableBar),用于修改当前组件的 Schema。 +​ + +**什么是 Schema Component?** +通过 Schema 协议编写的类 JSON Schema 格式的组件,如: + +```js +{ + type: 'void', + 'x-Component': 'Hello', + 'x-designable-bar': 'Hello.DesignableBar', + 'x-dect': 'CardItem', +} +``` + +举几个例子,如: + +表单字段的 JSON Schema + +```js +const schema = { + type: 'string', + 'x-component': 'Input', + 'x-decorator': 'FormItem', + 'x-designable-bar': 'Form.Field.DesignableBar', +}; +``` + +表单项的配置工具栏 `Form.Field.DesignableBar` 的效果 + +![image.png](https://cdn.nlark.com/yuque/0/2021/png/1304394/1634135895582-57b6ce7d-af08-4c11-ad14-19c75acf8f8a.png#clientId=u67b0cc68-db77-4&from=paste&height=183&id=m0SAT&margin=%5Bobject%20Object%5D&name=image.png&originHeight=366&originWidth=636&originalType=binary&ratio=1&size=49669&status=done&style=none&taskId=udd86527b-c0f4-46b9-9970-d7c3b23e624&width=318) + +表格的 JSON Schema + +```js +const schema = { + type: 'array', + 'x-component': 'Table', + 'x-decorator': 'CardItem', + 'x-designable-bar': 'Table.DesignableBar', +}; +``` + +表格配置工具栏 `Table.DesignableBar` 的效果 + +![image.png](https://cdn.nlark.com/yuque/0/2021/png/1304394/1634134283034-a3660288-f903-4f19-8334-fda34f0bbe61.png#clientId=u67b0cc68-db77-4&from=paste&height=249&id=ud9d383da&margin=%5Bobject%20Object%5D&name=image.png&originHeight=498&originWidth=440&originalType=binary&ratio=1&size=51402&status=done&style=none&taskId=u94bfca27-5467-42f9-b7a3-ec00a9c688f&width=220) + +菜单项的 JSON Schema: + +```js +const schema = { + type: 'array', + 'x-component': 'Menu.Item', + 'x-designable-bar': 'Menu.Item.DesignableBar', +}; +``` + +菜单项配置工具栏 `Menu.Item.DesignableBar` 的效果 + +![image.png](https://cdn.nlark.com/yuque/0/2021/png/1304394/1634134190121-f4b028ec-93bf-4a65-8ba8-dcc78886deae.png) + +更多配置工具栏详情点此查看 + +## 字段组件 - CollectionField + +字段组件的配置参数可能非常多,在不同数据区块里也可能用到同一个字段组件,为了减少代码重复,NocoBase 里,将字段组件的配置交由数据表统一管理。一处配置,多处使用。数据区块里直接引用字段组件,如果有其他不同参数再另行扩展。 + +
+
+  // 原生态的写法
+  
+    
+  
+  // 简化之后的字段引用
+  
+    
+  
+
+ +
+ // 如果在表格里也用到,再写一遍 + + + + // 字段引用,只需要提供 name 即可 + + + +
+
+ +字段组件有三种显示状态: + +- 可填写 - editable +- 不可填写 - disabled +- 阅读模式 - read-pretty + +以单行文本(Input)为例: + +```js +// 示例(Input 的三种显示状态) + +// 示例待补充 +``` + +**为什么字段有多种显示状态?** + +- 在表单中,一般情况字段为可填写状态(editable),但如果只供查看,这时候就会把字段设置为 disabled 或 read-pretty。 +- 在表格中,一般情况字段为阅读模式(read-pretty),但如果需要在表格内快捷编辑,又可以动态的将某个字段激活为 editable。 + +字段组件可以任意扩展,如何扩展查看 createCollectionField 章节。 diff --git a/docs/guide/basic/client-components.zh-CN.md b/docs/guide/basic/client-components.zh-CN.md new file mode 100644 index 0000000000..8bf08ed249 --- /dev/null +++ b/docs/guide/basic/client-components.zh-CN.md @@ -0,0 +1,324 @@ +--- +order: 2 +--- + +# 客户端组件 + +为了让更多非开发人员也能参与进来,NocoBase 提供了配套的客户端 —— 无代码的可视化界面。客户端界面非常灵活,由不同组件构成,分为了三类: + +- 通过 createRouteSwitch 创建的路由组件,如 Layout、Page +- 通过 createCollectionField 创建的字段组件,用于扩展字段 +- 通过 createSchemaComponent 创建的 JSON Schema 组件,可以是任意东西,比如表格、表单、日历、看板等 + +[更多组件内容,查看组件章节](#) + +## 组件树结构 + +界面是由组件构成的组件树,结构如下: + +
+// 布局
+
+  // 页面
+  
+    // 栅格
+    
+      // 区块,以表格为例
+      
+        // 配置工具栏
+        
+        // 操作栏
+        
+          // 操作
+          
+          
+        
+        // 内容区
+        
+          
+            // 表格列的字段
+            
+          
+          
+            
+          
+        
+      
+
+ +
+
+
+ +注:以上例子只为表达组件树的结构和组件之间的关系,实际代码并不如此。 + +接下来,我们来详细的介绍各部分的概念。 + +## 布局和页面 + +页面是可以通过地址访问的网页,不同页面之间可能具有相同的页眉、页脚和导航,通常我们会把这些公共的内容放在布局组件里。例如,初始化的 NocoBase 提供了两个布局组件,如图所示: + +图 + +- AuthLayout:无需登录就能访问,一般用于嵌入登录、注册、忘记密码等页面。 +- AdminLayout:需要登录,管理后台的所有页面。 + +布局和页面组件通过 createRouteSwitch 注册,更多扩展内容点此查看。 + +## 页面内容排版 + +对开发者来说,页面内容的编写是自由的,不过为了方便对页面内容进行排版,提供了两种排版方式: + +### 简易的上下结构 + +
+
+  
+  
+  
+
+
+ +例子如下: + +```js +// 示例 +``` + +### 可拖拽的栅格 + +
+
+  
+    
+      
+        
+      
+      
+        
+      
+    
+    
+      
+        
+      
+      
+        
+      
+    
+  
+
+
+ +栅格组件 Grid 基于行(Grid.Row)和列(Grid.Col)来定义区块的外部框架。例子如下: + +```js +// 示例 +``` + +## AddNew + +AddNew 是页面可视化配置最重要的操作按钮,更多关于 AddNew 的内容点此查看 + +## 区块 - Block + +区块一般放在页面里,可以是任意东西,包括文字、附件、表格、表单、日历、看板等等。一个完整的区块由三部分组成: + +- 内容区 Content,区块的主体 +- 操作栏 ActionBar,可以放置各种操作按钮,用于操作区块数据(可选) +- 配置工具栏 DesignableBar,操作区块配置的按钮(可选) + +以表格区块为例,组件结构如下: + +
+
+  
+  
+  
+
+
+ +具体形态: + +```js +//示例(这里放上一个表示区块结构的示例) +``` + +区块有几种类型: + +- 数据类型,用于展示数据表的数据,如表格、日历、看板、表单、详情等。 +- 多媒体,用于丰富页面内容,如文本段、附件等。暂时只有一个简易的 Markdown。 +- 图表,用于展示数据统计。 +- 模板,可直接将某些成品模板化,直接应用到页面上。 + +区块可以任意扩展,如何扩展查看 createSchemaComponent 章节。 + +## 操作栏 - ActionBar + +操作栏是一系列操作的集合,一般用于区块内部。用户发出操作指令,程序做出改变,并将结果响应在区块内容区。 + +例如: + +表格,内容区是一个表格,操作区会放置一些操作按钮,如筛选、新增、删除、导出等 + +```js +// 示例(放一个简易的表格,把操作栏重点突出一下) +``` + +详情,内容区是详情数据,操作区会放置编辑、导出等按钮 + +```js +// 示例(放一个简易的详情,把操作栏重点突出一下) +``` + +不同的区块,操作栏的按钮可能不同。操作栏的按钮也是可以自定义的,具体内容查看操作章节。 + +## 操作 - Action + +操作是封装的一段指令,一般需要用户参与。 + +例如: + +- 删除数据,需要用户选中待删除数据,再触发删除指令 +- 筛选数据,需要用户填写筛选项,再触发筛选指令 +- 新增数据,需要用户填写数据之后提交,触发新增操作指令 +- 查看详情,用户点击操作按钮,弹窗查看详情或当前窗口打开详情页查看 + +最简单的操作,只需要绑定一段指令即可,简单来说就是指定一段函数,无需传参。组件结构如下: + +
+
+
+ +大部分的操作指令需要用户提供参数,如新增数据操作,需要用户填写数据,填写数据一般需要弹出表单,用户填写完数据,点击提交,才触发操作指令。组件结构如下: + +
+
+  {/* 这是个弹窗表单,内置提交按钮,点击提交触发操作指令,具体代码省略 */}
+  
+
+
+ +一个完整的操作大概分为两步: + +- 为 Action 绑定一段指令 +- 如果指令需要用户提供参数,需要提供交互界面,目前内置的有: + - Action.Drawer:抽屉 + - Action.Modal:对话框 + - Action.Popover:气泡 + +操作是 NocoBase 里非常重要的一个概念,更多详情点此查看 + +## 配置工具栏 - DesignableBar + +所有的 Schema Component 都可以绑定自己的配置工具栏(DesignableBar),用于修改当前组件的 Schema。 +​ + +**什么是 Schema Component?** +通过 Schema 协议编写的类 JSON Schema 格式的组件,如: + +```js +{ + type: 'void', + 'x-Component': 'Hello', + 'x-designable-bar': 'Hello.DesignableBar', + 'x-dect': 'CardItem', +} +``` + +举几个例子,如: + +表单字段的 JSON Schema + +```js +const schema = { + type: 'string', + 'x-component': 'Input', + 'x-decorator': 'FormItem', + 'x-designable-bar': 'Form.Field.DesignableBar', +}; +``` + +表单项的配置工具栏 `Form.Field.DesignableBar` 的效果 + +![image.png](https://cdn.nlark.com/yuque/0/2021/png/1304394/1634135895582-57b6ce7d-af08-4c11-ad14-19c75acf8f8a.png#clientId=u67b0cc68-db77-4&from=paste&height=183&id=m0SAT&margin=%5Bobject%20Object%5D&name=image.png&originHeight=366&originWidth=636&originalType=binary&ratio=1&size=49669&status=done&style=none&taskId=udd86527b-c0f4-46b9-9970-d7c3b23e624&width=318) + +表格的 JSON Schema + +```js +const schema = { + type: 'array', + 'x-component': 'Table', + 'x-decorator': 'CardItem', + 'x-designable-bar': 'Table.DesignableBar', +}; +``` + +表格配置工具栏 `Table.DesignableBar` 的效果 + +![image.png](https://cdn.nlark.com/yuque/0/2021/png/1304394/1634134283034-a3660288-f903-4f19-8334-fda34f0bbe61.png#clientId=u67b0cc68-db77-4&from=paste&height=249&id=ud9d383da&margin=%5Bobject%20Object%5D&name=image.png&originHeight=498&originWidth=440&originalType=binary&ratio=1&size=51402&status=done&style=none&taskId=u94bfca27-5467-42f9-b7a3-ec00a9c688f&width=220) + +菜单项的 JSON Schema: + +```js +const schema = { + type: 'array', + 'x-component': 'Menu.Item', + 'x-designable-bar': 'Menu.Item.DesignableBar', +}; +``` + +菜单项配置工具栏 `Menu.Item.DesignableBar` 的效果 + +![image.png](https://cdn.nlark.com/yuque/0/2021/png/1304394/1634134190121-f4b028ec-93bf-4a65-8ba8-dcc78886deae.png) + +更多配置工具栏详情点此查看 + +## 字段组件 - CollectionField + +字段组件的配置参数可能非常多,在不同数据区块里也可能用到同一个字段组件,为了减少代码重复,NocoBase 里,将字段组件的配置交由数据表统一管理。一处配置,多处使用。数据区块里直接引用字段组件,如果有其他不同参数再另行扩展。 + +
+
+  // 原生态的写法
+  
+    
+  
+  // 简化之后的字段引用
+  
+    
+  
+
+ +
+ // 如果在表格里也用到,再写一遍 + + + + // 字段引用,只需要提供 name 即可 + + + +
+
+ +字段组件有三种显示状态: + +- 可填写 - editable +- 不可填写 - disabled +- 阅读模式 - read-pretty + +以单行文本(Input)为例: + +```js +// 示例(Input 的三种显示状态) + +// 示例待补充 +``` + +**为什么字段有多种显示状态?** + +- 在表单中,一般情况字段为可填写状态(editable),但如果只供查看,这时候就会把字段设置为 disabled 或 read-pretty。 +- 在表格中,一般情况字段为阅读模式(read-pretty),但如果需要在表格内快捷编辑,又可以动态的将某个字段激活为 editable。 + +字段组件可以任意扩展,如何扩展查看 createCollectionField 章节。 diff --git a/docs/guide/basic/collections.md b/docs/guide/basic/collections.md new file mode 100644 index 0000000000..1b2b972d33 --- /dev/null +++ b/docs/guide/basic/collections.md @@ -0,0 +1,82 @@ +--- +order: 1 +group: + title: Basic Concepts + path: /guide/basic + order: 4 +--- + +# Collections & Fields + +NocoBase 的数据表由字段(列)和记录(行)组成。数据表的概念与关系型数据库的数据表概念相近,但是字段的概念并不相同。 + +## 字段 + +NocoBase 里,最常见的字段具有组件形态,如:单行文本、多行文本、单选框。这些组件都有数值(value),可交由用户填写,称为有值组件。结构如下: + +```ts +{ + interface: 'textarea', + type: 'text', + name: 'description', + uiSchema: { + type: 'string', + title: '描述', + 'x-component': 'Input.TextArea', + 'x-decorator': 'FormItem', + }, +} +``` + +上述是一个描述字段的配置: + +- type 表示字段的存储类型,为 text 长文本类型 +- uiSchema 为字段的组件参数 +- uiSchema.type 为字段组件的数值类型 +- uiSchema.x-component 表示组件类型,为多行输入框 +- 绑定了组件的字段,都要设置一个 interface,表示当前字段的类型,例子描述字段为多行文本类型 + +除了常见的绑定了组件的字段以外,还有一些无需绑定组件的字段,如 token 字段,这类组件并不会显示在界面上。无组件字段的结构如下: + +```ts +{ + type: 'string', + name: 'token', +} +``` + +**为什么字段要区分存储类型和组件类型?** + +其一:存储类型和组件类型是多对多关系,并不适合合并处理。 +同一组件的 value 的类型(存储类型)可能并不相同,比如 select 的 value 可能是 string 或者 integer。同一存储类型也可能以不同的组件呈现,如 string 绑定的组件可能是 Input,也可能是 Select。 + +其二:有限的存储类型和组件类型可以组合出无数种字段类型。 +单行文本、电子邮件、网址、手机号这些字段的存储类型和组件类型虽然都相同,但是校验参数并不相同,只需要调整 validate 参数即可创建出无数种字段。 + +## 字段的类型 + +| 名称 | Interface | Type | Component | 备注 | +| :------- | :-------- | :----- | :------------- | :---------------- | +| 单行文本 | string | string | Input | | +| 多行文本 | textarea | text | Input.TextArea | | +| 邮箱 | email | string | Input | validate: 'email' | +| 手机号 | phone | string | Input | validate: 'phone' | + +## 可以做什么? + +### 快速建模 + +与专业的建模工具不同,NocoBase 提供了一种更利于普通用户理解的数据表配置方法。 + +- 可以直接通过 app.collection() 直接写代码里,多用于配置底层系统表。 +- 也可以通过无代码平台的数据表配置入口配置数据表,多用于配置业务表。 + +### 创建数据区块 + +配置好的数据表可用于创建对应的数据区块,如以表格的形式展示某个数据表的内容。表格里可以选择哪些字段作为表格列显示出来。 + +更多关于区块的内容可以查看客户端组件章节。 + +### HTTP API + +跨平台也可以通过 HTTP API 的方式操作数据表(增删改查配置等),更多内容查看 SDK 章节。 diff --git a/docs/guide/basic/collections.zh-CN.md b/docs/guide/basic/collections.zh-CN.md new file mode 100644 index 0000000000..c6b1fb521c --- /dev/null +++ b/docs/guide/basic/collections.zh-CN.md @@ -0,0 +1,82 @@ +--- +order: 1 +group: + title: 基础概念 + path: /zh-CN/guide/basic + order: 4 +--- + +# 数据表和字段 + +NocoBase 的数据表由字段(列)和记录(行)组成。数据表的概念与关系型数据库的数据表概念相近,但是字段的概念并不相同。 + +## 字段 + +NocoBase 里,最常见的字段具有组件形态,如:单行文本、多行文本、单选框。这些组件都有数值(value),可交由用户填写,称为有值组件。结构如下: + +```ts +{ + interface: 'textarea', + type: 'text', + name: 'description', + uiSchema: { + type: 'string', + title: '描述', + 'x-component': 'Input.TextArea', + 'x-decorator': 'FormItem', + }, +} +``` + +上述是一个描述字段的配置: + +- type 表示字段的存储类型,为 text 长文本类型 +- uiSchema 为字段的组件参数 +- uiSchema.type 为字段组件的数值类型 +- uiSchema.x-component 表示组件类型,为多行输入框 +- 绑定了组件的字段,都要设置一个 interface,表示当前字段的类型,例子描述字段为多行文本类型 + +除了常见的绑定了组件的字段以外,还有一些无需绑定组件的字段,如 token 字段,这类组件并不会显示在界面上。无组件字段的结构如下: + +```ts +{ + type: 'string', + name: 'token', +} +``` + +**为什么字段要区分存储类型和组件类型?** + +其一:存储类型和组件类型是多对多关系,并不适合合并处理。 +同一组件的 value 的类型(存储类型)可能并不相同,比如 select 的 value 可能是 string 或者 integer。同一存储类型也可能以不同的组件呈现,如 string 绑定的组件可能是 Input,也可能是 Select。 + +其二:有限的存储类型和组件类型可以组合出无数种字段类型。 +单行文本、电子邮件、网址、手机号这些字段的存储类型和组件类型虽然都相同,但是校验参数并不相同,只需要调整 validate 参数即可创建出无数种字段。 + +## 字段的类型 + +| 名称 | Interface | Type | Component | 备注 | +| :------- | :-------- | :----- | :------------- | :---------------- | +| 单行文本 | string | string | Input | | +| 多行文本 | textarea | text | Input.TextArea | | +| 邮箱 | email | string | Input | validate: 'email' | +| 手机号 | phone | string | Input | validate: 'phone' | + +## 可以做什么? + +### 快速建模 + +与专业的建模工具不同,NocoBase 提供了一种更利于普通用户理解的数据表配置方法。 + +- 可以直接通过 app.collection() 直接写代码里,多用于配置底层系统表。 +- 也可以通过无代码平台的数据表配置入口配置数据表,多用于配置业务表。 + +### 创建数据区块 + +配置好的数据表可用于创建对应的数据区块,如以表格的形式展示某个数据表的内容。表格里可以选择哪些字段作为表格列显示出来。 + +更多关于区块的内容可以查看客户端组件章节。 + +### HTTP API + +跨平台也可以通过 HTTP API 的方式操作数据表(增删改查配置等),更多内容查看 SDK 章节。 diff --git a/docs/guide/deployment.md b/docs/guide/deployment.md new file mode 100644 index 0000000000..efff653dce --- /dev/null +++ b/docs/guide/deployment.md @@ -0,0 +1,6 @@ +--- +order: 3 + +--- + +# Deployment \ No newline at end of file diff --git a/docs/guide/deployment.zh-CN.md b/docs/guide/deployment.zh-CN.md new file mode 100644 index 0000000000..00d8e69b2b --- /dev/null +++ b/docs/guide/deployment.zh-CN.md @@ -0,0 +1,5 @@ +--- +order: 3 +--- + +# 部署 diff --git a/docs/guide/index.md b/docs/guide/index.md new file mode 100644 index 0000000000..5abf110a12 --- /dev/null +++ b/docs/guide/index.md @@ -0,0 +1,42 @@ +--- +nav: + title: Guide + order: 1 +order: 1 +title: Introducation +--- + +## What is NocoBase + +NocoBase is a scalability-first, open-source no-code development platform. No programming required, build your own collaboration platform, management system with NocoBase in minutes. + +## When to use NocoBase + +- **SMEs and organizations build business platforms and management systems for themselves or for their industry** + - Want the price to be low enough or even free + - Can be flexibly customized without programming knowledge + - Need full control of source code and data + - Can freely distribute and sell as their own products +- **Service providers and outsourcing teams develop collaboration platforms and management systems for their clients** + - Want to keep development costs as low as possible + - Need the most user-friendly secondary development experience + - Must be deployed privately as a standalone product for the client + - Can be freely distributed and sold by the client + +## Why choose NocoBase + +- **Open source and free** + - Unrestricted commercial use under the MIT license + - Full code ownership, private deployment, private and secure data + - Free to expand and develop for actual needs + - Good ecological support +- **Strong no-code capability** + - WYSIWYG visual configuration + - Separation of data structure configuration from interface configuration + - Rich combination of blocks and operations + - Role-based access control +- **Developer-friendly** + - Microkernel architecture, flexible and easy to extend, with a robust plug-in system + - Node.js-based, with popular frameworks and technologies, including Koa, Sequelize, React, Formily, Ant Design, etc. + - Progressive development, easy for getting-started, friendly to newcomers + - No binding, no strong dependencies, can be used in any combination or extensions, can be used in existing projects diff --git a/docs/guide/index.zh-CN.md b/docs/guide/index.zh-CN.md new file mode 100644 index 0000000000..809f9efa82 --- /dev/null +++ b/docs/guide/index.zh-CN.md @@ -0,0 +1,42 @@ +--- +nav: + title: 指南 + order: 1 +order: 1 +title: 介绍 +--- + +## NocoBase 是什么 + +NocoBase 是一个极易扩展的开源无代码开发平台。无需编程,使用 NocoBase 搭建自己的协作平台、管理系统,只需要几分钟时间。 + +## 哪些场景适合使用 NocoBase + +- 中小企业和组织为自己或者为所在行业搭建业务平台和管理系统 + - 希望价格足够低,甚至免费 + - 不懂编程也可以灵活定制 + - 需要完全掌控源代码和数据 + - 可以以自有产品的形态自由分发和销售 +- 服务商和外包团队为客户开发协作平台和管理系统 + - 希望尽可能降低开发成本 + - 需要极致友好的二次开发体验 + - 必须以独立产品的形态为客户私有部署 + - 客户可以自由分发和销售 + +## 为什么选择 NocoBase + +- 开源免费 + - 采用 MIT 许可协议,不限制商业使用 + - 拥有全部代码,私有化部署,保障数据私有和安全 + - 针对实际需求自由扩展开发 + - 具备良好的生态支持 +- 无代码能力强 + - 所见即所得的可视化配置 + - 数据结构配置与界面配置分离 + - 丰富的区块和操作任意组合 + - 基于角色的访问权限 +- 对开发者友好 + - 微内核,灵活易扩展,具备健全的插件体系 + - 基于 Node.js,使用主流框架和技术,包括 Koa、Sequelize、React、Formily、Ant Design 等 + - 渐进式开发,上手难度低,对新人友好 + - 不绑架、不强依赖,可任意组合使用或扩展,可用于现有项目中 diff --git a/docs/guide/kernel-principle/choose-an-orm.md b/docs/guide/kernel-principle/choose-an-orm.md new file mode 100644 index 0000000000..e0bebc40c9 --- /dev/null +++ b/docs/guide/kernel-principle/choose-an-orm.md @@ -0,0 +1,1268 @@ +--- +order: 4 +--- + +# How to Choose an ORM + +本篇文章,需要读者至少熟悉一种较流行的 ORM,对 Model、Migration、QueryBuilder、Repository 也有所了解。在正式介绍 NocoBase 的 Database 设计之前,先来看看大部分 ORM 都有的三个概念: + +- Model(ModelAttributes)、Entity(EntitySchema):将数据表与模型类或实体类对应起来 +- Migration、Sync API:用于创建、修改、删除数据库表、字段、索引等 +- QueryBuilder、EntityManager、Repository、CRUD API:提供增删改查 + + + +## Model/Entity +无代码的第一个改造,**Model 动态化**。 +​ + +简单来说 Model/Entity 的作用就是将数据表、字段、索引、关系映射到类、属性或方法上。我们先来看看 Node.js 里各个 ORM 都是怎么做的。 +### Typeorm +在 Typeorm 里叫 Entity,通过类属性映射表字段,装饰器风格来配置字段属性 +```typescript +import { Entity, Column, PrimaryGeneratedColumn } from "typeorm"; + +@Entity() +export class Photo { + + @PrimaryGeneratedColumn() + id: number; + + @Column({ + length: 100 + }) + name: string; + + @Column("text") + description: string; + + @Column() + filename: string; + + @Column("double") + views: number; + + @Column() + isPublished: boolean; +} +``` +这种写法,如果 Entity 有修改,需要修改代码,不过 Typeorm 提供了 JSON 风格的 EntitySchema +```typescript +import {EntitySchema} from "typeorm"; + +export const CategoryEntity = new EntitySchema({ + name: "category", + columns: { + id: { + type: Number, + primary: true, + generated: true + }, + name: { + type: String + } + } +}); + +export const PostEntity = new EntitySchema({ + name: "post", + columns: { + id: { + type: Number, + primary: true, + generated: true + }, + title: { + type: String + }, + text: { + type: String + } + }, + relations: { + categories: { + type: "many-to-many", + target: "category" // CategoryEntity + } + } +}); +``` +修改 JSON 就容易多了,这种写法非常适用于无代码平台。平台配置 JSON 动态生成对应的 Entity。 +### Prisma +与 Typeorm 装饰器的风格非常接近,但不同的是 Prisma 另辟蹊径,提供了自成一套的 PSL(Prisma Schema Language): +```ts +datasource db { + url = env("DATABASE_URL") + provider = "postgresql" +} + +generator client { + provider = "prisma-client-js" +} + +model User { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + email String @unique + name String? + role Role @default(USER) + posts Post[] +} + +model Post { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + published Boolean @default(false) + title String @db.VarChar(255) + author User? @relation(fields: [authorId], references: [id]) + authorId Int? +} + +enum Role { + USER + ADMIN +} +``` +这种写法,需要修改 PSL 文件,除非自己实现一套 PSL 解析与生成器,不然没办法直接无代码改造。 +### Sequelize +作为老牌 ORM,下载量和使用量也是惊人,提供了多种配置 Model 的风格: + +1. 传统的 JSON 风格 +```ts +sequelize.define('User', { + // Model attributes are defined here + firstName: { + type: DataTypes.STRING, + allowNull: false + }, + lastName: { + type: DataTypes.STRING + // allowNull defaults to true + } +}, { + // model options +}); +``` + +2. 改进之后的 Model 风格 +```ts +const { Sequelize, DataTypes, Model } = require('sequelize'); +const sequelize = new Sequelize('sqlite::memory'); + +class User extends Model {} + +User.init({ + // Model attributes are defined here + firstName: { + type: DataTypes.STRING, + allowNull: false + }, + lastName: { + type: DataTypes.STRING + // allowNull defaults to true + } +}, { + // Other model options go here + sequelize, // We need to pass the connection instance + modelName: 'User' // We need to choose the model name +}); + +// the defined model is the class itself +console.log(User === sequelize.models.User); // true +``` + +3. 装饰器风格 +```ts +import { Table, Column, Model, HasMany } from 'sequelize-typescript' + +@Table +class Person extends Model { + @Column + name: string + + @Column + birthday: Date + + @HasMany(() => Hobby) + hobbies: Hobby[] +} +``` +Sequelize 可以使用 JSON 风格配置 Model,非常适用于无代码平台。平台配置 JSON 动态生成对应的 Model。 +### Objection.js +基于 Knex,Model 如下: +```typescript +const { Model } = require('objection'); + +class Person extends Model { + // Table name is the only required property. + static get tableName() { + return 'persons'; + } + + // Each model must have a column (or a set of columns) that uniquely + // identifies the rows. The column(s) can be specified using the `idColumn` + // property. `idColumn` returns `id` by default and doesn't need to be + // specified unless the model's primary key is something else. + static get idColumn() { + return 'id'; + } + + // Methods can be defined for model classes just as you would for + // any JavaScript class. If you want to include the result of these + // methods in the output json, see `virtualAttributes`. + fullName() { + return this.firstName + ' ' + this.lastName; + } + + // Optional JSON schema. This is not the database schema! + // No tables or columns are generated based on this. This is only + // used for input validation. Whenever a model instance is created + // either explicitly or implicitly it is checked against this schema. + // See http://json-schema.org/ for more info. + static get jsonSchema() { + return { + type: 'object', + required: ['firstName', 'lastName'], + + properties: { + id: { type: 'integer' }, + parentId: { type: ['integer', 'null'] }, + firstName: { type: 'string', minLength: 1, maxLength: 255 }, + lastName: { type: 'string', minLength: 1, maxLength: 255 }, + age: { type: 'number' }, + + // Properties defined as objects or arrays are + // automatically converted to JSON strings when + // writing to database and back to objects and arrays + // when reading from database. To override this + // behaviour, you can override the + // Model.jsonAttributes property. + address: { + type: 'object', + properties: { + street: { type: 'string' }, + city: { type: 'string' }, + zipCode: { type: 'string' } + } + } + } + }; + } + + // This object defines the relations to other models. + static get relationMappings() { + // Importing models here is a one way to avoid require loops. + const Animal = require('./Animal'); + const Movie = require('./Movie'); + + return { + pets: { + relation: Model.HasManyRelation, + // The related model. This can be either a Model + // subclass constructor or an absolute file path + // to a module that exports one. We use a model + // subclass constructor `Animal` here. + modelClass: Animal, + join: { + from: 'persons.id', + to: 'animals.ownerId' + } + }, + + movies: { + relation: Model.ManyToManyRelation, + modelClass: Movie, + join: { + from: 'persons.id', + // ManyToMany relation needs the `through` object + // to describe the join table. + through: { + // If you have a model class for the join table + // you need to specify it like this: + // modelClass: PersonMovie, + from: 'persons_movies.personId', + to: 'persons_movies.movieId' + }, + to: 'movies.id' + } + }, + + children: { + relation: Model.HasManyRelation, + modelClass: Person, + join: { + from: 'persons.id', + to: 'persons.parentId' + } + }, + + parent: { + relation: Model.BelongsToOneRelation, + modelClass: Person, + join: { + from: 'persons.parentId', + to: 'persons.id' + } + } + }; + } +} +``` +直观感受,并没有 Typeorm、Prisma、Sequelize 精炼。不过 Objection.js 提供了 mixin,也可以将 Model 进一步抽象,代码就精炼多了,也可以支持装饰器风格,比如这样: +```ts +const { compose, Model } = require('objection'); + +const mixins = compose( + SomeMixin, + SomeOtherMixin, + EvenMoreMixins, + LolSoManyMixins, + ImAMixinWithOptions({ foo: 'bar' }) +); + +class Person extends mixins(Model) {} + +@SomeMixin +@MixinWithOptions({ foo: 'bar' }) +class Person extends Model {} +``` +从构思上来说,可改造空间大,也可以巧妙的将各种配置 JSON 化,从而达到动态生成的 Model 目的。 +### Bookshelf.js +基于 Knex,也是类 JSON 的配置风格,一些非常流行的开源项目 Ghost、Strapi 就用的它。 +```typescript +const knex = require('knex')({ + client: 'mysql', + connection: process.env.MYSQL_DATABASE_CONNECTION +}) +const bookshelf = require('bookshelf')(knex) + +const User = bookshelf.model('User', { + tableName: 'users', + posts() { + return this.hasMany(Posts) + } +}) + +const Post = bookshelf.model('Post', { + tableName: 'posts', + tags() { + return this.belongsToMany(Tag) + } +}) + +const Tag = bookshelf.model('Tag', { + tableName: 'tags' +}) + +``` +同样支持动态化改造。Bookshelf 的可改造空间巨大,不过看似作者已经不再维护了。 +### 总结 +到底哪个好呢?**仅从动态化 Model/Entity 角度来说:** + +- Sequelize 细节做的最好 +- Typeorm 非常活跃,才迭代了 v0.2.38,但 stars 就已经超 25k+ 以上所有 ORM 关注度最多,装饰器的风格也被大家所喜爱。但是 EntitySchema 的细节做的还不够,需要进一步优化和改造,存在许多未知 +- Objection.js 的构思非常不错,尤其是 mixin,灵活性和可改造性非常强 +- Bookshelf.js 也不错,如果是早几年,Bookshelf 可能会是我的第一选择 +- 至于 Prisma,特立独行的 PSL,深受大家喜爱,但是 PSL 并不支持动态化 Model + +​ + +以上 ORM 都是以关系型数据库为主,不过 Typeorm 和 Prisma 也支持 MongoDB(细节有差异),只支持 MongoDB 的 ORM 不在讨论范围内。 +​ + +## Migration + + +有了 Model/Entity 之后,需要创建对应的数据库表、字段和索引。上文提及的大多数 Model/Entity 都可以详细的描述字段的属性和关系(Model 的 DSL),理论上就可以直接生成表和关系约束了,而并不需要单独再配置 migration 文件了。比如: +### Sequelize +提供了 `sequelize.sync()` 和 `Model.sync()` 方法,可以快速的根据 Model Attributes 生成数据表、字段和索引。sync 提供了丰富的参数,支持删掉重建、只新增不删除、只同步某些表等等处理。 +```typescript +const User = sequelize.define('User', { + // Model attributes are defined here + firstName: { + type: DataTypes.STRING, + allowNull: false + }, + lastName: { + type: DataTypes.STRING + // allowNull defaults to true + } +}, { + // Other model options go here +}); + +await sequelize.sync(); +// Or +await User.sync(); +``` +除了 sync,Sequelize 也提供了 migration 工具,具体写法如: +```typescript +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.createTable('Person', { + name: Sequelize.DataTypes.STRING, + isBetaMember: { + type: Sequelize.DataTypes.BOOLEAN, + defaultValue: false, + allowNull: false + } + }); + }, + down: (queryInterface, Sequelize) => { + return queryInterface.dropTable('Person'); + } +}; +``` +在生产环境或者希望精确控制时,都可以通过 Sequelize 提供的 queryInterface 来处理。 +### Typeorm +没有给力的 sequelize.sync() 方法,但是提供了 synchronize: true 配置参数,效果类似,会自动创建表,因为会重新建表,并不适用于生产环境。生产环境建议使用更为安全的 migration 方式。 +```typescript +createConnection({ + type: "mysql", + host: "localhost", + port: 3306, + username: "root", + password: "admin", + database: "test", + entities: [ + Photo + ], + synchronize: true, + logging: false +}); +``` +Migration 如下,具体的 QueryRunner 细节大家可以看官网,提供了一套标准的 Migration API +```typescript +import {MigrationInterface, QueryRunner, Table, TableIndex, TableColumn, TableForeignKey } from "typeorm"; + +export class QuestionRefactoringTIMESTAMP implements MigrationInterface { + + async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable(new Table({ + name: "question", + columns: [ + { + name: "id", + type: "int", + isPrimary: true + }, + { + name: "name", + type: "varchar", + } + ] + }), true) + + await queryRunner.createIndex("question", new TableIndex({ + name: "IDX_QUESTION_NAME", + columnNames: ["name"] + })); + + await queryRunner.createTable(new Table({ + name: "answer", + columns: [ + { + name: "id", + type: "int", + isPrimary: true + }, + { + name: "name", + type: "varchar", + }, + { + name: 'created_at', + type: 'timestamp', + default: 'now()' + } + ] + }), true); + + await queryRunner.addColumn("answer", new TableColumn({ + name: "questionId", + type: "int" + })); + + await queryRunner.createForeignKey("answer", new TableForeignKey({ + columnNames: ["questionId"], + referencedColumnNames: ["id"], + referencedTableName: "question", + onDelete: "CASCADE" + })); + } + + async down(queryRunner: QueryRunner): Promise { + const table = await queryRunner.getTable("answer"); + const foreignKey = table.foreignKeys.find(fk => fk.columnNames.indexOf("questionId") !== -1); + await queryRunner.dropForeignKey("answer", foreignKey); + await queryRunner.dropColumn("answer", "questionId"); + await queryRunner.dropTable("answer"); + await queryRunner.dropIndex("question", "IDX_QUESTION_NAME"); + await queryRunner.dropTable("question"); + } + +} +``` +### Knex +Objection.js 和 Bookshelf 都基于 Knex,所以 Migration 都使用的 Knex。 +```typescript +knex.schema.createTable('users', function (table) { + table.increments(); + table.string('name'); + table.timestamps(); +}) +// Outputs: +// create table `users` (`id` int unsigned not null auto_increment primary key, `name` varchar(255), `created_at` datetime, `updated_at` datetime) +``` +Knex 和 Laravel 的 QueryBuilder 非常接近,Schema Builder 也如此。单纯从 Migration API 设计来说,个人更喜欢这种语法风格,干净、简洁。 +### Prisma +Migration 的思路与上述的做法不同,Prisma 提供了完整的配置 Model 的 PSL 语法,为开发环境提供了 migrate dev 支持,每次修改 PSL 文件之后,可以执行 migrate dev 生成对应变更的 sql 文件,生产环境再执行 migrate 命令来同步修改,详情查看官方文档 [Prisma Migrate](https://www.prisma.io/docs/concepts/components/prisma-migrate) 章节 +```bash +migrations/ +└─ 20210305110829_first_migration/ + └─ migration.sql +└─ 20210305120829_add_fields/ + └─ migration.sql +└─ 20210308102042_type-change/ + └─ migration.sql +``` +### 总结 + +- 流程上,Prisma 的做法最省事儿,只配置一份 PSL,每次修改之后,通过 migrate dev 命令生成 sql,无需修改。 +- 其次是 Sequelize,提供了 sequelize.sync 方法,配置的 Model 都可以通过 sync 方法同步给数据库。但是这种方式有些暴力和冗余,如果能稍加改进就更好了。 +- 至于传统的 Migration 做法,配置 Model 已经写了一份 DSL 了,配置 Migration 再写另外一份 DSL,非常不友好,无代码改造也非常困难。 + + + +**怎么无代码改造来解决 Migration 问题呢?** +Sequelize 的方案非常不错,虽然有些暴力和冗余,但是可以再稍加改进,尤其是需要达到生产环境的精准控制。 +​ + +**sequelize.sync 是否会有安全问题呢?** +因为 sync 支持 force: true 参数,会强制删除重建,在生产环境要关掉。 +修改了 Model 的 DSL,在 sync 里怎么判断是创建、修改或删除呢?篇幅有限这里就不细说了。 + + +## QueryBuilder、EntityManager、Repository、CRUD API + + +有了 Model/Entity,也创建数据表和字段了,那接下来就能操控数据库了。Model/Entity 常见有两种模式 Active Record 和 Data Mapper,Typeorm 支持的最完整,所以我们先来看看 Typeorm 吧。 +### Typeorm + +1. Active Record 模式 +```typescript +import {BaseEntity, Entity, PrimaryGeneratedColumn, Column} from "typeorm"; + +@Entity() +export class User extends BaseEntity { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + firstName: string; + + @Column() + lastName: string; + + @Column() + isActive: boolean; + + static findByName(firstName: string, lastName: string) { + return this.createQueryBuilder("user") + .where("user.firstName = :firstName", { firstName }) + .andWhere("user.lastName = :lastName", { lastName }) + .getMany(); + } +} + +// example how to save AR entity +const user = new User(); +user.firstName = "Timber"; +user.lastName = "Saw"; +user.isActive = true; +await user.save(); + +// example how to remove AR entity +await user.remove(); + +// example how to load AR entities +const users = await User.find({ skip: 2, take: 5 }); +const newUsers = await User.find({ isActive: true }); +const timber = await User.findOne({ firstName: "Timber", lastName: "Saw" }); +const timber = await User.findByName("Timber", "Saw"); +``` + +2. Data Mapper 模式 +```typescript +import {Entity, EntityRepository, Repository, PrimaryGeneratedColumn, Column} from "typeorm"; + +@Entity() +export class User { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + firstName: string; + + @Column() + lastName: string; + + @Column() + isActive: boolean; + +} + +// custom repository +@EntityRepository() +export class UserRepository extends Repository { + + findByName(firstName: string, lastName: string) { + return this.createQueryBuilder("user") + .where("user.firstName = :firstName", { firstName }) + .andWhere("user.lastName = :lastName", { lastName }) + .getMany(); + } + +} + +const userRepository = connection.getRepository(User); + +// example how to save DM entity +const user = new User(); +user.firstName = "Timber"; +user.lastName = "Saw"; +user.isActive = true; +await userRepository.save(user); + +// example how to remove DM entity +await userRepository.remove(user); + +// example how to load DM entities +const users = await userRepository.find({ skip: 2, take: 5 }); +const newUsers = await userRepository.find({ isActive: true }); +const timber = await userRepository.findOne({ firstName: "Timber", lastName: "Saw" }); + +// custom repository +const userRepository = connection.getCustomRepository(UserRepository); +const timber = await userRepository.findByName("Timber", "Saw"); +``` +从无代码、低代码改造角度来说,Data Mapper 模式更适合,通过 EntitySchema 生成 Entity,再交给 EntityManager 或 Repository 来处理数据的增删改查,也可以根据需要自定义 Repository。这就是低代码的扩展能力,通用的 Repository 处理常规需求,特殊需求自定义扩展。除了常用的 CRUD API,Typeorm 也提供了强大的 QueryBuilder 来自定义其他 API,如上文例子的 findByName。 +### Sequelize +Sequelize 的 Model 是 Active Record 模式,提供了常用的 CRUD API,如: +```ts +const jane = await User.create({ name: "Jane" }); +console.log(jane.name); // "Jane" +jane.name = "Ada"; +// the name is still "Jane" in the database +await jane.save(); +// Now the name was updated to "Ada" in the database! + +Model.findAll({ + attributes: ['foo', 'bar'] +}); +// SELECT foo, bar FROM ... + +Post.findAll({ + where: { + authorId: 2 + } +}); +// SELECT * FROM post WHERE authorId = 2 + +const { Op } = require("sequelize"); +Post.findAll({ + where: { + [Op.and]: [{ a: 5 }, { b: 6 }], // (a = 5) AND (b = 6) + [Op.or]: [{ a: 5 }, { b: 6 }], // (a = 5) OR (b = 6) + someAttribute: { + // Basics + [Op.eq]: 3, // = 3 + [Op.ne]: 20, // != 20 + [Op.is]: null, // IS NULL + [Op.not]: true, // IS NOT TRUE + [Op.or]: [5, 6], // (someAttribute = 5) OR (someAttribute = 6) + + // Using dialect specific column identifiers (PG in the following example): + [Op.col]: 'user.organization_id', // = "user"."organization_id" + + // Number comparisons + [Op.gt]: 6, // > 6 + [Op.gte]: 6, // >= 6 + [Op.lt]: 10, // < 10 + [Op.lte]: 10, // <= 10 + [Op.between]: [6, 10], // BETWEEN 6 AND 10 + [Op.notBetween]: [11, 15], // NOT BETWEEN 11 AND 15 + + // Other operators + + [Op.all]: sequelize.literal('SELECT 1'), // > ALL (SELECT 1) + + [Op.in]: [1, 2], // IN [1, 2] + [Op.notIn]: [1, 2], // NOT IN [1, 2] + + [Op.like]: '%hat', // LIKE '%hat' + [Op.notLike]: '%hat', // NOT LIKE '%hat' + [Op.startsWith]: 'hat', // LIKE 'hat%' + [Op.endsWith]: 'hat', // LIKE '%hat' + [Op.substring]: 'hat', // LIKE '%hat%' + [Op.iLike]: '%hat', // ILIKE '%hat' (case insensitive) (PG only) + [Op.notILike]: '%hat', // NOT ILIKE '%hat' (PG only) + [Op.regexp]: '^[h|a|t]', // REGEXP/~ '^[h|a|t]' (MySQL/PG only) + [Op.notRegexp]: '^[h|a|t]', // NOT REGEXP/!~ '^[h|a|t]' (MySQL/PG only) + [Op.iRegexp]: '^[h|a|t]', // ~* '^[h|a|t]' (PG only) + [Op.notIRegexp]: '^[h|a|t]', // !~* '^[h|a|t]' (PG only) + + [Op.any]: [2, 3], // ANY ARRAY[2, 3]::INTEGER (PG only) + + // In Postgres, Op.like/Op.iLike/Op.notLike can be combined to Op.any: + [Op.like]: { [Op.any]: ['cat', 'hat'] } // LIKE ANY ARRAY['cat', 'hat'] + + // There are more postgres-only range operators, see below + } + } +}); +``` +常规的 CRUD API 支持的还不错,但是自定义查询或者基于 QueryBuilder 实现更复杂查询的支持就弱爆了,比如: +```ts +Post.findAll({ + where: { + [Op.or]: [ + sequelize.where(sequelize.fn('char_length', sequelize.col('content')), 7), + { + content: { + [Op.like]: 'Hello%' + } + }, + { + [Op.and]: [ + { status: 'draft' }, + sequelize.where(sequelize.fn('char_length', sequelize.col('content')), { + [Op.gt]: 10 + }) + ] + } + ] + } +}); + +Model.findAll({ + attributes: [ + 'id', 'foo', 'bar', 'baz', 'qux', 'hats', // We had to list all attributes... + [sequelize.fn('COUNT', sequelize.col('hats')), 'n_hats'] // To add the aggregation... + ] +}); + +// This is shorter, and less error prone because it still works if you add / remove attributes from your model later +Model.findAll({ + attributes: { + include: [ + [sequelize.fn('COUNT', sequelize.col('hats')), 'n_hats'] + ] + } +}); +``` +Sequelize 在配置 Model 和 Migration 的表现上都比较不错,但是在 QueryBuilder 的支持上弱爆了,几乎不解决各数据库的兼容性问题,提供的 queryInterface 也非常难用,一点也不 SQL-Friendly。 +### Prisma +提供了 Prisma Client 用于支持数据的 CRUD,用法与 Sequelize 相似。没有提供 QueryBuilder 如果常规 CRUD API 支持的不好,可能很难改造,这部分了解的不多,细节存在非常多未知。感兴趣的看官网 [Prisma Client](https://www.prisma.io/docs/concepts/components/prisma-client) 章节。 +```ts +const user = await prisma.user.create({ + data: { + email: 'elsa@prisma.io', + name: 'Elsa Prisma', + }, +}) + +const getPosts = await prisma.post.findMany({ + where: { + title: { + contains: 'cookies', + }, + }, + include: { + author: true, // Return all fields + }, +}) +``` +这种风格的 CRUD API 非常不利于调试,一旦出现问题将很难改造。 +### Objection.js +得益于 Knex,Objection 在 QueryBuilder 的表现上非常不错 SQL-Friendly,可造性非常强。 +```ts +const jennifer = await Person.query().insert({ + firstName: 'Jennifer', + lastName: 'Lawrence' +}); +// insert into "persons" ("firstName", "lastName") values ('Jennifer', 'Lawrence') + +console.log(jennifer instanceof Person); // --> true +console.log(jennifer.firstName); // --> 'Jennifer' +console.log(jennifer.fullName()); // --> 'Jennifer Lawrence' + +const numUpdated = await Person.query() + .patch({ lastName: 'Dinosaur' }) + .where('age', '>', 60); +// update "persons" set "lastName" = 'Dinosaur' where "age" > 60 + +console.log(numUpdated, 'people were updated'); + +const middleAgedJennifers = await Person.query() + .select('age', 'firstName', 'lastName') + .where('age', '>', 40) + .where('age', '<', 60) + .where('firstName', 'Jennifer') + .orderBy('lastName'); + +console.log('The last name of the first middle aged Jennifer is'); +console.log(middleAgedJennifers[0].lastName); + +// select "age", "firstName", "lastName" +// from "persons" +// where "age" > 40 +// and "age" < 60 +// and "firstName" = 'Jennifer' +// order by "lastName" asc +``` +### Bookshelf +一样基于 Knex,细节就不一一罗列了 +​ + +### 总结 +除了常规的增删改查,还有一个非常重要的能力就是关系数据的处理能力,关系数据的 eager loading 也是各 ORM 永恒的话题,这里就不细说了 + + +- 从完整性来说,Typeorm 的最完整 +- 如果说有什么理由让我选择 Objection.js,我会说它的 QueryBuilder 非常给力,写起来非常舒服,可造性也非常强 +- Sequelize 的常规 CRUD API 并没有太大问题,但是在关系数据的处理上问题太多了。而且黑箱设计的 CRUD API,非常难以调试和改造 +- Prisma 的 CRUD API 与 Sequelize 相似,这让我非常担心它是否也会有 Sequelize 的各种糟心问题 +- 至于 Bookshelf,已经不维护了就不深入讨论了,Objection.js 是类似最好的替代品 + +​ + +## 如何选择? + +- 综合实力 Typeorm 最强,各方面表现的都不差,但细节还差那么点,需要更深入使用才能知道细节表现力,存在非常多未知,但因为社区活跃,成长空间巨大 +- 从无代码改造角度来说,Sequelize 工作量是最少的,尤其是给力的 sync 方法。但我非常不喜欢它的 QueryBuilder 设计。Sequelize 的社区活跃度也很高,但是核心团队看起来出现了些问题 [issue #12956](https://github.com/sequelize/sequelize/issues/12956) +- 我非常喜欢 Objection.js 在 QueryBuilder 上的表现力,可造性非常强,但是要完整的无代码支持,工作量也非常多 + +​ + +**在这样的大环境下,无代码的 Database 要如何选择 ORM 和改造呢?** +综合考虑,Sequelize 最适合作为蓝本,基于 Sequelize 优先实现第一版,即使 Sequelize 存在问题,后续也可以替换为 Typeorm 或 Objection.js 等。 + + +## 改造开始 +### Collection +NocoBase 首先基于 Sequelize 的 ModelOptions、 ModelAttributes、ModelAssociations 提炼了一套更适合无代码配置的 JSON 风格的配置协议,称之为 Collection Schema Language,简称 CSL。示例如下: +```typescript +// 用户 +db.collection({ + name: 'users', + fields: { + username: { type: 'string', unique: true }, + password: { type: 'password', unique: true }, + posts: { type: 'hasMany' }, + }, +}); + +// 文章 +db.collection({ + name: 'posts', + fields: { + title: 'string', + content: 'text', + tags: 'belongsToMany', + comments: 'hasMany', + author: { type: 'belongsTo', target: 'users' }, + }, +}); + +// 标签 +db.collection({ + name: 'tags', + fields: [ + { type: 'string', name: 'name' }, + { type: 'belongsToMany', name: 'posts' }, + ], +}); + +// 评论 +db.collection({ + name: 'comments', + fields: [ + { type: 'text', name: 'content' }, + { type: 'belongsTo', name: 'user' }, + ], +}); +``` +与现在流行的装饰器配置的结构很像,只不过它是用 JSON 编写的,便于动态生成 Model。与 Typeorm 的 EntitySchema 做法也非常接近。 +​ + +**那为什么不直接使用 EntitySchema 呢?** +一方面对 EntitySchema 细节表现力如何未知,另一方面需要考虑后续的自定义字段需求,尤其是 UISchema 的扩展能力,自成一体的 CSL 更适合。而且结构上与 Typeorm 和 Prisma 的装饰器风格非常接近,学习成本并不高。 +### Model & Repository +配置好了 CSL 之后,会根据它自动初始化 ORM 的 Model(Active Record)和 Repository(Data Mapper): +```typescript +const User = app.collection({ + name: 'users', + fields: { + username: { type: 'string', unique: true }, + password: { type: 'password', unique: true }, + posts: { type: 'hasMany' }, + }, +}); + +User.model; +User.repository; +``` +Model 的具体形态取决于适配的 ORM,保留原滋原味,细节就不多说了。Repository 自成一体,提供更适合 NocoBase 的 CRUD API。Model 和 Repository 都可以自定义,如: +```ts +class UserModel extends Model { + +} + +class UserRepository extends Repository { + +} + +const User = app.collection({ + name: 'users', + model: UserModel, + repository: UserRepository, + fields: { + username: { type: 'string', unique: true }, + password: { type: 'password', unique: true }, + posts: { type: 'hasMany' }, + }, +}); +``` +支持 Model 也支持 Repository,与 Typeorm 的做法非常接近,但是 Repository 并不是 Typeorm 的 Repository,我们来看看 Repository API 吧。 +### Repository API +NocoBase 的 Repository API 有: + +- repository.findMany() +- repository.findOne() +- repository.create() +- repository.update() +- repository.destroy() +- repository.relation().of() + - findMany() + - findOne() + - create() + - update() + - destroy() + - set() + - add() + - remove() + - toggle() +#### findMany +```ts +repository.findMany({ + // 过滤 + filter: { + $and: [{ a: 5 }, { b: 6 }], // (a = 5) AND (b = 6) + $or: [{ a: 5 }, { b: 6 }], // (a = 5) OR (b = 6) + someAttribute: { + // Basics + $eq: 3, // = 3 + $ne: 20, // != 20 + $is: null, // IS NULL + $not: true, // IS NOT TRUE + $gt: 6, // > 6 + $gte: 6, + }, + 'someAttribute.$eq': 3, + 'nested.someAttribute': { + // + }, + nested: { + someAttribute: {}, + }, + }, + // 字段白名单 + fields: [], + // 附加字段,主要用于附加关系字段 + appends: [], + // 字段黑名单 + expect: [], + sort: [], + page: 1, + pageSize: 2, +}); +``` +#### findOne +```typescript +repository.findOne({ + // 过滤 + filter: {}, + // 为更快速的 pk 过滤提供 + filterByPk: 1, // 通常情况等同于 filter: {id: 1} + // 字段白名单 + fields: [], + // 附加字段,主要用于附加关系字段 + appends: [], + // 字段黑名单 + expect: [], +}); +``` +#### create +```ts +repository.create({ + // 待存数据 + values: { + a: 'a', + // 快速建立关联 + o2o: 1, // 建立一对一关联 + m2o: 1, // 建立多对一关联 + o2m: [1,2] // 建立一对多关联 + m2m: [1,2] // 建立多对多关联 + // 新建关联数据并建立关联 + o2o: { + key1: 'val1', + }, + o2m: [{key1: 'val1'}, {key2: 'val2'}], + // 子表格数据 + subTable: [ + // 如果数据存在,更新处理 + {id: 1, key1: 'val1111'}, + // 如果数据不存在,直接创建并关联 + {key2: 'val2'}, + ], + }, + // 字段白名单 + whitelist: [], + // 字段黑名单 + blacklist: [], + // 关系数据默认会新建并建立关联处理,如果是已存在的数据只关联,但不更新关系数据 + // 如果需要更新关联数据,可以通过 updateAssociations 指定 + updateAssociations: ['subTable'], +}); +``` +#### update +```ts +repository.update({ + // 待更新数据 + values: {}, + // 过滤,哪些数据要更新 + filter: {}, + // 为更快速的 pk 过滤提供 + filterByPk: 1, // 通常情况等同于 filter: {id: 1} + // 字段白名单 + whitelist: [], + // 字段黑名单 + blacklist: [], + // 指定需要更新数据的关联字段 + updateAssociations: [], +}); +``` +#### destroy +```ts +// 特定 primary key 值 +repository.destroy(1); + +// 批量 primary key 值 +repository.destroy([1, 2, 3]); + +// 复杂的 filter +repository.destroy({ + filter: {}, +}); +``` +#### relation +关系数据的 CRUD,用法与常规 Repository 的一致 +```ts +// user_id = 1 的 post 的 repository +const userPostsRepository = repository.relation('posts').of(1); + +userPostsRepository.findMany({ + +}); + +userPostsRepository.findOne({ + +}); + +userPostsRepository.create({ + values: {}, +}); + +userPostsRepository.update({ + values: {}, +}); + +userPostsRepository.destroy({ + +}); +``` +关联操作,只处理关系约束的建立与解除 +```ts +// user_id = 1 的 post 的 relatedQuery +const userPostsRepository = repository.relation('posts').of(1); + +// 建立关联 +userPostsRepository.set(1); + +// 批量,仅用于 HasMany 和 BelongsToMany +userPostsRepository.set([1,2,3]); + +// BelongsToMany 的中间表 +userPostsRepository.set([ + [1, {/* 中间表数据 */}], + [2, {/* 中间表数据 */}], + [3, {/* 中间表数据 */}], +]); + +// 仅用于 HasMany 和 BelongsToMany +userPostsRepository.add(1); + +// BelongsToMany 的中间表 +userPostsRepository.add(1, {/* 中间表数据 */}); + +// 删除关联 +userPostsRepository.remove(1); + +// 建立或解除 +userPostsRepository.toggle(1); +userPostsRepository.toggle([1, 2, 3]); +``` +### Collection Sync +在 Migration 章节,介绍了各种 ORM 的 Migration 做法。得出结论 Sequelize.sync() 的方案较优,但不够精细,而且有些暴力,不过也没有关系,Collection 也打算这样做,再进一步的改进 sync 的细节,流程上就变得非常友好了。 +​ + +只执行某个 collection 的 sync。虽然有 hasMany 的 posts,但因为关系表不存在并不会创建 +```ts +const User = db.collection({ + name: 'users', + fields: { + username: { type: 'string', unique: true }, + password: { type: 'password', unique: true }, + posts: { type: 'hasMany' }, + }, +}); + +await User.sync(); +``` +我们也可以通过 db.sync 批量的将多个 collections 同步给数据库,通常不需要关注建表顺序、关系主外键和关系约束的先后顺序等等,collection 内部通通自动帮你处理好。比如下面例子: + +- 不需要特意声明外键 user_id +- 不需要考虑关系外键要怎么建立,在哪里建立,也不需要考虑顺序问题 +- 自动创建多对多中间表以及相关外键及约束 +```ts +// 文章 +db.collection({ + name: 'posts', + fields: { + title: 'string', + content: 'text', + tags: 'belongsToMany', + comments: 'hasMany', + author: { type: 'belongsTo', target: 'users' }, + }, +}); + +// 用户 +db.collection({ + name: 'users', + fields: { + username: { type: 'string', unique: true }, + password: { type: 'password', unique: true }, + posts: { type: 'hasMany' }, + }, +}); + +// 标签 +db.collection({ + name: 'tags', + fields: [ + { type: 'string', name: 'name' }, + { type: 'belongsToMany', name: 'posts' }, + ], +}); + +// 评论 +db.collection({ + name: 'comments', + fields: [ + { type: 'text', name: 'content' }, + { type: 'belongsTo', name: 'user' }, + ], +}); + +await db.sync(); +``` +### 动态化 +为了更好的支持动态配置 collection,提供了以下 API: + +- db.collection 创建 +- db.getCollection 获取 +- collection.mergeOptions 和 collection 配置,不删除 +- collection.hasField +- collection.getField +- collection.addField +- collection.setFields +- collection.mergeField 合并参数,如果 field 不存在则添加 +- collection.removeField 移除 +- collection.sync + +备注:name 是非常重要的标识,但如果涉及 name 的修改如何处理比较合适? +​ + +有了 Collection API 就可以实现动态 Collection 了。解决动态数据的持久化问题,可以将数据储存在数据表里。为此,我们可以创建 collections 和 fields 两张表。 +```ts +const Collection = db.collection({ + name: 'collections', + fields: [ + { name: 'name', type: 'string', unique: true }, + { name: 'fields', type: 'hasMany', foreignKey: 'collectionName' }, + ], +}); + +const Field = db.collection({ + name: 'fields', + fields: [ + { name: 'name', type: 'string' }, + ], +}); + +db.on('collections.afterCreate', async (model) => { + const collection = db.collection(model.toJSON()); // 实际可能不是用 model.toJSON() 方法 + await collection.sync(); +}); + +db.on('collections.afterUpdate', async (model) => { + const collection = db.getCollection(model.get('name')); + // 更新配置 + collection.mergeOptions(model.get()); + await collection.sync(); +}); + +db.on('fields.afterCreate', async (model) => { + const collection = db.getCollection(model.get('collectionName')); + // 新增字段 + collection.addField(model.toJSON()); // 实际可能不是用 model.toJSON() 方法 + await collection.sync(); +}); + +db.on('fields.afterUpdate', async (model) => { + const collection = db.getCollection(model.get('collectionName')); + // 更新字段配置 + collection.mergeField(model.toJSON()); // 实际可能不是用 model.toJSON() 方法 + await collection.sync(); +}); +``` +备注:db.on 之后会另起一篇和 app.on 一起介绍 +​ + +这样就可以用 Collection.repository.create() 来动态创建表了,比如: +```ts +await Collection.repository.create({ + values: { + name: 'test', + fields: [ + { name: 'name', type: 'string' }, + ], + }, +}); +``` +以上就是 plugin-collections 的核心实现逻辑了。 diff --git a/docs/guide/kernel-principle/choose-an-orm.zh-CN.md b/docs/guide/kernel-principle/choose-an-orm.zh-CN.md new file mode 100644 index 0000000000..ebe46302e6 --- /dev/null +++ b/docs/guide/kernel-principle/choose-an-orm.zh-CN.md @@ -0,0 +1,1268 @@ +--- +order: 4 +--- + +# 如何选择 ORM + +本篇文章,需要读者至少熟悉一种较流行的 ORM,对 Model、Migration、QueryBuilder、Repository 也有所了解。在正式介绍 NocoBase 的 Database 设计之前,先来看看大部分 ORM 都有的三个概念: + +- Model(ModelAttributes)、Entity(EntitySchema):将数据表与模型类或实体类对应起来 +- Migration、Sync API:用于创建、修改、删除数据库表、字段、索引等 +- QueryBuilder、EntityManager、Repository、CRUD API:提供增删改查 + + + +## Model/Entity +无代码的第一个改造,**Model 动态化**。 +​ + +简单来说 Model/Entity 的作用就是将数据表、字段、索引、关系映射到类、属性或方法上。我们先来看看 Node.js 里各个 ORM 都是怎么做的。 +### Typeorm +在 Typeorm 里叫 Entity,通过类属性映射表字段,装饰器风格来配置字段属性 +```typescript +import { Entity, Column, PrimaryGeneratedColumn } from "typeorm"; + +@Entity() +export class Photo { + + @PrimaryGeneratedColumn() + id: number; + + @Column({ + length: 100 + }) + name: string; + + @Column("text") + description: string; + + @Column() + filename: string; + + @Column("double") + views: number; + + @Column() + isPublished: boolean; +} +``` +这种写法,如果 Entity 有修改,需要修改代码,不过 Typeorm 提供了 JSON 风格的 EntitySchema +```typescript +import {EntitySchema} from "typeorm"; + +export const CategoryEntity = new EntitySchema({ + name: "category", + columns: { + id: { + type: Number, + primary: true, + generated: true + }, + name: { + type: String + } + } +}); + +export const PostEntity = new EntitySchema({ + name: "post", + columns: { + id: { + type: Number, + primary: true, + generated: true + }, + title: { + type: String + }, + text: { + type: String + } + }, + relations: { + categories: { + type: "many-to-many", + target: "category" // CategoryEntity + } + } +}); +``` +修改 JSON 就容易多了,这种写法非常适用于无代码平台。平台配置 JSON 动态生成对应的 Entity。 +### Prisma +与 Typeorm 装饰器的风格非常接近,但不同的是 Prisma 另辟蹊径,提供了自成一套的 PSL(Prisma Schema Language): +```ts +datasource db { + url = env("DATABASE_URL") + provider = "postgresql" +} + +generator client { + provider = "prisma-client-js" +} + +model User { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + email String @unique + name String? + role Role @default(USER) + posts Post[] +} + +model Post { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + published Boolean @default(false) + title String @db.VarChar(255) + author User? @relation(fields: [authorId], references: [id]) + authorId Int? +} + +enum Role { + USER + ADMIN +} +``` +这种写法,需要修改 PSL 文件,除非自己实现一套 PSL 解析与生成器,不然没办法直接无代码改造。 +### Sequelize +作为老牌 ORM,下载量和使用量也是惊人,提供了多种配置 Model 的风格: + +1. 传统的 JSON 风格 +```ts +sequelize.define('User', { + // Model attributes are defined here + firstName: { + type: DataTypes.STRING, + allowNull: false + }, + lastName: { + type: DataTypes.STRING + // allowNull defaults to true + } +}, { + // model options +}); +``` + +2. 改进之后的 Model 风格 +```ts +const { Sequelize, DataTypes, Model } = require('sequelize'); +const sequelize = new Sequelize('sqlite::memory'); + +class User extends Model {} + +User.init({ + // Model attributes are defined here + firstName: { + type: DataTypes.STRING, + allowNull: false + }, + lastName: { + type: DataTypes.STRING + // allowNull defaults to true + } +}, { + // Other model options go here + sequelize, // We need to pass the connection instance + modelName: 'User' // We need to choose the model name +}); + +// the defined model is the class itself +console.log(User === sequelize.models.User); // true +``` + +3. 装饰器风格 +```ts +import { Table, Column, Model, HasMany } from 'sequelize-typescript' + +@Table +class Person extends Model { + @Column + name: string + + @Column + birthday: Date + + @HasMany(() => Hobby) + hobbies: Hobby[] +} +``` +Sequelize 可以使用 JSON 风格配置 Model,非常适用于无代码平台。平台配置 JSON 动态生成对应的 Model。 +### Objection.js +基于 Knex,Model 如下: +```typescript +const { Model } = require('objection'); + +class Person extends Model { + // Table name is the only required property. + static get tableName() { + return 'persons'; + } + + // Each model must have a column (or a set of columns) that uniquely + // identifies the rows. The column(s) can be specified using the `idColumn` + // property. `idColumn` returns `id` by default and doesn't need to be + // specified unless the model's primary key is something else. + static get idColumn() { + return 'id'; + } + + // Methods can be defined for model classes just as you would for + // any JavaScript class. If you want to include the result of these + // methods in the output json, see `virtualAttributes`. + fullName() { + return this.firstName + ' ' + this.lastName; + } + + // Optional JSON schema. This is not the database schema! + // No tables or columns are generated based on this. This is only + // used for input validation. Whenever a model instance is created + // either explicitly or implicitly it is checked against this schema. + // See http://json-schema.org/ for more info. + static get jsonSchema() { + return { + type: 'object', + required: ['firstName', 'lastName'], + + properties: { + id: { type: 'integer' }, + parentId: { type: ['integer', 'null'] }, + firstName: { type: 'string', minLength: 1, maxLength: 255 }, + lastName: { type: 'string', minLength: 1, maxLength: 255 }, + age: { type: 'number' }, + + // Properties defined as objects or arrays are + // automatically converted to JSON strings when + // writing to database and back to objects and arrays + // when reading from database. To override this + // behaviour, you can override the + // Model.jsonAttributes property. + address: { + type: 'object', + properties: { + street: { type: 'string' }, + city: { type: 'string' }, + zipCode: { type: 'string' } + } + } + } + }; + } + + // This object defines the relations to other models. + static get relationMappings() { + // Importing models here is a one way to avoid require loops. + const Animal = require('./Animal'); + const Movie = require('./Movie'); + + return { + pets: { + relation: Model.HasManyRelation, + // The related model. This can be either a Model + // subclass constructor or an absolute file path + // to a module that exports one. We use a model + // subclass constructor `Animal` here. + modelClass: Animal, + join: { + from: 'persons.id', + to: 'animals.ownerId' + } + }, + + movies: { + relation: Model.ManyToManyRelation, + modelClass: Movie, + join: { + from: 'persons.id', + // ManyToMany relation needs the `through` object + // to describe the join table. + through: { + // If you have a model class for the join table + // you need to specify it like this: + // modelClass: PersonMovie, + from: 'persons_movies.personId', + to: 'persons_movies.movieId' + }, + to: 'movies.id' + } + }, + + children: { + relation: Model.HasManyRelation, + modelClass: Person, + join: { + from: 'persons.id', + to: 'persons.parentId' + } + }, + + parent: { + relation: Model.BelongsToOneRelation, + modelClass: Person, + join: { + from: 'persons.parentId', + to: 'persons.id' + } + } + }; + } +} +``` +直观感受,并没有 Typeorm、Prisma、Sequelize 精炼。不过 Objection.js 提供了 mixin,也可以将 Model 进一步抽象,代码就精炼多了,也可以支持装饰器风格,比如这样: +```ts +const { compose, Model } = require('objection'); + +const mixins = compose( + SomeMixin, + SomeOtherMixin, + EvenMoreMixins, + LolSoManyMixins, + ImAMixinWithOptions({ foo: 'bar' }) +); + +class Person extends mixins(Model) {} + +@SomeMixin +@MixinWithOptions({ foo: 'bar' }) +class Person extends Model {} +``` +从构思上来说,可改造空间大,也可以巧妙的将各种配置 JSON 化,从而达到动态生成的 Model 目的。 +### Bookshelf.js +基于 Knex,也是类 JSON 的配置风格,一些非常流行的开源项目 Ghost、Strapi 就用的它。 +```typescript +const knex = require('knex')({ + client: 'mysql', + connection: process.env.MYSQL_DATABASE_CONNECTION +}) +const bookshelf = require('bookshelf')(knex) + +const User = bookshelf.model('User', { + tableName: 'users', + posts() { + return this.hasMany(Posts) + } +}) + +const Post = bookshelf.model('Post', { + tableName: 'posts', + tags() { + return this.belongsToMany(Tag) + } +}) + +const Tag = bookshelf.model('Tag', { + tableName: 'tags' +}) + +``` +同样支持动态化改造。Bookshelf 的可改造空间巨大,不过看似作者已经不再维护了。 +### 总结 +到底哪个好呢?**仅从动态化 Model/Entity 角度来说:** + +- Sequelize 细节做的最好 +- Typeorm 非常活跃,才迭代了 v0.2.38,但 stars 就已经超 25k+ 以上所有 ORM 关注度最多,装饰器的风格也被大家所喜爱。但是 EntitySchema 的细节做的还不够,需要进一步优化和改造,存在许多未知 +- Objection.js 的构思非常不错,尤其是 mixin,灵活性和可改造性非常强 +- Bookshelf.js 也不错,如果是早几年,Bookshelf 可能会是我的第一选择 +- 至于 Prisma,特立独行的 PSL,深受大家喜爱,但是 PSL 并不支持动态化 Model + +​ + +以上 ORM 都是以关系型数据库为主,不过 Typeorm 和 Prisma 也支持 MongoDB(细节有差异),只支持 MongoDB 的 ORM 不在讨论范围内。 +​ + +## Migration + + +有了 Model/Entity 之后,需要创建对应的数据库表、字段和索引。上文提及的大多数 Model/Entity 都可以详细的描述字段的属性和关系(Model 的 DSL),理论上就可以直接生成表和关系约束了,而并不需要单独再配置 migration 文件了。比如: +### Sequelize +提供了 `sequelize.sync()` 和 `Model.sync()` 方法,可以快速的根据 Model Attributes 生成数据表、字段和索引。sync 提供了丰富的参数,支持删掉重建、只新增不删除、只同步某些表等等处理。 +```typescript +const User = sequelize.define('User', { + // Model attributes are defined here + firstName: { + type: DataTypes.STRING, + allowNull: false + }, + lastName: { + type: DataTypes.STRING + // allowNull defaults to true + } +}, { + // Other model options go here +}); + +await sequelize.sync(); +// Or +await User.sync(); +``` +除了 sync,Sequelize 也提供了 migration 工具,具体写法如: +```typescript +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.createTable('Person', { + name: Sequelize.DataTypes.STRING, + isBetaMember: { + type: Sequelize.DataTypes.BOOLEAN, + defaultValue: false, + allowNull: false + } + }); + }, + down: (queryInterface, Sequelize) => { + return queryInterface.dropTable('Person'); + } +}; +``` +在生产环境或者希望精确控制时,都可以通过 Sequelize 提供的 queryInterface 来处理。 +### Typeorm +没有给力的 sequelize.sync() 方法,但是提供了 synchronize: true 配置参数,效果类似,会自动创建表,因为会重新建表,并不适用于生产环境。生产环境建议使用更为安全的 migration 方式。 +```typescript +createConnection({ + type: "mysql", + host: "localhost", + port: 3306, + username: "root", + password: "admin", + database: "test", + entities: [ + Photo + ], + synchronize: true, + logging: false +}); +``` +Migration 如下,具体的 QueryRunner 细节大家可以看官网,提供了一套标准的 Migration API +```typescript +import {MigrationInterface, QueryRunner, Table, TableIndex, TableColumn, TableForeignKey } from "typeorm"; + +export class QuestionRefactoringTIMESTAMP implements MigrationInterface { + + async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable(new Table({ + name: "question", + columns: [ + { + name: "id", + type: "int", + isPrimary: true + }, + { + name: "name", + type: "varchar", + } + ] + }), true) + + await queryRunner.createIndex("question", new TableIndex({ + name: "IDX_QUESTION_NAME", + columnNames: ["name"] + })); + + await queryRunner.createTable(new Table({ + name: "answer", + columns: [ + { + name: "id", + type: "int", + isPrimary: true + }, + { + name: "name", + type: "varchar", + }, + { + name: 'created_at', + type: 'timestamp', + default: 'now()' + } + ] + }), true); + + await queryRunner.addColumn("answer", new TableColumn({ + name: "questionId", + type: "int" + })); + + await queryRunner.createForeignKey("answer", new TableForeignKey({ + columnNames: ["questionId"], + referencedColumnNames: ["id"], + referencedTableName: "question", + onDelete: "CASCADE" + })); + } + + async down(queryRunner: QueryRunner): Promise { + const table = await queryRunner.getTable("answer"); + const foreignKey = table.foreignKeys.find(fk => fk.columnNames.indexOf("questionId") !== -1); + await queryRunner.dropForeignKey("answer", foreignKey); + await queryRunner.dropColumn("answer", "questionId"); + await queryRunner.dropTable("answer"); + await queryRunner.dropIndex("question", "IDX_QUESTION_NAME"); + await queryRunner.dropTable("question"); + } + +} +``` +### Knex +Objection.js 和 Bookshelf 都基于 Knex,所以 Migration 都使用的 Knex。 +```typescript +knex.schema.createTable('users', function (table) { + table.increments(); + table.string('name'); + table.timestamps(); +}) +// Outputs: +// create table `users` (`id` int unsigned not null auto_increment primary key, `name` varchar(255), `created_at` datetime, `updated_at` datetime) +``` +Knex 和 Laravel 的 QueryBuilder 非常接近,Schema Builder 也如此。单纯从 Migration API 设计来说,个人更喜欢这种语法风格,干净、简洁。 +### Prisma +Migration 的思路与上述的做法不同,Prisma 提供了完整的配置 Model 的 PSL 语法,为开发环境提供了 migrate dev 支持,每次修改 PSL 文件之后,可以执行 migrate dev 生成对应变更的 sql 文件,生产环境再执行 migrate 命令来同步修改,详情查看官方文档 [Prisma Migrate](https://www.prisma.io/docs/concepts/components/prisma-migrate) 章节 +```bash +migrations/ +└─ 20210305110829_first_migration/ + └─ migration.sql +└─ 20210305120829_add_fields/ + └─ migration.sql +└─ 20210308102042_type-change/ + └─ migration.sql +``` +### 总结 + +- 流程上,Prisma 的做法最省事儿,只配置一份 PSL,每次修改之后,通过 migrate dev 命令生成 sql,无需修改。 +- 其次是 Sequelize,提供了 sequelize.sync 方法,配置的 Model 都可以通过 sync 方法同步给数据库。但是这种方式有些暴力和冗余,如果能稍加改进就更好了。 +- 至于传统的 Migration 做法,配置 Model 已经写了一份 DSL 了,配置 Migration 再写另外一份 DSL,非常不友好,无代码改造也非常困难。 + + + +**怎么无代码改造来解决 Migration 问题呢?** +Sequelize 的方案非常不错,虽然有些暴力和冗余,但是可以再稍加改进,尤其是需要达到生产环境的精准控制。 +​ + +**sequelize.sync 是否会有安全问题呢?** +因为 sync 支持 force: true 参数,会强制删除重建,在生产环境要关掉。 +修改了 Model 的 DSL,在 sync 里怎么判断是创建、修改或删除呢?篇幅有限这里就不细说了。 + + +## QueryBuilder、EntityManager、Repository、CRUD API + + +有了 Model/Entity,也创建数据表和字段了,那接下来就能操控数据库了。Model/Entity 常见有两种模式 Active Record 和 Data Mapper,Typeorm 支持的最完整,所以我们先来看看 Typeorm 吧。 +### Typeorm + +1. Active Record 模式 +```typescript +import {BaseEntity, Entity, PrimaryGeneratedColumn, Column} from "typeorm"; + +@Entity() +export class User extends BaseEntity { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + firstName: string; + + @Column() + lastName: string; + + @Column() + isActive: boolean; + + static findByName(firstName: string, lastName: string) { + return this.createQueryBuilder("user") + .where("user.firstName = :firstName", { firstName }) + .andWhere("user.lastName = :lastName", { lastName }) + .getMany(); + } +} + +// example how to save AR entity +const user = new User(); +user.firstName = "Timber"; +user.lastName = "Saw"; +user.isActive = true; +await user.save(); + +// example how to remove AR entity +await user.remove(); + +// example how to load AR entities +const users = await User.find({ skip: 2, take: 5 }); +const newUsers = await User.find({ isActive: true }); +const timber = await User.findOne({ firstName: "Timber", lastName: "Saw" }); +const timber = await User.findByName("Timber", "Saw"); +``` + +2. Data Mapper 模式 +```typescript +import {Entity, EntityRepository, Repository, PrimaryGeneratedColumn, Column} from "typeorm"; + +@Entity() +export class User { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + firstName: string; + + @Column() + lastName: string; + + @Column() + isActive: boolean; + +} + +// custom repository +@EntityRepository() +export class UserRepository extends Repository { + + findByName(firstName: string, lastName: string) { + return this.createQueryBuilder("user") + .where("user.firstName = :firstName", { firstName }) + .andWhere("user.lastName = :lastName", { lastName }) + .getMany(); + } + +} + +const userRepository = connection.getRepository(User); + +// example how to save DM entity +const user = new User(); +user.firstName = "Timber"; +user.lastName = "Saw"; +user.isActive = true; +await userRepository.save(user); + +// example how to remove DM entity +await userRepository.remove(user); + +// example how to load DM entities +const users = await userRepository.find({ skip: 2, take: 5 }); +const newUsers = await userRepository.find({ isActive: true }); +const timber = await userRepository.findOne({ firstName: "Timber", lastName: "Saw" }); + +// custom repository +const userRepository = connection.getCustomRepository(UserRepository); +const timber = await userRepository.findByName("Timber", "Saw"); +``` +从无代码、低代码改造角度来说,Data Mapper 模式更适合,通过 EntitySchema 生成 Entity,再交给 EntityManager 或 Repository 来处理数据的增删改查,也可以根据需要自定义 Repository。这就是低代码的扩展能力,通用的 Repository 处理常规需求,特殊需求自定义扩展。除了常用的 CRUD API,Typeorm 也提供了强大的 QueryBuilder 来自定义其他 API,如上文例子的 findByName。 +### Sequelize +Sequelize 的 Model 是 Active Record 模式,提供了常用的 CRUD API,如: +```ts +const jane = await User.create({ name: "Jane" }); +console.log(jane.name); // "Jane" +jane.name = "Ada"; +// the name is still "Jane" in the database +await jane.save(); +// Now the name was updated to "Ada" in the database! + +Model.findAll({ + attributes: ['foo', 'bar'] +}); +// SELECT foo, bar FROM ... + +Post.findAll({ + where: { + authorId: 2 + } +}); +// SELECT * FROM post WHERE authorId = 2 + +const { Op } = require("sequelize"); +Post.findAll({ + where: { + [Op.and]: [{ a: 5 }, { b: 6 }], // (a = 5) AND (b = 6) + [Op.or]: [{ a: 5 }, { b: 6 }], // (a = 5) OR (b = 6) + someAttribute: { + // Basics + [Op.eq]: 3, // = 3 + [Op.ne]: 20, // != 20 + [Op.is]: null, // IS NULL + [Op.not]: true, // IS NOT TRUE + [Op.or]: [5, 6], // (someAttribute = 5) OR (someAttribute = 6) + + // Using dialect specific column identifiers (PG in the following example): + [Op.col]: 'user.organization_id', // = "user"."organization_id" + + // Number comparisons + [Op.gt]: 6, // > 6 + [Op.gte]: 6, // >= 6 + [Op.lt]: 10, // < 10 + [Op.lte]: 10, // <= 10 + [Op.between]: [6, 10], // BETWEEN 6 AND 10 + [Op.notBetween]: [11, 15], // NOT BETWEEN 11 AND 15 + + // Other operators + + [Op.all]: sequelize.literal('SELECT 1'), // > ALL (SELECT 1) + + [Op.in]: [1, 2], // IN [1, 2] + [Op.notIn]: [1, 2], // NOT IN [1, 2] + + [Op.like]: '%hat', // LIKE '%hat' + [Op.notLike]: '%hat', // NOT LIKE '%hat' + [Op.startsWith]: 'hat', // LIKE 'hat%' + [Op.endsWith]: 'hat', // LIKE '%hat' + [Op.substring]: 'hat', // LIKE '%hat%' + [Op.iLike]: '%hat', // ILIKE '%hat' (case insensitive) (PG only) + [Op.notILike]: '%hat', // NOT ILIKE '%hat' (PG only) + [Op.regexp]: '^[h|a|t]', // REGEXP/~ '^[h|a|t]' (MySQL/PG only) + [Op.notRegexp]: '^[h|a|t]', // NOT REGEXP/!~ '^[h|a|t]' (MySQL/PG only) + [Op.iRegexp]: '^[h|a|t]', // ~* '^[h|a|t]' (PG only) + [Op.notIRegexp]: '^[h|a|t]', // !~* '^[h|a|t]' (PG only) + + [Op.any]: [2, 3], // ANY ARRAY[2, 3]::INTEGER (PG only) + + // In Postgres, Op.like/Op.iLike/Op.notLike can be combined to Op.any: + [Op.like]: { [Op.any]: ['cat', 'hat'] } // LIKE ANY ARRAY['cat', 'hat'] + + // There are more postgres-only range operators, see below + } + } +}); +``` +常规的 CRUD API 支持的还不错,但是自定义查询或者基于 QueryBuilder 实现更复杂查询的支持就弱爆了,比如: +```ts +Post.findAll({ + where: { + [Op.or]: [ + sequelize.where(sequelize.fn('char_length', sequelize.col('content')), 7), + { + content: { + [Op.like]: 'Hello%' + } + }, + { + [Op.and]: [ + { status: 'draft' }, + sequelize.where(sequelize.fn('char_length', sequelize.col('content')), { + [Op.gt]: 10 + }) + ] + } + ] + } +}); + +Model.findAll({ + attributes: [ + 'id', 'foo', 'bar', 'baz', 'qux', 'hats', // We had to list all attributes... + [sequelize.fn('COUNT', sequelize.col('hats')), 'n_hats'] // To add the aggregation... + ] +}); + +// This is shorter, and less error prone because it still works if you add / remove attributes from your model later +Model.findAll({ + attributes: { + include: [ + [sequelize.fn('COUNT', sequelize.col('hats')), 'n_hats'] + ] + } +}); +``` +Sequelize 在配置 Model 和 Migration 的表现上都比较不错,但是在 QueryBuilder 的支持上弱爆了,几乎不解决各数据库的兼容性问题,提供的 queryInterface 也非常难用,一点也不 SQL-Friendly。 +### Prisma +提供了 Prisma Client 用于支持数据的 CRUD,用法与 Sequelize 相似。没有提供 QueryBuilder 如果常规 CRUD API 支持的不好,可能很难改造,这部分了解的不多,细节存在非常多未知。感兴趣的看官网 [Prisma Client](https://www.prisma.io/docs/concepts/components/prisma-client) 章节。 +```ts +const user = await prisma.user.create({ + data: { + email: 'elsa@prisma.io', + name: 'Elsa Prisma', + }, +}) + +const getPosts = await prisma.post.findMany({ + where: { + title: { + contains: 'cookies', + }, + }, + include: { + author: true, // Return all fields + }, +}) +``` +这种风格的 CRUD API 非常不利于调试,一旦出现问题将很难改造。 +### Objection.js +得益于 Knex,Objection 在 QueryBuilder 的表现上非常不错 SQL-Friendly,可造性非常强。 +```ts +const jennifer = await Person.query().insert({ + firstName: 'Jennifer', + lastName: 'Lawrence' +}); +// insert into "persons" ("firstName", "lastName") values ('Jennifer', 'Lawrence') + +console.log(jennifer instanceof Person); // --> true +console.log(jennifer.firstName); // --> 'Jennifer' +console.log(jennifer.fullName()); // --> 'Jennifer Lawrence' + +const numUpdated = await Person.query() + .patch({ lastName: 'Dinosaur' }) + .where('age', '>', 60); +// update "persons" set "lastName" = 'Dinosaur' where "age" > 60 + +console.log(numUpdated, 'people were updated'); + +const middleAgedJennifers = await Person.query() + .select('age', 'firstName', 'lastName') + .where('age', '>', 40) + .where('age', '<', 60) + .where('firstName', 'Jennifer') + .orderBy('lastName'); + +console.log('The last name of the first middle aged Jennifer is'); +console.log(middleAgedJennifers[0].lastName); + +// select "age", "firstName", "lastName" +// from "persons" +// where "age" > 40 +// and "age" < 60 +// and "firstName" = 'Jennifer' +// order by "lastName" asc +``` +### Bookshelf +一样基于 Knex,细节就不一一罗列了 +​ + +### 总结 +除了常规的增删改查,还有一个非常重要的能力就是关系数据的处理能力,关系数据的 eager loading 也是各 ORM 永恒的话题,这里就不细说了 + + +- 从完整性来说,Typeorm 的最完整 +- 如果说有什么理由让我选择 Objection.js,我会说它的 QueryBuilder 非常给力,写起来非常舒服,可造性也非常强 +- Sequelize 的常规 CRUD API 并没有太大问题,但是在关系数据的处理上问题太多了。而且黑箱设计的 CRUD API,非常难以调试和改造 +- Prisma 的 CRUD API 与 Sequelize 相似,这让我非常担心它是否也会有 Sequelize 的各种糟心问题 +- 至于 Bookshelf,已经不维护了就不深入讨论了,Objection.js 是类似最好的替代品 + +​ + +## 如何选择? + +- 综合实力 Typeorm 最强,各方面表现的都不差,但细节还差那么点,需要更深入使用才能知道细节表现力,存在非常多未知,但因为社区活跃,成长空间巨大 +- 从无代码改造角度来说,Sequelize 工作量是最少的,尤其是给力的 sync 方法。但我非常不喜欢它的 QueryBuilder 设计。Sequelize 的社区活跃度也很高,但是核心团队看起来出现了些问题 [issue #12956](https://github.com/sequelize/sequelize/issues/12956) +- 我非常喜欢 Objection.js 在 QueryBuilder 上的表现力,可造性非常强,但是要完整的无代码支持,工作量也非常多 + +​ + +**在这样的大环境下,无代码的 Database 要如何选择 ORM 和改造呢?** +综合考虑,Sequelize 最适合作为蓝本,基于 Sequelize 优先实现第一版,即使 Sequelize 存在问题,后续也可以替换为 Typeorm 或 Objection.js 等。 + + +## 改造开始 +### Collection +NocoBase 首先基于 Sequelize 的 ModelOptions、 ModelAttributes、ModelAssociations 提炼了一套更适合无代码配置的 JSON 风格的配置协议,称之为 Collection Schema Language,简称 CSL。示例如下: +```typescript +// 用户 +db.collection({ + name: 'users', + fields: { + username: { type: 'string', unique: true }, + password: { type: 'password', unique: true }, + posts: { type: 'hasMany' }, + }, +}); + +// 文章 +db.collection({ + name: 'posts', + fields: { + title: 'string', + content: 'text', + tags: 'belongsToMany', + comments: 'hasMany', + author: { type: 'belongsTo', target: 'users' }, + }, +}); + +// 标签 +db.collection({ + name: 'tags', + fields: [ + { type: 'string', name: 'name' }, + { type: 'belongsToMany', name: 'posts' }, + ], +}); + +// 评论 +db.collection({ + name: 'comments', + fields: [ + { type: 'text', name: 'content' }, + { type: 'belongsTo', name: 'user' }, + ], +}); +``` +与现在流行的装饰器配置的结构很像,只不过它是用 JSON 编写的,便于动态生成 Model。与 Typeorm 的 EntitySchema 做法也非常接近。 +​ + +**那为什么不直接使用 EntitySchema 呢?** +一方面对 EntitySchema 细节表现力如何未知,另一方面需要考虑后续的自定义字段需求,尤其是 UISchema 的扩展能力,自成一体的 CSL 更适合。而且结构上与 Typeorm 和 Prisma 的装饰器风格非常接近,学习成本并不高。 +### Model & Repository +配置好了 CSL 之后,会根据它自动初始化 ORM 的 Model(Active Record)和 Repository(Data Mapper): +```typescript +const User = app.collection({ + name: 'users', + fields: { + username: { type: 'string', unique: true }, + password: { type: 'password', unique: true }, + posts: { type: 'hasMany' }, + }, +}); + +User.model; +User.repository; +``` +Model 的具体形态取决于适配的 ORM,保留原滋原味,细节就不多说了。Repository 自成一体,提供更适合 NocoBase 的 CRUD API。Model 和 Repository 都可以自定义,如: +```ts +class UserModel extends Model { + +} + +class UserRepository extends Repository { + +} + +const User = app.collection({ + name: 'users', + model: UserModel, + repository: UserRepository, + fields: { + username: { type: 'string', unique: true }, + password: { type: 'password', unique: true }, + posts: { type: 'hasMany' }, + }, +}); +``` +支持 Model 也支持 Repository,与 Typeorm 的做法非常接近,但是 Repository 并不是 Typeorm 的 Repository,我们来看看 Repository API 吧。 +### Repository API +NocoBase 的 Repository API 有: + +- repository.findMany() +- repository.findOne() +- repository.create() +- repository.update() +- repository.destroy() +- repository.relation().of() + - findMany() + - findOne() + - create() + - update() + - destroy() + - set() + - add() + - remove() + - toggle() +#### findMany +```ts +repository.findMany({ + // 过滤 + filter: { + $and: [{ a: 5 }, { b: 6 }], // (a = 5) AND (b = 6) + $or: [{ a: 5 }, { b: 6 }], // (a = 5) OR (b = 6) + someAttribute: { + // Basics + $eq: 3, // = 3 + $ne: 20, // != 20 + $is: null, // IS NULL + $not: true, // IS NOT TRUE + $gt: 6, // > 6 + $gte: 6, + }, + 'someAttribute.$eq': 3, + 'nested.someAttribute': { + // + }, + nested: { + someAttribute: {}, + }, + }, + // 字段白名单 + fields: [], + // 附加字段,主要用于附加关系字段 + appends: [], + // 字段黑名单 + expect: [], + sort: [], + page: 1, + pageSize: 2, +}); +``` +#### findOne +```typescript +repository.findOne({ + // 过滤 + filter: {}, + // 为更快速的 pk 过滤提供 + filterByPk: 1, // 通常情况等同于 filter: {id: 1} + // 字段白名单 + fields: [], + // 附加字段,主要用于附加关系字段 + appends: [], + // 字段黑名单 + expect: [], +}); +``` +#### create +```ts +repository.create({ + // 待存数据 + values: { + a: 'a', + // 快速建立关联 + o2o: 1, // 建立一对一关联 + m2o: 1, // 建立多对一关联 + o2m: [1,2] // 建立一对多关联 + m2m: [1,2] // 建立多对多关联 + // 新建关联数据并建立关联 + o2o: { + key1: 'val1', + }, + o2m: [{key1: 'val1'}, {key2: 'val2'}], + // 子表格数据 + subTable: [ + // 如果数据存在,更新处理 + {id: 1, key1: 'val1111'}, + // 如果数据不存在,直接创建并关联 + {key2: 'val2'}, + ], + }, + // 字段白名单 + whitelist: [], + // 字段黑名单 + blacklist: [], + // 关系数据默认会新建并建立关联处理,如果是已存在的数据只关联,但不更新关系数据 + // 如果需要更新关联数据,可以通过 updateAssociations 指定 + updateAssociations: ['subTable'], +}); +``` +#### update +```ts +repository.update({ + // 待更新数据 + values: {}, + // 过滤,哪些数据要更新 + filter: {}, + // 为更快速的 pk 过滤提供 + filterByPk: 1, // 通常情况等同于 filter: {id: 1} + // 字段白名单 + whitelist: [], + // 字段黑名单 + blacklist: [], + // 指定需要更新数据的关联字段 + updateAssociations: [], +}); +``` +#### destroy +```ts +// 特定 primary key 值 +repository.destroy(1); + +// 批量 primary key 值 +repository.destroy([1, 2, 3]); + +// 复杂的 filter +repository.destroy({ + filter: {}, +}); +``` +#### relation +关系数据的 CRUD,用法与常规 Repository 的一致 +```ts +// user_id = 1 的 post 的 repository +const userPostsRepository = repository.relation('posts').of(1); + +userPostsRepository.findMany({ + +}); + +userPostsRepository.findOne({ + +}); + +userPostsRepository.create({ + values: {}, +}); + +userPostsRepository.update({ + values: {}, +}); + +userPostsRepository.destroy({ + +}); +``` +关联操作,只处理关系约束的建立与解除 +```ts +// user_id = 1 的 post 的 relatedQuery +const userPostsRepository = repository.relation('posts').of(1); + +// 建立关联 +userPostsRepository.set(1); + +// 批量,仅用于 HasMany 和 BelongsToMany +userPostsRepository.set([1,2,3]); + +// BelongsToMany 的中间表 +userPostsRepository.set([ + [1, {/* 中间表数据 */}], + [2, {/* 中间表数据 */}], + [3, {/* 中间表数据 */}], +]); + +// 仅用于 HasMany 和 BelongsToMany +userPostsRepository.add(1); + +// BelongsToMany 的中间表 +userPostsRepository.add(1, {/* 中间表数据 */}); + +// 删除关联 +userPostsRepository.remove(1); + +// 建立或解除 +userPostsRepository.toggle(1); +userPostsRepository.toggle([1, 2, 3]); +``` +### Collection Sync +在 Migration 章节,介绍了各种 ORM 的 Migration 做法。得出结论 Sequelize.sync() 的方案较优,但不够精细,而且有些暴力,不过也没有关系,Collection 也打算这样做,再进一步的改进 sync 的细节,流程上就变得非常友好了。 +​ + +只执行某个 collection 的 sync。虽然有 hasMany 的 posts,但因为关系表不存在并不会创建 +```ts +const User = db.collection({ + name: 'users', + fields: { + username: { type: 'string', unique: true }, + password: { type: 'password', unique: true }, + posts: { type: 'hasMany' }, + }, +}); + +await User.sync(); +``` +我们也可以通过 db.sync 批量的将多个 collections 同步给数据库,通常不需要关注建表顺序、关系主外键和关系约束的先后顺序等等,collection 内部通通自动帮你处理好。比如下面例子: + +- 不需要特意声明外键 user_id +- 不需要考虑关系外键要怎么建立,在哪里建立,也不需要考虑顺序问题 +- 自动创建多对多中间表以及相关外键及约束 +```ts +// 文章 +db.collection({ + name: 'posts', + fields: { + title: 'string', + content: 'text', + tags: 'belongsToMany', + comments: 'hasMany', + author: { type: 'belongsTo', target: 'users' }, + }, +}); + +// 用户 +db.collection({ + name: 'users', + fields: { + username: { type: 'string', unique: true }, + password: { type: 'password', unique: true }, + posts: { type: 'hasMany' }, + }, +}); + +// 标签 +db.collection({ + name: 'tags', + fields: [ + { type: 'string', name: 'name' }, + { type: 'belongsToMany', name: 'posts' }, + ], +}); + +// 评论 +db.collection({ + name: 'comments', + fields: [ + { type: 'text', name: 'content' }, + { type: 'belongsTo', name: 'user' }, + ], +}); + +await db.sync(); +``` +### 动态化 +为了更好的支持动态配置 collection,提供了以下 API: + +- db.collection 创建 +- db.getCollection 获取 +- collection.mergeOptions 和 collection 配置,不删除 +- collection.hasField +- collection.getField +- collection.addField +- collection.setFields +- collection.mergeField 合并参数,如果 field 不存在则添加 +- collection.removeField 移除 +- collection.sync + +备注:name 是非常重要的标识,但如果涉及 name 的修改如何处理比较合适? +​ + +有了 Collection API 就可以实现动态 Collection 了。解决动态数据的持久化问题,可以将数据储存在数据表里。为此,我们可以创建 collections 和 fields 两张表。 +```ts +const Collection = db.collection({ + name: 'collections', + fields: [ + { name: 'name', type: 'string', unique: true }, + { name: 'fields', type: 'hasMany', foreignKey: 'collectionName' }, + ], +}); + +const Field = db.collection({ + name: 'fields', + fields: [ + { name: 'name', type: 'string' }, + ], +}); + +db.on('collections.afterCreate', async (model) => { + const collection = db.collection(model.toJSON()); // 实际可能不是用 model.toJSON() 方法 + await collection.sync(); +}); + +db.on('collections.afterUpdate', async (model) => { + const collection = db.getCollection(model.get('name')); + // 更新配置 + collection.mergeOptions(model.get()); + await collection.sync(); +}); + +db.on('fields.afterCreate', async (model) => { + const collection = db.getCollection(model.get('collectionName')); + // 新增字段 + collection.addField(model.toJSON()); // 实际可能不是用 model.toJSON() 方法 + await collection.sync(); +}); + +db.on('fields.afterUpdate', async (model) => { + const collection = db.getCollection(model.get('collectionName')); + // 更新字段配置 + collection.mergeField(model.toJSON()); // 实际可能不是用 model.toJSON() 方法 + await collection.sync(); +}); +``` +备注:db.on 之后会另起一篇和 app.on 一起介绍 +​ + +这样就可以用 Collection.repository.create() 来动态创建表了,比如: +```ts +await Collection.repository.create({ + values: { + name: 'test', + fields: [ + { name: 'name', type: 'string' }, + ], + }, +}); +``` +以上就是 plugin-collections 的核心实现逻辑了。 diff --git a/docs/guide/kernel-principle/client-side-kernel.md b/docs/guide/kernel-principle/client-side-kernel.md new file mode 100644 index 0000000000..f40315b079 --- /dev/null +++ b/docs/guide/kernel-principle/client-side-kernel.md @@ -0,0 +1,158 @@ +--- +order: 2 +--- + +# Client-side Kernel + +为了让更多非开发人员也能参与进来,NocoBase 提供了配套的客户端插件 —— 无代码的可视化配置界面。这部分的核心就是 @nocobase/client,理想状态可以用在任意前端构建工具或框架内,如: + +- umijs +- create-react-app +- icejs +- vite +- snowpack +- nextjs +- 其他 + +暂时只支持 umijs(打包编译还有些问题),未来会逐步支持以上罗列的各个框架。 + +客户端主要的组成部分包括: + +## 请求 + +- API Client +- Request Hook + +```ts +const api = new APIClient({ + request, +}); + +api.auth(); +api.get(); +api.post(); +api.resource('collections').create(); +api.resource('collections').findOne({}); +api.resource('collections').findMany({}); +api.resource('collections').relation('fields').of(1).create(); +``` + +以下细节待定,特殊的资源 + +```js +api.collections.create(); +api.uiSchemas.create(); +``` + +Request Hook + +[https://www.npmjs.com/package/@ahooksjs/use-request](https://www.npmjs.com/package/@ahooksjs/use-request) + +```js +const { data } = useRequest(() => api.resource('users').findMany()); +``` + +## 路由 + +- createRouteSwitch + +```js +const RouteSwitch = createRouteSwitch({ + components: {}, +}); + + +``` + +## Schema 组件 + +- createSchemaComponent + +```js +function Hello() { + return
Hello Word
+} + +const SchemaComponent = createSchemaComponent({ + scope, + components: { + Hello + }, +}); + +const schema = { + type: 'void', + 'x-component': 'Hello', +}; + + +``` + +## 怎么组装起来? + +
+import { I18nextProvider } from 'react-i18next';
+import { createRouteSwitch, APIClient } from '@nocobase/client';
+
+const apiClient = new APIClient();
+const i18n = i18next.createInstance();
+
+const Hello = () => {
+	return 
Hello
; +} + +const SchemaComponent = createSchemaComponent({ + components: { + Hello, + }, +}); + +const PageTemplate = () => { + const schema = { + type: 'void', + 'x-component': 'Hello', + }; + return ( + + ); +} + +const RouteSwitch = createRouteSwitch({ + components: { + PageTemplate, + }, +}); + +const routes = [ + { path: '/hello', component: 'Hello' }, +]; + +function AntdProvider(props) { + // 可以根据 i18next 的情况动态处理这里的 locale + return ( + {props.children} + ); +} + +const App = () => { + return ( + + + + + + + + + + ); +} +
+ +- APIClientProvider:提供 APIClient +- I18nextProvider:国际化 +- AntdProvider:处理 antd 组件的国际化,需要放在 I18nextProvider 里 +- Router:路由驱动 +- RouteSwitch:路由分发 + +上面代码看似有些啰嗦,实际各部分的功能和作用并不一样,不适合过度封装。如果需要可以根据实际情况,再进一步封装。 \ No newline at end of file diff --git a/docs/guide/kernel-principle/client-side-kernel.zh-CN.md b/docs/guide/kernel-principle/client-side-kernel.zh-CN.md new file mode 100644 index 0000000000..847d05fca3 --- /dev/null +++ b/docs/guide/kernel-principle/client-side-kernel.zh-CN.md @@ -0,0 +1,158 @@ +--- +order: 2 +--- + +# 客户端内核 + +为了让更多非开发人员也能参与进来,NocoBase 提供了配套的客户端插件 —— 无代码的可视化配置界面。这部分的核心就是 @nocobase/client,理想状态可以用在任意前端构建工具或框架内,如: + +- umijs +- create-react-app +- icejs +- vite +- snowpack +- nextjs +- 其他 + +暂时只支持 umijs(打包编译还有些问题),未来会逐步支持以上罗列的各个框架。 + +客户端主要的组成部分包括: + +## 请求 + +- API Client +- Request Hook + +```ts +const api = new APIClient({ + request, +}); + +api.auth(); +api.get(); +api.post(); +api.resource('collections').create(); +api.resource('collections').findOne({}); +api.resource('collections').findMany({}); +api.resource('collections').relation('fields').of(1).create(); +``` + +以下细节待定,特殊的资源 + +```js +api.collections.create(); +api.uiSchemas.create(); +``` + +Request Hook + +[https://www.npmjs.com/package/@ahooksjs/use-request](https://www.npmjs.com/package/@ahooksjs/use-request) + +```js +const { data } = useRequest(() => api.resource('users').findMany()); +``` + +## 路由 + +- createRouteSwitch + +```js +const RouteSwitch = createRouteSwitch({ + components: {}, +}); + + +``` + +## Schema 组件 + +- createSchemaComponent + +```js +function Hello() { + return
Hello Word
+} + +const SchemaComponent = createSchemaComponent({ + scope, + components: { + Hello + }, +}); + +const schema = { + type: 'void', + 'x-component': 'Hello', +}; + + +``` + +## 怎么组装起来? + +
+import { I18nextProvider } from 'react-i18next';
+import { createRouteSwitch, APIClient } from '@nocobase/client';
+
+const apiClient = new APIClient();
+const i18n = i18next.createInstance();
+
+const Hello = () => {
+	return 
Hello
; +} + +const SchemaComponent = createSchemaComponent({ + components: { + Hello, + }, +}); + +const PageTemplate = () => { + const schema = { + type: 'void', + 'x-component': 'Hello', + }; + return ( + + ); +} + +const RouteSwitch = createRouteSwitch({ + components: { + PageTemplate, + }, +}); + +const routes = [ + { path: '/hello', component: 'Hello' }, +]; + +function AntdProvider(props) { + // 可以根据 i18next 的情况动态处理这里的 locale + return ( + {props.children} + ); +} + +const App = () => { + return ( + + + + + + + + + + ); +} +
+ +- APIClientProvider:提供 APIClient +- I18nextProvider:国际化 +- AntdProvider:处理 antd 组件的国际化,需要放在 I18nextProvider 里 +- Router:路由驱动 +- RouteSwitch:路由分发 + +上面代码看似有些啰嗦,实际各部分的功能和作用并不一样,不适合过度封装。如果需要可以根据实际情况,再进一步封装。 \ No newline at end of file diff --git a/docs/guide/kernel-principle/installation-startup.md b/docs/guide/kernel-principle/installation-startup.md new file mode 100644 index 0000000000..edd085b5b9 --- /dev/null +++ b/docs/guide/kernel-principle/installation-startup.md @@ -0,0 +1,73 @@ +--- +order: 3 +--- + +# Installation and Startup Process + +## 项目安装 + +```bash +yarn nocobase init +``` + +- app.constructor() +- app.parse() + - yarn nocobase init + 初始化安装 + - app.load() + 加载配置 + - app.emitAsync('beforeLoad') + 所有配置加载之前的钩子 + - app.pluginManager.load() + 按顺序载入所有激活的插件的配置 + - 加载 plugin-collections 的配置 + - 添加 app.on('init') 监听 + - db.getModel('collections').load() + 把 collections 表的配置都导入 db.table() + - app.db.sync({force: false}) + 再执行 sync,创建 collections 表里配置的数据表 + - app.emitAsync('afterLoad') + 所有配置加载之后的钩子 + - app.db.sync({force: true}) + 根据配置生成数据表、字段、索引等 + - app.emitAsync('init') + 执行所有 init listeners,一般是初始化的数据操作 + - 触发 plugin-collections 的 init 事件,数据表就创建好了 + - app.stop() + 结束 + +## 项目启动 + +```bash +yarn nocobase start --init --sync +# --init 用于启动时快捷安装 +# --sync 开发环境时,当 app.collection() 有更新时快速建表或更新表 +``` + + + +- app.constructor() +- app.parse() + - yarn nocobase start + 初始化安装 + - app.load() + 加载配置 + - app.emitAsync('beforeLoad') + 所有配置加载之前的钩子 + - app.pluginManager.load() + 按顺序载入所有激活的插件的配置 + - 加载 plugin-collections 的配置 + - 添加 app.on('start') 监听 + - db.getModel('collections').load() + 把 collections 表的配置都导入 db.table(),在 start 流程里不需要再 db.sync + - app.emitAsync('afterLoad') + 所有配置加载之后的钩子 + - app.db.sync({force: false}) + yarn nocobase start --sync 有更新时快速建表或更新表 + yarn nocobase start --init 快捷 init + - app.emitAsync('init') + yarn nocobase start --init 快捷 init + - app.emitAsync('start') + 执行所有 start listeners,一般是从数据表里读取一些必要的数据 + - app.listen() + 启动 http server diff --git a/docs/guide/kernel-principle/installation-startup.zh-CN.md b/docs/guide/kernel-principle/installation-startup.zh-CN.md new file mode 100644 index 0000000000..f7027f630b --- /dev/null +++ b/docs/guide/kernel-principle/installation-startup.zh-CN.md @@ -0,0 +1,73 @@ +--- +order: 3 +--- + +# 项目安装和启动流程 + +## 项目安装 + +```bash +yarn nocobase init +``` + +- app.constructor() +- app.parse() + - yarn nocobase init + 初始化安装 + - app.load() + 加载配置 + - app.emitAsync('beforeLoad') + 所有配置加载之前的钩子 + - app.pluginManager.load() + 按顺序载入所有激活的插件的配置 + - 加载 plugin-collections 的配置 + - 添加 app.on('init') 监听 + - db.getModel('collections').load() + 把 collections 表的配置都导入 db.table() + - app.db.sync({force: false}) + 再执行 sync,创建 collections 表里配置的数据表 + - app.emitAsync('afterLoad') + 所有配置加载之后的钩子 + - app.db.sync({force: true}) + 根据配置生成数据表、字段、索引等 + - app.emitAsync('init') + 执行所有 init listeners,一般是初始化的数据操作 + - 触发 plugin-collections 的 init 事件,数据表就创建好了 + - app.stop() + 结束 + +## 项目启动 + +```bash +yarn nocobase start --init --sync +# --init 用于启动时快捷安装 +# --sync 开发环境时,当 app.collection() 有更新时快速建表或更新表 +``` + + + +- app.constructor() +- app.parse() + - yarn nocobase start + 初始化安装 + - app.load() + 加载配置 + - app.emitAsync('beforeLoad') + 所有配置加载之前的钩子 + - app.pluginManager.load() + 按顺序载入所有激活的插件的配置 + - 加载 plugin-collections 的配置 + - 添加 app.on('start') 监听 + - db.getModel('collections').load() + 把 collections 表的配置都导入 db.table(),在 start 流程里不需要再 db.sync + - app.emitAsync('afterLoad') + 所有配置加载之后的钩子 + - app.db.sync({force: false}) + yarn nocobase start --sync 有更新时快速建表或更新表 + yarn nocobase start --init 快捷 init + - app.emitAsync('init') + yarn nocobase start --init 快捷 init + - app.emitAsync('start') + 执行所有 start listeners,一般是从数据表里读取一些必要的数据 + - app.listen() + 启动 http server diff --git a/docs/guide/kernel-principle/microkernel-architecture.md b/docs/guide/kernel-principle/microkernel-architecture.md new file mode 100644 index 0000000000..b43ed79dd0 --- /dev/null +++ b/docs/guide/kernel-principle/microkernel-architecture.md @@ -0,0 +1,13 @@ +--- +order: 0 +group: + title: Kernel Principle + path: /guide/kernel-principle + order: 6 +--- + +# Microkernel Architecture + + + +NocoBase 采用微内核架构,各类功能以插件形式扩展,所以微内核架构也叫插件化架构,由内核和插件两部分组成。内核提供了最小功能的 WEB 服务器,还提供了各种插件化接口;插件是按功能划分的各种独立模块,通过接口适配,具有可插拔的特点。插件化的设计降低了模块之间的耦合度,提高了复用率。随着插件库的不断扩充,常见的场景只需要组合插件即可完成基础搭建,这种设计理念非常适合无代码平台。 diff --git a/docs/guide/kernel-principle/microkernel-architecture.zh-CN.md b/docs/guide/kernel-principle/microkernel-architecture.zh-CN.md new file mode 100644 index 0000000000..47d3bb4621 --- /dev/null +++ b/docs/guide/kernel-principle/microkernel-architecture.zh-CN.md @@ -0,0 +1,13 @@ +--- +order: 0 +group: + title: 内核原理 + path: /zh-CN/guide/kernel-principle + order: 6 +--- + +# 微内核架构 + + + +NocoBase 采用微内核架构,各类功能以插件形式扩展,所以微内核架构也叫插件化架构,由内核和插件两部分组成。内核提供了最小功能的 WEB 服务器,还提供了各种插件化接口;插件是按功能划分的各种独立模块,通过接口适配,具有可插拔的特点。插件化的设计降低了模块之间的耦合度,提高了复用率。随着插件库的不断扩充,常见的场景只需要组合插件即可完成基础搭建,这种设计理念非常适合无代码平台。 diff --git a/docs/guide/kernel-principle/server-side-kernel.md b/docs/guide/kernel-principle/server-side-kernel.md new file mode 100644 index 0000000000..604903cc78 --- /dev/null +++ b/docs/guide/kernel-principle/server-side-kernel.md @@ -0,0 +1,903 @@ +--- +order: 1 +--- + +# Server-side Kernel + +## 微服务 - Microservices + +为了更快的理解 NocoBase,我们先创建一个应用,新建一个 app.js 文件,代码如下: + +```ts +const { Application } = require('@nocobase/server'); + +const app = new Application({ + // 省略配置信息 +}); + +// 配置一张 users 表 +app.collection({ + name: 'users', + fields: [ + { type: 'string', name: 'username' }, + { type: 'password', name: 'password' } + ], +}); + +// 解析 argv 参数,终端通过命令行进行不同操作 +app.parse(process.argv); +``` + +终端运行 + +```bash +# 根据配置生成数据库表结构 +node app.js db:sync +# 启动应用 +node app.js start --port=3000 +``` + +相关 users 表的 REST API 就生成了 + +```bash +GET http://localhost:3000/api/users +POST http://localhost:3000/api/users +GET http://localhost:3000/api/users/1 +PUT http://localhost:3000/api/users/1 +DELETE http://localhost:3000/api/users/1 +``` + +以上示例,只用了 10 行左右的代码就创建了真实可用的 REST API 服务。除了内置的 REST API 以外,还可以通过 `app.actions()` 自定义其他操作,如登录、注册、注销等。 + +```ts +app.actions({ + async login(ctx, next) {}, + async register(ctx, next) {}, + async logout(ctx, next) {}, +}, { + resourceName: 'users', // 属于 users 资源 +}); +``` + +以上自定义操作的 HTTP API 为: + +```bash +POST http://localhost:3000/api/users:login +POST http://localhost:3000/api/users:register +POST http://localhost:3000/api/users:logout +``` + +自定义的 HTTP API 依旧保持 REST API 的风格,以 `:` 格式表示。实际上 REST API 也可以显式指定 `actionName`,当指定了 `actionName`,无所谓使用什么请求方法,如: + +```bash +# 更新操作 +PUT http://localhost:3000/api/users/1 +# 等同于 +POST http://localhost:3000/api/users:update/1 + +# 删除操作 +DELETE http://localhost:3000/api/users/1 +# 等同于 +GET http://localhost:3000/api/users:destroy/1 +# 等同于 +POST http://localhost:3000/api/users:destroy/1 +``` + +NocoBase 的路由(Resourcer)基于资源(Resource)和操作(Action)设计,将 REST 和 RPC 结合起来,提供更为灵活且统一的 Resource Action API。结合客户端 SDK 是这样的: + +```ts +const { ClientSDK } = require('@nocobase/sdk'); + +const api = new ClientSDK({ + // 可以适配不同 request + request(params) => Promise.resolve({}), +}); + +await api.resource('users').list(); +await api.resource('users').create(); +await api.resource('users').get(); +await api.resource('users').update(); +await api.resource('users').destroy(); +await api.resource('users').login(); +await api.resource('users').register(); +await api.resource('users').logout(); +``` + +## 应用 - Application + +NocoBase 的 Application 继承了 Koa,集成了 DB 和 CLI,添加了一些必要的 API,这里列一些重点: + +- `app.db`:数据库实例,每个 app 都有自己的 db。 + - `db.getCollection()` 数据表/数据集 + - `collection.repository` 数据仓库 + - `collection.model` 数据模型 + - `db.on()` 添加事件监听,由 EventEmitter 提供 + - `db.emit()` 触发事件,由 EventEmitter 提供 + - `db.emitAsync()` 触发异步事件 +- `app.cli`,Commander 实例,提供命令行操作 +- `app.context`,上下文 + - `ctx.db` + - `ctx.action` 当前资源操作实例 + - `action.params` 操作参数 + - `action.mergeParams()` 参数合并方法 +- `app.constructor()` 初始化 +- `app.collection()` 定义数据 Schema,等同于 `app.db.collection()` +- `app.resource()` 定义资源 +- `app.actions()` 定义资源的操作方法 +- `app.on()` 添加事件监听,由 EventEmitter 提供 +- `app.emit()` 触发事件,由 EventEmitter 提供 +- `app.emitAsync()` 触发异步事件 +- `app.use()` 添加中间件,由 Koa 提供 +- `app.command()` 自定义命令行,等同于 `app.cli.command()` +- `app.plugin()` 添加插件 +- `app.load()` 载入配置,主要用于载入插件 +- `app.parse()` 解析 argv 参数,写在最后,等同于 `app.cli.parseAsync()` + +## 数据集 - Collection + +NocoBase 通过 `app.collection()` 方法定义数据的 Schema,Schema 的类型包括: + +属性 Attribute + +- Boolean 布尔型 +- String 字符串 +- Text 长文本 +- Integer 整数型 +- Float 浮点型 +- Decimal 货币 +- Json/Jsonb/Array 不同数据库的 JSON 类型不一致,存在兼容性问题 +- Time 时间 +- Date 日期 +- Virtual 虚拟字段 +- Reference 引用 +- Formula 计算公式 +- Context 上下文 +- Password 密码 +- Sort 排序 + +关系 Association/Realtion + +- HasOne 一对一 +- HasMany 一对多 +- BelongsTo 多对一 +- BelongsToMany 多对多 +- Polymorphic 多态 + +比如一个微型博客的表结构可以这样设计: + +```ts +// 用户 +app.collection({ + name: 'users', + fields: { + username: { type: 'string', unique: true }, + password: { type: 'password', unique: true }, + posts: { type: 'hasMany' }, + }, +}); + +// 文章 +app.collection({ + name: 'posts', + fields: { + title: 'string', + content: 'text', + tags: 'belongsToMany', + comments: 'hasMany', + author: { type: 'belongsTo', target: 'users' }, + }, +}); + +// 标签 +app.collection({ + name: 'tags', + fields: [ + { type: 'string', name: 'name' }, + { type: 'belongsToMany', name: 'posts' }, + ], +}); + +// 评论 +app.collection({ + name: 'comments', + fields: [ + { type: 'text', name: 'content' }, + { type: 'belongsTo', name: 'user' }, + ], +}); +``` + +除了通过 `app.collection()` 配置 schema,也可以直接调用 api 插入或修改 schema,collection 的核心 API 有: + +- `collection` 当前 collection 的数据结构 + - `collection.hasField()` 判断字段是否存在 + - `collection.addField()` 添加字段配置 + - `collection.getField()` 获取字段配置 + - `collection.removeField()` 移除字段配置 + - `collection.sync()` 与数据库表结构同步 +- `collection.repository` 当前 collection 的数据仓库 + - `repository.findMany()` + - `repository.findOne()` + - `repository.create()` + - `repository.update()` + - `repository.destroy()` + - `repository.relatedQuery().for()` + - `create()` + - `update()` + - `destroy()` + - `findMany()` + - `findOne()` + - `set()` + - `add()` + - `remove()` + - `toggle()` +- `collection.model` 当前 collection 的数据模型 + +Collection 示例: + +```ts +const collection = app.db.getCollection('posts'); + +collection.hasField('title'); + +collection.getField('title'); + +// 添加或更新 +collection.addField({ + type: 'string', + name: 'content', +}); + +// 移除 +collection.removeField('content'); + +// 添加、或指定 key path 替换 +collection.mergeField({ + name: 'content', + type: 'string', +}); + +除了全局的 `db.sync()`,也有 `collection.sync()` 方法。 + +await collection.sync(); +``` + +`db:sync` 是非常常用的命令行之一,数据库根据 collection 的 schema 生成表结构。更多详情见 CLI 章节。`db:sync` 之后,就可以往表里写入数据了,可以使用 Repository 或 Model 操作。 + +- Repository 初步提供了 findAll、findOne、create、update、destroy 核心操作方法。 +- Model 为 Sequelize.Model,详细使用说明可以查看 Sequelize 文档。 +- Model 取决于适配的 ORM,Repository 基于 Model 提供统一的接口。 + +通过 Repository 创建数据 + +```ts +const User = app.db.getCollection('users'); + +const user = await User.repository.create({ + title: 't1', + content: 'c1', + author: 1, + tags: [1,2,3], +}, { + whitelist: [], + blacklist: [], +}); + +await User.repository.findMany({ + filter: { + title: 't1', + }, + fields: ['id', 'title', 'content'], + sort: '-created_at', + page: 1, + perPage: 20, +}); + +await User.repository.findOne({ + filter: { + title: 't1', + }, + fields: ['id', 'title', 'content'], + sort: '-created_at', + page: 1, + perPage: 20, +}); + +await User.repository.update({ + title: 't1', + content: 'c1', + author: 1, + tags: [1,2,3], +}, { + filter: {}, + whitelist: [], + blacklist: [], +}); + +await User.repository.destroy({ + filter: {}, +}); +``` + +通过 Model 创建数据 + +```ts +const User = db.getCollection('users'); +const user = await User.model.create({ + title: 't1', + content: 'c1', +}); +``` + +## 资源 & 操作 - Resource & Action + +Resource 是互联网资源,互联网资源都对应一个地址。客户端请求资源地址,服务器响应请求,在这里「请求」就是一种「操作」,在 REST 里通过判断请求方法(GET/POST/PUT/DELETE)来识别具体的操作,但是请求方法局限性比较大,如上文提到的登录、注册、注销就无法用 REST API 的方式表示。为了解决这类问题,NocoBase 以 `:` 格式表示资源的操作。在关系模型的世界里,关系无处不在,基于关系,NocoBase 又延伸了关系资源的概念,对应关系资源的操作的格式为 `.:`。 + +Collection 会自动同步给 Resource,如上文 Collection 章节定义的 Schema,可以提炼的资源有: + +- `users` +- `users.posts` +- `posts` +- `posts.tags` +- `posts.comments` +- `posts.author` +- `tags` +- `tags.posts` +- `comments` +- `comments.user` + + + +- Collection 定义数据的 schema(结构和关系) +- Resource 定义数据的 action(操作方法) +- Resource 请求和响应的数据结构由 Collection 定义 +- Collection 默认自动同步给 Resource +- Resource 的概念更大,除了对接 Collection 以外,也可以对接外部数据或其他自定义 + + + +资源相关 API 有: + +- `app.resource()` +- `app.actions()` +- `ctx.action` + +一个资源可以有多个操作。 + +```ts +// 数据类 +app.resource({ + name: 'users', + actions: { + async list(ctx, next) {}, + async get(ctx, next) {}, + async create(ctx, next) {}, + async update(ctx, next) {}, + async destroy(ctx, next) {}, + }, +}); + +// 非数据类 +app.resource({ + name: 'server', + actions: { + // 获取服务器时间 + getTime(ctx, next) {}, + // 健康检测 + healthCheck(ctx, next) {}, + }, +}); +``` + +常规操作可以用于不同资源 + +```ts +app.actions({ + async list(ctx, next) {}, + async get(ctx, next) {}, + async create(ctx, next) {}, + async update(ctx, next) {}, + async destroy(ctx, next) {}, +}, { + // 不指定 resourceName 时,全局共享 + resourceNames: ['posts', 'comments', 'users'], +}); +``` + +在资源内部定义的 action 不会共享,常规类似增删改查的操作建议设置为全局,`app.resource()` 只设置参数,如: + +```ts +app.resource({ + name: 'users', + actions: { + list: { + fields: ['id', 'username'], // 只输出 id 和 username 字段 + filter: { + 'username.$ne': 'admin', // 数据范围筛选过滤 username != admin + }, + sort: ['-created_at'], // 创建时间倒序 + perPage: 50, + }, + get: { + fields: ['id', 'username'], // 只输出 id 和 username 字段 + filter: { + 'username.$ne': 'admin', // 数据范围筛选过滤 username != admin + }, + }, + create: { + fields: ['username'], // 白名单 + }, + update: { + fields: ['username'], // 白名单 + }, + destroy: { + filter: { // 不能删除 admin + 'username.$ne': 'admin', + }, + }, + }, +}); + +// app 默认已经内置了 list, get, create, update, destroy 操作 +app.actions({ + async list(ctx, next) {}, + async get(ctx, next) {}, + async create(ctx, next) {}, + async update(ctx, next) {}, + async destroy(ctx, next) {}, +}); +``` + +在 Middleware Handler 和 Action Handler 里,都可以通过 `ctx.action` 获取到当前 action 实例,提供了两个非常有用的 API: + +- `ctx.action.params`:获取操作对应的参数 +- `ctx.action.mergeParams()`:处理多来源参数合并 + +`ctx.action.params` 有: + +- 定位资源和操作 + - `actionName` + - `resourceName` + - `associatedName` +- 定位资源 ID + - `resourceId` + - `associatedId` +- request query + - `filter` + - `fields` + - `sort` + - `page` + - `perPage` + - 其他 query 值 +- request body + - `values` + +示例: + +```ts +async function (ctx, next) { + const { resourceName, resourceId, filter, fields } = ctx.action.params; + // ... +} +``` + +`ctx.action.mergeParams()` 主要用于多来源参数合并,以 `filter` 参数为例。如:客户端请求日期 2021-09-15 创建的文章 + +```bash +GET /api/posts:list?filter={"created_at": "2021-09-15"} +``` + +资源设置锁定只能查看已发布的文章 + +```ts +app.resource({ + name: 'posts', + actions: { + list: { + filter: { status: 'publish' }, // 只能查看已发布文章 + }, + }, +}) +``` + +权限设定,只能查看自己创建的文章 + +```ts +app.use(async (ctx, next) => { + const { resourceName, actionName } = ctx.action.params; + if (resourceName === 'posts' && actionName === 'list') { + ctx.action.mergeParams({ + filter: { + created_by_id: ctx.state.currentUser.id, + }, + }); + } + await next(); +}); +``` + +以上客户端、资源配置、中间件内我们都指定了 filter 参数,三个来源的参数最终会合并在一起作为最终的过滤条件: + +```ts +async function list(ctx, next) { + // list 操作中获取到的 filter + console.log(ctx.params.filter); + // filter 是特殊的 and 合并 + // { + // and: [ + // { created_at: '2021-09-15' }, + // { status: 'publish' }, + // { created_by_id: 1, } + // ] + // } +} +``` + +## 事件 - Event + +在操作执行前、后都放置了相关事件监听器,可以通过 `app.db.on()` 和 `app.on()` 添加。区别在于: + +- `app.db.on()` 添加数据库层面的监听器 +- `app.on()` 添加服务器应用层面的监听器 + +以 `users:login` 为例,在数据库里为「查询」操作,在应用里为「登录」操作。也就是说,如果需要记录登录操作日志,要在 `app.on()` 里处理。 + +```ts +// 创建数据时,执行 User.create() 时触发 +app.db.on('users.beforeCreate', async (model) => {}); + +// 客户端 `POST /api/users:login` 时触发 +app.on('users.beforeLogin', async (ctx, next) => {}); + +// 客户端 `POST /api/users` 时触发 +app.on('users.beforeCreate', async (ctx, next) => {}); +``` + +## 中间件 - Middleware + +Server Application 基于 Koa,所有 Koa 的插件(中间件)都可以直接使用,可以通过 `app.use()` 添加。如: + +```ts +const responseTime = require('koa-response-time'); +app.use(responseTime()); + +app.use(async (ctx, next) => { + await next(); +}); +``` + +与 `koa.use(middleware)` 略有不同,`app.use(middleware, options)` 多了个 options 参数,可以用于限定 resource 和 action,也可以用于控制中间件的插入位置。 + +```ts +import { middleware } from '@nocobase/server'; + +app.use(async (ctx, next) => {}, { + name: 'middlewareName1', + resourceNames: [], // 作用于资源内所有 actions + actionNames: [ + 'list', // 全部 list action + 'users:list', // 仅 users 资源的 list action, + ], + insertBefore: '', + insertAfter: '', +}); +``` + +## 命令行 - CLI + +Application 除了可以做 HTTP Server 以外,也是 CLI(内置了 Commander)。目前内置的命令有: + +- `init` 初始化 +- `db:sync --force` 用于配置与数据库表结构同步 +- `start --port` 启动应用 +- `plugin:**` 插件相关 + +自定义: + +```ts +app.command('foo').action(async () => { + console.log('foo...'); +}); +``` + +## 插件 - Plugin + +上文,讲述了核心的扩展接口,包括但不局限于: + +- Database/Collection + - `app.db` database 实例 + - `app.collection()` 等同于 `app.db.collection()` +- Resource/Action + - `app.resource()` 等同于 `app.resourcer.define()` + - `app.actions()` 等同于 `app.resourcer.registerActions()` +- Hook/Event + - `app.on()` 添加服务器监听器 + - `app.db.on()` 添加数据库监听器 +- Middleware + - `app.use()` 添加中间件 +- CLI + - `app.cli` commander 实例 + - `app.command()` 等同于 `app.cli.command()` + +基于以上扩展接口,进一步提供了模块化、可插拔的插件,可以通过 `app.plugin()` 添加。插件的流程包括安装、升级、激活、载入、禁用、卸载,不需要的流程可缺失。如: + +**最简单的插件** + +```ts +app.plugin(function pluginName1() { + +}); +``` + +这种方式添加的插件会直接载入,无需安装。 + +**JSON 风格** + +```ts +const plugin = app.plugin({ + enable: false, // 默认为 true,不需要启用时可以禁用。 + name: 'plugin-name1', + displayName: '插件名称', + version: '1.2.3', + dependencies: { + pluginName2: '1.x', + pluginName3: '1.x', + }, + async install() {}, + async upgrade() {}, + async activate() {}, + async bootstrap() {}, + async deactivate() {}, + async unstall() {}, +}); +// 通过 api 激活插件 +plugin.activate(); +``` + +**OOP 风格** + +```ts +class MyPlugin extends Plugin { + async install() {} + async upgrade() {} + async bootstrap() {} + async activate() {} + async deactivate() {} + async unstall() {} +} + +app.plugin(MyPlugin); +// 或 +app.plugin({ + name: 'plugin-name1', + displayName: '插件名称', + version: '1.2.3', + dependencies: { + pluginName2: '1.x', + pluginName3: '1.x', + }, + plugin: MyPlugin, +}); +``` + +**引用独立的 Package** + +```ts +app.plugin('@nocobase/plugin-action-logs'); +``` + +插件信息也可以直接写在 `package.json` 里 + +```js +{ + name: 'pluginName1', + displayName: '插件名称', + version: '1.2.3', + dependencies: { + pluginName2: '1.x', + pluginName3: '1.x', + }, +} +``` + +**插件 CLI** + +```bash +plugin:install pluginName1 +plugin:unstall pluginName1 +plugin:activate pluginName1 +plugin:deactivate pluginName1 +``` + +目前已有的插件: + +- @nocobase/plugin-collections 提供数据表配置接口,可通过 HTTP API 管理数据表。 +- @nocobase/plugin-action-logs 操作日志 +- @nocobase/plugin-automations 自动化(未升级 v0.5,暂不能使用) +- @nocobase/plugin-china-region 中国行政区 +- @nocobase/plugin-client 提供客户端,无代码的可视化配置界面,需要与 @nocobase/client 配合使用 +- @nocobase/plugin-export 导出 +- @nocobase/plugin-file-manager 文件管理器 +- @nocobase/plugin-permissions 角色和权限 +- @nocobase/plugin-system-settings 系统配置 +- @nocobase/plugin-ui-router 前端路由配置 +- @nocobase/plugin-ui-schema ui 配置 +- @nocobase/plugin-users 用户模块 + +## 测试 - Testing + +有代码就需要测试,@nocobase/test 提供了 mockDatabase 和 mockServer 用于数据库和服务器的测试,如: + +```ts +import { mockServer, MockServer } from '@nocobase/test'; + +describe('mock server', () => { + let api: MockServer; + + beforeEach(() => { + api = mockServer({ + dataWrapping: false, + }); + api.actions({ + list: async (ctx, next) => { + ctx.body = [1, 2]; + await next(); + }, + }); + api.resource({ + name: 'test', + }); + }); + + afterEach(async () => { + return api.destroy(); + }); + + it('agent.get', async () => { + const response = await api.agent().get('/test'); + expect(response.body).toEqual([1, 2]); + }); + + it('agent.resource', async () => { + const response = await api.agent().resource('test').list(); + expect(response.body).toEqual([1, 2]); + }); +}); +``` + +## 客户端 - Client + +为了让更多非开发人员也能参与进来,NocoBase 提供了配套的客户端插件 —— 无代码的可视化配置界面。客户端插件需要与 @nocobase/client 配合使用,可以直接使用,也可以自行改造。 + +插件配置 + +```ts +app.plugin('@nocobase/plugin-client', { + // 自定义 dist 路径 + dist: path.resolve(__dirname, './node_modules/@nocobase/client/app'), +}); +``` + +为了满足各类场景需求,客户端 `@nocobase/client` 提供了丰富的基础组件: + +- Action - 操作 + - Action.Window 当前浏览器窗口/标签里打开 + - Action.Drawer 打开抽屉(默认右侧划出) + - Action.Modal 打开对话框 + - Action.Dropdown 下拉菜单 + - Action.Popover 气泡卡片 + - Action.Group 按钮分组 + - Action.Bar 操作栏 +- AddNew 「添加」模块 + - AddNew.CardItem - 添加区块 + - AddNew.PaneItem - 添加区块(查看面板,与当前查看的数据相关) + - AddNew.FormItem - 添加字段 +- BlockItem/CardItem/FormItem - 装饰器 + - BlockItem - 普通装饰器(无包装效果) + - CardItem - 卡片装饰器 + - FormItem - 字段装饰器 +- Calendar - 日历 +- Cascader - 级联选择 +- Chart - 图表 +- Checkbox - 勾选 +- Checkbox.Group - 多选框 +- Collection - 数据表配置 +- Collection.Field - 数据表字段 +- ColorSelect - 颜色选择器 +- DatePicker - 日期选择器 +- DesignableBar - 配置工具栏 +- Filter - 筛选器 +- Form - 表单 +- Grid - 栅格布局 +- IconPicker - 图标选择器 +- Input - 输入框 +- Input.TextArea - 多行输入框 +- InputNumber - 数字框 +- Kanban - 看板 +- ListPicker - 列表选择器(用于选择、展示关联数据) +- Markdown 编辑器 +- Menu - 菜单 +- Password - 密码 +- Radio - 单选框 +- Select - 选择器 +- Table - 表格 +- Tabs - 标签页 +- TimePicker - 时间选择器 +- Upload - 上传 + +可以自行扩展组件,以上组件基于 Formily 构建,怎么自定义组件大家查看相关组件源码或 Formily 文档,这里说点不一样的。 + +- 如何扩展数据库字段? +- 如何将第三方区块添加到 AddNew 模块中? +- 如何在操作栏里添加更多的内置操作? +- 如何自定义配置工具栏? + +除了组件具备灵活的扩展以外,客户端也可以在任意前端框架中使用,可以自定义 Request 和 Router,如: + +
+import React from 'react';
+import { MemoryRouter } from 'react-router-dom';
+import { ClientSDK, Application } from '@nocobase/client';
+
+// 初始化 client 实例
+const client = new ClientSDK({
+  request: (options) => Promise.resolve({}),
+});
+
+// 适配 Route Component
+const RouteSwitch = createRouteSwitch({
+  components: {
+    AdminLayout,
+    AuthLayout,
+    RouteSchemaRenderer,
+  },
+});
+
+ReactDOM.render(
+  
+    
+      
+    
+  ,
+  document.getElementById('root'),
+);
+
+ +更多细节,可以通过 `create-nocobase-app` 初始化项目脚手架并体验。 + +```bash +yarn create nocobase-app my-nocobase-project +``` + +nocobase-app 默认使用 umijs 作为项目构建工具,并集成了 Server 作数据接口,初始化的目录结构如下: + +```bash +|- src + |- pages + |- apis +|- .env +|- .umirc.ts +|- package.json +``` + +## 场景 - Cases + +小型管理信息系统,具备完整的前后端。 + + + +API 服务,无客户端,提供纯后端接口。 + + + +小程序 + 后台管理,只需要一套数据库,但有两套用户和权限,一套用于后台用户,一套用于小程序用户。 + + + +SaaS 服务(共享用户),每个应用有自己配套的数据库,各应用数据完全隔离。应用不需要用户和权限模块,SaaS 主站全局共享了。 + + + +SaaS 服务(独立用户),每个应用有自己的独立用户模块和权限,应用可以绑定自己的域名。 + + diff --git a/docs/guide/kernel-principle/server-side-kernel.zh-CN.md b/docs/guide/kernel-principle/server-side-kernel.zh-CN.md new file mode 100644 index 0000000000..c9d8211f3f --- /dev/null +++ b/docs/guide/kernel-principle/server-side-kernel.zh-CN.md @@ -0,0 +1,903 @@ +--- +order: 1 +--- + +# 服务端内核 + +## 微服务 - Microservices + +为了更快的理解 NocoBase,我们先创建一个应用,新建一个 app.js 文件,代码如下: + +```ts +const { Application } = require('@nocobase/server'); + +const app = new Application({ + // 省略配置信息 +}); + +// 配置一张 users 表 +app.collection({ + name: 'users', + fields: [ + { type: 'string', name: 'username' }, + { type: 'password', name: 'password' } + ], +}); + +// 解析 argv 参数,终端通过命令行进行不同操作 +app.parse(process.argv); +``` + +终端运行 + +```bash +# 根据配置生成数据库表结构 +node app.js db:sync +# 启动应用 +node app.js start --port=3000 +``` + +相关 users 表的 REST API 就生成了 + +```bash +GET http://localhost:3000/api/users +POST http://localhost:3000/api/users +GET http://localhost:3000/api/users/1 +PUT http://localhost:3000/api/users/1 +DELETE http://localhost:3000/api/users/1 +``` + +以上示例,只用了 10 行左右的代码就创建了真实可用的 REST API 服务。除了内置的 REST API 以外,还可以通过 `app.actions()` 自定义其他操作,如登录、注册、注销等。 + +```ts +app.actions({ + async login(ctx, next) {}, + async register(ctx, next) {}, + async logout(ctx, next) {}, +}, { + resourceName: 'users', // 属于 users 资源 +}); +``` + +以上自定义操作的 HTTP API 为: + +```bash +POST http://localhost:3000/api/users:login +POST http://localhost:3000/api/users:register +POST http://localhost:3000/api/users:logout +``` + +自定义的 HTTP API 依旧保持 REST API 的风格,以 `:` 格式表示。实际上 REST API 也可以显式指定 `actionName`,当指定了 `actionName`,无所谓使用什么请求方法,如: + +```bash +# 更新操作 +PUT http://localhost:3000/api/users/1 +# 等同于 +POST http://localhost:3000/api/users:update/1 + +# 删除操作 +DELETE http://localhost:3000/api/users/1 +# 等同于 +GET http://localhost:3000/api/users:destroy/1 +# 等同于 +POST http://localhost:3000/api/users:destroy/1 +``` + +NocoBase 的路由(Resourcer)基于资源(Resource)和操作(Action)设计,将 REST 和 RPC 结合起来,提供更为灵活且统一的 Resource Action API。结合客户端 SDK 是这样的: + +```ts +const { ClientSDK } = require('@nocobase/sdk'); + +const api = new ClientSDK({ + // 可以适配不同 request + request(params) => Promise.resolve({}), +}); + +await api.resource('users').list(); +await api.resource('users').create(); +await api.resource('users').get(); +await api.resource('users').update(); +await api.resource('users').destroy(); +await api.resource('users').login(); +await api.resource('users').register(); +await api.resource('users').logout(); +``` + +## 应用 - Application + +NocoBase 的 Application 继承了 Koa,集成了 DB 和 CLI,添加了一些必要的 API,这里列一些重点: + +- `app.db`:数据库实例,每个 app 都有自己的 db。 + - `db.getCollection()` 数据表/数据集 + - `collection.repository` 数据仓库 + - `collection.model` 数据模型 + - `db.on()` 添加事件监听,由 EventEmitter 提供 + - `db.emit()` 触发事件,由 EventEmitter 提供 + - `db.emitAsync()` 触发异步事件 +- `app.cli`,Commander 实例,提供命令行操作 +- `app.context`,上下文 + - `ctx.db` + - `ctx.action` 当前资源操作实例 + - `action.params` 操作参数 + - `action.mergeParams()` 参数合并方法 +- `app.constructor()` 初始化 +- `app.collection()` 定义数据 Schema,等同于 `app.db.collection()` +- `app.resource()` 定义资源 +- `app.actions()` 定义资源的操作方法 +- `app.on()` 添加事件监听,由 EventEmitter 提供 +- `app.emit()` 触发事件,由 EventEmitter 提供 +- `app.emitAsync()` 触发异步事件 +- `app.use()` 添加中间件,由 Koa 提供 +- `app.command()` 自定义命令行,等同于 `app.cli.command()` +- `app.plugin()` 添加插件 +- `app.load()` 载入配置,主要用于载入插件 +- `app.parse()` 解析 argv 参数,写在最后,等同于 `app.cli.parseAsync()` + +## 数据集 - Collection + +NocoBase 通过 `app.collection()` 方法定义数据的 Schema,Schema 的类型包括: + +属性 Attribute + +- Boolean 布尔型 +- String 字符串 +- Text 长文本 +- Integer 整数型 +- Float 浮点型 +- Decimal 货币 +- Json/Jsonb/Array 不同数据库的 JSON 类型不一致,存在兼容性问题 +- Time 时间 +- Date 日期 +- Virtual 虚拟字段 +- Reference 引用 +- Formula 计算公式 +- Context 上下文 +- Password 密码 +- Sort 排序 + +关系 Association/Realtion + +- HasOne 一对一 +- HasMany 一对多 +- BelongsTo 多对一 +- BelongsToMany 多对多 +- Polymorphic 多态 + +比如一个微型博客的表结构可以这样设计: + +```ts +// 用户 +app.collection({ + name: 'users', + fields: { + username: { type: 'string', unique: true }, + password: { type: 'password', unique: true }, + posts: { type: 'hasMany' }, + }, +}); + +// 文章 +app.collection({ + name: 'posts', + fields: { + title: 'string', + content: 'text', + tags: 'belongsToMany', + comments: 'hasMany', + author: { type: 'belongsTo', target: 'users' }, + }, +}); + +// 标签 +app.collection({ + name: 'tags', + fields: [ + { type: 'string', name: 'name' }, + { type: 'belongsToMany', name: 'posts' }, + ], +}); + +// 评论 +app.collection({ + name: 'comments', + fields: [ + { type: 'text', name: 'content' }, + { type: 'belongsTo', name: 'user' }, + ], +}); +``` + +除了通过 `app.collection()` 配置 schema,也可以直接调用 api 插入或修改 schema,collection 的核心 API 有: + +- `collection` 当前 collection 的数据结构 + - `collection.hasField()` 判断字段是否存在 + - `collection.addField()` 添加字段配置 + - `collection.getField()` 获取字段配置 + - `collection.removeField()` 移除字段配置 + - `collection.sync()` 与数据库表结构同步 +- `collection.repository` 当前 collection 的数据仓库 + - `repository.findMany()` + - `repository.findOne()` + - `repository.create()` + - `repository.update()` + - `repository.destroy()` + - `repository.relatedQuery().for()` + - `create()` + - `update()` + - `destroy()` + - `findMany()` + - `findOne()` + - `set()` + - `add()` + - `remove()` + - `toggle()` +- `collection.model` 当前 collection 的数据模型 + +Collection 示例: + +```ts +const collection = app.db.getCollection('posts'); + +collection.hasField('title'); + +collection.getField('title'); + +// 添加或更新 +collection.addField({ + type: 'string', + name: 'content', +}); + +// 移除 +collection.removeField('content'); + +// 添加、或指定 key path 替换 +collection.mergeField({ + name: 'content', + type: 'string', +}); + +除了全局的 `db.sync()`,也有 `collection.sync()` 方法。 + +await collection.sync(); +``` + +`db:sync` 是非常常用的命令行之一,数据库根据 collection 的 schema 生成表结构。更多详情见 CLI 章节。`db:sync` 之后,就可以往表里写入数据了,可以使用 Repository 或 Model 操作。 + +- Repository 初步提供了 findAll、findOne、create、update、destroy 核心操作方法。 +- Model 为 Sequelize.Model,详细使用说明可以查看 Sequelize 文档。 +- Model 取决于适配的 ORM,Repository 基于 Model 提供统一的接口。 + +通过 Repository 创建数据 + +```ts +const User = app.db.getCollection('users'); + +const user = await User.repository.create({ + title: 't1', + content: 'c1', + author: 1, + tags: [1,2,3], +}, { + whitelist: [], + blacklist: [], +}); + +await User.repository.findMany({ + filter: { + title: 't1', + }, + fields: ['id', 'title', 'content'], + sort: '-created_at', + page: 1, + perPage: 20, +}); + +await User.repository.findOne({ + filter: { + title: 't1', + }, + fields: ['id', 'title', 'content'], + sort: '-created_at', + page: 1, + perPage: 20, +}); + +await User.repository.update({ + title: 't1', + content: 'c1', + author: 1, + tags: [1,2,3], +}, { + filter: {}, + whitelist: [], + blacklist: [], +}); + +await User.repository.destroy({ + filter: {}, +}); +``` + +通过 Model 创建数据 + +```ts +const User = db.getCollection('users'); +const user = await User.model.create({ + title: 't1', + content: 'c1', +}); +``` + +## 资源 & 操作 - Resource & Action + +Resource 是互联网资源,互联网资源都对应一个地址。客户端请求资源地址,服务器响应请求,在这里「请求」就是一种「操作」,在 REST 里通过判断请求方法(GET/POST/PUT/DELETE)来识别具体的操作,但是请求方法局限性比较大,如上文提到的登录、注册、注销就无法用 REST API 的方式表示。为了解决这类问题,NocoBase 以 `:` 格式表示资源的操作。在关系模型的世界里,关系无处不在,基于关系,NocoBase 又延伸了关系资源的概念,对应关系资源的操作的格式为 `.:`。 + +Collection 会自动同步给 Resource,如上文 Collection 章节定义的 Schema,可以提炼的资源有: + +- `users` +- `users.posts` +- `posts` +- `posts.tags` +- `posts.comments` +- `posts.author` +- `tags` +- `tags.posts` +- `comments` +- `comments.user` + + + +- Collection 定义数据的 schema(结构和关系) +- Resource 定义数据的 action(操作方法) +- Resource 请求和响应的数据结构由 Collection 定义 +- Collection 默认自动同步给 Resource +- Resource 的概念更大,除了对接 Collection 以外,也可以对接外部数据或其他自定义 + + + +资源相关 API 有: + +- `app.resource()` +- `app.actions()` +- `ctx.action` + +一个资源可以有多个操作。 + +```ts +// 数据类 +app.resource({ + name: 'users', + actions: { + async list(ctx, next) {}, + async get(ctx, next) {}, + async create(ctx, next) {}, + async update(ctx, next) {}, + async destroy(ctx, next) {}, + }, +}); + +// 非数据类 +app.resource({ + name: 'server', + actions: { + // 获取服务器时间 + getTime(ctx, next) {}, + // 健康检测 + healthCheck(ctx, next) {}, + }, +}); +``` + +常规操作可以用于不同资源 + +```ts +app.actions({ + async list(ctx, next) {}, + async get(ctx, next) {}, + async create(ctx, next) {}, + async update(ctx, next) {}, + async destroy(ctx, next) {}, +}, { + // 不指定 resourceName 时,全局共享 + resourceNames: ['posts', 'comments', 'users'], +}); +``` + +在资源内部定义的 action 不会共享,常规类似增删改查的操作建议设置为全局,`app.resource()` 只设置参数,如: + +```ts +app.resource({ + name: 'users', + actions: { + list: { + fields: ['id', 'username'], // 只输出 id 和 username 字段 + filter: { + 'username.$ne': 'admin', // 数据范围筛选过滤 username != admin + }, + sort: ['-created_at'], // 创建时间倒序 + perPage: 50, + }, + get: { + fields: ['id', 'username'], // 只输出 id 和 username 字段 + filter: { + 'username.$ne': 'admin', // 数据范围筛选过滤 username != admin + }, + }, + create: { + fields: ['username'], // 白名单 + }, + update: { + fields: ['username'], // 白名单 + }, + destroy: { + filter: { // 不能删除 admin + 'username.$ne': 'admin', + }, + }, + }, +}); + +// app 默认已经内置了 list, get, create, update, destroy 操作 +app.actions({ + async list(ctx, next) {}, + async get(ctx, next) {}, + async create(ctx, next) {}, + async update(ctx, next) {}, + async destroy(ctx, next) {}, +}); +``` + +在 Middleware Handler 和 Action Handler 里,都可以通过 `ctx.action` 获取到当前 action 实例,提供了两个非常有用的 API: + +- `ctx.action.params`:获取操作对应的参数 +- `ctx.action.mergeParams()`:处理多来源参数合并 + +`ctx.action.params` 有: + +- 定位资源和操作 + - `actionName` + - `resourceName` + - `associatedName` +- 定位资源 ID + - `resourceId` + - `associatedId` +- request query + - `filter` + - `fields` + - `sort` + - `page` + - `perPage` + - 其他 query 值 +- request body + - `values` + +示例: + +```ts +async function (ctx, next) { + const { resourceName, resourceId, filter, fields } = ctx.action.params; + // ... +} +``` + +`ctx.action.mergeParams()` 主要用于多来源参数合并,以 `filter` 参数为例。如:客户端请求日期 2021-09-15 创建的文章 + +```bash +GET /api/posts:list?filter={"created_at": "2021-09-15"} +``` + +资源设置锁定只能查看已发布的文章 + +```ts +app.resource({ + name: 'posts', + actions: { + list: { + filter: { status: 'publish' }, // 只能查看已发布文章 + }, + }, +}) +``` + +权限设定,只能查看自己创建的文章 + +```ts +app.use(async (ctx, next) => { + const { resourceName, actionName } = ctx.action.params; + if (resourceName === 'posts' && actionName === 'list') { + ctx.action.mergeParams({ + filter: { + created_by_id: ctx.state.currentUser.id, + }, + }); + } + await next(); +}); +``` + +以上客户端、资源配置、中间件内我们都指定了 filter 参数,三个来源的参数最终会合并在一起作为最终的过滤条件: + +```ts +async function list(ctx, next) { + // list 操作中获取到的 filter + console.log(ctx.params.filter); + // filter 是特殊的 and 合并 + // { + // and: [ + // { created_at: '2021-09-15' }, + // { status: 'publish' }, + // { created_by_id: 1, } + // ] + // } +} +``` + +## 事件 - Event + +在操作执行前、后都放置了相关事件监听器,可以通过 `app.db.on()` 和 `app.on()` 添加。区别在于: + +- `app.db.on()` 添加数据库层面的监听器 +- `app.on()` 添加服务器应用层面的监听器 + +以 `users:login` 为例,在数据库里为「查询」操作,在应用里为「登录」操作。也就是说,如果需要记录登录操作日志,要在 `app.on()` 里处理。 + +```ts +// 创建数据时,执行 User.create() 时触发 +app.db.on('users.beforeCreate', async (model) => {}); + +// 客户端 `POST /api/users:login` 时触发 +app.on('users.beforeLogin', async (ctx, next) => {}); + +// 客户端 `POST /api/users` 时触发 +app.on('users.beforeCreate', async (ctx, next) => {}); +``` + +## 中间件 - Middleware + +Server Application 基于 Koa,所有 Koa 的插件(中间件)都可以直接使用,可以通过 `app.use()` 添加。如: + +```ts +const responseTime = require('koa-response-time'); +app.use(responseTime()); + +app.use(async (ctx, next) => { + await next(); +}); +``` + +与 `koa.use(middleware)` 略有不同,`app.use(middleware, options)` 多了个 options 参数,可以用于限定 resource 和 action,也可以用于控制中间件的插入位置。 + +```ts +import { middleware } from '@nocobase/server'; + +app.use(async (ctx, next) => {}, { + name: 'middlewareName1', + resourceNames: [], // 作用于资源内所有 actions + actionNames: [ + 'list', // 全部 list action + 'users:list', // 仅 users 资源的 list action, + ], + insertBefore: '', + insertAfter: '', +}); +``` + +## 命令行 - CLI + +Application 除了可以做 HTTP Server 以外,也是 CLI(内置了 Commander)。目前内置的命令有: + +- `init` 初始化 +- `db:sync --force` 用于配置与数据库表结构同步 +- `start --port` 启动应用 +- `plugin:**` 插件相关 + +自定义: + +```ts +app.command('foo').action(async () => { + console.log('foo...'); +}); +``` + +## 插件 - Plugin + +上文,讲述了核心的扩展接口,包括但不局限于: + +- Database/Collection + - `app.db` database 实例 + - `app.collection()` 等同于 `app.db.collection()` +- Resource/Action + - `app.resource()` 等同于 `app.resourcer.define()` + - `app.actions()` 等同于 `app.resourcer.registerActions()` +- Hook/Event + - `app.on()` 添加服务器监听器 + - `app.db.on()` 添加数据库监听器 +- Middleware + - `app.use()` 添加中间件 +- CLI + - `app.cli` commander 实例 + - `app.command()` 等同于 `app.cli.command()` + +基于以上扩展接口,进一步提供了模块化、可插拔的插件,可以通过 `app.plugin()` 添加。插件的流程包括安装、升级、激活、载入、禁用、卸载,不需要的流程可缺失。如: + +**最简单的插件** + +```ts +app.plugin(function pluginName1() { + +}); +``` + +这种方式添加的插件会直接载入,无需安装。 + +**JSON 风格** + +```ts +const plugin = app.plugin({ + enable: false, // 默认为 true,不需要启用时可以禁用。 + name: 'plugin-name1', + displayName: '插件名称', + version: '1.2.3', + dependencies: { + pluginName2: '1.x', + pluginName3: '1.x', + }, + async install() {}, + async upgrade() {}, + async activate() {}, + async bootstrap() {}, + async deactivate() {}, + async unstall() {}, +}); +// 通过 api 激活插件 +plugin.activate(); +``` + +**OOP 风格** + +```ts +class MyPlugin extends Plugin { + async install() {} + async upgrade() {} + async bootstrap() {} + async activate() {} + async deactivate() {} + async unstall() {} +} + +app.plugin(MyPlugin); +// 或 +app.plugin({ + name: 'plugin-name1', + displayName: '插件名称', + version: '1.2.3', + dependencies: { + pluginName2: '1.x', + pluginName3: '1.x', + }, + plugin: MyPlugin, +}); +``` + +**引用独立的 Package** + +```ts +app.plugin('@nocobase/plugin-action-logs'); +``` + +插件信息也可以直接写在 `package.json` 里 + +```js +{ + name: 'pluginName1', + displayName: '插件名称', + version: '1.2.3', + dependencies: { + pluginName2: '1.x', + pluginName3: '1.x', + }, +} +``` + +**插件 CLI** + +```bash +plugin:install pluginName1 +plugin:unstall pluginName1 +plugin:activate pluginName1 +plugin:deactivate pluginName1 +``` + +目前已有的插件: + +- @nocobase/plugin-collections 提供数据表配置接口,可通过 HTTP API 管理数据表。 +- @nocobase/plugin-action-logs 操作日志 +- @nocobase/plugin-automations 自动化(未升级 v0.5,暂不能使用) +- @nocobase/plugin-china-region 中国行政区 +- @nocobase/plugin-client 提供客户端,无代码的可视化配置界面,需要与 @nocobase/client 配合使用 +- @nocobase/plugin-export 导出 +- @nocobase/plugin-file-manager 文件管理器 +- @nocobase/plugin-permissions 角色和权限 +- @nocobase/plugin-system-settings 系统配置 +- @nocobase/plugin-ui-router 前端路由配置 +- @nocobase/plugin-ui-schema ui 配置 +- @nocobase/plugin-users 用户模块 + +## 测试 - Testing + +有代码就需要测试,@nocobase/test 提供了 mockDatabase 和 mockServer 用于数据库和服务器的测试,如: + +```ts +import { mockServer, MockServer } from '@nocobase/test'; + +describe('mock server', () => { + let api: MockServer; + + beforeEach(() => { + api = mockServer({ + dataWrapping: false, + }); + api.actions({ + list: async (ctx, next) => { + ctx.body = [1, 2]; + await next(); + }, + }); + api.resource({ + name: 'test', + }); + }); + + afterEach(async () => { + return api.destroy(); + }); + + it('agent.get', async () => { + const response = await api.agent().get('/test'); + expect(response.body).toEqual([1, 2]); + }); + + it('agent.resource', async () => { + const response = await api.agent().resource('test').list(); + expect(response.body).toEqual([1, 2]); + }); +}); +``` + +## 客户端 - Client + +为了让更多非开发人员也能参与进来,NocoBase 提供了配套的客户端插件 —— 无代码的可视化配置界面。客户端插件需要与 @nocobase/client 配合使用,可以直接使用,也可以自行改造。 + +插件配置 + +```ts +app.plugin('@nocobase/plugin-client', { + // 自定义 dist 路径 + dist: path.resolve(__dirname, './node_modules/@nocobase/client/app'), +}); +``` + +为了满足各类场景需求,客户端 `@nocobase/client` 提供了丰富的基础组件: + +- Action - 操作 + - Action.Window 当前浏览器窗口/标签里打开 + - Action.Drawer 打开抽屉(默认右侧划出) + - Action.Modal 打开对话框 + - Action.Dropdown 下拉菜单 + - Action.Popover 气泡卡片 + - Action.Group 按钮分组 + - Action.Bar 操作栏 +- AddNew 「添加」模块 + - AddNew.CardItem - 添加区块 + - AddNew.PaneItem - 添加区块(查看面板,与当前查看的数据相关) + - AddNew.FormItem - 添加字段 +- BlockItem/CardItem/FormItem - 装饰器 + - BlockItem - 普通装饰器(无包装效果) + - CardItem - 卡片装饰器 + - FormItem - 字段装饰器 +- Calendar - 日历 +- Cascader - 级联选择 +- Chart - 图表 +- Checkbox - 勾选 +- Checkbox.Group - 多选框 +- Collection - 数据表配置 +- Collection.Field - 数据表字段 +- ColorSelect - 颜色选择器 +- DatePicker - 日期选择器 +- DesignableBar - 配置工具栏 +- Filter - 筛选器 +- Form - 表单 +- Grid - 栅格布局 +- IconPicker - 图标选择器 +- Input - 输入框 +- Input.TextArea - 多行输入框 +- InputNumber - 数字框 +- Kanban - 看板 +- ListPicker - 列表选择器(用于选择、展示关联数据) +- Markdown 编辑器 +- Menu - 菜单 +- Password - 密码 +- Radio - 单选框 +- Select - 选择器 +- Table - 表格 +- Tabs - 标签页 +- TimePicker - 时间选择器 +- Upload - 上传 + +可以自行扩展组件,以上组件基于 Formily 构建,怎么自定义组件大家查看相关组件源码或 Formily 文档,这里说点不一样的。 + +- 如何扩展数据库字段? +- 如何将第三方区块添加到 AddNew 模块中? +- 如何在操作栏里添加更多的内置操作? +- 如何自定义配置工具栏? + +除了组件具备灵活的扩展以外,客户端也可以在任意前端框架中使用,可以自定义 Request 和 Router,如: + +
+import React from 'react';
+import { MemoryRouter } from 'react-router-dom';
+import { ClientSDK, Application } from '@nocobase/client';
+
+// 初始化 client 实例
+const client = new ClientSDK({
+  request: (options) => Promise.resolve({}),
+});
+
+// 适配 Route Component
+const RouteSwitch = createRouteSwitch({
+  components: {
+    AdminLayout,
+    AuthLayout,
+    RouteSchemaRenderer,
+  },
+});
+
+ReactDOM.render(
+  
+    
+      
+    
+  ,
+  document.getElementById('root'),
+);
+
+ +更多细节,可以通过 `create-nocobase-app` 初始化项目脚手架并体验。 + +```bash +yarn create nocobase-app my-nocobase-project +``` + +nocobase-app 默认使用 umijs 作为项目构建工具,并集成了 Server 作数据接口,初始化的目录结构如下: + +```bash +|- src + |- pages + |- apis +|- .env +|- .umirc.ts +|- package.json +``` + +## 场景 - Cases + +小型管理信息系统,具备完整的前后端。 + + + +API 服务,无客户端,提供纯后端接口。 + + + +小程序 + 后台管理,只需要一套数据库,但有两套用户和权限,一套用于后台用户,一套用于小程序用户。 + + + +SaaS 服务(共享用户),每个应用有自己配套的数据库,各应用数据完全隔离。应用不需要用户和权限模块,SaaS 主站全局共享了。 + + + +SaaS 服务(独立用户),每个应用有自己的独立用户模块和权限,应用可以绑定自己的域名。 + + diff --git a/docs/guide/plugin-development/client-side-plugin.md b/docs/guide/plugin-development/client-side-plugin.md new file mode 100644 index 0000000000..0020dcf5de --- /dev/null +++ b/docs/guide/plugin-development/client-side-plugin.md @@ -0,0 +1,16 @@ +--- +order: 3 +--- + +# Client-side Plugin + +客户端插件的目录结构 + +```bash +|- /src/ + |- /api/ # 服务端扩展 + |- /components/ # 客户端组件 + |- index.ts +|- package.json +``` + diff --git a/docs/guide/plugin-development/client-side-plugin.zh-CN.md b/docs/guide/plugin-development/client-side-plugin.zh-CN.md new file mode 100644 index 0000000000..f511f55aa5 --- /dev/null +++ b/docs/guide/plugin-development/client-side-plugin.zh-CN.md @@ -0,0 +1,15 @@ +--- +order: 3 +--- + +# 客户端插件 + +客户端插件的目录结构 + +```bash +|- /src/ + |- /api/ # 服务端扩展 + |- /components/ # 客户端组件 + |- index.ts +|- package.json +``` diff --git a/docs/guide/plugin-development/i18n.md b/docs/guide/plugin-development/i18n.md new file mode 100644 index 0000000000..24f282aa1e --- /dev/null +++ b/docs/guide/plugin-development/i18n.md @@ -0,0 +1,107 @@ +--- +order: 4 +--- + +# Internationalization + +NocoBase 使用 i18next 做国际化支持,前后端统一,支持 namespace,非常适合 NocoBase 的插件系统。 + +## 服务端 + +初始化 i18n + +```ts +const app = new Application({ + i18n: {}, +}); + +// 翻译 +app.i18n.t('hello'); +``` + +在中间件中使用 + +```ts +async (ctx, next) => { + ctx.body = ctx.t('hello'); + // 在中间件中 i18n 是 cloneInstance + ctx.i18n.changeLanguage('zh-CN') +} +``` + +如何在插件中使用 + +```ts +// 添加插件的语言资源 +app.i18n.addResources('zh-CN', 'nocobase-plugin-xxx', { + hello: '你好 plugin-xxx', +}); + +// 需要指定 ns,如: +app.i18n.t('hello', { ns: 'nocobase-plugin-xxx' }); + +// 中间件 +async (ctx, next) => { + ctx.body = ctx.t('hello', { ns: 'nocobase-plugin-xxx' }); +} +``` + +## 客户端 + +在组件中使用,通过 `useTranslation` hook 的方式: + +```js +import { useTranslation } from 'react-i18next'; + +export default () => { + const { t, i18n } = useTranslation('nocobase-plugin-xxx'); + + return ( +
+ + +

{t('hello')}

+
+ ); +}; +``` + +在 Schema 中使用,将 t 注入给 scope + +```js +import { i18n, createSchemaComponent } from '@nocobase/client'; + +const SchemaComponent = createSchemaComponent({ + scope: { + t: i18n.t, + } +}); + +const schema = { + type: 'void', + title: "{{ t('hello') }}", + 'x-component': 'Hello', +}; + +export default () => { + return ( + + ); +} +``` + +## 示例 + +[点此查看完整的示例](#) diff --git a/docs/guide/plugin-development/i18n.zh-CN.md b/docs/guide/plugin-development/i18n.zh-CN.md new file mode 100644 index 0000000000..ce731eda55 --- /dev/null +++ b/docs/guide/plugin-development/i18n.zh-CN.md @@ -0,0 +1,107 @@ +--- +order: 4 +--- + +# 国际化 + +NocoBase 使用 i18next 做国际化支持,前后端统一,支持 namespace,非常适合 NocoBase 的插件系统。 + +## 服务端 + +初始化 i18n + +```ts +const app = new Application({ + i18n: {}, +}); + +// 翻译 +app.i18n.t('hello'); +``` + +在中间件中使用 + +```ts +async (ctx, next) => { + ctx.body = ctx.t('hello'); + // 在中间件中 i18n 是 cloneInstance + ctx.i18n.changeLanguage('zh-CN') +} +``` + +如何在插件中使用 + +```ts +// 添加插件的语言资源 +app.i18n.addResources('zh-CN', 'nocobase-plugin-xxx', { + hello: '你好 plugin-xxx', +}); + +// 需要指定 ns,如: +app.i18n.t('hello', { ns: 'nocobase-plugin-xxx' }); + +// 中间件 +async (ctx, next) => { + ctx.body = ctx.t('hello', { ns: 'nocobase-plugin-xxx' }); +} +``` + +## 客户端 + +在组件中使用,通过 `useTranslation` hook 的方式: + +```js +import { useTranslation } from 'react-i18next'; + +export default () => { + const { t, i18n } = useTranslation('nocobase-plugin-xxx'); + + return ( +
+ + +

{t('hello')}

+
+ ); +}; +``` + +在 Schema 中使用,将 t 注入给 scope + +```js +import { i18n, createSchemaComponent } from '@nocobase/client'; + +const SchemaComponent = createSchemaComponent({ + scope: { + t: i18n.t, + } +}); + +const schema = { + type: 'void', + title: "{{ t('hello') }}", + 'x-component': 'Hello', +}; + +export default () => { + return ( + + ); +} +``` + +## 示例 + +[点此查看完整的示例](#) diff --git a/docs/guide/plugin-development/index.md b/docs/guide/plugin-development/index.md new file mode 100644 index 0000000000..1ab7672d22 --- /dev/null +++ b/docs/guide/plugin-development/index.md @@ -0,0 +1,116 @@ +--- +order: 1 +toc: menu +group: + title: Plugin Development + path: /guide/plugin-development + order: 5 +--- + +# What is a Plugin? + +插件是按功能划分的可插拔的独立模块。 + +## Why Write Plugins? + +NocoBase 提供了丰富的 API 用于应用开发,即使不写插件也是可以实现功能扩展。之所以写成插件,是为了降低耦合,以及更好的复用。做到一处编写,随处使用。当然有些业务联系非常紧密,也没有必要过分的插件化拆分。 + +## How to Write a Plugin? + +例如,添加一个 ratelimit 中间件,可以这样写: + +```ts +import ratelimit from 'koa-ratelimit'; + +app.use(ratelimit({ + driver: 'memory', + db: new Map(), + duration: 60000, + errorMessage: 'Sometimes You Just Have to Slow Down.', + id: (ctx) => ctx.ip, + headers: { + remaining: 'Rate-Limit-Remaining', + reset: 'Rate-Limit-Reset', + total: 'Rate-Limit-Total' + }, + max: 100, + disableHeader: false, + whitelist: (ctx) => { + // some logic that returns a boolean + }, + blacklist: (ctx) => { + // some logic that returns a boolean + } +})); +``` + +但是这种写法,只能开发处理,不能动态移除。为此,NocoBase 提供了可插拔的 `app.plugin()` 接口,用于实现中间件的添加和移除。改造之后,代码如下: + +```ts +import ratelimit from 'koa-ratelimit'; + +class RateLimitPlugin extends Plugin { + constructor(options) { + super(options); + this.ratelimit = ratelimit(options.options); + } + + enable() { + this.app.use(this.ratelimit) + } + + disable() { + this.app.unuse(this.ratelimit); + } +} + +app.plugin(RateLimitPlugin, { + name: 'rate-limit', + version: '1.0.0', + options: { + driver: 'memory', + db: new Map(), + duration: 60000, + errorMessage: 'Sometimes You Just Have to Slow Down.', + id: (ctx) => ctx.ip, + headers: { + remaining: 'Rate-Limit-Remaining', + reset: 'Rate-Limit-Reset', + total: 'Rate-Limit-Total' + }, + max: 100, + disableHeader: false, + whitelist: (ctx) => { + // some logic that returns a boolean + }, + blacklist: (ctx) => { + // some logic that returns a boolean + } + } +}); +``` + +- 将 ratelimit 的参数提炼出来,更进一步可以把参数配置交给插件管理面板 +- 当插件激活时,执行 plugin.enable(),把 ratelimit 添加进来 +- 当插件禁用时,执行 plugin.disable(),把 ratelimit 移除 + +以上就是插件的核心内容了,任何功能扩展都可以这样处理。只要两步: + +- 实现插件的 enable 接口,用于添加功能; +- 再实现 disable 接口,用于移除功能模块。 + +```ts +class MyPlugin extends Plugin { + enable() { + // 添加的逻辑 + } + + disable() { + // 移除的逻辑 + } +} +``` + +**不实现 disable 可不可以?** + +disable 接口是为了实现插件的热插拔,应用不需要重启就能实现插件的激活和禁用。如果某个插件不需要被禁用,也可以只实现 enable 接口。 diff --git a/docs/guide/plugin-development/index.zh-CN.md b/docs/guide/plugin-development/index.zh-CN.md new file mode 100644 index 0000000000..6b2b118f14 --- /dev/null +++ b/docs/guide/plugin-development/index.zh-CN.md @@ -0,0 +1,116 @@ +--- +order: 1 +toc: menu +group: + title: 插件开发 + path: /zh-CN/guide/plugin-development + order: 5 +--- + +# 什么是插件? + +插件是按功能划分的可插拔的独立模块。 + +## 为什么要写插件? + +NocoBase 提供了丰富的 API 用于应用开发,即使不写插件也是可以实现功能扩展。之所以写成插件,是为了降低耦合,以及更好的复用。做到一处编写,随处使用。当然有些业务联系非常紧密,也没有必要过分的插件化拆分。 + +## 如何编写一个插件? + +例如,添加一个 ratelimit 中间件,可以这样写: + +```ts +import ratelimit from 'koa-ratelimit'; + +app.use(ratelimit({ + driver: 'memory', + db: new Map(), + duration: 60000, + errorMessage: 'Sometimes You Just Have to Slow Down.', + id: (ctx) => ctx.ip, + headers: { + remaining: 'Rate-Limit-Remaining', + reset: 'Rate-Limit-Reset', + total: 'Rate-Limit-Total' + }, + max: 100, + disableHeader: false, + whitelist: (ctx) => { + // some logic that returns a boolean + }, + blacklist: (ctx) => { + // some logic that returns a boolean + } +})); +``` + +但是这种写法,只能开发处理,不能动态移除。为此,NocoBase 提供了可插拔的 `app.plugin()` 接口,用于实现中间件的添加和移除。改造之后,代码如下: + +```ts +import ratelimit from 'koa-ratelimit'; + +class RateLimitPlugin extends Plugin { + constructor(options) { + super(options); + this.ratelimit = ratelimit(options.options); + } + + enable() { + this.app.use(this.ratelimit) + } + + disable() { + this.app.unuse(this.ratelimit); + } +} + +app.plugin(RateLimitPlugin, { + name: 'rate-limit', + version: '1.0.0', + options: { + driver: 'memory', + db: new Map(), + duration: 60000, + errorMessage: 'Sometimes You Just Have to Slow Down.', + id: (ctx) => ctx.ip, + headers: { + remaining: 'Rate-Limit-Remaining', + reset: 'Rate-Limit-Reset', + total: 'Rate-Limit-Total' + }, + max: 100, + disableHeader: false, + whitelist: (ctx) => { + // some logic that returns a boolean + }, + blacklist: (ctx) => { + // some logic that returns a boolean + } + } +}); +``` + +- 将 ratelimit 的参数提炼出来,更进一步可以把参数配置交给插件管理面板 +- 当插件激活时,执行 plugin.enable(),把 ratelimit 添加进来 +- 当插件禁用时,执行 plugin.disable(),把 ratelimit 移除 + +以上就是插件的核心内容了,任何功能扩展都可以这样处理。只要两步: + +- 实现插件的 enable 接口,用于添加功能; +- 再实现 disable 接口,用于移除功能模块。 + +```ts +class MyPlugin extends Plugin { + enable() { + // 添加的逻辑 + } + + disable() { + // 移除的逻辑 + } +} +``` + +**不实现 disable 可不可以?** + +disable 接口是为了实现插件的热插拔,应用不需要重启就能实现插件的激活和禁用。如果某个插件不需要被禁用,也可以只实现 enable 接口。 diff --git a/docs/guide/plugin-development/pluggable-interfaces.md b/docs/guide/plugin-development/pluggable-interfaces.md new file mode 100644 index 0000000000..3472d96dcb --- /dev/null +++ b/docs/guide/plugin-development/pluggable-interfaces.md @@ -0,0 +1,45 @@ +--- +order: 2 +--- +# Pluggable Interfaces + +插件是按功能划分的可插拔的独立模块,为了以插件的方式扩展功能,需要实现扩展功能的添加和删除方法。 +NocoBase 的插件化接口主要有: + +## 中间件 + +- 添加:app.use() +- 删除:app.unuse() 暂未实现,可以直接操作 app.middleware 数组来移除 + +## 事件 + +- 添加:app.on() +- 删除:app.removeListener() + +## 资源 + +- 添加:app.resource() +- 删除:暂无 + +## 操作 + +- 添加:app.actions() +- 删除:暂无 + +## 数据表 + +- 添加:app.collection() +- 删除:暂无 + +## 组件(前端) + +- 添加 createRouteSwitch、createCollectionField、createSchemaComponent +- 删除:暂无 + + + +目前 NocoBase 的插件化机制还不完善,不能完全实现热插拔。前端的扩展还得依赖开发手动处理再重新构建。 + + + + diff --git a/docs/guide/plugin-development/pluggable-interfaces.zh-CN.md b/docs/guide/plugin-development/pluggable-interfaces.zh-CN.md new file mode 100644 index 0000000000..4c6298d2fd --- /dev/null +++ b/docs/guide/plugin-development/pluggable-interfaces.zh-CN.md @@ -0,0 +1,46 @@ +--- +order: 2 +--- + +# 插件化接口有哪些? + +插件是按功能划分的可插拔的独立模块,为了以插件的方式扩展功能,需要实现扩展功能的添加和删除方法。 +NocoBase 的插件化接口主要有: + +## 中间件 + +- 添加:app.use() +- 删除:app.unuse() 暂未实现,可以直接操作 app.middleware 数组来移除 + +## 事件 + +- 添加:app.on() +- 删除:app.removeListener() + +## 资源 + +- 添加:app.resource() +- 删除:暂无 + +## 操作 + +- 添加:app.actions() +- 删除:暂无 + +## 数据表 + +- 添加:app.collection() +- 删除:暂无 + +## 组件(前端) + +- 添加 createRouteSwitch、createCollectionField、createSchemaComponent +- 删除:暂无 + + + +目前 NocoBase 的插件化机制还不完善,不能完全实现热插拔。前端的扩展还得依赖开发手动处理再重新构建。 + + + + diff --git a/docs/guide/quickstart.md b/docs/guide/quickstart.md new file mode 100644 index 0000000000..7074a5e483 --- /dev/null +++ b/docs/guide/quickstart.md @@ -0,0 +1,110 @@ +--- +order: 2 +toc: menu +--- + +# Quick Start + +本篇文章将帮助你快速安装并启动 NocoBase,并介绍基本的使用方法。 + +## 1. Requirements + +请确保你的系统已经安装了 Node.js 12.x 或以上版本。 + +```bash +$ node -v +v12.13.1 +``` + +如果你没有安装 Node.js 可以从官网下载并安装[最新的 LTS 版本](https://nodejs.org/en/download/)。如果你打算长期与 Node.js 打交道,推荐使用 [nvm](https://github.com/nvm-sh/nvm)(Win 系统可以使用 [nvm-windows](https://github.com/coreybutler/nvm-windows) )来管理 Node.js 版本。 + +另外,推荐使用 yarn 包管理器。 + +```bash +$ npm install --global yarn +``` + +由于国内网络环境的原因,强烈建议你更换国内镜像。 + +```bash +$ yarn config set registry https://registry.npm.taobao.org/ +``` + +环境准备就绪,下一步我们来安装一个 NocoBase 应用。 + +## 2. Installation and Start-up + +为了方便新人快速的安装并启动, NocoBase 提供了一行非常简单的命令: + +```bash +$ yarn create nocobase-app my-nocobase-app --quickstart +``` + +上面这行命令会帮助你快速的下载、安装并启动 NocoBase 应用。如果你喜欢分步执行,也可以这样: + +```bash +# 1. 创建项目 +$ yarn create nocobase-app my-nocobase-app + +# 2. 切换到项目根目录 +$ cd my-nocobase-app + +# 3. 初始化数据 +$ yarn nocobase init --import-demo + +# 4. 启动项目 +$ yarn start +``` + +分步执行有助于理解整个流程,也更易于排查安装过程中出现的问题。如果出现问题,你也无法自行解决,请将终端输出的错误日志贴在 [GitHub Issue](https://github.com/nocobase/nocobase/issues) 上,大家会一起帮你解决问题。 + +当你看到下面内容,说明你刚才创建的 NocoBase 已经安装并启动了。 + + + +## 3. Log in to NocoBase + +使用浏览器打开 http://localhost:8000 ,你会看到 NocoBase 的登录页面,初始的账号为 `admin@nocobase.com`,密码为 `admin`。 + + + +## 4. Create Collections and Fields + +NocoBase 提供了一个全局的数据表配置面板,方便用户快速的创建数据表和字段。 + + + +按照视频的提示,创建文章(posts)和标签(tags)两张数据表和若干字段。 + +## 5. Configure Menus and Pages + +接着,添加新的菜单分组和页面用于管理刚才创建的文章和标签数据。 + +```ts +// 视频 +``` + +## 6. Create Blocks to Pages + +在上一步配置的页面里创建文章和标签的表格区块,并启用需要开放的操作。 + +```ts +// 视频 +``` + +## 7. Add Data + +现在可以添加文章和标签了。 + +```ts +// 视频 +``` + +## 8. Connect to the API + +除了可视化界面以外,也可以通过 NocoBase 提供的 [REST API](/zh-CN/api/rest-api) 访问数据资源。 + +- 文章资源:http://localhost:8000/api/posts +- 标签资源:http://localhost:8000/api/tags + +你可以直接点击打开上面 API 地址,或者使用类似 Postman 的工具访问。NocoBase 也提供了更贴合的 API Client(JavaScript SDK)来管理 NocoBase 数据资源,更多内容请查看 [API Client](/zh-CN/api/client#apiclient) 章节。 \ No newline at end of file diff --git a/docs/guide/quickstart.zh-CN.md b/docs/guide/quickstart.zh-CN.md new file mode 100644 index 0000000000..9d0933114d --- /dev/null +++ b/docs/guide/quickstart.zh-CN.md @@ -0,0 +1,110 @@ +--- +order: 2 +toc: menu +--- + +# 快速入门 + +本篇文章将帮助你快速安装并启动 NocoBase,并介绍基本的使用方法。 + +## 1. 环境要求 + +请确保你的系统已经安装了 Node.js 12.x 或以上版本。 + +```bash +$ node -v +v12.13.1 +``` + +如果你没有安装 Node.js 可以从官网下载并安装[最新的 LTS 版本](https://nodejs.org/en/download/)。如果你打算长期与 Node.js 打交道,推荐使用 [nvm](https://github.com/nvm-sh/nvm)(Win 系统可以使用 [nvm-windows](https://github.com/coreybutler/nvm-windows) )来管理 Node.js 版本。 + +另外,推荐使用 yarn 包管理器。 + +```bash +$ npm install --global yarn +``` + +由于国内网络环境的原因,强烈建议你更换国内镜像。 + +```bash +$ yarn config set registry https://registry.npm.taobao.org/ +``` + +环境准备就绪,下一步我们来安装一个 NocoBase 应用。 + +## 2. 安装与启动 + +为了方便新人快速的安装并启动, NocoBase 提供了一行非常简单的命令: + +```bash +$ yarn create nocobase-app my-nocobase-app --quickstart +``` + +上面这行命令会帮助你快速的下载、安装并启动 NocoBase 应用。如果你喜欢分步执行,也可以这样: + +```bash +# 1. 创建项目 +$ yarn create nocobase-app my-nocobase-app + +# 2. 切换到项目根目录 +$ cd my-nocobase-app + +# 3. 初始化数据 +$ yarn nocobase init --import-demo + +# 4. 启动项目 +$ yarn start +``` + +分步执行有助于理解整个流程,也更易于排查安装过程中出现的问题。如果出现问题,你也无法自行解决,请将终端输出的错误日志贴在 [GitHub Issue](https://github.com/nocobase/nocobase/issues) 上,大家会一起帮你解决问题。 + +当你看到下面内容,说明你刚才创建的 NocoBase 已经安装并启动了。 + + + +## 3. 登录 NocoBase + +使用浏览器打开 http://localhost:8000 ,你会看到 NocoBase 的登录页面,初始的账号为 `admin@nocobase.com`,密码为 `admin`。 + + + +## 4. 创建数据表和字段 + +NocoBase 提供了一个全局的数据表配置面板,方便用户快速的创建数据表和字段。 + + + +按照视频的提示,创建文章(posts)和标签(tags)两张数据表和若干字段。 + +## 5. 配置菜单和页面 + +接着,添加新的菜单分组和页面用于管理刚才创建的文章和标签数据。 + +```ts +// 视频 +``` + +## 6. 在页面内布置区块 + +在上一步配置的页面里创建文章和标签的表格区块,并启用需要开放的操作。 + +```ts +// 视频 +``` + +## 7. 添加文章和标签数据 + +现在可以添加文章和标签了。 + +```ts +// 视频 +``` + +## 8. 通过 API 访问 + +除了可视化界面以外,也可以通过 NocoBase 提供的 [REST API](/zh-CN/api/rest-api) 访问数据资源。 + +- 文章资源:http://localhost:8000/api/posts +- 标签资源:http://localhost:8000/api/tags + +你可以直接点击打开上面 API 地址,或者使用类似 Postman 的工具访问。NocoBase 也提供了更贴合的 API Client(JavaScript SDK)来管理 NocoBase 数据资源,更多内容请查看 [API Client](/zh-CN/api/client#apiclient) 章节。 \ No newline at end of file diff --git a/docs/API.png b/docs/images/API.png similarity index 100% rename from docs/API.png rename to docs/images/API.png diff --git a/docs/MiniMIS.png b/docs/images/MiniMIS.png similarity index 100% rename from docs/MiniMIS.png rename to docs/images/MiniMIS.png diff --git a/docs/MiniProgram.png b/docs/images/MiniProgram.png similarity index 100% rename from docs/MiniProgram.png rename to docs/images/MiniProgram.png diff --git a/docs/NocoBase.png b/docs/images/NocoBase.png similarity index 100% rename from docs/NocoBase.png rename to docs/images/NocoBase.png diff --git a/docs/SaaS1.png b/docs/images/SaaS1.png similarity index 100% rename from docs/SaaS1.png rename to docs/images/SaaS1.png diff --git a/docs/SaaS2.png b/docs/images/SaaS2.png similarity index 100% rename from docs/SaaS2.png rename to docs/images/SaaS2.png diff --git a/docs/images/img1.png b/docs/images/img1.png new file mode 100644 index 0000000000..1d0234fd6f Binary files /dev/null and b/docs/images/img1.png differ diff --git a/docs/images/img2.png b/docs/images/img2.png new file mode 100644 index 0000000000..2462e69a71 Binary files /dev/null and b/docs/images/img2.png differ diff --git a/docs/index.md b/docs/index.md index b5046162d6..23944fd7c3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,908 +1,5 @@ --- -title: NocoBase -toc: menu +sidemenu: false --- -# NocoBase - -NocoBase 采用微内核架构,框架只保留核心,各类功能以插件形式扩展。 - - - -## 微服务 - Microservices - -为了更快的理解 NocoBase,我们先创建一个应用,新建一个 app.js 文件,代码如下: - -```ts -const { Application } = require('@nocobase/server'); - -const app = new Application({ - // 省略配置信息 -}); - -// 配置一张 users 表 -app.collection({ - name: 'users', - fields: [ - { type: 'string', name: 'username' }, - { type: 'password', name: 'password' } - ], -}); - -// 解析 argv 参数,终端通过命令行进行不同操作 -app.parse(process.argv); -``` - -终端运行 - -```bash -# 根据配置生成数据库表结构 -node app.js db:sync -# 启动应用 -node app.js start --port=3000 -``` - -相关 users 表的 REST API 就生成了 - -```bash -GET http://localhost:3000/api/users -POST http://localhost:3000/api/users -GET http://localhost:3000/api/users/1 -PUT http://localhost:3000/api/users/1 -DELETE http://localhost:3000/api/users/1 -``` - -以上示例,只用了 10 行左右的代码就创建了真实可用的 REST API 服务。除了内置的 REST API 以外,还可以通过 `app.actions()` 自定义其他操作,如登录、注册、注销等。 - -```ts -app.actions({ - async login(ctx, next) {}, - async register(ctx, next) {}, - async logout(ctx, next) {}, -}, { - resourceName: 'users', // 属于 users 资源 -}); -``` - -以上自定义操作的 HTTP API 为: - -```bash -POST http://localhost:3000/api/users:login -POST http://localhost:3000/api/users:register -POST http://localhost:3000/api/users:logout -``` - -自定义的 HTTP API 依旧保持 REST API 的风格,以 `:` 格式表示。实际上 REST API 也可以显式指定 `actionName`,当指定了 `actionName`,无所谓使用什么请求方法,如: - -```bash -# 更新操作 -PUT http://localhost:3000/api/users/1 -# 等同于 -POST http://localhost:3000/api/users:update/1 - -# 删除操作 -DELETE http://localhost:3000/api/users/1 -# 等同于 -GET http://localhost:3000/api/users:destroy/1 -# 等同于 -POST http://localhost:3000/api/users:destroy/1 -``` - -NocoBase 的路由(Resourcer)基于资源(Resource)和操作(Action)设计,将 REST 和 RPC 结合起来,提供更为灵活且统一的 Resource Action API。结合客户端 SDK 是这样的: - -```ts -const { ClientSDK } = require('@nocobase/sdk'); - -const api = new ClientSDK({ - // 可以适配不同 request - request(params) => Promise.resolve({}), -}); - -await api.resource('users').list(); -await api.resource('users').create(); -await api.resource('users').get(); -await api.resource('users').update(); -await api.resource('users').destroy(); -await api.resource('users').login(); -await api.resource('users').register(); -await api.resource('users').logout(); -``` - -## 应用 - Application - -NocoBase 的 Application 继承了 Koa,集成了 DB 和 CLI,添加了一些必要的 API,这里列一些重点: - -- `app.db`:数据库实例,每个 app 都有自己的 db。 - - `db.getCollection()` 数据表/数据集 - - `collection.repository` 数据仓库 - - `collection.model` 数据模型 - - `db.on()` 添加事件监听,由 EventEmitter 提供 - - `db.emit()` 触发事件,由 EventEmitter 提供 - - `db.emitAsync()` 触发异步事件 -- `app.cli`,Commander 实例,提供命令行操作 -- `app.context`,上下文 - - `ctx.db` - - `ctx.action` 当前资源操作实例 - - `action.params` 操作参数 - - `action.mergeParams()` 参数合并方法 -- `app.constructor()` 初始化 -- `app.collection()` 定义数据 Schema,等同于 `app.db.collection()` -- `app.resource()` 定义资源 -- `app.actions()` 定义资源的操作方法 -- `app.on()` 添加事件监听,由 EventEmitter 提供 -- `app.emit()` 触发事件,由 EventEmitter 提供 -- `app.emitAsync()` 触发异步事件 -- `app.use()` 添加中间件,由 Koa 提供 -- `app.command()` 自定义命令行,等同于 `app.cli.command()` -- `app.plugin()` 添加插件 -- `app.load()` 载入配置,主要用于载入插件 -- `app.parse()` 解析 argv 参数,写在最后,等同于 `app.cli.parseAsync()` - -## 数据集 - Collection - -NocoBase 通过 `app.collection()` 方法定义数据的 Schema,Schema 的类型包括: - -属性 Attribute - -- Boolean 布尔型 -- String 字符串 -- Text 长文本 -- Integer 整数型 -- Float 浮点型 -- Decimal 货币 -- Json/Jsonb/Array 不同数据库的 JSON 类型不一致,存在兼容性问题 -- Time 时间 -- Date 日期 -- Virtual 虚拟字段 -- Reference 引用 -- Formula 计算公式 -- Context 上下文 -- Password 密码 -- Sort 排序 - -关系 Association/Realtion - -- HasOne 一对一 -- HasMany 一对多 -- BelongsTo 多对一 -- BelongsToMany 多对多 -- Polymorphic 多态 - -比如一个微型博客的表结构可以这样设计: - -```ts -// 用户 -app.collection({ - name: 'users', - fields: { - username: { type: 'string', unique: true }, - password: { type: 'password', unique: true }, - posts: { type: 'hasMany' }, - }, -}); - -// 文章 -app.collection({ - name: 'posts', - fields: { - title: 'string', - content: 'text', - tags: 'belongsToMany', - comments: 'hasMany', - author: { type: 'belongsTo', target: 'users' }, - }, -}); - -// 标签 -app.collection({ - name: 'tags', - fields: [ - { type: 'string', name: 'name' }, - { type: 'belongsToMany', name: 'posts' }, - ], -}); - -// 评论 -app.collection({ - name: 'comments', - fields: [ - { type: 'text', name: 'content' }, - { type: 'belongsTo', name: 'user' }, - ], -}); -``` - -除了通过 `app.collection()` 配置 schema,也可以直接调用 api 插入或修改 schema,collection 的核心 API 有: - -- `collection` 当前 collection 的数据结构 - - `collection.hasField()` 判断字段是否存在 - - `collection.addField()` 添加字段配置 - - `collection.getField()` 获取字段配置 - - `collection.removeField()` 移除字段配置 - - `collection.sync()` 与数据库表结构同步 -- `collection.repository` 当前 collection 的数据仓库 - - `repository.findMany()` - - `repository.findOne()` - - `repository.create()` - - `repository.update()` - - `repository.destroy()` - - `repository.relatedQuery().for()` - - `create()` - - `update()` - - `destroy()` - - `findMany()` - - `findOne()` - - `set()` - - `add()` - - `remove()` - - `toggle()` -- `collection.model` 当前 collection 的数据模型 - -Collection 示例: - -```ts -const collection = app.db.getCollection('posts'); - -collection.hasField('title'); - -collection.getField('title'); - -// 添加或更新 -collection.addField({ - type: 'string', - name: 'content', -}); - -// 移除 -collection.removeField('content'); - -// 添加、或指定 key path 替换 -collection.mergeField({ - name: 'content', - type: 'string', -}); - -除了全局的 `db.sync()`,也有 `collection.sync()` 方法。 - -await collection.sync(); -``` - -`db:sync` 是非常常用的命令行之一,数据库根据 collection 的 schema 生成表结构。更多详情见 CLI 章节。`db:sync` 之后,就可以往表里写入数据了,可以使用 Repository 或 Model 操作。 - -- Repository 初步提供了 findAll、findOne、create、update、destroy 核心操作方法。 -- Model 为 Sequelize.Model,详细使用说明可以查看 Sequelize 文档。 -- Model 取决于适配的 ORM,Repository 基于 Model 提供统一的接口。 - -通过 Repository 创建数据 - -```ts -const User = app.db.getCollection('users'); - -const user = await User.repository.create({ - title: 't1', - content: 'c1', - author: 1, - tags: [1,2,3], -}, { - whitelist: [], - blacklist: [], -}); - -await User.repository.findMany({ - filter: { - title: 't1', - }, - fields: ['id', 'title', 'content'], - sort: '-created_at', - page: 1, - perPage: 20, -}); - -await User.repository.findOne({ - filter: { - title: 't1', - }, - fields: ['id', 'title', 'content'], - sort: '-created_at', - page: 1, - perPage: 20, -}); - -await User.repository.update({ - title: 't1', - content: 'c1', - author: 1, - tags: [1,2,3], -}, { - filter: {}, - whitelist: [], - blacklist: [], -}); - -await User.repository.destroy({ - filter: {}, -}); -``` - -通过 Model 创建数据 - -```ts -const User = db.getCollection('users'); -const user = await User.model.create({ - title: 't1', - content: 'c1', -}); -``` - -## 资源 & 操作 - Resource & Action - -Resource 是互联网资源,互联网资源都对应一个地址。客户端请求资源地址,服务器响应请求,在这里「请求」就是一种「操作」,在 REST 里通过判断请求方法(GET/POST/PUT/DELETE)来识别具体的操作,但是请求方法局限性比较大,如上文提到的登录、注册、注销就无法用 REST API 的方式表示。为了解决这类问题,NocoBase 以 `:` 格式表示资源的操作。在关系模型的世界里,关系无处不在,基于关系,NocoBase 又延伸了关系资源的概念,对应关系资源的操作的格式为 `.:`。 - -Collection 会自动同步给 Resource,如上文 Collection 章节定义的 Schema,可以提炼的资源有: - -- `users` -- `users.posts` -- `posts` -- `posts.tags` -- `posts.comments` -- `posts.author` -- `tags` -- `tags.posts` -- `comments` -- `comments.user` - - - -- Collection 定义数据的 schema(结构和关系) -- Resource 定义数据的 action(操作方法) -- Resource 请求和响应的数据结构由 Collection 定义 -- Collection 默认自动同步给 Resource -- Resource 的概念更大,除了对接 Collection 以外,也可以对接外部数据或其他自定义 - - - -资源相关 API 有: - -- `app.resource()` -- `app.actions()` -- `ctx.action` - -一个资源可以有多个操作。 - -```ts -// 数据类 -app.resource({ - name: 'users', - actions: { - async list(ctx, next) {}, - async get(ctx, next) {}, - async create(ctx, next) {}, - async update(ctx, next) {}, - async destroy(ctx, next) {}, - }, -}); - -// 非数据类 -app.resource({ - name: 'server', - actions: { - // 获取服务器时间 - getTime(ctx, next) {}, - // 健康检测 - healthCheck(ctx, next) {}, - }, -}); -``` - -常规操作可以用于不同资源 - -```ts -app.actions({ - async list(ctx, next) {}, - async get(ctx, next) {}, - async create(ctx, next) {}, - async update(ctx, next) {}, - async destroy(ctx, next) {}, -}, { - // 不指定 resourceName 时,全局共享 - resourceNames: ['posts', 'comments', 'users'], -}); -``` - -在资源内部定义的 action 不会共享,常规类似增删改查的操作建议设置为全局,`app.resource()` 只设置参数,如: - -```ts -app.resource({ - name: 'users', - actions: { - list: { - fields: ['id', 'username'], // 只输出 id 和 username 字段 - filter: { - 'username.$ne': 'admin', // 数据范围筛选过滤 username != admin - }, - sort: ['-created_at'], // 创建时间倒序 - perPage: 50, - }, - get: { - fields: ['id', 'username'], // 只输出 id 和 username 字段 - filter: { - 'username.$ne': 'admin', // 数据范围筛选过滤 username != admin - }, - }, - create: { - fields: ['username'], // 白名单 - }, - update: { - fields: ['username'], // 白名单 - }, - destroy: { - filter: { // 不能删除 admin - 'username.$ne': 'admin', - }, - }, - }, -}); - -// app 默认已经内置了 list, get, create, update, destroy 操作 -app.actions({ - async list(ctx, next) {}, - async get(ctx, next) {}, - async create(ctx, next) {}, - async update(ctx, next) {}, - async destroy(ctx, next) {}, -}); -``` - -在 Middleware Handler 和 Action Handler 里,都可以通过 `ctx.action` 获取到当前 action 实例,提供了两个非常有用的 API: - -- `ctx.action.params`:获取操作对应的参数 -- `ctx.action.mergeParams()`:处理多来源参数合并 - -`ctx.action.params` 有: - -- 定位资源和操作 - - `actionName` - - `resourceName` - - `associatedName` -- 定位资源 ID - - `resourceId` - - `associatedId` -- request query - - `filter` - - `fields` - - `sort` - - `page` - - `perPage` - - 其他 query 值 -- request body - - `values` - -示例: - -```ts -async function (ctx, next) { - const { resourceName, resourceId, filter, fields } = ctx.action.params; - // ... -} -``` - -`ctx.action.mergeParams()` 主要用于多来源参数合并,以 `filter` 参数为例。如:客户端请求日期 2021-09-15 创建的文章 - -```bash -GET /api/posts:list?filter={"created_at": "2021-09-15"} -``` - -资源设置锁定只能查看已发布的文章 - -```ts -app.resource({ - name: 'posts', - actions: { - list: { - filter: { status: 'publish' }, // 只能查看已发布文章 - }, - }, -}) -``` - -权限设定,只能查看自己创建的文章 - -```ts -app.use(async (ctx, next) => { - const { resourceName, actionName } = ctx.action.params; - if (resourceName === 'posts' && actionName === 'list') { - ctx.action.mergeParams({ - filter: { - created_by_id: ctx.state.currentUser.id, - }, - }); - } - await next(); -}); -``` - -以上客户端、资源配置、中间件内我们都指定了 filter 参数,三个来源的参数最终会合并在一起作为最终的过滤条件: - -```ts -async function list(ctx, next) { - // list 操作中获取到的 filter - console.log(ctx.params.filter); - // filter 是特殊的 and 合并 - // { - // and: [ - // { created_at: '2021-09-15' }, - // { status: 'publish' }, - // { created_by_id: 1, } - // ] - // } -} -``` - -## 事件 - Event - -在操作执行前、后都放置了相关事件监听器,可以通过 `app.db.on()` 和 `app.on()` 添加。区别在于: - -- `app.db.on()` 添加数据库层面的监听器 -- `app.on()` 添加服务器应用层面的监听器 - -以 `users:login` 为例,在数据库里为「查询」操作,在应用里为「登录」操作。也就是说,如果需要记录登录操作日志,要在 `app.on()` 里处理。 - -```ts -// 创建数据时,执行 User.create() 时触发 -app.db.on('users.beforeCreate', async (model) => {}); - -// 客户端 `POST /api/users:login` 时触发 -app.on('users.beforeLogin', async (ctx, next) => {}); - -// 客户端 `POST /api/users` 时触发 -app.on('users.beforeCreate', async (ctx, next) => {}); -``` - -## 中间件 - Middleware - -Server Application 基于 Koa,所有 Koa 的插件(中间件)都可以直接使用,可以通过 `app.use()` 添加。如: - -```ts -const responseTime = require('koa-response-time'); -app.use(responseTime()); - -app.use(async (ctx, next) => { - await next(); -}); -``` - -与 `koa.use(middleware)` 略有不同,`app.use(middleware, options)` 多了个 options 参数,可以用于限定 resource 和 action,也可以用于控制中间件的插入位置。 - -```ts -import { middleware } from '@nocobase/server'; - -app.use(async (ctx, next) => {}, { - name: 'middlewareName1', - resourceNames: [], // 作用于资源内所有 actions - actionNames: [ - 'list', // 全部 list action - 'users:list', // 仅 users 资源的 list action, - ], - insertBefore: '', - insertAfter: '', -}); -``` - -## 命令行 - CLI - -Application 除了可以做 HTTP Server 以外,也是 CLI(内置了 Commander)。目前内置的命令有: - -- `init` 初始化 -- `db:sync --force` 用于配置与数据库表结构同步 -- `start --port` 启动应用 -- `plugin:**` 插件相关 - -自定义: - -```ts -app.command('foo').action(async () => { - console.log('foo...'); -}); -``` - -## 插件 - Plugin - -上文,讲述了核心的扩展接口,包括但不局限于: - -- Database/Collection - - `app.db` database 实例 - - `app.collection()` 等同于 `app.db.collection()` -- Resource/Action - - `app.resource()` 等同于 `app.resourcer.define()` - - `app.actions()` 等同于 `app.resourcer.registerActions()` -- Hook/Event - - `app.on()` 添加服务器监听器 - - `app.db.on()` 添加数据库监听器 -- Middleware - - `app.use()` 添加中间件 -- CLI - - `app.cli` commander 实例 - - `app.command()` 等同于 `app.cli.command()` - -基于以上扩展接口,进一步提供了模块化、可插拔的插件,可以通过 `app.plugin()` 添加。插件的流程包括安装、升级、激活、载入、禁用、卸载,不需要的流程可缺失。如: - -**最简单的插件** - -```ts -app.plugin(function pluginName1() { - -}); -``` - -这种方式添加的插件会直接载入,无需安装。 - -**JSON 风格** - -```ts -const plugin = app.plugin({ - enable: false, // 默认为 true,不需要启用时可以禁用。 - name: 'plugin-name1', - displayName: '插件名称', - version: '1.2.3', - dependencies: { - pluginName2: '1.x', - pluginName3: '1.x', - }, - async install() {}, - async upgrade() {}, - async activate() {}, - async bootstrap() {}, - async deactivate() {}, - async unstall() {}, -}); -// 通过 api 激活插件 -plugin.activate(); -``` - -**OOP 风格** - -```ts -class MyPlugin extends Plugin { - async install() {} - async upgrade() {} - async bootstrap() {} - async activate() {} - async deactivate() {} - async unstall() {} -} - -app.plugin(MyPlugin); -// 或 -app.plugin({ - name: 'plugin-name1', - displayName: '插件名称', - version: '1.2.3', - dependencies: { - pluginName2: '1.x', - pluginName3: '1.x', - }, - plugin: MyPlugin, -}); -``` - -**引用独立的 Package** - -```ts -app.plugin('@nocobase/plugin-action-logs'); -``` - -插件信息也可以直接写在 `package.json` 里 - -```js -{ - name: 'pluginName1', - displayName: '插件名称', - version: '1.2.3', - dependencies: { - pluginName2: '1.x', - pluginName3: '1.x', - }, -} -``` - -**插件 CLI** - -```bash -plugin:install pluginName1 -plugin:unstall pluginName1 -plugin:activate pluginName1 -plugin:deactivate pluginName1 -``` - -目前已有的插件: - -- @nocobase/plugin-collections 提供数据表配置接口,可通过 HTTP API 管理数据表。 -- @nocobase/plugin-action-logs 操作日志 -- @nocobase/plugin-automations 自动化(未升级 v0.5,暂不能使用) -- @nocobase/plugin-china-region 中国行政区 -- @nocobase/plugin-client 提供客户端,无代码的可视化配置界面,需要与 @nocobase/client 配合使用 -- @nocobase/plugin-export 导出 -- @nocobase/plugin-file-manager 文件管理器 -- @nocobase/plugin-permissions 角色和权限 -- @nocobase/plugin-system-settings 系统配置 -- @nocobase/plugin-ui-router 前端路由配置 -- @nocobase/plugin-ui-schema ui 配置 -- @nocobase/plugin-users 用户模块 - -## 测试 - Testing - -有代码就需要测试,@nocobase/test 提供了 mockDatabase 和 mockServer 用于数据库和服务器的测试,如: - -```ts -import { mockServer, MockServer } from '@nocobase/test'; - -describe('mock server', () => { - let api: MockServer; - - beforeEach(() => { - api = mockServer({ - dataWrapping: false, - }); - api.actions({ - list: async (ctx, next) => { - ctx.body = [1, 2]; - await next(); - }, - }); - api.resource({ - name: 'test', - }); - }); - - afterEach(async () => { - return api.destroy(); - }); - - it('agent.get', async () => { - const response = await api.agent().get('/test'); - expect(response.body).toEqual([1, 2]); - }); - - it('agent.resource', async () => { - const response = await api.agent().resource('test').list(); - expect(response.body).toEqual([1, 2]); - }); -}); -``` - -## 客户端 - Client - -为了让更多非开发人员也能参与进来,NocoBase 提供了配套的客户端插件 —— 无代码的可视化配置界面。客户端插件需要与 @nocobase/client 配合使用,可以直接使用,也可以自行改造。 - -插件配置 - -```ts -app.plugin('@nocobase/plugin-client', { - // 自定义 dist 路径 - dist: path.resolve(__dirname, './node_modules/@nocobase/client/app'), -}); -``` - -为了满足各类场景需求,客户端 `@nocobase/client` 提供了丰富的基础组件: - -- Action - 操作 - - Action.Window 当前浏览器窗口/标签里打开 - - Action.Drawer 打开抽屉(默认右侧划出) - - Action.Modal 打开对话框 - - Action.Dropdown 下拉菜单 - - Action.Popover 气泡卡片 - - Action.Group 按钮分组 - - Action.Bar 操作栏 -- AddNew 「添加」模块 - - AddNew.CardItem - 添加区块 - - AddNew.PaneItem - 添加区块(查看面板,与当前查看的数据相关) - - AddNew.FormItem - 添加字段 -- BlockItem/CardItem/FormItem - 装饰器 - - BlockItem - 普通装饰器(无包装效果) - - CardItem - 卡片装饰器 - - FormItem - 字段装饰器 -- Calendar - 日历 -- Cascader - 级联选择 -- Chart - 图表 -- Checkbox - 勾选 -- Checkbox.Group - 多选框 -- Collection - 数据表配置 -- Collection.Field - 数据表字段 -- ColorSelect - 颜色选择器 -- DatePicker - 日期选择器 -- DesignableBar - 配置工具栏 -- Filter - 筛选器 -- Form - 表单 -- Grid - 栅格布局 -- IconPicker - 图标选择器 -- Input - 输入框 -- Input.TextArea - 多行输入框 -- InputNumber - 数字框 -- Kanban - 看板 -- ListPicker - 列表选择器(用于选择、展示关联数据) -- Markdown 编辑器 -- Menu - 菜单 -- Password - 密码 -- Radio - 单选框 -- Select - 选择器 -- Table - 表格 -- Tabs - 标签页 -- TimePicker - 时间选择器 -- Upload - 上传 - -可以自行扩展组件,以上组件基于 Formily 构建,怎么自定义组件大家查看相关组件源码或 Formily 文档,这里说点不一样的。 - -- 如何扩展数据库字段? -- 如何将第三方区块添加到 AddNew 模块中? -- 如何在操作栏里添加更多的内置操作? -- 如何自定义配置工具栏? - -除了组件具备灵活的扩展以外,客户端也可以在任意前端框架中使用,可以自定义 Request 和 Router,如: - -
-import React from 'react';
-import { MemoryRouter } from 'react-router-dom';
-import { ClientSDK, Application } from '@nocobase/client';
-
-// 初始化 client 实例
-const client = new ClientSDK({
-  request: (options) => Promise.resolve({}),
-});
-
-// 适配 Route Component
-const RouteSwitch = createRouteSwitch({
-  components: {
-    AdminLayout,
-    AuthLayout,
-    RouteSchemaRenderer,
-  },
-});
-
-ReactDOM.render(
-  
-    
-      
-    
-  ,
-  document.getElementById('root'),
-);
-
- -更多细节,可以通过 `create-nocobase-app` 初始化项目脚手架并体验。 - -```bash -yarn create nocobase-app my-nocobase-project -``` - -nocobase-app 默认使用 umijs 作为项目构建工具,并集成了 Server 作数据接口,初始化的目录结构如下: - -```bash -|- src - |- pages - |- apis -|- .env -|- .umirc.ts -|- package.json -``` - -## 场景 - Cases - -小型管理信息系统,具备完整的前后端。 - - - -API 服务,无客户端,提供纯后端接口。 - - - -小程序 + 后台管理,只需要一套数据库,但有两套用户和权限,一套用于后台用户,一套用于小程序用户。 - - - -SaaS 服务(共享用户),每个应用有自己配套的数据库,各应用数据完全隔离。应用不需要用户和权限模块,SaaS 主站全局共享了。 - - - -SaaS 服务(独立用户),每个应用有自己的独立用户模块和权限,应用可以绑定自己的域名。 - - + \ No newline at end of file diff --git a/docs/index.zh-CN.md b/docs/index.zh-CN.md new file mode 100644 index 0000000000..0476de3a3f --- /dev/null +++ b/docs/index.zh-CN.md @@ -0,0 +1,5 @@ +--- +sidemenu: false +--- + + \ No newline at end of file diff --git a/docs/plugins/index.md b/docs/plugins/index.md new file mode 100644 index 0000000000..0828f57164 --- /dev/null +++ b/docs/plugins/index.md @@ -0,0 +1,88 @@ +--- +title: Plugins +toc: menu +nav: + title: Plugins + order: 4 +--- + +## 插件管理器 + +开发可以通过命令行下载、激活、禁用、移除插件,对应的命令行有: + +```bash +# 下载插件,可以通过 --enable 参数快速激活 +yarn nocobase pm:download --enable +# 激活插件 +yarn nocobase pm:enable +# 禁用插件 +yarn nocobase pm:disable +# 移除插件 +yarn nocobase pm:remove +``` + +## 已有的插件列表 + +### @nocobase/plugin-collections 数据表配置 + +提供 HTTP API 的方式管理数据表和字段 + +### @nocobase/plugin-permissions + +权限模块 + +### @nocobase/plugin-users + +用户模块 + +### @nocobase/plugin-system-settings + +站点信息配置 + +### @nocobase/plugin-china-region + +字段扩展,中国行政区 + +### @nocobase/plugin-file-manager + +字段扩展,附件字段 + +### @nocobase/plugin-action-logs + +操作日志 + +### @nocobase/plugin-multi-apps + +动态多应用,一个简易的 SaaS + +### @nocobase/plugin-export + +操作扩展,导出 + +### @nocobase/plugin-notifications + +通知模块(半成品),暂时只支持邮件发送,没有可视化界面 + +### @nocobase/plugin-automations + +自动化(暂不可用) + +### @nocobase/plugin-client + +客户端插件,为 nocobase 提供可视化配置的支持。依赖的插件有: + +- @nocobase/plugin-collections(必须) +- @nocobase/plugin-permissions(必须) +- @nocobase/plugin-users(必须) +- @nocobase/plugin-system-settings(必须) +- @nocobase/plugin-file-manager(必须) +- @nocobase/plugin-china-region(可选) +- @nocobase/plugin-action-logs(可选) + +包括几部分内容: + +- 将客户端 ui-schema 存储在服务端,以实现按需动态输出 +- 将客户端 ui-router 存储在服务端,以实现按需动态输出 +- 提供 app dist 的 static server 支持,可以配置 app 的 dist 路径 +- 为 nocobase 安装提供初始化 demo 数据导入的支持,可通过 importData 配置 +- 提供 collections 可视化支持 diff --git a/docs/plugins/index.zh-CN.md b/docs/plugins/index.zh-CN.md new file mode 100644 index 0000000000..223db161c0 --- /dev/null +++ b/docs/plugins/index.zh-CN.md @@ -0,0 +1,88 @@ +--- +title: 插件 +toc: menu +nav: + title: 插件 + order: 4 +--- + +## 插件管理器 + +开发可以通过命令行下载、激活、禁用、移除插件,对应的命令行有: + +```bash +# 下载插件,可以通过 --enable 参数快速激活 +yarn nocobase pm:download --enable +# 激活插件 +yarn nocobase pm:enable +# 禁用插件 +yarn nocobase pm:disable +# 移除插件 +yarn nocobase pm:remove +``` + +## 已有的插件列表 + +### @nocobase/plugin-collections 数据表配置 + +提供 HTTP API 的方式管理数据表和字段 + +### @nocobase/plugin-permissions + +权限模块 + +### @nocobase/plugin-users + +用户模块 + +### @nocobase/plugin-system-settings + +站点信息配置 + +### @nocobase/plugin-china-region + +字段扩展,中国行政区 + +### @nocobase/plugin-file-manager + +字段扩展,附件字段 + +### @nocobase/plugin-action-logs + +操作日志 + +### @nocobase/plugin-multi-apps + +动态多应用,一个简易的 SaaS + +### @nocobase/plugin-export + +操作扩展,导出 + +### @nocobase/plugin-notifications + +通知模块(半成品),暂时只支持邮件发送,没有可视化界面 + +### @nocobase/plugin-automations + +自动化(暂不可用) + +### @nocobase/plugin-client + +客户端插件,为 nocobase 提供可视化配置的支持。依赖的插件有: + +- @nocobase/plugin-collections(必须) +- @nocobase/plugin-permissions(必须) +- @nocobase/plugin-users(必须) +- @nocobase/plugin-system-settings(必须) +- @nocobase/plugin-file-manager(必须) +- @nocobase/plugin-china-region(可选) +- @nocobase/plugin-action-logs(可选) + +包括几部分内容: + +- 将客户端 ui-schema 存储在服务端,以实现按需动态输出 +- 将客户端 ui-router 存储在服务端,以实现按需动态输出 +- 提供 app dist 的 static server 支持,可以配置 app 的 dist 路径 +- 为 nocobase 安装提供初始化 demo 数据导入的支持,可通过 importData 配置 +- 提供 collections 可视化支持