東京の気象データをDRF経由でVueで作った画面に表示させるまで(概要)

今回は趣味で、もとい、検証のため作ってみたWebアプリのことを書きたいと思います。 やりたかったことは、「データベースからWebAPI経由でデータを取得して画面に表示する」、ただそれだけです。 まぁ、これぐらいできれば大概何でもできるでしょう。

似たようなことをやりたい方へのヒントになればと思い、情報をまとめています。細かな手順やコードは記事には書いていません。 参照先のサイトを確認すればできると思います。

作成の際に気を付けたのは以下のような点です。

  • フレームワークらしく、モジュール化・コンポーネント化が考慮された納得できるファイル構成であること。1ファイルに異なる処理をダラダラ書かないこと。
  • 機械的に生成できる部分は省力化すること。(手で書くと間違えるので)

画面の見た目

こんな感じです。(しょぼいですが)

f:id:handatec:20200422134817p:plain
サンプルアプリ画面
こちらはグラフ。気温のデータをvue-chart.jsで表示させています。
f:id:handatec:20200428192341p:plain
サンプルアプリ画面(グラフ)

環境

以下のような構成で作成しています。

フロントエンド(Vue)

  • Vue CLI 4.3.0
  • Vuetify 2.2.11
  • axios 0.19.2
  • vue-router 3.1.6
  • vue-chartjs 3.5.0
  • chart.js 2.9.3

