typescript中的裝飾器有很多種,比如類裝飾器、方法裝飾器、屬性裝飾器等等,先看看裝飾器的定義吧,下面以類裝飾器和方法裝飾器為例子,順便說幾個點
類裝飾器
// 裝飾器都是以@符加在類或方法上面
@tsetDecorator
class Test {
name = '超人鴨'
}
// 裝飾器都是函數
function tsetDecorator(target: any) {
console.log('裝飾器')
}
現在我是沒有創建這個類的實例的,只是聲明,當我執行這個文件時就會打印出'裝飾器'
從這個最簡單的例子說幾個點
- 類的裝飾器有一個參數,為類的構造函數,通過這個參數可以改變類上的屬性方法等
- 類的裝飾器會在類定義后執行,不需要類實例化
再看一個簡單的例子:
@tsetDecorator
class Test {
}
function tsetDecorator(target: any) {
target.prototype.name = '吳彥祖'
}
console.log(Test.prototype) // {name:'吳彥祖'}
裝飾器的工廠模式
裝飾器是不可以直接傳遞參數的,但是之前看到vue里面用到裝飾器的時候,好像都是可以傳遞參數的,比如prop裝飾器:
@Prop(Boolean)
private visible: boolean | undefined
其實都是用到工廠模式,以上面設置name的例子,改寫一下:
@tsetDecorator('吳彥祖')
class Test {
}
function tsetDecorator(name: string) {
return function (target: any) {
target.prototype.name = name
}
}
console.log(Test.prototype) // {name:'吳彥祖'}
方法裝飾器
直接上例子:
class Test {
name = '超人鴨'
@tsetDecorator
getName() {
return this.name
}
}
function tsetDecorator(target: any, key: string, descriptor: PropertyDescriptor) {
descriptor.value = function () {
return '吳彥祖'
}
descriptor.writable = false
}
const test = new Test()
console.log(test.getName()) // 吳彥祖
test.getName = () => { // 報錯
return '111'
}
結合上面的例子講一下方法裝飾器涉及的點
- 和類裝飾器一樣,在類定義時方法裝飾器就會執行
- 方法裝飾器的三個參數,普通方法裝飾器的第一個參數為類的prototype,靜態方法裝飾器的第一個參數為類的構造函數;第二個參數為方法的名稱;第三個參數類似于object.defineproperty,可以對方法自身作某些修改,像上面例子一樣,可以改變方法是否可改,以及方法本身。
- 當一個類有類裝飾器和方法裝飾器同時存在,執行的順序是先執行方法裝飾器再執行類裝飾器
上面所說的就是裝飾器一些基本的概念,下面再介紹一個庫,通過這個庫配合裝飾器來管理koa的接口
reflect-metadata
這個庫可以往類或方法上添加元數據,這個數據簡單來說就是存在的,但是直接拿是拿不到的,得通過它的api存數據與取數據,先別懵逼,看到最后配合裝飾器的使用就知道這個庫的用處了。
import 'reflect-metadata'
class Test {
getName() {
}
}
// 存數據
Reflect.defineMetadata('data', 'test', Test)
Reflect.defineMetadata('data', 'test', Test, 'getName')
// 取數據
console.log(Reflect.getMetadata('data', Test)) // test
console.log(Reflect.getMetadata('data', Test, 'getName')) // test
- 用這個庫直接import就可以了
- 存數據,通過Reflect.defineMetadata存儲,第一個參數為存的數據的key,第二個參數為數據的值,第三個參數為存到哪個對象上(類),如果是存在方法上,那就有第四個參數,為存放的方法的方法名稱。
- 取數據,通過Reflect.getMetadata取數據,第一個參數為數據的key,第二個參數為存放的對象(類),如果是存在方法上,那第三個參數為存放的方法的方法名稱。
這就是reflect-metadata的基礎使用方法,接下來結合裝飾器對koa接口進行管理,先看看普通寫koa接口的寫法:
import Router from 'koa-router'
const router = new Router()
router.get('/', async (ctx) => {
ctx.body = '訪問根路徑'
})
router.get('/testGet', async (ctx) => {
ctx.body = '這個是get請求'
})
router.post('/testPost', async (ctx) => {
ctx.body = '這個是post請求'
})
export default router
正常寫koa接口時,都是按模塊分類然后加上路由前綴,比如用戶模塊、訂單模塊等,像我上面的例子,三個接口當成是一個模塊,那我們就可以把一個模塊寫成一個類來管理,先看改寫后的類:
import Application from 'koa' // koa-router中的一個類型
@controller
class TestController{
@get('/')
async home(ctx: Application.ParameterizedContext<any, Router.IRouterParamContext<any, {}>>) {
ctx.body =
`
<html>
<body>
<form method="post" action="/testPost">
<button>post請求</button>
</form>
<form method="get" action="/testGet">
<button>get請求</button>
</form>
</body>
</html>
`
}
@post('/testPost')
async testPost(ctx: Application.ParameterizedContext<any, Router.IRouterParamContext<any, {}>>) {
ctx.body = '這是post請求'
}
@get('/testGet')
async testGet(ctx: Application.ParameterizedContext<any, Router.IRouterParamContext<any, {}>>) {
ctx.body = '這是get請求'
}
}
我把原來三個router接口改寫成類中的三個方法,路徑用裝飾器進行傳入,我的目的就是達到與普通寫法一樣,生成三個router接口,訪問的路徑與請求方法都一致,所以實現都放到裝飾器里面,先看方法裝飾器,也就是get和post方法:
function get(path: string){
// 往方法上存上路徑與請求方法
return function (target: any, key: string) {
Reflect.defineMetadata('path', path, target, key)
Reflect.defineMetadata('method', 'get', target, key)
}
}
function post(path: string){
return function (target: any, key: string) {
Reflect.defineMetadata('path', path, target, key)
Reflect.defineMetadata('method', 'post', target, key)
}
}
// 這兩個方法都是高度相似的,可以再做一層封裝,所以上面兩個方法刪掉,改成下面:
enum Methods { // 定義一個枚舉類型
get = 'get',
post = 'post'
}
function getRequestDecorator(method: Methods) {
return function (path: string) {
return function (target: any, key: string) {
Reflect.defineMetadata('path', path, target, key)
Reflect.defineMetadata('method', method, target, key)
}
}
}
const get = getRequestDecorator(Methods.get)
const post = getRequestDecorator(Methods.post)
這樣就給每個方法都綁定了對應的路徑與請求方法,這里用到了枚舉類型,用它來做類型校驗,這種使用方式非常適合某個變量只能是固定的幾個值得情況,像http請求,就是get、post、put等,不能是其他的。
到這里該綁定的都綁定了,接下來就是生成router的接口,我們在類的裝飾器controller操作,因為方法執行器是先于類執行器,所以在類執行器里面可以操作到剛剛在方法裝飾器中綁定的數據:
function controller(target: any) {
for (let key in target.prototype) { // 遍歷類上的方法
const path: string = Reflect.getMetadata('path', target.prototype, key) // 拿到路徑
const method: Methods = Reflect.getMetadata('method', target.prototype, key) // 獲取請求方法
const hanlder = target.prototype[key] // 獲取方法
if (path && method && hanlder) {
router[method](path, hanlder) // 注冊router接口
}
}
}
到這里router接口就都注冊完成了,下面是完整代碼:
import Router from 'koa-router'
const router = new Router()
import Application from 'koa'
import 'reflect-metadata'
enum Methods {
get = 'get',
post = 'post'
}
function getRequestDecorator(method: Methods) {
return function (path: string) {
return function (target: any, key: string) {
Reflect.defineMetadata('path', path, target, key)
Reflect.defineMetadata('method', method, target, key)
}
}
}
const get = getRequestDecorator(Methods.get)
const post = getRequestDecorator(Methods.post)
function controller(target: any) {
for (let key in target.prototype) {
const path: string = Reflect.getMetadata('path', target.prototype, key)
const method: Methods = Reflect.getMetadata('method', target.prototype, key)
const hanlder = target.prototype[key]
if (path && method && hanlder) {
router[method](path, hanlder)
}
}
}
@controller
class TestController {
@get('/')
async home(ctx: Application.ParameterizedContext<any, Router.IRouterParamContext<any, {}>>) {
ctx.body =
`
<html>
<body>
<form method="post" action="/testPost">
<button>post請求</button>
</form>
<form method="get" action="/testGet">
<button>get請求</button>
</form>
</body>
</html>
`
}
@post('/testPost')
async testPost(ctx: Application.ParameterizedContext<any, Router.IRouterParamContext<any, {}>>) {
ctx.body = '這是post請求'
}
@get('/testGet')
async testGet(ctx: Application.ParameterizedContext<any, Router.IRouterParamContext<any, {}>>) {
ctx.body = '這是get請求'
}
}
export default router
接下來在koa的入口頁,一般叫app.ts中引入這個router,下面是我的app.ts的全部代碼
import Koa from 'koa'
const app = new Koa()
import Router from 'koa-router'
const router = new Router()
import testRouter from './controller/test' // 引入文件,class會自動定義
router.use('', testRouter.routes()) // 第一個參數為接口前綴,這里無前綴
app.use(router.routes())
app.use(router.allowedMethods()) // 允許http請求的所有方法
app.listen(3000, () => {
console.log('服務開啟在三千端口')
})
測試:
無問題,這就是我學了ts裝飾器的小總結demo,如果你有更好的看法見解,歡迎指教哦