vue.js 2.0 で自社プロダクトを spa + ssr 化した話
TRANSCRIPT
初夏の JavaScript 祭 in mixi
Vue.js 2.0 で自社プロダクトを SPA + SSR 化した話
Yutaro Miyazaki (@vwxyutarooo)
ニート ↓
フリーランス (Web 制作) ↓
アプリ屋の Web (フロントエンド)
今日話すこと
導入してみてどうだった? ってとこ
地味に困ったこと
今日話さないこと
Vue.js SSR のしくみ、ロジックなど
他フレームワークとの比較
サービスの概要
マンガ無料配信サービス
アプリを主軸に展開しているサービス
Web でもコンテンツを活かそう
リニューアルと導入の背景
Web 経験者無しで v1 を作ってしまった
イケてない
Web でももっとこう
アプリっぽい体験できないですかね
v1: Riot でページ毎にマウント
→ SPA
クライアントレンダリング
→ SSR (SEO ほんとにいいのか?)
Vue.js の評判がいい
どっかの調査で満足度1位
構成 (全体)
構成 (Web)
前提知識
vuejs/vue‑hackernews‑2.0
公式が作る SPA + SSR プロジェクト
https://github.com/vuejs/vue‑hackernews‑2.0
地味に悩んだポイント集
SSR は誰がやる?
404 ハンドリング
デバイス切り替えってどうする?
共通処理どうする?
メタタグの管理めんどいやりたくない
Analytics どうしよう?
広告
メモリリーク
Q: SSR は誰がやる?
バックエンドからも JS が起動できる
go
go‑duktape
goja + goja‑node
素直に Express から起動することに
Q: 404 ハンドリング
vue‑hackernews 2 では
ルータ設定にマッチするページが見つからなければ
Express が 404 返すようになってる
if (err && err.code === 404) { res.status(404).end('404 | Page Not Found') }
404 ページのデザイン欲しい
// router.js [ { path: '/', name: 'top', component: top }, ... { path: '*', name: 'not-found', component: notFound } ]
// server.js const termRoute = (context.state.route.name === 'not-found');
if (termRoute) res.status(404);
ルータにマッチするけど 404 の時は?
// router.js { path: '/comics/tag/:id', name: 'tag-archive', component: tagArchive },
API リクエスト時に、メインクエリを設定
// preFetch const options = { isMainQuery: (key === mainQueryKey) }
メインクエリの 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 }); }
Express サーバで、コンテキストを通じて
state のエラーからステータスを打つ
// server.js const termState = (context.state.error); // 404 | 50x
if (termState) res.status(context.state.error);
ちょっとイケてないけど
対象 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>
Q: デバイス切り替えってどうする?
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') }
テンプレートも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'] }),
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) };
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)... }
2.3.0 から createRenderer
にバンドル突っ込むのは非推奨に...
別の方法を考え中
Server バンドルはエントリーポイント分けず
context にデバイス情報渡して切り替えるのもありか?
Q: 共通処理どうする?
vuejs/vue-class-component
もともと TypeScript で書けるようにするため
Class でコンポーネントを定義できる
継承は非対応だが、Decorator と組み合わせる
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() } ... }
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' } }; });
使い方は君しだい!
Q: メタタグの管理めんどいやりたくない
declandewet/vue-meta
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 } ... ] } }
コンポーネントの深いやつが勝つ
全コンポーネント検査してるからパフォーマンスは疑問
SSR 対応 (Vue の公式にも例あり)
Q: Analytics どうしよう?
Web Analytics: MatteoGabriele/vue-analytics
App Analytics: ScreamZ/vue-analytics
router とくっつけて自動で PV | SV 送れる
Q: 広告
Google Adsense は SPA 非対応
ページのリフレッシュ無しに広告を打ち直すことは禁止
imp が絶望的
Google Adsense 以外の SSP 等広告運用が必須
Q: メモリリーク
起こしてた
1日でメモリを食い尽くし
ガベージコレクション走りまくり、CPU 回りまくり
API キャッシュ周りが原因
最新の Vuehackernews では直ってる
所感
SSR の実装はそれほど大変ではないのかも
普通に SPA を作る + ちょっとの手間でいい
メタタグとかアプリケーション側で扱いたいからついでに
SSR しちゃってたり
まあまあ安定稼働もする
CPU はそこそこ回るためページキャッシュを併用
コンポーネントキャッシュは考えて設計すべし
状態変化によるケースや slot が多いと効果的ではないかも?
KPI 的には
PV/セッション上がり
滞在時間は平行
回遊しやすくなってるって思いたい
なんか下がることはなかった
まとめ
あり
SEO 対策としてだけやるなら要らない
メタタグさえサーバ側で作れていれば
堅いこと言わずに作ってみようぜ
Vue.js 楽しいな! おい!!
ありがとうございました!