优化项目的良好框架--AngularJS

学习AngularJS之前先对它来个整体上的定位。AngularJS是为了克服HTML在构建应用上的不足而设计的,它有着诸多特性,最为核心的是:MVVM、模块化、自动化双向数据绑定、语义化标签、依赖注入等等。

ng(Angular的缩写)的作者这样理解它:“HTML是一门很好的为静态文本展示设计的声明式语言,但要构建WEB应用的话它就显得乏力了。所以我做了一些工作(你也可以觉得是小花招)来让浏览器做我想要的事。”

总结如下:ng 让 html 可运算,还有了MVVM的味道。接下来我们开始尝试使用angularJS。编写一个入门级的案例Hello World会很不错。有nodeJS环境的话我们可以用npm或者bower安装AngularJS(windows系统可能会出现错误==),

1
2
$ sudo npm init
$ sudo npm install angular 1.5.7

当然也可以到官网上直接下载angularJS文件,我们从第一个版本学起,先不选择angularJS–2。

1
2
3
4
5
6
7
8
9
<!doctype html>
<html ng-app>
<head>
<script src="http://code.angularjs.org/angular-1.0.1.min.js"></script>
</head>
<body>
Hello {{'World'}}!
</body>
</html>

打开浏览器查看效果,可以看到Hello World成功地显示了。
现在我们来瞧一瞧html代码,简单地引入angularJS(这里暂时使用CDN引入),然后注意给模板加上ng-app,表示声明一个angular实例

1
<html ng-app>

然后,我们使用双大括号这样的angular语法来编写我们的实例。双括号中可以写运算表达式,如”3“, “Helloworld“。这就是html的可运算化。

看完了第一个例子,我们来介绍下几个基本的angularJS语法:

1
2
3
4
5
ng-app          声明一个angular实例
ng-controller 指定一个控制器
ng-model 绑定数据变量到ng-app实例中
ng-bind 绑定变量到innerHTML
ng-repeat 遍历数组

第二个例子是经典的angularJS双向绑定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script type="text/javascript" src="./js/angular.js"></script>
</head>
<body ng-app>
<div class="container">
名字:<input type="text" ng-model="param">
<p>hello {{param}}</p>
</div>
</body>
</html>

在输入数据的同时,p标签会跟着同步渲染。在这里,ng-model 指令把输入域的值绑定到应用程序变量 param。angular会自动跟踪数据的变化并重新渲染html。

第三个例子,我们看看在angularJS中是怎么处理事件的

1
2
3
4
5
6
7
<div ng-app="">

<button ng-click="count = count + 1">点击!</button>

<p>点击次数:{{ count || 0}}</p>

</div>

这里我们看到,angularJS使用ng-click来绑定事件,你可能会有疑问,为什么把事件绑定写在html里?在传统的JS编写规则下,把事件写在标签上是不规范的,不好的体验。但我们会看到目前许多主流的框架都这么做,像React是用小驼峰

1
<button onClick=""></button>

vuejs是用

1
<button v-on:click=""></button>

所以原因是什么,我们回顾一下,传统的把事件绑定写在js里是为了让html更纯粹,能表达好结构并且不带其它杂质,不要看起来很乱,很难维护和JS调试,事件绑定这一逻辑层的东西自然要放在JS里。
那么在angular框架上,如果把事件写在JS里(也就是controller里,下文有controller更具体的描述)会怎样?最直接的它会导致controller依赖了view,这是一种反依赖。controller不允许访问dom元素,它应该只编写函数,然后让view中的事件去调用它。在html上写ng-bind属于行为定义,而行为描述写在controller里面,这是大框架下实现事件的正确姿势。

接下来我们尝试复杂一点的。考虑我们以往编写html的情况,在遇到诸如”ul”和多行”li”时容易产生冗余代码。看看我们在angularJS中如何巧妙地解决这样的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<html ng-app>
<head>
...
<script src="./lib/angular/angular.js"></script>
<script src="js/controllers.js"></script>
</head>
<body ng-controller="listCtrl">
<ul>
<li ng-repeat="item in list">
{{item.name}}
<p>{{item.age}}</p>
</li>
</ul>
</body>
</html>

在controllers.js中我们先写进静态数据,到了后面我们可以用拉取的sql数据来替换它。

1
2
3
4
5
6
7
8
9
10
function listCtrl($scope) {
$scope.list = [
{"name": "John",
"age": "20"},
{"name": "kitty",
"age": "14"},
{"name": "bob",
"age": "12"}
];
}

