20191224のvue.jsに関する記事は15件です。

Vue.js の フォーム とか tailwindcss とか Vue Router とか コンポーネント とか

この記事は、静岡 Advent Calendar 2019 の25日目の記事です。

はじめに

静岡の勉強会事情等々、皆様がいろいろと書いてくださっているので、自分はそちらの内容はまたの機会にすることにして、当初、Laravel を用いてテストについて何か書こうかなと思っておりました。

が、ちょっとばかり路線変更しまして、Vue.js の フォーム を中心に時間が許す限り何か思うがままに書こうかなと思います。
テストにつきましては、また別の機会にでも書きます。sqlite を使ってやるよくあるやつです。大した内容ではございません。
それから、次点として当初考えておりました、Laravel で過去に作った簡単なプロジェクトを Symfony(完全未経験) で書き直す。というものも考えてはおりましたが、風邪で昨日まで1週間寝込むという愚行を犯した自分には難しかったです。(今のところ娘に移っていないようなのが救いだったり...)

うだうだと書きましたが、それでは、早速本題に入りますよ。

今回は下記スクリーンショットのような form を想定しております。

スクリーンショット 2019-12-24 16.44.08.png

環境につきましては、バックエンドに 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 Routertailwindcss を入れる

## Using npm
npm install vue-router tailwindcss --save-dev

## Using Yarn
yarn add vue-router tailwindcss --save-dev

package.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.js
let 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.js
const 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.js
import 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.js
import 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

良いお年を〜

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js の フォーム について

この記事は、静岡 Advent Calendar 2019 の25日目の記事です。

はじめに

静岡の勉強会事情等々、皆様がいろいろと書いてくださっているので、自分はそちらの内容はまたの機会にすることにして、当初、Laravel を用いてテストについて何か書こうかなと思っておりました。

が、ちょっとばかり路線変更しまして、Vue.js の フォーム を中心に時間が許す限り何か思うがままに書こうかなと思います。
テストにつきましては、また別の機会にでも書きます。sqlite を使ってやるよくあるやつです。大した内容ではございません。
それから、次点として当初考えておりました、Laravel で過去に作った簡単なプロジェクトを Symfony(完全未経験) で書き直す。というものも考えてはおりましたが、風邪で昨日まで1週間寝込むという愚行を犯した自分には難しかったです。(今のところ娘に移っていないようなのが救いだったり...)

うだうだと書きましたが、それでは、早速本題に入りますよ。

今回は上記スクリーンショットのような form を想定しております。

スクリーンショット 2019-12-24 16.44.08.png

環境につきましては、バックエンドに 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 Routertailwindcss を入れる

## Using npm
npm install vue-router tailwindcss --save-dev

## Using Yarn
yarn add vue-router tailwindcss --save-dev

package.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.js
let 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.js
const 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

良いお年を〜

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js の フォーム とか

この記事は、静岡 Advent Calendar 2019 の25日目の記事です。

はじめに

静岡の勉強会事情等々、皆様がいろいろと書いてくださっているので、自分はそちらの内容はまたの機会にすることにして、当初、Laravel を用いてテストについて何か書こうかなと思っておりました。

が、ちょっとばかり路線変更しまして、Vue.js の フォーム を中心に時間が許す限り何か思うがままに書こうかなと思います。
テストにつきましては、また別の機会にでも書きます。sqlite を使ってやるよくあるやつです。大した内容ではございません。
それから、次点として当初考えておりました、Laravel で過去に作った簡単なプロジェクトを Symfony(完全未経験) で書き直す。というものも考えてはおりましたが、風邪で昨日まで1週間寝込むという愚行を犯した自分には難しかったです。(今のところ娘に移っていないようなのが救いだったり...)

うだうだと書きましたが、それでは、早速本題に入りますよ。

今回は下記スクリーンショットのような form を想定しております。

スクリーンショット 2019-12-24 16.44.08.png

環境につきましては、バックエンドに 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 Routertailwindcss を入れる

## Using npm
npm install vue-router tailwindcss --save-dev

## Using Yarn
yarn add vue-router tailwindcss --save-dev

package.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.js
let 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.js
const 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.js
import 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.js
import 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

良いお年を〜

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 mapperjest.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 でテスト実行してみましょう!
テストは通りましたか?

終わりに

テストを入念に書くコンポーネントとそうでないコンポーネントを見極めた上で、テストを書いていきましょう。それでは良いテストライフを! :thumbsup:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

画像投稿がメインのサービスをはじめて運営してみて、失敗したこと、改善したこと、やろうとしていること

こんにちは、キッド✈️と申します。
東南アジア発のスタートアップスタジオ、GAOGAOのサーバーサイドエンジニアです。
このQiitaはGAOGAO Advent Calendar 2019の25日目の記事です。

