ES 装饰器 (decorator) 在 AngularJS 1.x 中的使用
hjzheng opened this issue · comments
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
最后
装饰器特性不仅可以在不改变原有类或方法的前提下,增加新的功能和特性,另外还可以简化代码的写法,对于编码效率提升非常有用。