デザイン出身の方やWebで開発を始めた方でも心が折れないVue.js SPA（ElixirによるAPI開発付き）

[fukuoka.ex](https://fukuokaex.connpass.com/)代表のpiacereです
ご覧いただいて、ありがとうございます:bow:

Vue.jsを始めると、Vue CLIやnpmを使ったり、webpackを使う解説やチュートリアルが多いですが、デザインから動的なページ作成を始めた方や、Webで開発を始めたのでサーバサイドプログラミングでの複雑な手順に慣れていない方には、本質に辿り付く前に心が折れそうになります

そこで、CDN … つまり、URL指定のライブラリ利用のみでも、Vue.jsでSPA（Single Page Application）が組める、ということを実感していただくチュートリアルを作ってみました

流れとしては、まずブラウザのみで動くページをCDN版のVue.jsで作成した後、簡単なDBアクセスAPIを作成（開発にはElixir／Phoenixを使用）し、Vue.jsからデータ追加／更新／削除するSPAもCDN版のVue.jsでこなそうと思います

なお、[「Phoenix」](https://qiita.com/piacerex/items/b7787580fce5f148242f#phoenix%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)は、ElixirのWebフレームワークです

内容が、面白かったり、役に立ったら、「いいね」よろしくお願いします :wink:

# Vue.jsとHTMLの間でデータ受け渡しを行う

以下ファイルを適当な場所に作成します

```html:index.html
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>

<div id="app">

<h1>Posts</h1>
<table border="1">
<tr v-for="result in results">
	<td style="padding: 10px;">{{ result.id }}</td>
	<td style="padding: 10px;"><input type="text" v-model="result.title"></td>
	<td style="padding: 10px;"><input type="text" v-model="result.body"></td>
</tr>
</table>
<button v-on:click="onView">データ表示</button>

</div>

<script>
var app = new Vue
( {
	el: '#app',
	data: 
	{
		results:   [], 
	}, 
	mounted()
	{
		this.results.push( { id: 1, title: 'title1', body: 'body1' } )
		this.results.push( { id: 2, title: 'title2', body: 'body2' } )
		this.results.push( { id: 3, title: 'title3', body: 'body3' } )
	}, 
	methods: 
	{
		onView: async function( evt )
		{
			console.log( this.results )
		}, 
	}, 
} )
</script>
```

ブラウザを起動し、上記ファイルをドラッグ＆ドロップするか、オープンすると、以下の画面が表示されます
![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/4417fd00-25c2-1fa6-254e-98895c786232.png)

F12キーもしくはCtrl+Shift+iキーを押して、コンソールウインドウを開いた後、テーブル表示しているデータをVue.js側で保持している中身を「データ表示」ボタンでコンソールウインドウに出力します

コンソールウインドウに表示される、「```(3) [{...}, {...}, …```」が、Vue.js側で保持しているデータです
![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/e17ba3aa-05f3-4d5a-13ea-ef1f7361e027.png)

この「```(3) [{...}, {...}, …```」の左側の「▶」をクリックして開くと、「```0: {__ob__: _e}```」～「```2: {__ob__: _e}```」と出てきますが、これは画面上のテーブル3行に表示されているデータを表し、たとえば「```0: {__ob__: _e}```」の左側の「▶」をクリックして開くと、テーブルに表示されているデータと同じ値がコンソールウインドウで見れます
![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/05db9a7c-7437-6761-fe3c-d7bb51695b19.png)

テーブルの入力フィールドで値を書き換え、「データ表示」ボタンを押すと、書き換わったデータがコンソールウインドウで確認できます
![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/f63a3f25-2973-77e7-0ca9-3ba2f5f16892.png)

このような感じで、HTML側での入力値変更と、Vue.js側のデータ反映が、上記コードのみで実行できる訳です

大きく分けて、「①Vue.js側で保持しているデータ部分」と「②HTMLでのVue.js側データ表示」の2つによって実現されています

# ① Vue.js側で保持しているデータ部分（と初期化）

以下コードが、Vue.js側で保持しているデータ部分（data:の配下）と、その初期化（mounted）です

ページがロードされると、mountedが呼び出され、data:配下のresultsに、3件のデータが追加されます

```javascript:index.html
	data: 
	{
		results:   [], 
	}, 
	mounted()
	{
		this.results.push( { id: 1, title: 'title1', body: 'body1' } )
		this.results.push( { id: 2, title: 'title2', body: 'body2' } )
		this.results.push( { id: 3, title: 'title3', body: 'body3' } )
	}, 
```

# ② HTMLでのVue.js側データ表示

以下HTMLで、上記で初期化されたVue.js側データを表示します

入力フィールドで変更されたデータをVue.js側データに反映するには、「v-model」という属性を使います

v-modelの中に書くのは、Vue.jsの「data:」部で定義したモデルとなります（実際には、v-forでバラされたモデル断片を指定しています）

なお、v-modelは、HTMLからVue.jsへの一方通行の反映では無く、Vue.jsからHTMLへの反映も兼ねているので、mountedでの初期化では、Vue.js側で「data:」部のモデルを書き換えることで、画面表示に自動反映されています（双方向データバインディングと呼ばれます）

```javascript:index.html
…
<table border="1">
<tr v-for="result in results">
	<td style="padding: 10px;">{{ result.id }}</td>
	<td style="padding: 10px;"><input type="text" v-model="result.title"></td>
	<td style="padding: 10px;"><input type="text" v-model="result.body"></td>
</tr>
</table>
…
```

基本的には、ここまでのコードだけで、Vue.jsとHTMLとの間で、データの受け渡しが行なえます

このように非常に簡潔なコードにも関わらず、アプリ開発の根幹となる「表示」と「データ」の連携が自動的に行われることが、Vue.jsの最大の強みです

# その他コード解説

さて、データの受け渡し以外の部分も見てみましょう

## Vue.js側データのコンソールウインドウへのログ出力

以下HTMLとJSで、「データ表示」ボタンの表示と、クリック時のコンソールウインドウへのログ表示を行っています

「v-on:click」という属性を使うことで、クリック時のハンドラメソッドを指定できます

「v-on:click」以外にも、「v-on:change」や「v-on:focus」、「v-on:blur」等、JavaScriptで利用可能なハンドラと同じものをVue.jsでも利用できます

なお、引数を指定することもでき、引数指定無の場合は、クリックイベントが発生したパーツ（今回だとbutton）のDOMが、暗黙の引数として渡されます

```javascript:index.html
…
<button v-on:click="onView">データ表示</button>
…
	methods: 
	{
		onView: async function( evt )
		{
			console.log( this.results )
		}, 
	}, 
…
```

## Vue.jsの有効範囲

「var app = new Vue」で始まるブロックが、Vue.jsのメイン処理です

メイン処理の「el」で指定したものと同じidを、上方のdivタグで指定していますが、Vue.jsはこのdivタグの内部でのみ有効です

```javascript:index.html
…
<div id="app">
…
</div>
…
var app = new Vue
( {
	el: '#app',
…
```

## CDNでのライブラリ導入

先頭にある、URL指定で、Vue.jsと、APIアクセス用ライブラリ「axios」をロードしています

```html:index.html
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>
…
```

このように、CDNのみでも、Vue.jsは利用できるので、心が折れること無く、気軽にVue.jsを始められます

# Vue.jsと自前のAPIを連結する

では、このページをベースに、テーブルの中身のデータを、自前のAPIにすげ替えてみましょう

ここでは、自前のAPIをElixir＋Phoenixで作ってみます

# Elixirのインストール

Elixirを使い始めるのに、3種類の方法があります

1. インストーラ／Homebrewを使う
2. ソースコードからビルドする
3. DockerでElixirイメージをインスト―ル (pull) する

Windows／macOSは1.、Linux含むUNIX系は2.、普段Dockerを使い慣れている方は3.がオススメです

## 1. インストーラ／Homebrewを使う

下記URLの手順に沿って、Elixirをインストールします
https://elixir-lang.org/install.html

Windowsはインストーラをダウンロードしてインストール、macOSはHomebrewでインストールと、簡単です

![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/c45e99a6-00b7-80a6-3aed-31099ee1b8dc.png)

なお、Linux含むUNIX系の手順も記載されていますが、手順通りにすると、古いバージョンがインストールされるため、2. の方が良いです

## 2. ソースコードからビルドする

以下の手順通り、まずErlangをソースコードからビルドして、インストールします

なお、実施するタイミング次第では、新しいものがリリースされているかも知れないので、気になる方は、下記URLをチェックして、適宜、変更してください
http://erlang.org/download

```sh
wget http://erlang.org/download/otp_src_20.3.tar.gz
tar vzfx otp_src_20.3.tar.gz
cd otp_src_20.3/
./configure --enable-hipe
make && make install
```

次に、Elixirをソースコードからビルドして、インストールします

こちらも、Elixirのメジャーバージョンが新しくなっている場合は適宜、バージョンを変更してください

```sh
git clone https://github.com/elixir-lang/elixir/
cd elixir
git checkout v1.6
git pull
export PATH="${PATH}:/usr/local/bin"
make && make install
elixir -v
```

## 3. DockerでElixirイメージをインスト―ル (pull) する

下記URLを、「Docker Community Edition (CE)」までスクロールし、利用OS毎のDockerをインストールします
https://www.docker.com/get-docker

![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/3cf42635-c02e-c62f-f39c-62dfd62df79b.png)

その後、以下コマンドでElixirイメージを入れます

```sh
docker pull trenpixster/elixir
```

以下コマンドで、Elixirイメージのコンテナを起動します

```sh
docker run -p 4000:4000 -i -t  trenpixster/elixir /bin/bash
```

# PostgreSQLのインストール

下記OS毎のインストール手順を実施してください

なお、postgresユーザのパスワードは、「postgres」とするのを忘れず行ってください

Windows：https://eng-entrance.com/postgresql-download-install
macOS：https://qiita.com/okame_qiita/items/ac7b6a7d96d07ecbc50b
Ubuntu：https://qiita.com/eighty8/items/82063beab09ab9e41692
CentOS 7：https://weblabo.oscasierra.net/postgresql10-centos7-install/
CentOS 6：https://weblabo.oscasierra.net/postgresql-installing-postgresql9-centos6-1/

# Phoenixのインストール

以下コマンドでPhoenixをインストールします

```
mix archive.install hex phx_new 1.4.0
```

# Vue.js向けAPI用Phoenix PJを作成

Phoenix PJを作成します

```
mix phx.new vue_sample --no-webpack
Fetch and install dependencies? [Yn] （←y、Enterを入力）
…
cd vue_sample
```

```
mix ecto.create
```

Phoenixサーバーを起動します

```
iex -S mix phx.server
```

ブラウザで「```http://localhost:4000```」にアクセスすると、Phoenixで作られたWebページが見れます（この後も、このページを見ますので、閉じずに、開いたままにしておいてください）
![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/818578fd-10b1-ddfa-c32e-71526aa52a9a.png)

# PhoenixでAPIを作る

PhoenixでAPIを作るには、mixコマンドで、以下のように行います

Ctrl+cを2回押して、一度、Phoenixを停止してから、コマンドを入力します

```
mix phx.gen.json Api Post posts title:string body:text
```

以下ログと、実行後の作業指示が示されます

```
* creating lib/vue_sample_web/controllers/post_controller.ex
* creating lib/vue_sample_web/views/post_view.ex
* creating test/vue_sample_web/controllers/post_controller_test.exs
* creating lib/vue_sample_web/views/changeset_view.ex
* creating lib/vue_sample_web/controllers/fallback_controller.ex
* creating lib/vue_sample/api/post.ex
* creating priv/repo/migrations/20181021164507_create_posts.exs
* creating lib/vue_sample/api/api.ex
* injecting lib/vue_sample/api/api.ex
* creating test/vue_sample/api/api_test.exs
* injecting test/vue_sample/api/api_test.exs

Add the resource to your :api scope in lib/vue_sample_web/router.ex:

    resources "/posts", PostController, except: [:new, :edit]


Remember to update your repository by running migrations:

    $ mix ecto.migrate
```

まず、ルーティングにAPI用エントリーとして、上記「resources "/posts", ～」を、「get "/", ～」直下に追記します

```elixir:lib/vue_sample_web/router.ex
defmodule VueSampleWeb.Router do
  use VueSampleWeb, :router
  …
  scope "/", VueSampleWeb do
    pipe_through :browser # Use the default browser stack

    get "/", PageController, :index
    resources "/posts", PostController, except: [:new, :edit]
  end
  …
```

マイグレートします

```
mix ecto.migrate
```

以下ログのように、テーブルが作成されます

```
01:51:55.226 [debug] Selecting all records by match specification `[{{:schema_migrations, :"$1", :"$2"}, [], [[:"$1"]]}]` with limit nil

01:51:55.282 [info]  == Running VueSample.Repo.Migrations.CreatePosts.change/0 forward

01:51:55.282 [info]  create table posts

01:51:55.310 [info]  == Migrated in 0.0s
```

router.exにデフォルト設定されているSCRF対策は、API利用時に不要かつ邪魔なので、解除します

```elixir:lib/vue_sample_web/router.ex
defmodule VueSampleWeb.Router do
  use VueSampleWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
#   plug :protect_from_forgery
    plug :put_secure_browser_headers
  end
  …
```

Phoenixを起動してください

```
iex -S mix phx.server
```

# Vue.jsから自前のAPIを呼び出す

冒頭のVue.jsで作ったページをベースに、Phoenixで作成したAPIを呼び出すVue.jsへとindex.html.eexを置き換えます

処理概要は、以下の通りです

- データ追加（1件ずつ）
 - POSTメソッドで追加APIを呼び出す
 - メソッドに「async」、axios.deleteに「await」を付け、「同期処理」化
 - 削除が完全に終わってから、データ取得するよう、「同期処理」にするため
 - 「非同期処理」だと、データ削除が完了する前にデータ取得する可能性がある
 - その後、データ取得し直すことで画面更新する
- データ全件更新
 - v-modelで更新されたresultsを全件、PUTメソッドで更新APIを呼び出し続ける
 - results全件を更新に回す部分は、forEachを使うと、JSでも関数型っぽく書ける
- 1件毎のデータ削除
 - DELETEメソッドで削除APIをid指定付きで呼び出す
 - 追加同様、「同期処理」化
 - その後、データ取得し直すことで画面更新する



```html:lib/vue_sample_web/templates/page/index.html.eex
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>

<div id="app">

<h1>Posts</h1>
<table border="1">
<tr v-for="result in results">
	<td style="padding: 10px;"><input type="text" v-model="result.title"></td>
	<td style="padding: 10px;"><input type="text" v-model="result.body"></td>
	<td><button v-on:click="onDelete( result.id )">削除</button></td>
</tr>
<tr>
	<td style="padding: 10px;"><input type="text" v-model="new_title"></td>
	<td style="padding: 10px;"><input type="text" v-model="new_body"></td>
	<td><button v-on:click="onCreate">追加</button></td>
</tr>
</table>
<button v-on:click="onUpdate">全件更新</button>

</div>

<script>
	var app = new Vue
	( {
		el: '#app',
		data: 
		{
			results:   [], 
			new_title: '', 
			new_body:  '', 
		}, 
		mounted()
		{
			axios.get( '/posts' )
			.then( response => { this.results = response.data.data } )
		}, 
		methods: 
		{
			onUpdate: function( evt )
			{
				this.results.forEach( ( result, i ) => 
				{
					axios.put( '/posts/' + result.id, 
						{ 
							'post':
							{
								'title': result.title, 
								'body':  result.body, 
							} 
						} )
				} )
			}, 
			onDelete: async function( id )
			{
				await axios.delete( '/posts/' + id )

				axios.get( '/posts' )
				.then( response => { this.results = response.data.data } )
			}, 
			onCreate: async function( evt )
			{
				await axios.post( '/posts/', 
					{ 
						'post':
						{
							'title': this.new_title, 
							'body':  this.new_body, 
						} 
					} )

				this.new_title = ''
				this.new_body  = ''

				axios.get( '/posts' )
				.then( response => { this.results = response.data.data } )
			}, 
		}, 
	} )
</script>
```

上記ファイルを保存すると、データの追加／更新／削除ができるようになるので、色々遊んでみてください
![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/f97310f8-a3d5-cf01-4899-0259a0613e14.png)

# p.s.「いいね」よろしくお願いします

ページ左上の ![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/4d515047-cc48-382e-c2b1-3ad0cc50dbbf.png) や ![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/a4e3da58-70a3-4197-95a2-6a6906650d01.png) のクリックを、どうぞよろしくお願いします:bow:
ここの数字が増えると、書き手としては「ウケている」という感覚が得られ、連載を更に進化させていくモチベーションになりますので、もっとElixirネタを見たいというあなた、私達と一緒に盛り上げてください！:tada: