作成日:2021/04/12 更新日:2022/01/27
#1 WebAPIを自作してデータをJSON形式で受信
WebAPIについて
APIとは ユーザー側が欲しい情報を公開されたプログラムに対しリクエスト、 データベースのデータを取得したり操作できる仕組みのことです。Webで使われる場合は特にWebAPIと呼ばれます。ユーザーのリクエストに対し、WebサーバにあるAPI(ここではphpファイル)が実行され、データベースからデータがJSON形式で戻って来たものをHTMLで表示させます。
もちろんデータベースのどんな情報にも勝手にアクセスできるわけではなく、APIを作った人の意図するデータだけを取得できる仕組みです。
今回はその仕組を作っていきましょう。
REST APIとは
またAPIを調べるとREST APIというものに出くわすとおもいます。 REST(訳:理解しやすい通信)とは ブラウザとサーバ間のデータのやり取りの設計思想のことです。今回は小規模なので使っていません。隠しAPIのようなものを作ります。ちなみにRESTではURIにhttps://cbc-study.com?name=大橋太郎&dataX=123のように クエリ文字、パラメーター(変数)と呼ばれる文字列をくっつけ、変数名:nameに値:大橋太郎、 変数名:dataXに値:123というデータを乗せて、サーバに送信します。
サーバ側ではユーザーからの操作の状態を POST(新規作成)、 Get(読み込み)、 PUT(更新)、 DELETE(削除) という手法(HTTPメソッド)で受信、結果をJSONでレスポンスを返す、などのルールがあります。
アプリをvue化させる方法
#1でコマンドラインを使ってvueのテンプレートを作りました。今回も同じ手法でテンプレートを作り、 #4で作った「名前移動アプリ」を完全にvueだけで作ってみたいと思います。その際、PHPで作ったinputとupdateのロジックをAPI化し、jQueryで作ったドラッグアンドドロップ(以下、DnD)やajax機能を、vueのライブラリで実装します。
ファイル構成
jQueryライブラリを変更する
jQueryで利用していて変更するものとして、$.ajax()の代わりに非同期通信させるために axiosライブラリを使います。 また、jQueryの.draggable()メソッドの代わりに、vue-draggable-resizableライブラリを使います。 これらのライブラリはすべて、npmコマンドでインストールしていきます。vueテンプレート作成
$ brew updateは必要に応じて行いnode.jsやVUE-CLIはグローバルにインストール済みであるものとします。job/stg/vue/内に02-moveという名前のプロジェクト名で始めましょう。
初期のファイル構成・内容は前ページ、#2 静的なページをvue.jsを使って構築すると同じです。
ターミナルで作成
$ cd ~/job/stg/vue/
$ vue create 02-move
> Default ([Vue 2] babel, eslint) ←を選択
インストール完了後に移動
$ cd 02-move
$ npm i vue-router ress
$ npm i -D sass-loader@10 sass
エンターを押して必要なプラグインをインストールします。vue-routerとress、sass-loaderとsassをインストールします。sass-loaderのバージョンを10にしているのはwebpackのバージョンが4であるためです。
※sassではなくnode-sassをインストールすよう解説しているものもありますが、現在node-sassは非推奨なので、 sass(Dart-Sass)を使いましょう。
またfibersをインストールする場合はnode.jsのバージョンが15以下の場合のみですので、16以上の場合は利用できません。
47 vulnerabilities (19 moderate, 28 high)
このようなエラーが出ますが、一旦無視して進めてください。src/main.jsにrouterとscssの追記
/* main.js */
import Vue from 'vue'
import App from './App.vue'
import router from './router' /* ←追加 */
import 'ress' /* ←追加 */
import '@/assets/scss/main.scss' /* ←追加 */
Vue.config.productionTip = false
new Vue({
router, /* ←追加 */
render: h => h(App),
}).$mount('#app')
router/index.jsを作成し、routerの設定を記述
/* router/index.js */
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '@/views/Home.vue'
Vue.use(VueRouter)
const routes = [
{ path: '/', component: Home }
]
const router = new VueRouter({
mode: 'history',
routes
})
export default router
routerフォルダを作成し中にindex.jsを作ります。vue.config.jsの作成
/* vue.config.js */
module.exports = {
publicPath: process.env.NODE_ENV === 'production' ? '/test/' : '/',
productionSourceMap: process.env.NODE_ENV === 'production' ? false : true,
devServer: {
port: 8085, /* この場合、http://localhost:8085 で表示されます */
https: false
},
css: {
loaderOptions: {
scss: {
additionalData: `@import "@/assets/scss/_variables.scss";`
}
}
}
}
ポートは好きに変えてかまいません。
App.vueの編集
<template>
<div id="app">
<router-view/>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
1ページしかないので、上記のようにまるごと差し替えます。
SCSSファイルの作成
src/assets内に、scssディレクトリを作り、以下の2つのファイルを入れます。
$breakpoints: (
'sm': 340,
'md': 768,
'lg': 1215,
) !default;
@mixin mq($mq, $bp1:lg, $bp2:lg){
$w1 : map-get($breakpoints, $bp1);
$w2 : map-get($breakpoints, $bp2);
$min1 : 'min-width: #{($w1+1)}px';
$max1 : 'max-width: #{($w1)}px';
$min2 : 'min-width: #{($w1+1)}px';
$max2 : 'max-width: #{($w2)}px';
@if $mq == min {
@media screen and ($min1) {
@content;
}
}
@else if $mq == max {
@media screen and ($max1) {
@content;
}
}
@else if $mq == min-max {
@media screen and ($min2) and ($max2) {
@content;
}
}
}
$mainColor:#7BC2BA;
body,div,p{margin:0;}
.clearfix:after {
content:"";
display:block;
clear:both;
}
body {
height:100%;
color:#ccc;
font-size:10px;
}
views/Home.vueの作成
<template>
<div id="wrapper">
</div>
</template>
<script>
export default {
name: 'Home',
}
</script>
<style lang="scss" scoped>
#wrapper {
border: 2px dashed #ccc;
margin:15px;
border-radius: 6px;
height: 600px;
}
</style>
src内に、viewsフォルダを作成し、Home.vueを作成
確認
ここまでで、ブラウザで確認してみましょう。
$ npm run serve
APIファイルを作成
publicディレクトリにAPIファイルを作成
APIファイルは公開用に用意されているpublicディレクトリに配置します。
├── api
│ └── v1
│ ├── Controller
│ │ ├── AppController.php
│ │ └── Connect.php
│ └── user
│ └── index.php
├── favicon.ico
└── index.html
public/api/v1/ とディレクトリを作成します。v1とはバージョン1のことです。APIは機能をバージョンで管理していくので、
今回も便宜上v1としておきます。Controller内のDBに接続するためのクラスファイルAppController.phpConnect.php はSortable4と同じものを使用します。Sortable4からフォルダごとコピーして入れておきます。
user/index.phpはこのような内容です。
<?php
require_once('../Controller/Connect.php');
try{
$sql = '
SELECT
t1.*,
genders.gender
FROM
sortable AS t1
LEFT JOIN `genders` ON t1.gender_id = genders.id
';
$select = new SelectData();
$result = $select->select($sql);
$json_data = json_encode( $result, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_NUMERIC_CHECK);
header("Content-Type: application/json; charset=utf-8");
header("Access-Control-Allow-Origin: http://localhost:8085");
echo $json_data;
} catch (PDOException $e) {
echo $e->getMessage();
}
sortable4のindex.phpでは以下のように記述されていました。
新たにheader()関数が追加されています。
ただし、ローカル環境でAPIのテストをする場合、vueアプリのポート8085ではpublicフォルダは表示されません。
(http://localhost:8085/api/v1/user/ では表示されない)
<?php
$sql = '
SELECT
t1.*,
genders.gender
FROM
sortable AS t1
LEFT JOIN `genders` ON t1.gender_id = genders.id
';
$result = $select->select($sql);
$json_data = json_encode( $result, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
?>
SQLのSELECT文や、$json_data変数を作るまでは同じです。新たにheader()関数が追加されています。
header("Content-Type: application/json; charset=utf-8");
このファイルが「jsonテキストデータですよ」という宣言になります。
header("Access-Control-Allow-Origin: http://localhost:8085");
通常ではこのJSONファイルに、他のドメインからアクセスしようとした場合エラーが出ます。
第二引数に指定したアドレス(vueアプリ)からのアクセスを許可する場合に必要となります。
$json_data = json_encode(JSON_NUMERIC_CHECK);
json_encodeに引数JSON_NUMERIC_CHECKを追加しました。JSONデータで数字の部分をvueで使うときにNumberフォーマットにするためです。
echo $json_data;
このファイルにアクセスすると、JSONデータを出力するAPIの完成です。
そこで新たに、MAMPでvueのpublicフォルダに8086ポートを設定してアクセスできるようにします。
Listen 8086
<virtualhost *:8086>
DocumentRoot "/Users/ユーザ名/job/stg/vue/02-move/public/"
</virtualhost>
httpd-vhosts.confを開いて、上記を追記したあと、MAMPでサーバを再起動します。するとこのAPIのアドレスはhttp://localhost:8086/api/v1/user/で開けるようになります。
非同期通信を行う「axios」のインストール
コンポーネントでAPIと非同期通信をしたい場合にはaxiosを使うと便利です。
$ npm i axios
インストール
import axios from 'axios'
Vue.prototype.$axios = axios
axiosを使うためmain.jsにこの2行を追記します。
HTTPリクエストを簡単に書くことができます。
APIからコンポーネントでデータを読み込む(GET)
コンポーネント「AppDrag.vue」の作成
<template>
<div id="drag-area">
<div
class="drag"
v-for="item in users"
:key="item.id"
:class="'gender' + item.gender_id"
:data-num="item.id"
:style="'left:' + item.left_x + 'px; top:' + item.top_y + 'px;'"
>
<p><span class="name">{{item.id}} {{item.name}} ({{item.gender}})</span></p>
</div>
</div>
</template>
<script>
export default {
name: 'AppDrag',
data() {
return {
users: []
}
},
mounted: function() {
this.$axios.get('http://localhost:8086/api/v1/user/')
.then(response => (this.users = response.data))
.catch(error => console.log(error))
},
}
</script>
<style lang="scss" scoped>
#drag-area {
position: relative;
height: 500px;
width:980px;
margin: 5px auto;
border: 1px solid #333;
border-radius: 6px;
.vdr.active:before {
outline: none;
}
.drag {
width:auto;
height:auto;
position: absolute;
padding: 1em;
color: #666;
border-radius: 5px;
background: #efefef;
border: 3px solid #ccc;
}
.name {
font-size:12px;
}
.gender1{
border: 2px solid #00F;
}
.gender2{
border: 2px solid #F00;
}
}
</style>
<template>
<div id="drag-area">
<div
class="drag"
v-for="item in users"
:key="item.id"
:class="'gender' + item.gender_id"
:data-num="item.id"
:style="'left:' + item.left_x + 'px; top:' + item.top_y + 'px;'"
>
<p><span class="name">{{item.id}} {{item.name}} ({{item.gender}})</span></p>
</div>
</div>
</template>
基本的には前回のsertable4と同様です。JSONデータをjsonDataからusersに変更しました。 また、v-bindは省略できるため省略しています。
<script>
export default {
name: 'AppDrag',
data() {
return {
users: []
}
},
mounted: function() {
this.$axios.get('http://localhost:8086/api/v1/user/')
.then(response => (this.users = response.data))
.catch(error => console.log(error))
},
}
</script>
重要な設定はdata関数とcreated関数です。これらはライフサイクルフックと呼ばれ、vueファイルが読み込まれHTMLファイルが(DOMが)描画されるタイミングごとに、 処理を設定していきます。
詳しくはvueのマニュアル を見てみましょう。
usersという配列データを使うので、空のusersを用意しておきます。
dataはオブジェクト形式ではなく、関数形式で記述する必要があります。
/* オブジェクト形式 */
data: {
users: []
}
/* 関数形式 */
data() {
return {
users: []
}
}
new Vue()を使ってインスタンスを作る場合はオブジェクト形式で、コンポーネントの定義のときには、初期データオブジェクトを返す関数として宣言する必要があります。
mounted: function() {
this.$axios.get('http://localhost:8086/api/v1/user/')
.then(response => (this.users = response.data))
.catch(error => console.log(error))
}
mountedフックはDOM生成後に実行されます。
このコンポーネントでaxiosをimportした場合はaxios.get()で指定しますが、
main.jsにインポートしているのでどこでもaxiosを使えます。その場合は例のようにthis.$axios.get()で指定します。
this.$axios.get('http://localhost:8086/api/v1/user/')
this.$axiosに続き、getメソッドget('APIアドレス')でjSONデータを取得します。postメソッドの場合はpost('APIアドレス')になります。
APIアクセスが成功した場合、失敗した場合
.then(response => (this.users = response.data))
.catch(error => console.log(error))
.then()メソッドで成功したときの処理を、.catch()メソッドで失敗したときの処理を記述します。getリクエストが成功した場合、データ*1が戻ってきます(コールバック)のでresponseという名前で保存します。 いろんなデータが取得できますがresponse.dataで中身のデータが取り出せますので、 data()で設定したthis.usersに代入します。
*1 このデータはPromiseオブジェクトと呼ばれます。 Promiseデータは、then()メソッドを使うことで`正常データ`を取り出します。`エラー情報`はcatch()メソッドで取得できます。
Home.vueでコンポーネントファイルAppDrag.vueの読み込み設定
<template>
<div id="wrapper">
<AppDrag />
</div>
</template>
<script>
import AppDrag from '@/components/AppDrag.vue'
export default {
name: 'Home',
components: {
AppDrag
}
}
</script>
import でコンポーネントファイルを読み込みます。components オプションに使うコンポーネント名を登録。
template 内で使うコンポーネント(タグ)を指定します。
DnD機能の実装
vue-draggable-resizableのインストール
$ npm i vue-draggable-resizable
npmでインストールします。
コンポーネントAppDrag.vueを編集
JavaScriptの設定
<script>
import VueDraggableResizable from 'vue-draggable-resizable'
export default {
name: 'AppDrag',
components: {
VueDraggableResizable
},
data() {
・・・
DnDライブラリvue-draggable-resizableをインポートします。components: でこのライブラリのVueDraggableResizableコンポーネントを登録します。
templateの設定
<template>
<div id="drag-area">
<VueDraggableResizable
v-for="item in users"
:class="'drag gender' + item.gender_id"
:key="item.id"
:data-num="item.id"
:parent="true"
:resizable="false"
:x="item.left_x"
:y="item.top_y"
:w="150"
:h="38"
>
<p><span class="name">{{item.id}} {{item.name}} ({{item.gender}})</span></p>
</VueDraggableResizable>
</div>
</template>
前回までは、ただの<div>タグ内でv-for="item in jsonData"として、データをループ表示させていました。
今回は、DnDライブラリで設定されているVueDraggableResizableをAppDrag.vueの子コンポーネントとして読み込みます。
VueDraggableResizableで設定できるv-bindを設定して、動的にデータをやりとりできるようにします。
DnDはできるようになりましたが、座標データが保存されていないので、リロードすると要素はもとに戻ってしまいます。
今回は、DnDライブラリで設定されているVueDraggableResizableをAppDrag.vueの子コンポーネントとして読み込みます。
VueDraggableResizableで設定できるv-bindを設定して、動的にデータをやりとりできるようにします。
:parent="true" /* 親要素ないでDnDできるようにする */
:resizable="false" /* リサイズ機能は外しておきます */
:x="item.left_x" /* style属性のx座標データが動的に入ります */
:y="item.top_y" /* style属性のy座標データが動的に入ります */
:w="150" /* 要素の幅 */
:h="38" /* 要素の高さ */
x,y座標や要素の幅、高さをバインドします。
:class="'drag gender' + item.gender_id"
class="drag"は:class="'gender' + item.gender_id"に統合しました。
:style="'left:' + item.left_x + 'px; top:' + item.top_y + 'px;'"
またv-bind:styleで設定していた内容は、VueDraggableResizableに同様の機能としてtransformでx,y座標を保存・表示させるため、削除しました。DnDのあとに座標データをAPIに送信する(POST)
ドラッグ終了時のカスタムイベントの設定
@dragstop="onDragStop"
テンプレートの<VueDraggableResizable>タグにDnDの終了時の処理を入れます。@dragstopはv-on:dragstopのことです。 dragstopはDnDライブラリ で定義されているカスタムイベントです。 dragが終了したらxとy座標の情報を取得するように作られています。
onDragStopはmethods:{}内で定義したメソッド名です。どんな文字列でも構いません。
ただし、idデータがなければ、どの要素の座標かわからないので役に立ちません。DnDライブラリはidを取得するようには設計されていませんので、 カスタムイベントを以下のように改定しておきます。
@dragstop="(x, y) => onDragStop(x, y, item.id)"
本来の引数は2つですが、idも取得できるようにできました。
カスタムイベント
このようにテンプレート上で@:カスタムイベント名='メソッド名'のように設定できます。 子コンポーネント側ではthis.$emit('カスタムイベント名')と設定しておき、親側のメソッドにデータを渡します。 親のメソッドにはメソッド名: function(){}でイベントが実行されときの処理を設定しておきます。vueでは他のコンポーネントのデータ・メソッドに直接アクセスできない仕組みになっており、 コンポーネント同士でデータをやりとりする場合はprops(プロパティ)、 $emit(エミット)を使います。
カスタムイベントにおけるpropsとemitの図解
DnDライブラリのドラッグ終了時にaxiosでデータ送信
終了時のメソッドonDragStopを設定します。
methods: {
onDragStop: function(x, y, id) {
this.dragging = false
let params = new URLSearchParams();
params.append('left', x); /* params.append('変数名', 値); */
params.append('top', y);
params.append('id', id);
this.$axios.post('http://localhost:8086/api/v1/user/', params)
.catch(error => console.log(error))
}
}
this.dragging = false
ドラッグを止めていることを定義します。
let params = new URLSearchParams();
axiosが通信するときのContent-type: はapplication/json形式で送信されますが、 APIではPHPで受信するので、PHPで受けれるContent-type:であるapplication/x-www-form-urlencoded形式でaxios送信するためのクラスです。
※PHP側をjsonに対応することでも対処できます。
params.append('left', x);
params変数にappendで値を追加していきます。params.append('変数名', 値);という形です。
データはJSON形式{'left':0, 'top':0, 'id':20}ではなく
クエリパラメータ形式left=0&top=0&id=20という形で送信されます。
クエリパラメータ形式left=0&top=0&id=20という形で送信されます。
APIでデータの受信とDB登録
public/api/v1/user/index.phpに以下を追記する
<?php
require_once('../Controller/AppController.php');
・・・省略・・・
if(!empty($_POST['left'])){
try{
$sql = '
UPDATE
sortable
SET
left_x = :LEFT,
top_y = :TOP
WHERE
id = :NUMBER
';
$obj = new AppController();
$obj->update_sortable($sql, $_POST['left'], $_POST['top'], $_POST['id']);
} catch (PDOException $e) {
echo $e->getMessage();
}
}
axiosを経由してPOSTで受信した「座標、id」データを受けて、DBに登録。以前作成したアプリと同じものです。
リロードしても座標が変わらないことが確認できます。
新規登録コンポーネントの作成
コンポーネント「AppImport.vue」の作成
<template>
<div id="input_form">
<input v-model="name" type="text" name="inputName" placeholder="新メンバー名を入力">
<input v-model="gender" type="radio" name="inputGender" value="1">男性
<input v-model="gender" type="radio" name="inputGender" value="2">女性
<button v-on:click="createUser">送信</button>
</div>
</template>
<script>
export default {
name: 'AppImport',
data() {
return {
name: '',
gender: 2,
}
},
methods: {
createUser: function(){
let params = new URLSearchParams();
params.append('inputName', this.name);
params.append('inputGender', this.gender);
this.$axios.post('http://localhost:8086/api/v1/user/', params)
.then(location.reload())
.catch(error => console.log(error))
}
}
}
</script>
<style lang="scss" scoped>
#input_form {
padding:20px;
background:#efefef;
input{
margin:0 3px;
}
input[type="text"] {
width: 200px;
margin-right:15px;
padding: 6px 12px;
border:none;
font-size: 16px;
border-radius: 6px;
background-color:#fff;
}
button {
padding: 7px 20px;
font-size: 12px;
border:none;
border-radius: 6px;
background: #75d1dc;
color:#333;
}
}
</style>
解説
<template>
<div id="input_form">
<input v-model="name" type="text" name="inputName" placeholder="新メンバー名を入力">
<input v-model="gender" type="radio" name="inputGender" value="1">男性
<input v-model="gender" type="radio" name="inputGender" value="2">女性
<button v-on:click="createUser">送信</button>
</div>
</template>
inputの値をデータバインディングするため、それぞれのinputにv-modelを追加します。v-model="name"とすることで、データを取得できます。
<input type="submit" value="登録">は<button v-on:click="createUser">送信</button>に変更しました。 クリックされると、methods:で定義したcreateUserメソッドが起動します。
<script>
export default {
name: 'AppImport',
data() {
return {
name: '',
gender: 2,
}
},
methods: {
createUser: function(){
let params = new URLSearchParams();
params.append('inputName', this.name);
params.append('inputGender', this.gender);
this.$axios.post('http://localhost:8086/api/v1/user/', params)
.then(location.reload())
.catch(error => console.log(error))
}
}
}
</script>
dataオブジェクトにnameの初期設定、genderの初期値を入れることでcheckedの役割をもたせています。1を設定すると男性が初期にチェックされます。v-on:clickで呼び出されるcreateUserメソッドでは、送るデータを整理してaxiosでAPIに送ります。
paramsデータの作り方はDnDの座標を送る手順と同様です。名前と性別データを飛ばします。
成功した場合.thenに記述した処理が起動します。登録が完了したらlocation.reload()して、ブラウザをリロードします。
input{
margin:0 3px;
}
input[type="text"] {
background-color:#fff;
}
button {
color:#333;
}
前回から追加したscssは上記の3点です。
Home.vueにコンポーネントAppImport.vueを設定
<template>
<div id="wrapper">
<AppImport />
<AppDrag />
</div>
</template>
<script>
import AppImport from '@/components/AppImport.vue'
import AppDrag from '@/components/AppDrag.vue'
export default {
name: 'Home',
components: {
AppImport,
AppDrag
}
}
</script>
AppImport.vueをimportして、components:に登録、<AppImport />を追加します。
新規登録データをAPIで受信しDB登録
APIファイルに新規登録ロジックを追記
public/api/v1/user/index.phpに新規登録したらDBに登録するロジックを追記します。といっても、以前PHPで作った内容そのままです。
/* axiosを経由してPOSTで受信した「名前、性別」データを受けて、DBに登録 */
if(!empty($_POST['inputName'])){
try{
$sql = '
INSERT INTO sortable(
name,
gender_id
)
VALUES(
:ONAMAE,
:GENDER
)
';
$obj = new AppController();
$obj->insert_sortable($sql, $_POST['inputName'], $_POST['inputGender']);
/* file_put_contents('aaa.txt', $_POST); //データ取得確認用 */
} catch (PDOException $e) {
echo $e->getMessage();
}
}
これで、PHPで作ったアプリを、vue.jsに完全移植することに成功しました。ここまでのコードはこちら よりダウンロードできます。
Webサーバーにデプロイする
レンタルサーバのxserverを例に、Webサーバにデプロイする方法です。サーバにアップする場合に変更するヶ所は以下のとおりです
const DB_NAME ='cri_sortable';
const HOST ='mysql****.xserver.jp';
const UTF ='utf8';
const USER ='mysqlユーザ名';
const PASS ='mysqlパスワード';
api/v1/Controller/Connect.phpレンタルサーバの情報を入れます。
mountedに書いてある
this.$axios.get('https://******/.com/test/api/v1/user/')
methodsに書いてある
this.$axios.post('https://******.com/test/api/v1/user/', params)
AppDrag.vue
header("Access-Control-Allow-Origin: http://localhost:8085") /* ローカルの場合の設定 */
api/v1/user/index.php同じドメインからアクセスするのであれば削除する。
サブディレクトリ「test」でデプロイしたい場合
publicPath: process.env.NODE_ENV === 'production' ? '/test/' : './',
vue.config.js
/* { path: '/', component: Home } */
{ path: '/test', component: Home }
router/index.jsサブディレクトリを指定すると(https://******.com/test)でアクセスできます。