Vue.js、Firebaseを使用したwebサービスreftikaを作成しました。備忘録も兼ねて参考にした記事をまとめながらデプロイするまでの流れを書きます。
#※とてつもなく初心者です
現職は広告業界。Pythonはちょこちょこ簡単なスクリプトを書いて便利に使っていましたが、webは全くの初心者です。
HTML/CSSの勉強から始めて1ヶ月足らず。
本記事ではこうした方が良い、こうする必要はないということがあるかと思われます。コメントでご指摘いただけると幸いです。
#作ったもの
reftika
レファレンス協同データベースからの簡易キーワード検索とランダム表示を提供するサービスです。
##レファレンスってなに?
#####レファレンスサービスとは?
レファレンス協同データベース(レファ協)の名称にも使われているレファレンスサービスとは、図書館員が資料などを使って、みなさんの疑問や質問にお答えしたり、資料を探すお手伝いをするサービスです。
#####レファレンス協同データベースとは?
公共図書館、大学図書館、専門図書館、学校図書館がレファ協のデータベースにデータを登録し、それをみなさんや図書館員、研究者などが利用する。
レファレンス協同データベースは、全国の図書館等で日々行われているレファレンスサービスの記録や情報の調べ方などを図書館員がデータベースに登録し、そのデータをインターネットを通じてみなさんに提供するサービスです。この「図書館の知識が集まったデータベース」を使って、日常生活でふと感じた疑問からビジネスや調査研究に関する専門的なものまで、幅広いデータがご覧いただけます。
#####具体例
カラスが石鹸を食べるというのは事実か。また、なぜ石鹸を食べるのか。(鳥取県立図書館)
クラシック音楽作品の日本での演奏記録を調べる(桐朋学園大学音楽学部附属図書館)
自転車のことを「チャリンコ」「ママチャリ」「ケッタマシーン」などと呼ぶが、どういう語源か。 (岐阜県図書館)
参考:
レファ協活用術!
##なぜつくったのか
第一に勉強用ですが、実用的なものが作りたい思いがあり、スマホではちょっと見づらいと感じていたレファレンス協同データベースの検索に目を付けました。
はじめは、APIを利用してスマホ用に検索表示を見やすくするサービスをつくってみようと思っていたのですが、雑学チックなものから専門的なものまで多種多様なレファレンスがあるこのデータベースをランダムに見れたら面白そうだと考え、ランダム表示の機能をメインにしました。
ちなみに、この記事のサービスにも影響されました。→Qiitaの記事をランダムに読める API / サービス を4時間ぐらいで作った (Firebase/AWS/Docker) (運用費0円※)
#環境
APIの仕様はこちら。
レファレンス協同データベース・システム操作マニュアル参加館用(オンライン版)
レスポンスはxmlかrss形式です。
ランダムな記事取得機能は提供されていません。
#実装
Vue.jsで作成し、FirebaseのHostingを利用しています。
レファレンス協同データベースのAPIは「同一生成元ポリシー(Same-Origin Policy)」に引っ掛かるため、別途中継サーバーをfalconで作成しHerokuで運用しています。
#####利用した技術
"vue": "^2.6.10",
"vue-router": "^3.0.3",
"vuex": "^3.0.1"
"vue-toasted": "^1.1.27",
"axios": "^0.19.0",
その他、vue-cli、lodash、Firebase(Hosting)、python、falcon、Heroku、またGoogle Font、font awesomeなどを利用しました。
##公開までをざっくりと
0.この記事をベースに作り始めました。Vue.jsとAxiosなら驚くほど簡単に作れる!外部APIを使ったWebアプリの実例
1.apiの仕様の確認と取得したデータの処理
2.vue.jsでテスト実行→同一生成元ポリシーに引っ掛かる
3.同一生成元ポリシーを回避するためにfalconでサーバーを立てる&Herokuにアップ。
4.vue.jsで、一覧ページ・詳細ページの作成。routerの設定など
5.firebaseデプロイ
6.デザインの作成。レスポンシブ化。twitter card対応。
土日で作るつもりでした。しかし、CORSについてほとんど知識がなかったため2番の時点で終わらないことを覚悟。予定変更して、結果的に4日間で一通り完成しました。
##公開までをきっちりと
###0.この記事をベースに作り始めました。Vue.jsとAxiosなら驚くほど簡単に作れる!外部APIを使ったWebアプリの実例
上記記事ではcdn版のvue.jsを使用していますが、今回はpackage.jsonでの管理も学びたかったため、vue-cliを使用しました。また、cssの勉強のためfoundationも使用していません。
$ npm install -g @vue/cli
$ vue create reftika
$ cd reftika
$ npm run serve
でサンプルが表示されます。
参考:
Vue.js を vue-cli を使ってシンプルにはじめてみる
###1.apiの仕様の確認と取得したデータの処理
http://crd.ndl.go.jp/api/refsearch?type=reference&query=question any 読書
で「読書」というワードに適合度順のソートされた検索結果が200件返ってきます。
参考:レファレンス協同データベース・システム操作マニュアル参加館用(オンライン版)
#####ランダムな記事取得機能は提供されていなかったので対応。
ランダムな記事取得機能はAPIでは提供されていません。しかし、事例(記事)登録日による検索が可能です。そこで、一番最初のデータの登録日~現在までの日付をランダムに生成し、その日付をもとに検索することで疑似的にランダムなデータ取得を行えるようにします。
一番最初のデータの登録日が2004年3月6日だったため、以下の関数で2004年3月6日~今日までの間の日付をランダムに取得しています。
makeRandomDate: function() {
const start = new Date(2004, 3, 6);
const end = new Date;
const querydate = new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime()));
const month = ("00" + (querydate.getMonth() + 1)).slice(-2);
const date = ("00" + querydate.getDate()).slice(-2);
const regdate = querydate.getFullYear() + month + date;
return regdate;
},
#####xml形式のレスポンス
「APIを使ってみた」系の記事のほとんどはjson形式でデータを取得していたので不安でしたが、xmlでも処理に困ることは特にありませんでした。エンティティ参照で「&」が「&」で表示されてしまったので、エンコード処理を加えた程度です。
const url = result.getElementsByTagName("reference")[0].getElementsByTagName("url")[0];
const obj = {
省略
url: url.innerHTML.replace(/&/g, '&') + '\n',
};
#####その他
2010年にレファレンス協同データベース主催のAPI腕自慢という企画が行われていました。Twitterで今でも運用されているbot「おしえて!れはっち (@referty_bot)」はこの時に作られていたことを知りました。ほか、サービスは終了していますが、「ふわっとレファ協関連検索」も素晴らしいサービスだと思います。モチベーションに繋がりました。
###2.vue.jsでテスト実行→同一生成元ポリシーに引っ掛かる
APIの仕様をもとにApp.vueを調整していきます。
レファレンス協同データベースの結果はXMLとして返ってくるので、パーサーもいれます。
初めはテストするために、キーワードは「読書」固定&マウント時に即APIを呼び出して画面に表示するように実装しました。
<template>
<div id="app">
<div class="columns medium-3" v-for="refqa in refqas">
<div class="card">
<div class="card-divider">
{{ refqa.question }}
</div>
<div class="card-section">
<p>{{ refqa.answer }}.</p>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
refqas: [] ※APIから返ってきた記事データの格納先
}
},
mounted() {
axios.get("http://crd.ndl.go.jp/api/refsearch?type=reference&query=question%20any%20%E8%AA%AD%E6%9B%B8")
.then(response => {
var oParser = new DOMParser();
var oDOM = oParser.parseFromString(response.data, "application/xml");
const results = oDOM.getElementsByTagName('result');
※返答XMLにおいて、1つ1つの記事は<result></result>で囲まれている。
※その後、各記事の質問と回答を取得。
for(const result of results){
const question = result.getElementsByTagName("reference")[0].getElementsByTagName("question")[0];
const answer = result.getElementsByTagName("reference")[0].getElementsByTagName("answer")[0];
const obj ={
question:question.innerHTML,
answer:answer.innerHTML
};
this.refqas.push(obj)
}
}
)
}
}
</script>
しかし、
クロスオリジン要求をブロックしました: 同一生成元ポリシーによりhttp://crd.ndl.go.jp/api/refsearch?type=reference&query=question%20any%20%E8%AA%AD%E6%9B%B8にあるリモートリソースの読み込みは拒否されます
CORSまとめ
サーバサイドのCORS対応
Falconで光速のWeb APIサーバーを構築する
などを参考に「クロスオリジン要求」とは何かから調べました。
###3.同一生成元ポリシーを回避するためにfalconでサーバーを立てる&Herokuにアップ。
調べてみると、開発環境では簡易的にサーバーを立てている方が多いらしい。
Cloud Functions for Firebaseで対処できないかと思ったのですが、Google Cloud PlatformならPythonが使えると知って調べたり、試したり...。
紆余曲折の上falconとHerokuに落ち着きました。
まずは、Falcon 「Access-Control-Allow-Origin」の回避方法 やPython+FalconでサクッとAPIサーバを作るを参考にfalconでローカルにサーバーを立てる。
一旦、「読書」で検索した値だけを返すサーバーを作りました。
# -*- coding:utf-8 *-
import falcon
import requests
class CORSMiddleware:
def process_request(self, req, resp):
resp.set_header('Access-Control-Allow-Origin', '*')
class SampleResource:
def on_get(self, req, resp):
r_get = requests.get("http://crd.ndl.go.jp/api/refsearch?type=reference&query=question%20any%20%E8%AA%AD%E6%9B%B8")
”%E8%AA%AD%E6%9B%B8”は”読書”のパーセントエンコーディング。
r_get_text = r_get.text
obj = {
"body": {
"xml":r_get_text,
}
}
resp.body = r_get_text
app = falcon.API(middleware=[CORSMiddleware()])
app.add_route('/sample', SampleResource())
if __name__ == "__main__":
from wsgiref import simple_server
httpd = simple_server.make_server("0.0.0.0", 8000, app)
httpd.serve_forever()
サーバーを8000番ポートで起動して、
App.vueのアクセス先を変更。
axios.get("http://localhost:8000/sample")
再度起動
npm run serve
うまくいきました。
検索機能を実装したら、Herokuにデプロイします。
Procfileやruntime.txtなどを作成。
web: gunicorn fal:app --log-file -
python-3.7.3
falcon==2.0.0
gunicorn==19.9.0
requests==2.22.0
Herokuクライアントのインストール後、
$ heroku create
$ git remote add heroku https://git.heroku.com/xxxxx.git
$ git push heroku master
もうサービスが動いている!!ワクワクです!!
Procfileやruntime.txtなどをつくるのも初めてでしたが、こんな簡単にできるように整備されているのかと驚きました。
参考:
Herokuを久々に使用する
PythonのWebアプリフレームワークFalcon試してみた
【HEROKUとは】これを読めばOK!デプロイの仕方まで徹底解説
HerokuにRailsアプリをデプロイする手順
###4.vue.jsで、一覧ページ・詳細ページの作成。routerの設定など
この時点までrouterは使っていませんでした。
一覧画面から個別記事のページへの遷移後、戻るボタンを押すと一覧のデータがリフレッシュされるのは不都合・不自然であるためその対処として導入しました。また、無限スクロールを実装した後にこのサービスではあまり意味がない機能であることに気づいたり、同様の処理を行う関数をまとめたり、計画性のなさを痛感しました。
あちこちエラーが出ては直しての繰り返しでしたが、ググりながらなんとか進めていくことができました。
Vue.js で入力フォームを↑↓キーやタブ・シフトタブでフォーカス移動するなどのチップス的な記事やVue.js初心者向け:Vue.jsとaxiosでJsonを取得してコンポーネントに反映するメモの「勘違いしていたけどこうだった」という記事は本当に勉強になりました。
先人に感謝します。
#####loodash
また、
キーワードの入力後、SPAらしく自動で検索が行われるように。かつ、APIに負荷がかかりすぎないようにlodashというライブラリが使えるらしい。ということで導入しました。入力操作が止まってから検索を実行するまでに5秒後のタイムラグをつけています。
レファレンス協同データベースのAPIは一応呼び出し無制限とはなっていますが、私がAPIにあまり触ったことがないことを前提に、安全のために少し長めにしています。
ただでさえHerokuを挟んで遅めなのにもっと遅くなってしまった...。ちなみに、少し負荷をかけたときにはHerokuの方がエラーを吐きました。応答性にはあまり期待しないでください。コンテンツは面白いものばかりなので、気長に待っていただけるとありがたいです。
created: function() {
this.debouncedGetAnswer = _.debounce(this.getAnswer, 5000);
省略
},
省略
getAnswer: function() {
if (this.keyword === '') {
this.loading = false;
return;
}
this.refqas = [];
axios.get(省略
###5.firebaseデプロイ
以前Reactで勉強用の小さなサービスを作ったときや静的サイトのホスティングで使ったことはあったので、おさらいをしながらデプロイ。
一度SPA用の最適化の項目でNoと答えてしまったので、後でfirebase.jsonを修正しました。
? Configure as a single-page app (rewrite all urls to /index.html)? (y/N) N
としたために、
{
"hosting": {
"public": "dist",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [ ←この項目を後で付け足しました。![ScreenShotsp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89564/3132c61f-7ef3-aa2f-d8ae-0ca1ba6cdffc.png)
{
"source": "**",
"destination": "/index.html"
}
]
}
}
firebase init
firebase deploy
参考:
Firebaseで初めてのデプロイ
[Firebase][iOS] Firebase Hosting でキャンペーンページを作ってみよう
###6.デザインの作成。レスポンシブ化。twitter card対応。
スマホでの見づらさ解消が作り始めたきっかけでもあるのでモバイルファーストに。文字情報が多いので極力シンプルに作成しました。
twitter cardの対応については、人によってそれぞれやり方があるようです。
#####静的にどのページも固定で表示する派
Vue.jsで静的にOGP対応する
【Vue.js】ツイッターのツイートに画像がついてるツイッターカードのつけ方
#####動的に生成する派
Nuxt.jsでSPAしてFirebase Cloud FunctionsでOGPタグを作成する方法
vue-routerでtitleとdescriptionを動的に切り替える
SNS映えするWebアプリを...!FirebaseとVue.jsでSPAのOGP画像の動的生成をやってみたら案外楽だった
4つ目のSNS映えするWebアプリを...!FirebaseとVue.jsでSPAのOGP画像の動的生成をやってみたら案外楽だったは各ページ(画像を表示したいすべてのページ)の設定が必要です。動的に外部サーバーからデータを取得してページを生成する私のサイトでは難しそうです。
fiebase.jsonの設定を参考にして、静的に対応する方法にしました。
{
"hosting": {
"public": "dist",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "/ogpimg", ←これを設定
"destination": "/img/sns.jpg" ←画像格納先
},
{
"source": "**",
"destination": "/index.html"
}
]
}
}
ヘッダーのメタタグはこんな感じです。
<meta name="twitter:card" content="summary">
<meta property="og:url" content="https://testreftika.web.app/">
<meta property="og:title" content="reftika">
<meta property="og:description" content="図書館のレファレンスをもっと身近にするサービス">
<meta property="og:image" content="https://testreftika.web.app/ogpimg">
#その他仕様
キーワード検索について。入力後、初めて表示される結果はキーワードとのマッチ度順となっています(=何度やっても同じキーワードなら同じ結果)。
しかし、それだけでは面白みに欠けると思い、初回検索後、画面下部に表示される「もっと検索する」ボタンでは登録日のランダム検索と組み合わせて毎回変わるようにしました。
#今後
ソースは今後Githubにアップする予定です。
<追記>アップしました。
githubリンク