もがき系プログラマの日常

もがき系エンジニアの勉強したこと、日常のこと、気になっている技術、備忘録などを紹介するブログです。

Vue.js入門 基礎から実践アプリケーション開発までを読んだ vol.3

はじめに

こんにちは。

Kojirockの1人アドベントカレンダー Advent Calendar 2018の3日目の記事です。

以下の本を読んで勉強できたことを記載していきます。

今回は第3章です。

Vue.js入門 基礎から実践アプリケーション開発まで

Vue.js入門 基礎から実践アプリケーション開発まで

3章 コンポーネントの基礎

目次

3.1 コンポーネントとは何か

3.2 Vueコンポーネントの定義

3.3 コンポーネント間の通信

3.4 コンポーネントの設計

3.1 コンポーネントとは何か

このブロックの序盤では、コンポーネントの重要性などが説明されています。

以下の一文が重要なところかと思います。

UIをコンポーネント化する大きなメリットは、その再利用可能性にあります。再利用することを意識してコンポーネントを設計することは、アプリケーションの開発のしやすさでメンテナンス性を高めます。

再利用できるコンポーネントをうまく設計し作ることが、メリットを最大限受けるコツになってくるのかなと思いました。

簡単な使用方法

HTMLタグ名としてコンポーネント名を記載すれば、指定の場所に指定したコンポーネントの内容を流せます。 (カスタムタグ)

<!DOCTYPE html>
<html>
<head>
    <title>Page Title</title>
    <script src="https://unpkg.com/vue@2.5.17"></script>
</head>
<body>
    <div id="app">
        <test-cm></test-cm>
    </div>

    <script>
        Vue.component('test-cm', {
            template: '<p>おす!おら悟空!{{ skill }}</p>',
            data: function() {
                return {
                    skill: 'かめはめ波!'
                }
            }
        })

        new Vue({
            el: '#app'
        })
    </script>
</body>
</html>

=> おす!おら悟空!かめはめ波!

Vueコンポーネントの定義

Vueコンポーネントの種類として、グローバルコンポーネントとローカルコンポーネントの2種類があり、それぞれ定義方法として、前述したカスタムタグ方式と、Vue.extend()を使用したサブコンストラクタ定義という定義方法があると説明されています。

グローバルコンポーネント

カスタムタグ定義

前回のブロックで記載したコードが、グローバルコンポーネント(カスタムタグ定義)となります。

サブコンストラクタ定義

Vue.extend() を使用して、継承により定義できるようになります。

<!DOCTYPE html>
<html>
<head>
    <title>Page Title</title>
    <script src="https://unpkg.com/vue@2.5.17"></script>
</head>
<body>
    <div id="app">
        <test-cm></test-cm>
    </div>
    
    <script>
        Vue.component('test-cm', {
            template: '<p>おす!おら悟空!<skill-field></skill-field></p>',
        })

        const SkillField = Vue.extend({
            template: '<span>必殺技はかめはめ波だ!</span>'
        })
        Vue.component('skill-field', SkillField)

        new Vue({
            el: '#app'
        })
    </script>
</body>
</html>

=> おす!おら悟空!必殺技はかめはめ波だ!

ローカルコンポーネント

特定のコンポーネント内でしか使用できないローカルコンポーネントの定義です。

<!DOCTYPE html>
<html>
<head>
    <title>Page Title</title>
    <script src="https://unpkg.com/vue@2.5.17"></script>
</head>
<body>
    <div id="app">
        <test-cm></test-cm>
    </div>    

    <script>
        new Vue({
            el: '#app',
            components: {
                'test-cm': {
                    template: '<p>おす!おら悟空!</p>',
                }
            }
        })
    </script>
</body>
</html>

=> おす!おら悟空!

グローバルで作るコンポーネントはページの大枠など大きな枠でのコンポーネントが対象になり、それに紐づくコンポーネントはそれぞれのローカルコンポーネントみたいな印象ですが、あってますか。。。?

ページ全体でつかわれるnotificationとかはグローバルコンポーネントなのかな?

もう少し実践してから考えてみます。

