lenneth — 基於koa2 的web極簡框架

愛甩尾巴的貓發表於2019-02-28

說明

封裝 lenneth 旨在快速方便的搭建出一個 node web 應用,不過度封裝也不隨波逐流,koa 的 node 是簡單的,lenneth 也是。

基於 ES6+typescript 的一些特性,做了一些類似 spring 的註解,對開發本身不增加複雜度,並且不失 koa 的靈活性。
lenneth 內部已經整合了 koa, koa-bodyparser, koa-router 這 3 個基礎庫,已經滿足了大部分的開發,如果需要引入其他的中介軟體,可以在入口檔案中引入。

lenneth 抽象了 3 個模組,分別是 controller,middleware,service,內部不接入任何 db 和 cache。

安裝

yarn add lenneth
# or
npm install lenneth
複製程式碼

瞄一眼

import { LennethApplication, ServerSettings, ILenneth } from "lenneth";

@ServerSettings({
  port: 8081
})
class App extends LennethApplication implements ILenneth {
  $onMountingMiddlewares() {
    this.use(async (ctx, next) => {
      ctx.body = "hello world";
    });
  }
}

new App().start();
複製程式碼

open in browser

http://localhost:8081
複製程式碼

lenneth 核心

koa 最精髓的就是它的洋蔥模型,而洋蔥模型的元素就是一個一個的中介軟體,lenneth 的封裝就是將普通的類方法轉化成 koa 的中介軟體

  /**
   * koa中介軟體,有兩個引數
   * @params ctx 上下文
   * @params next 洋蔥模型執行下一個中介軟體
   */
  async (ctx, next) => {

  }

  /**
   * 這個類方法並不是koa的中介軟體
   * 按照設計思路,類方法的兩個引數一個是獲取path引數,一個是獲取返回物件,和koa的中介軟體引數不同
   */
  @Get("/detail/:id")
  @UseBefore(UserRuleAuth)
  async getUserDetail(
    @PathVariable("id") id: string,
    @Response() response: TResponse
  ) {
    response.body = this.userService.getUserById(id);
  }
複製程式碼

轉換函式

這個方法就是將上述的類方法轉成 koa 的中介軟體。在類方法的上層封裝了一個 koa 的中介軟體方法,在這個方法內部自動執行類方法,並將這個方法的 this 指向原來的類。

// lenneth封裝koa2的核心
const toAsyncMiddleware = (
  target: Object | any,
  middleware: TApiMiddleware,
  key?: string,
  cb?: (key: string, ctx: IContext, next: TNext) => any[]
) => {
  return async (ctx: IContext, next: TNext) => {
    if (key) {
      // 此處一定要用call來重新設定this指向
      return middleware.call(target, ...cb(key, ctx, next), ctx, next);
    }
    return middleware.call(target, ctx, next);
  };
};
複製程式碼

各個模組

  • application

入口檔案處,使用 ServerSettings 修飾,裡面的引數都是一些全域性方法,如 interceptor,response 等,這些都是一個 middleware,lenneth 只是依照 koa 的洋蔥模型調整了他們的執行順利

@ServerSettings({
  port: 8081,
  // controller
  imports: {
    "/apis": UserController
  },
  // 攔截器
  interceptor: Interceptor,
  // 返回值封裝
  response: AppResponse,
  // error事件捕獲
  globalError: GlobalError
})
class App extends LennethApplication implements ILenneth {
  $onMountingMiddlewares() {
    this.use(logger());
  }
}
複製程式碼
  • interceptor

其實也是一箇中介軟體,只不過在最前執行

import { IMiddleware, Middleware, HeaderParams, Next, TNext } from "lenneth";

@Middleware()
export class Interceptor implements IMiddleware {
  async use(@HeaderParams() header: any, @Next() next: TNext) {
    console.log("Interceptor", header);
    await next();
  }
}
複製程式碼
  • response

中介軟體,在最後執行,預設開啟,可以覆蓋

