angularjs で Twitter クライアントを作成し AWS.S3 で動かす

大寒波です。
霜も降り息も白くなり、やっと冬らしくなってきました。
気温が大きく変化する時期は体調を崩しやすくなります。
風邪に気をつけて、外から帰ったら手洗いうがいを怠らないようにしましょう。

AngularJS のお勉強がてら Twitter クライアントでも作ろうと思いました。
やったことを自分の備忘録も兼ねて書いておきます。

## やりたいこと

* Twitter クライアント作る（まずはタイムラインの表示）
 * AngularJS で。
 * yeoman 使いたい。
* AWS.S3 に配置してスマホで見たい。


んで、出来上がったものはこんなものです。
![mytwitter_demo.gif](https://qiita-image-store.s3.amazonaws.com/0/86201/9a6cc4cd-b48d-b4f2-3ad1-f871df670ee1.gif)

## Twitter アプリ登録
今回作成するアプリは Twitter に OAuth で認証（アクセス）します。
まずは、Twitter の [Application Management](https://apps.twitter.com/) にて今回作成するアプリを登録します。

<img width="400" alt="twitter_dev.png" src="https://qiita-image-store.s3.amazonaws.com/0/86201/6c2ea42e-2028-a826-76bd-1e26bf506214.png">

※ 諸事情により、上記で登録しているアプリ名（twikurom）とクライアントアプリ名（mytwitter）が異なっていますが気にしないでください。


Callback URL は OAuth の認証URL である https://oauth.io/auth を設定してください。
設定しないとログイン後にアプリに戻ってきません。

## OAuth.io アプリ登録

今回は、OAuth のプロバイダ認証を楽にしてくれる [OAuth.io](https://oauth.io/) サービスを使用します。
Twitter で登録した API Key を設定します。

<img width="400" alt="OAuth_twitter.png" src="https://qiita-image-store.s3.amazonaws.com/0/86201/d00ff73f-a72e-86b6-42a3-e7d7d20be426.png">

twikurom というアプリを追加していますが、デフォルトで追加されているアプリでも問題ないです。

General タブを開きます。
`Public Key` は後ほど使用します。
Domains & URLs whitelist に `localhost` と `*.amazonaws.com` を追加してください。

<img width="400" alt="OAuth_General.png" src="https://qiita-image-store.s3.amazonaws.com/0/86201/05caa12a-4a5c-24f5-a339-485081736737.png">

## アプリ作成

### 開発準備

まずは、ディレクトリの作成と必要なパッケージをインストールします。
yeoman の generator は公式の generator-angular を使用します。

```bash
$ mkdir mytwitter; cd mytwitter
$ npm install yo bower grunt-cli -g
$ npm install generator-angular generator-karma --save-dev
```
ジェネレータをローカルインストールしましたが、他でも使う場合は `-g` つけてグローバルにインストールしてもいいと思います。

yeoman でプロジェクトを作成します。

```bash
$ yo angular
```

んで、色々聞かれます。

```
     _-----_
    |       |    .--------------------------.
    |--(o)--|    |    Welcome to Yeoman,    |
   `---------´   |   ladies and gentlemen!  |
    ( _´U`_ )    '--------------------------'
    /___A___\    
     |  ~  |     
   __'.___.'__   
 ´   `  |° ´ Y ` 

Out of the box I include Bootstrap and some AngularJS recommended modules.

? Would you like to use Gulp (experimental) instead of Grunt? No
? Would you like to use Sass (with Compass)? No
? Would you like to include Bootstrap? Yes
? Which modules would you like to include? 
 ◉ angular-animate.js
 ◯ angular-aria.js
❯◉ angular-cookies.js
 ◉ angular-resource.js
 ◯ angular-messages.js
 ◉ angular-route.js
 ◉ angular-sanitize.js
 ◉ angular-touch.js
```

こんな感じで回答すると、プロジェクトのテンプレートがゔぁーっと作成されます。
途中で package.json を上書きするか聞かれてます（ログ流れちゃって見つけづらいんすけど）。Enter 押すと上書きしちゃうんで気をつけてくださいね。

```bash
npm WARN karma-jasmine@0.3.6 requires a peer of jasmine-core@* but none was installed.
npm WARN karma-phantomjs-launcher@0.2.3 requires a peer of karma@>=0.9 but none was installed.
npm WARN karma-phantomjs-launcher@0.2.3 requires a peer of phantomjs@>=1.9 but none was installed.
npm WARN grunt-karma@0.12.1 requires a peer of karma@^0.13.0 || >= 0.14.0-rc.0 but none was installed.
```
不足しているパッケージがあった場合、追加しましょう。
karma、jasmine-core、phantomjs が足らんと言われているので追加します。

```bash
$ npm install karma jasmine-core phantomjs --save-dev
```

ローカルサーバを起動して確認します。

```bash
$ grunt serve
```

<img width="300" alt="angular.png" src="https://qiita-image-store.s3.amazonaws.com/0/86201/1b655a63-af57-62d3-397d-d01cc0f2f163.png">

おk。
ライブリロードしているので、ソースを変更して保存すると即座に反映されます。すごい。

ビルドしてローカルサーバで確認します。

```bash
$ grunt serve:dist
```
先ほどと同じ画面が表示されますが、distディレクトリに配置したビルド後のアプリが動いています。

```
dist
├── 404.html
├── favicon.ico
├── fonts
│   ├── glyphicons-halflings-regular.eot
│   ├── glyphicons-halflings-regular.svg
│   ├── glyphicons-halflings-regular.ttf
│   ├── glyphicons-halflings-regular.woff
│   └── glyphicons-halflings-regular.woff2
├── images
│   └── yeoman.8cb970fb.png
├── index.html
├── robots.txt
├── scripts
│   ├── scripts.aa0894ba.js
│   └── vendor.cca3e06b.js
└── styles
    ├── main.16c86e60.css
    └── vendor.2ac5f564.css
```

### Twitterクライアント作成

まずはじめに、OAuth の SDK を bower でインストールします。
bower でインストールしたモジュールは、ビルド時に `index.html` に挿入されます。便利です。

```bash
$ bower install oauth-js --save
```

`main.html` を編集してざっくりとしたレイアウトを決めます。

```html:mytwitter/app/scripts/controllers/main.html
<div class="container">
	<h1>My Twitter</h1>

	<div class="row">
		<div class="col-xs-12">
			<button id="connectButton" type="button" class="btn btn-primary">サインイン</button>
			<button id="getTimelineButton" type="button" class="btn btn-info">更新</button>
			<button id="signOut" type="button" class="btn btn-link">サインアウト</button>
		</div>
	</div>
	<hr/>

	<div class="row">
		<div class="col-xs-12" id="results">
			<div class="row well">
				<div class="col-xs-2">
					<img src="https://pbs.twimg.com/profile_images/679162128750665729/r6ALQmhy_bigger.png" class="img-circle">
				</div>
				<div class="col-xs-10">
					<small>名前</small>
					<br> ツィート
				</div>
			</div>
		</div>
	</div>
</div>
```

`grunt serve` で動かしてみるとこんな感じ

<img width="400" alt="main.html.png" src="https://qiita-image-store.s3.amazonaws.com/0/86201/0253c7c2-1b55-c3f8-6a1a-bdc2ddbb53ce.png">


上記の html に angularjs のディレクティブを記述していきます。

```html:mytwitter/app/scripts/controllers/main.html
<div class="container" ng-controller="MainCtrl">
	<h1>My Twitter</h1>

	<div class="row">
		<div class="col-xs-12">
			<button ng-click="main.connect()" id="connectButton" type="button" class="btn btn-primary">サインイン</button>
			<button ng-click="main.refresh()" id="getTimelineButton" type="button" class="btn btn-info">更新</button>
			<button ng-click="main.signOut()" id="signOut" type="button" class="btn btn-link">サインアウト</button>
		</div>
	</div>
	<hr/>

	<div class="row">
		<div class="col-xs-12" id="results">
			<div class="row well" ng-repeat="tweet in main.tweets">
				<div class="col-xs-2">
					<img ng-src="{{tweet.user.profile_image_url}}" class="img-circle">
				</div>
				<div class="col-xs-10">
					<small>{{tweet.user.name}}</small>
					<br> <span ng-bind-html="tweet.text"></span>
				</div>
			</div>
		</div>
	</div>
</div>
```

Twitter の認証やデータ取得を行うサービスを作成します。
`services.js` を `scripts` 配下に追加します。

```js:mytwitter/app/scripts/services.js
'use strict';

function twitterService($q) {
	var authResult = false;
	return {
		initialize: function() {
			// OAuth.io アプリの公開キー(PublicKey)を設定する。
			OAuth.initialize('xxxxxxxxxxxxxxxxxxxxxxxxxxx', {
				cache: true
			});
			authResult = OAuth.create('twitter');
		},
		isReady: function() {
			return authResult;
		},
		connectTwitter: function() {
			var deferred = $q.defer();
			OAuth.popup('twitter').done(function(result) {
				authResult = result;
				deferred.resolve();
			});
			return deferred.promise;
		},
		clearCache: function() {
			OAuth.clearCache('twitter');
			authResult = false;
		},
		getLatestTweets: function() {
			var deferred = $q.defer(),
				url = '/1.1/statuses/home_timeline.json';

			authResult.get(url)
				.done(function(result) {
					deferred.resolve(result);
				})
				.fail(function(err) {
					deferred.reject(err);
				});
			return deferred.promise;
		}
	};
}

angular.module('mytwitterApp.services', [])
	.factory('twitterService', twitterService);
```

`index.html` にてサービスの `script` タグを追加します。

```html:mytwitter/app/index.html
<!-- build:js({.tmp,app}) scripts/scripts.js -->
<script src="scripts/app.js"></script>
<script src="scripts/services.js"></script>
<script src="scripts/controllers/main.js"></script>
<script src="scripts/controllers/about.js"></script>
<!-- endbuild -->
```

`app.js` にて、モジュールを追加します。

```js:mytwitter/app/scripts/app.js
angular
  .module('mytwitterApp', [
    'ngAnimate',
    'ngCookies',
    'ngResource',
    'ngRoute',
    'ngSanitize',
    'ngTouch',
    'mytwitterApp.services'
  ])
```

最後にコントローラを実装します。

```js:mytwitter/app/scripts/controllers/main.js
'use strict';

function MainController(twitterService) {
	var vm = this;

	vm.tweets = [];
	twitterService.initialize();

	vm.refresh = function() {
		twitterService.getLatestTweets().then(function(result) {
			vm.tweets = vm.tweets.concat(result);
		}, function() {
			// error
		});
	};

	vm.connect = function() {
		twitterService.connectTwitter().then(function() {
			if (twitterService.isReady()) {
				vm.refresh();
			} else {
				console.log('unable to connect twitter');
			}
		});
	};

	vm.signOut = function() {
		twitterService.clearCache();
		vm.tweets = [];
	};

	if (twitterService.isReady()) {
		vm.refresh();
	}
}

angular.module('mytwitterApp')
	.controller('MainCtrl', MainController);
```

初期画面です。
<img width="300" alt="イメージ１.png" src="https://qiita-image-store.s3.amazonaws.com/0/86201/ddbf41a9-de7b-f8c6-ad2f-01a8a51af43f.png">


サインインボタンを押下すると Twitter 認証ダイアログが開きます。
<img width="300" alt="2016-01-20 10.31.56.png" src="https://qiita-image-store.s3.amazonaws.com/0/86201/7aedee5e-d3f2-af49-5ea1-dd6fe18672bb.png">


認証に成功すると元画面にタイムラインが表示されます。
<img width="300" alt="2016-01-20 15.11.41.png" src="https://qiita-image-store.s3.amazonaws.com/0/86201/84209495-9ac0-b7bf-9786-d36af9e2ba3a.png">

## S3

### 配置

ビルドしておきます。

```bash
$ grunt build
```

AWS の S3 に新しいバケットを作成しましょう。
アップロードメニューから、`dist` 配下に作成されたファイルをアップロードします。

<img width="400" alt="s3_upload.png" src="https://qiita-image-store.s3.amazonaws.com/0/86201/b82b4d4f-b4b6-3749-cd4c-d75041e81830.png">


### バケットの設定
バケットのプロパティから、静的ウェブサイトホスティングを有効にします。
エントリポイントは `index.html` を指定し保存します。

アクセス許可 → バケットポリシーの編集 にて、以下を設定します。
バケット名は変更してください。

```json
{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Sid": "PublicReadGetObject",
			"Effect": "Allow",
			"Principal": "*",
			"Action": "s3:GetObject",
			"Resource": "arn:aws:s3:::aws.bucket.name/*"
		}
	]
}
```

静的ウェブサイトホスティングのエンドポイントに設定されている URL にアクセスします。

localhost で実行したものと同様であればOKです。
せっかくだから携帯端末で確認してみましょう。

<img width="200" alt="IMG_2949.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/86201/5243ebfe-ada7-2b8b-cd83-af7edf15f453.jpeg">

ちょっとレイアウトがごめんなさい。



## 参考
* http://www.sitepoint.com/building-twitter-app-using-angularjs/

## バージョン情報
* Chrome: 47.0.2526.106
* node: v4.2.4
* npm: 3.5.3
* yeoman: 1.6.0
 * generator-angular: 0.15.1
 * generator-karma: 1.0.1
* bower: 1.7.2
* grunt-cli v0.1.13
* grunt v0.4.5