また、前述した例では、template の値にhtml要素を書いていましたが、それ以外の定義方法が存在します。

本書で紹介されている方法が以下でした。

  1. text/x-template
  2. インラインテンプレート
  3. 描画関数
  4. JSX
  5. 単一ファイルコンポーネント

本ブロックでは、 text/x-templateと描画関数での定義方法について説明がされていました。

text/x-template

以下のように、表示するHTMLをtypeとidを指定したscriptタグで囲って定義する方法です。

<!DOCTYPE html>
<html>
<head>
    <title>Page Title</title>
    <script src="https://unpkg.com/vue@2.5.17"></script>
</head>
<body>
    <div id="app">
        <test-cm></test-cm>
    </div>    

    <script type="text/x-template" id="hello">
        <p>おす!おら悟空!</p>
    </script>

    <script>
        new Vue({
            el: '#app',
            components: {
                'test-cm': {
                    template: '#hello',
                }
            }
        })
    </script>
</body>
</html>

=> おす!おら悟空!

描画関数

render()という関数でtemplateの代替として使えます。メソッドなので、v-ifやv-forなどディレクティブでむやみにコードが複雑化するのを防げる役割があるようです。

<!DOCTYPE html>
<html>
<head>
    <title>Page Title</title>
    <script src="https://unpkg.com/vue@2.5.17"></script>
</head>
<body>
    <div id="app">
        <test-cm></test-cm>
    </div>    

    <script>
        new Vue({
            el: '#app',
            components: {
                'test-cm': {
                    render: createElement => {
                        return createElement('h1', 'ベジータだよ!')
                    }
                }
            }
        })
    </script>
</body>
</html>

=> ベジータだよ!

コンポーネント命名規則についても触れられていました。

ハイフン続きのケバブケースを選んでおくほうが無難ということです。

3.3 コンポーネント間の通信

親から子へデータを渡す場合は、propsでデータを渡すようです。

この辺はReactもそうでした。

<!DOCTYPE html>
<html>
<head>
    <title>Page Title</title>
    <script src="https://unpkg.com/vue@2.5.17"></script>
</head>
<body>
    <div id="app">
        <profile v-bind:name="name" v-bind:text="text"></profile>
        <profile v-bind:text="text"></profile>
    </div>    

    <script type="text/x-template" id="field">
        <div>
            <label>{{ name }}</label>
            <p>{{ text }}</p>
        </div>  
    </script>

    <script>
        Vue.component('profile', {
            props: {
                name: {
                    type: String,
                    default: '名無し',
                    required: false,
                },
                text: {
                    type: String,
                    required: true,
                }
            },
            template: '#field'
        })

        new Vue({
            el: '#app',
            data: {
                name: 'kojirock',
                text: 'こんにちは'
            }
        })
    </script>
</body>
</html>

=> kojirock
こんにちは

名無し
こんにちは

子 -> 親へのイベントの通知方法はいかが紹介されていました。

こちらは、本書で紹介されているコードがわかりやすかったので、引用の意味で、記載します。

<!DOCTYPE html>
<html>
<head>
    <title>Page Title</title>
    <script src="https://unpkg.com/vue@2.5.17"></script>
</head>
<body>
    <div id="fruits-counter">
        <div v-for="fruit in fruits">
            {{ fruit.name }}: <counter-button v-on:increment="incrementCartStatus()"></counter-button>
        </div>
        <p>合計: {{ total }}</p>
    </div>    

    <script>
        const counterButton = Vue.extend({
            template: '<span>{{counter}}個<button v-on:click="addToCart">追加</button></span>',
            data: function() {
                return {
                    counter: 0
                }
            },
            methods: {
                addToCart: function() {
                    this.counter += 1
                    this.$emit('increment')
                }
            }
        })

        new Vue({
            el: '#fruits-counter',
            components: {
                'counter-button': counterButton
            },
            data: {
                total: 0,
                fruits: [
                    {name: '梨'},
                    {name: '苺'}
                ]
            },
            methods: {
                incrementCartStatus: function() {
                    this.total += 1
                }
            }
        })
    </script>
</body>
</html>

buttonが押された際のクリックイベントで、addToCartメソッドが実行されて、this.$emit('increment') によって、カスタムイベントが発火します。

親の方では、v-onでincrementイベントをlistenしてます。

なのでボタンを押すと親の incrementCartStatus() 実行される形になります。

非常にわかりやすかったです。

実際には、$parent や、 $children を使用することで、親や子のデータに直接アクセスることが可能のようです。

ただ、本書ではこちらの方法は、使うべきではないと書かれています。親子間のやりとりにはpropsを使うことで統一すべきのようです。

3.4 コンポーネントの設計

こちらでは、実際にコンポーネントを作成する際の設計方法などが述べられていました。

前の記事でも紹介したAtmic Design等が挙げられています。

また、ヘッダーとログインフォームという2種類のコンポーネントの作成の例が挙げられています。

ヘッダーコンポーネントでは、スロットコンテンツ という仕組みで作成する例が記載されています。

本ブロックではスロットコンテンツについては、以下のように簡素にまとめられています。

コンポーネントの中に、親から差し替えやすい部分を残すための仕組み

実際になにか作ってみます。

<!DOCTYPE html>
<html>
<head>
    <title>Page Title</title>
    <script src="https://unpkg.com/vue@2.5.17"></script>
</head>
<body>
    <div id="app">
        <page-header></page-header>
    </div>

    <script>
        var headerTemplate = `<span><slot name="header"><h1>AAAAA</h1></slot></span>`

        Vue.component('page-header', {
            template: headerTemplate
        })

        new Vue({
            el: '#app',
        })
    </script>
</body>
</html>

この状態だと、表示は、「AAAAA」となります。

page-header内の部分を以下のように変更します。

<page-header>
    <h1 slot="header">BBBBB</h1>
</page-header>

そうすると、結果は、「BBBBB」となります。

親からの呼び出し時に子の要素を上書きできるという感じのようです。

スロットコンテンツについては、後の章で更に詳しく解説されるようなので、とりあえず現段階では上記のような理解でとどめておきます。

最後に、簡単なログインフォームコンポーネントを作成する流れになります。

このフォームではバリデートなどが入っていない簡単なものだったので、紹介されているコードにプラスして、バリデーションをいれてみました。

<!DOCTYPE html>
<html>
<head>
    <title>Page Title</title>
    <script src="https://unpkg.com/vue@2.5.17"></script>
</head>
<body>
    <div id="login-example">
        <user-login></user-login>
    </div>    

    <script type="text/x-template" id="login-template">
        <div>
            <div>
                <input type="text" name="userId" placeholder="ログインID" v-model="userId" />
                <span>{{ errors.userId }}</span>
            </div>
            <div>
                <input type="password" name="password" placeholder="パスワード" v-model="password" />
                <span>{{ errors.password }}</span>
            </div>
            <button @click="login()">ログイン</button>
        </div>
    </script>

    <script>
        Vue.component('user-login', {
            template: '#login-template',
            data: () => {
                return {
                    userId: '',
                    password: '',
                    errors: {
                        userId: '',
                        password: '', 
                    }
                }
            },
            methods: {
                isValid: function() {
                    this.errors.userId = ''
                    this.errors.password = ''
                    if (this.password.length === 0) {
                        this.errors.password = 'パスワードが未入力です。'
                    }

                    if (this.userId.length === 0) {
                        this.errors.userId = 'ユーザーIDが未入力です。'
                    }

                    return (this.errors.userId.length === 0 && this.errors.password.length === 0)
                },

                login: function() {
                    if (!this.isValid()) {
                        return false;
                    }

                    auth.login(this.userId, this.password)
                }
            }
        })

        var auth = {
            login: (id, pass) => {
                window.alert(`userId: ${id}\npassword: ${pass}`)
            }
        }

        new Vue({
            el: "#login-example"
        })
    </script>
</body>
</html>

読み進めると、今後もしかしたらもっとスマートなやり方が出てくるかもしれません。

終わりに

とりあえず3章まで終わりました。

やっとコード出てきて、学んでる感出てきました。

もっと複数コンポーネントを使うような場面が読み進めると出てくるのかな?

続けてがんばりますー。