XIANFESchool / FE-problem-collection

前端问题收集和知识经验总结
https://github.com/ShuyunXIANFESchool/FE-problem-collection/issues
63 stars 22 forks source link

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

Open hjzheng opened 8 years ago

hjzheng commented 8 years ago

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

最后

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