import { IMiddleware, IContext, TResponse, TNext } from "@interfaces";
import { Middleware, Response, Next } from "@decorators";
import { HttpStatus, ResponseStatus } from "@common";
import { LennethError } from "./Lenneth-error";
@Middleware()
export class LennethResponse implements IMiddleware {
  async use(
    @Response() response: TResponse,
    @Next() next: TNext,
    ctx: IContext
  ) {
    try {
      // 執行前面所有的中介軟體
      await next();
      // 統一處理返回
      if (response.body) {
        return (response.body = {
          code: 0,
          message: ResponseStatus.SUCCESS,
          data: response.body
        });
      }
      return (response.body = { code: 0, message: ResponseStatus.SUCCESS });
    } catch (err) {
      ctx.status = err.code;
      response.status = HttpStatus.OK;
      if (err instanceof LennethError) {
        response.body = {
          code: err.code,
          message: err.message || ResponseStatus.ERROR
        };
      } else {
        response.body = {
          code: err.code || HttpStatus.INTERNAL_SERVER_ERROR,
          message: err.message || ResponseStatus.ERROR
        };
        // 未識別錯誤 拋至最外層error全域性處理
        throw err;
      }
    }
  }
}
複製程式碼
  • controller

controller 主要是設定 router 和注入 services

router 的修飾器有 Post,Get 等,params 引數的獲取同 spring,注入 service 使用修飾器 Autowired,這個也和 spring 一致

import {
  Controller,
  Autowired,
  Post,
  Get,
  RequestBody,
  PathVariable,
  Response,
  TResponse,
  UseBefore,
  Description
} from "lenneth";
import { UserService } from "../services";
import { IUserInfo } from "../interface";
import { UserAuth, RuleAuth } from "../middleware";

@Controller("/user")
export class UserController {
  @Autowired() userService: UserService;

  @Post("/add")
  @Description("新增會員")
  @UseBefore(UserAuth, RuleAuth)
  async addUser(
    @RequestBody() user: IUserInfo,
    @Response() response: TResponse
  ) {
    response.body = this.userService.addUser(user);
  }

  @Get("/detail/:userId")
  @UseBefore(UserAuth)
  @Description("查詢會員")
  async getUser(
    @PathVariable("userId") userId: string,
    @Response() response: TResponse
  ) {
    response.body = this.userService.getUserInfo(userId);
  }
}
複製程式碼
  • middleware

middleware 本質上其實就是 koa 的中介軟體,只不過我在此基礎上又抽象出一層方法來引入獲取 params 的方法,用來方便開發

在 controller 每個 api 上,使用 UseBefore 修飾器即可使用這些 middleware,在執行期,middleware 先於 controller 定義的介面,如果 middleware 沒有呼叫 next 函式,則不會呼叫下一個中介軟體(kao 洋蔥模型)

import { IMiddleware, Middleware, Next, TNext, HeaderParams } from "lenneth";

@Middleware()
export class UserAuth implements IMiddleware {
  async use(@HeaderParams() headers: any, @Next() next: TNext) {
    await next();
  }
}

@Middleware()
export class RuleAuth implements IMiddleware {
  async use(@HeaderParams() headers: any, @Next() next: TNext) {
    await next();
  }
}
複製程式碼
  • service

這個模組只是做一個類輸出方法

export class UserService {
  addUser(userInfo: IUserInfo) {
    return userInfo;
  }

  getUserInfo(id: string) {
    return {
      name: "zhangsan",
      age: 30
    };
  }
}
複製程式碼

單元測試

yarn test
複製程式碼

案例

lenneth-demo

專案地址

lenneth

說在最後

當初做專案的時候,在 github 上搜過一個專案,是基於 express 的–ts-express-decorators,裡面有很多不錯的設計,lenneth 裡的服務啟動生命週期就是照搬其中的。不過我不喜歡把 node 弄得那麼大,那麼全,而且,koa 本身就是一個極簡的應用,所以,lenneth 僅僅只是做了一層封裝,繁簡自然。

相關文章