20210322のvue.jsに関する記事は7件です。

シンプルで使いやすいURLエンコード・デコードツールを作ってみた

はじめに

こんにちは、システム開発チーム「presto」です。
開発業務(プログラミング中)に「こんなツール、あんなツールが身近なところにあったらいいなぁ」と思ったことはありませんか?

ここではそんなツールをVuetifyを使って作ってみたので紹介させてください。

URLエンコード・デコードツール

今回紹介させていただくのは、URLエンコード・デコードツールです。

画面は以下の通りです。

URLエンコード・デコードツール

使い方

使い方は以下の通りです。
1. 変換前のURL文字列を入力する
2. 変換方法を選択する
3. 結果確認

1. 変換前のURL文字列を入力する

変換前のURL文字列を入力する

例として、以下を入力します。

https://あいうえお.com/

変換前のURL文字列を入力する

2. 変換方法を選択する

変換方法を選択する

変換方法は以下の4通りがあります。
エンコードもしくはデコードのそれぞれを選んでください。

変換方法 概要
EncodeURI URI (Uniform Resource Identifier; 統一資源識別子) をエンコードし、各文字のインスタンスをそれぞれ UTF-8 符号の文字を表す 1 個から 4 個のエスケープシーケンスに置き換えます (サロゲート文字のペアのみ 4 個のエスケープシーケンスになります)。
参考 encodeURI()|MDN web Docs

以下、エンコードされない文字列
A-Z a-z 0-9 ; , / ? : @ & = + $ - _ . ! ~ * ' ( ) #
EncodeURIComponent URI (Uniform Resource Identifier) 構成要素を特定の文字を UTF-8 文字エンコーディングで表された 1 個から 4 個のエスケープシーケンスに置き換えることでエンコードします (サロゲートペアで構成される文字のみ 4 個のエスケープシーケンスになります)。
参考 encodeURIComponent()|MDN web Docs

以下、エンコードされない文字列
A-Z a-z 0-9 - _ . ! ~ * ' ( )
DecodeURI encodeURI() 関数あるいは同様のルーチンによって事前に作成された URI (Uniform Resource Identifier; 統一資源識別子) をデコードします。
参考 decodeURI()|MDN web Docs
DecodeURIComponent encodeURIComponent() 関数あるいは同様のルーチンによって事前に作成された URI (Uniform Resource Identifier; 統一資源識別子) の構成要素をデコードします。
参考 encodeURIComponent()|MDN web Docs

3. 結果確認

上記例でEncodeURLした場合、以下のような値が表示されます。

https://%E3%81%82%E3%81%84%E3%81%86%E3%81%88%E3%81%8A.com/

エンコード結果

エンコード済みの文字列をデコードする場合、以下のような値が表示されます。

https://あいうえお.com/

デコード結果

まとめ

今回は、URLエンコード・デコードツールの紹介をさせていただきました。

JSONフォーマットツールの紹介もしていますので、良ければ合わせてご覧ください。

今後もよろしくお願いします。
ありがとうございましたー!

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

フォームを使ってv-modelを理解する

v-model

App.vue
    <input 
      type="text" 
      id="text"
      v-model="eventData.title"
    >
    <p>{{eventData.title}}</p>
<script>
export default {
  data() {
    return {
      eventData: {
        title: "タイトル"
      }
    }
  }
}

v-modelはdataの中の値を参照することができます。

修飾子について

lazy修飾子

v-model.lazy

lazy修飾子はinput要素に大してフォーカスが外れた瞬間に発火するようになります。
フロントエンドのバリデーションをかける際などに使われるイメージです!

number修飾子

App.vue
   <input 
      type="number" 
      id="text"
      v-model.number="eventData.maxNumber"
    >
    <p>{{eventData.maxNumber}}</p>
  </div>
<script>
export default {
  data() {
    return {
      eventData: {
        title: "タイトル",
        maxNumber: 0
      }
    }
  }
}
</script>

v-model.numberとするとフォームの値が更新された際も値がnumberになります。
input要素は最初がnumberでも値が更新されるとstringになるといった性質があります。v-model.numberとすることでこの値はnumberだよと言うことを明示づけてくれるわけです。

trim修飾子
こちらは前後の空白を取り除く修飾子です

v-model.trim

とするとフォームの前後の空白がなくなります。

テキストエリアの場合

textareaを使う場合も基本的には変わらないです

App.vue
<template>
<textarea id="detail" v-model="eventData.detail"></textarea>
<pre>{{eventData.detail}}</pre>
</template>
<script>
export default {
  data() {
    return {
      eventData: {
        title: "タイトル",
        maxNumber: 0,
        subTitle: "",
        detail: "",
      }
    }
  }
}
</script>

こんな感じです。
ちなみにpreタグを使うとテキストエリアの改行などを判断することができます!

checkboxの場合

App.vue
<template>
<div>
  <input type="checkbox" id="isPrivate" v-model="eventData.isPrivate">
  <label for="isPrivate"></label>
  <p>{{eventData.isPrivate}}</p>
</div>
</template>

<script>
export default {
  data() {
    return {
      eventData: {
        title: "タイトル",
        maxNumber: 0,
        subTitle: "",
        detail: "",
        isPrivate: false
      }
    }
  }
}
</script>

チェックボックスはbooleanを返します!
あとはidとforで紐づけるだけですね!

複数のチェックボックスの場合

