Router 描述了請求 URL 與 Controller 的對應關係。Eggjs 約定全部的路由都須要在 app/router.js 中申明,目錄結構以下:html
┌ app
├── router.js
│ ├── controller
│ │ ├── home.js
│ │ ├── ...
複製代碼
路由和對應的處理方法分開在 2 個地方維護,開發時常常須要在 router.js
與 Controller
之間來回切換。前端
先後臺協做時,後端須要爲每一個 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() { }
複製代碼
爲何設計如此複雜的功能,是否是在濫用
Decorator
?npm
先看看 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
,那麼 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 文檔