Single Page Application - AngularJS
最近半年做了不少单页面网站的开发,有静态也有动态的,主要基于 AngularJS(偶尔在我的 blog 中
用了 ractive)。对这种重前端应用就着迷了,优点是前后端分工明确,高效(异步请求数据)且用户体验较好(没有频繁的页面跳转)。
没有任何框架能够适应一切场景。SPA 也有缺点,比如应用复杂代码量增加时,单页面应用就会有内存泄露等棘手问题,还有 SEO 优化等等问题。
SPA 适应的场景,如小型静态应用,与后台频繁交互(但不涉及重大安全问题)的应用等,我认为都是适合单页面应用的。
SPA 与 RESTful API 非常合适,只要确定接口规范,前后端开发就能较为独立地进行,前端通过 mock 异步数据,后端则直接测试接口,在前期不用集成前后端一起调试。
在此总结一下自己在基于 AngualrJS 搭建 SPA 应用的过程中学到的东西。仅限于 AngularJS 1.x。
项目结构
1 | ./ |
应用框架
入口 - index.html
1 |
|
初始化 - app/app.js
1 | ; |
上面在初始化应用时添加了依赖 ngRoute ,这不是 AngularJS 原生自带的,需要单独引入 angular-route.js 文件,且必须保持与 angular.js 版本一致,否则会报错。
控制器 - *.controller.js
以 MainCtrl 为例
1 | ; |
常用命令
ng-repeat
ng-repeat 是一个非常好用的命令,类似 forEach,能够遍历数组或对象属性,循环创建 DOM 元素。
一个简单的例子:
1 | app.controller('SomeCtrl', function($scope) { |
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>
变成 <p>Hi</p>Hi
,然后浏览器上看到的就是<p>Hi<p>
.
AngualrJS 出于安全考虑对字符串做了过滤,但有时候我们希望输出没有实体化的 html 标签,这时候需要做一些工作。
1 | var someHtmlStr = '<p>Hi</p>'; |
$sce 服务中的 sce 是 ‘Stric Contextual Escaping’ 的缩写,即’严格的上下文模式’。
除了上面的方式,还可以写成自定义的 filter。
1 | app.filter('html2trusted', ['$sce', function ($sce) { |
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 | app |
1 | <p ng-repeat="item in items" on-finish-render="ngRepeatFinished">{{item}}</p> |
lazy-loading
在 ng-view 中渲染的 DOM 是不会解释其中包含的 script 标签的。这时候就需要用到写一个可以正常加载其中包含的 script 标签的指令。
1 | app.directive('script', function() { |
1 | <script type="text/javascript-lazy" src="somefile.js"></script> |
这样即使在模板中写 script 标签引入外部文件,就可以正常执行了。再查了一下,发现除了 script 标签,view, controller 等都可以 lazy loading,而且这个概念也不仅局限于 AngularJS.
概念理解
依赖注入
AngularJS 使用依赖注入提高控制器代码的灵活性,通过参数名匹配注入相应的功能或服务。比如一直写的
1 | app.controller('SomeCtrl', function ($scope, $http, ...) { |
参数列表中的 $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 属性,如 href
,src
等要使用 AngularJS 模板需要使用花括号包起变量,而对于 AngularJS 的属性,则直接使用,如 ng-if="someBoolean"
.
AngularJS 的作用域还可以嵌套,在父作用域中定义的属性,可以在子作用域中访问。
视图
ng-view 属性用于在页面的 hashtag 发生变化时,将对应的模板渲染到该属性对应的 DOM 容器中。视图可以嵌套,不过我还没遇到过这样的开发场景。
directive
这个可能是 AngularJS 最难学的部分了,属于高手进阶关卡。在开发中遇到问题时,总是能搜到各种各样的 directive 解决方案,然而并看不懂。
首先 directive 非常强大,可以做出自定义的一套标签库,还包括自定义属性等等。而 directive 就提供了解析和处理这些自定义标签/属性等的操作。