起因
Router 描述了請求 URL 與 Controller 的對應關係。Eggjs 約定所有的路由都需要在 app/router.js 中申明,目錄結構如下:
┌ app
├── router.js
│ ├── controller
│ │ ├── home.js
│ │ ├── ...
複製程式碼
路由和對應的處理方法分開在 2 個地方維護,開發時經常需要在 router.js
與 Controller
之間來回切換。
前後臺協作時,後端需要為每個 Api 都生成一份對應的 Api 文件給前端。
更優雅的實現
得益於 JavaScript 加入的 decorator 特性,可以使我們跟 Java/C# 一樣,更加直觀自然的做面向切面程式設計:
// 基礎版
@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() { }
複製程式碼
為什麼是這樣的方案
為什麼設計如此複雜的功能,是不是在濫用
Decorator
?
先看看 route
的功能:
- 路由定義
- 引數校驗
- 許可權
Controller
級別中介軟體
router
官方完整定義中包含的功能:路由定義、中介軟體、許可權,及文件中未直接寫的“許可權”:
router.verb('router-name', 'path-match', middleware1, ..., middlewareN, app.controller.action);
複製程式碼
比較下來會發現,只是多了“引數校驗”功能。
引數校驗
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 個問題:
-
引數漏校驗
比如使用者提交的資料為
{ a: 'a', 'b': 'b', c: 'c' }
,如果校驗規則只定義了a
,那麼b
、c
就被漏掉了,並且後續業務中可能會使用這 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 外掛,可以按下面的步驟安裝使用:
-
安裝
egg-controller
tnpm i -S egg-controller 複製程式碼
-
啟用外掛
開啟 config/plugin.js,增加以下配置
aop: { enable: true, package: 'egg-aop', }, controller: { enable: true, package: 'egg-controller', }, 複製程式碼
-
使用外掛
詳細用法參考 egg-controller 文件