93
95

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

fukuoka.ex Elixir/PhoenixAdvent Calendar 2018

Day 25

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

Last updated at Posted at 2018-12-06

fukuoka.ex代表の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」は、ElixirのWebフレームワークです

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

Vue.jsと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

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

コンソールウインドウに表示される、「(3) [{...}, {...}, …」が、Vue.js側で保持しているデータです
image.png

この「(3) [{...}, {...}, …」の左側の「▶」をクリックして開くと、「0: {__ob__: _e}」~「2: {__ob__: _e}」と出てきますが、これは画面上のテーブル3行に表示されているデータを表し、たとえば「0: {__ob__: _e}」の左側の「▶」をクリックして開くと、テーブルに表示されているデータと同じ値がコンソールウインドウで見れます
image.png

テーブルの入力フィールドで値を書き換え、「データ表示」ボタンを押すと、書き換わったデータがコンソールウインドウで確認できます
image.png

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

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

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

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

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

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:」部のモデルを書き換えることで、画面表示に自動反映されています(双方向データバインディングと呼ばれます)

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が、暗黙の引数として渡されます

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タグの内部でのみ有効です

index.html

<div id="app">

</div>
…
var app = new Vue
( {
	el: '#app',

CDNでのライブラリ導入

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

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

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

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

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

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

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のメジャーバージョンが新しくなっている場合は適宜、バージョンを変更してください

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

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

docker pull trenpixster/elixir

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

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

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 "/", ~」直下に追記します

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利用時に不要かつ邪魔なので、解除します

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指定付きで呼び出す
  • 追加同様、「同期処理」化
  • その後、データ取得し直すことで画面更新する
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

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

ページ左上の image.pngimage.png のクリックを、どうぞよろしくお願いします:bow:
ここの数字が増えると、書き手としては「ウケている」という感覚が得られ、連載を更に進化させていくモチベーションになりますので、もっとElixirネタを見たいというあなた、私達と一緒に盛り上げてください!:tada:

93
95
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
93
95

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?