Eggjs 的 Controller 最佳實踐

原由

Router 描述了請求 URL 與 Controller 的對應關係。Eggjs 約定全部的路由都須要在 app/router.js 中申明,目錄結構以下:html

┌ app
├── router.js
│  ├── controller
│  │  ├── home.js
│  │  ├── ...
複製代碼

路由和對應的處理方法分開在 2 個地方維護,開發時常常須要在 router.jsController 之間來回切換。前端

先後臺協做時,後端須要爲每一個 Api 都生成一份對應的 Api 文檔給前端。git

更優雅的實現

得益於 JavaScript 加入的 decorator 特性,可使咱們跟 Java/C# 同樣,更加直觀天然的作面向切面編程:github

// 基礎版
@route('/intro')
async intro() { }

// 定義 Method
@route('/intro', { method: 'post' })
async intro() { }

// 增長權限
@route('/intro', { method: 'post', role: xxxRole })
async intro() { }

// Controller 級別中間件
@route('/intro', { method: 'post', role: xxxRole, beforeMiddleware: xxMiddleware })
async intro() { }
複製代碼

爲何是這樣的方案

爲何設計如此複雜的功能,是否是在濫用 Decoratornpm

先看看 route 的功能:編程

  • 路由定義
  • 參數校驗
  • 權限
  • Controller 級別中間件

router 官方完整定義中包含的功能:路由定義、中間件、權限,及文檔中未直接寫的「權限」:後端

router.verb('router-name', 'path-match', middleware1, ..., middlewareN, app.controller.action);
複製代碼

比較下來會發現,只是多了「參數校驗」功能。api

參數校驗

Eggjs 中參數校驗的官方實踐安全

class PostController extends Controller {
    async create() {
        const ctx = this.ctx;
        try {
            // 校驗參數
            // 若是不傳第二個參數會自動校驗 `ctx.request.body`
            ctx.validate(createRule);
        } catch (err) {
            ctx.logger.warn(err.errors);
            ctx.body = { success: false };
            return;
        }
    }
};
複製代碼

在咱們的業務實踐中這個方案會有 2 個問題:bash

  • 參數漏校驗

    好比用戶提交的數據爲 { a: 'a', 'b': 'b', c: 'c' },若是校驗規則只定義了 a,那麼 bc 就被漏掉了,而且後續業務中可能會使用這 2 個值。

  • Eggjs 一個 request 生命週期內,能夠隨時隨地經過 ctx.request 拿到用戶數據

    由於「參數漏校驗」問題的存在,致使後續業務變的不穩定,隨時可能會由於用戶的異常數據致使業務崩潰,或者出現安全問題。

解決方案

爲了解決「參數漏校驗」問題,咱們作了以下約定:

  • Controller 也須要申明入參

    class UserController extends Controller {
        @route('/api/user', { method: 'post' })
        async updateUser(username) {
            // ...
        }
    }
    複製代碼

    上面的例子中,即便用戶提交了海量數據,業務代碼中也只能拿到 username

  • Controller 以外的業務不該該直接訪問 ctx.request 上的數據

    也就是說,當某個 Service 方法依賴用戶數據時,應該經過入參獲取,而不是直接訪問 ctx.request

基於以上約定,分別看看 JS、TypeScript 下咱們如何解決參數校驗問題:

  • JS

    @route('/api/user', {
        method: 'post',
        rule: {
            username: { type: 'string', max: 20 },
        }   
    })
    async updateUser(username) {
        // ...
    }
    複製代碼

    這裏使用了 egg-validate 底層依賴的 parameter 做爲校驗庫

  • TypeScript

    @route('/api/user', {
        method: 'post'
    })
    async updateUser(username: R<{ type: string, max: 20 }>) {
        // ...
    }
    複製代碼

沒看錯,手動調用 ctx.validate(createRule) 並捕獲異常的邏輯確實被咱們省略掉了。「懶惰」是提升生產力的第一要素。參數、規則都有了,爲何還要本身擼代碼呢?

新的先後端協做實踐

傳統的先後端開發協做方式中,後端提供 Api 給前端調用,代碼相似這樣:

function updateUser() {
    request
        .post(`/api/user`, { username })
        .then(ret => {
            
        });
}
複製代碼

前端同窗須要關注路由、參數、返回值。而這些信息 Controller 都已經有了,直接生成前臺 service 用起來是否是更方便呢:

  • Controller 代碼:

    export class UserController {
    
      @route({ url: '/api/user' })
      async getUserInfo(id: number) {
        return { ... };
      }
    }
    複製代碼
  • 生成的 service:

    export class UserService extends Base {
      /** 首頁  */
      async getUserInfo(id: number) {
        const __data = { id };
        return await this.request({
          method: `get`,
          url: `/api/user`,
          data: __data,
        });
      }
    
    }
    
    export const metaService = new UserService();
    export default new UserService();
    複製代碼
  • 前臺使用

    import { userService } from 'service/user';
    
    const userInfo = await userService.getUserInfo(id);
    複製代碼

    對比原來的寫法:

    function updateUser() {
        return new Promise((resolve, reject) => {
            request
            .post(`/api/user`, { username })
            .then(ret => {
                resolve(ret);
            }); 
        });
    }
    複製代碼

    userService.getUserInfo 內部封裝了 request 邏輯,前端不須要在關心調用過程。

如何在本身的項目中使用

咱們已經把最佳實踐抽象爲了 egg-controller 插件,能夠按下面的步驟安裝使用:

  1. 安裝 egg-controller

    tnpm i -S egg-controller
    複製代碼
  2. 啓用插件

    打開 config/plugin.js,增長如下配置

    aop: {
        enable: true,
        package: 'egg-aop',
    },
    controller: {
        enable: true,
        package: 'egg-controller',
    },
    複製代碼
  3. 使用插件

    詳細用法參考 egg-controller 文檔

相關文章
相關標籤/搜索