App.vue
<div>
<p>参加条件</p>
  <label for="10">10代</label>
  <input type="checkbox" id="10" value="10代" v-model="eventData.target">
  <label for="20">20代</label>
  <input type="checkbox" id="20" value="20代" v-model="eventData.target">
  <label for="30">30代</label>
  <input type="checkbox" id="30" value="30代" v-model="eventData.target">
  <p>{{eventData.target}}</p>
</div>

<script>
export default {
  data() {
    return {
      eventData: {
        title: "タイトル",
        maxNumber: 0,
        subTitle: "",
        detail: "",
        isPrivate: false,
        target: []
      }
    }
  }
}
</script>

複数のチェックボックスの場合はbooleanではなく配列を返します。
チェックを入れたものだけが配列に代入されていくイメージです。

ラジオボタン

App.vue
<div>
<p>参加費</p>
  <input type="radio" id="free" value="無料" v-model="eventData.price">
  <label for="free">無料</label>
  <input type="radio" id="paid" value="有料" v-model="eventData.price">
  <label for="paid">有料</label>
  <p>{{eventData.price}}</p>
</div>
<script
export default {
  data() {
    return {
      eventData: {
        title: "タイトル",
        maxNumber: 0,
        subTitle: "",
        detail: "",
        isPrivate: false,
        target: [],
        price: ""
      }
    }
  }
}
</script>

ラジオボタンも同様に実装できます。

お疲れ様でした!

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

Vue.jsのスロットを理解する!

プロジェクトの作成

今回はvue cliををつかっていきます。

vue create プロジェクト名

App.vue を書き換えていきます。

App.vue
<template>
<div>
  <h1>スロットを学んでいきます</h1>
  <hr>
  <Test name="阿部" message="よろしくお願いいたします。"></Test>
</div>
</template>

<script>
import Test from "../src/components/Test"
export default {
  components: {
    Test,
  }
}
</script>

<style>
hr {
  border: 1px solid red;
}
</style>

コンポーネントを一つ作成します。

src/components/Test.vue
<template>
  <div>
    <h1>名前は{{name}}です</h1>
    <p>{{message}}</p>
  </div>
</template>

<script>
export default {
  props: {
    name: {
      type: String,
      required: true,
    },
    message: {
      type: String,
      required: true,
    },
  },
}
</script>

スクリーンショット 2021-03-22 18.08.39.png

現在は純粋にpropsを受け取っている状態になります。

スロットとは?

親から子にデータを渡す際にHTMLのタグのデータまで同時に渡すことができます。
実際にやってみた方が早そうなのでやってみます

App.vue
 <Test name="阿部" message="よろしくお願いいたします。">
    <hr>
    <h1>こちらスロットです</h1>
 </Test>

App.vueの部分を書き換えてみましょう

Test.vue
<template>
  <div>
    <h1>名前は{{name}}です</h1>
    <p>{{message}}</p>
    <slot></slot>
  </div>
</template>

Testコンポーネント部分にslotタグを設置します。
スクリーンショット 2021-03-22 18.12.15.png

こうなります。
つまり親の コンポーネントの間に記述したHTMLの内容がslotで受け取れているのがわかると思います。

App.vue
<template>
<div>
  <h1>スロットを学んでいきます</h1>
  <hr>
  <Test name="阿部" message="よろしくお願いいたします。">
    <hr>
    <h1>{{slotMessage}}</h1>
  </Test>
</div>
</template>

<script>
import Test from "../src/components/Test"
export default {
  components: {
    Test,
  },
  data() {
    return {
      slotMessage: "スロットのメッセージです"
    }
  }
}
</script>

このようにデータプロパティを追加して子にデータを渡すことも可能になります。

デフォルトのスロット

<slot>デフォルトのスロット</slot>

スロットはスロットタグの中にデフォルトのスロットを設定することができます。
これはコンポーネント内のデータがない場合のみ、デフォルトのスロットが適用され、ある場合はデフォルトのスロットは打ち消されます。

名前つきスロット

<slot></slot>
<slot></slot>
<slot></slot>

複数スロットっておけるの?
結論から言うとできます。

スクリーンショット 2021-03-22 18.22.11.png
純粋に3つおくとスロットが3つ描画されます。

ではこのスロットをそれぞれ違う文字を描画していきたい場合、使うのが名前付きスロットです!

App.vueを書き換えましょう

App.vue
<template>
<div>
  <h1>スロットを学んでいきます</h1>
  <hr>
  <Test name="阿部" message="よろしくお願いいたします。">
    <hr>
    <template v-slot:first>        
      <p>一つ目のスロット</p>
    </template>
    <template v-slot:second>        
      <p>二つ目のスロット</p>
    </template>
    <template v-slot:third>        
      <p>三つ目のスロット</p>
    </template>
  </Test>
</div>
</template>

<script>
import Test from "../src/components/Test"
export default {
  components: {
    Test,
  },
}
</script>

<style>
hr {
  border: 1px solid red;
}
</style>
Test.vue
<template>
  <div>
    <h1>名前は{{name}}です</h1>
    <p>{{message}}</p>
    <slot name="first"></slot>
    <slot name="second"></slot>
    <slot name="third"></slot>
  </div>
</template>

<script>
export default {
  props: {
    name: {
      type: String,
      required: true,
    },
    message: {
      type: String,
      required: true,
    },
  },
}
</script>