先日、@rikuhiroseさんと共同で開発している、旅行サービスをリリースしました。ユーザーによる画像投稿がメインになるサービスです。

リリースしたサービスはこちらです。
20191214003023.png
FRIP | 思わず友達を旅行に誘いたくなる、 おすすめなスポットがあつまる旅行口コミサイト

実は、ユーザーによる画像投稿がメインのサービスを運営するのはこれがはじめてです。

実際に画像投稿メインのサービスを運営してみて、失敗したことがあったので、備忘録としてこちらに残すことにしました。

失敗したこと

(1)ファイルサイズが大きい画像のアップロードに失敗する

サービスをリリースしてすぐ、ありがたいことに友達が投稿をしてくれようとしました..!!嬉しい!!

と、喜んだのもつかの間。

投稿フォームを入力し、投稿する画像を選択し、いざアップロードしたら、画像が重すぎてエラーになりました。

スクリーンショット 2019-12-24 22.46.53.png

理由としては、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.ini
post_max_size = 20M
upload_max_filesize = 5M

(2)投稿に30秒以上かかってしまう

さて、upload_max_filesizeの上限をあげたことで、無事にサイズの大きな画像もアップロードできるようになりました。しかし、今度は別の問題が発生しました。

リリース当初、画像アップロードは、フォーム送信後にサーバーサイドで全てアップロード処理を行っていました。

スクリーンショット 2019-12-24 23.29.28.png

ですが、ファイルサイズの大きい画像で、かつ複数枚ともなると、フォーム送信してから完了するまでの時間が長い長い...。計測したら、フォーム送信から完了まで30秒かかってしまうこともざらにありました。

これではユーザーが使ってくれない...。

解決策:

画像アップロードは、画像を選択したら非同期でアップロードを行い、アップロードして画像のidを返すようにしました。

フォームを全て入力し、送信する時点で、画像はアップロードされるようにしたことで、フォーム送信から完了までの時間を大幅に短縮することができました。

(3)ファイルサイズが大きすぎてサイトが劇的に遅くなる

さて、無事に投稿はスムーズにできるようになりました。

しかし、今度はユーザーがアップロードした画像のファイルサイズが大きすぎて、サイトが劇的に遅くなりました...。

画像アップロードはS3にアップロードして、テーブルにs3へのurlを保存。CloudFront経由でS3の画像を読み込んでいました。

お恥ずかしいのですが、ユーザーがアップロードした画像は何も改変せず、そのままS3にあげていました。

旅行系サービスなのでユーザーが投稿する画像は、ファイルサイズが大きくなりがちです。

スクリーンショット 2019-12-24 22.40.44.png

トップページで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 への自動アップロードで即時に画像変換する|クラスメソッドブログ

最後に

記事からも分かりますように大変未熟ですので、画像アップロードのフローで、もっとこうした方がいいよ!など改善アドバイスやフィードバックいただけたらすごく嬉しいです?‍♂️

何卒よろしくお願いいたします?‍♂️

メリークリスマス!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.js
import 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で起動していることを知らせるメッセージが出ます。
これが「起動時のプロダクションのヒント」です。
スクリーンショット 2019-12-23 22.45.53.png

productionモードにするとこのような警告の出力はなくなりアプリの負荷を軽減できます。
https://vuejs.org/v2/guide/deployment.html#Turn-on-Production-Mode

productionTipの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を描画するための関数です。
公式ドキュメントによるとrendertemplateの代替として利用できるものとされており
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.js
import 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.js
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = true

new Vue({
  el: '#hoge',
  render: h => h(App),
}).$mount('#fugafuga')

※優先されるもののvm.$mount()が無効化されたわけではないようで、警告が出ました。
スクリーンショット 2019-12-24 22.28.38.png

たった6行のコードしか書かれていないmain.jsでしたが、ちゃんと読むとなると結構読み応えあって面白かったです。
年末年始の暇な時にでも、もっと深く読み込んでみたい。

それではよいクリスマスを

$\huge{?メリークリスマス!!!!?}$

【参照】
renderの実装
createElementの実装

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.yml

Docker環境

docker-compoe

appコンテナにはLaravelのビルドインサーバーを起動する設定を記載しています

docker-compose.yml
version: '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: local

Dockerfile

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 -d

laravelインストール

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
image.png

node_moduleのインストール

$ docker-compose exec app yarn

HMRの設定

ホットリロードの確認用に、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.js
mix.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へアクセスして、コンポーネントの内容が表示されれば完了です。
コンポーネントを修正して、変更が反映されることを確認しましょう。

image.png

参考

終わりに

ホットリロードでフロントエンド開発が捗ります。

注意点としては、HMRで監視するファイルはJavascriptやCSSなので、blade自体を更新した場合はブラウザの更新が必要となります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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);

インポートしたディレクティブのグローバル登録がよくわからなかったのでメモ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Veevalidate】 まとめ

【Veevalidate】 まとめ

example.vue
<ValidationObserver ref="obs1" v-slot="{ errors, invalid, touched }">
...
</ValidationObserver>

初期化

methods: {
  test () {
    this.$refs.obs1.reset()
  }
}

特定のflags(ex. touched)にアクセス

methods: {
  test () {
    this.$refs.obs1.flags.touched
  }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

.stopで伝播を防ぐ ❏Vue.js❏

マウスがのった場所のX軸とY軸を表示します。
ただし、stopにのった時は伝播を防いでmousemoveイベントを発火させません。

Screenshot from Gyazo

開発環境はJSFiddle
https://qiita.com/ITmanbow/items/9ae48d37aa5b847f1b3b

html
<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>
javascript
new 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と繋げることで簡単に実装できます。



ではまた!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.jsbuildModules の箇所に設定を追加します。

nuxt.config.js
export 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.diff
  build: {
-    extend(config, ctx) {}
+    extend(config: any, ctx: any) {}
  }

nuxt.config.ts に下記を追加します。

nuxt.config.ts
typescript: {
  typeCheck: true,
  ignoreNotFoundWarnings: true
}

eslint-config-typescriptをインストール

npm install --save-dev @nuxtjs/eslint-config-typescript

続いて、 package.jsonscriptlint を修正します。

package.json.diff
  "scripts": {
-    "lint": "eslint --ext .js,.vue --ignore-path .gitignore ."
+    "lint": "eslint --ext .ts,.js,.vue --ignore-path .gitignore ."
  }

eslintrc.jsextends に下記を追加します。
@nuxtjs がある場合は削除します。

eslintrc.js.diff
  extends: [
-   '@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.jsoncompilerOptions"skipLibCheck": true を追加します。

tsconfig.json
"compilerOptions": {
  "skipLibCheck": true,
}

nuxt-property-decoratorをインストール

クラスを使用するために、nuxt-property-decoratorをインストールします。

npm install --save-dev nuxt-property-decorator

nuxt-property-decoratorを有効にするために、 tsconfig.jsoncompilerOptions"experimentalDecorators": true を追加します。

tsconfig.json
"compilerOptions": {
  "experimentalDecorators": true,
}

TypeScriptを使ってみる

pages/index.vuescript の箇所を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.jsbabel-eslint を削除します。

eslintrc.js
parserOptions: {
  parser: 'babel-eslint' // この行を削除
}

確認

http://localhost:3000/にアクセスして、下記の様な画面が確認できれば完了です!
お疲れ様でした!

nuxt-ts-sample

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ネイティブの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>

と書きます。

これで下ごしらえは完了です。

本題です

画面収録 2019-12-24 11.14.33.mov.gif

今回は、<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.js
var 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ライフを楽しみましょう!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 は 今回はUser

Userview.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

はい。並び替わりましたね♪

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

以下ライブラリの宣伝をしながら解説をする記事になります :pray:

なぜComposition APIだけではだめなのか

※ Composition APIを理解している方は読み飛ばしてください。

Composition APIはそもそもロジックの再利用性を高めることを目的とした仕様です。例えば以下のようなComposition Functionがあるとします。

use/counter.js
export 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 を利用していて、基本的に同じ方法・方針で状態管理をしようとしています

デモ

実際にコードを見たり触ったりできるデモはこちらになります。
Edit [vue-unstated DEMO] Todo

vue-unstatedの使い方

ここからは上記デモのコードを使ってvue-unstatedの使い方について簡単に触れたいと思います。

:one: Composition Functionをつくる

まずはVue Composition APIだけでComposition Functionを作ります。
今回はアイテムを登録することができるだけのTodoListを作ります。

use/todos.js
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,
  }
}

:two: unstatedコンテナを作る

次に、 vue-unstatedcreateContainer で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)

:three: 使いたいコンポーネントの親で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>

:four: 使いたいコンポーネントで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を精緻にしたり、さらなるベストプラクティスやバッドプラクティスを見つけていけたらなと思っています。
もしライブラリを使ってもらえた場合、気軽にフィードバックいただけるとありがたいです :pray:


参考記事


  1. この場合 useCounter() で得られた { state, increment } を指す。 

  2. unstated-next を利用していたときは、react-routerとの兼ね合いで、コードを変更せざるを得なかったことがあった気がします。 

  3. まだ実際に動かして試してないので、動かなかったらメンゴです。動くように直します。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.jsとMapbox GL JSでオリジナルの地図を表示してみよう ~Mapbox GL JSの機能を知る編~

