はじめに
令和の時代がやって参りました!
一昨日、Firebase (Hosting & Functions) × Nuxt.js (universal) で ユーザ認証のベストプラクティスを探る旅という記事を投稿させていただいたのですが、「Service Workerを利用した構成の方がオススメ!」という有難い助言を頂戴しましたので、早速実践してみました。
大枠は前記事の通りですが、 nuxt 標準の pwa-module を使い、 Service Worker を利用してセッションを保持する実装にしています。
初学者としてつまづいたポイントと一緒に、構成を残しておきます。
目標
前回と同じ
- NuxtはUniversalモードで起動する
- Firebase HostingとFirebase FunctionsでサーバレスSSRを実現する
-
/account/login
にレンダリングしたFirebaseUIからFirebase Authenticationでログインする- ログイン後は
index
にリダイレクトする
- ログイン後は
- ログインユーザ情報をコンテンツ中で取得できるようにする
- ユーザがログインしていない場合、どのページにアクセスしても
/account/login
にリダイレクトする- リダイレクト前に元ページのレンダリングはしない
辿り着いた答え
ポイント
- Service Workerをnuxtに導入するにあたり
yarn create nuxt-app
でChoose features to install
と聞かれた時にProgressive Web App (PWA) Support
にチェックを入れていれば、追加パッケージ等は必要ありませんでした。 -
Firebase公式を参考に導入しましたが、nuxtのPWA Supportがお膳立てしてくれている部分が結構あります。firebase-auth用のservice worker を新しく作成し、
importScripts
で読み込むという方針であれば、Service Workerを追加する処理は不要でした。 - クライアント側はService Workerさえ導入してしまえば、その他変更なくただリクエスト投げるだけというびっくり簡単っぷりでした。
- ルーティング時の認証チェックは様々な実装方法があると思いますが、前回同様サーバ限定middlewareですんなり実装できました。
- 多分初学者限定ですが・・・service-workerはブラウザ上で動くので、importとかrequireができず苦労しました。
- これも初学者限定ですが・・・service-workerはFirebase Hostingでホストさせる必要があるというのがわからず苦戦しました。
ディレクトリ構成
.
├── firebase.json
├── functions
│ ├── index.js
│ ├── nuxt
│ └── package.json
├── public
│ ├── sw-firebase-auth.js
│ └── sw.js
└── src
├── assets
├── components
├── layouts
├── middleware
│ └── authenticated.js
├── nuxt.config.js
├── package.json
├── pages
├── plugins
│ ├── firebase-admin.js
│ └── firebase.js
├── server
├── static
│ ├── _sw-firebase-auth.js
│ ├── sw-firebase-auth.js
│ └── sw.js
└── store
例によって無駄なのは省いてます。
新しく追加したこととして、static配下とpublicに sw-firebas-auth.js
が追加されています。src/static
配下にアンスコ有無の2ファイルがある理由は後述。
導入の流れ
公式ドキュメント に沿って、どのように導入していったか書いておきます。
Service Worker に対する変更
ユーザーがログインしたときに現在の ID トークンを取得できるように、Service Worker に Auth ライブラリを組み込む必要があります。
ここで「Service Worker?え、どこにそんなファイルが??」でだいぶ詰みました。
色々文献を漁り、どうやらPWAサポートが追加されていればnuxtがいい感じにservice workerを追加してくれるらしいことがわかってきました。
yarn create nuxt-app
のダイアログで聞かれるコレ↓ですね。後から追加するやり方もあるみたいです。
? Choose features to install (Press <space> to select, <a> to toggle all, <i> to invert selection)
❯◯ Progressive Web App (PWA) Support
◯ Linter / Formatter
◯ Prettier
◯ Axios
で、 yarn run build
すると勝手に /src/static/sw.js
が生成されてました。簡単ですね。
「じゃあ編集対象ってどのファイルなの??」で結構悩みましたが、pwa-module公式ドキュメントを読んだところ、スクリプトが外出しでき、nuxt.conf.js
でインポートする設定で行けることが分かりました。
workbox: {
importScripts: [
'sw-firebase-auth.js'
]
}
コレだけでService Workerを追加することができます。簡単・・・。
で、/src/static/sw-firebase-auth.js
を追加してこいつを編集していけばOKでした。
余談ですが、pwa-module公式ドキュメントの記載だと、importScriptsのjsファイルは/assets
に配置するだけでオッケー!と書いてますが、/static
の間違いじゃないだろうか・・・。
Authライブラリの組み込み
次に詰まったのがここです。
// Initialize the Firebase app in the service worker script.
firebase.initializeApp(config);
ん? firebase
って未定義だからいきなり呼んだってダメでしょと思いながら動かしてみるとやっぱりfirebase is undefined
でChromeに怒られます。
var firebase = require('firebase')
// Initialize the Firebase app in the service worker script.
firebase.initializeApp(config);
じゃあコレでいけるっしょ!と思いきや、require
って何やねんとChromeに怒られます。
多分初学者あるある(だと思いたい)ですが、require
でライブラリが読み込めるのはNode上だけだとよく分かっておりませんでした。(だってvueだと普通にクライアントで動いてるっぽい感じで書けるから・・・恥)
service workerはブラウザ上で動くため、require
しようとしてもできないわけですね。
となると外部ライブラリをどうやって読み込むんじゃいと色々調べた結果、HTML側で <script src="">
を実行する以外に方法はなく、javascriptにライブラリインポートの概念自体存在しないことがわかってきました。
jsファイル内に <script src="">
を書いてみるという恥ずかしい悪あがきもして撃沈した後、Browserify
を使うという解決策にたどり着きました。
私と同じく初学者向けにBrowserifyを簡潔にご説明すると、jsだと外部ライブラリのインポートできないけど、外部ライブラリ全部ファイルの中に埋め込んじゃえば万事オッケーじゃん☆☆ という発想のシロモノです。
$ yarn global add browserify
で導入して
$ mv static/sw-firebase-auth.js static/_sw-firebase-auth.js
$ browserify static/_sw-firebase-auth.js -o static/sw-firebase-auth.js
で変換完了です。
当然、firebaseがまるっとファイルに含まれるので、正味78行のファイルが50,000行くらいになりました。
もっといいやり方ないだろうか・・・。
毎回やるのは面倒なので、package.json
のscripts.buildに入れておくと良いかもしれませんね。
あとは9割コピペですが、一応Service Workerの全文を残しておきます。
var firebase = require('firebase')
firebase.initializeApp({
apiKey: '*****************',
authDomain: '*****************',
databaseURL: '*****************',
projectId: '*****************',
storageBucket: '*****************',
messagingSenderId: '*****************'
})
const getIdToken = () => {
return new Promise((resolve) => {
const unsubscribe = firebase.auth().onAuthStateChanged((user) => {
unsubscribe();
if (user) {
user.getIdToken().then((idToken) => {
resolve(idToken)
}, () => {
resolve(null)
});
} else {
resolve(null)
}
})
})
}
const getOriginFromUrl = (url) => {
const pathArray = url.split('/');
const protocol = pathArray[0];
const host = pathArray[2];
return protocol + '//' + host;
};
self.addEventListener('fetch', (event) => {
const requestProcessor = (idToken) => {
let req = event.request;
if (self.location.origin == getOriginFromUrl(event.request.url) &&
(self.location.protocol == 'https:' ||
self.location.hostname == 'localhost') &&
idToken) {
const headers = new Headers();
for (let entry of req.headers.entries()) {
headers.append(entry[0], entry[1]);
}
headers.append('Authorization', 'Bearer ' + idToken);
try {
req = new Request(req.url, {
method: req.method,
headers: headers,
mode: 'same-origin',
credentials: req.credentials,
cache: req.cache,
redirect: req.redirect,
referrer: req.referrer,
body: req.body,
bodyUsed: req.bodyUsed,
context: req.context
});
} catch (e) {
console.log(e)
}
}
return fetch(req);
};
event.respondWith(getIdToken().then(requestProcessor, requestProcessor));
});
self.addEventListener('activate', event => {
event.waitUntil(clients.claim());
})
サーバー側に対する変更
あとはほとんどコピペでサーバ側の変更をmiddlewareとして実装すれば動きました。
これも9割型コピペなので、スクリプトを見ていただくのが早いと思います。
function getIdToken(req) {
const authorizationHeader = req.headers.authorization || ''
const components = authorizationHeader.split(' ')
return components.length > 1 ? components[1] : ''
}
export default (({ req, redirect }) => {
if (process.server && process.env.NODE_ENV === 'production') {
var admin = require('firebase-admin')
const idToken = getIdToken(req)
admin.auth().verifyIdToken(idToken).then(() => {
redirect('/')
}).catch((error) => {
console.log(error)
redirect('/account/login')
})
}
})
※ 2019/5/3追記: Service WorkerはProduction環境でないと生成されないので、process.env
の分岐を加えました。yarn run dev
だとmiddlewareはスルーされますのでご留意下さい。
現状だとリロードすると毎回 /
に戻っちゃうので、routerでうまく制御しないといけないですが、それは今後。
ログインしていない状態だとエラー吐いて気持ち悪いですが、それも今後。。
この状態で yarn run start
するとちゃんと動いてくれました。クライアント側は何も一切気にせずただリクエストを投げればいいという、実にデベロッパフレンドリー。
Firebaseへのデプロイ
これが最大の詰まりポイントで、令和元年初日をまるっと奪い去っていきました。
ローカルで問題なく動いたので firebase serve
でも全然いけるっしょ、と思いきや、以下Chromeに怒られます。
A bad HTTP response code (404) was received when fetching the script.
Failed to load resource: net::ERR_INVALID_RESPONSE
dcbac67bb39a765db27a.js:1 Service worker registration failed: TypeError: Failed to register a ServiceWorker: A bad HTTP response code (404) was received when fetching the script.
NetworkやSourceタブをみる限り、どうやら sw.js
が見つからない御様子。
dist
を見ても、そもそもそんなファイルがデプロイされてない。でも yarn run start
だと普通に動いている。
Firebaseを悪者にして「service-workerのファイル名に制約があるんでしょ?」とファイル名を公式サンプルの通りに変えてみてもダメ。
「じゃあsw.jsってやつ dist/client
に手動で置いてやれ」と思って置いてみてもダメ。
「pwa-moduleとFirebaseの相性でうまく動かないんだ!手動でサービスワーカーを配置しよう!」これもダメ。
堂々巡りに陥っていましたが、ようやく解決策と自分の勘違いに気づきました。
「Service Workerはブラウザで実行されるので、Firebase Hostingでホストされる必要がある」 これでした。
いくらdistを弄っても、Firebase Functions側の変更なので意味がなかったんですね。。。
それと同時に、/src/static
配下のファイルはFirebase Hostingでホスティングされる必要があるということに今更気づきました。今回の構成では /public
配下に置いてやる必要があります。
そういえば firebase serve
だとfaviconなかったよなあ。。。今更。
├── public
│ ├── sw-firebase-auth.js
│ └── sw.js
これで firebase serve
firebase deploy
共にしっかり動きました。
追記
firebase deploy
を実行した際、deployされる前に実行するスクリプトをあらかじめ定義するという超絶便利機能があるとコメント頂戴しましたので、今回の場合の設定方法を記載しておきます!
deploy前に /public
配下を /static
でまるっと更新しています。
【参考】デプロイ前フックおよびデプロイ後フック
{
"hosting":{
"postdeploy":"rm -rf public/* && cp src/static/* public/",
}
}
これで firebase deploy
するだけで /public
配下が /src/static
のファイルで更新されます。便利!
終わりに
前回の記事に有難いコメントを頂いたおかげで、Service Workerを用いた、より簡潔でパフォーマンスも高い実装にできたと思います。感謝感激雨嵐。
Firebase、Nuxtいずれも詰まるポイントは多いですが、大体自分のそもそものjavascript経験の浅さから来るものなので、解決してしまえば数ステップでかなりすんなりデプロイできてしまいます。面倒なことは全部これに任せて、サービスの市場投入を加速するにはもってこいですね。
今後はFirestore関連を掘り下げて学んでいきたいと思います。