説明するとまずまずtemplateタグで囲みv-slotディレクティブを設定します(こちらは好きな値を追加しましょう)
受け取る側はname属性で指定した値を受け取ります。
これで言うと

//これで送信
<template v-slot:first>
最初のスロット
</template>

//これで受け取り
<template name="first">

ってイメージです!
ここは必ずtemplateタグを使う必要がありv-slotとtemplateはセットだと思ってください。
ちなみにこの名前付きスロットはバージョン2.6.0以上で対応しています!

省略記法

v-slotは#に省略することができます。

v-slot:title  ===  #title

って感じです。

お疲れさまでした!!

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

Vue.js~オブジェクト(ドラえもん)~

https://qiita.com/akari_0618/items/36ba2fc4dee783def482
前回の記事でのオブジェクトについて触れていきたいと思います。

簡単に言うとオブジェクトとは物体なのですが、わけわからないと思うのでここではドラえもんという作品を作るということにしときます。
ということで最初にいきなりオブジェクトに突っ込むと詰むので、まずは周りから攻めていきましょう。
ドラえもんの登場人物で考えて行きましょう。

クラス

これは設計図のです。難しいことは考えずにもう設計図だと思ってください!
しずかちゃんとかのび太くんのプロフィール用紙のようなものです。

プロバティ

これはCSSを触っていると馴染みがあると思います。要は、1つ1の説明のようなものです。
ドラえもんだとわかりにくいので、のび太くんたちで例えます。
のび太くんたちの出身地、通ってる学校、学年などをかきます。ある程度共通項目があるので、それをかきます。

メゾット

行動のことです。
何をするのか、特技はなにか、好物はなにかなどですね

これが、のび太くんたちの共通項目です。
ここから一人ひとりの設計図を作っていきます。

そして共通のクラスをあたしく作ったクラスに受け継ぐことを『継承』と言います!!

新たにクラスを作ったあとに、プロバティを詳細に記載していきます。
のび太くん▶ <プロバティ>
      出身地:東京 学校:小学校 学年:5年生
      <メソッド>
      何をするのか:昼寝 特技:あやとり 好物:カレーライス
      のび太くんはジャイアンにいじめられて泣きますが、帰ってきたドラえもんみたいにたまに自分の力でなんとかすることもあるの  
      ので、ここはあまり変えたくないなってときには『カプセル化』します。
これを、登場人物順に作っていく感じです。

共通項目を作ることにより、たまには出来杉君入れようよってなっても継承するだけって感じで改修しやすくなりますよね!?
また話が進んでいくうちにのび太くんたちが社会人になる場合もあるのでその場合に設定を変える場合にオブジェクトで書くと楽だよってことですね。

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

通常のHTMLファイルに単一コンポーネントVue.jsを組み込む方法

前置き

しばらく前に下記のようなことができるVueCLIの導入をして勉強していたのですが、
この開発ツールで作成した単一ファイルコンポーネント(.vueファイル)をそのまま、
普通のHTMLファイルに組み込む方法が意外とぐぐっても情報が少なかったので、
改めてまとめてみたいと思います。

VueCLIでできること

  • プロジェクトのテンプレートの作成
  • 複数のjsファイルを一つにまとめる
  • .vueファイルを.jsに変換する
  • トランスパイル
  • JavaScriptの構文チェック
  • テストツールの導入など

単一ファイルコンポーネントとは

その前に、Vue.jsの単一ファイルコンポーネントファイルについて改めて復習します。
単一ファイルコンポーネントファイルとは、以下のように、template, script, styleの
3つがセットになった拡張子vueのファイルです。

<template>
  <p>{{ greeting }} World!</p>
</template>

<script>
module.exports = {
  data: function() {
    return {
      greeting: "Hello"
    };
  }
};
</script>

<style scoped>
p {
  font-size: 2em;
  text-align: center;
}
</style>

templateが表示部分、scriptがロジックや内部データ、styleはそのままCSSですね。
この1セットで一つの機能、コンポーネントになっています。

拡張子vueファイルについて

上述の単一ファイルコンポーネントは拡張子vueというファイルになっています。
VueCLI環境では、この拡張子vueファイルのまま開発が行えます。
これは前述のVueCLIでできることの中に、「.vueファイルを.jsに変換する機能」が組み込まれているからです。

拡張子vueファイルを扱えるのは、あくまでVueCLIという環境の中での話なので、通常のHTMLファイルに
そのまま組み込むことはできません。

Webpackについて

拡張子vueファイルを通常のHTMLファイルに組み込むためには、VueCLIにも含まれていた
「.vueファイルを.jsに変換する機能」が必要です。それを行ってくれるのがWebpackです。
Webpackはモジュールバンドラと呼ばれるツールで、複数のファイルを1つにまとめる(バンドル)
機能を持ちます。HTMLファイルに組み込む前に、このWebpackを用いて、Vue.jsと自作した
拡張子vueファイルをまとめて1つのjsファイルにすることで、通常のHTMLファイルに
単一コンポーネントファイル(拡張子vueファイル)を組み込める状態になります。

組み込み方法

それでは、以降で具体的な手順を記載します。
作業環境のOSはdebian系を利用します。
(最後にdockerファイル共有します)

VueおよびWebpackのインストール

適当な作業ディレクトリを作りそこで作業します。

$ mkdir ~/scf_test
$ cd ~/scf_test

各種必要なものをインストールしていきます。

