版本v1.0beta(若有勘誤,此博客會持續更新)

本篇將闡述koa restful的路由部分,數據部分的建立在上一篇說過了的,上一篇也包含了項目的創建和配置。沒有看到可以點擊以下:

koa restful實踐-1-資料庫建立
版本v1.0beta(若有勘誤,此博客會持續更新) 一直想應用typescript開發後端,製作一個開發模板以供之後快速開發上線,這樣自己就可以專注應用開發前端應用,並且得以於typescript的類型系統,使得代碼維護成本不那麼高,不然當時寫的什麼鬼自己都會忘記。 一個馬上能用的後端系統從功能上看,一定是要包括權限系統的。所以這個後端模板包括一個內置的權限和用戶認證系統。 還是要使用orm的,開發成本really matters, 但糾結過到底是sequelize-typescript還是typeorm。割捨不掉sequelzie的很重要的原因是,migrate和seed真的很方便,並…

我喜歡語法糖和抽象語言,作爲獨立開發者,龐大的軟件工程理念其實很拖累精力。減少代碼量和易於維護是最重要的,性能是最大可能性可以犧牲的,若是有客戶需要讓你優化性能,恭喜您,這說明也許你不需要做其他工作了,這是大項目且回報豐厚。

所以在通讀rust語言文檔的時候,我尤其重視元編程和抽象等概念的理解。(順便說下,我看好rust語言未來,認爲它能夠解決生產力的同時也能解決性能和安全,rust教程系列會很快推出)

所以我們的koa路由這樣抽象設計,以配合我們上一篇博客所描述的文件RestService.ts

(優先級自上而下)

url httpMethod status 解釋
/login post 401, 202, 200 登錄
/register post 201,202,200 註冊
/:resource post 201,202,200 創建資源
/:resource get 200,203,204,401,403 獲取資源列表
/:resource delete 201,202,200 刪除多個資源
/:resource put 201,202,200 更新多個資源
/:resource/:id get 200,203,204,401,403 獲取單一資源
/:resource/:id patch 200,202,204,401,403 修改單一資源
/:resource/:id delete 201,202,200 刪除單一資源

儘管我們設計的路由列表不算太長,但是我們也是不想要擠在一起寫的,像這樣:

//不要這樣寫!!!
router.post("/login", loginFunction)
.post("/register", registerFunciton)
.get("/:resource/", listFunction)
.l...

因爲若是我們要維護路由,這個文件會變得很長,且路由方法衆多且要調用的資源也複雜,不易拓展和維護。那麼如何在koa中使用typescript讓功能服務於代碼結構呢?答案是修飾符(decorators)。

先給出代碼結構:

src
├── App.ts
├── controllers
│   ├── AuthController.ts
│   └── RestController.ts
├── decorators
│   └── HttpRoutes.ts
├── entity
│   ├── Role.ts
│   └── User.ts
├── index.ts
├── migration
├── services
│   ├── daos
│   │   └── RestService.ts
│   └── RouteApi.ts
└── test
    └── daos
        └── RestService.spec.ts

我們將會在src/App.ts中構造一個類,這個類有方法start,並且在index.ts調用:

const app = new App();
app.start();

給出src/App.ts代碼:

// src/App.ts
import * as koa from 'koa';
import * as Router from 'koa-router';
import { registerController } from './decorators/HttpRoutes';
import RestController from './controllers/RestController';
import AuthController from './controllers/AuthController';

@registerController([AuthController ,RestController])
export default class App{
    private server:koa<koa.DefaultState, koa.DefaultState>;
    private router: Router<any, {}>;
    constructor(){
        this.server = new koa();
    }
    start(){
        this.server.use(this.router.routes()).use(this.router.allowedMethods());
        console.log(this.router);
        this.server.listen(3000, ()=>{
            console.log('server start at', 3000);
        })
    }
}

暫時沒有給出registerController這個修飾器的實現細節,但是通過上述代碼的語義,讀者大概能夠明白,在構造這個類的時候,一系列koa routes被組織執行好了就等app.server.use它們了。

下面給出兩個import進來的controllers的基礎代碼:

// src/controllers/RestController.ts
import * as koa from 'koa';
import { httpGet, httpPost, httpPut, httpDelete, httpPatch } from '../decorators/HttpRoutes';

export default class RestController{

    @httpGet('/:resource')
    list(ctx: koa.Context){
        ctx.body='get list'
    }

    @httpPost('/:resource')
    create(ctx: koa.Context){
        ctx.body='resource create'
    }

    @httpPut('/:resource')
    updateMany(ctx: koa.Context){
        ctx.body = 'update resources'
    }

    @httpDelete('/:resource')
    deleteMany(ctx: koa.Context){
        ctx.body = 'delete many';
    }

    @httpGet("/:resource/:id")
    getOne(ctx: koa.Context){
        ctx.body = 'get one'
    }

    @httpPatch('/:resource/:id')
    updateOne(ctx: koa.Context){
        ctx.body = 'update one'
    }
    @httpDelete("/:resource/:id")
    deleteOne(ctx: koa.Context){
        ctx.body = 'delete one'
    }
    
}

當然我們也沒有給出上述所有的修飾符的實現代碼,但是我們也可以明白這些代碼的意圖就是爲了實現自帶路由的控制器。

爲了上述代碼的結構可以很好的運行,我們縱慾要給出修飾符的代碼了:

import * as Router from 'koa-router';
const router = new Router();

export function registerController(_Controllers: Array<any>) {
    return function <T extends {new(...args:any[]):{}}>(constructor:T) {
        if(!constructor.prototype.router){
            constructor.prototype.router = router
        }
        for (let index = 0; index < _Controllers.length; index++) {
            const controller = new _Controllers[index]();
            constructor.prototype.router  = controller.router;
        }
    };
}

function httpMehod(url:string, method:string) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        if(!target.router){
            target.router = new Router();
        }
        target.router[method](url, target[propertyKey]);
      
    };
}

export function httpGet(url:string) {
    return httpMehod(url, "get");
}

export function httpPost(url: string){
    return httpMehod(url, 'post');
}

export function httpPatch(url: string){
    return httpMehod(url, "patch");
}

export function httpDelete(url: string){
    return httpMehod(url, 'delete');
}

export function httpPut(url: string){
    return httpMehod(url, 'put')
}

偶們在registerControllers方法中,實例化所有controller類,並且取出其router附在App類的實例屬性router上。在httpMethod方法中我們把修飾的方法所謂route方法附在所在controller的router。

雖然實現的過程比較複雜,但是後續開發的接口就簡單明瞭了。複雜度和性能一樣,需要權衡。把複雜度提早集中起來,是一種很好的架構習慣。

下期預告:

我們將路由和第一篇寫的資料庫方法daos,結合本期所述的koa routes使得http Api切實可以訪問,並且給出http Api的異步測試方法。