AngularJS中controller和service的继承与扩展

最近公司要做的项目中牵扯到很多场景需要对controller和service进行继承和扩展的,总结一下心得体会。开讲之前,先明确一下这里的所谓的“继承”与“扩展”。

  • 所谓继承,比较熟悉,这里就是指定义一个新的controller/service(不同名),继承原来的controller/service,然后在其基础上重写一些功能。
  • 所谓扩展,这里说的是在不产生新的controller/service的情况下,添加或修改原controller/service的功能。

目前研究的结果就是service可以轻松的实现继承和扩展,而controller貌似只能继承。

controller的继承

说到controller,我们在前面的文章中介绍过有两种写法:使用$scope或使用controller as。针对这两种方式的区别,我们也可以使用两种不同的继承方式:

  • 使用controller as的情况下,特点是controller不再依赖$scope,就跟普通的函数差不多,这个时候可以使用Javascript原生的继承方式。
  • 使用$scope时,可以使用AngularJS内置的$controllerservice,通过依赖注入的方式实现继承。

还是直接上例子吧。

使用原生的继承

See the Pen zxpRbK by Pinky Jie (@pinkyjie) on CodePen.

这个例子有左右两块区域,左边的是parent,右边的是child,它们分别有5行数据需要显示:

  • 第1行性别,对应属性sex。用于展示继承时不更改的属性。
  • 第2行角色,对应属性name。用于展示继承后覆盖的属性。
  • 第3行个数,对应属性num,左边是孩子的个数,右边是兄弟的个数,显然右边比左边少1。用于显示后面add()方法的结果。
  • 第4行是个按钮,对应方法add(),左边是parent想多要一个孩子,右边是想child想多要一个兄弟,结果是一样的。用于展示继承时不改变的方法。
  • 第5行也是个按钮,对应方法test(),用于展示继承时覆盖的方法。(不要在意这个随意的名字,因为我已经场景匮乏了。。。)

通过这5行,这个例子就基本涵盖了继承时发生的大部分情况。那么切换到JS里看看实现吧。JS里的代码结构大致分为5部分,用注释/* Section 1 */来区分(由于CodePen这工具不支持显示行号,所以只能用代码里的注释来分块讲解了)。

  • 第1部分是一个典型的Javascript实现的继承函数extend,里面的原理不再详述,有兴趣可以看以前写的《理解Backbone中extend的实现》

    这段代码怎么生成?访问CoffeeScript官网,点击“TRY COFFEESCRIPT”,会打开一个编辑器窗口,左边写CoffeeScript,右边就会生成相应的Javascript。在左边键入class A extends B,右面就会有extend函数啦。机!智!

  • 第2部分是一个名叫FamilyService的AngularJS的service。两个controller都需要依赖它。它的功能很简单,parent和child需要的孩子数量和兄弟数量都由这个service来提供,分别是方法getChildrenCount()getSiblingCount()。除此之外,每次生孩子的时候它会trigger一个new-child事件,并将孩子数量通过参数传播出去。
  • 第3部分就是定义ParentCtrl这个父controller了,由于采用controller as写法,这里的定义跟普通的对象没区别,在构造函数里定义属性,方法则定义在prototype上。这里值得注意的有两点:
    • add()函数其实仅仅是调用FamilyServicenewChild()方法。
    • num属性的改变是通过响应new-child事件来实现的。
  • 第4部分就是关键的ChildCtrl定义了,同样,属性的定义在构造函数中,方法定义在prototype上。可以发现,发生继承需要以下几步:
    1. 构造函数中先调用父controller的构造函数。这里是通过__super__来实现对父controller的引用的,因为在extend函数中我们已将讲ParentCtrl的prototype赋值给__super__变量了。这步可以保证把ParentCtrl里定义的属性以及事件响应继承过来。
    2. 覆盖属性和事件响应。 这里覆盖了namenum属性,并且更改了事件响应函数的内容,因为new-child事件返回的参数是孩子的总数,这里要减去自己才能得到兄弟的个数。
    3. 调用extend函数。
    4. 覆盖方法。这一步必须放在最后,如果放在extend函数的前面,则extend函数会将ChildCtrl重新定义的add方法用ParentCtrl的覆盖。
  • 第5部分就是各种模块、controller、service的定义了,没啥多说的。

