# titbit-loader

针对titbit框架的自动加载工具，用于自动创建并加载controller以及middleware的场景。也可以自动加载model。

基于此可实现MVC或类MVC的结构，并可以快速开发接口，生成RESTFul风格的API，路由映射文件等操作。

默认情况，会在当前目录创建controller、middleware、model目录。之后就可以在controller中编写class。

> titbit-loader只是做了应该手动设定路由和安排中间件的部分，把这部分自动化了，在服务运行后，titbit-loader的作用就结束了。

> 此扩展从一开始，不是为了开发单体复杂的软件准备的，只是为了解决在中小规模的应用上，可以方便组织代码结构。但是它现在功能十分强大，在保持健壮性的同时，可以加载的控制器、模型数量没有限制，并且针对开发过程中的问题和需求几乎都提供了解决方案。比如：

- 指定加载哪些控制器。

- 命名!开头的控制器、目录和model不加载，方便保留代码但封禁部分功能。

- 命名 _ 开头的model不加载，这方便你在model目录引入共享模块。

- 路由前缀。

- 自动化编排中间件。

- 根据TEST模式或DEV模式或正式环境决定加载哪些中间件。

- 加载控制后如果存在init函数自动运行并传递指定的参数。

- 加载Model可以根据是否为构造函数进行不同的加载过程。

- 自定义控制器参数。

- controller具备独立性，在内部启用的全局中间件不会扩展到整个应用，因此加载多个controller相互不会影响。

----

## 对应关系

- controller中的目录和文件名称映射为路由。

- model目录中的模块初始化后挂载到app.service上，文件名字即为属性名。

- middleware目录中的中间件扩展在controller目录中通过\_\_mid.js 或 js文件中的 \_\_mid 函数通过配置的方式进行编排。

## 路由规则

默认它按照RESTful规范进行路由的加载过程，比如controller目录中存在admin/account.js文件，那么生成的路由和对应的类方法如下：

```javascript

GET /admin/account/:id 对应于 async get(c) 方法，获取具体的账户信息

GET /admin/account/:id 对应于 async list(c) 方法，获取列表

POST /admin/account 对应于 async post(c) 方法，创建管理员用户

PUT /admin/account/:id 对应于 async put(c) 方法，更新用户信息

DELETE /admin/account/:id 对应于 async delete(c) 方法，删除用户

```

:id参数是account.js类中通过this.param指定的，这部分开发者可以自定义参数。


## POST请求的路由参数

默认在处理路由映射时，POST请求表示创建资源，不会带有参数，如果需要传递参数，需要通过在controller类中使用this.postParam属性指定。

```javascript

class api {

  constructor () {
    this.param = '/:name/:id'
    //给post请求添加参数路由。
    this.postParam = '/:name'
  }

  async get(c) {

  }

  async post (c) {

  }

}

module.exports = api

```



> 初始化选项中的mname用于指定在app.service哪个子对象上挂载model，在v22.0.0以后，默认直接挂载到app.service上。


使用titbit-loader需要先安装titbit框架：

```javaScript
const titbit = require('titbit');
const tbloader = require('titbit-loader');

const app = new titbit({
  debug: true        
});

let tbl = new tbloader();

tbl.init(app);

app.run(2022);

```

controller目录中class示例：

``` JavaScript
//假设存在文件test.js，那么路径就是/test开头。

'use strict';

class test {
  
  constructor () {
    //默认参数是this.param = '/:id'。
    //可以通过设置this.param来指定参数。
    //this.param = '/:name/:key';

    //this.param = '';表示不带参数。
  }

  /*
    对应HTTP请求类型，有同名小写的方法名称处理请求，可以不写，需要哪些请求就写哪些。
    这里只使用了GET、POST、DELETE请求。
  */

  async get(c) {
    c.res.body = 'test ok:' + c.param.id;
  }

  //注意POST请求表示创建资源，默认加载时是不带参数的，也就是发起POST请求对应的路由是/test。
  async post(c) {
    c.res.body = c.body;
  }

  async delete(c) {
    c.res.body = 'delete ok';
  }

}

module.exports = test;

```

## 加载model

