leibniz v0.4.3
Leibniz
面向 ES2018 的 Node.js 框架
如果要使用此框架,需使用与框架相同的 Babel 配置
特性
使用最新 ES2017/ES2018 的语法特性
基于 Koa2, 兼容 Koa2 全部中间件
约定优于配置
可扩展, 可自定义 loader 或覆盖框架自带 loader
在 sequelize ORM 基础上进行修改. 并与框架进行整合, 简化了 model 定义和书写, 能自动推导 mock 数据方法
依赖注入, 简化书写
例子
你可以参考该项目 onlinejudge-backend 来查看使用该框架后的样子
目录结构约定
config
在该文件夹留下你的配置文件, 推荐使用
config
模块. 框架自带的 sequelizeLoader 会读取该文件夹内的数据库配置, 框架使用了config
模块. 通过config
模块你可以使用任意格式的配置文件.controller
基于装饰器将控制器和路由整合, 路由方面使用了
koa-joi-router
, 对参数进行约束. controller 在应用启动时会通过 ControllerLoader 加载.model
ORM 使用了
sequelize
的最新v4
版本. model 会在应用启动时通过 modelLoader 自动加载, 需要在配置中写入自己数据库信息.service
这部分是业务代码的书写地方, 控制器可以直接通过 ctx.service.xxx 方式调用.
loader
您可以自定义 loader. loader 会按照
priority
从小到大顺序加载. 如果您的 loader 具有和框架提供 loader 相同的priority
, 那么您的 loader 将会覆盖框架的 loader. 一个priority
只能有一个 loader 被调用.index.js
启动文件. 框架重写了 listen 方法, 在调用 listen 时, 框架会进行一系列初始化工作后再调用 koa2 内部的 listen 方法启动项目.
详细使用说明
Controller
在 Controller 中你可以使用以下装饰器:
- Controller(prefix, ...middleware)
- Route(path, method, ...middleware)
- Get(path, ...middleware)
- Post(path, ...middleware)
- Head(path, ...middleware)
- Put(path, ...middleware)
- Patch(path, ...middleware)
- Put(path, ...middleware)
- Delete(path, ...middleware)
- Validate(validation)
@Controller
只能装饰类, 每个 Controller 都需要使用 @Controller
进行装饰, 其中, prefix
将作为后面所有路由的前缀. middleware
将作为后面所有路由的前置中间件.
@Validate
的 validation
接收 Joi
语法对象. 具体用法可以参考例子以及 Joi
的 API 文档.
一个简单的 Controller 如下:
// controller/post.js
import { Post, Controller, Validate, Joi } from 'Leibniz'
@Controller('/post', middleware1, middleware2)
class Post {
@Post('/:id', middleware3, middleware4)
@Validate({
params: {
id: Joi.number().integer()
},
type: 'json',
body: {
title: Joi.string().required(),
author: Joi.string().required()
}
})
async create (ctx) {
const data = await ctx.service.post.save(id, ctx.request.body)
ctx.ok(null, '创建成功') // { success: true, data: null, message: '创建成功' }
}
}
Service
Service 是一个类, 你可以直接在类中定义属性, 而不只只是方法. 类的文件名会作为类实例的名字, 可以通过 ctx.service.fileName
调用. 同时支持多层文件夹嵌套, 遇到文件夹的情况, 可以通过 ctx.service.dirName.fileName
调用, 多层嵌套同理.
一个简单的 Service 如下:
// service/post.js
export default class PostService {
constructor (Post) {
// 所有的 Model 都可以通过依赖注入获取得到, 如果你不想这么做,
// 也可以通过全局变量 Database 来得到该 Model, 此处为 Database.Post
// 你可以注入更多依赖然后在 constructor 中获取得到
this.PostModel = Post
}
async show (id) => {
return await this.PostModel.findById(id) // 与下面同效
// return await Database.Post.findById(id)
}
}
Model
Leibniz 集成了 Sequelize ORM
, 使用了最新的 v4
版本. 你可以像下面这样定义一个 Model:
// model/post.js
class Post {
// model 的配置
static options = {
paranoid: true,
tableName: 'Post' // 默认下, Post 即为这个 Model 对应的表名, 你可以在 options 进行更改
}
// model 表
static fields (DataTypes) {
// 此处 DataTypes 已被魔改
// 同时, 框架会自动推导出默认 random 方法如下注释
// 用户自定义的 random 方法会和自带的一同调用后返回合并的结果
return {
id: DataTypes.uuid().primary(), // UUID.v1()
title: DataTypes.string().allowNull(), // Random.string()
author: DataTypes.string().unique(), // Random.string()
test1: DataTypes.string(55).default('aaa'), // Random.string(55)
test2: DataTypes.integer(), // Random.integer()
test3: DataTypes.float().notNull(), // Random.float()
test4: DataTypes.double(), // Random.float()
test5: DataTypes.string(254), // Random.string(254)
test6: DataTypes.boolean(), // Random.boolean()
test7: DataTypes.integer(4), // Random.integer(1000, 9999)
test8: DataTypes.integer(2) // Random.integer(1000, 9999)
}
}
// mock 数据, model 拥有 mock 方法, 该 mock 方法会使用 random 返回的结果创建 mock 数据
// model 初始化的时候, 框架会为检测 fields 所有键的类型并尝试生成 mock 数据
// 但自带的 mock 数据可能不能满足你的需求, 你可以在此进行扩展
static random (Random) {
return {
title: Random.cword(30),
author: Random.cword(30)
}
}
// 建立表关联, Book 是另一个 model
static associate (Book) {
this.belongsTo(Book)
}
// 非静态方法为实例拥有
get aaaaa () {
return this.title + this.author
}
set aaaaa (val) {
this.title = val
this.author = val
}
}
export default Post
您可以使用 Model
装饰器并传入一些 model 的配置项来替代 options
. 如果 options
提供了同 Model
装饰器相同的配置, Model
装饰器的配置优先级更高.
您可以直接在 random
, fields
, associate
三个方法的参数中任意使用 DataTypes
, Random
和 Model
. 这三个都通过依赖注入获得. 这样做的目的在于减少模块的引入. Random
来自于 mockjs
模块.
你可以不用写 random
方法, 因为 Model 会自动检测 fields 并生成 random
方法. 如果你写了, 那他会对 Model 生成的 mock 数据进行覆盖. 注意, 你不能调用 random
方法, Model 具有静态方法 mock
, 使用该方法可以自动的生成 mock 数据, 他会调用 random
方法生成数据模板然后插入数据库中.
在 associate
中, 你可以直接使用其他 model 的 ModelName 作为函数参数, 这个也是通过依赖注入获取得到.
Loader
框架的核心是由 loader 组成的, 用户可以通过 loader 进一步扩展框架. 例如:
import path from 'path'
import glob from 'glob'
import 'reflect-metadata'
/**
* 用户控制器 Loader
*/
@Reflect.metadata('priority', 30) // 可选
export default class ControllerLoader {
static init (app) { // 通过 app 依赖注入获取得到 app 示例, 也可以输入 ctx 来获取 ctx. 两个只能输入其中一个.
const controllerPath = path.resolve(app.__appname, 'controller')
const controllerFiles = glob.sync(`${controllerPath}/**/*.js`)
controllerFiles.forEach(file => app.use(require(file).default.prototype.router.middleware()))
}
}
您可以在自己项目中的 loader 文件夹上创建多个 loader. 如有需要可以通过 @Reflect.metadata('priority', 30)
来指派优先级. 30 是内部 ControllerLoader 所使用的, 如果你也是用 30, 那么自带的 ControllerLoader 将不再被使用. 你也可以不指定 priority
, 那么他们会在默认的 loader 执行完后执行.
每个 loader 都必须具备静态方法 init
, 您可以在 init
中传入两种参数, 一个是 app
表示 leibniz
实例, 另一个是 ctx
.
框架目前自带的 loader 和对应 priority 如下:
// AppLoader
ConfigLoader(app) 10
SequelizeLoader(app) 20
ModelLoader(app) 30
ControllerLoader(app) 40
// CtxLoader (ctxloader 在 apploader 全部执行完后执行, 他们的优先级是互不干扰的)
ServiceLoader(ctx) 90
index.js
项目入口文件和 Koa2 如出一辙, 没有什么变化. 但注意此处的 listen 方法已经被重写过. 结合上述的例子和该文件, 你可以成功启动这个项目(未测试), 可以看到, 在入口文件, 你不需要做任何的路由, model, 以及 service 的配置, 非常简便.
另外, 在任意地方, 你都可以通过 app.sequelize 或者 Database(全局) 来获取得到数据库.
import Leibniz from 'leibniz'
const app = new Leibniz()
app.listen(8000)
leibniz
导出了以下东西:
- App as default
- Joi
- @Authorization 鉴权装饰器, 检验 JWT
- @Controller 控制器装饰器, 所有控制器必须使用
- @Model Model 装饰器, 配置 options 用, 更推荐通过 static options 的方式来处理
- @Validate Joi 校验装饰器
- @Route @Get @Post @Head @Put @Patch @Delete 路由装饰器
- di 依赖注入, 具有 register, unregister, getParamNames, resolve 四个方法
- requireDirectory from ./util/require-directory 导入文件夹, 接收两个参数, 第一个参数为 glob 形式路径, 第二个参数是标志位, 标志位为 1 表示进行实例化, 默认只引入
test
关于测试, 可以参考该项目 onlinejudge-backend
简单的说, 你只要在 before 的地方 listen 就好. Leibniz 实例的 listen 方法是异步的, 你可以 await 它:
before(async () => {
global.ctx = app.context // Get ctx. then you can test service by ctx.service
const server = await app.listen() // this will do some init work then return koa.listen
global.app = server // get httpserver. then you can request it with supertest to test route
})