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.set
、this.$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の設定をしていく。
以下プラグインのコード。
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
ヘッダーが設定されていることが確認できる。
またCookieも正しく設定されていることが確認できる。
値の設定だけではなくもちろん値の取得も機能している。
以下のコードのconsole.logが正しく出力されている。
created() {
console.log(this.$cookie.get('hoge'))
},
mounted() {
console.log(this.$cookie.get('hoge'))
}
この情報が誰かの役に立てば幸いです。