ユーザーエージェントでPCとSPで表示するページを切り替える
要件
- サーバーサイドレンダリングのレベルで切り替える。
- cssフラッシュが起きない
- 柔軟にルーティングが決められる(PCだけのページ、SPだけのページが表示可能)
デメリット
- ファイルシステムによるルーティングが使えなくなる。むしろこちらの方が良い場合がある。
- 公式でサポートされている方法でない。
イメージ図
手順
- nuxt.config.jsでmoduleの追加
- nuxt moduleの作成
- routingファイルの作成
1. nuxt.config.jsの修正
nuxt.config.js
modules: [
'~/modules/router', // これを追加
'~/modules/access-logger',
'~/modules/error-logger'
],
2. nuxt moduleの作成
何をしているかというと、サーバーサイドで作成されるnuxtのデフォルトのroutingは
.nuxt/router.jsを参照するのだけれどそれを自前のrouter.jsで置き換えるよという処理を書く。
src/modules/router.js
import { resolve } from 'path'
import { existsSync } from 'fs'
export default function(options) {
options = Object.assign({}, defaults, options)
const routerPath = resolve(this.options.srcDir, 'routes', 'router.js')
// Add ${srcDir}/router.js as the main template for routing
this.addTemplate({
fileName: 'router.js',
src: routerPath
})
}
4. routingファイルの作成
modules/router.jsでデフォルトのrouterを書き換える宣言を行ったので、
その実際の処理を書く場所。
※ nuxtのデフォルトの処理が変わる可能性があるけれど、少なくともroutingはvue routerに任せるというところが変わらなければそれほど問題にならないはず。
src/routes/router.js
import MobileDetect from 'mobile-detect'
import pcRoutes from '~/routes/pc-route.js'
import spRoutes from '~/routes/sp-route.js'
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
const isMobile = ssrContext => {
// ssrContext がない場合はクライアントの遷移なので window.navigator を見る
const ua = ssrContext
? ssrContext.req.headers['user-agent']
: window.navigator.userAgent
const md = new MobileDetect(ua)
return md.mobile()
}
if (process.client) {
window.history.scrollRestoration = 'auto'
}
const scrollBehavior = function(to, from, savedPosition) {
// if the returned position is falsy or an empty object,
// will retain current scroll position.
let position = false
// if no children detected
if (to.matched.length < 2) {
// scroll to the top of the page
position = { x: 0, y: 0 }
} else if (to.matched.some(r => r.components.default.options.scrollToTop)) {
// if one of the children has scrollToTop option set to true
position = { x: 0, y: 0 }
}
// savedPosition is only available for popstate navigations (back button)
if (savedPosition) {
position = savedPosition
}
return new Promise(resolve => {
// wait for the out transition to complete (if necessary)
window.$nuxt.$once('triggerScroll', () => {
// coords will be used if no selector is provided,
// or if the selector didn't match any element.
if (to.hash) {
let hash = to.hash
// CSS.escape() is not supported with IE and Edge.
if (
typeof window.CSS !== 'undefined' &&
typeof window.CSS.escape !== 'undefined'
) {
hash = '#' + window.CSS.escape(hash.substr(1))
}
try {
if (document.querySelector(hash)) {
// scroll to anchor by returning the selector
position = { selector: hash }
}
} catch (e) {
console.warn(
'Failed to save scroll position. Please add CSS.escape() polyfill (https://github.com/mathiasbynens/CSS.escape).'
)
}
}
resolve(position)
})
})
}
export function createRouter(ssrContext) {
// routingをリクエストの情報によって変更したい場合はこの場所で行う
let routes = []
if (isMobile(ssrContext)) {
routes = spRoutes
} else {
routes = pcRoutes
}
return new Router({
mode: 'history',
base: '/',
linkActiveClass: 'nuxt-link-active',
linkExactActiveClass: 'nuxt-link-exact-active',
scrollBehavior,
routes,
fallback: false
})
}
sp-route.js
import Pref from '~/pages/sp/pref.vue'
import Index from '~/pages/sp/index.vue'
const routes = [
{
path: '/',
component: Index,
name: 'index'
},
{
path: '/:pref_name',
component: Pref,
name: 'pref'
}
]
export default routes
pc-router.js
const routes = [
{
path: '/',
component: Index,
name: 'index'
},
{
path: '/:pref_name',
component: Pref,
name: 'pref'
}
]
export default routes
sp-routerとpc-routerでnameが被っているけれど、
そこは以下の分岐で担保する。
let routes = []
if (isMobile(ssrContext)) {
routes = spRoutes
} else {
routes = pcRoutes
}
なぜこれが成り立つのかは、
.nuxt/index.jsを見るとわかると思います。参考までに記載します。
.nuxt/index.js
async function createApp(ssrContext) {
const router = await createRouter(ssrContext)
const store = createStore(ssrContext)
// Add this.$router into store actions/mutations
store.$router = router
// Fix SSR caveat https://github.com/nuxt/nuxt.js/issues/3757#issuecomment-414689141
const registerModule = store.registerModule
store.registerModule = (path, rawModule, options) => registerModule.call(store, path, rawModule, Object.assign({ preserveState: process.client }, options))
// Create Root instance
// here we inject the router and store to all child components,
// making them available everywhere as `this.$router` and `this.$store`.
const app = {
router,
Tips
nuxtの処理の機構を考えると、cssはすべでscopedで実装した方がいいです。
というのも、webpackでbundleする際にすべてのpcとspのvueファイルをバンドルするので、名前が同じだとスタイル崩れます。
色々やってみて思ったのだけれど
nuxtでpcとspで対応するのはレスポンシブが良い。
需要あれば、pcとspで対応する際にもっとハマりどころがあったので、記事書きます