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

Angular ES

hjzheng 发布于

git 地址  https://github.com/ShuyunXIANFESchool/FE-problem-collection/issues/36

准备


关于 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 可惜目前babel 不支持)


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


最后


装饰器特性可以在不改变原有类或方法的前提下,增加新的功能,对于编码效率和体验提升非常有用。