23
18

More than 3 years have passed since last update.

Nuxt + universal-cookieでクライアント、サーバーサイド両方でcookieを扱えるようにする

Last updated at Posted at 2020-05-20

universal-cookieというcookieを扱うライブラリがあり、それをNuxtと組み合わせて使用するときはサーバーサイドでも正しくcookieを取得、設定できるようにする必要がある。
そこでこの記事ではその設定方法を記載していく。

universal-cookieにはexpressで動作するuniversal-cookie-expressというものもある。
Nuxtの設定ではuniversal-cookie-expressのコードを参考に設定してみた。

コードはこちら https://github.com/igayamaguchi/universal-cookie-nuxt-example

まずはNuxtのインストール

特に変わったことはしていないが、TypeScriptを選択しているので以降に続くサンプルコードはTypeScriptで記述していく。

$ create-nuxt-app

create-nuxt-app v2.15.0
✨  Generating Nuxt.js project in .
? Project name universal-cookie-nuxt-example
? Project description My first-rate Nuxt.js project
? Author name igayamaguchi
? Choose programming language TypeScript
? Choose the package manager Npm
? Choose UI framework None
? Choose custom server framework None (Recommended)
? Choose the runtime for TypeScript @nuxt/typescript-runtime
? Choose Nuxt.js modules (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Choose linting tools ESLint, Prettier, StyleLint
? Choose test framework Jest
? Choose rendering mode Universal (SSR)
? Choose development tools (Press <space> to select, <a> to toggle all, <i> to invert selection)

$ npm install universal-cookie
+ universal-cookie@4.0.3

実際に設定をしていく

設定していく前にuniversal-cookieの使い方

universal-cookieをdefault importしてインスタンス化、そのインスタンスのメソッドからcookieの操作が可能になる。
この例はcookieの設定と取得。
ただし、サーバーサイドでcookieの設定をしたとしてもクライアントにSet-Cookieヘッダーを送るわけではないのでブラウザがcookieの値を保存することはない。

import UniversalCookie from 'universal-cookie'

// set
const cookie = new UniversalCookie()
cookie.set('hoge', 'fuga')

// get
const cookie = new UniversalCookie()
const value = cookie.get('hoge')
console.log(value) // cookieの値 - fuga

Vueコンポーネント内でuniversal-cookieを使えるように

このset、getをVueコンポーネント内でthis.$cookie.setthis.$cookie.getという形でアクセスできるようにする。

以下ページコンポーネントでの使用例。
asyncData、fetchではapp.$cookie、それ以外ではthis.$cookieという形でアクセスする。

<script lang="ts">
import Vue from 'vue'
import Logo from '~/components/Logo.vue'

export default Vue.extend({
  asyncData({ app }) {
    app.$cookie.set('hoge', true)
  },
  fetch({ app }): Promise<void> | void {
    app.$cookie.set('hoge2', true)
  },
  components: {
    Logo
  },
  created() {
    console.log(this.$cookie.get('hoge'))
  },
  mounted() {
    console.log(this.$cookie.get('hoge'))
  }
})
</script>

このthis.$cookieという形を実現するために、Nuxtのplugin機構を活かしてinjectの設定をしていく。
以下プラグインのコード。

plugins/cookie
import { OutgoingMessage } from 'http'
import { serialize } from 'cookie'
import { Plugin } from '@nuxt/types'
import UniversalCookie, { CookieChangeOptions } from 'universal-cookie'

declare module 'vue/types/vue' {
  interface Vue {
    $cookie: UniversalCookie
  }
}

declare module '@nuxt/types' {
  interface NuxtAppOptions {
    $cookie: UniversalCookie
  }
}

declare module 'vuex/types/index' {
  interface Store<S> {
    $cookie: UniversalCookie
  }
}

/**
 * Vueでthis.$cookieからクッキーの操作を行える処理を追加する
 * サーバー、クライアント両方で同じインターフェースで扱えるように
 */
const plugin: Plugin = ({ req, res }, inject) => {
  let cookie: UniversalCookie

  if (process.server) {
    cookie = createServerCookie(req.headers.cookie || '', res)
  } else {
    cookie = new UniversalCookie()
  }

  inject('cookie', cookie)
}

/**
 * universal-cookieはサーバーでcookieを追加、変更、削除する場合、それをクライアントに知らせる仕組みがない
 * そのため自前でレスポンスヘッダーにSet-Cookieヘッダーを追加してクッキーの情報をクライアントに送る必要がある
 * その仕組みを備えたインスタンスを生成する
 */
export function createServerCookie(
  cookie: string,
  res: OutgoingMessage
): UniversalCookie {
  const universalCookie = new UniversalCookie(cookie)
  universalCookie.addChangeListener((change: CookieChangeOptions) => {
    if (res.headersSent) {
      return
    }
    let cookieHeader = res.getHeader('Set-Cookie')
    if (typeof cookieHeader === 'string') {
      cookieHeader = [cookieHeader]
    } else if (typeof cookieHeader === 'number') {
      cookieHeader = [cookieHeader.toString()]
    }
    cookieHeader = (cookieHeader as string[]) || []

    // cookieの削除時にはvalueにundefinedが入る
    if (change.value === undefined) {
      cookieHeader.push(serialize(change.name, '', change.options))
    } else {
      cookieHeader.push(serialize(change.name, change.value, change.options))
    }

    res.setHeader('Set-Cookie', cookieHeader)
  })

  return universalCookie
}

export default plugin

上から解説していく。

まずはインスタンスの生成。
クライアントサイドでの実行では特にすることもなくそのままuniversal-cookieをインスタンス化。
サーバーサイドではヘッダーに送られているクッキーの文字列を取得してそれを用いてuniversal-cookieをインスタンス化。
resをuniversal-cookieのインスタンスを作成する関数に送っているのは、レスポンスヘッダーの取得、設定を行うため。

import UniversalCookie from 'universal-cookie'

if (process.server) {
  cookie = createServerCookie(req.headers.cookie || '', res)
} else {
  cookie = new UniversalCookie()
}

// ------------------

function createServerCookie(
  cookie: string,
  res: OutgoingMessage
): UniversalCookie {
  const universalCookie = new UniversalCookie(cookie)

// ------------------

universal-cookieにはaddChangeListenrというcookieを設定、または削除したときによびだされるフック関数を設定することができるメソッドが備えられている。
サーバーサイドでクッキーを設定する場合はそのフック関数にSet-Cookieヘッダーを返す仕組みを導入することでサーバーで設定したクッキーがブラウザに送信され正しく保存されるようにしている。
フック関数は第一引数に設定されたcookieの値、オプションが渡されるようになっている。

universalCookie.addChangeListener((change: CookieChangeOptions) => { /* ... */ })

Nuxt上であり得るのか分からないが既にヘッダーが送られ始めた後にcookieの設定をしている場合はサーバーサイドのcookieの設定は行わないようにしている。

if (res.headerSent) {
  return
}

送る予定のレスポンスの中に既にSet-Cookieヘッダーが設定していた場合その値を取り出して加工していく。
res.getHeader('xxxxx')で任意のレスポンスヘッダーを取得することが出来る。今回はSet-Cookieヘッダーを使用するのでres.getHeader('Set-Cookie')
res.getHeader('Set-Cookie')では配列以外で返ってくるパターンを想定して文字列の配列形式に変換しておく。

let cookieHeader = res.getHeader('Set-Cookie')
if (typeof cookieHeader === 'string') {
  cookieHeader = [cookieHeader]
} else if (typeof cookieHeader === 'number') {
  cookieHeader = [cookieHeader.toString()]
}
cookieHeader = (cookieHeader as string[]) || []

先ほど作成した配列に設定、または削除するcookieを追加してヘッダーに設定する。
値はフック関数の第一引数に{ name: クッキー名, value: クッキーの値, options: クッキーのオプション(期限など)}という形で入っているのでそれを取り出し、cookieパッケージのserializeメソッドを使用して文字列に加工してあげることでヘッダーに設定するのに適した文字列となる。

削除時にはoptionsには{ expires: 1970-01-31T15:00:01.000Z, maxAge: 0 }のような過去の値、期限切れになるような値が入るようになっているので、それをそのままヘッダーとして送ればブラウザ側のcookieを削除することができる。

res.setHeader('xxxxx')でレスポンスヘッダーの追加、更新ができるので、res.setHeader('Set-Cookie', cookieHeader)Set-Cookieヘッダーの設定を行っている。

import { serialize } from 'cookie'

// cookieの削除時にはvalueにundefinedが入る
if (change.value === undefined) {
  cookieHeader.push(serialize(change.name, '', change.options))
} else {
  cookieHeader.push(serialize(change.name, change.value, change.options))
 }

res.setHeader('Set-Cookie', cookieHeader)

これでプラグインの記述が完成するので、nuxt.config.tsにプラグインファイルを読み込む記述を追加してNuxtの設定が完了する。

plugins: ['~/plugins/cookie'],

確認

最初に載せたコードをpage/index.vueに適用してそのページにアクセスしてみる。

<script lang="ts">
import Vue from 'vue'
import Logo from '~/components/Logo.vue'

export default Vue.extend({
  asyncData({ app }) {
    app.$cookie.set('hoge', true)
  },
  fetch({ app }): Promise<void> | void {
    app.$cookie.set('hoge2', true)
  },
  components: {
    Logo
  },
  created() {
    console.log(this.$cookie.get('hoge'))
  },
  mounted() {
    console.log(this.$cookie.get('hoge'))
  }
})
</script>

アクセスしてChromeの開発ツールのNetworkタブでページのレスポンスヘッダーを除いてみると以下のようにSet-Cookieヘッダーが設定されていることが確認できる。

image.png

またCookieも正しく設定されていることが確認できる。

Networkタブの中のCookie
image.png

ApplicationタブのCookies
image.png

値の設定だけではなくもちろん値の取得も機能している。
以下のコードのconsole.logが正しく出力されている。

created() {
  console.log(this.$cookie.get('hoge'))
},
mounted() {
  console.log(this.$cookie.get('hoge'))
}

image.png

この情報が誰かの役に立てば幸いです。

23
18
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
23
18