Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ES 装饰器 (decorator) 在 AngularJS 1.x 中的使用 #36

Open
hjzheng opened this issue Nov 8, 2016 · 0 comments
Open

ES 装饰器 (decorator) 在 AngularJS 1.x 中的使用 #36

hjzheng opened this issue Nov 8, 2016 · 0 comments

Comments

@hjzheng
Copy link
Member

hjzheng commented Nov 8, 2016

ES 装饰器在 AngularJS 1.x 中的使用

准备

关于 ES 装饰器(decorator) 这个特性,就不在这里详细的介绍了: 更多内容大家可以参考javascript-decorators

简单的总结一下:
ES 的装饰器可以装饰类和类的方法(也可以装饰对象的方法):

1.装饰类的方法

function readonly(target, key, descriptor) {
  // 注意这三个参数:
  // target 类的 prototype
  // key 方法名称
  // descriptor descriptor 对象 
  descriptor.writable = false
  return descriptor
}
class User {
  @readonly
  say () {
    return '你好!';
  }
}

2.装饰类

function test(target) {
   // target 指类本身
  target.test= true
}

@test
class User {
  say () {
    return '你好!';
  }
}

关于装饰器如何传参,参考上面提到的资料。

使用

因为现有产品需要切换成 ES6(当然这里不单指 ES6 的特性)
在公司 AngularJS1.x 与 ES6 的编码风格中,对 controller 的使用,已经全面使用 class 去实现,这为使用装饰器创造了条件。

1.声明依赖注入

AngularJS 依赖注入显示声明, 可以很好的利用装饰器。请看实现:

const Inject = (...dependencies) => (target) => {
    target.$inject = dependencies;
};

@Inject('$scope', '$q', '$resource')
class MainCtrl {
    constructor($scope, $q, $resource) {
    }
}

当然还有更好的实现,这个大家可以参看 angular-es-utils 中的 inject 实现,非常的巧妙。

如果考虑到继承的情况,angular-es-utils 中的 inject 就不合适了。 另外该 Inject 返回了新的 class 这样会导致一块使用的装饰器,无法获取原构造函数的信息。

const toString = Object.prototype.toString;

export const Inject = (...dependencies) => (target) => {

	// 获取当前 class 的父类
	const parentClass = Object.getPrototypeOf(target);

	const parentDependencies = parentClass.$inject;

	if (parentDependencies && toString.call(parentDependencies) === '[object Array]') {
		dependencies = [...dependencies, ...parentDependencies];
	}

	target.$inject = dependencies;
};

/**
 * 考虑继承父类的依赖注入
 *
 * @Inject('$q', '$scope')
 * class P {
 *      constructor(...dependencies) {
 *
 *      }
 * }
 *
 * @Inject('$http')
 * class C extends P {
 *      constructor($http, ...parentDependencies) {
 *          super(...parentDependencies);
 *      }
 * }
 * */

最终方案,使用 Proxy 修改 constructor,自动将注入的服务挂载到 controller prototype 上

const toString = Object.prototype.toString;

export const Inject = (...dependencies) => (originTarget) => {

	// 获取当前 class 的父类
	const parentClass = Object.getPrototypeOf(originTarget);

	const parentDependencies = parentClass.$inject;

	if (parentDependencies && toString.call(parentDependencies) === '[object Array]') {
		dependencies = [...dependencies, ...parentDependencies];
	}

	originTarget.$inject = dependencies;

        // 使用 Proxy 修改构造函数
	const handler = {
		construct(target, argumentsList) {
			dependencies.forEach((dependence, index) => {
				target.prototype[`_${dependence}`] = argumentsList[index];
			});
			return Reflect.construct(target, argumentsList);
		}
	};

	const newTarget = new Proxy(originTarget.prototype.constructor, handler);

	return newTarget;
};

2.$apply

该实现依赖 angular-es-utils

import injector from 'angular-es-utils/injector';
import angular from 'angular';
const $rootScope = injector.get('$rootScope');

export const $apply = (target, key, descriptor) => {
    const fn = descriptor.value;

    if (!angular.isFunction(fn)) {
        throw new SyntaxError('Only functions can be @$apply');
    }

    return {
	...descriptor,
	value(...args) {
	    if (!$rootScope.$$phase) {
	        $rootScope.$digest(() => {
                       fn.apply(this, args);
	        });
	    }
	}
   };
};

class MainCtrl {
   @$apply
   test(){
   }
}

3.$timeout

import injector from 'angular-es-utils/injector';
import angular from 'angular';
const $timeout = injector.get('$timeout');

export const $timeout = (delay = 0, invokeApply = true) => (target, key, descriptor) => {
	const fn = descriptor.value;

	if (!angular.isFunction(fn)) {
	    throw new SyntaxError('Only functions can be @timeout');
	}

	return {
	    ...descriptor,
	    value(...args) {
	        $timeout(() => {
	            fn.apply(this, args);
	         }, delay, invokeApply);
	     }
	};
};

class MainCtrl {
	@$timeout(0, false)
	test(){
	}
}

4.路由配置

使用 UI-Router 去实现应用中的路由,使用装饰器将路由配置与 controller class 进行绑定,当 Angular 声明 module 时,读取对应的路由配置进行路由设置。

@Router('example', {
    url: '/example',
    templateUrl: ExampleTplUrl,
    controller: 'ExampleCtrl',
    controllerAs: 'vm'
})
export default class ExampleCtrl {
    constructor() {
       this.init();
    }