使用$controllerservice

See the Pen zxaZPw by Pinky Jie (@pinkyjie) on CodePen.

例子的实际效果与上面的一致,我们直接看JS部分的实现吧。这个代码分为4个部分:

  • 第1部分与前面service的定义相同。
  • 第2部分是ParentCtrl的定义,这里与前面的不同一个是全部使用$scope,另一个比较特殊的是add方法,它直接调用了一个绑定在this上面的add方法,这样做没什么特殊含义,仅为了演示后面的继承。
  • 第3部分是ChildCtrl的定义,这里的继承是通过var parentCtrl = $controller('ParentCtrl', {$scope: $scope});来实现的,通过依赖注入得到父controller的实例,并将自己的$scope传入,这样,父controller绑定在$scope上面的东西就全部继承到子controller上面了。除此之外,还可以使用变量parentCtrl来引用父controller,跟前面的__super__一样。add方法的覆盖给出了使用parentCtrl的例子。
  • 第4部分与前面相同,区别就是ChildCtrl依赖了$controllerservice。

比较

可以发现,其实这两种方式最大的区别就是,原生继承中需要调用extend函数来继承,并且子controller里需要显式调用父controller的构造函数来是实现属性的继承。而使用$controllerservice则只需要依赖注入和传入$scope即可。

service的继承

service的继承就比较简单了,AngularJS中的service可以认为是new了service构造函数的实例。看下面这个例子,与上面的例子类似,同样是展示了继承过程中不改变或覆盖父service的属性和方法。

See the Pen WbyYdK by Pinky Jie (@pinkyjie) on CodePen.

熟悉的左右布局,三个属性两个方法。直接看JS中的实现,4个部分:

  • 第1部分定义了一个controller,这个controller的所有功能基本都来自于service。页面中左右两个部分用的是同样的controller,只是名字不同,而且依赖的service不同。这一点从第4部分可以直观的看出来。
  • 第2部分定义父service,没什么特别的。
  • 第3部分定义子servcie,继承就发生在这里。首先,子service需要依赖父service,然后直接使用AngularJS内置的extend函数来实现继承,将父service实例上的属性方法都拷贝到this上,即完成了继承,后面就是一些属性和方法的覆盖了。

    这里的extend和上面自己写的extend有什么区别呢?其实angular.extend的功能是一个Shallow Copy,类似上面我们自己的extend中的这段

    1
    2
    3
    4
    for (var key in parent) {
    if (hasProp.call(parent, key))
    child[key] = parent[key];
    }
  • 第4部分各种定义,可以看到TestCtrl1TestCtrl2的定义都是TestCtrl,只是一个依赖ParentService,另一个依赖ChildService罢了。

service的扩展

其实扩展说白了,就是可以把一个已经定义好的service进行修改,为了保证这个修改的优先级,可以在module的config阶段来实现。废话不说,直接上例子吧。

See the Pen LErmxz by Pinky Jie (@pinkyjie) on CodePen.

这个例子与前面的类似,只是页面只有一个部分,实现的结果跟上面的右半部分一致。直接看JS中的实现,除了第3部分其他都是一样的。第3部分中定义了一个extendServcie函数,这个函数是在app的config阶段调用的(见第4部分)。这个函数中依赖了一个特殊的service$provide,扩展功能就由它的decorator方法来实现,方法的第一个参数就是要扩展的service的名称,第二个参数就是实际的扩展。在第二个参数中依赖一个$delegateservice,这个service代表的其实就是TestService本身,可以看到在函数中我们直接使用$delegate去引用原有servcie,并进行随意更改,最终将这个$delegate返回即可。

也许很多人会觉得这种场景很诡异,平常根本不可能用得到。但是我在项目中就曾经遇到一个例子:在单页面应用中我们并没有把所有JS文件压缩在一起,而是根据不同的页面去lazy load不同的文件。具体去load哪些文件定义在一个servcie中,但这些文件名在development和production阶段是不一样的,这样就需要两套不同的文件名配置。利用servcie的扩展,可以在production环境时多include一个JS文件,在这个文件中对servcie进行扩展,更新那些相应的文件名。