Skip to content

SPA with AngularJS #8

Description

@muxfe

Single Page Application - AngularJS

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

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

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

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

项目结构

./
├─ 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

<!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

'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 为例

'use strict';

angular.module('simpleApp').controller('MainCtrl', function($scope, $http) {
  // do something
});

常用命令

ng-repeat

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

app.controller('SomeCtrl', function ($scope) {
  $scope.items = [{name: 'xm'}, {name: 'kyer'}]
});
<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 的数据替换是在脚本加载完成以后进行的,所以如果写了类似

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

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

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

一些问题的解决方法

输出 html 标签

使用 {{someHtmlStr}} 或 ng-bind="someHtmlStr" 输出带有 html 标签的字符串时,
AngularJS 默认对其中的 html 标签做实体化转义,即将 <p>Hi</p> 变成 &lt;p&gt;Hi&lt;/p&gt;Hi,然后浏览器上看到的就是<p>Hi<p>.

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

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

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

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

app.filter('html2trusted', ['$sce', function ($sce) {
  return function (text) {
      return $sce.trustAsHtml(text);
  };
});
<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 事件。

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);
        });
      }
    }
  }
});
<p ng-repeat="item in items" on-finish-render="ngRepeatFinished">{{item}}</p>

lazy-loading

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

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();
      }
    }
  };
});
<script type="text/javascript-lazy" src="somefile.js"></script>

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

概念理解

依赖注入

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

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 属性,如 href,src等要使用 AngularJS 模板需要使用 {{}} 包起变量,而对于 AngularJS 的属性,则直接使用,如 ng-if="someBoolean".

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

视图

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

directive

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

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

我的相关项目

参考链接

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions