LoginSignup
7
3

More than 3 years have passed since last update.

Nuxt + SSR + Vuex ORMの闇

Last updated at Posted at 2020-02-13

Vuex ORMの闇を紹介しようと思ったが、v0.35.0で治っていた。と思ったら別の闇に遭遇。そして、Vuex ORM graphql pluginの闇にも遭遇。

まとめ

  • Nuxt + Vuex ORM graphql pluginをそのままSSRで使うのは難しい -> 後述の解決策1 or 4
  • Nuxt + Vuex ORMをSSRで使うのは可能 -> 詳細は後述

俺氏の選択は、解決策1 + シングルトンVuex ORM + Vuex ORM graphql plugin
Vuex ORM graphql pluginをシングルトンじゃないように修正するのは辛すぎる。

Vuex ORM graphql pluginの闇

これはVuex ORMのSSR対応版(v0.35.0)に対応していない気がする。
少なくとも正しく動くのかかんたんには検証できない。

結論、Vuex ORM graphql pluginを使いたいなら、後述の解決策1を使うしかない。
パフォーマンスが問題になったら、解決策1を解決策4に昇華させるルートもある。

Vuex ORMの別の闇

以下の問題に遭遇。v0.34.1に戻せば良いと書いてあるが、後述のようにv0.35.0の差分を使いたいので戻せない。

上で言及されている、以下のコミットで治る。

Vuex ORMの正しい使い方

以下に書いてあるように、SSRの場合は、 this.$store.$db().model('users') の形式で使うと良い。2019年12月に報告があり、2020年1月のv0.35.0でこの差分が入っている。

参考資料

今まで普通にグローバルに使ってて気付かなかった。
ぎりぎり動いていたんだろう。

インターフェースの変更で修正量が多いので、一旦は後述の解決策1などでも良いかもしれない。

どこにも言及されていないが、初期化はvuex pluginでstore初期化のたびにデータベースを作るようにしないといけないと思う。シングルトンの名残がまだ残っているから、複雑。

Vuex ORMの闇

サーバーサイドレンダリングで、asyncData(nuxtのやつ)で、Vuex ORMをいじると、リクエスト間でVuexのstateが混じる可能性がある。

集めたファクト

  1. Vuex ORMではコンテクスト管理にシングルトンが使われている
  2. 本番のnuxtでは、runInNewContextがfalseになるので、リクエスト間でコンテクストが共有される
  3. nuxt、connect(nuxtで使われているライブラリ)、vue-server-rendererでは、直列化のコードは無い
  4. nuxtのasyncDataでログ出力させたら、リクエストが返るまでにVuex ORMが参照するstoreが置き換わっていることを確認

詳細

1. Vuex ORMではコンテクスト管理にシングルトンが使われている

Vuex ORMの実装の中では、storeに以下のようにアクセスできる。
同時に2つのstoreを扱えないことを示唆している。

Container.database.store

また、以下のようなVuex ORMのインターフェースから考えても、コンテクストを渡せないので、インターフェースを変えない限り、同時に2つのstoreを扱うのは難しい。

User.create({
  data: { id: 1, name: 'John' }
})

2. 本番のnuxtでは、runInNewContextがfalseになるので、リクエスト間でコンテクストが共有される

nuxtのコードを読んだが、runInNewContextがfalseになる。

3. nuxt、connect(nuxtで使われているライブラリ)、vue-server-rendererでは、直列化のコードは無い

それぞれのコードを読んだが、直列化しているコードはない。

以下、上から順に呼ばれる

  1. https://github.com/nuxt/nuxt.js/blob/e8aca9eb117851047e82e94948ff8b4bcb464b1a/packages/server/src/server.js
  2. https://github.com/nuxt/nuxt.js/blob/e8aca9eb117851047e82e94948ff8b4bcb464b1a/packages/server/src/middleware/nuxt.js
  3. https://github.com/nuxt/nuxt.js/blob/e8aca9eb117851047e82e94948ff8b4bcb464b1a/packages/vue-renderer/src/renderer.js
  4. https://github.com/nuxt/nuxt.js/blob/e8aca9eb117851047e82e94948ff8b4bcb464b1a/packages/vue-renderer/src/renderers/ssr.js
  5. https://github.com/vuejs/vue/blob/6390f70c2e4ec7e51cce1d2a4ba35e2d0d328205/packages/vue-server-renderer/build.dev.js

4. nuxtのasyncDataでhttpリクエストのところでログ出力させたら、リクエストが返るまでにVuex ORMが参照するstoreが置き換わっていることを確認

確認方法は、Vuex ORM graphql pluginでhttpリクエストのところに以下のようなコードを仕込んで確認。「同じリクエストIDである、ならば、同じstoreアドレスである」成立すれば問題ないが、

結果、成立しなかった。abコマンドなどで並行リクエストを送ると、store == store2 != store3となってしまうことがあった。