# yarnのインストール
$ apt-get install -y yarn
# vue関連のインストール
$ yarn add --dev vue vue-loader vue-template-compiler
# webpackのインストール
$ yarn add webpack webpack-cli 

Webpack用の設定ファイル作成

Webpackを実行するために必要な設定ファイルを作成します。
この設定ファイルを用意することで、拡張子vueファイルに対してwebpackによるVue.jsと
自分が作った単一コンポーネントファイルを結合し、1つのjsファイル(下記の設定ではbundle.jsというファイル)
にバンドルすることができるようになります。

./webpack.config.js
// output.pathに絶対パスを指定する必要があるため、pathモジュールを読み込んでおく
const path = require('path');
const { VueLoaderPlugin } = require("vue-loader");

module.exports = {
    // モードの設定、v4系以降はmodeを指定しないと、webpack実行時に警告が出る
    mode: 'development',
    // エントリーポイントの設定
    entry: './src/js/app.js',
    // 出力の設定
    output: {
        // 出力するファイル名
        filename: 'bundle.js',
        // 出力先のパス(絶対パスを指定する必要がある)
        path: path.join(__dirname, 'public/js')
    },
    module:{
        rules: [
            // .vue ファイルを組み込むためのモジュール
            {
                test: /\.vue$/,
                loader: 'vue-loader'
            },
        ]
    },
    resolve: {
        extensions: ['.js', '.vue'],
        modules: [
            "node_modules"
        ],
        alias: {
            // vue.js のビルドを指定する
            vue: 'vue/dist/vue.common.js'
        }
    },
    plugins: [new VueLoaderPlugin()]
};

サンプルの単一コンポーネントファイル作成

バンドルしたいサンプルの単一コンポーネントファイルを作っておきます。
上記のwebpackの設定内で、「エントリーポイントの設定」があると思います。
そこが、自分が作成する単一コンポーネントを呼び出す起点のjsファイルになります。
まずはそれの作成です。

./src/js/app.js
import Vue from 'vue';
import HelloWorld from './components/hello-world.vue';

new Vue({
    el: '#app',
    components: {
        'hello-wold': HelloWorld,
    },
});

次に単一コンポーネントファイルを作成します。

./src/js/components/hello-world.vue
<template>
  <div>{{msg}}</div>
</template>

<script>
export default {
  name: 'HelloWorld',
  props: {
    msg: String
  }
}
</script>

<style scoped>
</style>

Webpackによるバンドル処理

続いて、上記で作成したファイルを元にwebpackでバンドル処理を行います。
webpackコマンドは./node_modules/.bin/webpackにあります。

$ ./node_modules/.bin/webpack

成功すると、public/js/bundle.jsというファイルが作成されているはずです。
これがバンドルされたファイルです。

バンドルされたファイルを通常のHTMLファイルに組み込む

これは単純にHTMLファイルからさきほど作成したbundle.jsを読み込むだけです。
以下のようなHTMLファイルになります。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>scf tutorial</title>
</head>
<body>
<h1>scf test</h1>
<div id="app">
    <hello-wold msg="sayHello"></hello-wold>
</div>

<script src="public/js/bundle.js"></script>
</body>
</html>

上記のHTMLファイルをブラウザで開き、sayHelloと表示されていれば成功です。
なお、単一コンポーネントファイルを修正した場合は、webpackコマンドによる
バンドル処理を行い、bundle.jsを作り直す必要があります。

dockerファイル共有

上記の環境を整え済みのdockerファイルを共有しておきますので、お試し下さい。

$ git clone https://github.com/jcong7495/vuejs_scf.git
$ cd vuejs_scf
$ docker-compose up -d
$ docker-compose exec nginx bash
$ cd /usr/share/nginx/html/scf_test/
$ yarn install
$ ./node_modules/.bin/webpack

上記実行後、http://localhost/scf_test/にアクセスしてsayHelloと表示されれば成功です。

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

VueアプリをGithub Actionsを使ってレンタルサーバーにデプロイ

①Settings > Secretに必要な情報を登録

シークレット名 詳細
SSH_KEY 秘密鍵
KNOWN_HOSTS ホスト名(username@example.comみたいなやつ)

秘密鍵の注意点

丸ごと記載する

-----BEGIN RSA PRIVATE KEY-----と-----END RSA PRIVATE KEY-----もきちんと記載する。

パスワードは設定しない

秘密鍵を生成する際に、パスワードを設定することができるが、空にしよう。
ただ、たまに空で設定出来ないものがある(cpanel等。自分がまさにそうだった。)

その際はターミナルにログインして、手動で設定する必要がある。

$ ssh -i 秘密鍵 ホスト名 # または「ssh ホスト名」でパスワードを入力
$ cd .ssh # サーバーによってパスは違う
$ ssh-keygen # 秘密鍵を生成。質問みたいなのは全部Enter
$ ls # id_rsa id_rsa.pub みたいなのが作成されるはず
$ cat id_rsa.pub # ファイルの中身が表示されるのでコピー
$ vi authorized_keys # エディターが表示されるのでiでinsertモードに切替 & 貼付 & :wqで保存して終了
$ chmod 600 authorized_keys # 権限を与える
$ cat id_rsa # 内容をコピー SSH_KEYに入力するのはこれ

②.github/workflows/deploy.ymlを作成

Github Actionsの使用には、.github/workflowsの中に***.ymlを作成する必要がある。