打开浏览器可以看到li成功渲染了出来。我们来看看这JS代码。

1
<body ng-controller="listCtrl">

指定了listCtrl这个控制器(你也可以理解为一个处理数据的函数),实例捕获到func listCtrl() 里面的list数组,然后通过

1
<li ng-repeat="item in list">

将其遍历并渲染出来,这也是html可运算化的一个体现。

到了这里,有个概念我们可能需要了解一下,一个叫MVVM的概念。MVVM是modal-view-view-model的缩写,相似的还有MVC(model-view-controller)。这三者的关系大概是这样。
一个项目中有很多个视图(view),它们负责显示给用户。像我们项目中常见的index.html,my.html,reg.html等等。它们是纯静态的,不包含动态数据。动态数据的渲染和处理是交给controller来做的,它们也只负责处理数据,你可以把控制器想象成是一些向后台拉取和处理数据的function。而model负责逻辑,从而架构起来了一个项目。这样做和以往传统的简单写代码有很大的区别。它有不少好处,它成功地解耦了视图和功能,更好地维护了项目,逻辑和分工也更为清晰。

mvc

MVC是架构一个大项目的很好的体验,我们来看看在angularJS中是如何使用这Model,view,controller的。

1
2
3
4
5
6
7
8
9
10
11
12
13
<body>
<div ng-app="myApp" ng-controller="myCtrl">
名字: <input ng-model="name">
<p>你输入了: {{name}}</p>
</div>

<script>
var app = angular.module('myApp', []);
app.controller('myCtrl', function($scope) {
$scope.name = "John Carter";
});
</script>
</body>

一个html是可以声明多个ng-app的,只需要给相应的变量名来区分。一个ng-app对应一个module(模块,不是model–模型)。模块下面,便是MVC三者的分工。
在这里,model负责逻辑,将name这个变量绑定到了ng-app中。controller负责数据,它初始化了name这个变量,而view(即html结构)负责显示的形式。当model里的name改变时,它通知改变给view让其重新渲染。

到目前为止,我们接触到了不少东西。这是大框架的魅力所在。angularJS是框架而又不仅仅是框架。它里面隐藏了很多理念,而这些你都可以活用到任何你想得到的项目细节当中。
学习大框架的最后一步,是尝试使用它来搭建一个项目。使用的是angularJS的route路由系统,可以自行了解一下。这里我们选择一个自己以往做过的常规的项目即可。假设它已经做到了良好的代码分区,像简单的html、css、js、php分块,它的项目目录如下:

1
2
3
4
/html
/css
/js
/php

然后我们先分离出html里的html文件的公共标签框架,将其拎出来独立作为一个新html文件,命名为frame.html。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<link rel="stylesheet" href="../css/frame.css">
</head>
<body ng-app="routingApp">

<div class="header">
<!-- header块 -->
</div>

<div class="wrap" ng-view></div> <!-- 需要针对不同页面进行渲染的模块 -->

<div class="footer">
<!-- footer块 -->
</div>
<script type="text/javascript" src="../Lib/angular/angular.js"></script>
<script src="../js/app.js"></script>
</body>
</html>

然后剩下的每个html页面就只需要保留

1
<div class="wrap"></div>

的那一块代码,头部和尾部甚至doctype声明都裁剪掉。angular会将这些html文档按需加载进来(需要开启localhost服务)。html块就算是完成了。
我们开始编写app.js文件,来控制不同页面的显示和数据处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
angular.module('routingApp', ['ngRoute'])
.controller('indexCtrl', function ($scope) {
// 这里是index页面的js
})
.controller('my', function ($scope) {
// 这里是my.html页面的js
})
.controller('result', function () {
// 这里是result.html页面的js
})
.controller('login', function () {
// 这里是login.html页面的js
})
.controller('reg', function () {
// 这里是reg页面的js
})
.config(function ($logProvider, $routeProvider) {
$logProvider.debugEnabled(true);

// 用angularJS路由来分配view和controller
$routeProvider
.when('/home', {
templateUrl: 'index.html',
controller: 'indexCtrl'
})
.when('/my', {
templateUrl: 'my.html',
controller: 'my'
})
.when('/result', {
templateUrl: 'result.html',
controller: 'result'
})
.when('/login', {
templateUrl: 'login.html',
controller: 'login'
})
.when('/reg', {
templateUrl: 'reg.html',
controller: 'reg'
})
.otherwise({redirectTo: '/home'});
});

