标签 angular 下的文章

让我们通过本文从 0 开始使用 TypeScript 创建一个 Angular 2 应用。

先看看运行效果?

如果想先看看运行效果,可以点击访问我们在 Plunker 上面准备的示例,应用其实做的事情很简单,打开页面之后,开始加载程序代码,当程序代码加载完成之后,在页面中显示应用组件,该组件包含 My First Angular 2 App 字符。

其文件目录结构如下:

angular2-quickstart
|-app/
|-|-app.component.ts
|-|-main.ts 
|
|-index.html
|-license.md

功能文件包括一个 index.html 文件以及 app/ 目录下面的两个 TypeScript 文件。

当然了,我们开发不仅仅只是想在 Plunker 上面可以运行就可以了的,而是需要做一个真正的应用,包括:

  1. 配置我们的开发环境
  2. 创建 Angular 应用的根组件
  3. 启动它以让其接管整个页面
  4. 创建主页面 index.html

开发环境

我们首先需要一个地方存储整个应用的程序文件(应用项目目录),一些 TypeScript 配置以及一些开发与运行时需要的库。

创建新的项目目录

mkdir angular2-quickstart
cd angular2-quickstart

配置 TypeScript

我们必须要对 TypeScript 编译器进行一上结特殊的设置。

在项目目录下面添加一个名为 tsconfig.json 的文件,并复制/粘贴以下代码:

{
  "compilerOptions": {
    "target": "es5",
    "module": "system",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "noImplicitAny": false
  },
  "exclude": [
    "node_modules",
    "typings/main",
    "typings/main.d.ts"
  ]
}

TypeScript Typings

许多 JavaScript 库对 JavaScript 的功能与语法进行了扩展,而这些扩展很多是 TypeScript 编译器本身不识别的,我们需要通过 TypeScript 类型定义文件 —— d.ts 教会编译器如何编译这些扩展,在项目目录下面新建一个名为 typings.json 的文件,编辑复制并粘贴下面的代码片段:

{
  "ambientDependencies": {
    "es6-shim": "github:DefinitelyTyped/DefinitelyTyped/es6-shim/es6-shim.d.ts#6697d6f7dadbf5773cb40ecda35a76027e0783b2"
  }
}

添加项目必要的第三方库

我们推荐使用 npm 包管理工具管理第三方库。在项目目录中添加一个名为 package.json 的文件,复制并粘贴下面的代码片段:

{
  "name": "angular2-quickstart",
  "version": "1.0.0",
  "scripts": {
    "postinstall": "npm run typings install",
    "tsc": "tsc",
    "tsc:w": "tsc -w",
    "lite": "lite-server",
    "start": "concurrent \"npm run tsc:w\" \"npm run lite\" ",
    "typings" : "typings"
  },
  "license": "ISC",
  "dependencies": {
    "angular2": "2.0.0-beta.6",
    "systemjs": "0.19.20",
    "es6-promise": "^3.0.2",
    "es6-shim": "^0.33.3",
    "reflect-metadata": "0.1.2",
    "rxjs": "5.0.0-beta.0",
    "zone.js": "0.5.14"
  },
  "devDependencies": {
    "concurrently": "^1.0.0",
    "lite-server": "^2.0.1",
    "typescript": "^1.7.5",
    "typings":"^0.6.8"
  }
}

然后在项目目录中执行下面的命令以安装必要的第三方库:

npm install

第一个 Angular 组件

组件(Component) 是 Angular 最基本的概念,组件管理视图——用于显示网页内容或者对用户的反馈作出响应,技术上讲,一个组件就是一个类或者一个视频模板,我们在创建 Angular 应用的过程中,就是创建很多个组件的过程。

创建一个子目录

我们希望整个应用的代码都保存在项目根目录下名为 app/ 的子目录下,在终端中执行下面的命令即可:

mkdir app
cd app

创建组件文件

现在,添加一个名为 app.component.ts 的文件,复制并粘贴以下代码:

import { Component } from 'angular2/core';

@Component({
  selector: 'my-app',
  template: '<h1>我的第一个 Angular 2 应用</h1>'
})
export class AppComponent {

}

组件类(Component class)

