1. Nuxtを使ったサービス構築 ~PCとSPの切り替えを上手く行う方法~

## ユーザーエージェントでPCとSPで表示するページを切り替える

## 要件
- サーバーサイドレンダリングのレベルで切り替える。
- cssフラッシュが起きない
- 柔軟にルーティングが決められる(PCだけのページ、SPだけのページが表示可能) 

## デメリット
- ファイルシステムによるルーティングが使えなくなる。むしろこちらの方が良い場合がある。
- 公式でサポートされている方法でない。

### イメージ図


![nuxt.png](https://qiita-image-store.s3.amazonaws.com/0/96323/2d436720-5cfd-8cf2-c746-20a83f86e0e7.png)


## 手順

1. nuxt.config.jsでmoduleの追加
2. nuxt moduleの作成
3. routingファイルの作成

## 1. nuxt.config.jsの修正

```js:nuxt.config.js
  modules: [
    '~/modules/router', // これを追加
    '~/modules/access-logger',
    '~/modules/error-logger'
  ],

```

## 2. nuxt moduleの作成

何をしているかというと、サーバーサイドで作成されるnuxtのデフォルトのroutingは
.nuxt/router.jsを参照するのだけれどそれを自前のrouter.jsで置き換えるよという処理を書く。

```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に任せるというところが変わらなければそれほど問題にならないはず。


```js: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
  })
}
```

```js: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
```
```js: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が被っているけれど、
そこは以下の分岐で担保する。

```js
  let routes = []
  if (isMobile(ssrContext)) {
    routes = spRoutes
  } else {
    routes = pcRoutes
  }
```


なぜこれが成り立つのかは、
.nuxt/index.jsを見るとわかると思います。参考までに記載します。

```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で対応する際にもっとハマりどころがあったので、記事書きます