默认加载的model的名字就是文件名，没有.js。并且都在app.service.model对象中。但是你可以传递mname选项更改model的名字，或者设置选项directModel为true让model文件直接挂载到app.service上。

**如果模型文件不是一个构造函数，则仅仅把导出的实例返回，否则就会自动进行new操作并传递mdb参数。**

> 目前，titbit-loader不支持ES6模块的导出，请使用exports或module.exports进行导出操作。

**controller中不要写太复杂的业务逻辑，这部分你应该放在model中，对于model，如何封装，是否再分层都可以自定义。titbit-loader只是加载并放在app.service中，仅此而已。**

```javaScript
const titbit = require('titbit');
const tbloader = require('titbit-loader');
const dbconfig = require('./dbconfig');

//postgresql数据库的扩展
const pg = require('pg');

let app = new titbit({
  debug: true        
});


let tbl = new tbloader({
  //默认就是true，默认通过app.service.model可以获取。
  loadModel: true, 
  //设置了mdb，在你的model文件中初始化时会传递此参数。
  mdb: new pg.Pool(dbconfig),
  //设置了mname，则要通过app.service.m获取。
  mname: 'm'
});

tbl.init(app);

app.run(2022);

```

**如果导出模块不是构造函数，比如是一个object或箭头函数，此时就只是返回这个导出结果，但是如果它存在init属性并且是一个函数，则会执行一次init函数，并传递mdb参数。**

## model挂载到app.service

默认情况下，mname选项为null，这表示把初始化的model实例挂载到app.service。若要挂载到app.service的属性上，比如挂载到app.service.model上，则可以通过选项mname指定属性为model。在请求上下文中，可以通过c.service访问，c.service指向app.service。这种依赖注入方式在titbit框架的文档中有说明。

```javaScript

const titbit = require('titbit');
const tbloader = require('titbit-loader');
const dbconfig = require('./dbconfig');

//postgresql数据库的扩展
const pg = require('pg');

let app = new titbit({
  debug: true        
});

let tbl = new tbloader({
  //默认就是true，默认通过app.service.model可以获取。
  loadModel: true, 
  mdb: new pg.Pool(dbconfig),
});

tbl.init(app);

app.run(2022);

```

## 指定主页文件

你应该已经注意到了，因为文件要映射路径，所以，对于主页来说，需要添加的'/'路径是不能在文件名中体现的，所以需要指定一个文件，并添加get方法作为主页。

```javaScript

const titbit = require('titbit')
const tbloader = require('titbit-loader')

let app = new titbit({
  debug: true
})

let tbl = new tbloader({
  //只有GET请求，主页不允许其他请求
  homeFile : 'home.js',

  //如果要指定子目录的文件，则要使用这样的形式
  //homeFile : 'user/home.js'
});

tbl.init(app)

app.run(2022)

```

如果你不想让homeFile起作用，则只需要给一个空字符串，默认homeFile选项就是一个空字符串。

## 指定加载目录

```javaScript
const titbit = require('titbit');
const tbloader = require('titbit-loader');

let app = new titbit({
  debug: true
});

let tbl = new tbloader({
  //相对于程序所在目录，相对路径会自动计算转换为绝对路径。
  //如果指定目录下没有对应目录，会自动创建controller、model、middleware
  appPath : 'app1'
});

tbl.init(app);

app.run(2022);

```

## 加载中间件

middleware目录存放的是中间件模块，但是不会每个都加载，需要你在controller中进行设置，配置文件为__mid.js。注意controller中的__mid.js表示对全局开启中间件，controller中的子目录中存在__mid.js表示只对当前目录分组启用，所见即所得，简洁直观高效。

之所以能够按照分组加载执行，其本质不在于titbit-loader本身，而是titbit提供的中间件分组执行机制。因为titbit提供了路由分组功能，并且可以指定中间件严格匹配请求方法和路由名称，所以基于此开发扩展就变得很方便。

```javaScript
controller/:
    __mid.js    //对全局开启
    
    test.js

    api/:
      __mid.js  //只对/api分组启用
      ...

    user/:
      __mid.js  //只对/user分组启用
      ...

    ...

```

__mid.js示例：