バックエンド(Django

データベースサーバ(PostgreSQL

OS、サーバ構成、ネットワーク(共通)

OSはいずれもUtuntu 18.04 LTS、VirtualBox仮想マシン3台構成で同一ネットワーク上で動作させています。

フロントエンドにVue.jsを選んだ理由

単に「速かったから」です。ReactやAngularはそれ自体のインストールや開発サーバの起動に結構な時間がかかったり、エラーが出たりしてテンポよく作業できないと感じました。また、マイクロサービスアーキテクチャを念頭に置くと、フロントエンドはとにかくUI、表示に集中できればよいという考えがあり、Vueはそれにマッチしました。コンポーネント指向も気に入りました。

バックエンドにDjango/DRFを選んだ理由

バックエンドは少し複雑な処理を行って、結果をWebAPI経由でフロントに渡すところまでができれば何でもよかったのです。オールJavascriptで揃えたかったのですが、Python系にしたのは昨今の機械学習/ディープラーニングやデータ分析の主流になっていて、それらと組み合わせる場合に都合がよさそうという理由です。

Python系Webフレームワークの2大有名どころとして、DjangoとFlaskがあると思います。DjangoDjango Rest Frameworkと合わせてデータベースアクセスからAPI作成までフルスタックであるところ、また、DBマイグレーション機能がついているところが気に入りました。FlaskはAPI作成は簡単ですが、データベースアクセスとデータのJSON化に他のライブラリ(SQLAlchemyやmarshmallow)を組み合わせる必要があり、正直面倒だと感じました。

よさそうな書籍

ここで、よさそうな書籍をメモしておきます。Vue、Django Rest Frameworkそれぞれ1冊づつです。本来であれば、本屋さんに行って中身をざっとチェックしてから購入したいのですが、昨今のコロナ対応の件で本屋さん閉まっててできない状況です。

両書籍とも電子版がありますが、自分は今のところサンプルをダウンロードしているだけで購入の踏ん切りがついていません(笑)。ただ、DRFの方はサンプルでも1章まるまる読むことが可能で、概要が理解できたので内容に価値があると思いました。

サンプルアプリの元ネタ

今回のアプリ作成にあたり、以下のサイトを参考、ヒントにさせていただきました。大筋は同じですが、サーバOSやデータベースなどの一部の環境、フロントエンドの方式、扱うデータ、modelsの記述のやり方(後述)などが異なります。 qiita.com

データベース周りのポイント

アプリで扱うデータとデータベース、テーブルの準備

ある地点の気温、湿度、気圧のデータを使おうと考えました。気象庁のサイトではAPIなどは公開されていないため、「過去の気象データ・ダウンロード」より手作業でCSVファイルを取得して多少加工し、PostgreSQLにテーブルを作成してpgAdmin4のインポート機能で突っ込みました。テーブルの作成はDDLを書くのが面倒なので、GUIから行いました。 CSVの加工はtalendなどETLツールを使っても面白いかもしれないですね。

気象庁|過去の気象データ・ダウンロード

Djangoのmodelsの記述をテーブル定義から生成

Djangoのmodelsを手で書くのが面倒なので、inspectdbを使用してテーブル定義からリバースで生成しました。import文以外は生成したままでいけますし、makemigrations、migrateを実行しても問題起こりません。素晴らしい。

こちらのサイトを参考にさせて頂きました。 qiita.com

あと、地味に面倒なのは、DRFRest APIを作成する場合、models, serializers, viewsの3点セットを作成する必要があるところです。

psycopg2インストールでハマった

PythonアプリからPostgreSQLへ接続する際に必要となる「psycopg2」のインストールでハマりました。何をやってもビルドエラーが出まくるので、暫定的にバイナリ版をインストールしました。

sudo python3 -m pip install psycopg2-binary

このインストール方法は公式にも書かれていますが、sslモジュールと競合したりアップグレードの時に困るだろうからProduct版はソースからビルドしろ(?)云々の警告、ノートの記載があります。

Installation — Psycopg 2.8.5 documentation

メモ:バックエンド側の環境構築について感じたこと

anacondaも使ってみましたが、結局condaでインストールできないパッケージがあり、じゃあ最初からpyenvやpip使っておけばいいじゃんと思ってしまいました。メリットは、シェル上で使用している仮想環境の名前が表示されていて分かりやすいところぐらいでしょうか。Jupyter notebookとか手軽に使いたい人向けですかね。

環境管理の点では以下のサイトの記事が興味深かったです。 watlab-blog.com www.zopfco.de

CORS、プリフライト対応のポイント

Vueでのaxiosの使い方と合わせてどハマりしましたが、解決しました。詳細は下記の別記事をご覧ください。 handatec.hatenablog.com

フロントエンド側のポイント

Vuetifyの使い方

画面の見た目はVuetifyで作成しています。以下のサイトと公式ドキュメントを参考にさせて頂きました。 reffect.co.jp

vue-routerについて

以下のサイトが非常に分かりやすかったです。ルーティングに関する記載はrouter.jsに集中させることにします。

https://b1tblog.com/2019/10/03/vue-router/b1tblog.com

router.jsの記載例は以下の通りです。

import Vue from 'vue'
import Router from 'vue-router'

import table from './components/table'
import chart from './components/chart'

Vue.use(Router)

export default new Router({
  mode: 'history',
  routes: [
    {
        //一覧表用
        path: '/table',
        component: table,
        name: 'table',
    },
    {
        //グラフ用
        path: '/chart',
        component: chart,
        name: 'chart',
    },
  ]
})

Vuetifyのナビゲーション(画面左のメニュー)からのリンク設定は参考にしたサイトはありますが、少々込み入っているのでポイントを書いておきます。

上記router.jsを記載したうえで、App.vue内の3か所に記載を追加します。

  • コンテンツの表示領域の記載。タグ<router-view />部分のコンテンツがルーティング設定に基づいて切り替わります。
<v-content>
    <router-view />
</v-content>
  • <v-navigation-drawer>タグ内のリンク設定。:to="list.link"がリンクの記載になります。
<v-list-item
    v-for="list in nav_list.lists"
    :key="list.name"
    :to="list.link"
>
    <v-list-item-title>
        {{ list.name }}
    </v-list-item-title>
</v-list-item>
  • 具体的なリンク先について、メニューの設定を行っている箇所にlink;xxxを追記。xxx部分はrouter.jsに記載したpathと一致させます。
export default {
  data(){
    return{
      drawer: null,
      nav_lists:[
        {
          name:'ダッシュボード',
        },
        {
          name:'気象データ',
          lists:[
            {
              name:'一覧',
              link:'/table'
            },
            {
              name:'グラフ',
              link:'/chart'
            },
          ]
        },
      ],
    }
  },

現時点で やり残していること

  • グラフのデータ取得と表示のタイミング合わせ
  • グラフのコンテナvueファイル作成
  • API認証(アプリケーションIDとか発行して認証したい)
  • (Restじゃない)WebAPIの作成

 今回はここまで

誰もちゃんと教えてくれないDjangoのCORS、プリフライト対応(Vue CLI+axios+DRFのRestAPI通信エラー)

こういう泥臭い内容ってQiitaに書いたほうがいいのかな~と思いつつ。

とあるサンプルアプリを作っている中で、WebAPIの通信エラーに半日ハマりました。。。

 (とあるアプリの内容は本記事最下部のリンクにて)

  1. axios周りのコードをググってよくヒットする情報のまま書いてもうまく動いてくれない。
  2. DRFで作成したWebAPIにaxiosからアクセスするとCORS関連のエラーが出る。

ネットで調べても断片的な情報しか得られなかったのでまとめておきます。原因は複合していましたが、大きく分けると2点ほどです。 同じようなことで詰まっている方はお試しあれ。

環境

フロントエンド:

  • Vue CLI 4.3.0
  • axios 0.19.2

バックエンド:

いずれもOSはUbuntu 18.04 LTS、各々VirtualBox仮想マシンで構成し、同一ネットワーク上で動作させています。 IPアドレスとポート番号が異なる、つまりオリジンが違います。

1. axios周りのコードがうまく動いてくれない件

こちらは比較的短時間で解決。 ネット上でよく紹介されている内容は以下のような流れですが、4.のところでaxiosがうまく動いてくれません。(自分のvue-axiosの理解が足りないのかもしれませんが。)

  1. axiosとvue-axiosをインストール
  2. main.jsにaxiosとvue-axiosをインポート
  3. main.jsVue.use(axios,VueAxios) を追記
  4. データ操作(GET等)する処理のところでaxios.get()

以下のように修正するとちゃんと動いてくれます。

3.の代わりに main.js に以下を記述(2.のvue-axiosインポートの記述は削除)

Vue.prototype.$axios = axios

4.で実際にaxiosを使う際は以下のように記述

this.$axios.get()

この内容は公式にも記載されていました。Vueのインスタンスプロパティを利用するとのことです。$以降の文字列は何でもよさそうです。 jp.vuejs.org

あと、参考にさせて頂いたこちらのサイトでは正確かつ詳細に書かれています。 noumenon-th.net

2. DRFで作成したAPIにアクセスするとCORS関連のエラーが出る件

こちらはどハマりしました。 いわゆるCORS対応は済ませたつもりでしたが、足りなかったんですね。足りないと気付くまでに実施していた内容は次の通りです。

フロントエンド側(Vue)

main.js に以下を記載。

ちなみに、'Access-Control-Allow-Origin'はフロントエンド側の指定になりますが、ワイルドカード'*'はダメみたいです。

 axios.defaults.baseURL = 'http://x.x.x.x:nnnn' //バックエンド側のIPとポート
 axios.defaults.headers.common['Accept'] = 'application/json'
 axios.defaults.headers.common['Content-Type'] = 'application/json;charset=utf-8'
 axios.defaults.headers.common['Access-Control-Allow-Origin'] = 'http://y.y.y.y:mmmm'
 axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'

バックエンド側(Django

django-cors-headersをインストールして、setting.py に以下を記載。

  • INSTALLED_APPS に 'corsheaders' を追記
  • MIDDLEWAREに 'django.contrib.sessions.middleware.SessionMiddleware' を追記   ('corsheaders.middleware.CorsMiddleware'より上に記載する必要ありとのこと)
  • CORS_ORIGIN_WHITELIST を追加し、フロントエンド側のIPアドレス、ポート番号を記載

ネット検索するとよく出てくる、CORS_ORIGIN_ALLOW_ALL = Trueの記載は、ホワイトリストを明記していれば多分不要です。

INSTALLED_APPS = [
    'corsheaders', #追加
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware', #追加
    'django.middleware.common.CommonMiddleware',

]

#追加
CORS_ORIGIN_WHITELIST = (
    'http://y.y.y.y:mmmm',
)

しかしながら、このままではVue+axiosで作成したページを開いた瞬間にプリフライト要求(preflight response)が発生し、その通信がエラーになります。ブラウザ画面上では、さも何も起こっていないかのようにみえ、画面上からは気づけません。その後、this.$axios.get()の処理に到達してもNetwork Errorが発生します。

プリフライト要求発生時にバックエンド側のログには

 "OPTIONS / HTTP/1.1" 200

と出ているので、ブラウザ側でプリフライト要求に対する応答が拒否されていると判断。FireFoxの開発者ツールで(←ここ重要)出力されているメッセージをみると、 「access-control-allow-headersにaccess-control-allow-originが含まれていません」 といった旨の内容が(詳細失念)。 え?と思い、応答側のHTTPヘッダをみてみると確かに、access-control-allow-headersには"accept"や"content-type"等は入っていましたが、"access-control-allow-origin"が入っていないことを確認。 じゃあ、Djangoのsetting.pyに何かしら記載して応答ヘッダに"access-control-allow-origin"を入れることができれば何とかなるではと予想し、再度ググるとそれっぽいのが出てきた。(ドンピシャの情報はなかったです)

バックエンド側(Django)への追加設定

setting.py にCORS_ALLOW_HEADERSを追記

CORS_ALLOW_HEADERS = (
 'accept',
 'accept-encoding',
 'authorization',
 'content-type',
 'dnt',
 'origin',
 'user-agent',
 'x-csrftoken',
 'x-requested-with',
 'access-control-allow-origin',
)

ポイントは'access-control-allow-origin',の箇所です。これでプリフライト要求も、そのあとのGETなども正常に通ります。よかったよかった。なお、一度プリフライト要求がOKになるとしばらくの間は有効のようです。

こちらのサイトの内容がヒントになりました。ありがとうございます。 teratail.com ja.coder.work tkkm.tokyo

余談

ちなみに、プリフライト要求が発生するのは「通常の通信である条件」から外れるもののようです。RestやWebAPIでは必ず発生しますので、オリジンがフロント/バックで別れていてDjangoを使っている場合はこの記事の対応は必須だと思います。ネット検索でよくヒットする対応方法としてはProxyを使った回避方法が紹介されていますが、そんなことする必要な場合があるのでしょうかね?

あと、上記参考サイトにも書かれていましたが、HTTP関連の不具合調査をChromeの開発者ツールで実施しないほうがよいです。解析に必要となる情報が表示されないことがあります。  

「とあるサンプルアプリ」について

興味のある方は以下をご覧ください。 handatec.hatenablog.com

今回はここまで。  

6年半振りのブログ再開で、はてブロを選んだ

かくかくしかじかな理由※で時間に余裕ができ、かつ書きたいこともできたのでブログを再開することにしました。 ※込み入った話なので本記事最下部のリンクより

以前はblogger(http://handatec.blogspot.com/)で書いており、最後に記事を書いたのは2013年9月なので、およそ6年半振りの再開になります。

今や世の中にはQiitaやnoteなど色々なサービスがあり、1日ほど悩んだ結果、書く内容に縛りがないここに決めました。

このブログでは、その時々で興味のある技術の検証内容、具体例、参考情報の紹介などのほか、生活・ライフスタイルに関する内容も書きたいと思っています。

現在所属している某通信系SIerの会社では、仕事の70%が内部打合せや調整、20%がお客様対応、残りはExcelで表を作る日々を過ごしていたので、技術検証やプログラミングは趣味であり、ストレスの解消でもあります。

自分は元々Javaの人ですが、2020年4月現在の個人的な関心は以下のような事柄です(3周ぐらい遅いかな?)。

所属している会社はエンタープライズ系の重厚長大システム開発ばかりなので、仕事では軽量言語はほぼ触りませんが、それでは世の中に置いて行かれますからね。

ちなみに、bloggerでの過去の執筆記事数はちょうど40、年間5,000PVぐらいでした。さて、今回はいつまで続けられるかな~?

 

handalife.hatenadiary.com