    init() {
    }
}

将配置存入一个公共对象中,以 class 名称作为 key(也可以使用 Reflect.defineProperty 看你的浏览支持情况)

import map from '../utils/map';
import traverse from '../utils/traverse';

export const Router = (state, config) => (target) => {
	// use target replace controller name
	traverse(config, 'controller', target);

	const routers = map.get('uiRoutersConf') || {};
	const className = target.name;

	routers[className] = {
		state,
		config
	};
	map.set('uiRoutersConf', routers);
};

封装 AngularJS module 方法,当初始化 module 时,设置路由, 根据 AngularJS + ES6 风格指南,顺便不对外提供 factory 和 filter 方法

import angular from 'angular';
import map from './map';

class DecoratedModule {
	constructor(name, modules = false) {
		this.routers = map.get('uiRoutersConf') || {};
		this.name = name;
		if (modules) {
			this.ngModule = angular.module(name, modules);
		} else {
			this.ngModule = angular.module(name);
		}
	}

	router(className) {
		const routers = this.routers;
		configRouter.$inject = ['$stateProvider'];
		function configRouter($stateProvider) {
			if (className) {
				$stateProvider.state(routers[className].state, routers[className].config);
			} else {
				Object.keys(routers).forEach((key) => {
					$stateProvider.state(routers[key].state, routers[key].config);
				});
			}
		}
		this.ngModule.config(configRouter);
		return this;
	}

	routerAll() {
		return this.router();
	}

	config(configFunc) {
		this.ngModule.config(configFunc);
		return this;
	}

	run(runFunc) {
		this.ngModule.run(runFunc);
		return this;
	}

	controller(...params) {
		this.ngModule.controller(...params);
		return this;
	}
}

function Module(...params) {
	const module = new DecoratedModule(...params);
        module.routerAll();
	return module;
}

export default Module;

5.Mixin

除了使用继承外, 为了简化 controller, 将其它功能通过 Mixin 的方式混入 controller class 中。

// 该实现是对[core-decorators.js] (https://github.com/jayphelps/core-decorators.js)的 mixin 实现的简化
const { defineProperty, getOwnPropertyNames, getOwnPropertyDescriptor } = Object;

function getOwnPropertyDescriptors(obj) {
    const descs = {};

    getOwnPropertyNames(obj).forEach((key) => {
        descs[key] = getOwnPropertyDescriptor(obj, key);
    });

    return descs;
}


export const Mixin = (...mixins) => (target) => {

    if (!mixins.length) {
        throw new SyntaxError(`@mixin() class ${target.name} 至少需要一个参数.`);
    }

    for (let i = 0; i < mixins.length; i++) {
        const descs = getOwnPropertyDescriptors(mixins[i]);
        const keys = getOwnPropertyNames(descs);

        for (let j = 0, k = keys.length; j < k; j++) {
            const key = keys[j];

            if (!(key in target.prototype)) {
                defineProperty(target.prototype, key, descs[key]);
            }
        }
    }
};

const obj = {
   myMethod(){
   }
}

@Mixin(obj)
class MainCtrl {
    constructor() {
        this.myMethod();
    }
}

6.Before/After

在 AngularJS1.x 结合 ES6 规范中已经弃用了 filter/service/factory 具体原因参考规范中No Service/Filter !!。 $provide.decorator 已经没有应用场景了。此时需要扩展一个 util 类或对象的方法,除了继承外,也可以使用装饰器进行扩展。

import angular from 'angular';

export const Before = (beforeFn) => (target, key, descriptor) => {
	const fn = descriptor.value;

	if (!angular.isFunction(fn)) {
	    throw new SyntaxError('Only functions can be @Before');
	}

	if (!angular.isFunction(beforeFn)) {
	    throw new SyntaxError('Only function can be pass to @Before');
	}

	return {
	    ...descriptor,
	    value(...args) {
                    args = beforeFn.apply(this, args) || args;
	            return fn.apply(this, args);
	     }
	};
};

export const After = (afterFn) => (target, key, descriptor) => {
	const fn = descriptor.value;

	if (!angular.isFunction(fn)) {
	    throw new SyntaxError('Only functions can be @After');
	}

	if (!angular.isFunction(afterFn)) {
	    throw new SyntaxError('Only function can be pass to @After');
	}

	return {
	    ...descriptor,
	    value(...args) {
                    const result = fun.apply(this, args);
	            return fn.apply(this, args.unshift(result)) || result;
	     }
	};
};

7.其他功能

类似 Debounce Bind 等功能,非常有用。这些都可以参考 core-decorators.js

以上装饰器的实现,请参考 https://github.com/hjzheng/angular-utils

最后

装饰器特性不仅可以在不改变原有类或方法的前提下,增加新的功能和特性,另外还可以简化代码的写法,对于编码效率提升非常有用。

@hjzheng hjzheng changed the title ES7 装饰器在 AngularJS 1.x 中的使用 ES 装饰器在 AngularJS 1.x 中的使用 Nov 15, 2016
@hjzheng hjzheng changed the title ES 装饰器在 AngularJS 1.x 中的使用 ES 装饰器 (decorator) 在 AngularJS 1.x 中的使用 Nov 17, 2016
@hjzheng hjzheng added the 分享 label Nov 24, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant