オトナのVue.jsとNuxt.js入門
2018年11月21日(水)@未来会議室
ハンズオン資料
画像管理アプリを作る
- サーバーから画像のリストを取得して表示
- 画像をクリックすると拡大(モダル)で表示
- いいねボタンをつくる
jQueryと比べながらVue.js入門
サーバーからデータを取得して表示(1)
- jQueryの例(jquery.html)
<html>
<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.0/css/bulma.css">
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
</head>
<body>
<div>
<div id="images" class="columns">
</div>
</div>
<script>
(function() {
var url = "https://dog.ceo/api/breed/hound/images";
$.getJSON(url)
.done(function(data) {
$.each(data.message.slice(0, 8), function(i, item) {
var html = "<img src='" + item + "' />";
$("<div class='column' />").html(html).appendTo("#images");
});
});
})();
</script>
</body>
</html>
サーバーからデータを取得して表示(2)
- Vue.jsの例(vue.html)
<html>
<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.0/css/bulma.css">
<script src="https://unpkg.com/vue"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>
</head>
<body>
<div id="app">
<div class="columns">
<div v-for="item in items" class='column'>
<img v-bind:src='item' />
</div>
</div>
</div>
<script>
new Vue({
el: '#app',
data: {
items: []
},
mounted() {
let self = this;
let url = "https://dog.ceo/api/breed/hound/images";
axios.get(url)
.then(function(response) {
self.items = response.data.message.slice(0, 8);
});
}
})
</script>
</body>
</html>
サーバーからデータを取得して表示(3)
- 表示(どちらも同じ)
サーバーからデータを取得して表示(4)
- jQueryの例(jquery.html)
<div>
<div id="images" class="columns">
</div>
</div>
<script>
(function() {
var url = "https://dog.ceo/api/breed/hound/images";
$.getJSON(url)
.done(function(data) {
$.each(data.message.slice(0, 8), function(i, item) {
var html = "<img src='" + item + "' />";
$("<div class='column' />").html(html).appendTo("#images");
});
});
})();
</script>
APIで取得して、
imgタグを作って
IDで親(DOM)を取得して
ひっつける
サーバーからデータを取得して表示(5)
- Vue.jsの例(vue.html)
<div id="app">
<div class="columns">
<div v-for="item in items" class='column'>
<img v-bind:src='item' />
</div>
</div>
</div>
<script>
new Vue({
el: '#app',
data: {
items: []
},
mounted() {
let self = this;
let url = "https://dog.ceo/api/breed/hound/images";
axios.get(url)
.then(function(response) {
self.items = response.data.message.slice(0, 8);
});
}
})
</script>
← APIからデータを取得
← データを保存
← v-forディレクティブ
アイテムのリストを配列内のデータを使って表示
← v-bindディレクティブ
要素の属性(attribute)を束縛(バインディング)する
サーバーからデータを取得して表示
- jQueryもVue.jsもこの程度なら特に差はない
最新3件にNEWラベルを表示(1)
- jQueryの例(jquery.html)
<div>
<div id="images" class="columns">
</div>
</div>
<script>
(function() {
var url = "https://dog.ceo/api/breed/hound/images";
$.getJSON(url)
.done(function(data) {
$.each(data.message.slice(0, 8), function(i, item) {
var html = "<img src='" + item + "' />";
if (i < 3) {
html += '<span v-if="i < 3" class="tag is-danger">NEW</span>';
}
$("<div class='column' />").html(html).appendTo("#images");
});
});
})();
</script>
表示(View)の修正であるにも関わらず、
ロジック部分を修正している
最新3件にNEWラベルを表示(2)
- Vue.jsの例(vue.html)
<div id="app">
<div class="columns">
<div v-for="(item, i) in items" class='column'>
<img v-bind:src='item' />
<span v-if="i < 3" class="tag is-danger">NEW</span>
</div>
</div>
</div>
v-forのindexを
v-ifで判定してNEWラベルをつける
最新3件にNEWラベルを表示(3)
- 表示の例
最新3件にNEWラベルを表示(4)
- jQueryはロジック部分で動的にDOMを生成している
- デザインや見た目の変更時にロジック部分を修正することになる
- Vue.jsではディレクティブを使いView部分のみの修正で変更ができる
画像をクリックすると拡大して表示(1)
- jQueryの例(jquery.html)
<div>
<div id="images" class="columns">
</div>
<div id="modal" class="modal">
<div class="modal-background"></div>
<div class="modal-content"></div>
<button class="modal-close is-large" aria-label="close" onclick="closeModal()"></button>
</div>
</div>
<script>
var items = []; //APIのレスポンス保持用の配列
(function() {
var url = "https://dog.ceo/api/breed/hound/images";
$.getJSON(url)
.done(function(data) {
items = data.message.slice(0, 8);
$.each(items, function(i, item) {
var html = "<img src='" + item + "' onclick='openModal(" + i + ")' />"; // イベントをフック
if (i < 3) {
html += '<span v-if="i < 3" class="tag is-danger">NEW</span>';
}
$("<div class='column' />").html(html).appendTo("#images");
});
});
})();
/**
Modalのオープン
**/
function openModal(i) {
$("#modal .modal-content").html("<p class='image'><img src='" + items[i] + "' /></p>");
$("#modal").addClass("is-active");
}
/**
Modalのクローズ
**/
function closeModal() {
$("#modal").removeClass("is-active");
}
</script>
画像をクリックすると拡大して表示(2)
- jQueryの例(jquery.html)
items = data.message.slice(0, 8);
$.each(items, function(i, item) {
var html = "<img src='" + item + "' onclick='openModal(" + i + ")' />";
if (i < 3) {
html += '<span v-if="i < 3" class="tag is-danger">NEW</span>';
}
$("<div class='column' />").html(html).appendTo("#images");
});
DOM生成時にイベントを記述
→どこでイベントが記述されているかわかりづらい
画像をクリックすると拡大して表示(3)
- jQueryの例(jquery.html)
/**
Modalのオープン
**/
function openModal(i) {
$("#modal .modal-content").html("<p class='image'><img src='" + items[i] + "' /></p>");
$("#modal").addClass("is-active");
}
/**
Modalのクローズ
**/
function closeModal() {
$("#modal").removeClass("is-active");
}
状態を保存しているのはDOMのみ
→ロジックを組みにくい(状態を取得したいと思ったらDOMから取らないといけない)
画像をクリックすると拡大して表示(4)
- Vue.jsの例(vue.html)
<div id="app">
<div class="columns">
<div v-for="(item, i) in items" class='column'>
<img v-bind:src='item' v-on:click="modal=i" />
<span v-if="i < 3" class="tag is-danger">NEW</span>
</div>
</div>
<div class="modal" v-bind:class="modal!==null ? 'is-active': ''">
<div class="modal-background"></div>
<div class="modal-content">
<p class="image">
<img :src="items[modal]" />
</p>
</div>
<button class="modal-close is-large" aria-label="close" v-on:click="modal=null"></button>
</div>
</div>
<script>
new Vue({
el: '#app',
data: {
items: [],
modal: null,
},
mounted() {
let self = this;
let url = "https://dog.ceo/api/breed/hound/images";
axios.get(url)
.then(function(response) {
self.items = response.data.message.slice(0, 8);
});
}
})
</script>
画像をクリックすると拡大して表示(4)
- Vue.jsの例(vue.html)
<script>
new Vue({
el: '#app',
data: {
items: [],
modal: null,
},
mounted() {
let self = this;
let url = "https://dog.ceo/api/breed/hound/images";
axios.get(url)
.then(function(response) {
self.items = response.data.message.slice(0, 8);
});
}
})
</script>
← Modalで開いているitemを保持する変数を追加
画像をクリックすると拡大して表示(4)
- Vue.jsの例(vue.html)
<div id="app">
<div class="columns">
<div v-for="(item, i) in items" class='column'>
<img v-bind:src='item' v-on:click="modal=i" />
<span v-if="i < 3" class="tag is-danger">NEW</span>
</div>
</div>
<div class="modal" v-bind:class="modal!==null ? 'is-active': ''">
<div class="modal-background"></div>
<div class="modal-content">
<p class="image">
<img :src="items[modal]" />
</p>
</div>
<button class="modal-close is-large" aria-label="close" v-on:click="modal=null"></button>
</div>
</div>
← clickイベントを追加(modal変数にindexを代入)
↑ modalがnullでなければ表示
↑ itemsから画像URLを取得
↑ closeボタンでmodalをnullに戻す
状態はjsの変数で管理→リアクティブにViewに反映される
ディレクティブでイベントハンドリングを記述→見やすい
画像をクリックすると拡大して表示(5)
- 表示例
いいね!ボタンを実装(1)
- jQueryの例(jquery.html)
<div>
<div id="images" class="columns">
</div>
<div id="modal" class="modal">
<div class="modal-background"></div>
<div class="modal-content"></div>
<button class="modal-close is-large" aria-label="close" onclick="closeModal()"></button>
</div>
</div>
<script>
var items = []; //APIのレスポンス保持用の配列
(function() {
var url = "https://dog.ceo/api/breed/hound/images";
$.getJSON(url)
.done(function(data) {
items = data.message.slice(0, 8).map(function(element) {
return {
url: element,
like: 0
};
});
$.each(items, function(i, item) {
var html = "<img src='" + item.url + "' onclick='openModal(" + i + ")' />"; // イベントをフック
// NEWラベルを追加
if (i < 3) {
html += '<span v-if="i < 3" class="tag is-danger">NEW</span>';
}
// いいねボタンを追加
html += " <a class='button is-warning is-small' onclick='like( " + i + ")'><span>いいね! " + item.like + "件</span></a>";
$("<div id='item" + i + "' class='column' />").html(html).appendTo("#images");
});
});
})();
/**
Modalのオープン
**/
function openModal(i) {
console.log(items[i]);
$("#modal .modal-content").html("<p class='image'><img src='" + items[i].url + "' /></p>");
$("#modal").addClass("is-active");
}
/**
Modalのクローズ
**/
function closeModal() {
$("#modal").removeClass("is-active");
}
/**
like!
**/
function like(i) {
items[i].like += 1;
$("#item" + i + " .button").html("<span>いいね! " + items[i].like + "件</span>");
}
</script>
いいね!ボタンを実装(2)
- jQueryの例(jquery.html)
(function() {
var url = "https://dog.ceo/api/breed/hound/images";
$.getJSON(url)
.done(function(data) {
items = data.message.slice(0, 8).map(function(element) {
return {
url: element,
like: 0
};
});
$.each(items, function(i, item) {
var html = "<img src='" + item.url + "' onclick='openModal(" + i + ")' />"; // イベントをフック
// NEWラベルを追加
if (i < 3) {
html += '<span v-if="i < 3" class="tag is-danger">NEW</span>';
}
// いいねボタンを追加
html += " <a class='button is-warning is-small' onclick='like( " + i + ")'><span>いいね! " + item.like + "件</span></a>";
$("<div id='item" + i + "' class='column' />").html(html).appendTo("#images");
});
});
})();
← likeカウント用に変数を定義
↓いいねボタンを表示&イベントハンドラを追加
↑DOMを取得するためにIDを追加
いいね!ボタンを実装(3)
- jQueryの例(jquery.html)
/**
like!
**/
function like(i) {
items[i].like += 1;
$("#item" + i + " .button").html("<span>いいね! " + items[i].like + "件</span>");
}
↓いいねボタン
DOM操作のためにIDを追加
表示部分&イベントフックをjs内に記述
状態がDOMのみに保存される(今回は変数をもたせた)
いいね!ボタンを実装(4)
- Vue.jsの例(vue.html)
<div id="app">
<div class="columns">
<div v-for="(item, i) in items" class='column'>
<img v-bind:src='item.url' v-on:click="modal=i" />
<span v-if="i < 3" class="tag is-danger">NEW</span>
<a class="button is-warning is-small" v-on:click="item.like += 1">
<span>いいね!{{item.like}}件</span>
</a>
</div>
</div>
<div class="modal" v-bind:class="modal!==null ? 'is-active': ''">
<div class="modal-background"></div>
<div class="modal-content">
<p class="image">
<img v-if="modal" :src="items[modal].url" />
</p>
</div>
<button class="modal-close is-large" aria-label="close" v-on:click="modal=null"></button>
</div>
</div>
<script>
new Vue({
el: '#app',
data: {
items: [],
modal: null,
},
mounted() {
let self = this;
let url = "https://dog.ceo/api/breed/hound/images";
axios.get(url)
.then(function(response) {
self.items = response.data.message.slice(0, 8).map(function(element) {
return {
url: element,
like: 0
};
});
});
},
methods: {
like: function(i) {
if (!this.likes[i] && parseInt(this.likes[i])) {
this.likes[i] = this.likes[i] + 1;
} else {
this.likes[i] = 1;
}
console.log(Number(this.likes[i]));
}
}
})
</script>
いいね!ボタンを実装(5)
- Vue.jsの例(vue.html)
<div id="app">
<div class="columns">
<div v-for="(item, i) in items" class='column'>
<img v-bind:src='item.url' v-on:click="modal=i" />
<span v-if="i < 3" class="tag is-danger">NEW</span>
<a class="button is-warning is-small" v-on:click="item.like += 1">
<span>いいね!{{item.like}}件</span>
</a>
</div>
</div>
<div class="modal" v-bind:class="modal!==null ? 'is-active': ''">
<div class="modal-background"></div>
<div class="modal-content">
<p class="image">
<img v-if="modal" :src="items[modal].url" />
</p>
</div>
<button class="modal-close is-large" aria-label="close" v-on:click="modal=null"></button>
</div>
</div>
<script>
new Vue({
el: '#app',
data: {
items: [],
modal: null,
},
mounted() {
let self = this;
let url = "https://dog.ceo/api/breed/hound/images";
axios.get(url)
.then(function(response) {
self.items = response.data.message.slice(0, 8).map(function(element) {
return {
url: element,
like: 0
};
});
});
}
})
</script>
いいね!ボタンを実装(6)
- Vue.jsの例(vue.html)
mounted() {
let self = this;
let url = "https://dog.ceo/api/breed/hound/images";
axios.get(url)
.then(function(response) {
self.items = response.data.message.slice(0, 8).map(function(element) {
return {
url: element,
like: 0
};
});
});
}
jQuery同様likeカウント用の変数を定義
いいね!ボタンを実装(7)
- Vue.jsの例(vue.html)
<div id="app">
<div class="columns">
<div v-for="(item, i) in items" class='column'>
<img v-bind:src='item.url' v-on:click="modal=i" />
<span v-if="i < 3" class="tag is-danger">NEW</span>
<a class="button is-warning is-small" v-on:click="item.like += 1">
<span>いいね!{{item.like}}件</span>
</a>
</div>
</div>
<div class="modal" v-bind:class="modal!==null ? 'is-active': ''">
<div class="modal-background"></div>
<div class="modal-content">
<p class="image">
<img v-if="modal" :src="items[modal].url" />
</p>
</div>
<button class="modal-close is-large" aria-label="close" v-on:click="modal=null"></button>
</div>
</div>
↑likeのカウントアップ
↑like数の表示
イベントハンドラわかりやすい
変数と表示が自動で同期(リアクティブ)
いいね!ボタンを実装(8)
- 表示の例
jQueryと比べてのVue.js入門終わり
- DOMで全てを解決するのは難しい
- 状態をDOMに保存...
- IDでDOMを取得...
- JSでDOMを生成...
- jQueryではUIとロジックが密結合しやすい
- デザインを修正→JSを修正
- ロジックを修正→JSを修正
Vue.jsで解決
ディレクティブ:Directive
- v- から始まる特別な属性
- 配下のプロパティや表現の値が変更されたら、Directiveのupdate() 関数が同期的に呼ばれる
<span v-if="seen">Now you see me</span>
<li v-for="todo in todos">
{{ todo.text }}
</li>
<div v-bind:class="{ active: isActive }"></div>
v-if : 条件付きレンダリング
v-for : リストレンダリング
v-bind : 属性バインディング
<button v-on:click="greet">Greet</button>
v-on : DOMイベントの購読
Vue.jsの特長おさらい(1)
リアクティブ
- js内の変数が更新されると自動でDOMが更新される
- (今回やってませんが逆も更新→反映されます)
Vue.jsの特長おさらい(2)
data: {
items: []
}
<div v-for="item in items" class='column'>
<img v-bind:src='item' />
</div>
JSの中の変数
HTMLビュー(DOM)
Nuxt.js入門
(画像管理アプリ)
Dog API
The internet's biggest collection
of open source dog pictures.
https://dog.ceo/dog-api/
犬の画像がただで取得できます
Dog API
https://dog.ceo/dog-api/
犬の種別を取得(LIST ALL BREEDS)
種別毎の犬の画像を取得(BY BREED)
https://dog.ceo/api/breeds/list/all
https://dog.ceo/api/breed/hound/images
イヌコロ:完成予想図
種別一覧
画像一覧
いいね!ボタン
NEWラベル
プロジェクトの作成
$ vue init nuxt-community/starter-template inukoro
$ cd inukoro
$ npm install
$ npm run dev
まずはレイアウト
- bulma.cssを読み込む
- HTMLを修正
bulma.cssを読み込む
$ npm install @nuxtjs/bulma
module.exports = {
...
modules: [
'@nuxtjs/bulma'
],
...
}
npmでインストール
nuxt.config.jsを修正
<template>
<div>
<nav class="navbar">
<div class="container">
<div class="navbar-brand">
<nuxt-link to="/breeds" class="navbar-item">イヌコロ</nuxt-link>
<span class="navbar-burger burger" data-target="navbarMenu">
<span></span>
<span></span>
<span></span>
</span>
</div>
</div>
</nav>
</div>
</template>
pages/index.vueを修正
Nuxt.jsのビュー
.
├── README.md
├── assets
├── components
├── layouts
├── middleware
├── node_modules
├── nuxt.config.js
├── package-lock.json
├── package.json
├── pages
├── plugins
├── static
└── store
Nuxt.jsのレイアウト
<template>
<div>
<nav class="navbar">
<div class="container">
<div class="navbar-brand">
<nuxt-link to="/breeds" class="navbar-item">イヌコロ</nuxt-link>
<span class="navbar-burger burger" data-target="navbarMenu">
<span></span>
<span></span>
<span></span>
</span>
</div>
</div>
</nav>
</div>
</template>
pages/index.vue
<template>
<nav class="navbar">
<div class="container">
<div class="navbar-brand">
<nuxt-link to="/breeds" class="navbar-item">イヌコロ</nuxt-link>
<span class="navbar-burger burger" data-target="navbarMenu">
<span></span>
<span></span>
<span></span>
</span>
</div>
</div>
</nav>
</template>
components/AppHeader.vue
<template>
<div>
<AppHeader></AppHeader>
<nuxt/>
</div>
</template>
<script>
import AppHeader from '@/components/AppHeader.vue'
export default {
components: {
AppHeader
}
}
</script>
layouts/default.vue
<template>
<div></div>
</template>
pages/index.vue
犬種リストを表示
- Dog APIから犬種リストを取得
- ストアに犬種リストを保存
- 犬種リストを表示
APIからのデータ取得
import axios from 'axios'
class DogApi {
constructor() {
this.apiBase = 'https://dog.ceo/api';
}
breeds() {
return axios.get(`${this.apiBase}/breeds/list/all`)
.then(json => {
return json.data.message;
})
.catch(e => ({ error: e }));
}
}
const dogApi = new DogApi();
export default dogApi;
api/dog.js
Dog API用にクラスを作成
$ npm install axios
Ajax用のライブラリをインストール
APIからのデータ取得
import Vuex from 'vuex'
const appStore = () => {
return new Vuex.Store({
state: {
breed_list: {},
},
mutations: {
breed_list_update(state, payload) {
state.breed_list = {...payload}
},
}
})
};
export default appStore
store/index.js
犬種リスト保存用のストアを作成
<template>
<div></div>
</template>
<script>
import dogApi from '@/api/dog'
export default {
async fetch({store}) {
let json = await dogApi.breeds();
store.commit('breed_list_update', json)
},
}
</script>
pages/index.js
pageコンポーネントから実行
APIからのデータ取得
データを取得→ストアに保存ができていることを確認
ページコンポーネントAPI
fetchメソッド
- fetch メソッドは、コンポーネントがローディングされる前に毎回呼び出されます(ページコンポーネントに限り)
- fetch メソッドは第一引数として コンテキスト を受け取り、コンテキストを使ってデータを取得してデータをストアに入れることができます。
export default {
async fetch({store}) {
let json = await dogApi.breeds();
store.commit('breed_list_update', json)
},
}
ストア
- アプリケーションの状態(state)を保持するコンテナ
- ストアの状態を直接変更することはできない。
- ミューテーションをコミットすることによってのみ、ストアの状態を変更する
import Vuex from 'vuex'
const appStore = () => {
return new Vuex.Store({
state: {
breed_list: {},
},
mutations: {
breed_list_update(state, payload) {
state.breed_list = {...payload}
},
}
})
};
export default appStore
状態(state)
ミューテーション
犬種リストを表示する
<template>
<section class="container">
<div class="columns is-multiline">
<div v-for="(item, i) in breed_list" v-bind:key='i' class='column is-2'>
<a class="button">{{ i }}</a>
</div>
</div>
</section>
</template>
<script>
import dogApi from '@/api/dog'
import { mapState } from 'vuex'
export default {
async fetch({store}) {
let json = await dogApi.breeds();
store.commit('breed_list_update', json)
},
computed: mapState(['breed_list']),
}
</script>
pages/index.vue
VuxのmapStateヘルパーでstateを取得
v-forディレクティブで描画
犬一覧を取得
- 選択された犬種を取得
- 犬画像リストをAPIから取得
- 犬画像リストを描画
ルーティング
- 犬種一覧(BREED LIST)
URL : /
- 犬画像一覧(DOGS)
URL : /dogs/犬種名/
├── pages
│ ├── README.md
│ ├── dogs
│ │ └── _breed
│ │ └── index.vue
│ └── index.vue
ルーティング
<template>
<div>
test
</div>
</template>
http://localhost:3000/dogs/test/
pages/dogs/_breed/index.vue
APIから犬画像リストを取得
class DogApi {
....
dogs(breed) {
return axios.get(`${this.apiBase}/breed/${breed}/images`)
.then(json => {
return json.data.message.map(function(element) {
return {
url: element,
like: 0
};
});
})
.catch(e => ({ error: e }));
}
...
}
api/dog.jsにメソッドを追加
import Vuex from 'vuex'
const appStore = () => {
return new Vuex.Store({
state: {
breed_list: {},
dog_list: [],
},
mutations: {
breed_list_update(state, payload) {
state.breed_list = {...payload}
},
dog_list_update(state, payload) {
state.dog_list = [...payload]
},
}
})
};
export default appStore
store/index.jsにstateとmutationを追加
APIから犬画像リストを取得
<template>
<div>
test
</div>
</template>
<script>
import dogApi from '@/api/dog'
import { mapState } from 'vuex'
export default {
async fetch({store, params}) {
let json = await dogApi.dogs(params.breed);
store.commit('dog_list_update', json)
},
computed: mapState(['dog_list']),
}
</script>
pages/dogs/_breed/index.vue
<div v-for="(item, i) in breed_list" v-bind:key='i' class='column is-2'>
<nuxt-link :to="{ path: 'dogs/'+ i }" class="button">{{ i }}</nuxt-link>
</div>
pages/index.vueのリンクを修正
APIから犬画像リストを取得
データの取得を確認
犬画像リストを描画
pages/dogs/_breed/index.vue
<template>
<section class="container">
<div class="columns is-multiline">
<div v-for="(item, i) in dog_list" v-bind:key='i' class='column is-1'>
<img v-bind:src='item.url' />
</div>
</div>
</section>
</template>
<script>
import dogApi from '@/api/dog'
import { mapState } from 'vuex'
export default {
async fetch({store, params}) {
let json = await dogApi.dogs(params.breed);
store.commit('dog_list_update', json)
},
computed: mapState(['dog_list']),
}
</script>
Nuxt.jsおさらい
- Vue.jsで開発する時に必要なものが入っており、設定が不要。
- Vue-Router : ルーティング
- Vuex : 状態管理
- レイアウト 等々
- デプロイ方法が多様。
- サーバーサイドレンダリング
- シングルページアプリケーション
- 静的ファイル
20181121オトナのVue.jsとNuxt.js入門ハンズオン
By Keisuke Shingaki
20181121オトナのVue.jsとNuxt.js入門ハンズオン
- 1,526