若是你感興趣,能夠fork項目,本身體驗一下
Koa-spring: https://github.com/closertb/k...
related-client: https://github.com/closertb/k...
在轉型前端前,我是一個Java練習生(Servlet,SSH,SpringMvc都只會照着寫),嗯,真的是練習生。幾年後,又走上了接口開發的老路,雖然這不是本身第一次用Node(先前,去淌過SSR的水:初探SSR,React + Koa + Dva-Core),但寫接口服務,這仍然是黃花閨女上花轎:頭一回。雖然看過,聽過不少大佬將Node運用(BFF,SSR)到業務,延伸大前端的業務覆蓋範圍,但本身仍是對界限,Node承擔的角色有不少疑惑,爲此,還去脈脈上發了個動態,指望大佬指點迷津。但本身的路,真的只有本身知道那個路口是出口。
最後鑑於這是一個測試用的內部系統,就肯定前端頁面接口所有直接對接數據庫;登陸,權限,日誌做爲中間層對接公司的公共服務。肯定完邊界後,開始糾結框架選型。雖然本身私下都是用Koa,但感受離實際運用到業務,仍是缺乏必定的便捷性。後面又接觸到EggJs,Nest,routing-controllers。EggJS是阿里內部的專用Node框架,成熟天然不言而喻,但對我來講,框架過重,但裏面不少思想是值得借鑑的。NestJs和本身指望的很近,風格和SpringMvc很是類似,官方文檔看似也比較全,但同時製造了不少概念,和Egg同樣,過重,也許沒選它也和只支持Express有關吧。routing-controllers給人的感受就剛恰好,SpringMvc的開發風格、Koa的中間件機制,自由發揮,一見傾心的感受。前端
import {Controller, Param, Body, Get, Post, Put, Delete} from "routing-controllers"; // 路由相較於示例,有點小改動 @Controller('/user') export class UserController { @Get("/query") getAll() { return "This action returns all users"; } @Get("/query/:id") getOne(@Param("id") id: number) { return "This action returns user #" + id; } @Post("/save") post(@Body() user: any) { return "Saving user..."; } @Put("/update/:id") put(@Param("id") id: number, @Body() user: any) { return "Updating a user..."; } @Delete("/delete/:id") remove(@Param("id") id: number) { return "Removing user..."; } }
routing-controllers是一個相對於Egg和Nest較小衆的庫。 git
迭代較慢,三年時間纔到0.8.0的版本,沒有官網,只有Readme。但這些絲絕不掩蓋其易擴展的品質,routing-controllers的引入,未改變Koa的洋蔥模型中間件機制和錯誤捕獲機制,結合Typedi,也能作到Nest框架的效果。下圖是本身使用後整理的routing-controllers中間件機制。github
全局中間件和路由局部中間件,我以爲設計是十分巧妙的,這對解決通用問題,是及其有效的,在後面的中間件一節會具體分析。官方提供的Demo,也能夠下載運行一下試試。spring
頁面接口直接對接數據庫,因此但願選擇一個相似JPA,Hibernate這樣的ORM框架,簡化Sql操做,可選項很少,也沒作多少對比,最後選擇了Sequelize,結合sequelize-typescript,也收穫了一個不錯的開發體驗,下面的代碼就是一個日誌模型的聲明:typescript
import { Table, Column, Model } from 'sequelize-typescript'; import { toTimeStamp } from '../../config/common'; @Table({ tableName: 'change_logs' }) export default class ChangeLog extends Model<ChangeLog> { @Column userId: string; // 用戶Id @Column update_type: string; // 更新表 @Column update_id: number; // 更新表的Id @Column before!: string; // 字段更新前 @Column after!: string; // 字段更新後 @Column get update_time(): number { // 更新時間,轉時間戳 return toTimeStamp(this, 'update_time'); } }
下面一段代碼就是Sequelize的基本CUR操做,看起也是十分便捷的,這裏出現了幾個自定義的裝飾器,在後面會專門講到:數據庫
export default class Repository { private model = Model; @validWithPagination findAll(body: object = {}) { // 列表查詢 return this.model.findAll({ where: body }); } findOne(id: number) { // 詳情查詢 return this.model.findOne({ where: { id } }); } @validBody update(body: AnyObject) { // 更新 const { id, ...others } = body; return this.model.update({ ...others, }, { where: { id } }); } save(body: Model) { // 新增 return body.save(); } }
Sequelize帶給我惟一的困惑就是,其默認返回的響應體,是一個被他的Model類封裝過的數據集,提及來有點抽象,看下面的響應實例: 後端
指望響應體api
{ create_time: 1575642055000, update_time: 1576380905000, id: 5, scene_code: 'special', param_code: 'bit', param_name: '任何', param_type: 'string', operator_add: 'SYS', is_delete: 0 }
實際響應體:太長,截取部分跨域
// Rule { dataValues: { id: 5, scene_code: 'special', param_code: 'bit', param_name: '任何', param_type: 'string', operator_add: 'SYS', is_delete: 0, create_time: 2019-12-06T14:20:55.000Z, update_time: 2019-12-15T03:35:05.000Z }, _modelOptions: { timestamps: false, validate: {}, freezeTableName: true, underscored: false, ... } ... }
看起只須要拿響應體的dataValues就是咱們指望的響應體,但這個響應體是相關getter屬性方法並無執行。官方也提供了{ query: { raw: true }}這個設置去得到簡單的響應體,但也有一樣的問題,getter屬性未執行。看了一下官方實現,getter方法是在調用toJson方法時,纔會執行(迷惑不解臉)。promise
在實現登陸,權限,日誌,存儲做爲中間層對接公司的公共服務時,Node須要發起請求,並響應包裝轉發出去,這裏選擇了比較成熟的request和request-promise庫。
雖然這是一個內部系統,除了前端提交作校驗外,業務方仍是但願接口層要有一些必要的校驗。若是所有用If-else寫,想一想這仍是一個比較大的工做量的,不過還好,有class-validator這個庫的存在,加上裝飾器的寫法,仍是比較簡潔。好比下面這個登陸表單的校驗示例:
import { MinLength, Length } from "class-validator"; export default class User { @Length(6, 12) name: string; @MinLength(6) pwd: string; }
看上面那麼多,你應該猜到了,這個項目選擇了Typescript。
在個人項目中涉及到多箇中間件,既有全局中間件,好比鑑權,響應體包裝,錯誤處理;又有局部路由中間件,好比操做日誌,分頁。
routing-controllers提供了鑑權認證機制,但操做起來不方便,須要每一個路由去添加標誌。因此本身實現了鑑權中間件,全局中間件都繼承於KoaMiddlewareInterface,須要區分是路由響應前,仍是響應後。鑑權中間件的目的是驗證每個請求,是否有操做權限,驗證token的有效性。這裏的實現是一種簡易的形式,只檢查了本地緩存信息,未到用戶中心繼續驗證,供參考:
import { Middleware, KoaMiddlewareInterface } from "routing-controllers"; import * as cache from 'memory-cache'; @Middleware({ type: "before" }) // before 表示在請求路由響應前 export default class AuthCheckMiddleWare implements KoaMiddlewareInterface { async use(ctx: any, next: any): Promise<any> { const { request: { body = {}, query = {}, path } } = ctx; const { uid, token } = Object.assign({}, query, body); // 在用戶登陸時,會以Uid存儲當前用戶的信息,有效期20分鐘 const user = cache.get(uid); // 若是是非登陸,檢查攜帶的token是否和緩存的token一致 if(path === '/user/login' || (user && user.token === token)) { if (path !== '/user/login') { ctx.user = user; // 將user信息掛載到當前請求體 } await next(); } else { ctx.body = { code: '120001', message: uid ? 'Session過時,請從新登陸' : '請先登陸', status: 'fail' }; } } }
全局中間件須要在生成koa實例時,進行註冊:
const koaApp = createKoaServer({ cors: true, // 這裏開啓了Cors跨域 controllers: [__dirname + '/services/*/controller.js'], middlewares: [AuthCheckMiddleWare], });
操做日誌中間件,其目的是記錄某些表的數據新增,修改操做。須要記錄下字段修改前和修改的值,操做類型及操做人。若是按常規思惟,在每個須要記錄操做的路由Controller去加入日誌記錄代碼。代碼冗餘,且日誌記錄需求變更時,是一件很是被動的事情,因此局部路由中間件是最好的實現方式,在須要記錄的路由加入這個中間件便可。
import Model from '../services/changeLog/model'; import { AnyObject } from '../config/interface'; /** * 新增修改操做日誌記錄,入庫。 * @param ctx * @param next */ export default async function RecordMiddleWare(ctx: any, next: (err?: any) => Promise<any>): Promise<any> { const { user = {}, body: { before, after, update_type, id } } = ctx; const old: AnyObject = {}; const nw: AnyObject = {}; // 最新數據 if (!before) { Object.assign(nw, after); } else { // 記錄比較,只保存改變過的值的修改記錄 Object.keys(after).forEach((prop) => { // 數字比較時,因爲請求體,數字會被轉化成字符串,因此這裏用了==,來自動轉換數據類型 if (before[prop] == after[prop]) { return; } old[prop] = before[prop]; nw[prop] = after[prop]; }); } // 重寫body ctx.body = { msg: 'success', id }; await next(); const repository = new Model({ update_id: id, update_type, userId: user.id || 'SYS', // 獲取userId after: JSON.stringify(nw), before: JSON.stringify(old) }); repository.save() }
在規則數據更新時,加入操做日誌記錄中間件
import { JsonController, Post, Body, UseAfter } from "routing-controllers"; import { Service } from "typedi"; import RecordMiddleWare from '../../middlewares/RecordMiddleWare'; import RuleRepository from "./repository"; import { AnyObject } from '../../config/interface'; @Service() @JsonController('/rule') export default class RuleController { @Post("/update") @UseAfter(RecordMiddleWare) async update(@Body() body: AnyObject) { const { id } = body; const before = await this.ruleRepository.findOne(id); await this.ruleRepository.update(body); return { before, after: body, id, update_type: 'rule' }; } }
這一篇主要講了koa-spring的一些庫應用及項目實現方式,這裏不得不強力推廣routing-controllers與sequelize-typescript這兩個庫,Thanks to @RobinBuschmann for answering my issue so patient(maybe you can't understand what i write or say, just accept my thanks)。感嘆一句,寫Demo和實際應用到業務真的是天差地別,在下一篇,將會談一些深刻的優化和疑難點解決,主要關於:
提早預告: Koa-spring:後端太忙,讓我本身寫服務(下)