```javaScript
//导出的必须是数组，数组中的顺序就是执行顺序，name是middleware目录中文件的名字，不需要带.js
module.exports = [
  {
    name : 'cors',
    //表示要在接收body数据之前执行
    pre: true
  },
  {
    name : 'apilimit'
  }
];

```

#### 加载中间件类

如果你的中间件模块是需要new操作的，不是一个直接执行的中间件函数，则可以使用@指定，同时要提供一个middleware函数。

```javaScript
module.exports = [
  {
    //@开头表示模块是类，需要初始化，并且要提供middleware方法，
    //这时候加载时会自动初始化并加载middleware函数作为中间件，
    //并且会绑定this，你可以在中间件模块的middleware函数中比较放心的使用this。
    name : '@apilimit'
  }

];

```

#### 直接指定中间件

在 v21.3.0版本开始，可以通过middleware属性直接指定中间件。

``` JavaScript

//文件__mid.js

let mt = async (c, next) => {
  console.log(`mt run ${(new Date()).toLocaleString()}`)
  await next()
}

module.exports = [
  {
    middleware: mt
  }
]

```

## 只加载model并指定model路径

可以通过modelPath设定model所在目录，并通过loadModel加载。

``` JavaScript

const titbit = require('titbit')
const tbloader = require('titbit-loader')

const app = new titbit({
  debug: true
})

let tbl = new tbloader({
  modelPath : 'dbmodel',
  //指定挂载到app.service.dm上，这会创建dm对象并进行挂载。
  mname : 'dm',
})

//只是加载model类。
tbl.loadModel(app)

app.run(1234)

```

## 指定中间件的加载环境

如果要区分开发模式还是发布模式，并根据不同情况加载中间件，可以使用mode属性，这个功能在v21.4.0开始支持。

mode有2个可选的值：test | dev。都表示在对应的开发环境才会加载。没有mode，则会直接加载，不做任何区分。

mode为 online 则表示只有在生产环境才会加载执行，开发测试模式不会加载。

这个属性只是指定了加载条件，而对于条件的检测，是titbit框架的实例的service.TEST 或者 service.DEV属性是否存在并为true。

``` JavaScript

//文件__mid.js 

let mt = async (c, next) => {
  console.log('dev test -- ', c.method, c.path, c.routepath)
  await next()
}

module.exports = [
  {
    name : 'api-log',
    mode : 'test'
  },

  {
    middleware : mt,
    mode : 'dev'
  },

  {
    name : 'api-limit',
    mode : 'online'
  }

]

```

这个功能是具备开发性质的，就是这需要你在titbit服务中，只要设置了以下配置：

``` JavaScript
const app = new titbit()

//相当于app.service.TEST = true
app.addService('TEST', true)

```

这就表示，会开启测试模式（开发模式）。这个时候，不仅titbit-loader会检测并确定是否加载中间件，还可以在请求上下文中知道应用运行在开发模式。


## 高级功能

这部分功能相对要麻烦点，但是可以应对比较复杂的情况。

### 分组的名称

如果通过输出测试可以看到中间件分组，只是比较麻烦，在titbit-loader加载时，采用了非常简单的机制，controller所在目录，即为根分组，名字是'/'。其他都是目录名字作为分组名称，但是都以/开头。

比如以下目录结构：

```
controller/:
  a.js
  ...
  api/:
    user.js
    ...
  admin/:
    user.js
    ...
```

a.js所在分组是/。user.js所在分组是/api，这样，不通过titbit-loader加载的中间件，也可以指定分组，可以对相关分组生效。


### 加载中间件时传递参数

对于中间件是class的情况，有时还需要传递参数，这时候，可以通过__mid.js中的args属性来指定：

```
module.exports = [
  {
    name : '@apilimit',
    args : {
      maxLimit: 100,
      timeout: 56000
    }
  }

]
```

这在初始化apilimit实例时，会传递args参数。


### 只对文件中的某些请求启用中间件

比如，有controller/a.js文件，只对其中的post和put请求启用限制body大小的中间件，则可以在class中提供__mid函数：

