原创文章汇总:github/Nealyangcss
正在着手写 THE LAST TIME 系列的 Typescript
篇,而Decorator
一直是我我的看来一个很是不错的切面方案。所谓的切面方案就是咱们常说的切面编程 AOP
。一种编程思想,简单直白的解释就是,一种在运行时,动态的将代码切入到类的指定方法、指定位置上的编程思想就是 AOP。AOP
和咱们熟悉的 OOP
同样**,只是一个编程范式**,AOP
没有说什么规定要使用什么代码协议,必需要用什么方式去实现,这只是一个范式。而 Decorator
也就是AOP
的一种形式。html
而本文重点不在于讨论编程范式,主要介绍 Typescript
+Decorator
下图的一些知识讲解,其中包括最近笔者在写项目的一些应用。前端
貌似在去年的时候在公众号:【全栈前端精选】中,有分享过关于 Decorator
的基本介绍:Decorator 从原理到实战,里面有对Decorator
很是详细的介绍。node
本质上,它也就是个函数的语法糖。git
Decorator
是 ES7
添加的性特性,固然,在 Typescript
很早就有了。早在此以前,就有提出与 Decorator
思想很是相近的设计模式:装饰者模式。github
上图的WeaponAccessory
就是一个Decorator
,他们添加额外的功能到基类上。让其可以知足你的需求。typescript
简单的理解 Decorator
,能够认为它是一种包装,对 对象,方法,属性的包装。就像 Decorator 侠,一身盔甲,只是装饰,以知足需求,未改变是人类的本质。编程
为何要使用 Decorator
,其实就是介绍到 AOP
范式的最大特色了:非侵入式加强。json
好比笔者正在写的一个页面容器,交 PageContainer.tsx
,基本功能包括滚动、autoCell
、事件注入与解绑、placeHolder Container
的添加等基本功能。设计模式
class PageContainer extends Components{
xxx
}
复制代码
这时候我正使用这个容器,想接入微信分享功能。或者错误兜底功能。可是使用这个容器的人很是多。分享不必定都是微信分享、错误兜底不必定都是张着我想要的样子。因此我一定要对容器进行改造和加强。
从功能点划分,这些的确属于容器的能力。因此在无侵入式的加强方案中,装饰者模式是一个很是好的选择。也就是话落到咱们所说的 Decorator
。(对于 React
或者 Rax
,HOC
也是一种很好的方案,固然,其思想是一致的。)
+ @withError
+ @withWxShare
class PageContainer extends Components{
xxx
}
复制代码
咱们添加 Decorator
,这样的作法,对原有代码毫无入侵性,这就是AOP
的好处了,把和主业务无关的事情,放到代码外面去作。
JavaScript
毋庸置疑是一门很是好的语言,可是其也有不少的弊端,其中不乏是做者设计之处留下的一些 “bug”。固然,瑕不掩瑜~
话说回来,JavaScript
毕竟是一门弱类型语言,与强类型语言相比,其最大的编程陋习就是可能会形成咱们类型思惟的缺失(高级词汇,我从极客时间学到的)。而思惟方式决定了编程习惯,编程习惯奠基了编程质量,工程质量划定了能力边界,而学习 Typescript
,最重要的就是咱们类型思惟的重塑。
那么其实,Typescript
在我我的理解,并不能算是一个编程语言,它只是 JavaScript
的一层壳。固然,咱们彻底能够将它做为一门语言去学习。网上有不少推荐 or 不推荐 Typescript
之类的文章这里咱们不作任何讨论,学与不学,用于不用,利与弊。各自拿捏~
再说说 typescript
,其实对于 ts
相比你们已经不陌生了。更多关于 ts
入门文章和文档也是已经烂大街了。此文不去翻译或者搬运各类 api或者教程章节。只是总结罗列和解惑,笔者在学习 ts 过程当中曾疑惑的地方。道不到的地方,欢迎你们评论区积极讨论。
首先推荐下各自 ts 的编译环境:typescriptlang.org
再推荐笔者收藏的两个网站:
interface TypedPropertyDescriptor<T> {
enumerable?: boolean;
configurable?: boolean;
writable?: boolean;
value?: T;
get?: () => T;
set?: (value: T) => void;
}
declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;
declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;
复制代码
如上是 ClassDecorator
、PropertyDecorator
以及 MethodDecorator
的三个类型签名。
因为 Decorator
在 Typescript
中仍是一项实验性的给予支持,因此在 ts
的配置配置文件中,咱们指明编译器对 Decorator
的支持。
在命令行或tsconfig.json
里启用experimentalDecorators
编译器选项:
tsc --target ES5 --experimentalDecorators
复制代码
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
复制代码
在 Typescript
中,Decorator
能够修饰五种语句:类、属性、方法、访问器和方法参数。
类装饰器应用于构造函数之上,会在运行时看成函数被调用,类的构造函数做为其惟一的参数。
注意,在 Typescript
中的class
关键字只是 JavaScript
构造函数的一个语法糖。因为类装饰器的参数是一个构造函数,其也应该返回一个构造函数。
咱们先看一下官网的例子:
function classDecorator<T extends { new (...args: any[]): {} }>(
constructor: T
) {
return class extends constructor {
newProperty = "new property";
hello = "override";
};
}
@classDecorator
class Greeter {
property = "property";
hello: string;
constructor(m: string) {
this.hello = m;
}
}
const greeter: Greeter = new Greeter("world");
console.log({ greeter }, greeter.hello);
复制代码
{ new (...args: any[]): {} }
表示一个构造函数,为了看起来清晰一些,咱们也能够将其声明到外面:
/** *构造函数类型 * * @export * @interface Constructable */
export interface IConstructable {
new (...args:any[]):any
}
复制代码
属性装饰器有两个参数:
descriptor不会作为参数传入属性装饰器,这与TypeScript是如何初始化属性装饰器的有关。 由于目前没有办法在定义一个原型对象的成员时描述一个实例属性,而且没办法监视或修改一个属性的初始化方法。返回值也会被忽略。所以,属性描述符只能用来监视类中是否声明了某个名字的属性。
function setDefaultValue(target: Object, propertyName: string) {
target[propertyName] = "Nealayng";
}
class Person {
@setDefaultValue
name: string;
}
console.log(new Person().name); // 输出: Nealayng
复制代码
将上面的代码修改一下,咱们给静态成员添加一个 Decorator
function setDefaultValue(target: Object, propertyName: string) {
console.log(target === Person);
target[propertyName] = "Nealayng";
}
class Person {
@setDefaultValue
static displayName = 'PersonClass'
name: string;
constructor(name:string){
this.name = name;
}
}
console.log(Person.prototype);
console.log(new Person('全栈前端精选').name); // 输出: 全栈前端精选
console.log(Person.displayName); // 输出: Nealayng
复制代码
以此能够验证,上面咱们说的: Decorator 的第一个参数,对于静态成员来讲是类的构造函数,对于实例成员是类的原型对象
方法装饰器表达式会在运行时看成函数被调用,传入下列3个参数:
descriptor
。注意: 若是代码输出目标版本小于ES5,descriptor将会是undefined。
function log( target: Object, propertyName: string, descriptor: TypedPropertyDescriptor<(...args: any[]) => any> ) {
const method = descriptor.value;
descriptor.value = function(...args: any[]) {
// 将参数转为字符串
const params: string = args.map(a => JSON.stringify(a)).join();
const result = method!.apply(this, args);
// 将结果转为字符串
const resultString: string = JSON.stringify(result);
console.log(`Call:${propertyName}(${params}) => ${resultString}`);
return result;
};
}
class Author {
constructor(private firstName: string, private lastName: string) {}
@log
say(message: string): string {
return `${message} by: ${this.lastName}${this.firstName}`;
}
}
const author:Author = new Author('Yang','Neal');
author.say('《全站前端精选》');//Call:say("全站前端精选") => "全站前端精选 by: NealYang"
复制代码
上述的代码比较简单,也就不作过多解释了。其中须要注意的是属性描述符 descriptor
的类型和许多文章写的类型有些不一样:propertyDescriptor: PropertyDescriptor
。
从官方的声明文件能够看出,descriptor 设置为TypedPropertyDescriptor
加上泛型约束感受更加的严谨一些。
固然,官网也是直接声明为类型PropertyDescriptor
的。这个,仁者见仁。
访问器,不过是类声明中属性的读取访问器和写入访问器。访问器装饰器表达式会在运行时看成函数被调用,传入下列3个参数:
若是代码输出目标版本小于ES5,Property Descriptor将会是undefined。同时 TypeScript 不容许同时装饰一个成员的get和set访问器
function Enumerable( target: any, propertyKey: string, descriptor: PropertyDescriptor ) {
//make the method enumerable
descriptor.enumerable = true;
}
class Person {
_name: string;
constructor(name: string) {
this._name = name;
}
@Enumerable
get name() {
return this._name;
}
}
console.log("-- creating instance --");
let person = new Person("Diana");
console.log("-- looping --");
for (let key in person) {
console.log(key + " = " + person[key]);
}
复制代码
若是上面 get
不添加Enumerable
的话,那么 for in
只能出来_name
_name = Diana
参数装饰器表达式会在运行时看成函数被调用,传入下列3个参数:
参数装饰器只能用来监视一个方法的参数是否被传入。
在下面的示例中,咱们将使用参数装饰器@notNull
来注册目标参数以进行非空验证,可是因为仅在加载期间调用此装饰器(而不是在调用方法时),所以咱们还须要方法装饰器@validate
,它将拦截方法调用并执行所需的验证。
function notNull(target: any, propertyKey: string, parameterIndex: number) {
console.log("param decorator notNull function invoked ");
Validator.registerNotNull(target, propertyKey, parameterIndex);
}
function validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("method decorator validate function invoked ");
let originalMethod = descriptor.value;
//wrapping the original method
descriptor.value = function (...args: any[]) {//wrapper function
if (!Validator.performValidation(target, propertyKey, args)) {
console.log("validation failed, method call aborted: " + propertyKey);
return;
}
let result = originalMethod.apply(this, args);
return result;
}
}
class Validator {
private static notNullValidatorMap: Map<any, Map<string, number[]>> = new Map();
//todo add more validator maps
static registerNotNull(target: any, methodName: string, paramIndex: number): void {
let paramMap: Map<string, number[]> = this.notNullValidatorMap.get(target);
if (!paramMap) {
paramMap = new Map();
this.notNullValidatorMap.set(target, paramMap);
}
let paramIndexes: number[] = paramMap.get(methodName);
if (!paramIndexes) {
paramIndexes = [];
paramMap.set(methodName, paramIndexes);
}
paramIndexes.push(paramIndex);
}
static performValidation(target: any, methodName: string, paramValues: any[]): boolean {
let notNullMethodMap: Map<string, number[]> = this.notNullValidatorMap.get(target);
if (!notNullMethodMap) {
return true;
}
let paramIndexes: number[] = notNullMethodMap.get(methodName);
if (!paramIndexes) {
return true;
}
let hasErrors: boolean = false;
for (const [index, paramValue] of paramValues.entries()) {
if (paramIndexes.indexOf(index) != -1) {
if (!paramValue) {
console.error("method param at index " + index + " cannot be null");
hasErrors = true;
}
}
}
return !hasErrors;
}
}
class Task {
@validate
run(@notNull name: string): void {
console.log("running task, name: " + name);
}
}
console.log("-- creating instance --");
let task: Task = new Task();
console.log("-- calling Task#run(null) --");
task.run(null);
console.log("----------------");
console.log("-- calling Task#run('test') --");
task.run("test");
复制代码
对应的输出位:
param decorator notNull function invoked
method decorator validate function invoked
-- creating instance --
-- calling Task#run(null) --
method param at index 0 cannot be null
validation failed, method call aborted: run
----------------
-- calling Task#run('test') --
running task, name: test
复制代码
@validate
装饰器把run
方法包裹在一个函数里在调用原先的函数前验证函数参数.
装饰器工厂真的也就是一个噱头(造名词)而已,其实也是工厂的概念哈,毕竟官方也是这么号称的。在实际项目开发中,咱们使用的也仍是挺多的
**装饰器工厂就是一个简单的函数,它返回一个表达式,以供装饰器在运行时调用。**其实说白了,就是一个函数 return
一个 Decorator
。很是像 JavaScript
函数柯里化,我的称之为“函数式Decorator”~
import { logClass } from './class-decorator';
import { logMethod } from './method-decorator';
import { logProperty } from './property-decorator';
import { logParameter } from './parameter-decorator';
// 装饰器工厂,根据传入的参数调用相应的装饰器
export function log(...args) {
switch (args.length) {
case 3: // 多是方法装饰器或参数装饰器
// 若是第三个参数是数字,那么它是索引,因此这是参数装饰器
if typeof args[2] === "number") {
return logParameter.apply(this, args);
}
return logMethod.apply(this, args);
case 2: // 属性装饰器
return logProperty.apply(this, args);
case 1: // 类装饰器
return logClass.apply(this, args);
default: // 参数数目不合法
throw new Error('Not a valid decorator');
}
}
@log
class Employee {
@log
private name: string;
constructor(name: string) {
this.name = name;
}
@log
greet(@log message: string): string {
return `${this.name} says: ${message}`;
}
}
复制代码
一个类中,不一样位置申明的装饰器,按照如下规定的顺序应用:
parameterDecorator
)时,从最后一个参数依次向前执行methodDecorator
)和方法参数装饰器(parameterDecorator
)中,参数装饰器先执行classDecorator
)老是最后执行。methodDecorator
)和属性装饰器(propertyDecorator
),谁在前面谁先执行。由于参数属于方法一部分,因此参数会一直牢牢挨着方法执行。function ClassDecorator() {
return function (target) {
console.log("I am class decorator");
}
}
function MethodDecorator() {
return function (target, methodName: string, descriptor: PropertyDescriptor) {
console.log("I am method decorator");
}
}
function Param1Decorator() {
return function (target, methodName: string, paramIndex: number) {
console.log("I am parameter1 decorator");
}
}
function Param2Decorator() {
return function (target, methodName: string, paramIndex: number) {
console.log("I am parameter2 decorator");
}
}
function PropertyDecorator() {
return function (target, propertyName: string) {
console.log("I am property decorator");
}
}
@ClassDecorator()
class Hello {
@PropertyDecorator()
greeting: string;
@MethodDecorator()
greet( @Param1Decorator() p1: string, @Param2Decorator() p2: string) { }
}
复制代码
输出为:
I am parameter2 decorator
I am parameter1 decorator
I am method decorator
I am property decorator
I am class decorator 复制代码
因为是业务代码,与技术无关琐碎,只截取部分代码示意,非 Decorator 代码,以截图形式
这应该也是整理这篇文章最开始的缘由了。直接说说项目(rax1.0
+Decorator
)吧。
需求很简单,就是是编写一个页面的容器。
部分项目结构:
pm-detail
├─ constants
│ └─ index.ts //常量
├─ index.css
├─ index.tsx // 入口文件
└─ modules // 模块
└─ page-container // 容器组件
├─ base //容器基础组件
├─ decorator // 装饰器
├─ index.tsx
├─ lib // 工具
└─ style.ts
复制代码
重点看下以下几个文件
实际上是基础功能的封装
在此基础上,咱们须要个能滚动的容器
也是基于 Base.tsx
基础上,封装一些滚动容器具备的功能
import is from './util/is';
import map from './util/map';
const isObject = is(Object);
const isFunction = is(Function);
class Style {
static factory = (...args) => new Style(...args);
analyze(styles, props, state) {
return map(v => {
if (isFunction(v)) {
const r = v.call(this.component, props, state);
return isObject(r) ? this.analyze(r, props, state) : r;
}
if (isObject(v)) return this.analyze(v, props, state);
return v;
})(styles);
}
generateStyles(props, state) {
const { styles: customStyles } = props;
const mergedStyles = this.analyze(this.defaultStyles, props, state);
if (customStyles) {
Object.keys(customStyles).forEach(key => {
if (mergedStyles[key]) {
if (isObject(mergedStyles[key])) {
Object.assign(mergedStyles[key], customStyles[key]);
} else {
mergedStyles[key] = customStyles[key];
}
} else {
mergedStyles[key] = customStyles[key];
}
});
}
return {
styles: mergedStyles,
};
}
constructor(defaultStyles = {}, { vary = true } = {}) {
const manager = this;
this.defaultStyles = defaultStyles;
return BaseComponent => {
const componentWillMount = BaseComponent.prototype.componentWillMount;
const componentWillUpdate = BaseComponent.prototype.componentWillUpdate;
BaseComponent.prototype.componentWillMount = function() {
manager.component = this;
Object.assign(this, manager.generateStyles(this.props, this.state));
return componentWillMount && componentWillMount.apply(this, arguments);
};
if (vary) {
BaseComponent.prototype.componentWillUpdate = function(nextProps, nextState) {
Object.assign(this, manager.generateStyles(nextProps, nextState));
return componentWillUpdate && componentWillUpdate.apply(this, arguments);
};
}
return BaseComponent;
};
}
}
export default Style.factory;
复制代码
而后咱们须要一个错误的兜底功能,可是这个自己应该不属于容器的功能。因此咱们封装一个 errorDecorator
function withError<T extends IConstructable>(Wrapped: T) {
const willReceiveProps = Wrapped.prototype.componentWillReceiveProps;
const didMount = Wrapped.prototype.componentDidMount;
const willUnmount = Wrapped.prototype.componentWillUnmount;
return class extends Wrapped {
static displayName: string = `WithError${getDisplayName(Wrapped)}·`;
static defaultProps: IProps = {
isOffline: false,
isError: false,
errorRefresh: () => {
window.location.reload(true);
}
};
private state: StateType;
private eventNamespace: string = "";
constructor(...args: any[]) {
super(...args);
const { isOffline, isError, errorRefresh, tabPanelIndex } = this.props;
this.state = {
isOffline,
isError,
errorRefresh
};
if (tabPanelIndex > -1) {
this.eventNamespace = `.${tabPanelIndex}`;
}
}
triggerErrorHandler = e => {...};
componentWillReceiveProps(...args) {
if (willReceiveProps) {
willReceiveProps.apply(this, args);
}
const [nextProps] = args;
const { isOffline, isError, errorRefresh } = nextProps;
this.setState({
isOffline,
isError,
errorRefresh
});
}
componentDidMount(...args) {
if (didMount) {
didMount.apply(this, args);
}
const { eventNamespace } = this;
emitter.on(
EVENTS.TRIGGER_ERROR + eventNamespace,
this.triggerErrorHandler
);
}
componentWillUnmount(...args) {
if (willUnmount) {
willUnmount.apply(this, args);
}
const { eventNamespace } = this;
emitter.off(
EVENTS.TRIGGER_ERROR + eventNamespace,
this.triggerErrorHandler
);
}
render() {
const { isOffline, isError, errorRefresh } = this.state;
if (isOffline || isError) {
let errorType = "system";
if (isOffline) {
errorType = "offline";
}
return <Error errorType={errorType} refresh={errorRefresh} />; } return super.render(); } }; } 复制代码
而后咱们进行整合导出
import { createElement, PureComponent, RaxNode } from 'rax';
import ScrollBase from "./base/scrollBase";
import withError from "./decorator/withError";
interface IScrollContainerProps {
spmA:string;
spmB:string;
renderHeader?:()=>RaxNode;
renderFooter?:()=>RaxNode;
[key:string]:any;
}
@withError
class ScrollContainer extends PureComponent<IScrollContainerProps,{}> {
render() {
return <ScrollBase {...this.props} />; } } export default ScrollContainer; 复制代码
使用以下:
最后附一张,本文思惟导图。
公众号回复:【xmind1】 获取思惟导图源文件
公众号【全栈前端精选】 | 我的微信【is_Nealyang】 |
---|---|
![]() |
![]() |