本篇博客主要讲解了 Decorator 的基础知识,以及在实际开发中接口(api)逻辑层中的实践。
前言
Decorator 目前还处于 Stage 2 阶段,也就是草案是规范的第一个版本,与最终标准中包含的特性不会有太大差别,尽管还没正式发布,但是 Decorator 在开发中的使用还是较为普遍的,例如 Redux 中的 @connect、React-Router 中的 @withRouter 等等,本篇博客主要讲解了 Decorator 的基础知识,以及在实际开发中接口(api)逻辑层中的实践。
背景
日常开发中,经常会写很多重复性的代码,比如每次请求发回来,如果请求错误,那么要在 .catch 做一些 toast 的提示,还有一些需要注入到请求体中的监控逻辑,如记录请求时间、成功率,需要向后台发送这些监控日志,这会让函数内部充满各种与请求无关的逻辑,是否有一些办法能够简化代码,对一些固有模式做一些封装呢?
重复代码
这里我们列出一个很简单的例子,相信很多小伙伴在开发中都会写这样的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class UserApi { static setUserName(name) { return setUserName(name) } }
UserApi.setUserName('vince').then(res => { toast('设置成功') }).catch(err => { toast('设置失败,请重试') })
|
看似很平常的代码,如果接口非常多,每个接口函数都需要加入这套 toast 提示逻辑,这就出现了代码中的坏味道:重复代码。
使用装饰器后
这里我们先不讨论 Decotator 的用法,直接看使用后的样子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| function operateToast(target, key, descriptor) { const originFunc = descriptor.value;
descriptor.value = function() { return originFunc.apply(this, arguments).then(res => { toast('操作成功') return res }).catch(err => { toast('操作失败') throw err }) } return descriptor }
class UserApi { @operateToast static setUserName(name) { return setUserName(name) } }
UserApi.setUserName('vince')
|
使用装饰器后,我们只需要在定义方法的时候加上 @xxx 便能无痕地注入 toast 提示逻辑,下面我们将一步步介绍装饰器的用法和更多的实践。
装饰器入门
装饰器能够只能对类、类属性起作用,改写其特性或者执行逻辑,可以讲一些固有的模式注入其中。那为什么不能用在函数身上呢,因为存在函数提升,由于不是本篇文章的重点,可以看阮一峰老师对其的的解释:为什么装饰器不能用于函数?。
准备工作
本篇文章主要讲解装饰器的内容,不赘述如何配置 babel,因此我们直接使用 create-react-app 创建一个前端项目,并且安装装饰器对应的 babel 转译插件。
- 初始化项目
1 2 3
| create-react-app es7-decorator-practice cd es7-decorator-practice npm run eject
|
- 安装插件
1
| npm i @babel/plugin-proposal-decorators -D
|
- 更改 babel 配置
打开 package.json 文件,增加以下 babel 配置:
1 2 3 4 5 6 7 8 9 10 11 12 13
| "babel": { "presets": [ "react-app" ], "plugins": [ [ "@babel/plugin-proposal-decorators", { "legacy": true } ] ] },
|
- 运行项目
配置完成后,我们加入一些装饰器的代码,测试是否能正常运行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| function sayAge(target, name, descriptor) { let sayName = descriptor.value; descriptor.value = function() { sayName.apply(this); console.log('age: 12'); } }
class Cat { name = 'vince' @sayAge sayName() { console.log(this.name) } }
class App extends React.Component { componentDidMount() { let catA = new Cat(); catA.sayName(); } render() { ... } } export default App;
|
运行 npm start ,查看 Console 中,是否输出
什么是装饰器
装饰器是一种与 class (类)相关的语法,用来修改类和类方法、属性的函数,减少书写重复性的代码,使得代码逻辑更易读,避免过多与主逻辑无关的代码注入其中。
类的装饰器
装饰器可以用来修改整个类,例如,有些类是不在维护的,有些类是持续维护的,那么我们可以这样表示:
1 2 3 4 5 6
| @updateable(true) class DesignerApi { ... }
console.log(DesignerApi.isUpdateable)
|
这个装饰器的实现方式也很简单:
1 2 3 4 5
| function updateable(isUpdateable) { return function(target) { target.isUpdateable = isUpdateable; } }
|
这里包两层函数是为了给装饰器增加配置参数,我们可以在 updateable 函数参数添加各种配置,而真正的装饰器逻辑是在内部 return 的函数中,这里的 target 参数指的是 DesignerApi 类本身。
类方法装饰器
对类方法的装饰器就是最上面的例子,我们先来看看该装饰器中的三个参数分别指的是什么:
1 2 3 4 5 6 7 8 9 10 11 12
| function testDecorator(target, name, descriptor) { console.log(target) console.log(name) console.log(descriptor) }
class DesignerApi { @testDecorator getUserDate() { console.log('xxx') } }
|
输出的结果如下图:

