概要
Web アプリケーションでオフライン対応する場合は Service Worker を導入するのが良いでしょう。Angular では @angular/pwa
を追加することで簡単に実現できますが、Scully と組み合わせる場合、期待した動作とならないことが多いため Workbox を使うことをお勧めします。
Angular Service Worker での問題
まずは @angular/pwa
を追加してみましょう。
ng add @angular/pwa
ngsw-config.json
で Scully が出力するファイルもキャッシュするように設定します。
{
...,
"assetGroups": [
...,
{
"name": "blog",
"installMode": "lazy",
"updateMode": "prefetch",
"resources": {
"files": ["/blog/**/*.html"]
}
}
]
}
Scully 実行後に index.html
のハッシュ値が変わり Service Worker で正常にキャッシュされなくるため、忘れずに再生成しましょう(Issue 参照)。
{
"scripts": {
...,
"pwa:regenerate": "rm dist/static/ngsw.json;node_modules/.bin/ngsw-config dist/static ./ngsw-config.json"
},
npm run build -- --prod
npm run scully
npm run pwa:regenerate
早速起動してみましょう。一見すると正常にキャッシュできているようです。
では次に記事詳細に移動してからページをリロードしてみましょう。
一瞬ですが、記事一覧が表示されます。
末尾スラッシュ /
を含む URL をリロードした時にキャッシュの読み込み先が /index.html
とならないため、Scully と Angular Service Worker の組み合わせは避けるか、今後の修正を待った方が良いかもしれません。
Workbox による PWA 化
代替案として Service Worker の実装を Workbox に変更することをお勧めします。
まず前述の Issue を参考に URL のシリアライズ処理を末尾スラッシュ対応に上書きします。
import { DefaultUrlSerializer, UrlTree } from '@angular/router';
/**
* @see https://github.com/angular/angular/issues/16051
*/
export class TrailingSlashUrlSerializer extends DefaultUrlSerializer {
private static _withTrailingSlash(url: string): string {
const splitOn = url.indexOf('?') > -1 ? '?' : '#';
const pathArr = url.split(splitOn);
if (!pathArr[0].endsWith('/')) {
const fileName: string = url.substring(url.lastIndexOf('/') + 1);
if (fileName.indexOf('.') === -1) {
pathArr[0] += '/';
}
}
const result = pathArr.join(splitOn);
return result;
}
serialize(tree: UrlTree): string {
return TrailingSlashUrlSerializer._withTrailingSlash(super.serialize(tree));
}
}
app.module.ts
に追加しましょう。
import { UrlSerializer } from '@angular/router';
@NgModule({
declarations: [AppComponent],
imports: [...],
providers:[
{ provide: UrlSerializer, useClass: TrailingSlashUrlSerializer }
],
bootstrap: [AppComponent],
})
export class AppModule {}
Scully の設定に seoHrefOptimize
追加します。
import { ScullyConfig, setPluginConfig } from '@scullyio/scully';
export const config: ScullyConfig = {
projectRoot: './src',
projectName: 'website',
outDir: './dist/static',
routes: {
'/blog/:slug': {
type: 'contentFolder',
slug: {
folder: './blog',
},
},
},
defaultPostRenderers: ['seoHrefOptimise'],
};
Router を使わない場合は Angular を無効化してしまっても良いかもしれません。
import { ScullyConfig, setPluginConfig } from '@scullyio/scully';
import { DisableAngular } from 'scully-plugin-disable-angular';
setPluginConfig(DisableAngular, 'render', { removeState: true });
export const config: ScullyConfig = {
projectRoot: './src',
projectName: 'website',
outDir: './dist/static',
routes: {
'/blog/:slug': {
type: 'contentFolder',
slug: {
folder: './posts',
},
},
},
defaultPostRenderers: ['seoHrefOptimise', DisableAngular],
};
次にプロジェクトのルートディレクトリに workbox-config.js
を作成します。
module.exports = {
globDirectory: 'dist/static',
globPatterns: [
'**/*.{js,LICENSE,txt,png,svg,jpg,webp,ico,html,css,json,webmanifest}',
],
swDest: 'dist/static/service-worker.js',
};
最後に index.html
の <head></head>
内に次のスクリプトを追加しましょう。レンダリングを阻害しないよう defer
属性をつけるのをお勧めします。
<script defer>
(function () {
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('/service-worker.js');
});
}
})();
</script>
これで準備完了です。
あとはデプロイ前にアプリケーションのビルドと Scully によるプリレンダリング、Service Worker 用スクリプトの生成を実行すれば Scully を使いつつ PWA として公開できます。
npm run build -- --prod --statsJson
npm run scully
npx workbox-cli generateSW workbox-config.js
まとめ
現状では Scully と Angular Service Worker を組み合わせるのは難しいようです。
しかし、Workbox を使うことでも PWA 化は可能ですので興味のある方は是非試してみましょう。