.github/workflows/deploy.yml
name: deploy

on:
  push:
    branches:
      - main # mainブランチに変更があったら処理開始

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1

      - name: Get yarn cache directory path # キャッシュ系(高速化)
        id: yarn-cache-dir-path
        run: echo "::set-output name=dir::$(yarn cache dir)"

      - uses: actions/cache@v1 # キャッシュ系(高速化)
        id: yarn-cache
        with:
          path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
          key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
          restore-keys: |
            ${{ runner.os }}-yarn-

      - name: Install # パッケージをインストール
        run: yarn install --prefer-offline

      - name: Build # ビルド
        run: yarn build

      - name: SSH # sshキーを作成
        run: echo "$SSH_KEY" > key && chmod 600 key
        env:
          SSH_KEY: ${{ secrets.SSH_KEY }}

      - name: Deploy # アップロード
        run: rsync -av -e "ssh -i key -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" dist ${{ secrets.KNOWN_HOSTS }}:任意のフォルダ

どうなるか

任意のフォルダ/
  └ dist/
     └ index.html
     └ css/
     └ js/
     └ img/

もし以下のようにしたい場合は

任意のフォルダ/
 └ index.html
 └ css/
 └ js/
 └ img/

distをdist/*に変更する。

補足 --deleteについて

このオプションをつけると、削除も反映してくれる...が!割と危険なオプションなので、今回はつけなかった。

取り扱う場合はrsync -avのところをrsync -avnにして、どうなるかを確認することをおすすめする。
rsync --delete で泣かないために

ちなみにサーバ側に.envなどの消したくないフォルダがある時は.rsyncignoreに記載すると無視されるので、おすすめ(--exclude-from=.rsyncignoreをつける必要がある)

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

[Vue.js] Composition Api の良いなと思ったところ

Composition Api は 各モジュールを関数ベースで定義するのがスタンダードなようですが、これにより依存性を注入しながらモジュールを初期化できるようになりました。
個人的にはそこが非常に便利だなと思いました。
今回以下のような、 商品売り上げ・支出テーブル を作成する例の中でこちらのメリットについてまとめてみたいと思います。

top.png

機能としては以下を想定してみます。

  • 右上部カレンダーから日付を変更すると、その日付の 商品売り上げ・支出 を共に取得し描画する。
    calendar.png

  • 各テーブル左上部のソート基準のセレクトボックスからソート基準を変更すると、選択された基準で昇順でソートしたデータを取得し描画する。
    sort.png

実際に今回これらの機能を完全に作り込む訳ではありませんのであくまでイメージとなります。

必要コンポーネントを作る

必要となるコンポーネントを作っていきます。
これらは今回の主題とはあまり関係がありません。

カレンダーコンポーネント

Calender.vue
<template>
  <input
    v-model="dateAccesor"
    type="date"
    class="form-control"
  />
</template>

<script lang="ts">
import { DateTime } from 'luxon'
import { PropType, defineComponent, computed } from 'vue'

export default defineComponent({
  props: {
    date: {
      type: Object as PropType<DateTime>,
      required: true
    }
  },
  setup (props, ctx) {
    const dateAccesor = computed({
      get: () => props.date.toFormat('yyyy-MM-dd'),
      set: (updatedDate: string) => {
        ctx.emit('on-date-change', DateTime.fromISO(updatedDate))
      }
    })

    return {
      dateAccesor
    }
  }
})
</script>

  • 日時ですが、今回 luxon というライブラリーで扱ってみます。
  • dateAccessor という算出プロパティが、注入された日時を input type="date" 要素がバインドできるように変換し(getter)、またこちらの変更イベントを通知する(setter)役割を担っています。

ソート基準変更セレクトボックス付きテーブルコンポーネント

SortableTable.vue
<template>
  <div>
    <div class="d-flex align-items-center">
      <select
        :value="sortBy"
        @input="$emit('on-sort-by-change', $event.target.value)"
        class="form-control w-25 mb-3"
      >
        <option
          v-for="column in columns"
          :key="column"
          :value="column">
          {{ column }}
        </option>
      </select>
      <p class="ml-2">
        ソート基準
      </p>
    </div>
    <table class="table">
      <thead>
        <tr>
          <th
            v-for="column in columns"
            :key="column"
          >
            {{ column }}
          </th>
        </tr>
      </thead>
      <tbody>
        <tr
          v-for="tableItem in tableItems"
          :key="tableItem.id"
        >
          <td
            v-for="column in columns"
            :key="column"
          >
            {{ tableItem[column] }}
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script lang="ts">
import { defineComponent, PropType } from 'vue'

export default defineComponent({
  props: {
    tableItems: {
      type: Array as PropType<{ id: number; [key: string]: string | number }[]>,
      required: true
    },
    columns: {
      type: Array as PropType<string[]>,
      required: true
    },
    sortBy: {
      type: String,
      required: true
    }
  }
})
</script>
  • columns は その名の通りテーブルカラムのリストになるのですが、こちらはソート基準を選択するセレクトボックスの各アイテムとしての機能も持ちます。
  • sortBy は現在選択中のソート基準になります。

画面を作る

次にこれらのコンポーネントを利用して画面を作ってみます。
ただし最初は、支出を表示する予定はなく、商品売り上げのみ表示するという用件だったと仮定します。

商品売り上げを表示する

View1.vue
<template>
  <div class="position-absolute w-100 h-100 p-3">
    <calender
      :date="date"
      @on-date-change="onDateChange"
      class="w-25 mb-5 ml-auto"
    />
    <h5 class="mb-3">
      商品売り上げ
    </h5>
    <sortable-table
      :table-items="products"
      :columns="columns"
      :sort-by="sortBy"
      @on-sort-by-change="onSortByChange"
      class="mb-5"
    />
  </div>
</template>

<script lang="ts">
import { DateTime } from 'luxon'
import {
  defineComponent,
  reactive,
  ref,
  computed,
  toRefs,
  onMounted
} from 'vue'

import { Product } from '@/types/'

import Calender from '@/components/Calender.vue'
import SortableTable from '@/components/SortableTable.vue'

import fetchProductsMock from '@/mocks/fetchProduts'

export default defineComponent({
  components: {
    Calender,
    SortableTable
  },
  setup () {
    const date = ref(DateTime.local()) // カレンダーにバインドする日付
    const products = ref<Product[]>([]) // 商品売り上げリスト
    const productsInfo = reactive({ // 商品売り上げ表示情報
      sortBy: 'name',
      columns: ['name', 'sales', 'count']
    })

    // 商品売り上げ取得リクエストパラム(まだパラムは少ないが、今後増えることも想定して別 computed として切り出してみる)
    const fetchProductsParams = computed(() => {
      return {
        sortBy: productsInfo.sortBy,
        date: date.value
      }
    })

    // 商品売り上げ取得メソッド
    const fetchProducts = async () => {
      products.value = await fetchProductsMock(fetchProductsParams.value)
    }

    // ソート基準変更メソッド
    const onSortByChange = (sortBy: string) => {
      productsInfo.sortBy = sortBy
      fetchProducts()
    }

    // 日付変更メソッド
    const onDateChange = (updatedDate: DateTime) => {
      date.value = updatedDate
      fetchProducts()
    }

    onMounted(fetchProducts)

    return {
      date,
      products,
      ...toRefs(productsInfo),

      onSortByChange,
      onDateChange
    }
  }
})
</script>

要件の規模がそこまで大きくなかったので一個のコンポーネントに収めてみました。

支出も表示したい

次に支出も表示したいという要件が出たとします。
ただし、このまま一つのコンポーネントに収めると、コード量が肥大化するためロジックをモジュール化することになったとします。
もちろんモジュール化することでロジックの再利用が可能となるためそこのメリットも見込めます。

mixin

ここまで setup で書いてきたので、 mixin の例は少し分かりづらいですが、今回の要件のようなモジュール分割を mixin で書くとどうなるのか。
かなり簡単にですが、考えてみます。

商品売り上げ mixin

products.js
import fetchProductsMock from '@/mocks/fetchProduts'

export default {
  data () {
    return {
      products: [],
      productsInfo: {
        sortBy: 'name',
        columns: ['name', 'sales', 'count']
      }
    }
  },
  computed: {
    fetchProductsParams () {
      return {
        sortBy: this.productsInfo.sortBy,
        date: this.date
      }
    }
  },
  methods: {
    async fetchProducts () {
      this.products = await fetchProductsMock(this.fetchProductsParams)
    },
    async onSortByChangeProducts (updatedSortBy) {
      this.productsInfo.sortBy = updatedSortBy
      this.fetchProducts()
    }
  },
  mounted () {
    this.fetchProducts()
  }
}

こんなイメージにしてみました。
先に setup で書いた内容のうち products テーブルに関連する箇所をそのまま mixin に移しただけになります。

商品売り上げと支出を表示する

支出(spending) mixin も同じようなものを作って実際に画面側で利用すると以下のようになるかなと思います。
(実際にはそれぞれの mixin にはそれぞれ固有の処理や機能があると思いますが、今回はそこまで作り込みません)。

View2.vue
<template>
  <div class="position-absolute w-100 h-100 p-3">
    <calender
      :date="date"
      @on-date-change="onDateChange"
      class="w-25 mb-5 ml-auto"
    />
    <h5 class="mb-3">
      商品売り上げ
    </h5>
    <sortable-table
      :table-items="products"
      :columns="productsInfo.columns"
      :sort-by="productsInfo.sortBy"
      @on-sort-by-change="onSortByChangeProducts"
      class="mb-5"
    />
    <h5 class="mb-3">
      支出
    </h5>
    <sortable-table
      :table-items="spendings"
      :columns="spendingsInfo.columns"
      :sort-by="spendingsInfo.sortBy"
      @on-sort-by-change="onSortByChangeSpendings"
    />
  </div>
</template>

<script>
import { DateTime } from 'luxon'

import productsMixin from '@/mixins/products' // 商品売り上げ
import spendingsMixin from '@/mixins/spendings' // 支出

import Calender from '@/components/Calender.vue'
import SortableTable from '@/components/SortableTable.vue'

export default {
  components: {
    Calender,
    SortableTable
  },
  mixins: [productsMixin, spendingsMixin],
  data () {
    return {
      date: DateTime.local()
    }
  },
  methods: {
    onDateChange (updatedDate) {
      this.date = updatedDate
      this.fetchProducts()
      this.fetchSpendings()
    }
  }
}
</script>

かなりすっきりしました。
ですがいくつか弱点もあります。

今回の mixin を用いた設計の悪いところ

1. モジュールは、外部の date プロパティに依存してるが、 date の宣言を mixin 利用側に強制させる仕組みがない。

モジュール側の fetchProductsParamsdate を参照します。
上記例のように、モジュール利用側で両方の mixin 共通で扱う date を宣言していますが、それが必要だということが実際にこちらの fetchProductsParams を利用する時でないと分かりません。
また、最悪な想定としては dateundefined のまま開発中エラーがスローされず正常に動作してると勘違いし、バグを生んでしまうかもしれません。

対応策として以下のようなものが挙げられるかもしれませんがちょっと微妙です。

  • モジュール内の created のタイミングなどで date が無かった場合にエラーをスローする。
    → できればモジュール利用時にコードベースでエラーが出てほしい。また、これをすることで少なからずコードが荒れてしまう。
  • date を mixin 側で宣言しておく。
    → これはやっておいた方が良さそう。ただ、双方の mixin で共通の date を利用してるということが mixin 利用側から分かりづらい。また、分かりづらさを解消するために、 date を改めて画面側で宣言しても良さそうだが、それはオーバーライドという本来の目的として使いたい。

など...。
今回同じ date を複数のモジュールで共通で利用するということなので、やはりコードベースでの外部からの注入を強制させたいです。

2. それぞれの mixin で別の date を参照したいとなった際、大規模な修正が必要になる可能性がある。

以下のような要件が出たとします。

  • 新たに別のページを作りたい。
  • そのページでも、 商品売り上げ・支出テーブル を表示させたい。
  • ただしそのページでは、共通のカレンダーを 商品売り上げ・支出 で使うのではなく、カレンダーをもう一つ表示させ、それぞれが、 商品売り上げ用・支出用 と、異なるカレンダーから日付を選べるようにしたい

作成済みの mixin を利用した実装イメージとしては以下のような感じでしょうか。

View3.vue
<template>
  <div class="position-absolute w-100 h-100 p-3">
    <calender
      :date="date"
      @on-date-change="onDateChange"
      class="w-25 mb-5 ml-auto"
    />
    <h5 class="mb-3">
      商品売り上げ
    </h5>
    <sortable-table
      :table-items="products"
      :columns="productsInfo.columns"
      :sort-by="productsInfo.sortBy"
      @on-sort-by-change="onSortByChangeProducts"
      class="mb-5"
    />
    <!-- カレンダー追加 ↓ -->
    <calender
      :date="date2"
      @on-date-change="onDateChange2"
      class="w-25 mb-5 ml-auto"
    />
    <h5 class="mb-3">
      支出
    </h5>
    <sortable-table
      :table-items="spendings"
      :columns="spendingsInfo.columns"
      :sort-by="spendingsInfo.sortBy"
      @on-sort-by-change="onSortByChangeSpendings"
    />
  </div>
</template>

<script>
import { DateTime } from 'luxon'

import productsMixin from '@/mixins/products'
import spendingsMixin from '@/mixins/spendings'

import Calender from '@/components/Calender.vue'
import SortableTable from '@/components/SortableTable.vue'

export default {
  components: {
    Calender,
    SortableTable
  },
  mixins: [productsMixin, spendingsMixin],
  data () {
    return {
      date: DateTime.local(),
      date2: DateTime.local() // 追加したカレンダーとバインドするプロパティ
    }
  },
  computed: {
    fetchSpendingsParams () { // !!! this.date2 を参照するようにオーバーライドさせなければいけない !!!
      return {
        sortBy: this.spendingsInfo.sortBy,
        date: this.date2
      }
    }
  },
  methods: {
    onDateChange (updatedDate) {
      this.date = updatedDate
      this.fetchProducts()
    },
    onDateChange2 (updatedDate) {  // 追加したカレンダーから日付を変更するメソッド
      this.date2 = updatedDate
      this.fetchSpending()
    }
  }
}
</script>
  • カレンダーUIをビューに二つ用意する。
  • それぞれでバインドするために date の他に、 date2 も用意する
  • それぞれでカレンダーの変更をハンドリングするために onDateChange の他に onDateChange2 も用意する

ここまでは良いのですが、片方で this.date を参照してたプロパティを this.date2 を参照するようにオーバーライドしなければなりません(fetchSpendingsParams)。
これをしなければいけないプロパティ(メソッド含む)が増えてしまうと、手が回らなくなったり、バグを生む要因になってしまいます。

mixin との付き合い方

以上 mixin の悪い設計とその弱点でしたが、そもそも外部に依存するプロパティ( date ) を mixin 側で直接参照するのはやめた方が良さそうです。
従って、 computed > fetchProductsParams のようなプロパティはそもそも廃止し、その他今回の例ではありませんが this.date を参照してる他のメソッドなどは全て毎回引数で date を受け取る設計にしておくのが無難そうです。
またその場合 mixin 側で mounted フックよりデータ取得をすることができなくなりますが、それはそれでそっちの方が元々安全だったかもしれません。
いずれにしろ、 date を参照したいメソッドが多ければ多いほどそれらは毎回引数として date を受け取れなければならず mixin 利用側としても大変な作業になってしまいそうです。
そして、 date を参照する computed などをそもそも使えないというのも、やはり不便です。

Composition Api

対して Composition Api の場合は

モジュールを関数ベースで定義することができる = 依存性を注入しながらそのモジュールを初期化できる

これにより上記 mixin の問題を解消できます。

商品売り上げ mixin を Composition Api のモジュールに書き換えてみます。

useProducts.ts
import { DateTime } from 'luxon'
import {
  Ref,
  reactive,
  ref,
  computed,
  toRefs
} from 'vue'

import { Product } from '@/types/'

import fetchProductsMock from '@/mocks/fetchProducts'

export const useProducts = (date: Ref<DateTime>) => {
  const products = ref<Product[]>([])
  const productsInfo = reactive({
    sortBy: 'name',
    columns: ['name', 'sales', 'count']
  })

  const fetchProductsParams = computed(() => {
    return {
      sortBy: productsInfo.sortBy,
      date: date.value
    }
  })

  const fetchProducts = async () => {
    products.value = await fetchProductsMock(fetchProductsParams.value)
  }

  const onSortByChange = async (sortBy: string) => {
    productsInfo.sortBy = sortBy
    fetchProducts()
  }

  return {
    products,
    ...toRefs(productsInfo),

    fetchProducts,
    onSortByChange
  }
}

ポイントはやはり引数として date: Ref<DateTime> を宣言してるところです。
これにより、

  • プロパティとモジュール間の連携が明示的になる。
  • DateTime 型のリアクティブなプロパティを注入していない場合、TypeScript であればコードベースでエラーが表示されてくれる
    (加えてコンバイルエラーになってくれる)。
  • 連携させるプロパティをモジュール初期化時に自由に決められる。

など...、先ほどの mixin の問題が全て解消されました。
実際に作り直した画面は以下のようなイメージになります。

View4.vue
<template>
  <div class="position-absolute w-100 h-100 p-3">
    <calender
      :date="date"
      @on-date-change="onDateChange"
      class="w-25 mb-5 ml-auto"
    />
    <h5 class="mb-3">
      商品売り上げ
    </h5>
    <sortable-table
      :table-items="products"
      :columns="columnsProducts"
      :sort-by="sortByProducts"
      @on-sort-by-change="onSortByChangeProducts"
      class="mb-5"
    />
    <h5 class="mb-3">
      支出
    </h5>
    <sortable-table
      :table-items="spendings"
      :columns="columnsSpendings"
      :sort-by="sortBySpendings"
      @on-sort-by-change="onSortByChangeSpendings"
    />
  </div>
</template>

<script lang="ts">
import { DateTime } from 'luxon'
import { defineComponent, ref, onMounted } from 'vue'

import { useProducts } from '@/modules/useProducts'
import { useSpendings } from '@/modules/useSpendings'

import Calender from '@/components/Calender.vue'
import SortableTable from '@/components/SortableTable.vue'

export default defineComponent({
  components: {
    Calender,
    SortableTable
  },
  setup () {
    const date = ref(DateTime.local())
    const {
      products,
      sortBy: sortByProducts,
      columns: columnsProducts,
      fetchProducts,
      onSortByChange: onSortByChangeProducts
    } = useProducts(date)
    const {
      spendings,
      sortBy: sortBySpendings,
      columns: columnsSpendings,
      fetchSpendings,
      onSortByChange: onSortByChangeSpendings
    } = useSpendings(date)

    const onDateChange = (updatedDate: DateTime) => {
      date.value = updatedDate
      fetchProducts()
      fetchSpendings()
    }

    onMounted(() => {
      fetchProducts()
      fetchSpendings()
    })

    return {
      date,
      products,
      sortByProducts,
      columnsProducts,
      spendings,
      sortBySpendings,
      columnsSpendings,

      onDateChange,
      onSortByChangeProducts,
      onSortByChangeSpendings
    }
  }
})
</script>

1個1個モジュールから返されるプロパティを分割代入してるのでちょっとコードが冗長ですね...。
細かい設計方法やプロパティの命名方法、モジュールの利用方法などでまだまだ改良の余地がありそうですが、やはりこちらは

  • プロパティ ⇄ モジュール 間の連携
  • モジュール ⇄ モジュール 間のプロパティ共有

などにおいて自由な拡張性を持ちながらも堅牢に思えました。

注意点としてはおそらく、モジュール初期化時に必要とされるプロパティの数はなるべく絞るよう慎重に考えた方が良さそうです。
モジュールの一部の機能を利用したいとなった時に、その一部の機能が必要としてないプロパティをわざわざ注入しなければならないみたいな自体が起きると困るので
(Composition Api モジュールの場合、 setupコンテキスト 内で、モジュールが提供する機能のうち使いたい機能を明示的に指定することができます)。
従って、先に mixin との付き合い方 で言及したような、

従って、 computed > fetchProductsParams のようなプロパティはそもそも廃止し、その他今回の例ではありませんが this.date を参照してる他のメソッドなどは全て毎回引数で date を受け取る設計にしておくのが無難そうです。

メソッドに自由な引数を与えるような視点も常に持っておいて、良い具合にモジュールを設計する必要がありそうです。

あるいは、そもそもそういった悩みが生まれないようになるべく各モジュールは小さく定義して、ある程度の規模のモジュールはモジュール同士を組み合わせた設計にする、など...、
考えることが色々あり、Composition Api は便利なのですが、かなり腕が問われるなと思いました。

最後に

読んでくださりありがとうございました。
正直自分自身 Composition Api を利用したモジュール設計はまだまだ分からないことだらけで手探り状態です...。
引き続き今後明らかになるベストプラクティスなどをキャッチアップしていきたいです。

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