Single Page Application - AngularJS

Single Page Application - AngularJS

最近半年做了不少单页面网站的开发,有静态也有动态的,主要基于 AngularJS(偶尔在我的 blog
用了 ractive)。对这种重前端应用就着迷了,优点是前后端分工明确,高效(异步请求数据)且用户体验较好(没有频繁的页面跳转)。

没有任何框架能够适应一切场景。SPA 也有缺点,比如应用复杂代码量增加时,单页面应用就会有内存泄露等棘手问题,还有 SEO 优化等等问题。
SPA 适应的场景,如小型静态应用,与后台频繁交互(但不涉及重大安全问题)的应用等,我认为都是适合单页面应用的。

SPA 与 RESTful API 非常合适,只要确定接口规范,前后端开发就能较为独立地进行,前端通过 mock 异步数据,后端则直接测试接口,在前期不用集成前后端一起调试。

在此总结一下自己在基于 AngualrJS 搭建 SPA 应用的过程中学到的东西。仅限于 AngularJS 1.x。

项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
./
├─ src/
│ ├─ app/
│ │ ├─ [controller]/[.html, .controller.js, .css]
│ │ ├─ app.js
│ │ └─ app.controller.js
│ ├─ component/[controller]/[.html, .controller.js*, .css*]
│ ├─ fonts
│ ├─ index.html
│ ├─ 404.html
│ ├─ favicon.ico
│ └─ assets/[images, styles]
├─ dist/
├─ test/
├─ Gruntfile.js
├─ package.json
└─ README.md

应用框架

入口 - index.html

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
<!doctype html>
<html ng-app="simpleApp">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta charset="utf-8" />
<title></title>
<!-- disable caches for all browser -->
<meta http-equiv="cache-control" content="max-age=0" />
<meta http-equiv="cache-control" content="no-cache" />
<meta http-equiv="cache-control" content="no-store" />
<meta http-equiv="expires" content="0" />
<meta http-equiv="expires" content="Tue, 01 Jan 1980 1:00:00 GMT" />
<meta http-equiv="pragma" content="no-cache" />
<meta name="viewport" content="width=device-width" />
<!-- stylesheets start -->
<!-- stylesheets end -->
</head>
<body ng-controller="MainCtrl">
<div class="container">
<div ng-view></div>
</div>
<!-- scripts start -->
<!-- scripts end -->
</body>
</html>

初始化 - app/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
'use strict';
(function() {
var app = angular
.module('simpleApp', ['ngRoute'])
.filter('breakFilter', function() {
return function(text) {
if (!!text) {
return text.replace(/\n/g, '<br />');
}
};
})
.config(function($routeProvider, $compileProvider) {
$routeProvider
.when('home', {
templateUrl: 'app/home/home.html',
})
.when('somepage/somepath/:someparam?', {
templateUrl: 'app/somepage/somepage.html',
})
.otherwise({
redirectTo: '/home',
});
// 数据绑定写入 a.href 时的白名单过滤
$compileProvider.aHrefSanitizationWhitelist(/^(http|https|file|tel):/);
});
})();

上面在初始化应用时添加了依赖 ngRoute ,这不是 AngularJS 原生自带的,需要单独引入 angular-route.js 文件,且必须保持与 angular.js 版本一致,否则会报错。

控制器 - *.controller.js

以 MainCtrl 为例

1
2
3
4
'use strict';
angular.module('simpleApp').controller('MainCtrl', function($scope, $http) {
// do something
});

常用命令

ng-repeat

ng-repeat 是一个非常好用的命令,类似 forEach,能够遍历数组或对象属性,循环创建 DOM 元素。
一个简单的例子:

1
2
3
app.controller('SomeCtrl', function($scope) {
$scope.items = [{ name: 'xm' }, { name: 'kyer' }];
});
1
<p ng-repeat="item in items">{{item.name}}</p>

ng-if/ng-show

这两个命令的用法看起来很像,都是根据属性值的真假来决定是否显示该 DOM 元素,但是有一点很重要的区别。
ng-if 值为 false 时,不会创建该 DOM 元素,而 ng-show 值为 false 时,该元素依然会被创建,只是通过设置 css 属性 display: none; 使其不显示而已。

ng-src/ng-href

由于 AngularJS 的数据替换是在脚本加载完成以后进行的,所以如果写了类似

1
<img src="{{data.image}}" />

其中 img 标签的 src 值会在被替换前无法正确显示图片,因为根本不存在 data.image 路径的图片文件,这时候就需要 ng-src 命令了,它会在 AngularJS
替换完数据后为 img 标签添加 src 属性,这时候就是正确的值了。

同理,ng-href 也是解决在数据替换前,a 标签的不正确行为。

一些问题的解决方法

输出 html 标签

使用 \{\{someHtmlStr\}\}(注:双花括号与 hexo 模板解析冲突,故在此加\) 或 ng-bind="someHtmlStr" 输出带有 html 标签的字符串时,
AngularJS 默认对其中的 html 标签做实体化转义,即将 <p>Hi</p> 变成 &lt;p&gt;Hi&lt;/p&gt;Hi,然后浏览器上看到的就是<p>Hi<p>.

AngualrJS 出于安全考虑对字符串做了过滤,但有时候我们希望输出没有实体化的 html 标签,这时候需要做一些工作。

1
2
var someHtmlStr = '<p>Hi</p>';
$scope.someHtmlStr = $sce.trustAsHtml(someHtmlStr);