let seq = 1
const addressToId = new WeakMap()

async asyncData({ store }) {
    const reqId = seq++
    const store2 = Project.store()
    if (!addressToId.get(store)) addressToId.set(store, seq++)
    if (!addressToId.get(store2)) addressToId.set(store2, seq++)
    console.log('request started ' + reqId + ' ' + addressToId.get(store) + ' ' + addressToId.get(store2))

    // await http request

    const store3 = Project.store()
    if (!addressToId.get(store3)) addressToId.set(store3, seq++)
    console.log('request finished ' + reqId+ ' ' + addressToId.get(store) + ' ' + addressToId.get(store3))
  },

解決策1

nuxtのモジュールで、vueのrenderの前後にフック(幸い、vueの実装ではawaitされている)を仕掛けて、直列実行させる。

並行実行されなくなるので、サーバーのスループットが犠牲になるが、nuxtなら一人のユーザーが一度に行うSSRリクエストは一回だけなので、ユーザーから見たパフォーマンスはあまり劣化しないはず。

以下のフックを使う。

nuxt module例 (moduleの使い方は https://ja.nuxtjs.org/guide/modules/ )
サーバーサイドのみなのでWeakMapを使ってみた。


let seq = 1
let lockPromise
const auxData = new WeakMap()

const lock = async (data) => {
  while (lockPromise) {
    console.log('serial-render waiting')
    await lockPromise
  }
  lockPromise = new Promise((resolve) => {
    data.lockPromiseResolve = resolve
  })
}

const unlock = (data) => {
  if (data.lockPromiseResolve) {
    lockPromise = void 0
    const resolve = data.lockPromiseResolve
    data.lockPromiseResolve = void 0
    resolve()
  }
}

export default function () {
  this.nuxt.hook('vue-renderer:ssr:prepareContext', async ({ req }) => {
    const data = { id: seq++ }
    auxData.set(req, data)
    await lock(data)
    console.log('serial-render start ' + data.id)
  })

  this.nuxt.hook('vue-renderer:ssr:context', async ({ req }) => {
    const data = auxData.get(req)
    console.log('serial-render finished ' + data.id)
    unlock(data)
  })

  this.nuxt.hook('render:errorMiddleware', async (connectInstance) => {
    connectInstance.use((err, req, res, next) => {
      const data = auxData.get(req)
      if (data) {
        console.log('serial-render error ' + data.id)
        unlock(data)
      }
      next(err)
    })
  })
}

懸念: ↑のフックの間でthrowすることは無いか? -> asyncDataとかでthrowしたらthrowした。そして、vue-renderer:ssr:contextは呼ばれなかった。

throwしたら以下に行く

そしてnextで以下に行く。ここで独自の例外補足用のエラーmiddlewareを入れておけば良い。

解決策2

vue-server-renderのrunInNewContextに'fork'モードを追加する。

railsのunicornやpumaみたいに、ライブラリを読み込んだあとのプロセスを用意しておいて、そこからforkでサーバープロセスを作るパターンを使う。

linux上のnodeでnative moduleでforkが使えることは過去に確認したことがあるが、OSに依存するので不安定かもしれない。

解決策3

解決策2と似ているがZone.jsを使う。thread local storage的なものらしい。
nuxtのモジュールを作るのと、Vuex ORMの実装を変更する必要があるが、Vuex ORMのインターフェースは変更する必要がないのがメリット。いろいろ関係ないところまで影響しそうなのがデメリット。

解決策4

解決策1の進化版。

vue-server-renderのrunInNewContextに'pool'モードを追加する。
contextプールを用意して空いているものに投げる。すでにtrueと'once'が実現できているから、実現できるはず。

https://github.com/vuejs/vue/blob/52719ccab8fccffbdf497b96d3731dc86f04c1ce/src/server/bundle-renderer/create-bundle-runner.js#L107
このrunnerを複数にして、直列実行にすれば良い。

メモリ消費量が問題になるかもしれないけど。

解決策5

かんたんで安全だけど、レイテンシーとスループット低下。in memory cacheが使えない。
以下の設定をnuxt.config.jsに追加。
参考: https://github.com/nuxt/nuxt.js/blob/21aaef3b4825e9b11aeecf7eaa8cd2c3d24fc3ce/packages/config/src/config/render.js#L4

  render: {
    bundleRenderer: {
      shouldPrefetch: () => false,
      shouldPreload: (fileWithoutQuery, asType) => ['script', 'style'].includes(asType),
      runInNewContext: true
    },
  }

俺のアプリだとnuxtの処理時間(response headerに書かれるやつ)が2倍〜3倍になった。

解決策6

bundleRendererにpreevaluate的なオプションを用意する。
runInNewContext: trueだけど、アイドル時に事前にevaluateしておく。
runInNewContext: trueのメリットと、レイテンシーが小さいメリットはあるが、スループットが下がる。in memoryキャッシュが使えない。

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3