Vue.js も SPAも Firebaseも初めてなので、WEB Frameworkでやるような404をどうすればできるか考えてやってみたメモ。
Vue.jsは下記のPWAのテンプレートを利用し、vue routerも使っている
github.com
routerで指定していないパスで404を表示
これは簡単だった。
routesの最後にキャッチオール的に404、NotFound用コンポーネントを指定して表示させる
stackoverflow.com
const ROUTER_INSTANCE = new VueRouter({
mode: "history",
routes: [
{ path: "/", component: HomeComponent },
{ path: "*", component: PageNotFound }
]
})
しかし、これだけだとパスにID等を含めた動的ルートマッチングの場合はできない。
動的ルートマッチングで404を表示する
router.vuejs.org
例えば今回だと
path: '/format/:id/edit/:cardId',
のようなpathがあり、format idと、card idと二つの変数が含まれていた。
- idと
- cardIdは、別のJSONファイルに含まれており、コードでは表現できない。
最初表示した先のコンポーネントのmountedでどうにかしようと思ったけど、タイミング等によって出るエラー対処が面倒くさすぎてrouter側で事前にやってしまうのがいいと実感。
そのためにはナビゲーションガードのルート単位ガード、beforeEnterを使用する。
router.vuejs.org
router.vuejs.org
今回はformatExistsOr404 という名前で関数を準備した
# コンテンツが入ったJSONファイル
import formatsData from '@/assets/formats/index'
const SITE_TITLE = 'サイト名'
const NOT_FOUND_PATH = '/404'
const formatExistsOr404 = function (to, from, next) {
if (!to.params.id) {
next({name:'notFound'})
return
}
const id = to.params.id
let formatResult = formatsData.filter(
function (format) {
if (String(format.id) === String(id)) {
return true
}
}, this
)
if (!formatResult.length) {
next({name: 'notFound'})
return
}
if (!to.params.cardId) {
next()
return
}
const cardId = to.params.cardId
let cardResult = formatResult[0].cards.filter(
function (card) {
if (String(card.id) === String(cardId)) {
return true
}
}, this
)
if (!cardResult.length) {
next({name: 'notFound'})
return
}
next()
}
const router = new Router({
mode: 'history',
routes: [
{
path: '/format/:id/edit/:cardId',
component: formatEdit,
props: true,
name: 'formatEditCard',
beforeEnter: formatExistsOr404,
meta: {
title: '書式編集 - ' + SITE_TITLE
}
},
まだ書き慣れてなくて汚いけど、toのparamsに含まれる変数を見て、コンテンツがあったらnext()し、無かったらnext({name: 'notFound'})で404ページに飛ばす処理を書けばいい。
今回はfilterでやったけど、Firestoreに入れている場合など状況によって書き換える必要があると思う。
これで存在しないidが指定されたときは404のURLに変わり、専用のコンポーネントが表示されるようになった。
Firebase HostingにWebpackでつくった404.htmlデプロイ
上記のPWAテンプレートではindex.html以外のHTMLファイルがなく、手で404.htmlファイルを作っただけではnpm run buildでdist/には書き出されない。
また置いたとしてもビルドのたびに消されてしまうので、設定が必要になる。
app\build\webpack.prod.conf.js
plugins: [
new HtmlWebpackPlugin({
filename: process.env.NODE_ENV === 'testing' ?
'index.html' : config.build.index,
template: 'index.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
},
chunksSortMode: 'dependency',
serviceWorkerLoader: `<script>${loadMinified(path.join(__dirname,
'./service-worker-prod.js'))}</script>`
}),
new HtmlWebpackPlugin({
filename: "404.html",
template: "404.html",
inject: false,
}),
こんな感じでシンプルにappディレクトリ直下の404.htmlを追加しただけ。
404はエラー時などhtml単体で動くよう、VueやCSSを使用しないのでinject: falseとした。
他の設定はreadme.mdにある
github.com
これでbuildするとdist以下に書き出されるので、Firebase Hostingにデプロイ( firebase deploy )する。
そうすると/404.htmlで表示出来て、ステータスコードも404になる。
Vue側の404用ページコンポーネント、Firebase Hostingで404.htmlと、404用のファイルが二つあるのが若干気持ち悪いけど、FHで404.htmlは必須だし、vue-routerでそれを読み込ませるのは難しいのであきらめた。
Service Workerで404ページをprecacheするのを除外する
PWAのデフォ設定だと、すべての.htmlをキャッシュしようとするが、404.htmlは当然404を返すのでエラーになってしまう。
chromeのconsoleだと下記のようなエラーがでる
service-worker.js:1 Uncaught (in promise) Error: Request for https://{project_id}.firebaseapp.com/404.html?_sw-precache=99120a382671cea98d8e49ff29caea44 returned a response with status 404
at service-worker.js:1
(anonymous) @ service-worker.js:1
そのため404がパスに含まれる場合、precacheの対象から外すことにした。
SWPrecacheWebpackPluginの設定を追加する
app\build\webpack.prod.conf.js
// 省略
// service worker caching
new SWPrecacheWebpackPlugin({
cacheId: 'shanyang20180531-03',
filename: 'service-worker.js',
staticFileGlobs: ['dist/index.html', 'dist/**/*.{js,css,png,jpg,svg}'],
minify: true,
stripPrefix: 'dist/'
})]
]
})
www.npmjs.com
最初staticFileGlobsIgnorePatternsに404.htmlを渡したが、いろいろ試しても除外されないのであきらめてstaticFileGlobsでindex.htmlだけ指定した。
実際に消えているかは書き出されたdist/service-worker.jsでファイル名検索かければわかる。
htmlが他にもある場合は正規表現の調整が必要なると思うけど今回はとりあえず除外できたのでおk。
/404へのサーバーリクエストは404.htmlにリダイレクト
今回、vue-routerではなく直接サーバーに/404をリクエストした場合、Vueで受けてしまうとindex.htmlを返し、ステータスコードは200になってしまう。
そのためHistory APIではなく、サーバーリクエストの場合、Hosting側の404.htmlにリダイレクトさせてしまうことにした。
これはfirebase.jsonで指定する
{
"hosting": {
"public": "dist",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"redirects": [{
"source": "/404",
"destination": "/404.html",
"type": 301
}],
"rewrites": [{
"source": "**",
"destination": "/index.html"
}]
}
}
今のところ404でやってみたのはこれぐらい。