我爱编程程序员@IT·互联网

为 Angular 1.x 添加依赖注入 Decorator

2016-08-26  本文已影响0人  coderbiq

最近在研究从 Angular 1.x 切换到 Angular 2,因为目前 Angular 2 正处于 RC 阶段还会有比较多的变化,暂时还是使用 1.x 。 为了以后能比较方便的迁移,所以虽然用的是 1.x 但是在写法上还是希望更接近于 2.0 的写法,也就有了以下我想要实现的功能:使用 decorator 来处理 angular 1.x 的依赖注入。

在设计、实现的时候我尽量只使用 ES6 和 ES7 的新特性,因为在试用 Angular 2 的时候发现 Typescript 编译确实是太慢了,而且 Typescript 的静态类型检查虽然是我喜欢的但是使用起来对开发效率影响比较大,综合平衡后决定未来还是基于 ES 标准开发就好了,不打算切换到 Typescript 或者其它 XType。

Angular 1.x 依赖注入的现状和问题

使用过 Angular 的朋友都知道依赖注入是 Angular 中非常重要的特性,在 1.x 时代有3种方式可以实现依赖注入:

方式一:

angular.module('app', [])
    .controller('mainCtrl', [
        '$scope',
        function($scope) { ... }
    ])

方式二:

function mainCtrl($scope) { ... }
mainCtrl.$inject = ['$scope'];
angular.module('app', [])
    .controller('mainCtrl', mainCtrl);

方式三:

angular.module('app', [])
    .controller(
        'mainCtrl', 
        function($scope) { ... }
    );

方式1、2都需要写两次需要注入的服务并且配置服务名称和编写入参时的顺序必需一致,比较影响开发效率而且容易出错。而方式3在框架内部是通过分析函数的入参名称来确定需要注入的服务,使用起来非常方便但是如果对JS代码进行压缩处理后,框架就无法得知真正需要注入的服务了。

所以,目前这三种依赖注入的方式都存在一些问题。不过基本方式三和 ng-annotate 还是可以比较完美的解决依赖注入的问题,简单来说 ng-annotate 就是通过自动构建工具在压缩JS代码前将方式三转换成方式二,而这一切都是由构建工具自动完成我们在开发的时候并不用关心。

废话了这么多只是为了说明本文希望解决的问题:其实很简单,就是希望在依赖注入的时候只写一次需要注入的服务!虽然使用 ng-annotate 已经可以实现,但是我希望用最新的标准来处理这个问题,以便未来更方便的向 Angular2 过渡,这种方式就是使用 Decorator。

使用 Decorator 解决 angular 1.x 的依赖注入问题

Decorator 是 ES7 的一个提案,目前通过 Babel 或者 Typescropt 已经可以提前享用。尝试过 Angular2 的朋友应该知道在 Angular2 中就引入了很多的 Decorator,例如:

import { Component } from '@angular/core';

@Component({
...
})
export class MainComponent {
...
}

这里的 Component 就是一个 Decorator, 使用过 Python 的朋友应该也非常熟悉这种语法。简单来说 Decorator 就是一个模板,可以在编译阶段对被装饰的类或类成员进行修改(目前通过Babel使用的时候貌似还是在Runtime阶段起使用)。

在使用 Decorator 解决依赖注入问题的时候我首先想到的就是 ng-annotate 的方案,在 Decorator 里去分析被装饰类构造函数的入参列表,然后为被装饰类添加$inject属性,代码如下:

function inject() {
    return function(target) {
        let services = angular.injector.$$annotate(target);
        target.$inject = services;
    }
}

@inject()
class MainComponent {
    constructor($scope) {
        ...
    }
}

其中angular.injector.$$annotate(target)就是 angular 中解析入参名称列表的函数,之前还花了好长时间自己去实现,后来一想 angular 既然可以自动发现需要注入的服务肯定有完整的入参分析,果然就在源码的 src/auto/injector.js 里找到了。这种方式在没有压缩JS代码的情况下测试运行没有问题,$inject属性正确添加。但是,在压缩JS代码后就出现没有找到aProvider这样的错误了,阅读 Babel 转译后的代码也没有发现 MainComponent.$inject = [...] 这行代码,所以我认为在 Babel 中 Decorator 其实是在 Runtime 中执行而不是在编译的时候执行(难道我对编译和运行时的理解有错?还是提案里不是说的在编译时执行?)。

其实在 Github 中已经有人为 Angular 1.x 的依赖注入提供了 Decorator 的实现,大概的实现方式是:

function inject(...services) {
    return function(target) {
        target.$inject = services;
    }
}

@inject('$scope', '$http')
class MainComponent {
    constructor($scope, $http) {
        ...
    }
}

但是这种实现方式还是需要写两次要注入的服务并且入参的顺序与配置的顺序还必须一致,无非是将MainComponent.$inject = []改成了@inject(...),并不是我希望能达到的目的。但是基于这个思路我想到了第二种实现方案,通过 decorator 的入参获取到需要注入的服务,然后在 decorator 中去修改被装饰类的构造函数,在实例化被装饰类时自动将注入的服务添加为实例的属性。

如何在 decorator 中去修改被装饰类的 constructor 呢?这就需要使用到 ES6 的一个新 API Proxy 了,具体实现如下:

function inject(...services) {
    return function(target, property, descriptor) {
        var p = new Proxy(target, {
            construct: function(target, args) {
                for(let i in services) {
                    target.prototype[services[i]] = args[i];
                }
                return new target(...args);
            }
        });
        p.$inject = services;

        return p;
    }
}

@inject('$scope', '$http')
class MainComponent {
    constructor() {
        this.$http.get(url).then(data => {
            this.$scope.data = data;
        })
    }
}

在 decorator 中我创建了一个 Proxy 实例来拦截被装饰类的实例化行为,将需要注入的服务自动添加为被装饰类的实例属性,从而在被装饰类的实例中可以直接使用而不用关心依赖注入的细节(在 constructor 中不用再关心第一个入参对应的是那个服务,第二个参数对应的是那个服务...)。当然,这只是一个最基础的实现还可以更完善一些,例如:添加 $scope as scope 语法修改服务实例属性名称等。

而 Proxy 在 ES6 的文档中有详细说明,它可以提供一种拦截机制从而去改变被拦截对象的一些行为,配套的还有一个 Reflect 可以在修改被拦截对象后获取对象原始的行为。

以上就是本文的全部,使用 decorator 可以实现很多有意思的功能,而从 Angular 1.x 向 2.0 迁移在 Angular 团队内部也在做很多的努力。不过我的真正目的还是脱离框架去实现一些常用功能模块,以便将来不管是用什么框架都能使用的到,最近在开发前后端的时候都在往这个方向努力。在当前各个语言都拥有完善的包管理器的情况下,个人觉得一个大而全的框架已经不是我需要的了!

上一篇下一篇

猜你喜欢

热点阅读