feat: docs & examples

This commit is contained in:
chenos 2021-09-16 15:57:21 +08:00
parent b7eb720eb4
commit 17db8b8afb
9 changed files with 178 additions and 129 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

View File

@ -1,17 +1,17 @@
--- ---
title: 介绍 title: NocoBase
toc: menu toc: menu
--- ---
# NocoBase # NocoBase
考虑到大家是初次接触 NocoBase开发文档的第一篇先带大家了解基础概念。NocoBase 采用微内核架构,框架只保留核心,各类功能以插件形式扩展。 NocoBase 采用微内核架构,框架只保留核心,各类功能以插件形式扩展。
<img src="./NocoBase.png" style="max-width: 800px; width: 100%;"> <img src="./NocoBase.png" style="max-width: 800px; width: 100%;">
## 微服务 - Microservices ## 微服务 - Microservices
首先我们创建一个应用,新建一个 app.js 文件,代码如下: 为了更快的理解 NocoBase我们先创建一个应用,新建一个 app.js 文件,代码如下:
```ts ```ts
const { Application } = require('@nocobase/server'); const { Application } = require('@nocobase/server');
@ -23,10 +23,10 @@ const app = new Application({
// 配置一张 users 表 // 配置一张 users 表
app.collection({ app.collection({
name: 'users', name: 'users',
schema: { schema: [
username: 'string', { type: 'string', name: 'username' },
password: 'password', { type: 'password', name: 'password' }
}, ],
}); });
// 解析 argv 参数,终端通过命令行进行不同操作 // 解析 argv 参数,终端通过命令行进行不同操作
@ -37,7 +37,7 @@ app.parse(process.argv);
```bash ```bash
# 根据配置生成数据库表结构 # 根据配置生成数据库表结构
node app.js db sync node app.js db:sync
# 启动应用 # 启动应用
node app.js start --port=3000 node app.js start --port=3000
``` ```
@ -113,18 +113,21 @@ await api.resource('users').logout();
NocoBase 的 Application 继承了 Koa集成了 DB 和 CLI添加了一些必要的 API这里列一些重点 NocoBase 的 Application 继承了 Koa集成了 DB 和 CLI添加了一些必要的 API这里列一些重点
- `app.db`:数据库实例,每个 app 都有自己的 db。 - `app.db`:数据库实例,每个 app 都有自己的 db。
- `db.getTable()` 数据表/数据集配置 - `db.getCollection()` 数据表/数据集
- `db.getRepository()` 数据仓库 - `collection.schema` 数据结构
- `db.getModel()` 数据模型 - `collection.repository` 数据仓库
- `collection.model` 数据模型
- `db.on()` 添加事件监听,由 EventEmitter 提供 - `db.on()` 添加事件监听,由 EventEmitter 提供
- `db.emit()` 触发事件,由 EventEmitter 提供 - `db.emit()` 触发事件,由 EventEmitter 提供
- `db.emitAsync()` 触发异步事件 - `db.emitAsync()` 触发异步事件
- `app.cli`commander 实例,提供命令行操作 - `app.cli`Commander 实例,提供命令行操作
- `app.context`,上下文 - `app.context`,上下文
- `ctx.db` - `ctx.db`
- `ctx.action` - `ctx.action` 当前资源操作实例
- `action.params` 操作参数
- `action.mergeParams()` 参数合并方法
- `app.constructor()` 初始化 - `app.constructor()` 初始化
- `app.collection()` 定义数据 Schema等同于 `app.db.table()` - `app.collection()` 定义数据 Schema等同于 `app.db.collection()`
- `app.resource()` 定义资源 - `app.resource()` 定义资源
- `app.actions()` 定义资源的操作方法 - `app.actions()` 定义资源的操作方法
- `app.on()` 添加事件监听,由 EventEmitter 提供 - `app.on()` 添加事件监听,由 EventEmitter 提供
@ -136,8 +139,6 @@ NocoBase 的 Application 继承了 Koa集成了 DB 和 CLI添加了一些
- `app.load()` 载入配置,主要用于载入插件 - `app.load()` 载入配置,主要用于载入插件
- `app.parse()` 解析 argv 参数,写在最后,等同于 `app.cli.parseAsync()` - `app.parse()` 解析 argv 参数,写在最后,等同于 `app.cli.parseAsync()`
经过几次改进,以上罗列的 API 趋近于稳定,但也可能有所变动。
## 数据集 - Collection ## 数据集 - Collection
NocoBase 通过 `app.collection()` 方法定义数据的 SchemaSchema 的类型包括: NocoBase 通过 `app.collection()` 方法定义数据的 SchemaSchema 的类型包括:
@ -175,17 +176,9 @@ NocoBase 通过 `app.collection()` 方法定义数据的 SchemaSchema 的类
app.collection({ app.collection({
name: 'users', name: 'users',
schema: { schema: {
username: { username: { type: 'string', unique: true },
type: 'string', password: { type: 'password', unique: true },
unique: true, posts: { type: 'hasMany' },
},
password: {
type: 'password',
unique: true,
},
posts: {
type: 'hasMany',
},
}, },
}); });
@ -193,14 +186,11 @@ app.collection({
app.collection({ app.collection({
name: 'posts', name: 'posts',
schema: { schema: {
title: 'string', title: 'string',
content: 'text', content: 'text',
tags: 'belongsToMany', tags: 'belongsToMany',
comments: 'hasMany', comments: 'hasMany',
author: { author: { type: 'belongsTo', target: 'users' },
type: 'belongsTo',
target: 'users',
},
}, },
}); });
@ -225,13 +215,6 @@ app.collection({
除了通过 `app.collection()` 配置 schema也可以直接调用 api 插入或修改 schemacollection 的核心 API 有: 除了通过 `app.collection()` 配置 schema也可以直接调用 api 插入或修改 schemacollection 的核心 API 有:
- `collection.model` 当前 collection 的数据模型
- `collection.repository` 当前 collection 的数据仓库
- `repository.findAll()`
- `repository.findOne()`
- `repository.create()`
- `repository.update()`
- `repository.destroy()`
- `collection.schema` 当前 collection 的数据结构 - `collection.schema` 当前 collection 的数据结构
- `schema.has()` 判断是否存在 - `schema.has()` 判断是否存在
- `schema.get()` 获取 - `schema.get()` 获取
@ -239,8 +222,15 @@ app.collection({
- `schema.merge()` 添加、或指定 key path 替换 - `schema.merge()` 添加、或指定 key path 替换
- `schema.replace()` 替换 - `schema.replace()` 替换
- `schema.delete()` 删除 - `schema.delete()` 删除
- `collection.repository` 当前 collection 的数据仓库
- `repository.findAll()`
- `repository.findOne()`
- `repository.create()`
- `repository.update()`
- `repository.destroy()`
- `collection.model` 当前 collection 的数据模型
如: Schema 示例
```ts ```ts
const collection = app.db.getCollection('posts'); const collection = app.db.getCollection('posts');
@ -269,7 +259,7 @@ collection.schema.merge({
await collection.sync(); await collection.sync();
``` ```
存在外键关联时,也无需顾虑建表和字段的顺序,`db sync` 时会自动处理。`db sync` 之后,就可以往表里写入数据了。可以使用 Repository 或 Model 操作。 `db:sync` 是非常常用的命令行之一,数据库根据 collection 的 schema 生成表结构。更多详情见 CLI 章节。`db:sync` 之后,就可以往表里写入数据了,可以使用 Repository 或 Model 操作。
- Repository 初步提供了 findAll、findOne、create、update、destroy 核心操作方法。 - Repository 初步提供了 findAll、findOne、create、update、destroy 核心操作方法。
- Model 为 Sequelize.Model详细使用说明可以查看 Sequelize 文档。 - Model 为 Sequelize.Model详细使用说明可以查看 Sequelize 文档。
@ -344,7 +334,7 @@ await user.updateAssociations({
Resource 是互联网资源,互联网资源都对应一个地址。客户端请求资源地址,服务器响应请求,在这里「请求」就是一种「操作」,在 REST 里通过判断请求方法GET/POST/PUT/DELETE来识别具体的操作但是请求方法局限性比较大如上文提到的登录、注册、注销就无法用 REST API 的方式表示。为了解决这类问题NocoBase 以 `<resourceName>:<actionName>` 格式表示资源的操作。在关系模型的世界里关系无处不在基于关系NocoBase 又延伸了关系资源的概念,对应关系资源的操作的格式为 `<associatedName>.<resourceName>:<actionName>` Resource 是互联网资源,互联网资源都对应一个地址。客户端请求资源地址,服务器响应请求,在这里「请求」就是一种「操作」,在 REST 里通过判断请求方法GET/POST/PUT/DELETE来识别具体的操作但是请求方法局限性比较大如上文提到的登录、注册、注销就无法用 REST API 的方式表示。为了解决这类问题NocoBase 以 `<resourceName>:<actionName>` 格式表示资源的操作。在关系模型的世界里关系无处不在基于关系NocoBase 又延伸了关系资源的概念,对应关系资源的操作的格式为 `<associatedName>.<resourceName>:<actionName>`
Collection 会自动同步给 Resource上文 Collection 章节定义的 Schema提炼的资源有 Collection 会自动同步给 Resource上文 Collection 章节定义的 Schema可以提炼的资源有:
- `users` - `users`
- `users.posts` - `users.posts`
@ -492,7 +482,7 @@ async function (ctx, next) {
} }
``` ```
多来源参数合并,以 `filter` 参数为例。如:客户端请求日期 2021-09-15 创建的文章 `ctx.action.mergeParams()` 主要用于多来源参数合并,以 `filter` 参数为例。如:客户端请求日期 2021-09-15 创建的文章
```bash ```bash
GET /api/posts:list?filter={"created_at": "2021-09-15"} GET /api/posts:list?filter={"created_at": "2021-09-15"}
@ -533,6 +523,7 @@ app.use(async (ctx, next) => {
async function list(ctx, next) { async function list(ctx, next) {
// list 操作中获取到的 filter // list 操作中获取到的 filter
console.log(ctx.params.filter); console.log(ctx.params.filter);
// filter 是特殊的 and 合并
// { // {
// and: [ // and: [
// { created_at: '2021-09-15' }, // { created_at: '2021-09-15' },
@ -576,27 +567,31 @@ app.use(async (ctx, next) => {
}); });
``` ```
弥补 `app.use()` 不足,加了个 `middleware()` 适配器,可以用于限定 resource 和 action。除此之外也可以控制中间件的插入位置。 `koa.use(middleware)` 略有不同,`app.use(middleware, options)` 多了个 options 参数,可以用于限定 resource 和 action也可以用于控制中间件的插入位置。
```ts ```ts
import { middleware } from '@nocobase/server'; import { middleware } from '@nocobase/server';
app.use(middleware(async (ctx, next) => {}, { app.use(async (ctx, next) => {}, {
name: 'middlewareName1', name: 'middlewareName1',
resourceNames: [], resourceNames: [], // 作用于资源内所有 actions
actionNames: [], actionNames: [
'list', // 全部 list action
'users:list', // 仅 users 资源的 list action,
],
insertBefore: '', insertBefore: '',
insertAfter: '', insertAfter: '',
})); });
``` ```
## 命令行 - CLI ## 命令行 - CLI
Application 除了可以做 HTTP Server 以外,也可以是 CLI内置了 Commander。目前内置的命令有 Application 除了可以做 HTTP Server 以外,也是 CLI内置了 Commander。目前内置的命令有
- `db sync --force` 用于配置与数据库表结构同步 - `init` 初始化
- `db:sync --force` 用于配置与数据库表结构同步
- `start --port` 启动应用 - `start --port` 启动应用
- `plugin` 插件相关 - `plugin:**` 插件相关
自定义: 自定义:
@ -624,10 +619,8 @@ app.command('foo').action(async () => {
- CLI - CLI
- `app.cli` commander 实例 - `app.cli` commander 实例
- `app.command()` 等同于 `app.cli.command()` - `app.command()` 等同于 `app.cli.command()`
- Plugin
- `app.plugin` 添加插件
基于以上扩展接口,进一步提供了模块化、可插拔的插件,可以通过 `app.plugin()` 添加。完整的插件包括安装、升级、激活、载入、禁用、卸载流程,但是并不是所有插件都要这完整的流程。比如: 基于以上扩展接口,进一步提供了模块化、可插拔的插件,可以通过 `app.plugin()` 添加。插件的流程包括安装、升级、激活、载入、禁用、卸载,不需要的流程可缺失。如:
**最简单的插件** **最简单的插件**
@ -709,10 +702,10 @@ app.plugin('@nocobase/plugin-action-logs');
**插件 CLI** **插件 CLI**
```bash ```bash
plugin install pluginName1 plugin:install pluginName1
plugin unstall pluginName1 plugin:unstall pluginName1
plugin activate pluginName1 plugin:activate pluginName1
plugin deactivate pluginName1 plugin:deactivate pluginName1
``` ```
目前已有的插件: 目前已有的插件:

View File

@ -1,16 +1,12 @@
import { Application } from '@nocobase/server/src'; import { Application } from '@nocobase/server/src';
import path from 'path'; import path from 'path';
import mount from 'koa-mount';
import compose from 'koa-compose'; import compose from 'koa-compose';
const keys = __dirname.split(path.sep); const keys = __dirname.split(path.sep);
const slug = keys[keys.length - 2]; const slug = keys[keys.length - 2];
const apps = new Map<string, Application>();
function createApp(opts) { function createApp(opts) {
const { name, prefix } = opts; const { name } = opts;
const options = { const options = {
database: { database: {
username: process.env.DB_USER, username: process.env.DB_USER,
@ -23,28 +19,27 @@ function createApp(opts) {
charset: 'utf8mb4', charset: 'utf8mb4',
collate: 'utf8mb4_unicode_ci', collate: 'utf8mb4_unicode_ci',
}, },
appName: name,
hooks: { hooks: {
beforeDefine(model, options) { beforeDefine(model, options) {
options.tableName = `examples_${slug}_${name}_${options.tableName || options.name.plural}`; options.tableName = `examples_${slug}_${name}_${
options.tableName || options.name.plural
}`;
}, },
}, },
}, },
resourcer: { resourcer: {
prefix, prefix: `/api/examples/${slug}/${name}`,
}, },
}; };
console.log(options);
const app = new Application(options); const app = new Application(options);
if (name) {
apps.set(name, app);
}
app.resource({ app.resource({
name: 'server', name: 'saas',
actions: { actions: {
async getInfo(ctx, next) { async getInfo(ctx, next) {
ctx.body = name; ctx.body = ctx.db.options;
await next(); await next();
} },
}, },
}); });
app.collection({ app.collection({
@ -57,78 +52,103 @@ function createApp(opts) {
return app; return app;
} }
const app = createApp({ const saas = createApp({
name: 'main', name: 'main',
prefix: `/api/examples/${slug}/main`
}); });
app.collection({ saas['apps'] = new Map<string, Application>();
saas.collection({
name: 'applications', name: 'applications',
fields: [ fields: [
{ type: 'string', name: 'name', unique: true }, { type: 'string', name: 'name', unique: true },
], ],
}); });
app.command('app-create').argument('<appName>').action(async (appName) => { saas
const App = app.db.getModel('applications'); .command('app:create')
const server = await App.create({ .argument('<appName>')
name: appName, .action(async (appName) => {
const App = saas.db.getModel('applications');
const model = await App.create({
name: appName,
});
const app = createApp({
name: appName,
});
await app.db.sync();
await app.destroy();
await saas.destroy();
console.log(model.toJSON());
}); });
const api = createApp({
name: appName,
prefix: `/api/examples/${slug}/${appName}`,
});
await api.db.sync();
await api.destroy();
console.log(server.toJSON());
await app.destroy();
});
app.command('dbsync') saas
.command('db:sync')
.option('-f, --force') .option('-f, --force')
.option('--app [app]') .option('--app [app]')
.action(async (...args) => { .action(async (...args) => {
const cli = args.pop(); const cli = args.pop();
const force = cli.opts()?.force; const force = cli.opts()?.force;
const appName = cli.opts()?.app; const appName = cli.opts()?.app;
console.log('ac ac', cli.opts()); const app = !appName
const api = apps.get(appName) || app; ? saas
await api.load(); : createApp({
await api.db.sync( name: appName,
});
await app.load();
await app.db.sync(
force force
? { ? {
force: true, force: true,
alter: { alter: {
drop: true, drop: true,
}, },
} }
: {}, : {},
); );
await api.destroy(); await app.destroy();
await saas.destroy();
}); });
app.use(async function(ctx, next) { function multiApps({ getAppName }) {
const appName = ctx.path.split('/')[4]; return async function (ctx, next) {
if (appName === 'main') { const appName = getAppName(ctx);
return next(); if (!appName) {
} return next();
const App = ctx.db.getModel('applications'); }
const model = await App.findOne({ const App = ctx.db.getModel('applications');
where: { name: appName }, const model = await App.findOne({
}); where: { name: appName },
console.log({ appName, model })
if (!model) {
return next();
}
if (!apps.has(appName)) {
const app1 = createApp({
name: appName,
prefix: `/api/examples/${slug}/${appName}`
}); });
apps.set(appName, app1); console.log({ appName, model });
} if (!model) {
const server = apps.get(appName); return next();
await compose(server.middleware)(ctx, next); }
}); const apps = ctx.app.apps;
if (!apps.has(appName)) {
const app = createApp({
name: appName,
});
apps.set(appName, app);
}
const saas = apps.get(appName);
await compose(saas.middleware)(ctx, async () => {});
};
}
app.parse(process.argv); saas.use(
multiApps({
getAppName(ctx) {
const appName = ctx.path.split('/')[4];
return appName === 'main' ? null : appName;
},
}),
);
// saas.use(async (ctx, next) => {
// ctx.body = 'aaaaa';
// console.log(ctx.db.options);
// await next();
// });
saas.parse(process.argv);

View File

@ -20,7 +20,34 @@ export interface ApplicationOptions {
dataWrapping?: boolean; dataWrapping?: boolean;
} }
export class Application extends Koa { interface DefaultState {
currentUser?: any;
[key: string]: any;
}
interface DefaultContext {
db: Database;
resourcer: Resourcer;
[key: string]: any;
}
interface MiddlewareOptions {
name?: string;
resourceName?: string;
resourceNames?: string[];
insertBefore?: string;
insertAfter?: string;
}
interface ActionsOptions {
resourceName?: string;
resourceNames?: string[];
}
export class Application<
StateT = DefaultState,
ContextT = DefaultContext
> extends Koa {
public readonly db: Database; public readonly db: Database;
@ -55,7 +82,7 @@ export class Application extends Koa {
}), }),
); );
this.use(async (ctx, next) => { this.use<DefaultState, DefaultContext>(async (ctx, next) => {
ctx.db = this.db; ctx.db = this.db;
ctx.resourcer = this.resourcer; ctx.resourcer = this.resourcer;
await next(); await next();
@ -71,7 +98,7 @@ export class Application extends Koa {
registerActions(this); registerActions(this);
this.cli this.cli
.command('db sync') .command('db:sync')
.option('-f, --force') .option('-f, --force')
.action(async (...args) => { .action(async (...args) => {
console.log('db sync...'); console.log('db sync...');
@ -92,7 +119,7 @@ export class Application extends Koa {
}); });
this.cli this.cli
.command('db init') .command('init')
// .option('-f, --force') // .option('-f, --force')
.action(async (...args) => { .action(async (...args) => {
const cli = args.pop(); const cli = args.pop();
@ -121,6 +148,15 @@ export class Application extends Koa {
}); });
} }
// @ts-ignore
use<NewStateT = {}, NewContextT = {}>(
middleware: Koa.Middleware<StateT & NewStateT, ContextT & NewContextT>,
options?: MiddlewareOptions,
): Application<StateT & NewStateT, ContextT & NewContextT> {
// @ts-ignore
return super.use(middleware);
}
collection(options: TableOptions) { collection(options: TableOptions) {
return this.db.table(options); return this.db.table(options);
} }
@ -129,7 +165,7 @@ export class Application extends Koa {
return this.resourcer.define(options); return this.resourcer.define(options);
} }
actions(handlers: any) { actions(handlers: any, options?: ActionsOptions) {
return this.resourcer.registerActions(handlers); return this.resourcer.registerActions(handlers);
} }