3. 模块与控制器
模块是AngularJS页面最重要的组成单元,每个模块相当于一个独立的AngularJS应用。模块是一系列服务(service、指令、控制器、过滤器和配置信息)的集合。控制器是模块的下一级程序单元,也表示AngularJS模块内的重要作用域。
3.1 模块的加载
AngularJS模块的加载分成两种情况。
➢ 匿名模块(就是没有为ng-app指定属性值或属性值为空字符串),此时AngularJS可以自动加载并创建AngularJS模块。
➢ 命名模块(为 ng-app 指定了属性值,该属性值就是该模块的名称),此时必须调用angular对象的module()方法来创建AngularJS模块。
关于匿名模块,前面已经见过很多示例了。下面将会示范命名模块的创建,创建命名模块需要调用angular对象的module()方法,该方法的语法格式如下:
angular.module(name,[requires],[configEn])
该方法的第一个参数指定要创建的模块名;第二个参数是一个数组,指定创建该模块时需要依赖的模块;第三个参数用于传入一个配置函数。实际上调用 module()方法会返回一个angular.Module 对象,该对象表示新创建的模块,该对象包含一个 config()方法,调用该方法时也可传入configFn参数。由此可见,config()方法的作用完全等同于第三个参数的作用,本小节后面会给出使用configFn配置函数的示例。
如下页面代码示范了命名模块的创建。
<body>
<div ng-app="zkApp">
<input type="text" ng-mode="name" />
{{name}}
</div>
<script type="text/javascript">
//用名称来创建模块
var app = angular.module("zkApp",[]);
</script>
</body>
在该页面代码中为 ng-app 属性指定了属性值 zkApp,这表明它是一个命名模块,因此程序需要使用angular对象的module()方法来创建该模块。模块创建完成之后,可调用模块的如下两个方法为模块添加全局变量或常量。
➢ value(name,value):添加全局变量。
➢ constant(name,value):添加全局常量。
这两个方法的用法基本相似,区别只是value()方法添加的是变量,而constant()方法添加的是常量。使用value()方法或constant()方法添加的变量或常量可被本模块内的任意控制器访问。一个模块可包含多个控制器,模块可调用controller()方法来注册控制器。
下面代码示范了如何利用控制器访问模块内的变量或常量。
<div ng-app="zkApp" ng-controller="zkCtrl">
<input type="text" ng-mode="name" />
{{name}}
</div>
<script type="text/javascript">
//用名称来创建模块
var app = angular.module("zkApp",[]);
//设置变量
app.value("name","张三");
//设置常量
app.constant("MAX_AGE",100);
app.value("name","李四");
app.constant("MAX_AGE",200);
//注册名为zkCtrl的控制器
app.controller("zkCtrl",function(name,MAX_AGE){
console.log(name);
console.log(MAX_AGE);
});
</script>
这里在HTML代码中又用到了一个新的指令:ng-controller,该指令用于定义控制器,该控制器的名称为zkCtrl。接下来程序即可通过调用模块的controller方法并根据zkCtrl名称来操作控制器,可在操作控制器时传入的函数中声明多个参数—所有需要在控制器中使用的对象都应该在函数参数中声明。由于这里需要访问模块内定义的name、MAX_AGE,因此程序为该函数声明了name、MAX_AGE两个形参。在浏览器中浏览该页面,可以在控制台看到如图所示的输出。
如果一个页面内包含多个AngularJS 模块,AngularJS 默认只会启动第一个模块,因此程序必须调用angular对象的bootstrap()方法来启动指定模块。例如如下代码。
<!--匿名模块-->
<div ng-app>
<input type="text" ng-mode="name" />
{{name}}
</div>
<!--下一个div-->
<div id="userDiv">
用户名:<input type="text" ng-mode="name" /><p>
密码:<input type="text" ng-mode="pass" /><p>
用户名为:{{name}}
密码为:{{pass}}
{{name}}
</div>
<script type="text/javascript">
var app = angular.module("myApp",[]); //①
angular.bootstrap(document.getElementById("userDiv"),["myApp"]);
</script>
页面中的第一个<div…/>元素指定了ng-app属性,但并未指定属性值,因此这是一个匿名模块,AngularJS可以自动启动并加载该模块,因此该模块内的AngularJS应用可以运行良好。页面中的第二个<div…/>元素没有指定ng-app属性(就算指定了ng-app属性也没用),因为程序最后一行粗体字代码调用了angular对象的bootstrap()方法来启动AngularJS应用。需要为bootstrap()方法传入两个参数:第一个参数是一个DOM对象,指定要将哪个DOM对象(对应为HTML元素)启动为AngularJS应用;第二个参数是一个数组,用于为该AngularJS应用指定名称,例如此处为该 AngularJS 应用指定的名称为 myApp,这样程序在①号代码处使用module()方法加载模块时也指定了模块myApp。AngularJS 要求把调用 bootstrap()方法启动模块的代码放在最后,也就是上面示例中粗体字代码必须放在①号代码之后。
3.2 控制器初始化$scope对象
AngularJS最早是按MVC架构设计的,在这种设计下,AngularJS应用内各组件可分为:
➢ 模型(Model)。模型由普通JavaScript对象充当。
➢ 视图(View)。视图由HTML页面充当,AngularJS添加了一些指令来增强HTML标签的作用。
➢ 控制器(Controller)。控制器就是前面介绍的通过模块的controller()方法注册的对象。
但后来大家发现 AngularJS 并不是 MVC 架构,反而更像是MVVM(Model、View、ViewModel)架构,MVVM架构将“双向绑定”的思想作为核心,切断了View和Model之间的联系,View、Model完全通过ViewModel进行交互,而且Model和ViewModel之间的交互是双向的,因此视图的数据的变化会同时引起数据源数据的变化,而数据源数据的变化也会立即反映到View上。MVVM的示意图大致如图所示。
从MVVM架构来看,AngularJS的控制器其实并不是控制器,而是ViewModel组件。后来总有人不断讨论AngularJS到底是MVC呢,还是MVVM。AngularJS官方提出一个新名称:MVW(Model、View、Whatever),意思是不要深究它到底是Controller还是ViewModel。调用模块的controller()方法注册控制器时,传入的第二个参数是一个函数,在该函数中可声明一个 s c o p e 形 参 , 该 形 参 就 表 示 了 A n g u l a r J S 控 制 器 对 应 的 作 用 域 。 从 A n g u l a r J S 的 设 计 架 构 来 看 , scope形参,该形参就表示了AngularJS控制器对应的作用域。从AngularJS的设计架构来看, scope形参,该形参就表示了AngularJS控制器对应的作用域。从AngularJS的设计架构来看,scope充当了AngularJS应用的模型,因此通过$scope作用域指定的属性,和 HTML 中 ng-model 对应的变量是双向绑定的,也可自动显示在与指定ng-bind绑定的HTML元素中。
例如如下代码。
<div ng-app="zkApp" ng-controller="zkCtrl">
商品价格:<input type="number" min="50" max="100" ng-model="price" />
商品数量:<input type="number" min="0" ng-model="num" />
总价:{{price * num}}
</div>
<script type="text/javascript">
//用名称来创建模块
var app = angular.module("zkApp",[]);
//注册名为zkCtrl的控制器
app.controller("zkCtrl",function($scope){
$scope.price = 50.2;
$scope.num = 4;
});
</script>
该示例在控制器中通过 s c o p e 为 p r i c e 和 n u m 两 个 属 性 指 定 初 始 值 , 这 两 个 属 性 被 双 向 绑 定 到 H T M L 中 的 两 个 < i n p u t . . . / > 元 素 上 , 因 此 在 加 载 页 面 时 即 可 看 到 文 本 框 显 示 了 初 始 值 , 如 图 所 示 。 ! [ 在 这 里 插 入 图 片 描 述 ] ( h t t p s : / / i m g − b l o g . c s d n i m g . c n / 20200616193430359. p n g ? x − o s s − p r o c e s s = i m a g e / w a t e r m a r k , t y p e Z m F u Z 3 p o Z W 5 n a G V p d G k , s h a d o w 1 0 , t e x t a H R 0 c H M 6 L y 9 i b G 9 n L m N z Z G 4 u b m V 0 L 3 F x X z Q z N T k x M z Y z , s i z e 1 6 , c o l o r F F F F F F , t 7 0 ) 此 外 也 可 通 过 scope为price和num两个属性指定初始值,这两个属性被双向绑定到 HTML 中的两个<input.../>元素上,因此在加载页面时即可看到文本框显示了初始值,如图所示。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200616193430359.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQzNTkxMzYz,size_16,color_FFFFFF,t_70) 此外也可通过 scope为price和num两个属性指定初始值,这两个属性被双向绑定到HTML中的两个<input.../>元素上,因此在加载页面时即可看到文本框显示了初始值,如图所示。![在这里插入图片描述](https://img−blog.csdnimg.cn/20200616193430359.png?x−oss−process=image/watermark,typeZmFuZ3poZW5naGVpdGk,shadow10,textaHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQzNTkxMzYz,size16,colorFFFFFF,t70)此外也可通过scope 为控制器作用域设置方法(相当于函数),例如如下代码。
<div ng-app="zkApp" ng-controller="zkCtrl">
<!--使用ng-click绑定事件处理函数-->
<button ng-click="clickhandler();">单击我</button>
单击了{{count}}次;
</div>
<script type="text/javascript">
//用名称来创建模块
var app = angular.module("zkApp",[]);
//注册名为zkCtrl的控制器
app.controller("zkCtrl",function($scope){
$scope.count = 0;
//为scope添加方法
$scope.clickhandler = function(e){
$scope.count++;
console.log(e);
}
});
</script>
在该示例中,在页面的<script…/>元素内使用 JavaScript 脚本为$scope 增加了一个clickhandler方法,接下来就可通过ng-click指令为按钮的单击事件指定事件处理函数。在浏览器中浏览该页面,单击页面上的“单击我”按钮.
即可看到如图所示的效果。
通过前面的介绍可以看出,不管使用AngularJS的指令“双向绑定”变量,还是使用表达式输出AngularJS变量,还是使用AngularJS指令绑定事件处理函数,这些变量、函数都应该是$scope作用域内的。
3.3 $rootScope作用域
除了普通 s c o p e 之 外 , A n g u l a r J S 还 有 一 个 scope之外,AngularJS还有一个 scope之外,AngularJS还有一个rootScope, r o o t S c o p e 的 作 用 域 对 应 于 整 个 应 用 , 因 此 通 过 rootScope的作用域对应于整个应用,因此通过 rootScope的作用域对应于整个应用,因此通过rootScope作用域指定的属性和方法可以在多个控制器中共享。
例如如下代码。
<body ng-app="zkApp">
<div ng-controller="zkCtrl">
书名:<input type="text" ng-model="bookName" />
</div>
<div ng-controller="myCtrl">
{{theName}}
</div>
<script type="text/javascript">
//用名称来创建模块
var app = angular.module("zkApp",[]);
//根据名称注册控制器
app.controller("zkCtrl",function($scope,$rootScope){
$scope.bookName = '第一行代码';
//为scope添加方法
$rootScope.theName = $scope.bookName;
});
app.controller("myCtrl",function($scope,$rootScope){
});
</script>
</body>
该页面代码在HTML中使用ng-model将页面的<input…/>元素绑定到bookName变量,这个变量会被绑定到 s c o p e 作 用 域 的 b o o k N a m e 属 性 。 第 一 行 粗 体 字 代 码 为 scope 作用域的 bookName属性。第一行粗体字代码为 scope作用域的bookName属性。第一行粗体字代码为scope 作用域的bookName属性设置了初始值;第二行粗体字代码为 r o o t S c o p e 的 t h e N a m e 属 性 设 置 了 初 始 值 — 在 rootScope的theName属性设置了初始值—在 rootScope的theName属性设置了初始值—在rootScope作用域内设置的属性值可以被所有控制器访问,因此在该页面中第二个控制器的HTML中可直接使用{{theName}}来输出 r o o t S c o p e 内 的 t h e N a m e 属 性 值 。 使 用 浏 览 器 浏 览 该 页 面 , 可 以 看 到 如 下 图 所 示 的 效 果 。 需 要 指 出 的 是 , 在 H T M L 中 通 过 表 达 式 输 出 变 量 值 , 或 通 过 n g − m o d e l 或 n g − b i n d 绑 定 作 用 域 内 的 变 量 时 , A n g u l a r J S 优 先 使 用 rootScope内的theName属性值。 使用浏览器浏览该页面,可以看到如下图所示的效果。需要指出的是,在HTML中通过表达式输出变量值,或通过 ng-model 或 ng-bind 绑定作用域内的变量时,AngularJS优先使用 rootScope内的theName属性值。使用浏览器浏览该页面,可以看到如下图所示的效果。需要指出的是,在HTML中通过表达式输出变量值,或通过ng−model或ng−bind绑定作用域内的变量时,AngularJS优先使用scope作用域内的属性,只有当 s c o p e 作 用 域 内 不 存 在 该 属 性 时 , 才 会 使 用 scope作用域内不存在该属性时,才会使用 scope作用域内不存在该属性时,才会使用rootScope作用域内的属性。
当 s c o p e 作 用 域 和 scope作用域和 scope作用域和rootScope作用域内存在同名的属性时, s c o p e 作 用 域 内 属 性 的 值 会 覆 盖 scope作用域内属性的值会覆盖 scope作用域内属性的值会覆盖rootScope作用域内该属性的值。
3.4 $watch方法的使用
在前面的示例中,当用户第一次浏览时可以看到第一个控制器中文本框的内容和第二个控制器中表达式的输出完全相同,但当用户修改第一个控制器内的文本框的内容时,第二个控制器内的表达式的值并不会随之改变。这是由于当第一个文本框的值发生改变时,与该文本框“双向绑定”的bookName会随之改变,也就是 s c o p e 作 用 域 内 b o o k N a m e 属 性 值 会 随 之 改 变 , 但 scope作用域内bookName属性值会随之改变,但 scope作用域内bookName属性值会随之改变,但rootScope内的theName属性并没有发生改变,因此第二个控制器内的{{theName}}表达式输出不会发生改变。为了让第一个控制器的 s c o p e 作 用 域 内 的 b o o k N a m e 发 生 改 变 时 , scope作用域内的bookName发生改变时, scope作用域内的bookName发生改变时,rootScope作用域内的theName也会随之发生改变,可使用 w a t c h ( ) 方 法 来 监 听 watch()方法来监听 watch()方法来监听scope作用域内属性的改变。$watch()方法的语法格式如下:
$watch(watchExpression,listener,[objectEquallity])
该方法的第一个参数指定要监听的属性;第二个参数指定监听函数,在该函数中可声明两个形参:第一个形参表示该属性之前的值,第二个形参表示该属性被修改之后的值。objectEquality参数是可选的,指定是否使用angular对象的equals()方法来判断两个属性值是否相等,该参数默认为false,这意味着依然使用JavaScript原生的方式比较两个属性值是否相等。下面的例子对前一个示例进行修改,修改后的示例将可让 r o o t S c o p e 作 用 域 内 的 t h e N a m e 随 着 rootScope作用域内的theName随着 rootScope作用域内的theName随着scope作用域内的bookName的改变而改变。示例代码如下。
<body ng-app="zkApp">
<div ng-controller="zkCtrl">
书名:<input type="text" ng-model="bookName" />
</div>
<div ng-controller="myCtrl">
{{theName}}
</div>
<script type="text/javascript">
//用名称来创建模块
var app = angular.module("zkApp",[]);
//根据名称注册控制器
app.controller("zkCtrl",function($scope,$rootScope){
$scope.bookName = '第一行代码';
//为scope添加方法
$rootScope.theName = $scope.bookName;
//监听bookName的改变
$scope.$watch("bookName",
//指定监听函数
function(newValue,oldValue){
//只要修改后的的值与原值不相同,改变$rootScope作用域的theName属性值
if( newValue !== oldValue){
$rootScope.theName = $scope.bookName;
}
})
});
app.controller("myCtrl",function($scope,$rootScope){
});
</script>
</body>
粗体字代码调用了 s c o p e 对 象 的 scope对象的 scope对象的watch()方法监听bookName属性的改变,当bookName发生改变时,程序会将 s c o p e . b o o k N a m e 的 值 赋 给 scope.bookName 的值赋给 scope.bookName的值赋给rootScope.theName。使用浏览器浏览该页面,可以看到当用户通过文本框修改图书名时,第二个控制器内{{theName}}表达式总可以显示最新的书名。