在该文件的最下方,我们添加了一个空的,不做什么事情的名为 AppComponent 的类,当我们需要去做一个具有实质性功能的应用时,可以去扩展该类,但是在快速入门这个项目中,它不需要做任何事情。

模块

Angular 应用是模块化的,它们会使用很多个不同的模块去实现特定的功能,绝大多数应用的文件都会导出一个事物,比如一个组件,我们的 app.component 文件导出了 AppComponent 类。

export class AppComponent {

}

导出使得这个文件变成了一个模块,而这个文件的名称一般就是模块的名称,如上所述, app.component 就是我们的第一个模块。

表单是任何一个应用的基石,在 Angular 2 中,表单的实现作了不少改变。

曾经在 Angular 1 中,我们使用 ngModel 将表单数据映射至应用的数据模型,但是在 Angular 2,我们可以使用表单控件更加明确地构建表单,虽然看上去,相比于 Angular 1,我们需要写更多的代码,但是却不可以让我们不再需要去解决那些烦人的 ngModel 声明与数据绑定问题了。

一个简单的表单示例

<form [ngFormModel]="loginForm" (submit)="doLogin($event)">
  <input ngControl="email" type="email" placeholder="Email" />
  <input ngControl="password" type="password" placeholder="Password"| />
  <button type="submit">Log in</button>
</form>

与其相应的组件 JS 代码如下:

import {Component, FormBuilder, Validators} from 'angular2/angular2'

@Component({
  selector: 'login-page',
  templateUrl: 'login-page.html'
})
export class LoginPage {
  constructor (fb: FormBuilder) {
    this.loginForm = fb.group({
      email: ["", Validators.required],
      password: ["", Validators.required]
    });
  }

  doLogin(event) {
    console.log(this.loginForm.value);
    event.preventDefault();
  }
}

当我们运行该应用时,浏览器中会展示如下的登录表单

    +--------------------+
    | Email              |
    +--------------------+

    +--------------------+
    | Password           |
    +--------------------+

    +--------------------+
    |      Log in        |
    +--------------------+

表单构建器(FormBuilder)

在上面的示例中,我们使用了 FormBuilder,它可以让我们可以快速方便的定义表单控件以及每一个控件需要绑定的验证器,在上面示例中,我们创建了两个文本输入框 emailpassword

this.loginForm = fb.group({
  email: ["", Validators.required],
  password: ["", Validators.required]
})

控件组(ControlGroup)

FormBuilder 创建的就是 ControlGroup 实例,我们称之为 form,除了使用 FormBuilder 之外,我们还可以直接手工构建 ControlGroup

this.loginForm = new ControlGroup({
  email: new Control("email", Validators.required),
  password: new Control("password", Validators.required)
});

但是在实际的开发中,使用 FormBuilder 会方便很多。

表单指令 (For,m Directives)

上面的示例中,你应该已经注意到了,没有使用一个 ngModel 声明,但是我们使用 ngControl 将表单中的值映射至控件对象中。

<input ngControl="email" type="email" placeholder="Email" />

这将 email 输入框 ”绑定“ 至了 email 控件。

自定义验证器

我们可以创建自定义的验证器,它就是一个简单的函数:

