静的Webアプリのホスト先に関する補足

本題に入る前に、静的なWebアプリを公開するためのAzureサービスについて一点補足しておきたい。いま作っている蔵書管理アプリのサンプルでは、フロントエンドのWebアプリのホスト先としてAzureストレージを利用している。第8回で紹介したように、Azureストレージには静的Webサイトのホスト先として利用するためのオプションがある。これを使えば、Azureストレージを単なるファイルの保管庫ではなくWebサイトの公開用サーバとして利用できるわけだ。

それに対して、最近Microsoftは新たに「Azure Static Web Apps」というAzureサービスを発表した。これは名前の通り静的なWebアプリをホストするためのサービスである。Azureストレージとはコンセプトが違うサービスなので単純に比較はできないが、より柔軟で高機能なホスティング環境が提供されるため、いま作っているようなWebアプリを公開する際はこちらを利用するほうが適していると言えるだろう。Azure Static Web Appsはまだプレビュー版だが、有用なサービスなので、また別の機会に詳しく紹介したい。

ホームページとなるHomeコンポーネント

それでは本題に入ろう。前回は、蔵書管理アプリのフロントエンド部分について、その基本的な構成を解説した上で、ルーティングの設定やAjax通信を行う部分のプログラムを作成した。サンプルのプログラム一式はこのGitHubリポジトリで公開している。

  • 蔵書管理アプリケーションの概要

    蔵書管理アプリケーションの概要

このアプリは、大きく分けると「ホーム」「書籍リスト」「詳細編集」の3つのページから構成されており、それぞれを「Home」「Booklist」「BookDetail」という名前のコンポーネントとして定義している。JavaScriptフレームワークのVue.jsを利用して作成フロントエンドの全体像を紹介したので、今回はそのプログラムの中身をもう少し詳しく見ていこう。

まずHomeコンポーネントだが、これは単にタイトルと概要を表示しているだけのものだ。

  • ホームページの表示例

    ホームページの表示例

ソースコードは次のようになっている。

Home.vue

<template>
    <div style="background-color:transparent !important;">
        <h1>蔵書管理アプリケーション</h1>
        <p>This is a demo application for Azure App Service using Java and Vue.js.</p>
    </div>
</template>

<script>
    export default {
        name: "Home"
    }
</script>

蔵書の一覧を表示するBooklistコンポーネント

Booklistコンポーネントは、バックエンドから取得した書籍情報を一覧表示するページである。

  • 書籍リストページの表示例

    書籍リストページの表示例

ソースコード全体は少し長いので、スクリプト部分とテンプレート定義部分の2つに分けて見ていこう。CSS定義部分は省略する。まずスクリプト部分は次のようになっている。

Booklist.vueのスクリプト部分

<script>
    import booklistService from '../booklistService.js'

    export default {
        name: "Booklist",
        data: function () {            // データ定義
            return {
                booklist: null,     // サーバから取得した書籍情報のリスト
                targettitle: ''     // 検索条件に指定されたタイトル
            }
        },
        created: async function () {    // 初期設定
            let res = await booklistService.getItems()
            this.booklist = res
            console.log("Initialized.")
            console.log(JSON.stringify(this.booklist))
        },
        methods: {                      // メソッド定義
            async refreshItems() {          // ページの初期化
                let res = await booklistService.getItems()
                this.booklist = res
                this.targettitle = ''
                console.log("Refreshed.")
                console.log(JSON.stringify(this.booklist))
            },
            addNewItem() {                  // [新規追加]ボタンの処理
                this.$router.push({ name: 'BookDatail', params: { id: null} })
            },
            editItem(itemid) {
                this.$router.push({ name: 'BookDatail', params: { id: itemid} })
            },
            async searchByTitle(title) {    // [検索]ボタンの処理
                if (title) {
                    let res = await booklistService.getByTitle(title)
                    this.booklist = res;
                    console.log(res)
                } else {
                    this.refreshItems();
                }
            },
            async deleteItem(item) {        // [削除]ボタンの処理
                let res = await booklistService.deleteItem(item)
                console.log(res)
                this.refreshItems()
            }
        }
    }
</script>

dataオブジェクトには書籍情報をリストとして格納するbooklistと、検索条件の書籍名を格納するtargettitleという2つのプロパティが定義してある。最初にページが読み込まれた時と、refreshItems()メソッドが呼ばれた時は、バックエンドから全書籍の情報を取得してbooklistに格納する。

searchByTitle()メソッドとdeleteItem()メソッドは、バックエンドに対して、それぞれ書籍タイトルを指定しての検索と、対象書籍情報の削除のリクエストを送信する。addNewItem()メソッドとeditItem()メソッドは、いずれもBookDatailコンポーネントに遷移するが、前者は対象書籍としてnullが渡される。

テンプレート定義は次のようになる。テーブルの要素がbooklistプロパティの値にバインディングされている。[検索]ボタン、[新規追加]ボタンはそれぞれsearchByTitle()とaddNewItem()を呼び出す。表の[詳細編集]ボタンと[削除]ボタンは、それぞれ対象の書籍のIDを指定してeditItem()、deleteItem()を呼び出す。

Booklist.vueのテンプレート定義部分

<template>
    <div style="background-color:transparent !important;">
        <h1>書籍リスト</h1>

        <div class="panel">
            <div class="input-group flex-column" style="margin-bottom: 20px !important;">
                <form>
                    <div class="form-group form-inline float-right">
                        <input v-model="targettitle" class="form-control" id="titleSearch" placeholder="タイトルで検索"/>
                        <button type="button" class="btn btn-primary" v-on:click="searchByTitle(targettitle)">検索</button>
                    </div>
                </form>
            </div>

            <div class="input-group flex-column" style="margin-bottom: 20px !important;">
                    <span class="input-group-btn">
                        <button class="btn btn-primary" v-on:click="addNewItem">新規追加</button>
                        <button class="btn btn-primary float-right" v-on:click="refreshItems">再読込み</button>
                    </span>
            </div>

            <table class="table table-bordered" id="booklist-table">
                <thead class="thead-light">
                    <tr>
                        <th scope="col">タイトル</th>
                        <th scope="col">著者</th>
                        <th scope="col">出版社</th>
                        <th scope="col"></th>
                    </tr>
                </thead>
                <tbody>
                    <tr v-for="item in this.booklist" v-bind:key="item.id">
                        <td class="title">{{item.title}}</td>
                        <td class="author">{{item.author}}</td>
                        <td class="publisher">{{item.publisher}}</td>
                        <td class="button">
                            <button type="button" class="btn btn-success btn-sm" v-on:click="editItem(item.id)">詳細編集</button>
                            <button type="button" class="btn btn-danger btn-sm" v-on:click="deleteItem(item.id)">削除</button>
                        </td>
                    </tr>
                </tbody>
            </table>
        </div>
    </div>
</template>

書籍の詳細情報を表示・編集するBookDatailコンポーネント

書籍情報の編集や新規追加はBookDatailコンポーネントで行う。ここでは、一覧に表示されていた情報に加えて、カテゴリーと発行日の2つの要素も表示する。

  • 詳細編集ページの表示例

    詳細編集ページの表示例

スクリプト部分のコードは次のようになった。

BookDatail.vueのスクリプト部分

<script>
    import booklistService from '../booklistService.js'

    export default {
        name: "BookDetail",
        data: function () {         // データ定義
            return {
                book:  {"id": null, "title": "", "category": "", "author": "", "publisher": "", "publicationDate": ""}  // 書籍の詳細情報
            }
        },
        created: async function () {    // 初期設定
            const id = this.$route.params.id
            if(id != null) {
                let res = await booklistService.getItem(id)
                this.book = res
            }
        },
        methods: {                      // メソッド定義
            cancelEdit() {                  // [キャンセル]ボタンの処理
                // Booklistに移動
                this.$router.push({ name: 'Booklist' })
            },
            async saveItem() {              // [登録]ボタンの処理
                // 既存アイテムなら更新、新規アイテムなら追加のリクエストを送る
                if (this.book.id != null) {
                    console.log(this.book)
                    let res = await booklistService.saveItem(this.book)
                    console.log(res)
                } else {
                    console.log(this.book)
                    let res = await booklistService.addItem(this.book)
                    console.log(res)
                }
                // Booklistに移動
                this.$router.push({ name: 'Booklist' })
            }
        }
    }
</script>

dataオブジェクトには書籍の情報を格納するbookプロパティが定義されている。書籍IDが指定されている場合は、ページ読み込み時にバックエンドから対象の書籍情報を取得して表示する。

saveItem()メソッドは、もしIDが指定されていればそのIDの書籍情報の更新を、IDがnullであれば新規追加のリクエストをバックエンドに対して送信する。cancelEdit()メソッドが呼び出された場合にはBooklistコンポーネントに遷移する。

テンプレート定義は次のようになる。

BookDatail.vueのテンプレート定義部分

<template>
    <div style="background-color:transparent !important;">
        <h1>詳細編集</h1>

        <div class="panel">
            <form>
                <div class="form-group">
                    <label for="titleInput">タイトル</label>
                    <input v-model="book.title" class="form-control" id="titleInput"/>
                </div>
                <div class="form-group">
                    <label for="categoryInput">カテゴリー</label>
                    <input v-model="book.category" class="form-control" id="categoryInput"/>
                </div>
                <div class="form-group">
                    <label for="authorInput">著者</label>
                    <input v-model="book.author" class="form-control" id="authorInput"/>
                </div>
                <div class="form-group">
                    <label for="publisherInput">出版社</label>
                    <input v-model="book.publisher" class="form-control" id="publisherInput"/>
                </div>
                <div class="form-group">
                    <label for="publicationDateInput">発行日</label>
                    <input v-model="book.publicationDate" type="date" class="form-control" id="publicationDateInput"/>
                </div>
                <button type="button" class="btn btn-primary" v-on:click="saveItem()">登録</button>
                <button type="button" class="btn btn-secondary" v-on:click="cancelEdit()">キャンセル</button>
            </form>
        </div>
    </div>
</template>

書籍情報の各要素がbookプロパティとバインディングされている。[登録]ボタンがsaveItem()メソッドに、[キャンセル]ボタンがcanselEdit()メソッドにそれぞれ対応する。

ルートパスを設定する

以上でアプリの本体は完成だが、Azureストレージで正しく動作させるには、コンテキストルートのパスの設定も追加しておかなければならない。プロジェクトのルートディレクトリに「vue.config.js」というファイルを用意して、次のように記述しよう。

vue.config.js

module.exports = {
    publicPath: './'
}

Azureストレージのサイトは静的Webサイトのコンテキストルートはエンドポイントの直下になっているため、Vueのビルド時にその相対パスが明示的に指定されている必要があるためだ。

Webアプリのビルド

以上で蔵書管理アプリのフロントエンド部分は完成だ。Vue.jsのプロジェクトは下記のコマンドでビルドできる。

> npm run build

ビルドした成果物はdistディレクトリ以下に展開される。今回のサンプルでは、jsディレクトとcssディレクトリ、index.html、404.htmlなどが作られているはずだ。

  • ビルドで生成されたファイル群

    ビルドで生成されたファイル群

なお、Azureストレージにデプロイする前にローカルで動作を確認したい場合には、次のコマンドを実行すればlocalhostのWebサーバでアプリが起動する。

> npm run serve

Azureストレージへのアップロード

それでは、ビルドされたdistディレクトリ以下のファイルをAzureストレージにアップロートしよう。Azureストレージのセットアップ方法については第8回で解説した。

Azure CLIを使っている場合、Azureストレージへのデータのアップロードはコマンドひとつで行うことができる。Azure CLIについては第3回の記事を参照していただきたい。この記事ではLinux版について紹介しているが、Azure CLIはWindows版やmacOS版も用意されている。

まず、az loginコマンドでAzureサービスにログインしよう。その状態で、先ほど作成したbooklist_webプロジェクトのdistディレクトリへ移動し、次のコマンドを実行する。

> cd [プロジェクトのルートパス]/dist
> az storage blob upload-batch -s ./ -d \$web --account-name [ストレージアカウント名]

これで、dist以下にあるすべてのファイルとディレクトリが、指定されたストレージアカウントにアップロードされるはずだ。ストレージアカウント名のところは各自の環境に合わせて記述して欲しい。

Azure CLIを使わずにAzureポータルからファイルをアップロードすることもできる。その場合は、作成したストレージアカウントの設定画面から$webコンテナの設定を表示し、下図のように[アップロード]をクリックしてアップロードしたいファイルを選択すればよい。

  • Azureストレージへのファイルのアップロード

    Azureストレージへのファイルのアップロード

複数ファイルを選ぶこともできるが、ディレクトリ単位でアップロードすることはできないので、ディレクトリ数が多い場合はAzure CLIを使った方が手軽だ。そのほかに、ストレージ管理用の専用アプリが提供されているので、それを使ってもよい。

アップロードできたら、ストレージアカウントのエンドポイントにアクセスして、ホームページが表示されれば成功だ。バックエンドのSpringアプリケーションとの連携が正しくできていれば、書籍リストおよび詳細編集のページから、書籍の情報を追加したり、一覧を取得したりすることができるはずだ。書籍情報の追加や編集ができたら、Cosmos DBにもそれが反映されていることを確認しよう。