RailsAPI×AngularでかっこいいWebアプリを作（ろうと試み）る【環境構築編】


先日社内で`Railsをものすごい速さで習得しようよ合宿`が開催された。
自分はRailsを少しだけかじったことがあったためまだ見ぬAPIモードを果敢に攻めてみたのでメモ。

## 環境
ruby   ： 2.2.4
Rails  ： 5.0.0.1
Angular： 1.5.8（だってAngular2はずっと`-rc.~`なんだもの）
Webpack： 1.13.2
AngularMaterial：1.1.1

[ソース](https://github.com/4take/rails-api-angular)も置いとくので参考にどうぞ。

### Railsの環境は出来ているものとして話を進めていくよ

## 1.RailsAPI
【参考】
[Rails による API 専用アプリ](http://railsguides.jp/api_app.html)

大事なことは全てRailsGuidesに（ちゃんと日本語で）書いてあるのでここでは端折ります。

API専用アプリを作るためにはいつもの`rails new`コマンドに`--api`オプションをつけるだけになります。
db使う場合は後から設定いじるのは手間なので`-d`オプションもつけましょう。

```
$rails new rails-api-angular --api
```

出てきたディレクトリ構成がこう

<img src="https://qiita-image-store.s3.amazonaws.com/0/127115/f9855fd7-015a-5a50-9ffd-0a4238ca27d6.png" width="250px"/>

通常のRailsアプリと比較して

- app/helpers
- app/frontend

が減って

- app/jobs

が増えてる。

`app/controllers/application_controller.rb`も`::Base`から`::API`に変わってます。

```application_controller.rb
class ApplicationController < ActionController::API
end
```

`Gemfile`もすっきり

```Gemfile
source 'https://rubygems.org'


# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 5.0.0', '>= 5.0.0.1'
# Use mysql as the database for Active Record
gem 'mysql2', '>= 0.3.18', '< 0.5'
# Use Puma as the app server
gem 'puma', '~> 3.0'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
# gem 'jbuilder', '~> 2.5'
# Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 3.0'
# Use ActiveModel has_secure_password
# gem 'bcrypt', '~> 3.1.7'

# Use Capistrano for deployment
# gem 'capistrano-rails', group: :development

# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible
# gem 'rack-cors'

group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platform: :mri
end

group :development do
  gem 'listen', '~> 3.0.5'
  # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
  gem 'spring'
  gem 'spring-watcher-listen', '~> 2.0.0'
end

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

```

## 2.Angular

今回は作ったアプリの直下にAngularの環境も作ってしまい、
それをwebpackでbundleしてpublicに突っ込んであげるという方式を取ります。

【参考】
[WebPackを使ってRailsからJavaScriptを楽に良い感じに分離する](http://qiita.com/necojackarc/items/afa674ab10aafa9784eb)
[Rails 4.2でSprocketsを捨ててwebpackに移行する](http://qiita.com/dakatsuka/items/c815a27b9740c843bee0)

### 2-1.Angularとwebpackをインストール

まずは`package.json`を準備（既存プロジェクトから持ってきたので必要無いのが混ざってても気にしないでね）

```package.json
{
  "name": "rails-api-angular",
  "version": "1.0.0",
  "description": "rails-api-angular",
  "scripts": {
    "dev": "webpack-dev-server --hot --inline --port 3500 --progress --profile --colors",
    "build": "NODE_ENV=production webpack -p --progress --profile --colors "
  },
  "dependencies": {
    "angular": "1.5.8",
    "angular-animate": "1.5.8",
    "angular-aria": "1.5.8",
    "angular-cookies": "1.5.8",
    "angular-material": "1.1.1",
    "angular-material-icons": "0.7.1",
    "angular-messages": "1.5.8",
    "angular-resource": "1.5.8",
    "angular-route": "1.5.8",
    "angular-sanitize": "1.5.8",
    "clean-webpack-plugin": "^0.1.9",
    "css-loader": "^0.23.1",
    "es6-promise": "~3.2.1",
    "eslint-plugin-jsx-a11y": "^1.5.5",
    "eslint-plugin-react": "^5.2.2",
    "extract-text-webpack-plugin": "^1.0.1",
    "file-loader": "^0.8.5",
    "font-awesome": "^4.6.3",
    "font-awesome-sass-loader": "^1.0.1",
    "imports-loader": "^0.6.5",
    "jquery": "^2.2.4",
    "json-loader": "^0.5.4",
    "node-sass": "^3.7.0",
    "sass-loader": "^3.2.0",
    "style-loader": "^0.13.1",
    "ts-loader": "^0.8.1",
    "url-loader": "^0.5.7",
    "webpack": "^1.13.2",
    "webpack-manifest-plugin": "^1.0.1"
  },
  "devDependencies": {
    "es5-shim": "^4.5.7",
    "eslint": "^2.9.0",
    "eslint-config-airbnb": "^9.0.1",
    "eslint-import-resolver-webpack": "^0.2.4",
    "eslint-plugin-import": "^1.7.0",
    "typescript": "^1.8.10",
    "webpack-dev-middleware": "^1.6.1",
    "webpack-dev-server": "^1.14.1",
    "webpack-hot-middleware": "^2.10.0"
  }
}

```

インストールします

```
$npm install
```

### 2-2. Angularプロジェクト作成

必要最低限のAngularプロジェクト作っていきます。

```
$mkdir app/angular
$cd app/angular/
$mkdir javascripts
$touch javascripts/application.ts
$mkdir stylesheets
$touch stylesheets/application.sass
```

Typescriptの型定義を用意します。
今回はいつも使っている[dtsm](http://qiita.com/vvakame/items/38b953ab0f4de63cce8b)使っていきますがどの型定義管理ツールも大体勝手は一緒なので好きなものを使ったらいいじゃない。

```
$dtsm init
```

できた`dtsm.json`を書き換えます。

```dtsm.json
{
  "repos": [
    {
      "url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git",
      "ref": "master"
    }
  ],
  "path": "typings",
  "bundle": "typings/bundle.d.ts",
  "link": {
    "npm": {
      "include": true
    }
  },
  "dependencies": {
    "node/node.d.ts": {
      "ref": "17795ae18fc214be862fe578ad48c28fecfef8a6"
    },
    "angularjs/angular.d.ts": {
      "ref": "17795ae18fc214be862fe578ad48c28fecfef8a6"
    },
    "angular-material/angular-material.d.ts": {
      "ref": "17795ae18fc214be862fe578ad48c28fecfef8a6"
    },
    "angular-cookie/angular-cookie.d.ts": {
      "ref": "17795ae18fc214be862fe578ad48c28fecfef8a6"
    },
    "angularjs/angular-animate.d.ts": {
      "ref": "0c5c7a2d2bd0ce7dcab963a8402a9042749ca2da"
    }
  }
}
```

インストールします

```
$dtsm install
```

`javascripts/application.ts`でangularとその他諸々、`stylesheets/application.sass`を`require`します

```application.ts
/// <reference path="./typings/bundle.d.ts"/>

// angular
import angular = require('angular')
require('angular-material')
require('angular-cookies')
require('angular-resource')
require('angular-sanitize')
require('angular-route')
require('angular-animate')
require('angular-material-icons')
require('es5-shim')

let app = angular.module('App', [
	'ngMaterial',
	'ngCookies',
	'ngResource',
	'ngSanitize',
	'ngRoute',
	'ngMdIcons',
	'ngAnimate'
]);

//stylesheets
require('../stylesheets/application')

//angular-materialのTheme
app.config(($mdThemingProvider) => {
	$mdThemingProvider.theme('default')
					  .primaryPalette('grey', {
						  'default': '100'
					  })
					  .accentPalette('pink', {
						  'default': '700'
					  });
});

//Controllerも一緒に定義しちゃいました
export default class AppCtrl {
	private title: string = 'Hello RailsAPI × Angular！'
	static $inject = ['$rootScope', '$scope', '$cookies', '$window', '$timeout', '$location']
	constructor(
		private rootScope: ng.IRootScopeService,
		private scope: ng.IScope,
		private cookies: any,
		private window: ng.IWindowService,
		private timeout: ng.ITimeoutService,
		private location: ng.ILocationService
	) {
	}
}

app.controller('AppCtrl', AppCtrl)
```

`stylesheets/application.sass`でangular-materialをimportします

```application.sass
@import '~angular-material/angular-material.css'

body
  background-color: whitesmoke

.header
  md-toolbar
    color: #D81B60
    button
      height: 100%
      margin: 0px

// angularの評価式が一瞬見えちゃう問題対応
[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak
  display: none !important
```


### 2-3.webpack設定

プロジェクト直下に`webpack.config.js`を作成する

```webpack.config.js
const DEBUG = process.env.NODE_ENV === 'development' || process.env.NODE_ENV === undefined;
const webpack = require('webpack');
const path = require('path');

/**
 * Require webpack plugins
 */
const ManifestPlugin = require('webpack-manifest-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');

/**
 * Environment settings
 */
const devtool = DEBUG ? '#inline-source-map' : '#eval';
const fileName = DEBUG ? '[name]' : '[name]-[hash]';
const publicPath = DEBUG ? 'http://localhost:3500/assets/' : '/assets/';

/**
 *  Entries
 */
const entries = {
	application: ['./app/angular/javascripts/application.ts']
}

/**
 * Add plugins
 */
const plugins = [
	new ExtractTextPlugin(fileName + '.css')
]

if (DEBUG) {
	plugins.push(new webpack.NoErrorsPlugin());
} else {
	plugins.push(new ManifestPlugin({fileName: 'webpack-manifest.json'}));
	plugins.push(new webpack.optimize.UglifyJsPlugin({compress: {warnings: false}}));
	plugins.push(new CleanWebpackPlugin(['assets'], {
		root: __dirname + '/public',
		verbose: true,
		dry: false
	}));
}

module.exports = {
	entry: entries,
	output: {
		path: __dirname + '/public/assets',
		filename: fileName + '.js',
		publicPath: publicPath
	},
	devtool: devtool,
	plugins: plugins,
	module: {
		loaders: [
			{
				test: /\.ts$/,
				loader: 'ts',
				exclude: [/node_modules/]
			},
			{
				test: /\.css$/,
				loader: ExtractTextPlugin.extract('style-loader', 'css-loader?minimize')
			},
			{
				test: /\.scss$/,
				loader: ExtractTextPlugin.extract('style-loader', 'css-loader!sass-loader?minimize')
			},
			{
				test: /\.sass$/,
				loader: ExtractTextPlugin.extract('style-loader', 'css-loader!sass-loader?minimize')
			},
			{
				test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
				loader: 'url-loader?mimetype=image/svg+xml'
			},
			{
				test: /\.woff(\d+)?(\?v=\d+\.\d+\.\d+)?$/,
				loader: 'url-loader?mimetype=application/font-woff'
			},
			{
				test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
				loader: 'url-loader?mimetype=application/font-woff'
			},
			{
				test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
				loader: 'url-loader?mimetype=application/font-woff'
			},
			{
				test: /\.(jpg|png|gif)$/,
				loader: DEBUG ? 'file-loader?name=[name].[ext]' : 'file-loader?name=[name]-[hash].[ext]'
			}
		]
	},
	resolve: {
		root: path.resolve(__dirname, 'app', 'angular'),
		extensions: ['', '.js', '.ts', '.css', '.scss', '.sass'],
	},
	devServer: {
		headers: {
			"Access-Control-Allow-Origin": "http://localhost:3000",
			"Access-Control-Allow-Credentials": "true"
		}
	}
}
```

これで開発中はサーバーを立ち上げてwatchしてくれるようになる。便利。
先ほどの`package.json`にこっそり定義しておいたコマンドを叩いてみる

```
$npm run dev
```
するとこんなエラー出た

```
ERROR in ./app/angular/javascripts/application.ts
Module build failed: TypeError: Path must be a string. Received undefined
    at assertPath (path.js:7:11)
    at Object.dirname (path.js:1324:5)
    at ensureTypeScriptInstance (/Users/nozakishohei/WorkSpace/repository/sample/rails-api-angular/node_modules/ts-loader/index.js:156:103)
    at Object.loader (/Users/nozakishohei/WorkSpace/repository/sample/rails-api-angular/node_modules/ts-loader/index.js:403:14)
 @ multi application
```

なるほど。[ts-loader](https://github.com/TypeStrong/ts-loader)は`tsconfig.json`が必須だってよ！

というわけで作ってあげる

```
$touch app/angular/javascripts/tsconfig.json
```

```tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "sourceMap": true
  },
  "exclude": [
    "node_modules"
  ]
}
```

これで再度`npm run dev`すると成功した。
あとは画面から呼ぶだけ。

## 3.画面作る
タイトルから一貫して`RailsAPI`という単語を使ってきたがここにきて掟をぶち破る。（root設定して1画面だけ作っちゃう）

### 3-1 rootの設定をしてしまう（1画面だけだから許して）

`routes.rb`に`root`を設定する

もう何も思いつかなくて`root#index`にしてしまったが好きな名前をつけてね。

```config/routes.rb
Rails.application.routes.draw do
	root 'root#index'
end
```

コントローラーも作る

```controllers/root_controller.rb
class RootController < ActionController::Base
	def index
	end
end
```

それっぽいトップ画面も作る

```views/root/index.erb
<html ng-app="App">
<head>
  <meta charset="UTF-8">
  <meta name="viewport", content="width=device-width, initial-scale=1, maximum-scale=1">
  <title>rails-api-angular</title>
</head>
<body ng-controller="AppCtrl as appCtrl" ng-cloak>
  <header class="header md-whiteframe-3dp">
    <md-toolbar md-scroll-shrink>
      <div class="md-toolbar-tools">
        <h3>
          <span>rails-api-angular</span>
        </h3>
        <span flex></span>
        <md-button class="md-accent">Sign up</md-button>
        <md-button class="md-accent">Log in</md-button>
      </div>
    </md-toolbar>
  </header>

  <div layout="row" layout-align="center center">
    <h1>{{appCtrl.title}}</h1>
  </div>

<%= stylesheet_link_tag webpack_asset_path('application.css') %>
<%= javascript_include_tag webpack_asset_path('application.js')%>
</body>
</html>
```

webpackしたリソースを読み分けるヘルパー関数`webpack_asset_path`を生やす。

```app/helpers/root_helper.rb
module RootHelper
	def webpack_asset_path(path)
		if Rails.env.development?
			return "http://localhost:3500/assets/#{path}"
		end

		host = Rails.application.config.action_controller.asset_host
		manifest = Rails.application.config.assets.webpack_manifest
		path = manifest[path] if manifest && manifest[path].present?
		"#{host}/assets/#{path}"
	end
end
```
ついにrailsを起動して`localhost:3000`にアクセスしてみる

```
$rails s
```

きた！

![Kobito.oUViNg.png](https://qiita-image-store.s3.amazonaws.com/0/127115/d4650122-2a2b-2358-969d-adf8f5831d8e.png "Kobito.oUViNg.png")