很多面物件語言中都有裝飾器(Decorator)函式的概念,Javascript語言的ES7標準中也提及了Decorator,個人認為裝飾器是和async/await
一樣讓人興奮的的變化。正如其“裝飾器”的叫法所表達的,他可以對一些物件進行裝飾包裝然後返回一個被包裝過的物件,可以裝飾的物件包括:類,屬性,方法等。
Node.js目前已經支援了async/await
語法,但decorator
還需要babel的外掛支援,具體的配置不在敘述。(截至發稿時間2018-12-29)
下面是引用的關於decorator
語法的一個示例:
@testable
class Person {
@readonly
@nonenumerable
name() { return `${this.first} ${this.last}` }
}
複製程式碼
從上面程式碼中,我們一眼就能看出,Person
類是可測試的,而name
方法是隻讀和不可列舉的。
關於 Decorator 的詳細介紹參見下面兩篇文章:
期望效果
關於Node.js中的路由,大家應該都很熟悉了,無論是在自己寫的http/https
服務中,還是在Express
、Koa
等框架中。我們要為路由提供請求的URL
和其他需要的GET
及POST
等引數,隨後路由需要根據這些資料來執行相應的程式碼。
關於Decorator和路由的結合我們這次希望寫出類似下面的程式碼:
@Controller('/tags')
export default class TagRouter {
@Get(':/id')
@Login
@admin(['developer', 'adminWebsite'])
@require(['phone', 'password'])
@Log
async getTagDetail(ctx, next) {
//...
}
}
複製程式碼
關於這段程式碼的解釋: 第一行,通過
Controller
裝飾TagRouter
類,為類下的路由函式新增統一路徑字首/tags
。 第二行,建立並匯出TagRouter
類。 第三行,通過裝飾器為getTagDetail
方法新增路徑和請求方法。 第四行,通過裝飾器限制發起請求需要使用者登入。 第五行,通過裝飾器限制發起請求的使用者必須擁有開發者或者網站管理員許可權。 第六行,通過裝飾器檢查請求引數必須包含phone
和password
欄位。 第七行,通過裝飾器為請求列印log。 第八行,路由真正執行的方法。
這樣不僅簡化、規範化了路由的寫法,減少了程式碼的冗餘和錯誤,還使程式碼含義一目瞭然,無需註釋也能通俗易懂,便於維護、交接等事宜。
##具體實現
下面就著手寫一個關於movies
的路由具體例項,示例採用koa2
+ koa-router
為基礎組織程式碼。
檔案路徑:/server/routers/movies.js
import mongoose from 'mongoose';
import { Controller, Get, Log } from '../decorator/router';
import { getAllMovies, getSingleMovie, getRelativeMovies } from '../service/movie';
@Controller('/movies')
export default class MovieRouter {
@Get('/all')
@Log
async getMovieList(ctx, next) {
const type = ctx.query.type;
const year = ctx.query.year;
const movies = await getAllMovies(type, year);
ctx.body = {
data: movies,
success: true,
};
}
@Get('/detail/:id')
@Log
async getMovieDetail(ctx, next) {
const id = ctx.params.id;
const movie = await getSingleMovie(id);
const relativeMovies = await getRelativeMovies(movie);
ctx.body = {
data: {
movie,
relativeMovies,
},
success: true,
}
}
}
複製程式碼
程式碼中Controller
為路由新增統一字首,Get
指定請求方法和路徑,Log
列印日誌,參考上面的預期示例。
關於
mongodb
以及獲取資料的程式碼這裡就不貼出了,畢竟只是示例而已,大家可以根據自己的資源,自行修改為自己的邏輯。
重點我們看一下,GET /movies/all
以及GET /movies//detail/:id
這兩個路由的裝飾器實現。
檔案路徑:/server/decorator/router.js
import KoaRouter from 'koa-router';
import { resolve } from 'path';
import glob from 'glob'; // 使用shell模式匹配檔案
export class Route {
constructor(app, routesPath) {
this.app = app;
this.router = new KoaRouter();
this.routesPath = routesPath;
}
init = () => {
const {app, router, routesPath} = this;
glob.sync(resolve(routesPath, './*.js')).forEach(require);
// 具體處理邏輯
app.use(router.routes());
app.use(router.allowedMethods());
}
};
複製程式碼
- 首先,匯出一個
Route
類,提供給外部使用,Route
類的建構函式接收兩個引數app
和routesPath
,app
即為koa2
例項,routesPath
為路由檔案路徑,如上面movies.js
的routesPath
為/server/routers/
。 - 然後,提供一個初始化函式
init
,引用所有routesPath
下的路由,並use
路由例項。
這樣的話我們就可以在外部這樣呼叫Route類:
import {Route} from '../decorator/router';
import {resolve} from 'path';
export const router = (app) => {
const routesPath = resolve(__dirname, '../routes');
const instance = new Route(app, routesPath);
instance.init();
}
複製程式碼
好了,基本框架搭好了,來看具體邏輯的實現。
先補充完init方法:
檔案路徑:/server/decorator/router.js
const pathPrefix = Symbol('pathPrefix');
init = () => {
const {app, router, routesPath} = this;
glob.sync(resolve(routesPath, './*.js')).forEach(require);
R.forEach( // R為'ramda'方法庫,類似'lodash'
({target, method, path, callback}) => {
const prefix = resolvePath(target[pathPrefix]);
router[method](prefix + path, ...callback);
}
)(routeMap)
app.use(router.routes());
app.use(router.allowedMethods());
}
複製程式碼
為了載入路由,需要一個路由列表routeMap
,然後遍歷routeMap
,掛載路由,init
工作就完成了。
下邊的重點就是向routeMap
中塞入資料,這裡每個路由物件採用object
的形式有四個key
,分別為target
, method
, path
, callback
。
target
即為裝飾器函式的target
(這裡主要為了獲取路由路徑的字首)method
為請求方法path
為請求路徑callback
為請求執行的函式。
下邊是設定路由路徑字首和塞入routeMap
內容的裝飾器函式:
export const Controller = path => (target, key, descriptor) => {
target.prototype[pathPrefix] = path;
return descriptor;
}
export const setRouter = method => path => (target, key, descriptor) => {
routeMap.push({
target,
method,
path: resolvePath(path),
callback: changeToArr(target[key]),
});
return descriptor;
}
複製程式碼
-
Controller
就不多說了,就是掛載字首路徑到類的原型物件上,這裡需要注意的是Controller
作用於類,所以target
是被修飾的類本身。 -
setRouter
函式也很簡單,把接受到的引數path
做格式化處理,把callback
函式包裝成陣列,之後與target
、method
一起構造成物件塞入routeMap
。
這裡有兩個輔助函式,簡單貼下程式碼看下:
import R from 'ramda'; // 類似'lodash'的方法庫
// 如果路徑是以/開頭直接返回,否則補充/後返回
const resolvePath = R.unless(
R.startsWith('/'),
R.curryN(2, R.concat)('/'),
);
// 如果引數是函式直接返回,否則包裝成陣列返回
const changeToArr = R.unless(
R.is(Array),
R.of,
);
複製程式碼
接下來是get
、post
、put
、delete
方法的具體實現,其實就是呼叫setRouter
就行了:
export const Get = setRouter('get');
export const Post = setRouter('post');
export const Put = setRouter('put');
export const Delete = setRouter('delete');
複製程式碼
至此,主要的功能就全部實現了,接下來是一些輔助Decorator,大家可以參考和使用core-decorators.js,它是一個第三方模組,提供了幾個常見的修飾器,通過它也可以更好地理解修飾器。
下面以Log
為示例,實現一個輔助Decorator,其他Decorator大家自己發揮:
let logTimes = 0;
export const convert = middleware => (target, key, descriptor) => {
target[key] = R.compose(
R.concat(
changeToArr(middleware)
),
changeToArr,
)(target[key]);
return descriptor;
}
export const Log = convert(async (ctx, next) => {
logTimes++;
console.time(`${logTimes}: ${ctx.method} - ${ctx.url}`);
await next();
console.timeEnd(`${logTimes}: ${ctx.method} - ${ctx.url}`);
})
複製程式碼
convert
是一個輔助函式,首先把普通函式轉換成陣列,然後跟其他中介軟體函式合併。此輔助函式也可用於其他輔助Decorator。
好了,到此文章就結束了,大家多交流,本人github
下一篇:分享koa2原始碼解讀
最後貼出關鍵的/server/decorator/router.js的完整程式碼
import R from 'ramda';
import KoaRouter from 'koa-router';
import glob from 'glob';
import {resolve} from 'path';
const pathPrefix = Symbol('pathPrefix')
const routeMap = [];
let logTimes = 0;
const resolvePath = R.unless(
R.startsWith('/'),
R.curryN(2, R.concat)('/'),
);
const changeToArr = R.unless(
R.is(Array),
R.of,
);
export class Route {
constructor(app, routesPath) {
this.app = app;
this.router = new KoaRouter();
this.routesPath = routesPath;
}
init = () => {
const {app, router, routesPath} = this;
glob.sync(resolve(routesPath, './*.js')).forEach(require);
R.forEach(
({target, method, path, callback}) => {
const prefix = resolvePath(target[pathPrefix]);
router[method](prefix + path, ...callback);
}
)(routeMap)
app.use(router.routes());
app.use(router.allowedMethods());
}
};
export const Controller = path => (target, key, descriptor) => {
console.log(target);
target.prototype[pathPrefix] = path;
return descriptor;
}
export const setRouter = method => path => (target, key, descriptor) => {
console.log('setRouter');
routeMap.push({
target,
method,
path: resolvePath(path),
callback: changeToArr(target[key]),
});
return descriptor;
}
export const Get = setRouter('get');
export const Post = setRouter('post');
export const Put = setRouter('put');
export const Delete = setRouter('delete');
export const convert = middleware => (target, key, descriptor) => {
target[key] = R.compose(
R.concat(
changeToArr(middleware)
),
changeToArr,
)(target[key]);
return descriptor;
}
export const Log = convert(async (ctx, next) => {
logTimes++;
console.time(`${logTimes}: ${ctx.method} - ${ctx.url}`);
await next();
console.timeEnd(`${logTimes}: ${ctx.method} - ${ctx.url}`);
})
複製程式碼