理清楚这些问题后,我们将会讲解几个在接口层逻辑中的装饰器方法。
在接口逻辑中的实践
接口层逻辑方法模式都差不多,首先是发送请求,接收请求,判断请求是否成功,分别对其做相应的提示反馈,并且部分接口需要监控其具体行为数据、做一些容错,这时候接口逻辑就需要注入较多与接口无关的代码,显得有些浮肿,我们将针对这些问题,使用装饰器来优化。
接口时间监控
需求:每个接口都需要记录耗时时间,并且需要向后台发送该接口的唯一id和耗时时间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| function logTime(apiId) { return function(target, key, descriptor) { const originFunc = descriptor.value; descriptor.value = function() { const startTime = new Date().valueOf(); return originFunc.apply(this, arguments).then(res => { const endTime = new Date().valueOf(); const spendTime = endTime - startTime; console.log('apiId: ',apiId); console.log('spendTime: ', spendTime) return res; }).catch(err => { throw err }) } return descriptor } }
class UserApi { @logTime('ididid') static setUserName(name) { return setUserName(name) } }
UserApi.setUserName('vince')
|
注意:这里我们使用了一个函数包裹着装饰器函数,这样的目的是为了添加装饰器自定义参数配置。
这样我们就能毫无侵入性地将监控逻辑注入到接口中。
接口 Toast 提醒
需求:在变更操作较多的页面,往往需要写很多post类型接口,操作成功/失败都需要给用户反馈,并且根据接口类型不同,反馈的文案也不同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| function operateToast(successInfo = '操作成功', errorInfo = '操作失败,请重试') { return function (target, key, descriptor) { const originFunc = descriptor.value; descriptor.value = function() { return originFunc.apply(this, arguments).then(res => { toast(successInfo) return res }).catch(err => { toast(errorInfo) throw err }) } return descriptor } }
class UserApi { @operateToast('设置用户名称成功', '后端太垃圾了,设置用户名称接口挂了') static setUserName(name) { return setUserName(name) }
@operateToast('设置用户年龄成功', '后端太垃圾了,接口又挂了') static setUserAge(age) { return setUserAge(age) } }
UserApi.setUserName('vince') UserApi.setUserAge(12)
|
假如把所以的 toast 提示都放入接口逻辑中,将会有一大片冗余的与接口无关的提示逻辑,装饰器完美地给我们解决了这个问题,省时省力代码还清晰易懂。
接口容错发送
需求:在一些不可抗拒的条件下,比如用户网络状况差、接口存在较高的错误率,这时候需要对接口做特殊的处理,假如接口挂了,需要隔 1000ms 再次发送请求,重复发送 3 次,3 次都失败,则接口最终失败。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| function retryFunc(counts, times) { return function(target, name, descriptor) { const originFunc = descriptor.value; descriptor.value = function() { let count = 1; return new Promise((resolve, reject) => { const retry = () => { console.log('开始请求') return originFunc().then(res => { resolve(res); }).catch(() => { count++; if (count > counts) { reject(new Error('多次请求错误,请稍后再试')); return; } console.log(`请求失败,第${count}次重试`) setTimeout(() => { retry(); }, times) }) } retry(); }) } return descriptor } }
class UserApi { @retryFunc(3, 1000) static setUserName(name) { return setUserName(name) } }
UserApi.setUserName('vince')
|
由于情况特殊,该装饰器有些许复杂,不要被多个 return 搞昏了头脑,当然这种逻辑也可以写在接口函数中,但是无法很好的抽离出重试的逻辑。
多个装饰器共用
上述讲解了三个与接口逻辑相关的装饰器,很多情况下,我们需要使用多个装饰器,用法也很简单:
1 2 3 4 5 6 7 8 9
| class UserApi { @logTime('ididid') @operateToast('设置用户名称成功', '后端太垃圾了,设置用户名称接口挂了') @retryFunc(3, 1000) static setUserName(name) { return setUserName(name) }
}
|
不过需要注意的是,在特殊情况下,不同顺序会造成不同的结果,这主要看装饰器的实现过程。
总结
本文没有详细讲解装饰器的每个细节,主要围绕着在接口逻辑层中遇到的场景来讲解具体的实践方案,装饰器能够简化开发流程,将固定模式代码封装到装饰器中,使得接口逻辑更加清晰简洁,当然只是给读者一个案例,还有更多场景需要读者结合自身情况去发掘装饰器的威力。
项目源码
decorator-practice
Refs
阮一峰 ES6标准入门