最后css需要处理一下。我们可以用js来加载对应的css文件。简单的也可以直接在不同的view文件中引入。比如在index.html中加载index.css

1
2
3
4
<link rel="stylesheet" href="../css/index.css">
<div class="wrap">
<!-- ...... -->
</div>

大功告成,进入到目录下的frame.html#/home。即可看到项目。当然还需要一些完善。比如把ng-repeat、ng-model等加入到html当中。这些我们就不讨论了,按自己的兴趣去修改吧,相信做好后你就能很好地掌握angularJS了。

结尾再讲点细节处理

AngularJS 采用外部加载HTML文档的模式来实现路由,而成功地做到了单页面(single page)并提高了路由性能。不过页面闪频问题也跟着频繁出现。在应用的页面或者组件需要加载数据时,浏览器和Angular渲染页面都需要花费一定时间。这时间间隔可能很小,让人感觉不到区别;但也可能很长,导致让我们的用户看到了没有被渲染过的页面,甚至双大括号在解析前呈现给了用户。以下是解决该问题的一些小技巧:

  1. 使用ng-bind
    ng-bind是angular里面另一个内置的用于操作绑定页面数据的指令。我们可以使用ng-bind代替双大括号的形式绑定元素到页面上;使用ng-bind替代双大括号可以防止未被渲染的双大括号就展示给用户了,这会显得友好很多。
1
2
3
<div ng-app>
<p ng-bind="username"></p> // 替代<p>{{username}}</p>
</div>
  1. 使用ng-cloak
    ng-cloak指令是angular的内置指令,它的作用是隐藏所有被它包含的元素:

    1
    2
    3
    <div ng-cloak>
    <h1>Hello <span ng-bind="username || 'world'"></span></h1>
    </div>
  2. resolve
    在不同的页面之间使用routes时,我们有另外的方式防止页面在数据被完全加载到route之前被渲染。
    在route里使用resolve可以让我们在route被完全加载之前获取我们需要加载的数据。当数据被加载成功之后,路由就会改变而页面也会呈现给用户;数据没有被加载成功route就不会改变。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    angular.module('myApp', ['ngRoute'])
    .config(function($routeProvider) {
    $routeProvider
    .when('/account', {
    controller: 'AccountCtrl',
    templateUrl: 'views/account.html',
    resolve: {
    account: function($q) {
    var d = $q.defer();
    $timeout(function() {
    d.resolve({
    id: 1,
    name: 'Ari Lerner'
    })
    }, 1000);
    return d.promise;
    }
    }
    })
    });

resolve 项需要一个key/value对象,key是resolve依赖的名称,value可以是一个字符串(as a service)或者一个返回依赖的方法。

resolve is very useful when the resolve value returns a promise that becomes resolved or rejected.

当路由加载的时候,resolve参数里的keys可以作为可注入的依赖:

1
2
3
4
5
angular.module('myApp')
.controller('AccountCtrl',
function($scope, account) {
$scope.account = account;
});

我们同样可以使用resolve key传递$http方法返回的结果,as $http returns promises from it’s method calls:

1
2
3
4
5
6
7
8
9
10
11
12
13
angular.module('myApp', ['ngRoute'])
.config(function($routeProvider) {
$routeProvider
.when('/account', {
controller: 'AccountCtrl',
templateUrl: 'views/account.html',
resolve: {
account: function($http) {
return $http.get('http://example.com/account.json')
}
}
})
});

推荐定义一个独立的service的方式来使用resolve key,并且使用service来相应返回所需的数据(这种方式更容易测试)。要这样处理的话,我们需要创建一个service:

首先,看一下accountService,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
angular.module('app')
.factory('accountService', function($http, $q) {
return {
getAccount: function() {
var d = $q.defer();
$http.get('/account')
.then(function(response) {
d.resolve(response.data)
}, function err(reason) {
d.reject(reason);
});
return d.promise;
}
}
})

定义好service之后我们就可以使用这个service来替换上面代码中直接调用$http的方式了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
angular.module('myApp', ['ngRoute'])
.config(function($routeProvider) {
$routeProvider
.when('/account', {
controller: 'AccountCtrl',
templateUrl: 'views/account.html',
resolve: {
// We specify a promise to be resolved
account: function(accountService) {
return accountService.getAccount()
}
}
})
});