$sce 服务中的 sce 是 ‘Stric Contextual Escaping’ 的缩写,即’严格的上下文模式’。

除了上面的方式,还可以写成自定义的 filter。

1
2
3
4
5
app.filter('html2trusted', ['$sce', function ($sce) {
return function (text) {
return $sce.trustAsHtml(text);
};
});
1
<p ng-bind-html="someHtmlStr | html2trusted"></p>

ng-repeat 渲染完成事件监听

有时候在一些第三方库时,会要求所需的 DOMContent 已经渲染完成,但 ng-repeat 命令渲染 DOM 和后续 js 代码执行时并行的,
我们必须监听 ng-repeat 完成时的事件,然后再对相应的 DOM 初始化第三方库。

一个比较容易理解的例子就是轮播组件,轮播内容用 ng-repeat 命令输出,但是初始化轮播时都要计算一些元素的 width/height 属性等,
由于代码是异步执行的,一旦进行了数据绑定,ng-repeat 渲染就会开始,而往往我们执行 $("#slide").slide({}) 时渲染还未完成,
就会得到不正确的显示结果。所以,轮播初始化必须在 ng-repeat 渲染完成后执行。使用 AngularJS 自定义的属性标签可以实现监听 ngRepeatFinished 事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
app
.controller('SomeCtrl', function($scope) {
$scope.items = [1, 2, 3];

$scope.$on('ngRepeatFinished', function(ngRepeatFinishedEvent) {
// do something
});
})
.directive('onFinishRender', function($timeout) {
return {
restrict: 'A',
link: function(scope, element, attr) {
if (scope.$last === true) {
// $timeout 将触发事件的操作放到事件队列的最后,以保证已全部渲染完成
$timeout(function() {
scope.$emit(attr.onFinishRender);
});
}
},
};
});
1
<p ng-repeat="item in items" on-finish-render="ngRepeatFinished">{{item}}</p>

lazy-loading

在 ng-view 中渲染的 DOM 是不会解释其中包含的 script 标签的。这时候就需要用到写一个可以正常加载其中包含的 script 标签的指令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
app.directive('script', function() {
return {
restrict: 'E',
scope: false,
link: function(scope, elem, attr) {
if (attr.type === 'text/javascript-lazy') {
var s = document.createElement('script');
s.type = 'text/javascript';
var src = elem.attr('src');
if (src !== undefined) {
s.src = src;
} else {
var code = elem.text();
s.text = code;
}
document.body.appendChild(s);
elem.remove();
}
},
};
});
1
<script type="text/javascript-lazy" src="somefile.js"></script>

这样即使在模板中写 script 标签引入外部文件,就可以正常执行了。再查了一下,发现除了 script 标签,view, controller 等都可以 lazy loading,而且这个概念也不仅局限于 AngularJS.

概念理解

依赖注入

AngularJS 使用依赖注入提高控制器代码的灵活性,通过参数名匹配注入相应的功能或服务。比如一直写的

1
2
3
app.controller('SomeCtrl', function ($scope, $http, ...) {
// some code
});

参数列表中的 $scope, $http 等都是 AngularJS 原生的(前面有 $),参数列表的顺序可以是任意的,也不用列出所有可用的参数,只需选择自己用到的即可。

自己也可以定义服务、工厂函数等,AngularJS 的依赖注入机制提供了多种类型,包括

  • 值 - value
  • 工厂 - factory
  • 服务 - service
  • 提供者 - $provide
  • 常量 - constant

目前还没用过这些类型,都是自己在控制器里写相应的代码。看过一点前端的 MVC,即使用自定义工厂映射 RESTful API 对应的资源,通过对这些对象的修改,会直接 ajax 请求更新到后台,非常方便,值得学习使用。

数据绑定

AngularJS 最让人惊喜莫过于数据双向绑定了,对于表单的验证与提交,动态数据渲染 DOM 方面不能更好用了。对它的实现没有深入了解过多,
应该是用到了脏数据检查,将数据与 DOM 元素属性绑定,一旦数据修改了就更新,而且这种更新是双向的,DOM 内容可以直接修改 js 对象,js 对象值的修改也会直接反映到 DOM 中。 这块还要深入学习。

作用域

不论是前后端模板,在生成 html 内容时都有作用域的概念,如 JSP 中,通过将 <% %> 标签外的 html 代码变成字符串,标签内容代码生成 Java 代码,从而使得 JSP 页面内拥有了对应 Servlet 变量的作用域。

AngularJS 则通过 $scope 为页面模板绑定作用域,所有挂在 $scope 上的属性,可以直接在页面中使用。其中对于非 AngularJS 属性,如 hrefsrc等要使用 AngularJS 模板需要使用花括号包起变量,而对于 AngularJS 的属性,则直接使用,如 ng-if="someBoolean".

AngularJS 的作用域还可以嵌套,在父作用域中定义的属性,可以在子作用域中访问。

视图

ng-view 属性用于在页面的 hashtag 发生变化时,将对应的模板渲染到该属性对应的 DOM 容器中。视图可以嵌套,不过我还没遇到过这样的开发场景。

directive

这个可能是 AngularJS 最难学的部分了,属于高手进阶关卡。在开发中遇到问题时,总是能搜到各种各样的 directive 解决方案,然而并看不懂。

首先 directive 非常强大,可以做出自定义的一套标签库,还包括自定义属性等等。而 directive 就提供了解析和处理这些自定义标签/属性等的操作。

我的相关项目

参考链接

打赏