この記事はパーソルキャリア Advent Calendar 2018の2日目です。
Nuxt.jsを使ってPWAを作る際にCDNを使おうとしてハマったのでそのことについて書いていきます。
この記事の内容
- Nuxt.jsにおけるCDNの利用
- Service Workerとクロスオリジン
- Opaque Responseとその取扱い
- manifest.jsonとクロスオリジン
この記事で触れないこと
- Nuxt.jsとは何か
- PWAとは何か
Nuxtについては公式ドキュメントがわかりやすいのでこちらをご参照ください。
PWAについては、Googleのコードラボや私自身が簡単にまとめたスライドをご参照いただければと思います。
tl;dr
- Service Workerからクロスオリジンのリクエストができないので、静的アセットをCDNに置いてService Workerでキャッシュする場合CDN側で
Access-Control-Allow-Origin
を設定してあげる -
manifest.json
(マニフェストファイル)は原則として同一オリジンに置く必要があるのでCDNから読まないようにする
Nuxt.jsにおけるCDNの利用
NuxtでCDNを利用する場合は、nuxt.config.js
(設定ファイル)のbuild.publicPath
に対応するCDNのURLを指定します。こうすることによって、ビルド時に.nuxt/dist/client
の内容がCDNにアップロードされます。
export default {
build: {
publicPath: 'https://cdn.nuxtjs.org'
}
}
詳しくはこちら。
※https://cdn.nuxtjs.org/
はサンプルなので、適宜ご自身の利用するものを想定して読み替えてください。
これだけだでは何ということもなく、ページを表示した際にjavascriptのファイルが普通にCDNから読まれるようになります。問題となるのはPWA化してService Workerを稼動させた場合です。
Service Workerとクロスオリジン
Nuxt.jsをPWA化したい場合、pwa-moduleを使うととても簡単です。nuxt.config.js
のmodules
プロパティに追加して、設定を少し書くだけでホーム画面への追加やService Workerによるキャッシュができるようになります。
NuxtでCDNを利用する場合は、
nuxt.config.js
(設定ファイル)のbuild.publicPath
に対応するCDNのURLを指定します。こうすることによって、ビルド時に.nuxt/dist/client
の内容がCDNにアップロードされます。
と述べましたが、pwa-module
を追加してからビルドすると、Service Workerのスクリプトが自動生成されます。npm run build
したあとにstatic/sw.js
に出力されます。
importScripts('https://cdn.nuxtjs.org/workbox.3rm34239g.js')
workbox.precaching.precacheAndRoute([
{
"url": "https://cdn.nuxtjs.org/0392146f2d6284.js",
"revision": "028b52e95a28bb08c34ad092c99422ab8f"
},
// 以下、ビルドされたファイルが続きます
], {
"cacheId": "sample",
"directoryIndex": "/",
"cleanUrls": false
})
この記事ではpreCache
について述べます。publicPath
を設定すると、対象のCDNに.nuxt/dist/client
の中身がアップロードされますが、Service WorkerのpreCache
対象のファイルとしても登録されます。
preCache
では、アプリケーションの静的アセットが最初のページ読み込み時にキャッシュされます。PWAにおけるプログレッシブ・エンハンスメント
の概念で言うと、アプリケーションの骨格であるAppShell
がキャッシュされると考えても良いでしょう。
この状態でいざページを読み込んでブラウザのコンソールを見ると下記のようなエラーが出ます(CDN側の設定を特に何もしていない場合)。
Access to fetch at 'https://cdn.nuxtjs.org/f9fbee91a383e90d0.js' from origin 'https://your.domain' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
見慣れてる人も多いかもしれませんが、「クロスオリジンのアクセスするならサーバ側でAccess-Control-Allow-Origin
指定してあげてね。opaque responseでいいならフェッチするときにmodeをno-cors
で指定してね」という旨のエラーです。
なんでPWA化した途端にCORSのエラーが出るの?
「何もしてないのにエラーが出た」まではいきませんが、「なんでダメなの?」となってしまいます。
状況を整理すると次の通りです。
- PWA化する前はエラーが出なかった
- Service Workerによる
preCache
を追加するとエラーになった
ページを見てみるとわかると思いますが、正しく表示されているはずです。これはHTMLに埋め込んでいる<script>
タグからはうまく参照できているということです。ということは、Service Workerでキャッシュしようとしたときにリクエストがエラーになっているということになります。
HTML5におけるcross-orgin
の仕様
原点に立ち返ると、このエラーは特殊でもなんでもなく、ごく普通の内容です。
- 通常、サーバ側の対応がないとクロスオリジンのリクエストは通らない
- Service Workerのリクエストがコケるのは当たり前
これらを意識すると「なんで<script src="https://cdn.nuxtjs.org/f9fbee91a383e90d0.js">
はうまくいくんだ?」という話になります。
MDNを見ると次のように記述されています。
HTML5 では、 CORS への対応が
<img>
,<video>
,<script>
など一部の HTML 要素で行われ、 crossorigin 属性 (crossOrigin プロパティ) で、要素が取得するデータに関する CORS リクエストを構成することができます。
また、こちらのサイトには次にように記述されていました。
The browser does allow us to access cross-origin resources for a subset of elements including
<script>
,<img>
,<video>
,<audio>
,<link rel="stylesheet">
,<object>
,<embed>
and<iframe>
.
というわけで、(特定の)HTMLタグで埋め込んであればOKで、Javascriptでリクエストを投げるとNGということで状況が整理できます。
試しに、ブラウザのコンソールにfetch('https://cdn.nuxtjs.org/f9fbee91a383e90d0.js')
という感じでクロスオリジンへのリクエストを打ち込んで実行してみると、うまくいかないことが確認できます。
Service Workerでクロスオリジンのリソースをキャッシュするには
実は、どのように対処すれば良いかはエラーメッセージが親切に教えてくれています。
すでに述べたとおりですが、対処方法は次の2通りあります。
- CDNからのレスポンスヘッダに
Access-Control-Allow-Origin
をつける - Javascriptでフェッチするときに
mode: 'no-cors'
を指定する
結論としては何のへんてつもないのですが、1の対応をするのが一番良いと考えます。
CDNもAmazon CloudFrontやGoogle Cloud CDN等色々あると思いますが、管理画面から設定できるかと思います(当方はAkamaiで試しました)。
結論は出たのですが、じゃあ2番目の対処方法はなんなんだよってことなので、ちょっと脇道にそれますが補足します。
Opaque Responseとその取扱い
試しにブラウザのコンソールに下記のようなクロスオリジンのリソースをフェッチするスクリプトを打ち込んでみます。
const res = await fetch('https://cdn.nuxtjs.org/f9fbee91a383e90d0.js', {mode: 'no-cors'});
こんな感じで、fetch関数の第2引数でno-cors
を指定してあげると、クロスオリジンのアクセスでもエラーが出なくなります。
ではレスポンスはどうなっているでしょうか。
console.log
でレスポンスを見てみるとこんな感じになってます。
これがOpaque Response!
- bodyがnull
- typeがopaque
- statusが0
特徴的なのはといたっところでしょうか。
Opaque Responseとはなにか
直訳すると「不透明なレスポンス」となりますが、定義を見てみましょう。
[MDN](opaque: Response for “no-cors” request to cross-origin resource.)には次のようにあります。
opaque: Response for “no-cors” request to cross-origin resource. Severely restricted.
もうちょっと掘り下げて仕様書を見ると、
An opaque filtered response is a filtered response whose type is "opaque", URL list is the empty list, status is 0, status message is the empty byte sequence, header list is empty, body is null, and trailer is empty.
とありました。
文中に出てくるfiltered response
は次の通り。
A filtered response is a limited view on a response that is not a network error. This response is referred to as the filtered response’s associated internal response.
要するに、no-cors
を指定してクロスオリジンにリクエストした結果得られるレスポンスであり、その中身はフィルタリングされていて見えないものとざっくり解釈できます。
Opaque ResponseはService Workerでキャッシュできるか?
結論としてはできます。
Workboxのドキュメントを見ると、Opaque Responseを強制的にキャッシュする方法が書かれています。
ただ、注意が必要です。
Warning: This will cache a response that could be an error, which would then never get updated!
とあるように、Opaque Responseはステータスコードが0なので成功してるか失敗してるか(正しいデータがとれてるのかどうか)わかりません。つまり、実は失敗してたレスポンスをキャッシュしてしまう可能性があります。
なので、よほどの理由がない限りOpaque Responseはキャッシュしないほうが良いと思います。
Service Workerとクロスオリジン、Opaque Responseについては下記を参考にさせていただきました。
- http://azu.github.io/slide/pixelgrid/serviceworker-cors.html
- https://fullstack-developer.academy/what-is-an-opaque-response/
manifest.jsonとクロスオリジン
無事にAppShellがキャッシュされるようになって一安心、となったのですが、また別のエラーが発生していました。
manifest.json
が読み込まれないというものです。
nuxt.config.js
のbuild.publicPath
でCDNを指定すると、manifest.json
もCDNを参照するようになります。
manifest.json
はPWAにおいてとても大事なファイルで、ホーム画面に追加する際のアイコンを指定したり、アプリ名やテーマカラー、表示モードなどの各種設定を記述します。
少し古いですがgithubのissueを見たところ、manifestファイルは同一オリジンであることが求められるようです。
(クロスオリジンから読める方法知ってるよ〜という方がいらっしゃいましたら是非ご指摘いただければと思いますmm)
pwa-module
にはmanifestモジュールが含まれていて、各種設定が行えます。少なくとも日本語ドキュメントにはほとんど情報がありませんが、実はCDNを使っていてもmanifestファイルを同一オリジンで参照できます。manifestモジュールにもpublicPath
属性が存在するので指定すればOKです。
modules: [
[
'@nuxtjs/pwa',
{
manifest: {
publicPath: '/_nuxt/',
},
},
],
],
/_nuxt/
はビルドファイルが出力されるデフォルトのパスです。
何も指定しない場合は最初に指定したbuild.publicPath
と同じものが入ります。
気になる方はソースコードをご確認ください。
まとめ
Nuxtで作ったWebアプリををPWA化してかつCDNを使うにあたって、多くの学びを得られました。
本記事が同じような構成でPWAに挑戦する方のお役に立てば幸いです。
AppShellは最初のアクセスでキャッシュされるのにわざわざCDN使うメリットはそんなにないような気もしますが、ご参考まで。