Vue.js入門 Vol.5 ～業務効率改善ツール作成編～

こんにちは！
LIFULLエンジニアの吉永です。
本日も最近あまり関わらなくなったのであまりキャッチアップできていなかったフロントエンド開発技術についてインプットした内容を備忘録として記載していきます。

## 本記事の概要

[Vue.js入門 Vol.1 ～jQueryとの対比編～](https://qiita.com/YoshinagaYuta/items/278a0d6223952ecc7e29)
[Vue.js入門 Vol.2 ～jQueryとの対比編～](https://qiita.com/YoshinagaYuta/items/471fc5cb0cadc8910334)
[Vue.js入門 Vol.3 ～基礎まとめで簡易家計簿を作る編～](https://qiita.com/YoshinagaYuta/items/675e2538fa163f12cd49)
[Vue.js入門 Vol.4 ～外部API呼び出し編～](https://qiita.com/YoshinagaYuta/items/08d78c393319753fe804)
上記4記事の続きで、業務効率を改善する為のちょっとした便利ツールを開発してみたので、共有したいと思います。

このツールを作った背景として、e2eテストを手動操作にて行っていた時に、実際に遷移したURLとテスト仕様書に記載されたURLのクエリパラメータが一致しているかをチェックする際に、この時URLのパス部分は動的に変わる要件だったり、クエリパラメータの並び順がテスト仕様書と実装で相違が生まれた場合に、ひとまず期待通りのクエリパラメータを含んでいて、バリューも想定通りなのかを目視チェックすることなく、機械的に行いたいというニーズから作成しました。

本来はテスト仕様書と実装でURLは完全一致であれば、単純なテキスト比較でいいですし、それが望ましいと思いますが、今回私が業務にて実際に遭遇したのは、とあるスマホアプリからリンクを踏んで、スマホのデフォルトブラウザでページを開く際に、クエリパラメータの並び順がデフォルトブラウザのアドレスバーからコピーすると変わっていて、かつその変化もアルファベット順などの規則性を見いだせなかったので、クエリパラメータを一つ一つ分解して一致してるかを確認する必要があり、これは大変面倒だということで作成しました！

## 作成するアプリケーションの仕様

- 二つのURLを入力値として受け取る。
- 二つのURLクエリのキーが一致していてかつ、バリューが一致しているかを検査して結果を表示する。
- クエリ内のキーの並び順は比較対象外とする。
- 入力されたURLクエリからキー・バリューの一覧を表示する。
- キー・バリューの行は一致しているか不一致なのかをアイコンで表現する。

## 画面仕様

### 初期表示

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/690018/97579e5a-6de5-9677-7928-0b4b8ba6a324.png)

### クエリパラメーターが一致した時

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/690018/c81ec504-d477-3046-a9e5-60918eebfc9b.png)

### クエリパラメーターが不一致の時

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/690018/81a27dcc-3ac4-bec0-2aff-0b053da5d6e7.png)

## クエリパラメーター比較ツール～完成版デモ～

<p class="codepen" data-height="600" data-default-tab="result" data-slug-hash="LYWwQXb" data-user="yuta-yoshinaga" style="height: 600px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/yuta-yoshinaga/pen/LYWwQXb">
  クエリ文字列比較ツール～完成版デモ～</a> by Yuta Yoshinaga (<a href="https://codepen.io/yuta-yoshinaga">@yuta-yoshinaga</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

この後の解説部分では各ソースコードをパーツごとに表記しているので、ソースコードの全文を見たい方は上記codepenへのリンクから参照してください。

## クエリパラメーター比較ツール実装

### 入力フォーム

```vuejs
<h1 class="title">クエリパラメーター比較ツール</h1>
<div class="columns">
    <div class="column">
        <label class="label">比較元URL</label>
        <input class="input" type="text" v-model="srcUrl">
    </div>
    <div class="column">
        <label class="label">比較先URL</label>
        <input class="input" type="text" v-model="dstUrl">
    </div>
</div>
```

まずは比較元と比較先のURLを入力できるinputタグを二つ置いて、それぞれv-modelにて双方向データバインディングできるように設定します。

### 比較結果表示部分

```vuejs
<table class="table" v-if="querys.length">
    <thead>
        <tr>
            <th>クエリ名</th>
            <th>比較元の値</th>
            <th>比較先の値</th>
        </tr>
    </thead>
    <tbody>
        <tr v-for="(query, index) in querys">
            <td>
                <span v-if="isMatch(index)" class="icon has-text-success">
                    <i class="fas fa-check-square"></i>
                </span>
                <span v-else class="icon has-text-danger">
                    <i class="fas fa-ban"></i>
                </span>
                {{ query.name }}
            </td>
            <td>{{ query.srcValue }}</td>
            <td>{{ query.dstValue }}</td>
        </tr>
    </tbody>
    <tfoot>
        <th></th>
        <th>比較結果</th>
        <th>{{ getQueryResultMessage() }}</th>
    </tfoot>
</table>
```

まず`querys`というオブジェクトからひとつづつデータを取り出して、1行づつ表を作っていきます。
その際に、`isMatch`というメソッド使って、その行のクエリパラメータのバリューが一致しているかどうかによってアイコンを切り替えるようにしています。`v-else`はv-ifにて条件式がfalseになった際に有効になるタグです。
最後に`getQueryResultMessage`というメソッドを使って、比較した結果のメッセージを表示するようにしています。

### クエリパラメーター比較ツールJS

```js
const app = new Vue({
    el: '#app',
    data: {
        srcUrl: '',
        dstUrl: ''
    },
    computed: {
        querys: function () {
            let querys = [];
            let srcQuerys = null;
            try {
                srcQuerys = new URL(this.srcUrl);
                srcQuerys.searchParams.forEach(function (value, key) {
                    let idx = querys.findIndex(query => query.name == key);
                    if (idx !== -1) {
                        querys[idx].srcValue = value;
                    } else {
                        querys.push({ name: key, srcValue: value, dstValue: '' })
                    }
                });
            } catch (e) {
                console.log(e);
            }

            let dstQuerys = null;
            try {
                dstQuerys = new URL(this.dstUrl);
                dstQuerys.searchParams.forEach(function (value, key) {
                    let idx = querys.findIndex(query => query.name == key);
                    if (idx !== -1) {
                        querys[idx].dstValue = value;
                    } else {
                        querys.push({ name: key, srcValue: '', dstValue: value })
                    }
                });
            } catch (e) {
                console.log(e);
            }

            return querys;
        },
        missMatchCnt: function () {
            let missMatchCnt = 0;
            for (let query of this.querys) {
                if (query.srcValue !== query.dstValue) {
                    missMatchCnt++;
                }
            }
            return missMatchCnt;
        },
    },
    methods: {
        /**
         * 比較結果文字列を取得。
         * @returns {string} 比較結果文字列
         */
        getQueryResultMessage: function () {
            let message = '全ての項目が一致しました！';
            if (this.missMatchCnt) {
                message = String(this.missMatchCnt) + '件の不一致が見つかりました。';
            }
            return message;
        },
        /**
         * 指定行のソースとデスティネーションが一致しているか
         * @returns {bool} 指定行の比較結果
         */
        isMatch: function (idx) {
            return this.querys[idx].srcValue === this.querys[idx].dstValue;
        }
    }
})
```

まずはJSの全体像です。
各詳細を以降で解説していきます。

### computed

#### querys

```js
querys: function () {
    let querys = [];
    let srcQuerys = null;
    try {
        srcQuerys = new URL(this.srcUrl);
        srcQuerys.searchParams.forEach(function (value, key) {
            let idx = querys.findIndex(query => query.name == key);
            if (idx !== -1) {
                querys[idx].srcValue = value;
            } else {
                querys.push({ name: key, srcValue: value, dstValue: '' })
            }
        });
    } catch (e) {
        console.log(e);
    }

    let dstQuerys = null;
    try {
        dstQuerys = new URL(this.dstUrl);
        dstQuerys.searchParams.forEach(function (value, key) {
            let idx = querys.findIndex(query => query.name == key);
            if (idx !== -1) {
                querys[idx].dstValue = value;
            } else {
                querys.push({ name: key, srcValue: '', dstValue: value })
            }
        });
    } catch (e) {
        console.log(e);
    }

    return querys;
},
```

今回の機能の心臓部分に当たる重要なプロパティとなります。
このプロパティは`srcUrl`と`dstUrl`に依存しており、この二つは`v-model`にてinputタグのvalueと連動しています。
よって、ユーザーが比較元・先のURLに文字を入力するとこのプロパティも自動で更新されます。
プロパティ内では入力されたURLからクエリパラメータ部分のみを抽出し、オブジェクト変数に配列形式で格納していって、最終的に比較結果表示部分で参照していた`querys`というオブジェクト配列が出来上がります。

#### missMatchCnt

```js
missMatchCnt: function () {
    let missMatchCnt = 0;
    for (let query of this.querys) {
        if (query.srcValue !== query.dstValue) {
            missMatchCnt++;
        }
    }
    return missMatchCnt;
},
```

こちらのプロパティは比較元と先のクエリパラメータで不一致を起こしている行の数を求めています。
このプロパティは`querys`に依存しているので、先述した`querys`プロパティの内容が更新されると次にこちらのプロパティも更新されます。

### methods

```js
/**
 * 比較結果文字列を取得。
 * @returns {string} 比較結果文字列
 */
getQueryResultMessage: function () {
    let message = '全ての項目が一致しました！';
    if (this.missMatchCnt) {
        message = String(this.missMatchCnt) + '件の不一致が見つかりました。';
    }
    return message;
},
/**
 * 指定行のソースとデスティネーションが一致しているか
 * @returns {bool} 指定行の比較結果
 */
isMatch: function (idx) {
    return this.querys[idx].srcValue === this.querys[idx].dstValue;
}
```

最後に本ツールにて使用しているメソッドです。
`getQueryResultMessage`では`missMatchCnt`プロパティの数に応じて表示するメッセージを切り替えるようになっています。
`isMatch`では指定した行の`srcValue`と`dstValue`が一致しているかどうかをtrue/falseの真偽値で返却しています。

### 最後に

本日は以上となります。

Vue.jsを用いると、というかフロントエンド技術全般に言えるかもしれませんが、ちょっとしたユーティリティツールが欲しい時にサクッと作るのに非常に適しているなぁと、つくづく思いました。

今日必要になったツールが、今日用意できるのっていいですよね！

実際に他者に配布して使ってもらう時とかはもう少しエラー処理きちんとしなきゃとか、配布しやすいように[Electron](https://www.electronjs.org/)でデスクトップアプリにできるようにビルドしてインストーラー作って渡そうとか、今回の場合だとローカルファイルを開いたりする必要もないから[GitHub Pages](https://docs.github.com/ja/pages/getting-started-with-github-pages/about-github-pages)でさくっと静的ページを公開して使えるようにしちゃおうとか、諸々の配慮が必要になりますが、ひとまず自分が使うだけならローカルファイルで持っといてとかでも十分だと思うので、業務効率化ユーティリティツールを作りつつ、Vue.jsの理解も深めつつの一石二鳥で学習を進めていけると、効率良いなと思いました！

それではまた次の記事でお会いしましょう！