はじめに

Vue.jsとMapbox GL JSを使って、このような機能を実装していきます。

  • 地名検索
  • 国名・地名の日本語化
  • 位置情報表示

VueMapbox といったラッパーライブラリもありますが、
Mapbox GL JSの機能を知る編ということで、使わずに進めていきたいと思います。
結果として、Vue.jsの機能は全く使っていませんが・・・
次のステップとして、プラグイン化などしていけたらと思います:bow_tone1:

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 files

Vue CLI Installation
Vue CLI Creating a Project

Mapbox GL JSのインストール

  1. npmでMapbox GL JSをインストール

    npm install mapbox-gl @types/mapbox-gl --save
    

    ※TypeScriptを使用しているため、typesもインストールしています。

  2. /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 GL JS Install

mapboxアカウント作成

Access Tokenを取得するため、
Create your Mapbox accountからアカウントを作成します。
SignUpするとAccountページにAccess Tokenが表示されます。
image.png

料金体系は、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>

すると、このように地図が表示されます!
image.png

仕組み

  1. mapbox-gl を読み込む

    /src/components/MyMap.vue
    import mapboxgl, { MapboxOptions, Map } from 'mapbox-gl'
    
  2. 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 へ。
    accessTokencontainerは必須になります。
    containerには、マップをバインドするタグのidを指定します。

  3. マップをバインドするタグを作成し、高さを指定する

    /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>
    
  4. Mapオブジェクトを生成する

    /src/components/MyMap.vue(抜粋)
    mounted() {
      this.map = new mapboxgl.Map(this.option)
    }
    

    DOM作成後である必要があるので、mountedフックでMapオブジェクトを生成します。
    このMapオブジェクトに、マップを操作するファンクションやイベントが定義されています。

検索機能を追加する

次に、検索機能を追加してみましょう。
検索機能は、mapbox-gl-geocoder プラグインから提供されています。

  1. npmで mapbox-gl-geocoder をインストール

    npm install @mapbox/mapbox-gl-geocoder --save
    
  2. /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" />
    

    GitHub mapbox-gl-geocoder

  3. 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>
    

すると、このように検索ができるようになります。
mapbox検索.gif

仕組み

検索時には、Geocoding API が呼ばれています。

GET https://api.mapbox.com/geocoding/v5/{endpoint}/{search_text}.json

Mapbox GL JSから、このような Control などのUIも提供されていますし、APIとしても提供されていますので、UIを自作することもできます。
その他のAPIの詳細は、mapbox API Documentation へ。

地名を日本語化する

英語のままでは見づらいので、日本語化してみましょう。
日本語化は、mapbox-gl-language プラグインから提供されています。

  1. npmでmapbox-gl-languageをインストール

    npm install @mapbox/mapbox-gl-language --save
    

    GitHub mapbox-gl-language

  2. 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>
    

すると、このように国名や地名が日本語になります。
image.png

マップのスタイルを作成する

Mapbox Studioでは、Web上で好きなスタイルを作成することができます。

Mapbox Studio での編集

  1. New styleをクリック
    image.png

  2. 好きなテンプレートを選択し、Customize xxxをクリック
    image.png

  3. エディターが表示される
    image.png

ここで様々な編集ができます。
詳しい使い方は、Mapbox Studio Manual へ。

今回は、日本語のSatellite Streetsを作成しました。

作成したスタイルの適用

  1. シェアアイコンをクリック
    Mapbox Studio share.png

  2. Style URLをコピー
    Mapbox Studio style.png

  3. optionstyleを変更

    /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>
    

すると、作成したスタイルが適用されます。
image.png

位置情報を表示する

最後に、位置情報を表示してみましょう。
今回は私の好きな美術館を表示してみたいと思います。

  1. レイヤーを追加

    /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>
    

すると、このようにアイコンと地名が表示されます。
image.png

仕組み

様々なレイヤーを組み合わせることでマップを表示しています。
Layerには主に typesorcelayoutpaint が設定できます。
詳しい仕様は、mapbox Style Specification Layers へ。

今回は 座標アイコン表示 するので、
typesymbolsourcegeojson にしています。

GeoJSON とは

地理空間を表現するためのJSONフォーマットです。
詳しい仕様は、GeoJSON へ。

おわりに

ここまでMapbox GL JSの主要な機能を見てきました。
mapboxのSolutionsには、様々なユースケースが紹介されていますので、こちらもぜひ!

業務ではラッパーライブラリを使用していますが、もう少し自由度がほしいと思うこともしばしば・・・。
次は、ここからVue.jsのアプリとしてもっと使いやすくしていきたいと思います:bow_tone1:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む