最近公司要做的项目中牵扯到很多场景需要对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()函数其实仅仅是调用FamilyService的newChild()方法。num属性的改变是通过响应new-child事件来实现的。
- 第4部分就是关键的
ChildCtrl定义了,同样,属性的定义在构造函数中,方法定义在prototype上。可以发现,发生继承需要以下几步:- 构造函数中先调用父controller的构造函数。这里是通过
__super__来实现对父controller的引用的,因为在extend函数中我们已将讲ParentCtrl的prototype赋值给__super__变量了。这步可以保证把ParentCtrl里定义的属性以及事件响应继承过来。 - 覆盖属性和事件响应。 这里覆盖了
name和num属性,并且更改了事件响应函数的内容,因为new-child事件返回的参数是孩子的总数,这里要减去自己才能得到兄弟的个数。 - 调用
extend函数。 - 覆盖方法。这一步必须放在最后,如果放在
extend函数的前面,则extend函数会将ChildCtrl重新定义的add方法用ParentCtrl的覆盖。
- 构造函数中先调用父controller的构造函数。这里是通过
- 第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
4for (var key in parent) {
if (hasProp.call(parent, key))
child[key] = parent[key];
} - 第4部分各种定义,可以看到
TestCtrl1和TestCtrl2的定义都是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进行扩展,更新那些相应的文件名。