vue.js 2.0 で自社プロダクトを spa + ssr 化した話
TRANSCRIPT
![Page 1: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/1.jpg)
初夏の JavaScript 祭 in mixi
Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話
![Page 2: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/2.jpg)
Yutaro Miyazaki (@vwxyutarooo)
ニート ↓
フリーランス (Web 制作) ↓
アプリ屋の Web (フロントエンド)
![Page 3: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/3.jpg)
![Page 4: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/4.jpg)
今日話すこと
導入してみてどうだった? ってとこ
地味に困ったこと
![Page 5: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/5.jpg)
今日話さないこと
Vue.js SSR のしくみ、ロジックなど
他フレームワークとの比較
![Page 6: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/6.jpg)
サービスの概要
マンガ無料配信サービス
アプリを主軸に展開しているサービス
Web でもコンテンツを活かそう
![Page 7: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/7.jpg)
![Page 8: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/8.jpg)
リニューアルと導入の背景
Web 経験者無しで v1 を作ってしまった
イケてない
![Page 9: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/9.jpg)
Web でももっとこう
アプリっぽい体験できないですかね
![Page 10: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/10.jpg)
v1: Riot でページ毎にマウント
→ SPA
クライアントレンダリング
→ SSR (SEO ほんとにいいのか?)
Vue.js の評判がいい
どっかの調査で満足度1位
![Page 11: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/11.jpg)
構成 (全体)
![Page 12: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/12.jpg)
構成 (Web)
![Page 13: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/13.jpg)
前提知識
![Page 14: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/14.jpg)
vuejs/vue‑hackernews‑2.0
公式が作る SPA + SSR プロジェクト
https://github.com/vuejs/vue‑hackernews‑2.0
![Page 16: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/16.jpg)
地味に悩んだポイント集
![Page 17: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/17.jpg)
SSR は誰がやる?
404 ハンドリング
デバイス切り替えってどうする?
共通処理どうする?
メタタグの管理めんどいやりたくない
Analytics どうしよう?
広告
メモリリーク
![Page 18: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/18.jpg)
Q: SSR は誰がやる?
![Page 19: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/19.jpg)
バックエンドからも JS が起動できる
go
go‑duktape
goja + goja‑node
![Page 20: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/20.jpg)
素直に Express から起動することに
![Page 21: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/21.jpg)
Q: 404 ハンドリング
![Page 22: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/22.jpg)
vue‑hackernews 2 では
ルータ設定にマッチするページが見つからなければ
Express が 404 返すようになってる
![Page 23: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/23.jpg)
if (err && err.code === 404) { res.status(404).end('404 | Page Not Found') }
![Page 24: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/24.jpg)
404 ページのデザイン欲しい
// router.js [ { path: '/', name: 'top', component: top }, ... { path: '*', name: 'not-found', component: notFound } ]
![Page 25: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/25.jpg)
// server.js const termRoute = (context.state.route.name === 'not-found');
if (termRoute) res.status(404);
![Page 26: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/26.jpg)
ルータにマッチするけど 404 の時は?
// router.js { path: '/comics/tag/:id', name: 'tag-archive', component: tagArchive },
![Page 27: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/27.jpg)
API リクエスト時に、メインクエリを設定
// preFetch const options = { isMainQuery: (key === mainQueryKey) }
![Page 28: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/28.jpg)
メインクエリの API レスポンスが
200 じゃなかったら state にエラーをセット
// action.js if (result.status === 200) { commit(mutation, { key, result: result.data }); } else if (options.isMainQuery) { commit(types.SET_STATUS, { key: type, value: {} }); commit(types.SET_STATUS, { key: 'error', value: result.status // 404 }); }
![Page 29: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/29.jpg)
Express サーバで、コンテキストを通じて
state のエラーからステータスを打つ
// server.js const termState = (context.state.error); // 404 | 50x
if (termState) res.status(context.state.error);
![Page 30: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/30.jpg)
ちょっとイケてないけど
対象 View コンポーネント内で not found を表示させた
<div :key="`tag-archives-${id}-${currentPage}`"> <div v-if="isLoading" class="l-root"> <screen-spinner></screen-spinner> </div> <content-not-found v-else-if="status === 404"></content-not-found> <template v-else="v-else"> ... </template> </div>
![Page 31: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/31.jpg)
Q: デバイス切り替えってどうする?
![Page 32: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/32.jpg)
PC/SP 用エントリーポイントをそれぞれ用意
// webpack.config.client.js entry: { 'polyfills': [path.join(..., 'app/entry/polyfills.js')], 'vendor': [path.join(..., 'app/entry/vendor.js')], 'app.pc': [path.join(..., 'app/entry/pc/client-entry.js')], 'app.sp': [path.join(..., 'app/entry/sp/client-entry.js')] },
// webpack.config.server.js entry: { 'server-bundle.pc': path.join(..., 'app/entry/pc/server-entry.js'), 'server-bundle.sp': path.join(..., 'app/entry/sp/server-entry.js') }
![Page 33: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/33.jpg)
テンプレートも2つ
// webpack.config.client.js new HTMLPlugin({ template: path.join(..., 'templates/pc.html'), filename: 'index.pc.html', excludeChunks: ['app.sp'] }), new HTMLPlugin({ template: path.join(..., 'templates/sp.html'), filename: 'index.sp.html', excludeChunks: ['app.pc'] }),
![Page 34: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/34.jpg)
createRenderer でレンダラを2つ作成
// server.js const bundle = { pc: fs.readFileSync(resolve('./dist/js/server-bundle.pc.js'), 'utf-8'), sp: fs.readFileSync(resolve('./dist/js/server-bundle.sp.js'), 'utf-8') } const template = { pc: fs.readFileSync(resolve('./dist/index.pc.html'), 'utf-8'), sp: fs.readFileSync(resolve('./dist/index.sp.html'), 'utf-8') } renderer = { pc: createRenderer(bundle.pc, template.pc), sp: createRenderer(bundle.sp, template.sp) };
![Page 35: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/35.jpg)
Express で UA 判定して起動するレンダラを切り替え
// server.js const useragent = require('express-useragent'); ... app.use(useragent.express()); app.get('*', (req, res) => { ... const device = (req.useragent.isMobile) ? 'sp' : 'pc'; ... renderer[device].renderToStream(context)... }
![Page 36: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/36.jpg)
2.3.0 から createRenderer
にバンドル突っ込むのは非推奨に...
![Page 37: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/37.jpg)
別の方法を考え中
![Page 38: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/38.jpg)
Server バンドルはエントリーポイント分けず
context にデバイス情報渡して切り替えるのもありか?
![Page 39: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/39.jpg)
Q: 共通処理どうする?
![Page 40: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/40.jpg)
vuejs/vue-class-component
もともと TypeScript で書けるようにするため
Class でコンポーネントを定義できる
継承は非対応だが、Decorator と組み合わせる
![Page 41: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/41.jpg)
import Vue from 'vue' import Component from 'vue-class-component'
@Component({ props: { propMessage: String } }) export default class App extends Vue { // initial data msg = 123
// use prop values for initial data helloMsg = 'Hello, ' + this.propMessage
// lifecycle hook mounted () { this.greet() } ... }
![Page 42: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/42.jpg)
import { createDecorator } from 'vue-class-component';
export const Options = createDecorator((options) => { Object.assign(options, { ... watch: { // call again the method if the route changes '$route': 'routeUpdated' } }; });
![Page 43: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/43.jpg)
使い方は君しだい!
![Page 44: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/44.jpg)
Q: メタタグの管理めんどいやりたくない
![Page 45: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/45.jpg)
declandewet/vue-meta
![Page 46: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/46.jpg)
export default { name: 'App', metaInfo: { title: METAINFO.title, titleTemplate: METAINFO.titleTemplate, meta: [ { vmid: 'og:title', name: 'og:title', content: METAINFO.title }, { vmid: 'description', name: 'description', content: METAINFO.description } ... ] } }
![Page 47: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/47.jpg)
コンポーネントの深いやつが勝つ
全コンポーネント検査してるからパフォーマンスは疑問
SSR 対応 (Vue の公式にも例あり)
![Page 48: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/48.jpg)
Q: Analytics どうしよう?
![Page 49: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/49.jpg)
Web Analytics: MatteoGabriele/vue-analytics
App Analytics: ScreamZ/vue-analytics
![Page 50: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/50.jpg)
router とくっつけて自動で PV | SV 送れる
![Page 51: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/51.jpg)
Q: 広告
![Page 52: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/52.jpg)
Google Adsense は SPA 非対応
![Page 53: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/53.jpg)
ページのリフレッシュ無しに広告を打ち直すことは禁止
imp が絶望的
Google Adsense 以外の SSP 等広告運用が必須
![Page 54: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/54.jpg)
Q: メモリリーク
![Page 55: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/55.jpg)
起こしてた
![Page 56: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/56.jpg)
1日でメモリを食い尽くし
ガベージコレクション走りまくり、CPU 回りまくり
API キャッシュ周りが原因
最新の Vuehackernews では直ってる
![Page 57: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/57.jpg)
所感
![Page 58: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/58.jpg)
SSR の実装はそれほど大変ではないのかも
普通に SPA を作る + ちょっとの手間でいい
メタタグとかアプリケーション側で扱いたいからついでに
SSR しちゃってたり
![Page 59: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/59.jpg)
まあまあ安定稼働もする
CPU はそこそこ回るためページキャッシュを併用
コンポーネントキャッシュは考えて設計すべし
状態変化によるケースや slot が多いと効果的ではないかも?
![Page 60: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/60.jpg)
KPI 的には
PV/セッション上がり
滞在時間は平行
回遊しやすくなってるって思いたい
なんか下がることはなかった
![Page 61: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/61.jpg)
まとめ
あり
SEO 対策としてだけやるなら要らない
メタタグさえサーバ側で作れていれば
堅いこと言わずに作ってみようぜ
![Page 62: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/62.jpg)
Vue.js 楽しいな! おい!!
![Page 63: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/63.jpg)
![Page 64: Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話](https://reader034.vdocuments.site/reader034/viewer/2022052116/5a6478af7f8b9a2c568b4623/html5/thumbnails/64.jpg)
ありがとうございました!