- 投稿日:2019-12-24T23:51:07+09:00
Vue.js の フォーム とか tailwindcss とか Vue Router とか コンポーネント とか
この記事は、静岡 Advent Calendar 2019 の25日目の記事です。
はじめに
静岡の勉強会事情等々、皆様がいろいろと書いてくださっているので、自分はそちらの内容はまたの機会にすることにして、当初、Laravel を用いてテストについて何か書こうかなと思っておりました。
が、ちょっとばかり路線変更しまして、Vue.js の フォーム を中心に時間が許す限り何か思うがままに書こうかなと思います。
テストにつきましては、また別の機会にでも書きます。sqlite を使ってやるよくあるやつです。大した内容ではございません。
それから、次点として当初考えておりました、Laravel で過去に作った簡単なプロジェクトを Symfony(完全未経験) で書き直す。というものも考えてはおりましたが、風邪で昨日まで1週間寝込むという愚行を犯した自分には難しかったです。(今のところ娘に移っていないようなのが救いだったり...)うだうだと書きましたが、それでは、早速本題に入りますよ。
今回は下記スクリーンショットのような form を想定しております。
環境につきましては、バックエンドに Laravel (今回あまり関係ございませんが、ディレクトリ名とか Vue CLI のものとは異なります)を、フロントエンドに Vue.js / tailwindcss (Laravel Mixを使用)を使用いたします。
tailwindcss?? なんじゃそりゃ? という方は、こちらをご参照くださいませ。
CSSのクラスとして提供しているだけであり、個人的には、UI系のフレームワークより使い勝手がいいかなと思っています。作者さんはAdam Wathanです。
まぁ、Laravel / Vue 関連クラスターの著名な方なので、なんとなく安心感もありますね笑何はともあれ、まずはセットアップから〜
Vue.js と tailwindcss のセットアップ
それでは、最初に Vue.js と tailwindcss の下準備とセットアップでもしましょうか。
どこか好きな ディレクトリ に新規で Laravel プロジェクト を作りましょう。
$ composer create-project laravel/laravel プロジェクト名 "バージョン指定.*" Application key XXXXXXXXXうにゃうにゃでます set successfully.set successfully. と出ていたらインストールが完了です。インストールが終わりましたら、今作成したプロジェクトのディレクトリへ移動しておきましょう。
また、エディタでもプロジェクトを開いておきます。下記のコマンドを打って下準備をはじめましょう。
$ php artisan preset none
これで package.json の構成が最小限のものに変わったかと思います。
続きまして、今日の本題 Vue.js を入れていきましょう。$ php artisan preset vue
これで、package.json でも
"vue": "^2.5.7",
な感じに追加されていることが確認できるかと思います。npm or yarn で Vue Router と tailwindcss を入れる
## Using npm npm install vue-router tailwindcss --save-dev ## Using Yarn yarn add vue-router tailwindcss --save-devpackage.json に、下記のものが追加されていればOKです。
package.json"devDependencies": { "tailwindcss": "^1.1.4", "vue": "^2.5.7", "vue-router": "^3.1.3" },あともう少しです。
CSS に tailwindcss を追加します
resources/assets/sass/app.scss に下記の内容を書きます。
app.scss@tailwind base; @tailwind components; @tailwind utilities;tailwindcssは、ビルド時にこれらのディレクティブを、生成されたすべてのCSSと交換します。
tailwindcss 構成ファイルを作成します
下記のコマンドを打ち込み、tailwind.config.jsを作成します。
npx tailwind init
これにより、tailwind.config.js が、プロジェクトのルートディレクトリの直下に作成され、最小限のファイルが作成されます。tailwindcssに何か追加したいときに追加していくものですね。
詳細はこちら。webpack.mix.js を修正する (Laravel5.5 の場合)
webpack.mix.jslet mix = require('laravel-mix'); let tailwindcss = require('tailwindcss') /* |-------------------------------------------------------------------------- | Mix Asset Management |-------------------------------------------------------------------------- | | Mix provides a clean, fluent API for defining some Webpack build steps | for your Laravel application. By default, we are compiling the Sass | file for the application as well as bundling up all the JS files. | */ mix.js('resources/assets/js/app.js', 'public/js') .sass('resources/assets/sass/app.scss', 'public/css') .options({ processCssUrls: false, postCss: [ tailwindcss('./tailwind.config.js') ], });webpack.mix.js を修正する。(Laravel6.0 の場合)
webpack.mix.jsconst mix = require('laravel-mix'); const tailwindcss = require('tailwindcss') /* |-------------------------------------------------------------------------- | Mix Asset Management |-------------------------------------------------------------------------- | | Mix provides a clean, fluent API for defining some Webpack build steps | for your Laravel application. By default, we are compiling the Sass | file for the application as well as bundling up all the JS files. | */ mix.js('resources/js/app.js', 'public/js') .sass('resources/sass/app.scss', 'public/css') .options({ processCssUrls: false, postCss: [ tailwindcss('./tailwind.config.js') ], });npm or yarn で ビルド する
## Using npm npm run watch ## Using yarn yarn run watchちょっと長かったですね、あとちょっとです。
resources/views/layouts/app.blade.php の bladeテンプレートの、
<div id="app"></div>
の中を、下記のように変更します。resources/views/layouts/app.blade.php<!DOCTYPE html> <html lang="{{ app()->getLocale() }}"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- CSRF Token --> <meta name="csrf-token" content="{{ csrf_token() }}"> <title>{{ config('app.name', 'Laravel') }}</title> <!-- Styles --> <link href="{{ asset('css/app.css') }}" rel="stylesheet"> </head> <body> <div id="app"> <div class="h-screen"> @yield('content') </div> </div> <!-- Scripts --> <script src="{{ asset('js/app.js') }}"></script> </body> </html>それから、resources/views/home.blade.php も、下記のように変更しましょう。
resources/assets/js/components/App.vue を読み込むように記載しています。resources/views/home.blade.php@extends('layouts.app') @section('content') <App></App> @endsection続きまして、resources/assets/js/components/App.vue も下記のように書きます。
ポイントは、<router-link to="/contacts/create">
の箇所です。
/contacts/create のURLに遷移するように設定します。 ( Vue Router用の設定 )resources/assets/js/components/App.vue<template> <div> <router-link to="/contacts/create"> <p>新規登録</p> </router-link> </div> </template>ここまで出来たので、冒頭に導入した Vue Router を使ってみます。
resources/assets/js/app.js と resources/assets/js/router.js を、それぞれ開きます。まずは、resources/assets/js/app.js から修正していきますね。
下記のようにしておきましょう。
resources/assets/js/app.jsimport Vue from 'vue'; import router from "./router"; import App from "./components/App"; const app = new Vue({ el: '#app', components: { App }, router });続いて、resources/assets/js/router.js では、ヒストリーモードを指定し、URLが、/contacts/create のときに、ContactsCreate のコンポーネントが表示されるようにします。
resources/assets/js/router.jsimport Vue from 'vue'; import VueRouter from 'vue-router'; import ContactsCreate from "./views/ContactsCreate"; Vue.use(VueRouter); export default new VueRouter({ routes: [ { path: '/contacts/create', name: 'ContactsCreate', component: ContactsCreate } ], mode: 'history' });それでは、下準備が終わったところで、
まずは、ファイルを2つ用意しましょう。それぞれ、views/ContactsCreate.vue と、components/InputField.vue を作ります。
views/ContactsCreate.vue は、先ほど resources/assets/js/router.js で 指定したコンポーネントのファイルになりますね。
ContactsCreate.vue から作業を進めていきます。
一旦、props を用いまして、親コンポーネントであります ContactsCreate.vue から、
子コンポーネントの components/InputField.vue へ、
v-bind を 使って、label や placeholder の値を受け渡すところまで書いてみます。views/ContactsCreate.vue<template> <div> <form> <input-field name="name" label="お名前" placeholder="例:山田ルイ53世"/> <input-field name="email" label="メールアドレス" placeholder="例:test@test.jp"/> <input-field name="company" label="組織名" placeholder="例:藤原カンパニー" /> <input-field name="birthday" label="生年月日" placeholder="例:MM/DD/YYYY"/> <div class="flex justify-end relative pb-4"> <button class="py-2 px-4 text-blue-500 border border-blue-500 mr-5 hover:bg-gray-100 hover:border-blue-300 rounded">キャンセル</button> <button class="bg-blue-500 border border-blue-500 py-2 px-4 text-white hover:bg-blue-400 mr-1 rounded">新規登録</button> </div> </form> </div> </template> <script> import InputField from "../components/InputField"; export default { name: "ContactsCreate", components: { InputField, }, data() { return { form: { 'name': '', 'email': '', 'company': '', 'birthday': '' } } } } </script> <style scoped> </style>続きまして、components/InputField.vue です。
props に、'name', 'label', 'placeholder' を指定しております。components/InputField.vue<template> <div class="relative pb-4"> <label :for="name" class="text-blue-500 pt-2 uppercase text-xs font-bold absolute">{{ label }}</label> <input :id="name" type="text" class="pt-8 w-full text-gray-900 border-b pb-2 focus:outline-none focus:border-blue-400" :placeholder="placeholder"> </div> </template> <script> export default { name: "InputField", props: [ 'name', 'label', 'placeholder' ], data() { return { value: '' } } } </script> <style scoped> </style>tailwindcss の部分も書いておりますので、ちょっとごちゃごちゃと見難い部分があるかと思いますが、
これで、name や placeholder、label に値が受け渡されたのが確認できるかと思います。次は、今やったこととは反対に、子コンポーネントから親コンポーネントへデータを受け渡してみましょう。
components/InputField.vue// <input>タグに @input="updateField()" を追記 <template> <div class="relative pb-4"> <label :for="name" class="text-blue-500 pt-2 uppercase text-xs font-bold absolute">{{ label }}</label> <input :id="name" type="text" class="pt-8 w-full text-gray-900 border-b pb-2 focus:outline-none focus:border-blue-400" :placeholder="placeholder" v-model="value" @input="updateField()"> </div> </template> <script> export default { name: "InputField", props: [ 'name', 'label', 'placeholder' ], data() { return { value: '' } }, // 以下の methods:{} 部分を追加 methods: { updateField() { this.$emit('update:field', this.value); } } } </script>最後に、views/ContactsCreate.vue に
@update:field="form.name = $event"
等を<input-field/>
の中に書いて終わりです。<input-field/>
のコンポーネントが4つある感じになりますね。views/ContactsCreate.vue<template> <div> <form> <input-field name="name" label="お名前" placeholder="例:山田ルイ53世" @update:field="form.name = $event"/> <input-field name="email" label="メールアドレス" placeholder="例:test@test.jp" @update:field="form.email = $event"/> <input-field name="company" label="組織名" placeholder="例:藤原カンパニー" @update:field="form.company = $event"/> <input-field name="birthday" label="生年月日" placeholder="例:MM/DD/YYYY" @update:field="form.birthday = $event"/> <div class="flex justify-end relative pb-4"> <button class="py-2 px-4 text-blue-500 border border-blue-500 mr-5 hover:bg-gray-100 hover:border-blue-300 rounded">キャンセル</button> <button class="bg-blue-500 border border-blue-500 py-2 px-4 text-white hover:bg-blue-400 mr-1 rounded">新規登録</button> </div> </form> </div> </template> <script> import InputField from "../components/InputField"; export default { name: "ContactsCreate", components: { InputField, }, data() { return { form: { 'name': '', 'email': '', 'company': '', 'birthday': '' } } } } </script> <style scoped> </style>いかがでしたでしょうか? いろいろと足らない部分は多々ございますが、一旦ここで筆を置かせていただき、自分の担当であります、洗濯に移ります。明日も朝から洗濯するぞ!
tailwindcss は、一見カオティックな印象ですが、慣れるとそうでもないようなところがなかなか面白いですね!
それでは皆様、
Merry Christmas and Happy New year
良いお年を〜
- 投稿日:2019-12-24T23:51:07+09:00
Vue.js の フォーム について
この記事は、静岡 Advent Calendar 2019 の25日目の記事です。
はじめに
静岡の勉強会事情等々、皆様がいろいろと書いてくださっているので、自分はそちらの内容はまたの機会にすることにして、当初、Laravel を用いてテストについて何か書こうかなと思っておりました。
が、ちょっとばかり路線変更しまして、Vue.js の フォーム を中心に時間が許す限り何か思うがままに書こうかなと思います。
テストにつきましては、また別の機会にでも書きます。sqlite を使ってやるよくあるやつです。大した内容ではございません。
それから、次点として当初考えておりました、Laravel で過去に作った簡単なプロジェクトを Symfony(完全未経験) で書き直す。というものも考えてはおりましたが、風邪で昨日まで1週間寝込むという愚行を犯した自分には難しかったです。(今のところ娘に移っていないようなのが救いだったり...)うだうだと書きましたが、それでは、早速本題に入りますよ。
今回は上記スクリーンショットのような form を想定しております。
環境につきましては、バックエンドに Laravel (今回あまり関係ございませんが、ディレクトリ名とか Vue CLI のものとは異なります)を、フロントエンドに Vue.js / tailwindcss (Laravel Mixを使用)を使用いたします。
tailwindcss?? なんじゃそりゃ? という方は、こちらをご参照くださいませ。
CSSのクラスとして提供しているだけであり、個人的には、UI系のフレームワークより使い勝手がいいかなと思っています。作者さんはAdam Wathanです。
まぁ、Laravel / Vue 関連クラスターの著名な方なので、なんとなく安心感もありますね笑何はともあれ、まずはセットアップから〜
Vue.js と tailwindcss のセットアップ
それでは、最初に Vue.js と tailwindcss の下準備とセットアップでもしましょうか。
どこか好きな ディレクトリ に新規で Laravel プロジェクト を作りましょう。
$ composer create-project laravel/laravel プロジェクト名 "バージョン指定.*" Application key XXXXXXXXXうにゃうにゃでます set successfully.set successfully. と出ていたらインストールが完了です。インストールが終わりましたら、今作成したプロジェクトのディレクトリへ移動しておきましょう。
また、エディタでもプロジェクトを開いておきます。下記のコマンドを打って下準備をはじめましょう。
$ php artisan preset none
これで package.json の構成が最小限のものに変わったかと思います。
続きまして、今日の本題 Vue.js を入れていきましょう。$ php artisan preset vue
これで、package.json でも
"vue": "^2.5.7",
な感じに追加されていることが確認できるかと思います。npm or yarn で Vue Router と tailwindcss を入れる
## Using npm npm install vue-router tailwindcss --save-dev ## Using Yarn yarn add vue-router tailwindcss --save-devpackage.json に、下記のものが追加されていればOKです。
package.json"devDependencies": { "tailwindcss": "^1.1.4", "vue": "^2.5.7", "vue-router": "^3.1.3" },あともう少しです。
CSS に tailwindcss を追加します
resources/assets/sass/app.scss に下記の内容を書きます。
app.scss@tailwind base; @tailwind components; @tailwind utilities;tailwindcssは、ビルド時にこれらのディレクティブを、生成されたすべてのCSSと交換します。
tailwindcss 構成ファイルを作成します
下記のコマンドを打ち込み、tailwind.config.jsを作成します。
npx tailwind init
これにより、tailwind.config.js が、プロジェクトのルートディレクトリの直下に作成され、最小限のファイルが作成されます。tailwindcssに何か追加したいときに追加していくものですね。
詳細はこちら。webpack.mix.js を修正する (Laravel5.5 の場合)
webpack.mix.jslet mix = require('laravel-mix'); let tailwindcss = require('tailwindcss') /* |-------------------------------------------------------------------------- | Mix Asset Management |-------------------------------------------------------------------------- | | Mix provides a clean, fluent API for defining some Webpack build steps | for your Laravel application. By default, we are compiling the Sass | file for the application as well as bundling up all the JS files. | */ mix.js('resources/assets/js/app.js', 'public/js') .sass('resources/assets/sass/app.scss', 'public/css') .options({ processCssUrls: false, postCss: [ tailwindcss('./tailwind.config.js') ], });webpack.mix.js を修正する。(Laravel6.0 の場合)
webpack.mix.jsconst mix = require('laravel-mix'); const tailwindcss = require('tailwindcss') /* |-------------------------------------------------------------------------- | Mix Asset Management |-------------------------------------------------------------------------- | | Mix provides a clean, fluent API for defining some Webpack build steps | for your Laravel application. By default, we are compiling the Sass | file for the application as well as bundling up all the JS files. | */ mix.js('resources/js/app.js', 'public/js') .sass('resources/sass/app.scss', 'public/css') .options({ processCssUrls: false, postCss: [ tailwindcss('./tailwind.config.js') ], });npm or yarn で ビルド する
## Using npm npm run watch ## Using yarn yarn run watchちょっと長かったですね、それでは、下準備が終わったところで、
まずは、ファイルを2つ用意しましょう。それぞれ、views/ContactsCreate.vue と、components/InputField.vue を作ります。
ContactsCreate.vue から作業を進めていきます。
一旦、props を用いまして、親コンポーネントであります ContactsCreate.vue から、
子コンポーネントの components/InputField.vue へ、
v-bind を 使って、label や placeholder の値を受け渡すところまで書いてみます。views/ContactsCreate.vue<template> <div> <form> <input-field name="name" label="お名前" placeholder="例:山田ルイ53世"/> <input-field name="email" label="メールアドレス" placeholder="例:test@test.jp"/> <input-field name="company" label="組織名" placeholder="例:藤原カンパニー" /> <input-field name="birthday" label="生年月日" placeholder="例:MM/DD/YYYY"/> <div class="flex justify-end relative pb-4"> <button class="py-2 px-4 text-blue-500 border border-blue-500 mr-5 hover:bg-gray-100 hover:border-blue-300 rounded">キャンセル</button> <button class="bg-blue-500 border border-blue-500 py-2 px-4 text-white hover:bg-blue-400 mr-1 rounded">新規登録</button> </div> </form> </div> </template> <script> import InputField from "../components/InputField"; export default { name: "ContactsCreate", components: { InputField, }, data() { return { form: { 'name': '', 'email': '', 'company': '', 'birthday': '' } } } } </script> <style scoped> </style>続きまして、components/InputField.vue です。
props に、'name', 'label', 'placeholder' を指定しております。components/InputField.vue<template> <div class="relative pb-4"> <label :for="name" class="text-blue-500 pt-2 uppercase text-xs font-bold absolute">{{ label }}</label> <input :id="name" type="text" class="pt-8 w-full text-gray-900 border-b pb-2 focus:outline-none focus:border-blue-400" :placeholder="placeholder"> </div> </template> <script> export default { name: "InputField", props: [ 'name', 'label', 'placeholder' ], data() { return { value: '' } } } </script> <style scoped> </style>TailWind CSS の部分も書いておりますので、ちょっとごちゃごちゃと見難い部分があるかと思いますが、
これで、name や placeholder、label に値が受け渡されたのが確認できるかと思います。次は、今やったこととは反対に、子コンポーネントから親コンポーネントへデータを受け渡してみましょう。
components/InputField.vue// <input>タグに @input="updateField()" を追記 <template> <div class="relative pb-4"> <label :for="name" class="text-blue-500 pt-2 uppercase text-xs font-bold absolute">{{ label }}</label> <input :id="name" type="text" class="pt-8 w-full text-gray-900 border-b pb-2 focus:outline-none focus:border-blue-400" :placeholder="placeholder" v-model="value" @input="updateField()"> </div> </template> <script> export default { name: "InputField", props: [ 'name', 'label', 'placeholder' ], data() { return { value: '' } }, // 以下の methods:{} 部分を追加 methods: { updateField() { this.$emit('update:field', this.value); } } } </script>最後に、views/ContactsCreate.vue に
@update:field="form.name = $event"
等を<input-field/>
の中に書いて終わりです。<input-field/>
のコンポーネントが4つある感じになりますね。views/ContactsCreate.vue<template> <div> <form action=""> <input-field name="name" label="お名前" placeholder="例:山田ルイ53世" @update:field="form.name = $event"/> <input-field name="email" label="メールアドレス" placeholder="例:test@test.jp" @update:field="form.email = $event"/> <input-field name="company" label="組織名" placeholder="例:藤原カンパニー" @update:field="form.company = $event"/> <input-field name="birthday" label="生年月日" placeholder="例:MM/DD/YYYY" @update:field="form.birthday = $event"/> <div class="flex justify-end relative pb-4"> <button class="py-2 px-4 text-blue-500 border border-blue-500 mr-5 hover:bg-gray-100 hover:border-blue-300 rounded">キャンセル</button> <button class="bg-blue-500 border border-blue-500 py-2 px-4 text-white hover:bg-blue-400 mr-1 rounded">新規登録</button> </div> </form> </div> </template> <script> import InputField from "../components/InputField"; export default { name: "ContactsCreate", components: { InputField, }, data() { return { form: { 'name': '', 'email': '', 'company': '', 'birthday': '' } } } } </script> <style scoped> </style>いかがでしたでしょうか? いろいろと足らない部分は多々ございますが、一旦ここで筆を置かせていただき、自分の担当であります、洗濯に移ります。明日も朝から洗濯するぞ!
tailwindcss は、一見カオティックな印象ですが、慣れるとそうでもないようなところがなかなか面白いですね!
それでは皆様、
Merry Christmas and Happy New year
良いお年を〜
- 投稿日:2019-12-24T23:51:07+09:00
Vue.js の フォーム とか
この記事は、静岡 Advent Calendar 2019 の25日目の記事です。
はじめに
静岡の勉強会事情等々、皆様がいろいろと書いてくださっているので、自分はそちらの内容はまたの機会にすることにして、当初、Laravel を用いてテストについて何か書こうかなと思っておりました。
が、ちょっとばかり路線変更しまして、Vue.js の フォーム を中心に時間が許す限り何か思うがままに書こうかなと思います。
テストにつきましては、また別の機会にでも書きます。sqlite を使ってやるよくあるやつです。大した内容ではございません。
それから、次点として当初考えておりました、Laravel で過去に作った簡単なプロジェクトを Symfony(完全未経験) で書き直す。というものも考えてはおりましたが、風邪で昨日まで1週間寝込むという愚行を犯した自分には難しかったです。(今のところ娘に移っていないようなのが救いだったり...)うだうだと書きましたが、それでは、早速本題に入りますよ。
今回は下記スクリーンショットのような form を想定しております。
環境につきましては、バックエンドに Laravel (今回あまり関係ございませんが、ディレクトリ名とか Vue CLI のものとは異なります)を、フロントエンドに Vue.js / tailwindcss (Laravel Mixを使用)を使用いたします。
tailwindcss?? なんじゃそりゃ? という方は、こちらをご参照くださいませ。
CSSのクラスとして提供しているだけであり、個人的には、UI系のフレームワークより使い勝手がいいかなと思っています。作者さんはAdam Wathanです。
まぁ、Laravel / Vue 関連クラスターの著名な方なので、なんとなく安心感もありますね笑何はともあれ、まずはセットアップから〜
Vue.js と tailwindcss のセットアップ
それでは、最初に Vue.js と tailwindcss の下準備とセットアップでもしましょうか。
どこか好きな ディレクトリ に新規で Laravel プロジェクト を作りましょう。
$ composer create-project laravel/laravel プロジェクト名 "バージョン指定.*" Application key XXXXXXXXXうにゃうにゃでます set successfully.set successfully. と出ていたらインストールが完了です。インストールが終わりましたら、今作成したプロジェクトのディレクトリへ移動しておきましょう。
また、エディタでもプロジェクトを開いておきます。下記のコマンドを打って下準備をはじめましょう。
$ php artisan preset none
これで package.json の構成が最小限のものに変わったかと思います。
続きまして、今日の本題 Vue.js を入れていきましょう。$ php artisan preset vue
これで、package.json でも
"vue": "^2.5.7",
な感じに追加されていることが確認できるかと思います。npm or yarn で Vue Router と tailwindcss を入れる
## Using npm npm install vue-router tailwindcss --save-dev ## Using Yarn yarn add vue-router tailwindcss --save-devpackage.json に、下記のものが追加されていればOKです。
package.json"devDependencies": { "tailwindcss": "^1.1.4", "vue": "^2.5.7", "vue-router": "^3.1.3" },あともう少しです。
CSS に tailwindcss を追加します
resources/assets/sass/app.scss に下記の内容を書きます。
app.scss@tailwind base; @tailwind components; @tailwind utilities;tailwindcssは、ビルド時にこれらのディレクティブを、生成されたすべてのCSSと交換します。
tailwindcss 構成ファイルを作成します
下記のコマンドを打ち込み、tailwind.config.jsを作成します。
npx tailwind init
これにより、tailwind.config.js が、プロジェクトのルートディレクトリの直下に作成され、最小限のファイルが作成されます。tailwindcssに何か追加したいときに追加していくものですね。
詳細はこちら。webpack.mix.js を修正する (Laravel5.5 の場合)
webpack.mix.jslet mix = require('laravel-mix'); let tailwindcss = require('tailwindcss') /* |-------------------------------------------------------------------------- | Mix Asset Management |-------------------------------------------------------------------------- | | Mix provides a clean, fluent API for defining some Webpack build steps | for your Laravel application. By default, we are compiling the Sass | file for the application as well as bundling up all the JS files. | */ mix.js('resources/assets/js/app.js', 'public/js') .sass('resources/assets/sass/app.scss', 'public/css') .options({ processCssUrls: false, postCss: [ tailwindcss('./tailwind.config.js') ], });webpack.mix.js を修正する。(Laravel6.0 の場合)
webpack.mix.jsconst mix = require('laravel-mix'); const tailwindcss = require('tailwindcss') /* |-------------------------------------------------------------------------- | Mix Asset Management |-------------------------------------------------------------------------- | | Mix provides a clean, fluent API for defining some Webpack build steps | for your Laravel application. By default, we are compiling the Sass | file for the application as well as bundling up all the JS files. | */ mix.js('resources/js/app.js', 'public/js') .sass('resources/sass/app.scss', 'public/css') .options({ processCssUrls: false, postCss: [ tailwindcss('./tailwind.config.js') ], });npm or yarn で ビルド する
## Using npm npm run watch ## Using yarn yarn run watchちょっと長かったですね、あとちょっとです。
resources/views/layouts/app.blade.php の bladeテンプレートの、
<div id="app"></div>
の中を、下記のように変更します。resources/views/layouts/app.blade.php<!DOCTYPE html> <html lang="{{ app()->getLocale() }}"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- CSRF Token --> <meta name="csrf-token" content="{{ csrf_token() }}"> <title>{{ config('app.name', 'Laravel') }}</title> <!-- Styles --> <link href="{{ asset('css/app.css') }}" rel="stylesheet"> </head> <body> <div id="app"> <div class="h-screen"> @yield('content') </div> </div> <!-- Scripts --> <script src="{{ asset('js/app.js') }}"></script> </body> </html>それから、resources/views/home.blade.php も、下記のように変更しましょう。
resources/assets/js/components/App.vue を読み込むように記載しています。resources/views/home.blade.php@extends('layouts.app') @section('content') <App></App> @endsection続きまして、resources/assets/js/components/App.vue も下記のように書きます。
ポイントは、<router-link to="/contacts/create">
の箇所です。
/contacts/create のURLに遷移するように設定します。 ( Vue Router用の設定 )resources/assets/js/components/App.vue<template> <div> <router-link to="/contacts/create"> <p>新規登録</p> </router-link> </div> </template>ここまで出来たので、冒頭に導入した Vue Router を使ってみます。
resources/assets/js/app.js と resources/assets/js/router.js を、それぞれ開きます。まずは、resources/assets/js/app.js から修正していきますね。
下記のようにしておきましょう。
resources/assets/js/app.jsimport Vue from 'vue'; import router from "./router"; import App from "./components/App"; const app = new Vue({ el: '#app', components: { App }, router });続いて、resources/assets/js/router.js では、ヒストリーモードを指定し、URLが、/contacts/create のときに、ContactsCreate のコンポーネントが表示されるようにします。
resources/assets/js/router.jsimport Vue from 'vue'; import VueRouter from 'vue-router'; import ContactsCreate from "./views/ContactsCreate"; Vue.use(VueRouter); export default new VueRouter({ routes: [ { path: '/contacts/create', name: 'ContactsCreate', component: ContactsCreate } ], mode: 'history' });それでは、下準備が終わったところで、
まずは、ファイルを2つ用意しましょう。それぞれ、views/ContactsCreate.vue と、components/InputField.vue を作ります。
views/ContactsCreate.vue は、先ほど resources/assets/js/router.js で 指定したコンポーネントのファイルになりますね。
ContactsCreate.vue から作業を進めていきます。
一旦、props を用いまして、親コンポーネントであります ContactsCreate.vue から、
子コンポーネントの components/InputField.vue へ、
v-bind を 使って、label や placeholder の値を受け渡すところまで書いてみます。views/ContactsCreate.vue<template> <div> <form> <input-field name="name" label="お名前" placeholder="例:山田ルイ53世"/> <input-field name="email" label="メールアドレス" placeholder="例:test@test.jp"/> <input-field name="company" label="組織名" placeholder="例:藤原カンパニー" /> <input-field name="birthday" label="生年月日" placeholder="例:MM/DD/YYYY"/> <div class="flex justify-end relative pb-4"> <button class="py-2 px-4 text-blue-500 border border-blue-500 mr-5 hover:bg-gray-100 hover:border-blue-300 rounded">キャンセル</button> <button class="bg-blue-500 border border-blue-500 py-2 px-4 text-white hover:bg-blue-400 mr-1 rounded">新規登録</button> </div> </form> </div> </template> <script> import InputField from "../components/InputField"; export default { name: "ContactsCreate", components: { InputField, }, data() { return { form: { 'name': '', 'email': '', 'company': '', 'birthday': '' } } } } </script> <style scoped> </style>続きまして、components/InputField.vue です。
props に、'name', 'label', 'placeholder' を指定しております。components/InputField.vue<template> <div class="relative pb-4"> <label :for="name" class="text-blue-500 pt-2 uppercase text-xs font-bold absolute">{{ label }}</label> <input :id="name" type="text" class="pt-8 w-full text-gray-900 border-b pb-2 focus:outline-none focus:border-blue-400" :placeholder="placeholder"> </div> </template> <script> export default { name: "InputField", props: [ 'name', 'label', 'placeholder' ], data() { return { value: '' } } } </script> <style scoped> </style>tailwindcss の部分も書いておりますので、ちょっとごちゃごちゃと見難い部分があるかと思いますが、
これで、name や placeholder、label に値が受け渡されたのが確認できるかと思います。次は、今やったこととは反対に、子コンポーネントから親コンポーネントへデータを受け渡してみましょう。
components/InputField.vue// <input>タグに @input="updateField()" を追記 <template> <div class="relative pb-4"> <label :for="name" class="text-blue-500 pt-2 uppercase text-xs font-bold absolute">{{ label }}</label> <input :id="name" type="text" class="pt-8 w-full text-gray-900 border-b pb-2 focus:outline-none focus:border-blue-400" :placeholder="placeholder" v-model="value" @input="updateField()"> </div> </template> <script> export default { name: "InputField", props: [ 'name', 'label', 'placeholder' ], data() { return { value: '' } }, // 以下の methods:{} 部分を追加 methods: { updateField() { this.$emit('update:field', this.value); } } } </script>最後に、views/ContactsCreate.vue に
@update:field="form.name = $event"
等を<input-field/>
の中に書いて終わりです。<input-field/>
のコンポーネントが4つある感じになりますね。views/ContactsCreate.vue<template> <div> <form> <input-field name="name" label="お名前" placeholder="例:山田ルイ53世" @update:field="form.name = $event"/> <input-field name="email" label="メールアドレス" placeholder="例:test@test.jp" @update:field="form.email = $event"/> <input-field name="company" label="組織名" placeholder="例:藤原カンパニー" @update:field="form.company = $event"/> <input-field name="birthday" label="生年月日" placeholder="例:MM/DD/YYYY" @update:field="form.birthday = $event"/> <div class="flex justify-end relative pb-4"> <button class="py-2 px-4 text-blue-500 border border-blue-500 mr-5 hover:bg-gray-100 hover:border-blue-300 rounded">キャンセル</button> <button class="bg-blue-500 border border-blue-500 py-2 px-4 text-white hover:bg-blue-400 mr-1 rounded">新規登録</button> </div> </form> </div> </template> <script> import InputField from "../components/InputField"; export default { name: "ContactsCreate", components: { InputField, }, data() { return { form: { 'name': '', 'email': '', 'company': '', 'birthday': '' } } } } </script> <style scoped> </style>いかがでしたでしょうか? いろいろと足らない部分は多々ございますが、一旦ここで筆を置かせていただき、自分の担当であります、洗濯に移ります。明日も朝から洗濯するぞ!
tailwindcss は、一見カオティックな印象ですが、慣れるとそうでもないようなところがなかなか面白いですね!
それでは皆様、
Merry Christmas and Happy New year
良いお年を〜
- 投稿日:2019-12-24T23:43:29+09:00
Nuxt.js に後からテスト (Jest/vue-test-utils)を入れる
概要
Nuxt.js のプロジェクトに後から Jest と vue-test-utils を入れる機会があったのでメモしておきます。
必要なライブラリをインストール
yarn add -D @babel/plugin-transform-runtime @babel/preset-env @vue/test-utils jest vue-jest babel-core vue-jest
.babelrc を追加
プロジェクトのルート直下に
.babelrc
を追加します。// .babelrc { "presets": [ [ "@babel/preset-env", { "useBuiltIns": false, "targets": { "node": "current" } } ] ], "plugins": [ "@babel/plugin-transform-runtime" ] }jest.config.js を追加
続いて
jest.config.js
を追加します。module.exports = { transform: { '^.+\\.js$': 'babel-jest', '.*\\.(vue)$': 'vue-jest' }, moduleNameMapper: { '^@/(.*)$': '<rootDir>/$1', '^~/(.*)$': '<rootDir>/$1' }, moduleFileExtensions: ['js', 'json', 'vue'] }package.json に test script を追加
追加します。
"scripts": { .. "test": "jest --config jest.config.js" },これで
yarn run test
でテストを実行することができます。テストを追加
ここでは、
userInfo
のみをprops
にもつUserInfoCard
というコンポーネントをテストします。テストは.user-name
という CSS クラスの中にユーザー名が表示されるかのテストです。// test/components/userInfoCard.spec.js import { mount } from '@vue/test-utils' import UserInfoCard from '~/app/components/UserInfoCard.vue' import UserFixture from '@fixture/user' describe('UserInfoCard', () => { test('Display text', () => { const wrapper = mount(UserInfoCard, { propsData: { userInfo: UserFixture } }) expect(wrapper.find('.user-name').text()).toEqual('Test Name') }) })テストで
propsData
を渡す時の hash key 名は、コンポーネントが受け取るprops
名と合わせる必要があります。テストデータはfixture
として別ファイルで定義してあげると使いまわせて便利です。// test/fixtures/user.js export default { id: 1, name: 'Test Name', .. }
fixture
用のname mapper
もjest.config.js
に追加してあげると良いです。module.exports = { transform: { '^.+\\.js$': 'babel-jest', '.*\\.(vue)$': 'vue-jest' }, moduleNameMapper: { '^@/(.*)$': '<rootDir>/$1', '^~/(.*)$': '<rootDir>/$1', "^@fixture/(.*)$": "<rootDir>/test/fixtures/$1" // 追加 }, moduleFileExtensions: ['js', 'json', 'vue'] }テスト実行
yarn run test
でテスト実行してみましょう!
テストは通りましたか?終わりに
テストを入念に書くコンポーネントとそうでないコンポーネントを見極めた上で、テストを書いていきましょう。それでは良いテストライフを!
![]()
- 投稿日:2019-12-24T23:32:27+09:00
画像投稿がメインのサービスをはじめて運営してみて、失敗したこと、改善したこと、やろうとしていること
こんにちは、キッド✈️と申します。
東南アジア発のスタートアップスタジオ、GAOGAOのサーバーサイドエンジニアです。
このQiitaはGAOGAO Advent Calendar 2019の25日目の記事です。先日、@rikuhiroseさんと共同で開発している、旅行サービスをリリースしました。ユーザーによる画像投稿がメインになるサービスです。
リリースしたサービスはこちらです。
FRIP | 思わず友達を旅行に誘いたくなる、 おすすめなスポットがあつまる旅行口コミサイト実は、ユーザーによる画像投稿がメインのサービスを運営するのはこれがはじめてです。
実際に画像投稿メインのサービスを運営してみて、失敗したことがあったので、備忘録としてこちらに残すことにしました。
失敗したこと
(1)ファイルサイズが大きい画像のアップロードに失敗する
サービスをリリースしてすぐ、ありがたいことに友達が投稿をしてくれようとしました..!!嬉しい!!
と、喜んだのもつかの間。
投稿フォームを入力し、投稿する画像を選択し、いざアップロードしたら、画像が重すぎてエラーになりました。
理由としては、PHPのupload_max_filesizeを超えてしまったために、サーバー側まで画像を渡すことができませんでした。
お恥ずかしいのですが、PHPのupload_max_filesizeの上限を変更しておらず、上限に引っかかってしまったのです。
解決策:
php.iniのpost_max_sizeとupload_max_filesizeの上限を変更しました。
FRIPはHerokuで運用しています。
HerokuでPHPのupload_max_filesizeを変更するには、publicディレクトリ配下に、.user.ini という名前のファイルを作成し、そちらに書き込むことで反映させることができます。
.user.inipost_max_size = 20M upload_max_filesize = 5M(2)投稿に30秒以上かかってしまう
さて、upload_max_filesizeの上限をあげたことで、無事にサイズの大きな画像もアップロードできるようになりました。しかし、今度は別の問題が発生しました。
リリース当初、画像アップロードは、フォーム送信後にサーバーサイドで全てアップロード処理を行っていました。
ですが、ファイルサイズの大きい画像で、かつ複数枚ともなると、フォーム送信してから完了するまでの時間が長い長い...。計測したら、フォーム送信から完了まで30秒かかってしまうこともざらにありました。
これではユーザーが使ってくれない...。
解決策:
画像アップロードは、画像を選択したら非同期でアップロードを行い、アップロードして画像のidを返すようにしました。
フォームを全て入力し、送信する時点で、画像はアップロードされるようにしたことで、フォーム送信から完了までの時間を大幅に短縮することができました。
(3)ファイルサイズが大きすぎてサイトが劇的に遅くなる
さて、無事に投稿はスムーズにできるようになりました。
しかし、今度はユーザーがアップロードした画像のファイルサイズが大きすぎて、サイトが劇的に遅くなりました...。
画像アップロードはS3にアップロードして、テーブルにs3へのurlを保存。CloudFront経由でS3の画像を読み込んでいました。
お恥ずかしいのですが、ユーザーがアップロードした画像は何も改変せず、そのままS3にあげていました。
旅行系サービスなのでユーザーが投稿する画像は、ファイルサイズが大きくなりがちです。
トップページで152 × 152 pixelsでしか表示していないのに、読み込んでいる画像は4032 × 3024 pixels。そりゃ重くなるわけだ..。
今現状、投稿は16個ほどですが、読み込みが劇的に遅くなってしまいました。
解決策:
画像をリサイズ後に、S3へアップロードするようにしました。
FRIPはLaravelを使っているのですが、Laravel向けのライブラリで、簡単に画像をリサイズできるIntervention/imageがあり、こちらでリサイズする処理を入れました。
画像をS3にアップロードする前にこちらの処理を入れています。このように書くことで、width:600で、画像のアスペクト比を維持したままリサイズすることができます。
image.php\InterventionImage::make($image) ->resize(600, null, function ($constraint) { $constraint->aspectRatio(); $constraint->upsize(); })->save();また、こちらのリサイズ機能を追加する前に投稿された画像は未対応なままです。
そちらについては、今後はCloudinaly経由でリサイズして配信する予定しています。
今後やること
Cloudinalyから画像配信
現在は、画像アップロードまでに画像を一定のサイズにリサイズしてから、S3にアップロードし、CloudFront経由でS3の画像を読み込んでいます。
しかし、将来的にはデバイスごとに配信する画像のサイズを改変したいと思いました。
そこで便利なのが、Cloudinalyです。
Cloudinalyでは、画像のパスに「w_500」と、widthを指定するだけ、そのサイズにリサイズしてくれます。
https://res.cloudinary.com/frip/image/upload/c_scale,w_500/v1574897562/sample.jpgすでにS3で画像管理していても、CloudinalyならリソースURLのパスを紐づけるマッピング設定を行うことで、即時に自動アップロード・画像変換を行うことができます。
詳しくはこちらの記事に解説を譲ります。僕もこちらの記事を参考に設定しました。
S3 から Cloudinary への自動アップロードで即時に画像変換する|クラスメソッドブログ最後に
記事からも分かりますように大変未熟ですので、画像アップロードのフローで、もっとこうした方がいいよ!など改善アドバイスやフィードバックいただけたらすごく嬉しいです?♂️
何卒よろしくお願いいたします?♂️
メリークリスマス!
- 投稿日:2019-12-24T22:39:49+09:00
Vue CLIで自動生成されるmain.jsを読む
はじめに
この記事ではVue CLIの
vue create
コマンドでプロジェクトを作成した際に自動で生成されるmain.js
の中に記述されている数行のコードは何をする処理なのか読み解きます。
render関数の使い方やVue.configの各種設定には踏み込みません。利用するVueのバージョンは2.x、Vue CLIは3.xです。
読む前にVue CLIを利用して適当なプロジェクトを作成することをおすすめします。
vue create プロジェクトの名前
でプロジェクトを作成。
cd プロジェクトの名前
npm run serve
でローカルで開発サーバーを利用して起動。main.js ?
Vue CLIを利用してvueのプロジェクトを作成すると自動で
./src/main.js
というファイルが作成されます。
作成したプロジェクトはmain.js
をエントリーポイントとして動きます。main.jsimport Vue from 'vue' import App from './App.vue' Vue.config.productionTip = false new Vue({ render: h => h(App), }).$mount('#app')6行をほどの記述の中のimport文を除く以下の4つに分けて何をしているのか読んでいきます。
Vue.config.productionTip
new Vue
render: h => h(App)
.$mount('#app')
Vue.config.productionTip
公式ドキュメントによると
"これを false に設定すると、 Vue の起動時のプロダクションのヒントが表示されなくなります。"とのこと。
https://jp.vuejs.org/v2/api/index.html#productionTipここで言われるプロダクションのヒントとは何かを見てみます。
Vue.config.productionTip
にtrueを指定してブラウザの開発者ツールでconsoleを見ると下のように
development modeで起動していることを知らせるメッセージが出ます。
これが「起動時のプロダクションのヒント」です。
productionモードにするとこのような警告の出力はなくなりアプリの負荷を軽減できます。
https://vuejs.org/v2/guide/deployment.html#Turn-on-Production-ModeproductionTipのON/OFFで表示を切り替えられるメッセージは上であげたもの以外に見つかりませんでした。
メッセージ1つだけを切り替えるだけの設定でもなさそうなんですが...
(Vueのソース内でも探しましたが上記のメッセージ以外見つけられず...知ってるかた教えてください...)new Vue
シンプル。
Vueのインスタンスを作成しています。render: h => h(App)
まず書き方が構文的に分かりづらいので書き下します。
ちょうどこの部分について触れたissueがありました。
https://github.com/vuejs-templates/webpack-simple/issues/29#issuecomment-312902539
issue内であるように、下のように書き下すことができます。render: function (createElement) { return createElement(App); }render関数の引数にはVueインスタンスのcreateElement関数が来ていて、
h
と省略されています。
※h
はHyperscriptの略で仮想dom実装の中で一般的に使われるんだとか。
https://github.com/vuejs/babel-plugin-transform-vue-jsx/issues/6#issuecomment-232994673ここで登場するrenderはhtmlを描画するための関数です。
公式ドキュメントによるとrender
はtemplateの代替として利用できるものとされており
template
を利用して以下のように書き換えることもできます。main.js// import Vue from 'vue' import Vue from "vue/dist/vue.esm.js"; import App from './App.vue' Vue.config.productionTip = true // new Vue({ // render: h => h(App), // }).$mount('#test') new Vue({ components: { App }, template: '<App />' }).$mount('#test')templateを用いて書いた場合、事前にJavaScriptレンダリング関数にコンパイルされる必要があります。
importでVueにvue.esm.js
を利用するように書き変えたのはvueのビルドがランタイム限定ビルドを利用するのを回避するためで、この記述がなければ画面には何も表示されなくなります。vueのランタイム限定ビルドは完全版(コンパイラ込み)のビルドに比べておよそ30%軽量なため利用可能なら利用することが推奨されています。
ランタイム + コンパイラとランタイム限定の違い描画関数とJSXの項目に
Vue ではほとんどの場合 HTML をビルドするためにテンプレートを使うことが推奨されます
と書かれているため、なぜmain.js
でrender関数を利用するのか疑問でしたが
ランタイム限定ビルドを利用するのがモチベーションかなと考えています。
(明確な理由を探したが見つからないので知ってる方教えてください)引数の
createElement
はVueインスタンスの関数で、指定されたパラメータの情報を元に生成した仮想DOMを返します。.$mount('#app')
vm.$mount( [elementOrSelector] )によると
Vue インスタンスがインスタンス化において el オプションを受け取らない場合は、DOM 要素は関連付けなしで、”アンマウント(マウントされていない)” 状態になります。vm.$mount() は アンマウントな Vue インスタンスのマウンティングを手動で開始するために使用することができます。
とのこと。これだけ読んでもわかりにくいので
わかりやすくするために、少しコードを書き換えます。
./public/index.html
を見ると下のような記述がありますindex.html<body> <noscript> <strong>We're sorry but vue-banner doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> </noscript> <div id="app"></div> <!-- built files will be auto injected --> </body>ここの
<div id="app">を下のように書き換えてみてください
<div id="hoge">何も表示されなくなります。
続けてmain.js
の.$mount('#app')を下のように書き換えてみてください。
.$mount('#hoge')再び
App.vue
の内容が表示されるようになります。
この時点で気づくかもしれませんが
このvm.$mount()
は引数のセレクタで指定したhtml要素の子要素にVueインスタンスを挿入します。
main.js
を下のようにelオプションを利用する形式に書き換えても同様に<div id="hoge">
の子要素としてVueインスタンスが挿入されます。main.jsimport Vue from 'vue' import App from './App.vue' Vue.config.productionTip = true new Vue({ el: '#hoge', render: h => h(App), })また、
elオプション
とvm.$mount()
が同居した場合はelオプション
が優先されます。main.jsimport Vue from 'vue' import App from './App.vue' Vue.config.productionTip = true new Vue({ el: '#hoge', render: h => h(App), }).$mount('#fugafuga')※優先されるものの
vm.$mount()
が無効化されたわけではないようで、警告が出ました。
たった6行のコードしか書かれていない
main.js
でしたが、ちゃんと読むとなると結構読み応えあって面白かったです。
年末年始の暇な時にでも、もっと深く読み込んでみたい。それではよいクリスマスを
$\huge{?メリークリスマス!!!!?}$
- 投稿日:2019-12-24T21:16:27+09:00
DockerなLaravel6でHMR(Hot Module Replacement)
はじめに
Laravelでvue.jsを使った開発をするときは
laravel-mix付属のホットリロード機能(HMR)が便利です。ただ、ホストで動かすことが想定されており、dockerベースでHMRな開発環境を作ろうとして戸惑ったので情報を整理します。
概要
Laravel6 + vue.js on Docker な環境でHMRをする設定を説明します。
Laradockは使いません。ホスト側にバージョン管理が必要なツールはあまり入れたくないため、すべてDockerコンテナ内で完結させる方針です。
(yarnの実行はコンテナ内で実行すると遅いのですが、Ubuntu 上ではそれほど違いが感じられなかったため、コンテナ内で行う場合について記載しております。)環境
- Ubuntu 18.04
- Docker 19.03.5
- docker-compose 1.25.0
- Laravel 6
結論
- WebサーバーはLaravelのビルドインサーバーを使う
- PHPとNode.jsは同じコンテナに設定
- Webpackのプロキシ機能で8080のアクセスを8000ポート(ビルドインサーバー)に転送
ソースはこちら
https://github.com/odaryo/laravel6_vuejs_HMR環境構築について
ディレクトリ構成
┬- docker (Dockerの設定) │ └- app │ ├- conf.d (php設定ファイル) │ │ ├- php_settings.ini │ │ └- xdebug.ini │ └- Dockerfile ├- src (Laravelディレクトリ) └- docker-compose.ymlDocker環境
docker-compoe
appコンテナにはLaravelのビルドインサーバーを起動する設定を記載しています
docker-compose.ymlversion: '3' services: # laravel app: build: context: ./ dockerfile: ./docker/app/Dockerfile depends_on: - db links: - db volumes: - ./app:/app:cached - ./docker/app/conf.d/xdebug.ini:/usr/local/etc/php/conf.d/xdebug.ini - ./docker/app/conf.d/php_settings.ini:/usr/local/etc/php/conf.d/php_settings.ini command: bash -c "php artisan serve --host 0.0.0.0" # 起動時にビルドインサーバーを起動 ports: - "8000:8000" # php artisan serve 用のポート - "8080:8080" # HMR用のポート # DB (mysql) db: image: mysql:8 environment: - MYSQL_DATABASE=testdb - MYSQL_USER=testuser - MYSQL_PASSWORD=password - MYSQL_ROOT_PASSWORD=root - TZ=Asia/Tokyo restart: always volumes: - dev_mysql_data:/var/lib/mysql ports: - "33306:3306" volumes: dev_mysql_data: driver: localDockerfile
PHPコンテナとNode.jsコンテナに分けるとうまく行かなかったため、マルチステージビルドの形でPHPコンテナにNode.jsを追加する
Dockerfile# Nodeイメージ FROM node:13-alpine as node # PHPイメージ FROM php:7.3-alpine # Laravel環境に必要なパッケージをインストール RUN apk update \ && apk upgrade \ && apk add --no-cache \ bash \ git \ unzip \ libpng \ libpng-dev \ libjpeg \ icu \ icu-dev \ icu-libs \ libxml2 \ libxml2-dev \ openssl \ openssl-dev \ && docker-php-ext-install \ pdo_mysql \ mysqli \ gd \ mbstring \ intl \ xml \ opcache \ && docker-php-ext-enable intl mbstring \ && apk --update --no-cache add autoconf g++ make \ # xdebugインストール && pecl install -f xdebug \ && docker-php-ext-enable xdebug \ && apk del --purge autoconf g++ make # Composerのインストール RUN curl -sS https://getcomposer.org/installer | php ;mv composer.phar /usr/local/bin/composer; RUN composer global require hirak/prestissimo \ && composer global require phpunit/phpunit # Nodeコンテナからyarnとnodeをコピー COPY --from=node /opt/yarn-v* /opt/yarn COPY --from=node /usr/local/bin/node /usr/local/bin/ # 使いやすいようにシンボリックリンク作成 RUN ln -s /opt/yarn/bin/yarn /usr/local/bin/yarn \ && ln -s /opt/yarn/bin/yarnpkg /usr/local/bin/yarnpkg # ホスト側とuser_id, group_idを合わせる ARG USER_ID ARG GROUP_ID RUN addgroup -g ${GROUP_ID} -S app-user && \ adduser -u ${USER_ID} -S app-user -G app-user USER app-user:app-user # Setup working directory WORKDIR /app参考
構築手順
docker起動
$ docker-compose build $ docker-compose up -dlaravelインストール
appコンテナに入って実行します。
※laravelインストールまではコンテナが起動しないため、runコマンドで実行する必要があります。$ docker-compose run app /bin/bash $ composer create-project --prefer-dist laravel/laravel .vue.jsを使う設定
laravel 6.0からデフォルトではインストールされないため、laravel/uiからインストールします。
$ composer require laravel/ui --dev $ php artisan ui vue $ exitここでdockerを再起動しておく
$ docker-compose up -d
http://localhost:8000
にアクセスして、スタート画面が表示されたらOK
node_moduleのインストール
$ docker-compose exec app yarnHMRの設定
ホットリロードの確認用に、welcome.blade.phpのbody内にVue.jsのComponentを追加しておく
Componentはインストール時に作成されるサンプルを使用welcome.blade.php<div id="app"> <example-component></example-component> </div> <script src="{{ mix('js/app.js') }}"></script>webpack.mix.jsにHMRの設定を追加
8080へのアクセスではエラーとなり
Cannot GET /
が表示されてしまうため、ビルドインサーバーの8000ポートへプロキシしてやりますwebpack.mix.jsmix.webpackConfig({ devServer: { host: '0.0.0.0', port: 8080, proxy: { '*': 'http://0.0.0.0:8000' }, // Windows(Docker for windows)の場合は下記を追加する // watchOptions:{ // aggregateTimeout:200, // poll:5000 // }, } });ホットリロードの実行
$ docker-compose exec app yarn hot
http://localhost:8080
へアクセスして、コンポーネントの内容が表示されれば完了です。
コンポーネントを修正して、変更が反映されることを確認しましょう。参考
終わりに
ホットリロードでフロントエンド開発が捗ります。
注意点としては、HMRで監視するファイルはJavascriptやCSSなので、blade自体を更新した場合はブラウザの更新が必要となります。
- 投稿日:2019-12-24T20:03:18+09:00
Nuxtで「vue-clickaway」のディレクティブをグローバルに登録する
vue-clickaway
https://github.com/simplesmiler/vue-clickaway
要素の外側をクリックしたときにイベントを起こせるライブラリコード
pluginsの中で、
import Vue from 'vue' import { directive as onClickaway } from 'vue-clickaway'; Vue.directive('onClickaway', onClickaway);インポートしたディレクティブのグローバル登録がよくわからなかったのでメモ
- 投稿日:2019-12-24T19:32:41+09:00
【Veevalidate】 まとめ
- 投稿日:2019-12-24T17:03:57+09:00
.stopで伝播を防ぐ ❏Vue.js❏
マウスがのった場所のX軸とY軸を表示します。
ただし、stop
にのった時は伝播を防いでmousemove
イベントを発火させません。開発環境はJSFiddle
https://qiita.com/ITmanbow/items/9ae48d37aa5b847f1b3bhtml<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <div id="app"> <p v-on:mousemove="changeMousePosition">のせて! <span v-on:mousemove.stop>stop</span></p> <p>x:{{ x }}, y:{{ y }}</p> </div>javascriptnew Vue({ el: "#app", data: { x: 0, y: 0 }, methods: { changeMousePosition: function(event) { this.x = event.clientX; this.y = event.clientY; } } })
stopPropagation
を仕込むと伝播を防いでくれます。
本来はJS側でevent.stopPropagation()
と記述します。しかし、Vue.jsでは、html側で
v-on:mousemove.stop
と繋げることで簡単に実装できます。
ではまた!
- 投稿日:2019-12-24T14:33:55+09:00
Nuxt.jsにTypeScriptを導入する手順まとめ
Nuxt.jsのプロジェクトにTypeScriptを導入するには、基本的に公式ドキュメント通りで問題ないのですが、ちょっとハマった箇所もあったので備忘録としてまとめておきます。
Nuxt.jsのバージョンは
2.11.0
、typescriptのバージョンは3.7.4
です。まずはNuxt.jsプロジェクトを作成
設定は適宜変更してください。
# プロジェクト作成 npx create-nuxt-app nuxt-ts-sample > Generating Nuxt.js project in nuxt-ts-sample ? Project name nuxt-ts-sample ? Project description My astounding Nuxt.js project ? Author name itouuuuuuuuu ? Choose the package manager Npm ? Choose UI framework Element ? Choose custom server framework None (Recommended) ? Choose Nuxt.js modules Axios ? Choose linting tools ESLint ? Choose test framework None ? Choose rendering mode Universal (SSR) ? Choose development tools jsconfig.json (Recommended for VS Code)typescript-buildをインストール
まずはtypescript-buildをインストールします。
npm install --save-dev @nuxt/typescript-build次に
nuxt.config.js
のbuildModules
の箇所に設定を追加します。nuxt.config.jsexport default { buildModules: ['@nuxt/typescript-build'] }typescript-runtimeをインストール
npm install --save-dev @nuxt/typescript-runtimeインストール完了後、
package.json
のscriptsを、nuxt
からnuxt-ts
に書き換えます。package.json"scripts": { "dev": "nuxt-ts", "build": "nuxt-ts build", "generate": "nuxt-ts generate", "start": "nuxt-ts start" }nuxt.config.ts の設定
nuxt.config.js
のファイル名をnuxt.config.ts
に変更します。
また、extend(config, ctx) {}
で型を指定しなければエラーになるため、指定してあげます。nuxt.config.ts.diffbuild: { - extend(config, ctx) {} + extend(config: any, ctx: any) {} }
nuxt.config.ts
に下記を追加します。nuxt.config.tstypescript: { typeCheck: true, ignoreNotFoundWarnings: true }eslint-config-typescriptをインストール
npm install --save-dev @nuxtjs/eslint-config-typescript続いて、
package.json
のscript
のlint
を修正します。package.json.diff"scripts": { - "lint": "eslint --ext .js,.vue --ignore-path .gitignore ." + "lint": "eslint --ext .ts,.js,.vue --ignore-path .gitignore ." }
eslintrc.js
のextends
に下記を追加します。
@nuxtjs
がある場合は削除します。eslintrc.js.diffextends: [ - '@nuxtjs', + '@nuxtjs/eslint-config-typescript', ],必要なファイルの追加
tsconfig.json
ファイルをルート直下に作成します。tsconfig.json{ "compilerOptions": { "target": "es2018", "module": "esnext", "moduleResolution": "node", "lib": [ "esnext", "esnext.asynciterable", "dom" ], "esModuleInterop": true, "allowJs": true, "sourceMap": true, "strict": true, "noEmit": true, "baseUrl": ".", "paths": { "~/*": [ "./*" ], "@/*": [ "./*" ] }, "types": [ "@types/node", "@nuxt/types" ] }, "exclude": [ "node_modules" ] }Elementの問題を解決
UIフレームワークに
Element
を使用していない場合は、この項目は飛ばしても構いません。
Element
を選んでいる場合、npm run dev
を行うと下記のようなエラーが出ます。92:18 Interface 'NuxtApp' incorrectly extends interface 'Vue'. Types of property '$loading' are incompatible. Type 'NuxtLoading' is not assignable to type '(options: LoadingServiceOptions) => ElLoadingComponent'. Type 'NuxtLoading' provides no match for the signature '(options: LoadingServiceOptions): ElLoadingComponent'. 90 | } 91 | > 92 | export interface NuxtApp extends Vue { | ^ 93 | $options: NuxtAppOptions 94 | $loading: NuxtLoading 95 | context: Context ℹ Version: typescript 3.7.4 ℹ Time: 16566msこれを解決するため、
tsconfig.json
のcompilerOptions
に"skipLibCheck": true
を追加します。tsconfig.json"compilerOptions": { "skipLibCheck": true, }nuxt-property-decoratorをインストール
クラスを使用するために、nuxt-property-decoratorをインストールします。
npm install --save-dev nuxt-property-decoratornuxt-property-decoratorを有効にするために、
tsconfig.json
のcompilerOptions
に"experimentalDecorators": true
を追加します。tsconfig.json"compilerOptions": { "experimentalDecorators": true, }TypeScriptを使ってみる
pages/index.vue
のscript
の箇所をTypeScriptで書いてみます。
~/components/Logo.vue
をimportしている箇所に、.vue
をつけることに注意してください。pages/index.vue<script lang="ts"> import { Component, Vue } from 'nuxt-property-decorator' import Logo from '~/components/Logo.vue' // .vueを忘れずにつける @Component({ components: { Logo } }) export default class extends Vue { } </script>ESLintの問題を解決
ESLint
を使用している場合、npm run dev
を行うと下記のようなエラーが出ます。40:0 error Parsing error: Using the export keyword between a decorator and a class is not allowed. Please use `export @dec class` instead. 8 | }, 9 | }) > 10 | export default class extends Vue { } | ^ 11 | ✖ 1 problem (1 error, 0 warnings)これを解決するために、
.eslintrc.js
のbabel-eslint
を削除します。eslintrc.jsparserOptions: { parser: 'babel-eslint' // この行を削除 }確認
http://localhost:3000/にアクセスして、下記の様な画面が確認できれば完了です!
お疲れ様でした!
- 投稿日:2019-12-24T11:33:54+09:00
ネイティブのJavaScriptばっかり使ってた人のためのvue.js超入門
三度の飯よりJavaScript(大嘘)。
どうもなっかのうです。
今回はJavascriptについて話します。
※ネタ要素マシマシなので、胃がもたれやすい方は気をつけてください。
読んで欲しい人
- HTML/CSS/JSをちょっと理解してる人
- おふざけ嫌いじゃないよって人
「Vue.js」って誰?親戚にいたような...
Vue.jsは人ではありません。
JavaScriptのライブラリ、フレームワークです。
jQueryとかネイティブのJavaScriptでは結構手間のかかることも割と簡単にしてくれます。
Vue.jsは主にフロントエンドの開発に使うもので、
「DOM」と呼ばれるプログラムからHTMLを操るやつを自動的に行ってくれるそうです。
なんかすごいね。(わかってない)
どうやったら使えるのかね?
HTMLのheadタグのところに、
index.html<head> <!-- 省略 --> <script src="https://cdn.jsdelivr.net/npm/vue"></script> </head>と書きます。
これで下ごしらえは完了です。
本題です
今回は、
<input type="text">
の中身がHTMLにすぐさま更新されるものを作っていきます。それではソースコードです。
index.html<div id="app"> <h2>最優秀賞 <p>{{ message }}</p> </h2> <p>({{ name }})</p> <input v-model="message"> <input v-model="name"> </div>script.jsvar app = new Vue({ el: '#app', data: { message: '赤信号は止まらないと ダメよ〜 ダメダメ', name: 'ゆでたまご小学校 6年 エレキテル太郎くん' } })これだけであの動きができるようになります!すごいでしょ!
(内容がツッコミどころ満載ですが、許してくださいな)
ちなみに、この動作のキモとなるのは
<input v-model="name">
です。「v-model」がこのリアルタイムで書き換える動作をやってくれます。
そして、
{{ message }}
というのがVue.jsによって書き換えられる部分です。その書き換えられるデータは、script.jsの
data:{}
に書いてあります。Vueの最初の3ステップ
{{ message }}
で場所を作る!
el: 'id,class名'
で選択!
data:{ message:"書き換えたい文字"}
で置き換える!これでオーケーです!
みなさんも楽しいVue.jsライフを楽しみましょう!
- 投稿日:2019-12-24T10:47:24+09:00
vue.js laravel で いいね機能を作ろう
定義
・ユーザーにも、記事にも、色々なテーブルに対応できるように
・vue.js , laravel を利用
・コンポーネントにして使いまわしが効くように
・redis でも考えたが、そんなにめちゃくちゃいいねされる訳じゃないし、
いいね順にソートするとき微妙かなと考えたのでmysqlで今回は ユーザー に対し いいねできるように。
テーブルを作成
users id,name iines id,foreign_key,model(varchar255),user_id,updated_at,created_atここまででMYSQLの設定完了
User と iine を関連付ける。
User.php//User.id Iine の foreign_key 。さらには model が User のものを紐付ける。 public function iines() { return $this->hasMany('App\Iine','foreign_key')->where('model', 'User'); }いいねをしたり、いいねしているか取得する。
OkwsController.php//いいねを取得 public static function getIine(request $request) { $model = new Iine; $model->foreign_key = $request->foreign_key; $model->user_id = $request->user_id; $model->model = $request->model; $tmp = $model ->select('id') ->first(); //要素の存在チェック bool if(!empty($tmp->id)){ $res = true; } else { $res = false; } return response()->json(['res'=> $res]); } //いいねを追加 public static function addIine(request $request) { $model = new Iine; $model->foreign_key = $request->foreign_key; $model->user_id = $request->user_id; $model->model = $request->model; $tmp = $model ->select('id') ->first(); //要素の存在チェック bool if(!empty($tmp->id)){ $res = false; $model ->where('id',$tmp->id) ->delete();//削除 } else { $res = true; $model->save(); } return response()->json(['res'=> $res]); }あとは、web.phpにも動くようにルーティングを追加。
ここまででLaravel側の設定を完了。
つづいて、vue.js。
まずは、使い回しができるようにComponent化しておく。IineComponent.vue<template> <div> <button v-if="flag" @click="addIine"> いいね済 </button> <button @click="addIine" v-else> いいね </button> </div> </template> <script> export default { props: ['foreign_key','user_id','model'], data () { return { flag:false, }; }, created () { this.getIine(); }, methods: { //足跡を追加 addIine(){ let dataform = new FormData(); dataform.append('foreign_key',this.foreign_key); dataform.append('user_id',this.user_id); dataform.append('model',this.model); axios.post('/okws/addIine/', dataform).then(e => { this.flag = e.data.res; console.log("いいね成功"); }).catch((error) => { console.log("エラー"); }); }, //足跡を追加 getIine(){ let dataform = new FormData(); dataform.append('foreign_key',this.foreign_key); dataform.append('user_id',this.user_id); dataform.append('model',this.model); axios.post('/okws/getIine/', dataform).then(e => { this.flag = e.data.res; console.log("いいねできたか取得"); }).catch((error) => { console.log("エラー"); }); }, }, } </script>つづいては、 Userview.vue から コンポーネントを読み出そう。
foreign_key に 訪問先ページの User.id。
user_id に 現在ログインしている User.id
model は 今回はUserUserview.vue<!--500,100,'User'--> <iine-component :foreign_key="$route.params.id" :user_id="$root.user.id" :model="model"></iine-component>Userview.vue は app.js でルーティングを忘れずに。
ここまででいいね機能をつけることができました。
■ いいね順に並び替えよう
// 関連モデル hasmany iineをいくつ持っているか で並び替え $res = User::withCount('iines') ->orderBy('iines_count', 'desc') ->get(); foreach ($res as $v) { echo "記事のID : " . $v->id."<br>"; echo "いいね数 : " . $v->iines_count."<br>"; }結果
記事のID : 10186 いいね数 : 2 記事のID : 10185 いいね数 : 1 記事のID : 0 いいね数 : 0はい。並び替わりましたね♪
- 投稿日:2019-12-24T02:41:04+09:00
Vue Composition APIでストアパターンをスマートに使って状態管理をする
TL;DR
- [PR] Reactの状態管理ライブラリ「unstated-next」をVue Composition APIベースに移植したよ
- 状態をComposition APIで共有し、Read/Writeできるので便利だよ
- 特定のコンポーネントツリーでしか利用しないようにスコーピングができるよ
- 型もばっちり効くよ
Vue Composition APIのRFCもマージされ、Vue3のalphaもひっそりとリリースされていて、もういくつ寝るとVue3!という雰囲気になってきました。多くのVue.jsユーザがComposition APIの実戦投入について検討をしたり、それに向けた素振りをしているのではないかと思います。今年のアドベントカレンダーでも多くのアドベントカレンダーがComposition APIに触れていたり、いくつかの記事がストアの設計を絡めていて、自分も記事を横にサンプルコードを書いてみたりしました。
ところで、hooksで先行しているReactにはunstated-nextという必要最低限の実装(なんとTSで38行!)でとてもシンプルなライブラリがあります(使い方や仕組みは後述します)。過日業務で使ってみたのですが、導入したエンジニアのアツい推薦も納得するほどの使い勝手の良さでした。
Vueスタックでも同様の状態管理を行えたらなとぼんやりと思っていたのですが、特に同様のライブラリが存在しないようだったのと、Composition APIのドキュメントを読んでいる際に移植が可能だとわかったので作ってみました。
[Github] : https://github.com/resessh/vue-unstated
[npm] : https://www.npmjs.com/package/vue-unstated以下ライブラリの宣伝をしながら解説をする記事になります
![]()
なぜComposition APIだけではだめなのか
※ Composition APIを理解している方は読み飛ばしてください。
Composition APIはそもそもロジックの再利用性を高めることを目的とした仕様です。例えば以下のようなComposition Functionがあるとします。
use/counter.jsexport const useCounter = () => { // 状態 const state = reactive({ count: 0 }) // 状態を変更する関数 const increment = () => { state.count++ } return { state, increment, } }上記のComposition Functionをコンポーネントで使うには以下のように呼び出します。
App.vue<template> <div> <!-- クリックしたらカウントを増やす --> <button @click="increment">+</button> <!-- カウントを表示する --> <p>{{ count }}</p> </div> </template> <script> import { useCounter } from 'use/counter' export default { setup() { // Composition Functionを実行し、初期化された状態とメソッドを取り出す const { state, increment } = useCounter() // templateに状態・メソッドを渡す return { count: state.count, increment, } } } </script>使う際に
useCounter()
でComposition Functionを実行していることから察せられるように、各コンポーネントでuseCounter
を利用しても状態は共有されず、それぞれのコンポーネント内で別々のインスタンスを初期化しているような動作になります。Composition APIは上記コードのとおり、状態とロジックを再利用可能な形で切り出すことがとてもきれいにできる反面、切り出した状態を共有する仕組みは提供されていません。
つまり、このカウンターの状態を共有したい場合は、初期化されたComposition Functionの中身を1何かしらの方法でコンポーネントをまたがって共有しなければなりません。Vue Composition API を使ったストアパターンと TypeScript の組み合わせはどのくらいスケールするか? の記事ではこれを実現するために、 Vueの provide/inject というAPIを使っています。このAPIは「コンポーネントツリーのルートの方で
provide
したものは、同一コンポーネントツリー内であればどんなにルートから遠いコンポーネントでもinject
だけで呼び出すことができる」という機能を利用しています。今回作った
vue-unstated
もこのprovide/inject
を利用していて、基本的に同じ方法・方針で状態管理をしようとしています。デモ
vue-unstatedの使い方
ここからは上記デモのコードを使ってvue-unstatedの使い方について簡単に触れたいと思います。
Composition Functionをつくる
まずはVue Composition APIだけでComposition Functionを作ります。
今回はアイテムを登録することができるだけのTodoListを作ります。use/todos.jsimport { reactive } from "@vue/composition-api" const useTodos = () => { const state = reactive({ items: [], // Todoのアイテムの配列 latestId: 0, // 最新のアイテムのid }) const addItem = item => { state.latestId++ // Todoは { id: number, title: string } の構造 state.items.push({ id: state.latestId, title: item.title }) } return { items: state.items, // 共有したい状態はitemsだけなので、これだけexportする addItem, } }
unstatedコンテナを作る
次に、
vue-unstated
のcreateContainer
でunstatedコンテナにしてexportします。use/todos.js+import { createContainer } from 'vue-unstated' import { reactive } from "@vue/composition-api" const useTodos = () => { const state = reactive({ items: [], // Todoのアイテムの配列 latestId: 0, // 最新のアイテムのid }) const addItem = item => { state.latestId++ // Todoは { id: number, title: string } の構造 state.items.push({ id: state.latestId, title: item.title }) } return { items: state.items, // 共有したい状態はitemsだけなので、これだけexportする addItem, } } +export default createContainer(useTodos)
使いたいコンポーネントの親でprovideする
続けて、使いたいコンポーネントツリーのルートに近いコンポーネント(親側のコンポーネント)でコンテナを
provide
します。App.vue<template> <div id="app"> <todo-register/> <todo-list/> </div> </template> <script> import TodoRegister from "./components/TodoRegister.vue" import TodoList from "./components/TodoList.vue" +import TodoContainer from "./use/todos" export default { name: "App", components: { TodoRegister, TodoList, }, setup() { // const { items, addItem } = TodoContainer.provide() で即座に使うこともできます + TodoContainer.provide() } } </script>
使いたいコンポーネントでuseContainerする
最後に使いたい子コンポーネントでコンテナを
useContainer
します。Todoアイテムのリスト
components/TodoList.vue<template> <ul class="list"> <li v-for="item in items" :key="item.id"> <todo-item :item="item"/> </li> </ul> </template> <script> import TodoItem from "./TodoItem.vue" +import TodoContainer from "../use/todos" export default { name: "TodoList", components: { TodoItem }, setup() { + const { items } = TodoContainer.useContainer() + + return { items } } }; </script>Todoアイテムを登録するフォーム
components/TodoRegister.vue<template> <form @submit.prevent="onSubmit"> <input type="text" v-model="state.title"> <button type="submit">add</button> </form> </template> <script> import { reactive } from "@vue/composition-api" +import TodoContainer from "../use/todos" export default { name: "TodoRegister", setup() { + const { addItem } = TodoContainer.useContainer() const state = reactive({ title: "" }) const onSubmit = () => { + addItem({ title: state.title }) // Todoアイテムの登録 state.title = "" } return { state, onSubmit } } } </script>これだけでTodoアイテムを登録・閲覧できるようになります。
Vue Composition APIを使ってロジックを切り出しただけの状態と比べてもほぼ差がないのがわかるでしょうか?動作の仕組み
本家
unstated-next
も38行と大変短いコードですが、ほぼ同様の実装であるvue-unstated
も34行とわずかなコードで動いています。
基本的な方針は上述の Vue Composition API を使ったストアパターンと TypeScript の組み合わせはどのくらいスケールするか? の 「兄弟コンポーネント間でストアを共有」の項で示されているprovide/inject
でのストアオブジェクトの共有とほぼ同一です。
強いて挙げるとすれば、unstated
ではComposition Functionのインスタンスをまるごと共有しようという発想である点でしょうか?はじめてunstated-next
のコードを読んだ時は、腰が抜けてイスから転げ落ちました。興味がある方はぜひソースを読んでみてください。
https://github.com/resessh/vue-unstated/blob/master/src/index.tsまた、フィーチャーリクエストやコントリビューションもwelcomeですが、どうしてもプロジェクト内の固有の理由で変更を加えたい場合は、プロジェクトのリポジトリにコピペして編集してしまったほうが早いかもしれません2。34行しかないし、アクティブに変更も無いのではないかと思っています。
vue-unstatedを使うことのメリット
vue-unstated
を使うことで得られるメリットがいくつかあるので触れておきます。1. 自分でアノテーションしなくてもバッチリ型がつく
provide/inject
を自分で叩いてストアを共有する場合、inject<HogeStore>(key)
のようにアノテーションでお型付けをする必要があります。
しかし、unstatedコンテナでラップすると、Container.useContainer()
だけでバッチリ型がついたStoreが返ってきます。2. 簡単にstoreの依存関係を表現できる
下記コード例のように、ストア同士の依存関係をシンプルに記述することができます3。
// 検索結果をfetchするだけのComposition const useSearchResult = () => { // ... return { result, search } } export const SearchResultContainer = createContainer(useSearchResult) // 検索結果をフィルタするだけのComposition const useFilteredSearchResult = () => { // 検索結果をfetchするコンテナを利用する const { result } = SearchResultContainer.useContainer(); // フィルタする処理をかける(実際はフィルタのパラメータの状態を持ったりしてもっと複雑になる) const filteredSearchResult = SomeFilterFunction(result); return { result: filteredSearchResult } } export const FilteredSearchResultContainer = createContainer(useFilteredSearchResult)3. 利用しているコンポーネントツリーのインスタンスがGCされた場合、ストアもGCされる。
大規模なアプリケーションを運用していると、主にパフォーマンスの関係で、各ページをDynamic importしたり、いらない状態を消したかったりするのではないでしょうか。
vue-unstated
のメリットというより、provide/inject
のメリットですが、provideしたコンポーネントが消えれば参照カウントでunstatedインスタンスもGCされるため、今表示されているコンポーネントツリーにだけ必要な状態を持つことが可能です。まとめ
Vue Composition APIはまだまだこれからといったフェーズなので、nuxtのサーバサイドプロセスからのhydrationなど、Vuexから離れられなかったり、その他の状態管理方法にそれぞれの必要性や良さがあるのでないかと思っています。
ですが、今回ご紹介したprovide/inject
方式は、いまの所使い勝手の良さからメインストリームになってもおかしくないと思っています。
これから事例を積み上げていってpros/consを精緻にしたり、さらなるベストプラクティスやバッドプラクティスを見つけていけたらなと思っています。
もしライブラリを使ってもらえた場合、気軽にフィードバックいただけるとありがたいです![]()
参考記事
- 投稿日:2019-12-24T01:34:58+09:00
Vue.jsとMapbox GL JSでオリジナルの地図を表示してみよう ~Mapbox GL JSの機能を知る編~
はじめに
Vue.jsとMapbox GL JSを使って、このような機能を実装していきます。
- 地名検索
- 国名・地名の日本語化
- 位置情報表示
VueMapbox といったラッパーライブラリもありますが、
Mapbox GL JSの機能を知る編ということで、使わずに進めていきたいと思います。
結果として、Vue.jsの機能は全く使っていませんが・・・
次のステップとして、プラグイン化などしていけたらと思いますmapboxとは
mapboxは、地図を使用したアプリ開発者向けのプラットフォームです。
そのなかでMapbox GL JSは、Web GLを使用して地図を表示するJavaScriptライブラリです。
その他の地図サービス・APIに比べて、カスタマイズ性が高いとされています。環境構築
Vue.jsのプロジェクト作成とMapbox GL JSのインストールを行います。
Vue CLIでプロジェクト作成
vue create mapbox-vue今回はMapbox GL JSの機能を試したいので、
Vue RouterもVuexもなしのシンプルなプロジェクトにしています。? Please pick a preset: Manually select features ? Check the features needed for your project: Babel, TS, Linter ? Use class-style component syntax? Yes ? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes ? Pick a linter / formatter config: Prettier ? Pick additional lint features: Lint on save, Lint and fix on commit ? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config filesVue CLI Installation
Vue CLI Creating a ProjectMapbox GL JSのインストール
npmでMapbox GL JSをインストール
npm install mapbox-gl @types/mapbox-gl --save※TypeScriptを使用しているため、typesもインストールしています。
/public/index.html
へ以下を追加し、CSSを読み込む/public/index.html(抜粋)<link href="https://api.mapbox.com/mapbox-gl-js/v1.4.1/mapbox-gl.css" rel="stylesheet" />mapboxアカウント作成
Access Tokenを取得するため、
Create your Mapbox accountからアカウントを作成します。
SignUpするとAccountページにAccess Tokenが表示されます。
料金体系は、Webの場合50,000回までのマップ読み込みは無料となっています。
詳細は、Mapbox pricingへ。マップを表示する
まずは、マップを表示してみましょう。
/src/components/MyMap.vue
を追加します。
これからこのファイルを編集して機能を追加していきます。
Your Access Token
の箇所は、先ほど取得したAccess Tokenに変更してください。/src/App.vue<template> <div id="app"> <my-map /> </div> </template> <script lang="ts"> import { Component, Vue } from 'vue-property-decorator' import MyMap from './components/MyMap.vue' @Component({ components: { 'my-map': MyMap } }) export default class App extends Vue {} </script> <style> body { padding: 0; margin: 0; } #app { height: 100vh; } </style>/src/components/MyMap.vue<template> <div id="map"></div> </template> <script lang="ts"> import { Component, Vue } from 'vue-property-decorator' import mapboxgl, { MapboxOptions, Map } from 'mapbox-gl' @Component({}) export default class extends Vue { map: Map = {} as Map option: MapboxOptions = { accessToken: 'Your Access Token', container: 'map', style: 'mapbox://styles/mapbox/streets-v8', center: [143.767125, 38.681236], zoom: 4 } mounted() { this.map = new mapboxgl.Map(this.option) } } </script> <style scoped> #map { width: 100%; height: 100%; } </style>仕組み
mapbox-gl
を読み込む/src/components/MyMap.vueimport mapboxgl, { MapboxOptions, Map } from 'mapbox-gl'mapboxのオプションを設定する
/src/components/MyMap.vue(抜粋)option: MapboxOptions = { accessToken: 'Your Access Token', container: 'map', style: 'mapbox://styles/mapbox/streets-v8', center: [143.767125, 38.681236], zoom: 4 }設定できるオプションの詳細は、Mapbox GL JS API reference Map へ。
accessToken
、container
は必須になります。
container
には、マップをバインドするタグのid
を指定します。マップをバインドするタグを作成し、高さを指定する
/src/App.vue(抜粋)<style> body { padding: 0; margin: 0; } #app { height: 100vh; } </style>/src/components/MyMap.vue(抜粋)<template> <div id="map"></div> </template> <style scoped> #map { width: 100%; height: 100%; } </style>Mapオブジェクトを生成する
/src/components/MyMap.vue(抜粋)mounted() { this.map = new mapboxgl.Map(this.option) }DOM作成後である必要があるので、
mounted
フックでMap
オブジェクトを生成します。
このMap
オブジェクトに、マップを操作するファンクションやイベントが定義されています。検索機能を追加する
次に、検索機能を追加してみましょう。
検索機能は、mapbox-gl-geocoder
プラグインから提供されています。
npmで
mapbox-gl-geocoder
をインストールnpm install @mapbox/mapbox-gl-geocoder --save
/public/index.html
へ以下を追加し、CSSを読み込む/public/index.html(抜粋)<link href="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v4.4.2/mapbox-gl-geocoder.css" rel="stylesheet" />
Map
オブジェクトにコントロールを追加/src/components/MyMap.vue(抜粋)<script lang="ts"> // 省略 const MapboxGeocoder = require('@mapbox/mapbox-gl-geocoder') @Component({}) export default class extends Vue { // 省略 mounted() { this.map = new mapboxgl.Map(this.option) this.map.addControl( new MapboxGeocoder({ accessToken: this.option.accessToken, mapboxgl: mapboxgl }) ) } } </script>仕組み
検索時には、Geocoding API が呼ばれています。
GET https://api.mapbox.com/geocoding/v5/{endpoint}/{search_text}.jsonMapbox GL JSから、このような
Control
などのUIも提供されていますし、APIとしても提供されていますので、UIを自作することもできます。
その他のAPIの詳細は、mapbox API Documentation へ。地名を日本語化する
英語のままでは見づらいので、日本語化してみましょう。
日本語化は、mapbox-gl-language
プラグインから提供されています。
npmで
mapbox-gl-language
をインストールnpm install @mapbox/mapbox-gl-language --save
Map
オブジェクトにコントロールを追加/src/components/MyMap.vue(抜粋)<script lang="ts"> // 省略 const MapboxLanguage = require('@mapbox/mapbox-gl-language') @Component({}) export default class extends Vue { // 省略 mounted() { this.map = new mapboxgl.Map(this.option) // 省略 this.map.addControl( new MapboxLanguage({ defaultLanguage: 'ja' }) ) } } </script>マップのスタイルを作成する
Mapbox Studioでは、Web上で好きなスタイルを作成することができます。
Mapbox Studio での編集
ここで様々な編集ができます。
詳しい使い方は、Mapbox Studio Manual へ。今回は、日本語のSatellite Streetsを作成しました。
作成したスタイルの適用
option
のstyle
を変更/src/components/MyMap.vue(抜粋)<script lang="ts"> // 省略 @Component({}) export default class extends Vue { // 省略 option: MapboxOptions = { accessToken: 'Your Access Token', container: 'map', style: 'Copied Style URL', center: [143.767125, 38.681236], zoom: 4 } // 省略 } </script>位置情報を表示する
最後に、位置情報を表示してみましょう。
今回は私の好きな美術館を表示してみたいと思います。
レイヤーを追加
/src/components/MyMap.vue(抜粋)<script lang="ts"> // 省略 @Component({}) export default class extends Vue { // 省略 mounted() { // 省略 this.map.on('load', () => { this.map.addLayer({ id: 'points', type: 'symbol', source: { type: 'geojson', data: { type: 'FeatureCollection', features: [ { type: 'Feature', geometry: { type: 'Point', coordinates: [139.775792, 35.715622] }, properties: { title: '国立西洋美術館', icon: 'museum' } }, { type: 'Feature', geometry: { type: 'Point', coordinates: [139.630669, 35.457194] }, properties: { title: '横浜美術館', icon: 'museum' } }, { type: 'Feature', geometry: { type: 'Point', coordinates: [139.051066, 35.2454] }, properties: { title: '彫刻の森美術館', icon: 'museum' } }, { type: 'Feature', geometry: { type: 'Point', coordinates: [139.021225, 35.256709] }, properties: { title: 'ポーラ美術館', icon: 'museum' } }, { type: 'Feature', geometry: { type: 'Point', coordinates: [139.726423, 35.665322] }, properties: { title: '国立新美術館', icon: 'museum' } } ] } }, layout: { 'icon-image': ['concat', ['get', 'icon'], '-15'], 'text-field': ['get', 'title'], 'text-font': ['ヒラギノ角ゴ Pro W3', 'メイリオ', 'sans-serif'], 'text-offset': [0, 0.6], 'text-anchor': 'top' } }) }) } } </script>仕組み
様々なレイヤーを組み合わせることでマップを表示しています。
Layerには主にtype
、sorce
、layout
、paint
が設定できます。
詳しい仕様は、mapbox Style Specification Layers へ。今回は
座標
をアイコン表示
するので、
type
はsymbol
、source
はgeojson
にしています。GeoJSON とは
地理空間を表現するためのJSONフォーマットです。
詳しい仕様は、GeoJSON へ。おわりに
ここまでMapbox GL JSの主要な機能を見てきました。
mapboxのSolutionsには、様々なユースケースが紹介されていますので、こちらもぜひ!業務ではラッパーライブラリを使用していますが、もう少し自由度がほしいと思うこともしばしば・・・。
次は、ここからVue.jsのアプリとしてもっと使いやすくしていきたいと思います