分类 文章 下的文章

让我们通过本文从 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 中的模板与 Angular 1 中的模板非常类似,但是还是有了很多的小改进。

简单示例

下面这个示例展示了一个 Angular 2 模板,它展示出我的姓名以及我喜欢的事物。

<div>
  您好,我叫 {{ name }} ,我喜欢 {{ thing }}。
</div>

{{}} :渲染

要渲染出某一个变量的值 ,我们可以使用双大括号 {{}}

我的名字叫 {{ name }}

在渲染声明中,我们还像 Angular 1 一样可以使用管道工具(Pipes`),比如过滤器,他们可以转换一个值为另一个值,或者将一段字符串本地话,或者可以将一个数字按照本地的货币数值格式化。

[] :绑定属性

要为一个组件绑定一个属性,可以使用 [] 声明,如果你在组件中有一个 this.currentVolume 声明,那么我们将可以将该值单向同步至组件,以帮助组件可以实时具有该属性的值:

<video-control [volume]="currentVolume"></video-control>

() :事件监听

要在一个组件上监听一个事件,可以使用 () 声明:

<my-component (click)="onClick($event)"></my-component>

[()] :数据双向绑定

要实现数据的双向绑定,我们可以使用 [()] 声明:

<input [(ngModel)]="myName" />

此时,this.myName 的值将与 input 的值双向同步。

* : 星号

* 声明表示该指令把当前组件作为一个模板,而且不会直接将其按愿你渲染,比如 ngFor 指令遍历出 items 中的每一个 item<my-component> 每一个 item 会创建一个组件的实例,但是组件本身的声明并不会被渲染出来。

<my-component *ngFor="#item fo items">
</my-component>

其它类似的指令还有 *ngIf*ngSwitch 等。

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

这是一篇关于湖南湘西地区农村丧葬习俗的文章,里面会带有很多与丧事相关的文字与图片,在您阅读前,请先确定您对这类题材没有不适。


我的家乡是一个湖南西部的小县城,辰溪县,外公去年去世,第一次过年没有听到他的声音,所以,无意间又开始想起他来,我写不出能表达我内心对他怀念的词语,就再一次一个人细细的看了一遍送他最后一程的所有照片,然后古迹群里鲁言老师问了我一些关于丧葬照片的问题,很细节,所以,就想着,要不我就写写家乡的丧葬习俗吧,也算是对家乡习俗的一些文字记载。

外公去世的时间是2015年12月12日的凌晨三点多,听外婆说走得很突然,虽然生前遭受太多病痛的折磨,但是走的时候并没有再受煎熬,躺在床上想要上厕所,然后走了几步就坐到地上,之后就再也没有醒来,听说应该是心肌梗塞,因为肿瘤手痛的问题,而让我们大家都忽略了他的这个病,也算是我们做后辈的不孝吧,尤其是我,不善表达我的情感,但是我却很后悔为什么没有见到最后一面,在还有机会的时候,只剩下生后的缅怀。

我是2015年12月16日回到家里,家乡现在是这样的景像:

_DSC3140.jpg

请道人

人去世之后,需要做的第一件事情就是请道人(佛教)给逝者开路,亡者在道人举办了开路仪式之后才可以进入阴界,才能来世投胎。

具体的作法是道人带领孝子去主管本姓的“土堂”招魂后再回到家中,整个过程孝子牵着一只母鸡(爬路鸡,,寓意就是这个鸡在人的前面开道(开路),回家之后,母鸡放生。

这里面的道人(佛教)本身并不一定就是一个佛教徒,这群人更加类似于家传事业一样,一代传一代,子承父业,他们同样结婚、生子,喝酒吃肉,但是却通晓法事之术,我们可以认为,道人就是管着一方丧事法事的人,他们有自己的地域,是不允许(也没有人会请他们)去别人的地域做法事的。

请人唱老人歌

在请完道人的同时,还需要请人唱老人歌陪唱到晚上十二点。

此老人歌中的老人意为“去世”,即人年老后去世,而非老年的老,该词在此处是一个动词,“老人”即“人去世”,“老人歌”即“为去世之人唱的歌曲”,一般会请两至三人,唱歌是为了把已故之人吵醒。

老人歌就是一些古时的戏剧(唱书),如红兰贵打酒、薛丁山征西等。

出殡前晚称为大葬夜,需要唱通宵,而且唱的内容也与其它夜的内容不太一样,这个在下面会详细描述。

做道场(法事)

做道场就是做法事,只是在我们那边的农村都叫作道场,它有一根桅杆道场、二根桅杆道场、三根桅杆道场之分,做道场是需要念经,如金刚经、地藏经等

桅杆是什么?

酒席

红白事都会有酒席,亲朋好友邻里乡亲都会来家里面吃酒席,也会收人情,因为外公家房子外面就是竹林,空地不是很大,所以外公的灵屋和酒席不在一起,满席是在大舅舅的老房子(现在也都没有人住了,都在县城买了房子了)家办的,掌厨的也都是乡亲,每个村子都有几个,能做出同村人都喜欢吃的饭菜来。

而在出殡前,道士一直需要超度,而在超度时,我们这些做后辈的都需要去跪拜,尤其是外公的儿子们(也就是我们舅舅们)。

转灯

我们每过一段时间就需要转灯,就是后辈们拿着一根竹棍和一支香,跟着道士绕着棺材转圈,每走几步就要磕头,转完一次之后,需要向死者敬酒,一杯酒敬三次,敬完之后需要倒地上,香插入棺材前的香炉里。

我现在正在进行 信用生活 的业务运行系统的开发,使用了 AngularJS 以及 Material Design 以及 [Angular UI Router](),系统中涉及到了一个特别常见的问题——一个Router的网址带有可选的参数,比如:/cms/promotions/movies 对应的是 /cms/promotions/:promotionType ,然后我还需要网址中能带有分页参数以及每一页数据的条目数量参数,这些参数也还应该有默认值,这个时候就使用到了可选参数。

在 AngularJS UI Router 中有以下方法可以创建可选参数的路由。

Query Parameters

使用 Query Parameters 是最简单也是最常见的一种方式了, UI Router 会将定义的参数都添加至 $stateParams 对象中去,比如:

state('app.console.cms.promotions', {
  url: '/cms/promotions/:promotionType?page',
  templateUrl: 'app/console/cms/promotions/promotion-list.tmpl.html',
  controller: function($scope, $stateParams) {
    $scope.promotionType = $stateParams.promotionType;
    $scope.page = $stateParams.page || 0;
  }
})

接着,你就可以向下面这样定义一个新的链接了:

<a ui-sref="app.console.cms.promotions({ promotionType: 'foods', page: 1})">美食</a>

如果有多个参数,也没有关系,直接把多个参数使用 & 符号连接即可:

state('app.console.cms.promotions', {
  url: '/cms/promotions/:promotionType?page&size',
  templateUrl: 'app/console/cms/promotions/promotion-list.tmpl.html',
  controller: function($scope, $stateParams) {
    $scope.promotionType = $stateParams.promotionType;
    $scope.page = $stateParams.page || 0;
    $scope.size = $stateParams.size || 10;
  }
})

直接使用 Route Parameter

还有一种方式只适合只有一个可选参数的路由,比如:/cms/promotions/:promotionType

state('app.console.cms.promotions', {
  url: '/cms/promotions/:promotionType',
  templateUrl: 'app/console/cms/promotions/promotion-list.tmpl.html',
  controller: function($scope, $stateParams) {
    $scope.promotionType = $stateParams.promotionType || 'foods';
  }
})

此时, /cms/promotions/ 就对应的就是参数没有值时的路由了,此时我在 Controller 里面使用了一个默认的 foods 值为其值。

但是对于多个参数的时候,我们也只能定义多个路由了:

state('app.console.cms.promotions', {
  url: '/cms/promotions/:promotionType',
  templateUrl: 'app/console/cms/promotions/promotion-list.tmpl.html',
  controller: function($scope, $stateParams) {
    $scope.promotionType = $stateParams.promotionType || 'foods';
  }
})

...

state('app.console.cms.promotions', {
  url: '/cms/promotions/:promotionType/:page',
  templateUrl: 'app/console/cms/promotions/promotion-list.tmpl.html',
  controller: function($scope, $stateParams) {
    $scope.promotionType = $stateParams.promotionType || 'foods';
    $scope.page = $stateParams.page || 0;
  }
})

非 URL 路由参数

还有一种方式,可以不通过 URL 即可定义参数:

state('app.console.cms.promotions', {
  url: '/cms/promotions',
  params: {
    promotionType: 'foods'
  },
  templateUrl: 'app/console/cms/promotions/promotion-list.tmpl.html',
  controller: function($scope, $stateParams) {
    $scope.promotionType = $stateParams.promotionType;
  }
})

此时,我们还是可以使用 ui-sref=app.console.cms.promotions({promotionType: 'movies'}) 创建一个链接 /cms/promotions,但是却会把 movies 做为 promotionType 的值传递给控制器。

Accept

客户端用 Accept 首部来通知服务器 可以接受哪些媒体类型,其值就是客户端支持的媒体类型列表,比如 image/gif 就是表示客户端支持 gif 格式的图片, * 则是一个特殊的值,用来通配媒体类型,比如 */* 表示所有媒体,image/* 则表示所有的图片,同时,还可以添加一个质量值 q 来告诉浏览器优先选择哪种媒体类型。

示例

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8

Accept-Charset

客户端用 Accept-Charset 首部来通知服务器,它可以接受哪些字符集或者哪些优选字符集,其值为多个字符集列表以及字符集可能的质量值,当服务器上有以多种可接受字符集表示的文档时,可以通过质量值告知服务器哪个字符集是优选的,该首部同样有一个 * 通配符。

示例

Accept-Charset: iso-latin-1

Accept-Encoding

客户端用 Accept-Encoding 首部来告知服务器它可以接受哪些编码方式,如果服务器的内容是经过编码的(可能是经过压缩的),那么这个请求首部可以告诉服务器客户是否会接受它。

示例

Accept-Encoding: gzip, compress;q=0.5

Accept-Language

客户端用 Accept-Language 表示可接受或者优选哪些语言。

示例

Accept-Language: zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4

Accept-Ranges

Accept-Ranges 首部与其它 Accept 首部不同——它是服务器使用的一种响应首部,用来告诉客户端它们是否接受请求资源的某个范围。如果这个首部有赋值的话,这个值就说明了服务器允许对指定资源的哪个范围类型进行访问。

客户端可以在没有收到这个首部的情况下,对某资源发起范围请求。如果服务器法巴拉圭对那个资源的范围请求,可以以适当的状态码(比如 416)进行响应,奖 Accept-Ranges 的值设置为 none ,客户端以后就不会(也不应该)再发起范围请求了。

应用场景

该首部最常见的应用场景就是断点下载,当我们从网络上下载一个大小为 4096 bytes 的文件时,可能下载到 2048 bytes时,网络中断了,这个时候,如果再次下载,我们就可以通过一个客户端的 Range 首部告诉服务器,我只需要下载 2049 bytes 之后的数据即可,前面的数据已经有了(这个在后面会讲到),当然,这需要服务器返回一个正确的 Accept-Rangesbytes

另一个常见的场景就是在做 RESTFul 接口时,比如一个用于查询所有用户列表的接口 /users,可能数据库里面总共有 10000 条记录,但是我们不可能一次性就全部返回,这个时候,服务器可以返回一个 Accept-Ranges: items ,告诉客户端,你可以以 items 类型对该资源进行部分下载。

示例

Accept-Ranges: bytes

Age

Allow

Allow 首部经用于通知客户端可以对特定资源使用哪些HTTP方法。

示例

Allow: GET, HEAD

Authorization

Authorization 首部是由客户端发送的,用来向服务器回应自己的身份验证信息,客户端收到来自服务器的 401 Authentication Required 响应后,要在其请求中包含这个首部,这个首部的值 取决非于所使用的认证方案。

示例

Authorization: Basic YnJpYW4tdG90dHk6T3ch

说明

在有一些时候,我们可能也不会使用这个首部,而是使用自己定义的相类似的首部,比如在信用生活的项目里面我们使用的就是 X-Auth-Token,这个时候,但是使用这种自定义的首部之后,通用的客户端就无法为我们进行任何建议或处理了,就像在浏览器里面,若服务器返回 401 Authentication Required,这个时候,浏览器表现为弹出一个输入用户名和密码的输入框 ,用户输入之后,浏览器会对其进行简单的 Base64 编码,然后直接重新发起一次请求(本次请求会带上如上面示例所示的首部),但是对于服务器来讲,因为认证并不是通过 Authorization 首部进行的,所以还是会返回 401 响应。

Cache-Control

Cache-Control 首部用于传输对象 的缓存信息,这个首部是 HTTP/1.1 引入的比较复杂的首部之一,它的值是一个缓存命令,给出了某个对象可缓存性有关的缓存 特有指令。

示例

Cache-Control: no-cache

Client-ip

这是一个在一些比较老的客户端和代理上面会出现的首部,用于表示运行当前客户端或者代理的计算机的IP地址。

示例

Client-ip: 10.106.4.199

Connection

Connection 首部是个多少有点过载了的首部,它可能会把你搞晕。这个首部用于 扩展了 keep-alive 连接的 HTTP/1.0 客户端keep-alive 连接用于控制信息,在 `HTTP/1.1中,能识别出大部分较老的语义,但这个首部被赋予了新的功能。

HTTP/1.1 中,Connection 首部的值 是一个标记列表,这些标记对应各种名称,应用程序收到带有 Connection 首部的 HTTP/1.1 报文后,应该对列表进行解析,并删除报文中所有在 Connection 首部列表中出现过的首部,它主要用于有代理的网络环境,这样服务器或者其它代理 就可以指定不应该传递的逐跳首部了。

比如 close 这个标记值,这个标记意味着响应结束之后,连接会被关闭,不支持持久连接的 HTTP/1.1 应用程序要在所有请求和响应中插入带有 close 标记的 Connection 首部。

示例

Connection: close

Content-Base

服务器可以通过 Content-Base 首部为响应主体部分中要解析的 URL 指定一个基础 URL ,其值是一个绝对 URL,用于解析在实体内找到的相对 URL

示例

Content-Base: http://www.onmr.com/

Content-Encoding

Content-Encoding 首部用于说明是否对某对象进行过编码,通过对内容进行编码,服务器可以在发送响应之前将其进行压缩,然后通过 Content-Encoding 首部的值告诉客户端,服务器对对象执行过哪种或者哪些类型的编码,有了这个信息,客户端就可以对报文进行编码了,这个值,理论上来讲应该是由客户端发送的 Accept-Encoding 首部中所有支持的编码类型的一个子集。

示例

Content-Encoding: compress, gzip

Content-Language

Content-Language 首部用来告诉想要理解对象的客户端,应该理解哪种自然语言,比如说,一篇用法语编写的文档就应该有一个表示法语的 Content-Language 值。

示例

Content-Language: en

Content-Length

Content-Length 首部说明了主体部分的长度或尺寸,如果对 HEAD 请求的响应报文中带上这个首部,则这个首部就告诉客户端,如果要发送内容的话,那么内容主体的长度就是这个值。

示例

Content-Length: 2417

Content-Location

Content-Location 首部包含在一个 HTTP 报文中,给出了与报文的实体部分相对应的 URL ,对可能有多个 URL 的对象来说,响应报文中可以包含一个 Content-Location 首部,说明用来产生响应的对象的 URLContent-Location 可以与所请求的 URL 不同,服务器通常会用它奖客户端导向或者重定向到一个新的 URL 上去。

如果 URL 是相对的,就应该相对于 Content-Base 首部加以解释,如果没有提供 Content-Base,则应该使用请求的 URL 进行解释。

示例

下面的示例就是信用生活中,信用卡信息管理接口 /api/v1/discovery/creditcards ,当我使用 POST 请求,向服务器发送了张新卡片详情数据之后,服务器完成数据的校验与保存,然后返回一个 Content-Location ,告诉客户端,新的内容的位置在哪里:

Content-Location:/api/v1/discovery/creditcards/1600

Content-MD5

Content-MD5 首部是服务器用来对报文主体进行报文完整性检查的,只有原始服务器或发起请求的客户端可以在报文中插入 Content-MD5 首部,首部值就是报文主体的 MD5 摘要(根据 RFC 1864 的定义,MD5 摘要值是一个 Base64 或者 128 位的 MD5 摘要。

示例

Content-MD5: YnJpYW4tdG90dHk6T3ch

Content-Range

请求传输某范围内的文档时,产生的结果由 Content-Range 首部给出,它提供了请求实体所在的原始实体内的位置(范围),还给出了整体实体的长度。如果值为 *,而不是整个实体的长度,就意味首发送响应时,长度未知(这个我们在使用浏览器下载文件的时候,绝大多数情况下,我们都是可以根据当前网速知道剩余下载时间的,但是有一些下载,我们却不知道下载时间,这就是因为,在下载时,服务器明确表示了内容长度未知引起的)。

示例

下面的示例就表示了,内容的长度单位为 bytes,总长度为 5400 bytes ,而本次响应的内容为整个主体对象第 500 - 999 bytes 间的内容。

Content-Range: bytes 500-999 / 5400

应用场景

这个首部在我来宜人贷的这段时间内,还没有看到哪个项目或者团队在使用,但是在我自己的项目里面使用到了,我自己项目的接口是完全基于 RESUful 建议的,这里面就以 /user/users 接口为例来讲讲,该接口的 GET 请求用于查询数据库中的所有用户,默认情况下,服务器会返回查询到的前20条记录,现在假如整个库中有 1000 个用户,那么, /user/users 这个资源主体大小就是 1000,因为这里面的大小单位不再是 bytes ,所以,我们自己定义了一个单位 items,当客户端 GET /user/users 时,会得到下面这样的首部:

Content-Range: items 0-19/1000

此时,客户端已经得到它所需要的所有信息了:

  1. 当前资源返回的是第0至第19个
  2. 资源总条目(items)数为 1000

在接下来的访问过程中,客户端就可以发起请求,获取其它范围的内容了,比如下面这样:

Range: items=20-39

这时,服务器就知道,此次请求需要查询的是第20至第39个条目

说明

关于 RESTful 中的分页,我现在的解决方案如下:

  • 请求格式:Range: items=start-end
  • 响应格式:Content-Range: items start-end/total
Range 首部

Range 首页由客户端发送请求时携带,用于标识此次请求需要服务器端返回哪个区间的数据,Range的取值方式如下:

  1. items=0-9:取结果集中的 第0至第9 个子集
  2. items=1000-:取结果集中 第1000及其以后所有数据 的子集
  3. items=-500:取结果集中 最后500个 子集
  4. items=0-9,20-29:取结果集中 第0-9个 以及 第20-29个 两个子集的合集
  5. items=0-9,-9:取结果集中 第0-9个 以及 最后9个 两个子集的合集
Content-Range 首部

Content-Range 首部是由服务器在返回结果的响应时,携带的首部,用于表示此次返回的数据的范围,其值如下:

Content-Range: items 0-9/100

详细的说明可以见 http://git.onmr.com/grassroots/grassroots-api-docs/blob/master/structure.md

关于 Range 首部,会在 Range 首部中专门细说一下。

Content-Type

Content-Type 首部说明了报文中对象的媒体类型。

示例

Content-Type: text/html; charset=iso-latin-1

Cookie

Cookie 首部是用于客户端识别和跟踪的扩展首部。

示例

Cookie: sessionid=IDFIOSDUFSDFLKIU@#IOL#KKJoi

Cookie2

Cookie2 首部是用于客户端识别和跟踪的扩展首部,Cookie2 用于识别请求发起者能够理解哪种类型的 Cookie1

示例

Cookie2: $version="1"

Date

Date 首部给出了报文创建的日期和时间,服务器响应中要包含这个首部,因为缓存在评估响应的新鲜度时,要用到这个服务器认定的报文创建时间和日期,对客户端来说,这个首部是可选的,但包含这个首部会更好。

HTTP 有几种特定的日期 格式,这种格式是在 RFC 822中定义的,这是 HTTP/1.1 报文的优选格式,但是在早期的HTTP规范中,没有明确说明日期的格式,因此服务器和客户端的实现者使用了一些其他 格式,为了解决这些遗留问题仍然需要支持这些格式:

Date: Tuesday, 03-Oct-14 02:15:31 GMT RFC 850 format
Date: Tue Oct 3 02:15:31 1997 asctime( ) format

示例

Date: Thu, 07 Jan 2015 15:15:15 GMT

ETag

ETag 首部为报文中包含的实体提供了实体标记,实体标记实际上就是一种标识资源的方式。

示例

ETag: "11e92a-457b-31345aa"
ETag: W/"11e92a-457b-31345aa"
W/ 前缀广播一个 实体标记,对于弱实体标记来说,只有当关联 的实体在语义上发生了重大改变时,标记才会变化 。

Expect

客户端通过 Expect 首部来告知服务器它们需求某种行为,现在此首部与响应码 100 Continue 紧密相关。

如果服务器无法理解 Expect 首部的值,就应该以状态码 417 Expectation Failded 进行响应。

示例

Expect: 100-continue

Expires

Expires 首部给出了响应失效的日期和时间,这样,像浏览器这样的客户端就可以缓存一份副本,在这个时间到期之前 ,不用去询问服务器它是否有效了。

示例

Expires: Thu, 07 Jan 2015 15:15:15 GMT

From

From 首部说明请求来自何方,其格式就是客户端用户的有效电子邮件地址。

示例

From: name@domain.com

Host

客户端通过 Host 首部为服务器提供客户端想要访问的好地台机器的因特网主机名和端口号,主机名和端口号来自客户端所请求的 URL

如果一台服务器上面服务着多个网站,每一个网站绑定了一个独立的域名,这个时候,服务器软件就需要通过 Host 首部来判断用户是需要访问当前该服务器上面的那一个网站了。

HTTP/1.1 客户端必须在所有请求中包含 Host 首部,所有的 HTTP/1.1 服务器都必须以 400 Bad Request 状态码去响应没有提供 Host 首部的请求。

示例

Host: onmr.com:80

If-Modified-Since

If-Modified-Since 请求首部用来发起条件请求,客户端可以用 GET 方法去请求服务器上的资源,而响应则取决于客户端上次请求此资源之后,该资源是否被修改过,如果对象未被修改过,服务器就会回送一条 304 Not Modified 响应,而不会回送此资源,如果对象对修改过,则服务器就会像非条件 GET 请求一样进行响应。

示例

If-Modified-Since: Thu, 07 Jan 2015 15:15:15 GMT

If-Match

If-Modified-Since 首部类似,If-Match 首部也可以用于发起条件请求,If-Match 请求使用的是实体标记(ETag)而不是日期。服务器将对比 If-Match 首部的实体标记与资源当前的实体标记,如果标记匹配就将对象返回。

服务器应该用 If-Match* 与资源拥有的所有实体标记进行匹配,除非服务器上没有这个资源了,否则 * 总会与实体标记相匹配。

示例

If-Match: "11e92a-457b-31345aa"

If-None-Match

与所有 If 首部一样,If-None-Match 首部可以用于发起条件请求,客户端为服务器提供 一个实体标记列表,服务器将这些标记与它拥有的资源实体标记进行比较,只在都不匹配的时候才将资源返回。这样缓存就可以只在资源已被修改的情况下才更新。通过 If-None-Match 首部,缓存可以用一条请求使它拥有 的实体失效,同时在响应中接收新的实体。

示例

If-None-Match: "11e92a-457b-31345aa"

If-Range

If 首部一样,If-Range 首部可以用于发起条件请求。应该程序拥有某范围内资源的副本,它要对范围进行再验证,如果范围无效的话,要获取新的资源,在这种情况下会使用这个首部。

示例

If-Range: "11e92a-457b-3134b5aa"

If-Unmodified-Since

If-Unmodified-SinceIf-Modified-Since 是一对,该首部查询时,未被修改过才返回对象。

示例

If-Unmodified-Since: Thu, 03 Oct 2015 17:17:17 GMT

Last-Modified

Last-Modified 首部试图提供这个实体最后一次被修改的相关信息,这个值可以说明很多事情,比如,资源通常都是一台服务器上的文件,因此 Last-Modified 值可能是服务器的文件系统所提供的最后修改时间;对于那些动态创建的资源,Last-Modified 的值可能就是创建响应的时间。

Location

Location 首部可以将客房端导向某个资源的地址,这个资源可能在客房端最后一次请示之后被移动过,也可能是在对请示的响应中创建的。

示例

Location: http://www.onmr.com/press/http-headers

Max-Forwards

这个首部必须和 Trace 方法一同使用,以指定请示所经过的代理或者其它中间节点的最大数目,它的值是一个整数,所有收到带有此首部的 Trace 请示,都需要在请示转发出去之前,将该值减1。如果应用程序在收到请示时,这个首部的值为0,就要向请示回应一条 200 OK响应,并在实体的主体部分包含原始请示。

示例

Max-Forwards: 5

MIME-Version

MIME-Version 用于提供服务器支持的 MIME 版本,老服务器才可能存在,没有进入标准中。

Pragma

Pragma 首部用于随报文传送一些指令,这些指令几乎可以包含任何内容,但通常会用这些指令来控制缓存的行为。Pragma 首部的目标可以是接收这条报文的所有应用程序,因为此理和网关一定不能将删除。

最常见的形式,比如用户点击刷新按扭或者重新加载时,浏览器一般都会带上 Pragma: no-cache 首部,用于强制在有新鲜副本可用的情况下,向原始服务器请示文档或对其进行再验证。很多服务器也会向客户端发送 Pragma: no-cache 首部的响应(作用与 Cache-Control: no-cache 等价)

规范中唯一一个被定义的指令就是 no-cache

Proxy-Authenticate

Proxy-AuthenticateWWW-Authenticate 类似,当代理服务器发送了一条 407 Proxy Authentication Required 响应时,发起请示的应用程序就必须包含 Proxy-Authenticate 首部。

示例

Proxy-Authenticate: Basic realm="Yirendai Super User"

Proxy-Authorization

Proxy-Authorization 被客户端用来响应代理服务器的 Proxy-Authenticate 声明

Proxy-Connection

Proxy-ConnectionHTTP/1.0 Connection 首部类似,

Public

Public 首部可让服务器告知客户端其支持哪些方法,今后客房端就可以在发起的请示中使用这些方法了。

示例

Public: OPTIONS, GET, HEAD, TRACE, POST

Range

在请示某个实体的部分内容时,会使用到这个首部

示例

Range: bytes=500-1000

Referer

在客户端的请求中插入 Referer 首部,可以使服务器知道客房端是从哪里获取到其请示的 URL的,这是一种对服务器有益的自愿行为,这样服务器就可以更好的记录请求,或执行其它任务。

浏览器所做的工作非常简单,如果在主页A上点击了一个金田白立,进入主页B,那么浏览器就会在请求B的请求中带有一个 Referer 首部,其值为主页A的URL。

Retry-After

服务器通过 Retry-After 首部告知客房端在什么时候可以重新发送某资源的请求。

Server

ServerUser-Agent 类似,服务器使用该首部来标识自己。

Set-Cookie

Set-Cookie 用于告知客户端设置 Cookie

Set-Cookie2

Set-Cookie 首部的扩展

TE

Accept-Transfer-Encoding ,类似于 Accept-Encoding,只不过它用于定义传输编码。

Trailer

Trailer 首部用于说明报文拖挂中提供了哪些首部。

示例

Trailer: Content-Length

Title

Title 发送 HTML 文档的标题(非标准)

Transfer-Encoding

Transfer-Encoding 定义报文的传输编码。

示例

Transfer-Encoding: chunked

UA-(CPU,Disp,OS,Color,Pixels)

非标准

Upgrade

User-Agent

Vary

Via

Warning

WWW-Authenticate

X-Cache

X-Forwarded-For

X-Pad

X-Serial-Number

如果在安装 npm 的各种包时,总是需要 sudo 才能完成安装的话,那是因为你的 npm 包安装目录的权限问题,你可以通过下面两种方式的任何一种解决:

方法一:修改 npm 默认安装目录的权限

  1. 找到 npm 默认安装目录:

    npm config get prefix

    很多系统都应该是 /usr/local ,如果目录是 /usr 的话,请不要使用此方法,改用方法二。

  2. 将该目录的所有者改成当前用户即可(就是你啦):

    sudo chown -R `whoami` <directory>

如果你不想修改目录的权限,你可以单独修改下面这些子目录即可:

  • lib/node_modules
  • bin
  • share

方法二:修改 npm 默认的安装目录至另一个目录

很多时候,可能因为各种各样的原因,你并不想或者根本就不能修改默认目录的所有者,那么,改变 npm 的默认安装目录将是最好的选择了:

  1. 创建一个新的目录,比如下面这样:

    make ~/.npm-global
  2. 设置 npm 使用刚才新建的目录:

    npm config set prefix '~/.npm-global'
  3. 创建或者打开现有的 ~/.profile 文件,添加下面这一行:

    export PATH=~/.npm-glopbal/bin:$PATH
  4. 保存之后返回至命令行,更新系统变量:

    source ~/.profile

现在你可以直接通过下面这行命令全局安装一下 jshint 试试。

npm install -g jshint

安装

全局安装

如果你希望以命令行的方式在任何一个目录启动 Browsersync,那么需要你全局安,使用下面这行命令即可:

npm install -g browser-sync

本地安装

本地安装则是直接将 Browsersync 直接安装至你的项目中,这是应该优先选择的方式,安装完成之后,便将 Browsersync 作为你的项目的一个依赖添加至 package.json 文件中,这样所有的人都会在使用你的项目时,自动安装 Browsersync。

npm install browser-sync --save-dev
不管是全局安装,还是本地安装,请都不要使用 sudo,如果你在安装过程中告诉你需要使用 sudo,你可以通过 《修复 npm 的权限》这篇文章修复,很简单,只需要一分钟不到。

Gulp.js 配合

安装

在你的项目中安装 browsersyncgulp

npm install browser-sync gulp --save-dev

然后在 gulpfile.js 中添加如下代码:

var gulp = require('gulp');

var browserSync = require('browser-sync').create();

// 服务本地静态文件
gulp.task('browser-sync', function() {
    browserSync.init({
        server: {
            baseDir: './www/'
        }
    })
});

// 代理你的其它的项目
gulp.task('browser-sync', function() {
  browserSync.init({
      proxy: "yourlocal.dev"
  })
});

之后,在你的项目中即可以使用以下方式直接打开开发服务器了:

gulp browser-sync

SASSCSS 注入

Browsersync 支持数据流,所以你可以在一个特定的时间点重载页面,比如你修改了某个CSS文件或者HTML(注意,你需要在 gulp.dest 之后再调用 .stream)。

var gulp = require('gulp');
var browserSync = require('browser-sync').create();
var sass = require('gulp-sass');

// 服务本地静态文件
gulp.task('browser-sync', function() {
    browserSync.init({
        server: {
            baseDir: './www/'
        }
    });

    gulp.watch('./scss/*.scss', ['sass']);
    gulp.watch('./www/*.html').on('change', browserSync.reload);
});

gulp.task('sass', function() {
    return gulp.src('scss/*.scss')
    .pipe(sass())
    .pipe(gulp.dest('www/css'))
    .pipe(browserSync.stream());
});

gulp.task('default', ['browser-sync']);

与比对应的项目目录结构如下:

project/
    scss/
        main.scss
    www/
        index.html
        css/
            main.css
    gulpfile.js

ruby-sasssourcemap

如果使用了 gulp-ruby-sass ,并且开启了 sourcemap: true ,附加的 .map 文件也会被生成,如下:

var gulp = require('gulp');
var browserSync = require('browser-sync').create();
var sass = require('gulp-ruby-sass');
var sourcemaps = require('gulp-sourcemaps');

// 服务本地静态文件
gulp.task('browser-sync', ['sass'], function() {
    // 初始化 browser-sync
    browserSync.init({
        // 设置服务器
        server: {
            // 根目录为 ./www/
            baseDir: './www/'
        }
    });

    // 观察 ./scss 目录下所有 scss 文件的变更,同时将变更通知给 sass task
    gulp.watch('./scss/*.scss', ['sass']);
    // 观察 ./www 目录下的所有 .html 文件,当其修改时, browser-sync 重新加载(reload)
    gulp.watch('./www/*.html').on('change', browserSync.reload);
});

// sass 任务
gulp.task('sass', function() {
  return sass('scss/*.scss', {
      sourcemap: true
  }).on('error', function(err){
      console.error('Error!', err.message);
  }).pipe(sourcemaps.write('./www/css', {
      includeContent: false,
      sourceRoot: 'scss'
  })).pipe(browserSync.stream({
      match: '**/*.css'
  }))
});

gulp.task('default', ['browser-sync']);

Phalcon 是一个用 C 语言编写的,号称是速度最快、占用资源最少的 PHP 框架。它以一个 PHP 扩展的形式安装,与 CodeIgniter、CakePHP 等框架有显著的不同。

Phalcon 在 Windows 上的安装很简单,只要在官方网站上找到对应 PHP 版本的 DLL,放进 PHP 目录,然后在 php.ini 里加上就行了。但在 Linux 和 Mac 上需要自己编译。

在 Mac 上做 PHP 开发,很多人都用 MAMP。情况比较麻烦,因为除了 MAMP 以外,OS X 还自带了一个 PHP;而且 MAMP 没有自带 PHP 的源码。所以需要一些额外的步骤。

准备编译环境

首先,你得有一个包管理器,比如 Homebrew,用来安装一些工具。另外,还要安装 Xcode 或者只安装它的命令行工具,才能进行编译。

接下来,用 Homebrew安装一些工具:

$ brew install autoconf automake libtool

修改环境变量

现在,如果你在终端使用 PHP,实际上用的是 OS X 自带的那个:

$ which php
/usr/bin/php

修改环境变量,让终端调用 MAMP 里的 PHP:

$ export PATH=/Applications/MAMP/bin/php/php5.6.10/bin:$PATH
$ which php
/Applications/MAMP/bin/php/php5.6.10/bin/php

下载 PHP 源码

php --version 获得 PHP 的版本,然后在 php.net 下载对应的源码包。

$ php --version
PHP 5.6.10 (cli) (built: Jul  6 2015 14:28:54) 
Copyright (c) 1997-2015 The PHP Group
Zend Engine v2.6.0, Copyright (c) 1998-2015 Zend Technologies

$ curl http://cn2.php.net/distributions/php-5.6.10.tar.bz2 | tar -xj
$ mkdir /Applications/MAMP/bin/php/php5.6.10/include
$ mv php-5.6.10/ /Applications/MAMP/bin/php/php5.6.10/php
$ cd /Applications/MAMP/bin/php/php5.6.10/php/
$ ./configure

安装 Phalcon

$ curl -L -o cphalcon-master.zip https://github.com/phalcon/cphalcon/archive/master.zip
$ unzip cphalcon-master.zip
$ cd cphalcon-master/build
$ sudo ./install

修改 MAMP PHP 配置文件模板

打开 MAMP,点击 File -> File -> Edit Template -> php 5.6.10 php.ini ,添加如下一行:

extension=phalcon.so

重启服务后,即可通过 phpinfo() 函数看到已安装的 Phalcon 信息。

介绍

很多软件都必须服务器提供了 Java 支持,本文将指导你完成在 Ubuntu 服务器如何安装与管理多版本的 Java.

安装默认的 JRE/JDK

这是被推荐的,也是最简单的方式,默认情况下,在 Ubuntu 12.04 上面,会安装 OpenJDK 6,而在 Ubuntu 12.10+ 上面,会安装 OpenJDK 7

通过 apt-get 工具安装 Java 很简单,首先更新包索引:

sudo apt-get update

安装检查你的服务器上面是否已经安装了 Java:

java -version

如果该命令返回 『The program java can be found in the following packages』,则表明你的服务器上面还没有安装任何版本的 Java,那么,执行下面这行命令即可安装默认版本的 Java:

sudo apt-get install default-jre

这会安装 Java 运行时环境(JRE),如果你需要安装 Java 开发工具包(JDK)来构建或编译 Java 程序(比如 Apache Ant,Apache Maven 等),那么执行下面这行命令:

sudo apt-get install default-jdk

这人安装 Java 需要的所有内容。

其它的步骤,都是可选的,而且应该在你需要的时候才执行。

安装 OpenJDK 7 (可选)

要安装 OpenJDK 7 ,执行下面的命令:

sudo apt-get install openjdk-7-jre 

这会安装 Java 运行时环境 (JRE),如果你需要安装 Java 开发工具包,则执行下面的命令:

sudo apt-get install openjdk-7-jdk

安装 Orache JDK (可选)

Orache JDK 是官方的 JDK,但是它现在已经不再被是 Ubuntu 的默认安装选项了。但是你还仍然可以通过 apt-get 安装它,要安装任何版本的 Oracle JDK,都需要先执行下面的命令:

sudo apt-get install python-software-properties
sudo add-apt-repository ppa:webupd8team/java
sudo apt-get update

根据你要安装的 JDK 版本的不同,选择下面不同的命令执行:

Oracle JDK 6

这是一个很古老的版本,但是同样可以安装

sudo apt-get install oracle-java6-installer

Oracle JDK 7

这是最新的稳定发布版本:

sudo apt-get install oracle-java7-installer

Oracle JDK 8

这是一个开发者预览版:

sudo apt-get install oracle-java8-installer

多版本安装 (可选)

如果你的系统中安装了多个版本的 Java,那么你可以随时设置并切换默认版本的 Java,执行下面的命令:

sudo update-alternatives --config java

如果你的系统中安装了多个版本,那么通常会有如下这样的返回结果:

There are 2 choices for the alternative java (providing /usr/bin/java).

Selection    Path                                            Priority   Status
------------------------------------------------------------
* 0            /usr/lib/jvm/java-7-oracle/jre/bin/java          1062      auto mode
  1            /usr/lib/jvm/java-6-openjdk-amd64/jre/bin/java   1061      manual mode
  2            /usr/lib/jvm/java-7-oracle/jre/bin/java          1062      manual mode

Press enter to keep the current choice[*], or type selection number:

你现在就可以通过输入每一个版本前面的序号来设置默认的 Java 版本,这种默认版本的设置方法,对于 Java 编译器 javac 同样适用:

sudo update-alternatives --config javac

同样的,keytooljavadoc 以及 jarsigner 等均可以通过此种方法来设置默认版本。

设置 JAVA_HOME 环境变量

有一些程序需要系统提供一个 JAVA_HOME 环境变量,首先找到当前系统安装了哪些版本的Java。

sudo update-alternatives --config java

返回如下这样的结果:

There are 2 choices for the alternative java (providing /usr/bin/java).

Selection    Path                                            Priority   Status
------------------------------------------------------------
* 0            /usr/lib/jvm/java-7-oracle/jre/bin/java          1062      auto mode
  1            /usr/lib/jvm/java-6-openjdk-amd64/jre/bin/java   1061      manual mode
  2            /usr/lib/jvm/java-7-oracle/jre/bin/java          1062      manual mode

Press enter to keep the current choice[*], or type selection number:

那么,安装路径就有下面这些:

/usr/lib/jvm/java-7-oracle

/usr/lib/jvm/java-6-openjdk-amd64

/usr/lib/jvm/java-7-oracle

复制你需要版本的路径,然后编辑:/etc/environment 文件:

sudo nano /etc/environment

在该文件中,添加下面这一行(同时将 YOUR_PATH 改为你刚才复制的路径):

JAVA_HOME="YOUR_PATH"

然后,重新加载该文件:

source /etc/environment

测试设置是否正确,可以执行下面这行命令:

echo $JAVA_HOME

如果成功显示了你设置的路径,则表示设置成功。

吾本布衣,湘西辰溪人士,姓潘,名韬,农村户口,年方二十有七,家有一妻一女。

鄙人不慧、禀性淳朴、内外兼修、将有志于世。大方无隅、大道无形、行者无疆、优雅无像、上善若水,厚德载物。好行摄于天地,喜深宅于蜗居,相貌奇特但奇而不突,相随心生,心随神动,性随势行,与时俱进,与时偕行。

生来奋斗拼搏于盛世,力求名扬于四海。韶年之时,便立下雄心壮志,三更灯火,闻鸡起舞欲以勤奋之功修身增识;弱冠之年,突发奇志:“麒麟岂是池中物,一遇风云便为龙”。然纵有凌云万丈志,却无通天晓地之才。几经坎坷几多沉浮,自感本领恐慌,时光飞逝不待人,一日难再晨。临近而立之年,却是三十功名尘与土,八千里路云和月。白发悄来临,少年已不在。然雄心未泯,壮志仍在,激情依旧,不减当年。若凭一人之力,微乎其微,聚众人之力,泰山可移。善假于物者,足可纵横捭阖,席卷天下,包举宇内,囊括四海,并吞八荒,可上九天揽明月,在下五洋去捉鳖,扶摇直上九万里,飞流直下三千尺,鹰击长空,鱼翔浅底,极尽善用之奥妙,痛快淋漓,信手捏来,游刃有余!

天行健,君子以自强不息。地势坤,君子以厚德载物!有感于同僚垂青,实乃三生有幸,临书增恩,不知所言。


装B完毕,本人已于2015年8月25日正式加入宜信了,也不知道这公司能要我多久,如果不出意外的话,这次应该是你若不弃,我必生死相依,但这最后的结局是啥样儿的了,Who knows?

继续干前端,公司气氛不错,办公地点位于朗园,记得第一次来这边的时候就被这里面浓浓的酱香味儿给吸引,来了之后发现,确实是一个打酱油的好地方,每天十一点到公司,休息片刻,平复一下自己一天激动的心情之后,去下面吃个中饭,然后回来休息片刻,待午餐消化消化,再回到一楼抽支烟,就可以开始一天的工作了。

大约五点至六点,进点餐系统里面点一个美味的晚餐,七点就送到了,吃完晚餐之后,休息片刻,待晚饭消化消化,八点多,回家睡觉……

其它的,不多说了。

今天Git服务器已经快不行了,空间又要满了,而且常常502,另外两台服务器也快到期了,那两台还是去年创业的时候买的,后来一直也没有怎么使用,所以想想,还是都不要了吧,今天就买了一台新的服务器,配置没有以前的高,但是足够一年的使用了,以前用的都是 CentOS 6.5,CentOS 7 也已经出来很长一段时间了,QCloud也有这个版本的镜像,反正,迟早是要升级到新版本的,所以,这次就索性直接使用了 CentOS 7,但是,以前熟悉的工具、命令似乎都没有用了,所以,一切又得重新来,自己不是一个善于记住事情的人,所以,Get一点新技能,就把这点记下来吧。

更新与升级

每一次登录服务器,尤其是要安装新软件的时候,总是习惯于先更新一下,我是一个一直使用最新版本的软件的人。

yum update

或者使用

yum upgrade

挂载数据盘

这是要做的第二件事情了,系统安装好了之后,默认是只有一个系统盘的,就20G,我们需要先将数据盘挂载至服务器上。

# 创建分区
fdisk /dev/vdb
# 格式化分区
mkfs.ext3 /dev/vdb1
# 挂载分区
echo '/dev/vdb1 /home ext3 defaults 0 0' >> /etc/fstab
mount -a

个性化设置

设置主机名

主机名是肯定需要设置的,要不然,服务器一多,登录上了之后,都不知道哪个是哪个了。

hostnamectl

hostnamectl 命令用于管理系统的主机名,这里面需要知道几个概念,在CentOS或RHEL中,有三种定义的主机名:

  1. 静态的(static):“静态”主机名也称为内核主机名,是系统在启动时从/etc/hostname自动初始化的主机名;
  2. 瞬态的(transient):“瞬态”主机名是在系统运行时临时分配的主机名,例如,通过DHCP或mDNS服务器分配;
  3. 灵活的(pretty)

静态主机名和瞬态主机名都遵从作为互联网域名同样的字符限制规则,“灵活”主机名则允许使用自由形式(包括特殊/空白字符)的主机名,以展示给终端用户(如 Tao's Computer)。

[root@vicpan ~]# hostnamectl status
   Static hostname: onmr.com
         Icon name: computer-vm
           Chassis: vm
        Machine ID: 3f57f163dfaf1ec9ed891518d1d2fafe
           Boot ID: a7316ea3bd284d6da2ecc6cfc3bfe959
    Virtualization: kvm
  Operating System: CentOS Linux 7 (Core)
       CPE OS Name: cpe:/o:centos:centos:7
            Kernel: Linux 3.10.0-123.el7.x86_64
      Architecture: x86_64

hostnamectl status 可以查看系统的主机名状态,如果想只查看静态、瞬态或灵活主机名,分别使用“--static”,“--transient”或“--pretty”选项。

hostnamectl set-hostname onmr.com

hostnamectl set-hostname 命令可以设置主机名,若不特别指定,该命令会修改所有主机名,一旦修改了静态主机名,/etc/hostname 将被自动更新。然而,/etc/hosts 不会更新以保存所做的修改,所以你需要手动更新/etc/hosts。

若要指定某一种主机名,则需要添加相应的参数:

hostnamectl --static set-hostname <host-name>

设置 DNS 服务器

vi /etc/resolv.conf  

nameserver 10.138.224.65
nameserver 10.182.20.26
nameserver 10.182.24.12
options timeout:1 rotate 

安装 Nginx + PHP-FPM 环境

安装 Nginx

安装官方的 Red Hat/CentOS 预编译包,为了追加 nginx 的 yum 仓库,需要创建一个文件 /etc/yum.repos.d/nginx.repo,并将下面的其中一个内容复制进去:

[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/centos/$releasever/$basearch/
gpgcheck=0
priority=10
enabled=1

七月份的下半个月,有幸做了奔驰 Smart 2015年新官网,包括手机端和PC端的宣传页,地址:

这里,为了证明这个是一个事实,我还特意的留存了两张截图:

QQ20150802-1@2x.jpg

QQ20150802-2@2x.jpg

这里只想说明这么几个问题:

  1. 这东西确实是我做了,而且是那种创意95天,开发两天,三天测试,100天的时候就要上线的;
  2. 奥美负责创意,把项目外包,结果就是,丫的居然告诉我,不合格,准备直接把代码转交给另外一个团队,意思就是这项目跟我没半毛钱关系;
  3. 这是纯粹对劳苦码农工作成果的蔑视,本人发誓,老子有生之年,再也不会跟任何外企扯上关系,即使被国人坑,至少不流外人田;
  4. 大家以后跟这两公司打交道,小心……切记

抵制奥美,从我做起

抵制奔驰,人人有责

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运营的不好。

Looking for something else? Take a look at the awesome collection of other awesome lists.