``` JavaScript

class a {

  constructor () {

  }

  async get (c) {
    //...
  }

  async post (c) {
    //...
  }

  async put (c) {
    //...
  }

  __mid () {
    return [
      {
        name : 'setMaxBody',
        pre: true,
        //只对post和put函数启用，而且只有请求/a路径时才会生效。
        path : ['post', 'put']
      }
    ]
  }

}

```

### 不导出controller和model

在controller和model目录中的文件，如果不想导出，则可以命名文件开头加上!（英文符号）。这时候会忽略此文件。对于model来说，以!和_开头的文件都不会导出，以_开头的文件可以作为model的公共模块。


### 导出controller中的某些分组

通过subgroup选项可以指定要加载哪些目录下的路由文件，注意这时候若要对controller目录中的文件也加载，要在subgroup数组中包括空字符串或 '/'，比如在controller中存在三个目录和文件：

```
abc/ bcd/ xyz/ a.js
```

如果只想加载xyz 和 a.js则可以这样做：

``` JavaScript

const app = new titbit({
  debug: true
});

let tbl = new tbloader({
  subgroup: ['xyz', '']
});

tbl.init(app);

```

这时候会加载xyz目录中的文件以及a.js。

> 对于大规模应用来说，你最好是进行服务拆分，这个时候，titbit+titbit-loader组成一个服务处理业务，然后再把多个这样的应用组合完成更大规模的处理。

----

----

### mdbMap 指定多个模型关系

比如，你要对接读写分离的数据库服务。可以这样使用：

```javascript
'use strict'

const Titloader = require('titbit-loader')
const Titbit = require('titbit')
const pg = require('pg')

let readorm = new pg.Pool({
  host: '127.0.0.1',
  database: 'read',
  port:5432,
  user: 'pt',
  password: '222werrr'
})

let writeorm = new pg.Pool({
  host: '127.0.0.1',
  database: 'write',
  port: 5432,
  user: 'pt',
  password: '222werrr'
})

const app = new Titbit()

let tbl = new Titloader({
  loadModel: false,
  mdbMap: {
    read: {
      mdb: readorm
    },

    write: {
      mdb: writeorm
    }
  }
})

tbl.init(app)

app.run(1234)

```

mdbMap支持属性path指定不同的model目录，默认和普通model加载配置目录一致。mdb如果不设置则默认为null，不会使用全局mdb的配置。

mdbMap和mdb以及loadModel不冲突，如果不设置loadModel为false，仍然会加载model目录下的模型。


## 一些常量和service函数

在v22.1.2版本开始，加载完成后，会在app.service上添加几个常量：

- **\_\_prepath\_\_** 获取路由的前缀路径。

- **\_\_appdir\_\_** controller、model等目录所在的绝对路径。

- **\_\_model\_\_** 指向对应的模型对象，当mname为空，没有此属性，否则指向app.service[mname]。

- **modelMap(key: string)函数** 获取mdbMap设定的key值指向的Model对象。

- **getModel(name, key='')函数** 获取具体的Model实例，如果key值为空，则会在默认加载的Model上获取，否则会在mdbMap设定的加载关系上获取。


## 完整选项

| 选项 | 说明 | 默认值 |
|----|----|----|
| appPath | 指定要加载的路径 | 默认为调用扩展的文件所在路径。 |
| controllerPath | 指定要加载的控制器目录 | 默认为controller |
| modelPath | 指定要加载的模型目录 | 默认为model |
| midwarePath | 指定中间件所在目录 | 默认为middleware |
| optionsRoute | 是否自动设定OPTIONS路由 | 默认为true |
| prepath | 路由前缀 | 默认为空字符串 |
| initArgs | controller中初始化类要传递的参数 | 默认为null，表示不传递。 |
| homeFile | 首页的文件 | 默认为空字符串 |
| modelNamePre | 模型挂载时，名字的前缀。 | 默认为空字符串 |
| mname | 模型所在的app.service上的属性名字 | 默认为空，表示直接挂载到app.service上。 |
| multi | 是否允许多次加载 | 默认为false |
| mdbMap | model映射关系 | 默认为null, 如果需要指定不同model的多个服务，需要使用此选项。比如，读写分离的两个数据库服务。 |
| fileAsGroup | 以文件作为路由分组 | 从v22.3.0开始默认为true，设置为false回到之前的模式。|