function containsMagicWord(c: Control) {
  if(c.value.indexOf('magic' >= 0) {
    return {
      noMagic: true
    }
  }
  
  // Null 表示验证通过
  return null;
}

this.loginForm = fb.group({
  email: ["", containsMagicWord],
  password: ["", Validators.required],
});

处理表单数据

我们可以通过很方便的访问表单对应的 JavaScript 对象,或者某一个控件的值:

doLogin(event) {
  // 展示表单的数据
  var formData = this.loginForm.value;
 
  // 或者获取控件的值
  var email = this.loginForm.controls.email.value;

  event.preventDefault();
}

在 Angular 2 中,使用括号 () 绑定事件,并触发组件类中相应的方法,比如:

@component(...)
class MyComponent {
  clicked(event) {

  }
}

模板如下:

<button (click)="clicked()">点击我</button>

委托

Angular 2 采用普通 DOM 事件一样的机制处理事件,它们同样可以冒泡,我们不需要做什么特别的工作。

事件对象

要访问事件对象,只需要将 $event 作为事件出发的​参数即可。

@Component(...)
class MyComponent {
  clicked(event) {
    console.log(event(
  }
}

模板如下:

<button (click)="clicked($event)">点击我</button>

Angular 2 应用的生命周期需要经过一个多级的启动过程,我们可以在 App 启动、运行与创建/销毁组件的过程中响应大家的事件。

启动

Angular 2 应用需要通过应用的 根组件 (root component) 启动,在我们的主 JS 文件中,我们可以像下面这样写:

import {Component, bootstrap} from 'angular2/angular2'

// 注解部分
@Component({
  selector: 'my-app',
  template: '<h1>你好,{{ name }}</h1>'
})
// 组件控制器
class MyApp {
  constructor() {
    this.name = '潘韬'
  }
}

bootstrap(MyApp)

该代码片段中,你可以添加应用级别的代码与配置等,它的模板将是其它所有组件的容器。

组件初始化

当一个组件创建成功之后,它的构建函数 (constructor) 将被调用,在该函数中,你可以执行组件状态的初始化工作,但是如果应用依赖子组件的属性与数据的话,那么需要先等待子组件先完成初始化。要实现这样的功能,只需要使用生命周期事件 ngOnInit 即可,我们可以在 constructor 中使用 setTimeout 来模拟出相似的效果。

import {Component, bootstrap} from 'angular2/angular2'

@Component({
  selector: 'street-map',
  template: '<map-window></map-window><map-controls></map-controls>'
})
class StreetMap {
  constructor() {
    this.name = '潘韬'
  }

  setMapWindow(mapWindow) {
    this.mapWindow = mapWindow;
  }

  setMapControls(mapControls) {
    this.mapControls = mapControls;
  }

  ngOnInit() {
    // Properties are resolved and things like
    // this.mapWindow and this.mapControls
    // had a chance to resolve from the
    // two child components <map-window> and <map-controls>
  }
}

组件的生命周期

就像 ngOnInit 一样,我们可以在一个组件的生命周期中跟踪多个事件,下面列举出了部分常见的事件,查看完整的 Angular 2 生命周期事件钩子,可以查看官方文档

import {Component} from 'angular2/angular2'

@Component({
  selector: 'street-map',
  template: '<map-window></map-window><map-controls></map-controls>'
})
class StreetMap {
  constructor() {
    this.name = '潘韬';
  }

  ngOnInit() {

  }

  ngOnDestroy() {

  }

  ngOnCheck() {

  }

  ngOnChanges(changes) {

  }

  ngAfterContentInit() {

  }

  ngAfterContentChecked() {

  }

  ngAfterViewInit() {

  }

  ngAfterViewChecked() {

  }
}

在 Angular 2 中,组件(Component)是创建页面元素与实现业务逻辑的主要方式,与之相对应的,在 Angular 1 中,我们通过 directivecontrollersscope 等技术去实现,但是在 Angular 2 中,所有前面的这些实现都被 组件 取代。

一个最简单的 Angular 2 组件示例

下面的这个组件将展示出我的姓名,然后带有一个按钮,当按钮被点击时,将在浏览器的 console 中打印出我的名字。

import {Component} from 'angular2/angular2'

@Component({
  selector: 'my-component',
  template: '<div>大家好,我的名字叫 {{ name }},<button (click)="sayMyName()">叫一声我的名字</button>'
})
export class MyComponent({
  constructor() {
    this.name = '潘韬'
  }

  sayMyName() {
    console.log('我的名字叫', this.name)
  }
})

当我们在 HTML 模板中使用 `' 标签时,该组件将被创建。

Ionic V2 与 Ionic V1 一样提供了 CLI 工具与 GUI 的工具。

安装 Ionic V2

安装 Ionic 2,可以使用下面的命令:

npm install -g ionic@beta
完全不需要担心 Ionic V1 版本的项目,Ionic@Beta 可以完全兼容 Ionic V1 的项目。

安装完成之后可以使用下面的命令创建新项目:

ionic start cutePuppyPics --v2

运行新创建的项目, cd 进入项目目录之后,运行 ionic serve 命令即可:

cd cutePuppyPics
ionic serve

执行之后,即可像 Ionic v1 一样在浏览器中查看项目了。

构建

要构建 Ionic 项目,需要先安装 cordova

sudo npm install -g cordova

iOS 构建

ionic platform add ios
你需要先安装 XCodeXCode 允许你直接为 iOS 系统的目标设备构建应用。添加了 iOS 系统之后,即可使用下面的命令运行模拟器:
ionic emulate ios

Android 构建

ionic platform add android

接下来你还需要安装 Android SDK ,Android SDK 允许你为 Android 目录设备构建应用,虽然 Android SDK 本身就带了一个模拟器,但是更加推荐你使用 Genymotion

ionic run android

迁移

Ionic 1.x 是基于 Angular 1.x 技术的,同样的 Ionic 2.x 基于 Angular 2.x,所以,虽然 Ionic 本身的理念没有太多改变,但是代码的写法也因为 Angular 的改变而变得很不一样,Angular 2.x 使用了完全不一样的语法与代码结构(要了解 Angular 2.x 的变化,可以查看 学习 Angular 2 这个网站。

下面这个是 Ionic 1.x 中的写法:

.config(function($stateProvider) {
  $stateProvider
  .state('main', {
    url: '/',
    templateUrl: 'templates/main.html',
    controller: 'MainController'
  })
})

.controller('MainController', function() {

})

现在在 Ionic 2.x 中可以像下面这样写:

@Page({
  templateUrl: 'main/main.html'
})

export class MainCmp {
  constructor() {

  }
}

从 Angular 1.x 迁移

ControllerAs 语法是一个 Angular 1.x 提供的功能,它可以我们不需要使用 $scope 变量即可做到数据绑定,而是将数据直接绑定至 Controller 上,在 Angular 2.x 中,ControllerAs 的实现更加简单了,对比下面是 Angular 1.x 实现:

index.html

<ion-content ng-controller="MainController">
  <ion-item>
    {{ data.text }}
  </ion-item>
</ion-content>

app.js

.controller('MainController', function($scope) {
  $scope.data = {
    text: 'hello world'
  }
})

将上面的示例改成 ControllerAs 语法只需要改变很小的一点代码:

index.html

<ion-content ng-controller="MainController as main">
  <ion-item>
    {{ data.text }}
  </ion-item>
</ion-content>

app.js

.controller('MainController', function() {
  this.data = {
    text: 'Hello World'
  }
})

TypeScript

TypeScript 是一个提供了 ES6 类 与类型注释的 JavaScript 超集 ,这使得我们可以在 Ionic 项目中按照 ES6 的标准来写。

app.js

.controller('MainController', function() {
  this.data = {
    text: 'Hello World'
  }
})

app.ts

export class MainController {
  constructor() {
    this.data = {
      text: 'Hello World'
    }
  }
}

项目结构

在 Angular 1.x 中,最好的项目实践是将所有的 JavaScript 脚本都放在一起,模板文件也放在一起,但是他们两者却是分开的,比如下面这样:

|-www/
|
|--js/
|--|-app.js
|--|-HomeController.js
|--|-DetailController.js
|
|--templates/
|--|-Home.html
|--|-Detail.html
|
|-index.html

但是在 Angular 2.x 中,推荐像下面这样的:

|-www/
|
|--Home/
|--|-HomeController.js
|--|-Home.html
|
|--Detail/
|--|-DetailController.js
|--|-Detail.html
|
|-index.html
|-app.js

doraemoney-loan-app.jpg

6月14号,和另外两个同事商量着不能再像最近这几个月这样了,似乎每一个公司的产品经理与码农们都是死对头,我也没有逃出这个怪圈,每天在对产品的“精雕细琢”中,让我对产品越发的反感,不经意间,看了看自己的 Git Commits List,好长啊,每天都有好多,然后就想着看看自己的干了些什么,突然之间,发现这就是一个循环啊,基本上是下面这样的:

for var keepGoing = true; keepGoing  {
    // 4B中
}

不行啊,我们得自己整一个,但是不能在上班时间整,因为这是一个只有我们参与的事情,而且也不希望他人对我们的指指点点,所以,决定每天的空余时间抽出几个小时,计划着一个星期之内整一个新的东西出来,恩,是的,App,最后还是花了我们3个人十天的时间。

这还是一个借款给有需要的人的App,没有风控模型,但是我们有完善的催债模型和真实性模型,我们只做一件事情,让借款给你的人更快的相信你在按时还款,所以,我们选择了通讯录、通话记录、地理位置、手机型号等这些通过一个App能快速获取到的数据。

然后我们开始了规划,简单的设计,接口的定义以及数据结构的定义,这花了一天的时间,我们按着花了三天时间把整个系统开发完了,也就是所有的功能都有了,接着花了两天时间把所有的功能接口串连上,最后再连了四天的时间测试、调试与Bug修复,Ok,一个全新的App就这么出来了。

技术使用的是:

  • Java
  • Ionic
  • Cordova
  • 一些必要的插件

Java

选择Java的原因很简单,也很纯粹,我们的核心业务系统就是Java的,为了能更快速的开发,我们还是直接使用Java,这样很多接口的代码可以直接复制过来改改就能使用,这为我们节省了很多开发的时间。

Ionic

这个不用想,简单的App开发中的神器,有了这个东西,即使我对App开发一无所知,我也能仅使用我自己会的前端技术实现一个完善的App。

Cordova

这为我们的App兼容到各种平台 iOA/Andoird等提供支持。

我是怎么做的

关于本地的数据存储

因为数据量很少,所以直接使用了 LocalStorage,我自己写了一个 AngularJSLocalStorage 的数据绑定的 Angular Module,代码如下:

/**
 * 本地存储
 */
app.factory('$storage', [
  '$rootScope',
  '$window',
  function(
      $rootScope,
      $window
  ){
    // #9: Assign a placeholder object if Web Storage is unavailable to prevent breaking the entire AngularJS app
    var webStorage = $window['localStorage'] || (console.warn('This browser does not support Web Storage!'), {}),
        storage = {
          $default: function(items) {
            for (var k in items) {
              angular.isDefined(storage[k]) || (storage[k] = items[k]);
            }

            return storage;
          },
          $reset: function(items) {
            for (var k in storage) {
              '$' === k[0] || delete storage[k];
            }

            return storage.$default(items);
          }
        },
        _laststorage,
        _debounce;

    for (var i = 0, k; i < webStorage.length; i++) {
      // #8, #10: `webStorage.key(i)` may be an empty string (or throw an exception in IE9 if `webStorage` is empty)

      (k = webStorage.key(i)) && 'storage-' === k.slice(0, 8) && (storage[k.slice(8)] = angular.fromJson(webStorage.getItem(k)));
    }

    _laststorage = angular.copy(storage);

    $rootScope.$watch(function() {
      _debounce || (_debounce = setTimeout(function() {
        _debounce = null;

        if (!angular.equals(storage, _laststorage)) {
          angular.forEach(storage, function(v, k) {
            angular.isDefined(v) && '$' !== k[0] && webStorage.setItem('storage-' + k, angular.toJson(v));

            delete _laststorage[k];
          });

          for (var k in _laststorage) {
            webStorage.removeItem('storage-' + k);
          }

          _laststorage = angular.copy(storage);
        }
      }, 100));
    });

    // #6: Use `$window.addEventListener` instead of `angular.element` to avoid the jQuery-specific `event.originalEvent`
    'localStorage' === 'localStorage' && $window.addEventListener && $window.addEventListener('storage', function(event) {
      if ('storage-' === event.key.slice(0, 10)) {
        event.newValue ? storage[event.key.slice(10)] = angular.fromJson(event.newValue) : delete storage[event.key.slice(10)];

        _laststorage = angular.copy(storage);

        $rootScope.$apply();
      }
    });

    return storage;
  }
]);

使用起来很简单:

$storage.token = 'TOKEN_STRING'; // 这就会在localStorage 中存储一个 `key` 为 `storage-token` 而 `value` 为 `TOKEN_STRING` 的键值对,这是一个单向存储的过程,也就是我们再手工修改 `localStorage` 里面的值是没有用的,`100ms` 之后就会被 `$storage.token` 的值覆盖,这是一个更新存储的时间。

数据请求

因为我们这边的接口走的不是 AngularJS 的默认请求方式,数据结构为类似表单提交,所以,我还修改了 Angular 中的 $http,转换对象为 x-www-form-urlencoded 序列代的字符串:

/**
 * 配置
 */
app.config([
  '$ionicConfigProvider',
  '$logProvider',
  '$httpProvider',
  function(
      $ionicConfigProvider,
      $logProvider,
      $httpProvider
  ) {
    // .. 其它代码
    // 开启日志
    $logProvider.debugEnabled(true);

    /**
     * 服务器接口端要求在发起请求时,同时发送 Content-Type 头信息,且其值必须为: application/x-www-form-urlencoded
     * 可选添加字符编码,在此处我默认将编码设置为 utf-8
     *
     * @type {string}
     */

    $httpProvider.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8';
    $httpProvider.defaults.headers.put['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8';

    /**
     * 请求只接受服务器端返回 JSON 数据
     * @type {string}
     */
    $httpProvider.defaults.headers.post['Accept'] = 'application/json';

    /**
     * AngularJS 对默认提交的数据结构为 json 格式的,但是我们NiuBilitity的服务器端不能解析 JSON 数据,所以
     * 我们经 x-www-form-urlencoded 的方式提交,此处将对数据进行封装为 foo=bar&bar=other 的方式
     * @type {*[]}
     */
    $httpProvider.defaults.transformRequest = [function(data) {
      /**
       * 转换对象为 x-www-form-urlencoded 序列代的字符串
       * @param {Object} obj
       * @return {String}
       */
      var param = function(obj) {
        var query = '';
        var name, value, fullSubName, subName, subValue, innerObj, i;

        for (name in obj) {
          value = obj[name];

          if (value instanceof Array) {
            for (i = 0; i < value.length; ++i) {
              subValue = value[i];
              fullSubName = name + '[' + i + ']';
              innerObj = {};
              innerObj[fullSubName] = subValue;
              query += param(innerObj) + '&';
            }
          } else if (value instanceof Object) {
            for (subName in value) {
              subValue = value[subName];
              fullSubName = name + '[' + subName + ']';
              innerObj = {};
              innerObj[fullSubName] = subValue;
              query += param(innerObj) + '&';
            }
          } else if (value !== undefined && value !== null) {
            query += encodeURIComponent(name) + '='
                + encodeURIComponent(value) + '&';
          }
        }

        return query.length ? query.substr(0, query.length - 1) : query;
      };

      return angular.isObject(data) && String(data) !== '[object File]'
          ? param(data)
          : data;
    }];

  }
]);

JSON 请求数据结构

我们的数据结构是下面这样的:

Request

{
  "apiVersion" : "0.0.1",
  "token" : "TOKEN_STRING",
  "requestId" : "ID_STRING",
  "data" : {
    // Data goes here
  }
}

Response

{
  "apiVersion" : "0.0.1",
  "data" : {},
  "error" : {
    "code" : ERROR_CODE_NUMBER,
    "message" : "Error Message Here",
    "errors" : [
      {
        "code" : 0,
        "message" : "",
        "location" : ""
      }
    ]
  }
}

说明

在上面的这些数据结构中,请求的很好理解,响应的 json 结构只有三个字段, apiVersion 表示了当前请求的接口版本号, data 就是数据对象, error 则是错误对象,一般情况下,一个 error 只有 codemessage 两个值,但是有一些情况下可能会需要提供一些额外的错误信息,那么都放入了 error.errors 这个数组中。

App前端是下面这样的判断的:

  1. errornull 时,表示请求成功,此时从 data 中取数据;
  2. error 不为 null 时,表示请求失败,此时从 error 中取错误信息,而完全不管 data ,我采取的方式是直接抛弃(其实前后端已经约定了,所以不存在 error 不为 null 时,data 中还有数据的情况出现。

关于 $http

我没有直接将接口的 url 地址、$http 请求等暴露给 Controller,而是做了一层封装,我叫作为 sack(也就是 App 的名称):

app.factory('sack', [
  '$http',
  '$q',
  '$log',
  '$location',
  '$ionicPopup',
  '$storage',
  'API_VERSION',
  'API_PROTOCOL',
  'API_HOSTNAME',
  'API_URI_MAP',
  'util',
  function(
      $http,
      $q,
      $log,
      $location,
      $ionicPopup,
      $storage,
      API_VERSION,
      API_PROTOCOL,
      API_HOSTNAME,
      API_URI_MAP,
      util
  ){
    var HTTPUnknownError = {code: -1, message: '出现未知错误'};
    var HTTPAuthFaildError = {code: -1, message: '授权失败'};
    var APIPanicError = {code: -1, message: '服务器端出现未知错误'};
    var _host = API_PROTOCOL + '://' + API_HOSTNAME + '/',
        _map = API_URI_MAP,
        _apiVersion = API_VERSION,
        _token = (function(){return $storage.token;}()) ;

    setInterval(function(){
      _token = (function(){return $storage.token;}());
      //$log.info("Got Token: " + _token);
    }, 1000);

    var appendTransform = function(defaultFunc, transFunc) {
      // We can't guarantee that the default transformation is an array
      defaultFunc = angular.isArray(defaultFunc) ? defaultFunc : [defaultFunc];

      // Append the new transformation to the defaults
      return defaultFunc.concat(transFunc);
    };

    var _prepareRequestData = function(originData) {
      originData.token = _token;
      originData.apiVersion = _apiVersion;
      originData.requestId = util.getRandomUniqueRequestId();
      return originData;
    };

    var _prepareRequestJson = function(originData) {
      return angular.toJson({
        apiVersion: _apiVersion,
        token: _token,
        requestId: util.getRandomUniqueRequestId(),
        data: originData
      });
    };

    var _getUriObject = function(uon) {
      // 若传入的参数带有 _host 头
      if((typeof uon === 'string' && (uon.indexOf(_host) == 0) ) || uon === '') {
        return {
          uri: uon.replace(_host, ''),
          methods: ['post']
        };
      }

      if(typeof _map === 'undefined') {
        return {
          uri: '',
          methods: ['post']
        };
      }

      var _uon = uon.split('.'),
          _ns,
          _n;

      if(_uon.length == 1) {
        return {
          uri: '',
          methods: ['post']
        };
      }
      _ns = _uon[0];
      _n = _uon[1];

      _mod = _map[_ns];

      if(typeof _mod === 'undefined') {
        return {
          uri: '',
          methods: ['post']
        };
      }

      _uriObject = _mod[_n];

      if(typeof _uriObject === 'undefined') {
        return {
          uri: '',
          methods: ['post']
        };
      }

      return _uriObject;
    };

    var _getUri = function(uon) {
      return _getUriObject(uon).uri;
    };

    var _getUrl = function(uon) {
      return _host + _getUri(uon);
    };

    var _auth = function(uon) {
      var _uo = _getUriObject(uon),
          _authed = false;
      $log.log('Check Auth of : ' + uon);
      $log.log('Is this api need auth: ' + angular.toJson(_uo.needAuth));
      $log.log('Is check passed: ' + angular.toJson(!(!_token && _uo.needAuth)));
      $log.log('Token is: ' + _token);
      if(!_token && _uo.needAuth) {
        
        $ionicPopup.alert({
          title: '提示',
          subTitle: '您当前的登录状态已失效,请重新登录。'
        }).then(function(){
          $location.path('/sign');
        });
        
        $location.path('/sign');
      } else {
        _authed = true;
      }
      return _authed;
    };

    var get = function(uon) {
      return $http.get(_getUrl(uon));
    };

    var post = function(uon, data, headers) {
      var _url = _getUrl(uon),
          _data = _prepareRequestData(data);
      $log.info('========> POST START [ ' + uon + ' ] ========>');
      $log.log('REQUEST URL  : ' + _url);
      $log.log('REQUEST DATA : ' + angular.toJson(_data));

      return $http.post(_url, _data, {
        transformResponse: appendTransform($http.defaults.transformResponse, function(value) {
          $log.log('RECEIVED JSON : ' + angular.toJson(value));
          if(typeof value.ex != 'undefined') {
            return {
              error: APIPanicError
            };
          }
          return value;
        })
      });
    };

    var promise = function(uon, data, headers) {
      var defer = $q.defer();

      if(!_auth(uon)) {
        defer.reject(HTTPAuthFaildError);
        return defer.promise;
      }

      post(uon, data, headers).success(function(res){
        if(res.error) {
          defer.reject(res.error);
        } else {
          defer.resolve(res.data);
        }
      }).error(function(res){
        defer.reject(HTTPUnknownError);
      });
      return defer.promise;
    };

    var postJson = function(uon, data, headers) {
      var _url = _getUrl(uon),
          _json = _prepareRequestJson(data);
      $log.info('========> POST START [ ' + uon + ' ] ========>');
      $log.log('REQUEST URL  : ' + _url);
      $log.log('REQUEST JSON : ' + _json);
      return $http.post(_url, _json, {
        transformResponse: appendTransform($http.defaults.transformResponse, function(value) {
          $log.log('RECEIVED JSON : ' + angular.toJson(value));
          if(typeof value.ex != 'undefined') {
            return {
              error: APIPanicError
            };
          }
          return value;
        })
      });
    };

    var promiseJson = function(uon, data, headers) {
      var defer = $q.defer();

      if(!_auth(uon)) {
        defer.reject(HTTPAuthFaildError);
        return defer.promise;
      }

      postJson(uon, data, headers).success(function(res){
        if(res.error) {
          defer.reject(res.error);
        } else {
          defer.resolve(res.data);
        }
      }).error(function(res){
        defer.reject(HTTPUnknownError);
      });
      return defer.promise;
    };

    return {
      get: get,
      post: post,
      promise: promise,

      postJson: postJson,
      promiseJson: promiseJson,
      _auth: _auth,
      HTTPAuthFaildError: HTTPAuthFaildError
    };
  }
]);

这样里面最主要是使用一个方法: sack.promiseJson,这个方法是以 json 数据向服务器发送请求,然后返回一个 promise 的。

上面的 API_URI_MAP 的数据结构类似于下面这样的:

app.constant('API_URI_MAP', {
  user : {
    sign : {
      needAuth: false,
      uri : 'sack/user/sign.json',
      methods: [
        'post'
      ],
      params: {
        mobile: 'string', // 手机号码
        captcha: 'string' // 验证码
      }
    },
    unsign: {
      needAuth: true,
      uri: 'sack/user/unsign.json',
      methods: [
        'post'
      ],
      params: {
        token: 'string'
      }
    },
    //...
  }
  //...
});

然后,更具体的,在 Controller 中也不直接使用 sack.promiseJson 这个方法,而是使用封装好的服务进行,比如下面这个服务:

app.factory('UserService', function($rootScope, $q, $storage, API_CACHE_TIME, sack) {
  var sign = function(data) {
    return sack.promiseJson('user.sign', data);
  };

  return {
    sign: sign
  }
});

这样的好处是,我可以直接使用类似下面这样发起请求:

UserService.sign({mobile:'xxxxxxxxxxx',captcha:'000000'}).then(function(res){
  // 授权成功
}, function(err){
  // 授权失败
});

但是

好吧,又来但是了,App做完了之后,我们可爱的领导们感觉这个还可以,然后就又要开始发挥他们的各种NB的指导了,还好从一开始我们就没有使用上班时间,这使得我们有理由拒绝领导的指导,但是,公司却说了,不接受指导那就不让上,好吧,那就不上呗,这似乎惹怒了我们的领导们,所以,就直接没有跟我们通气的开始招兵买马要上App了,我瞬间就想问:

我们的战略不是说不做App么?现在怎么看到App比现在的简单就又开始做了

然后我又想到一种可能

  1. 我们把App上了,
  2. 另一个领导带招一些新人把也做了一个App
  3. 如果App还可以的话,把我们的功能直接复制过去,然后让我们的下线
  4. 然后领导又可以邀功了
  5. 如果App不可以的话,那我们是在浪费时间,把我们的下线,然后……

反正,似乎都跟我没半毛钱关系了,除非这个App运营的不好。