feat(nuxt3, bridge): `useCookie` universal composable (#2085)

This commit is contained in:
pooya parsa 2021-11-22 21:43:00 +01:00 committed by GitHub
parent cc342eb6c4
commit 9920181df3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 311 additions and 1 deletions

View File

@ -0,0 +1,156 @@
# Cookies
> Nuxt provides SSR-friendly composable to read and write cookies.
## Usage
Within your pages, components, and plugins you can use `useCookie` to create a reactive reference bound to a specific cookie.
```js
const cookie = useCookie(name, options)
```
::alert{icon=👉}
**`useCookie` only works during `setup` or `Lifecycle Hooks`**
::
::alert{icon=😌}
`useCookie` ref will be automatically serialize and deserialized cookie value to JSON.
::
## Example
The example below creates a cookie called counter and if it doesn't exist set a random value. Whenever we update `counter`, the cookie will be updated.
```vue
<template>
<div>
<h1> Counter: {{ counter || '-' }}</h1>
<button @click="counter = null">
reset
</button>
<button @click="counter--">
-
</button>
<button @click="counter++">
+
</button>
</div>
</template>
<script setup>
const counter = useCookie('counter')
counter.value = counter.value || Math.round(Math.random() * 1000)
</script>
```
:button-link[Open on StackBlitz]{href="https://stackblitz.com/github/nuxt/framework/tree/main/examples/use-cookie?terminal=dev" blank}
## Options
Cookie composable accepts these properties in the options. Use them to modify the behavior of cookies.
Most of the options will be directly passed to [cookie](https://github.com/jshttp/cookie) package.
### `maxAge` / `expires`
**`maxAge`** Specifies the `number` (in seconds) to be the value for the [`Max-Age` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.2).
The given number will be converted to an integer by rounding down. By default, no maximum age is set.
**`expires`**: Specifies the `Date` object to be the value for the [`Expires` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.1).
By default, no expiration is set, and most clients will consider this a "non-persistent cookie" and
will delete it on a condition like exiting a web browser application.
::alert{icon=💡}
**Note:** The [cookie storage model specification](https://tools.ietf.org/html/rfc6265#section-5.3) states that if both `expires` and
`maxAge` are set, then `maxAge` takes precedence, but it is possible not all clients obey this,
so if both are set, they should point to the same date and time.eaks!
::
::alert
If neither of `expires` and `maxAge` are set, cookie will be session-only and removed if the user closes their browser.
::
#### `httpOnly`
Specifies the `boolean` value for the [`HttpOnly` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.6). When truthy,
the `HttpOnly` attribute is set, otherwise, it is not. By default, the `HttpOnly` attribute is not set.
::alert{icon=💡}
**Note** be careful when setting this to `true`, as compliant clients will not allow client-side
JavaScript to see the cookie in `document.cookie`.
::
#### `secure`
Specifies the `boolean` value for the [`Secure` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.5). When truthy,
the `Secure` attribute is set, otherwise,it is not. By default, the `Secure` attribute is not set.
::alert{icon=💡}
**Note:** be careful when setting this to `true`, as compliant clients will not send the cookie back to
the server in the future if the browser does not have an HTTPS connection. This can lead to hydration errors.
::
#### `domain`
Specifies the value for the [`Domain` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.3). By default, no
domain is set, and most clients will consider the cookie to apply to only the current domain.
#### `path`
Specifies the value for the [`Path` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.4). By default, the path
is considered the ["default path"](https://tools.ietf.org/html/rfc6265#section-5.1.4).
#### `sameSite`
Specifies the `boolean` or `string` to be the value for the [`SameSite` `Set-Cookie` attribute](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7).
- `true` will set the `SameSite` attribute to `Strict` for strict same site enforcement.
- `false` will not set the `SameSite` attribute.
- `'lax'` will set the `SameSite` attribute to `Lax` for lax same site enforcement.
- `'none'` will set the `SameSite` attribute to `None` for an explicit cross-site cookie.
- `'strict'` will set the `SameSite` attribute to `Strict` for strict same site enforcement.
More information about the different enforcement levels can be found in [the specification](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7).
#### `encode`
Specifies a function that will be used to encode a cookie's value. Since value of a cookie
has a limited character set (and must be a simple string), this function can be used to encode
a value into a string suited for a cookie's value.
The default encoder is the `JSON.stringify` + `encodeURIComponent`.
#### `decode`
Specifies a function that will be used to decode a cookie's value. Since the value of a cookie
has a limited character set (and must be a simple string), this function can be used to decode
a previously-encoded cookie value into a JavaScript string or other object.
The default decoder is `decodeURIComponent` + [destr](https://github.com/unjs/destr).
::alert{icon=💡}
**Note** if an error is thrown from this function, the original, non-decoded cookie value will
be returned as the cookie's value.
::
## Handling cookies in API routes
You can use `useCookie` and `setCookie` from [`h3`](https://github.com/unjs/h3) package to set cookies in server API routes.
**Example:**
```js
import { useCookie, setCookie } from 'h3'
export default (req, res) => {
// Reat counter cookie
let counter = useCookie(req, 'counter') || 0
// Increase counter cookie by 1
setCookie(res, 'counter', ++counter)
// Send JSON response
return { counter }
}
```

View File

@ -0,0 +1,23 @@
<template>
<div>
<h1> Counter: {{ counter || '-' }}</h1>
<button @click="counter = null">
reset
</button>
<button @click="counter--">
-
</button>
<button @click="counter++">
+
</button>
</div>
</template>
<script setup>
const counter = useCookie('counter')
counter.value = counter.value || Math.round(Math.random() * 1000)
</script>
<style scoped>
button { margin: 10px 5px; }
</style>

View File

@ -0,0 +1,4 @@
import { defineNuxtConfig } from 'nuxt3'
export default defineNuxtConfig({
})

View File

@ -0,0 +1,12 @@
{
"name": "example-use-cookie",
"private": true,
"devDependencies": {
"nuxt3": "latest"
},
"scripts": {
"dev": "nuxi dev",
"build": "nuxi build",
"start": "node .output/server/index.mjs"
}
}

View File

@ -0,0 +1,3 @@
{
"extends": "./.nuxt/tsconfig.json"
}

View File

@ -23,17 +23,21 @@
"@nuxt/nitro": "3.0.0",
"@nuxt/postcss8": "^1.1.3",
"@nuxt/schema": "3.0.0",
"@types/cookie": "^0.4.1",
"@vitejs/plugin-legacy": "^1.6.3",
"@vue/composition-api": "^1.4.0",
"@vueuse/head": "^0.7.2",
"acorn": "^8.6.0",
"consola": "^2.15.3",
"cookie": "^0.4.1",
"defu": "^5.0.0",
"destr": "^1.1.0",
"enhanced-resolve": "^5.8.3",
"estree-walker": "^2.0.2",
"externality": "^0.1.5",
"fs-extra": "^10.0.0",
"globby": "^11.0.4",
"h3": "^0.3.3",
"hash-sum": "^2.0.0",
"magic-string": "^0.25.7",
"mlly": "^0.3.13",

View File

@ -8,6 +8,7 @@ import { useNuxtApp } from './app'
export { useLazyAsyncData } from './asyncData'
export { useLazyFetch } from './fetch'
export { useCookie } from './cookie'
export * from '@vue/composition-api'

View File

@ -0,0 +1 @@
../../../nuxt3/src/app/composables/cookie.ts

View File

@ -25,13 +25,17 @@
"@nuxt/schema": "3.0.0",
"@nuxt/vite-builder": "3.0.0",
"@nuxt/webpack-builder": "3.0.0",
"@types/cookie": "^0.4.1",
"@vue/reactivity": "^3.2.22",
"@vue/shared": "^3.2.22",
"@vueuse/head": "^0.7.2",
"chokidar": "^3.5.2",
"consola": "^2.15.3",
"cookie": "^0.4.1",
"defu": "^5.0.0",
"destr": "^1.1.0",
"globby": "^11.0.4",
"h3": "^0.3.3",
"hash-sum": "^2.0.0",
"hookable": "^5.0.0",
"ignore": "^5.1.9",

View File

@ -0,0 +1,77 @@
import type { ServerResponse } from 'http'
import { Ref, ref, watch } from 'vue'
import type { CookieParseOptions, CookieSerializeOptions } from 'cookie'
import { parse, serialize } from 'cookie'
import { appendHeader } from 'h3'
import type { NuxtApp } from '@nuxt/schema'
import destr from 'destr'
import { useNuxtApp } from '#app'
type _CookieOptions = Omit<CookieSerializeOptions & CookieParseOptions, 'decode' | 'encode'>
export interface CookieOptions<T=any> extends _CookieOptions {
decode?(value: string): T
encode?(value: T): string;
}
export interface CookieRef<T> extends Ref<T> {}
const CookieDefaults: CookieOptions<any> = {
decode: val => destr(decodeURIComponent(val)),
encode: val => encodeURIComponent(typeof val === 'string' ? val : JSON.stringify(val))
}
export function useCookie <T=string> (name: string, _opts: CookieOptions<T>): CookieRef<T> {
const opts = { ...CookieDefaults, ..._opts }
const cookies = readRawCookies(opts)
const cookie = ref(opts.decode(cookies[name]))
if (process.client) {
watch(cookie, () => { writeClientCookie(name, cookie.value, opts) })
} else if (process.server) {
const initialValue = cookie.value
const nuxtApp = useNuxtApp()
nuxtApp.hooks.hookOnce('app:rendered', () => {
if (cookie.value !== initialValue) {
// @ts-ignore
writeServerCookie(useSSRRes(nuxtApp), name, cookie.value, opts)
}
})
}
return cookie
}
// @ts-ignore
function useSSRReq (nuxtApp?: NuxtApp = useNuxtApp()) { return nuxtApp.ssrContext?.req }
// @ts-ignore
function useSSRRes (nuxtApp?: NuxtApp = useNuxtApp()) { return nuxtApp.ssrContext?.res }
function readRawCookies (opts: CookieOptions = {}): Record<string, string> {
if (process.server) {
return parse(useSSRReq().headers.cookie || '', opts)
} else if (process.client) {
return parse(document.cookie, opts)
}
}
function serializeCookie (name: string, value: any, opts: CookieSerializeOptions = {}) {
if (value === null || value === undefined) {
opts.maxAge = -1
}
return serialize(name, value, opts)
}
function writeClientCookie (name: string, value: any, opts: CookieSerializeOptions = {}) {
if (process.client) {
document.cookie = serializeCookie(name, value, opts)
}
}
function writeServerCookie (res: ServerResponse, name: string, value: any, opts: CookieSerializeOptions = {}) {
if (res) {
// TODO: Try to smart join with exisiting Set-Cookie headers
appendHeader(res, 'Set-Cookie', serializeCookie(name, value, opts))
}
}

View File

@ -3,3 +3,4 @@ export { useAsyncData, useLazyAsyncData } from './asyncData'
export { useHydration } from './hydrate'
export { useState } from './state'
export { useFetch, useLazyFetch } from './fetch'
export { useCookie } from './cookie'

View File

@ -13,7 +13,8 @@ export const Nuxt3AutoImports: AutoImportSource[] = [
'useRuntimeConfig',
'useState',
'useFetch',
'useLazyFetch'
'useLazyFetch',
'useCookie'
]
},
// #meta

View File

@ -2427,6 +2427,7 @@ __metadata:
"@nuxt/postcss8": ^1.1.3
"@nuxt/schema": 3.0.0
"@nuxt/types": ^2.15.8
"@types/cookie": ^0.4.1
"@types/fs-extra": ^9.0.13
"@types/hash-sum": ^1.0.0
"@types/node-fetch": ^3.0.2
@ -2435,12 +2436,15 @@ __metadata:
"@vueuse/head": ^0.7.2
acorn: ^8.6.0
consola: ^2.15.3
cookie: ^0.4.1
defu: ^5.0.0
destr: ^1.1.0
enhanced-resolve: ^5.8.3
estree-walker: ^2.0.2
externality: ^0.1.5
fs-extra: ^10.0.0
globby: ^11.0.4
h3: ^0.3.3
hash-sum: ^2.0.0
magic-string: ^0.25.7
mlly: ^0.3.13
@ -3800,6 +3804,13 @@ __metadata:
languageName: node
linkType: hard
"@types/cookie@npm:^0.4.1":
version: 0.4.1
resolution: "@types/cookie@npm:0.4.1"
checksum: 3275534ed69a76c68eb1a77d547d75f99fedc80befb75a3d1d03662fb08d697e6f8b1274e12af1a74c6896071b11510631ba891f64d30c78528d0ec45a9c1a18
languageName: node
linkType: hard
"@types/eslint-scope@npm:^3.7.0":
version: 3.7.1
resolution: "@types/eslint-scope@npm:3.7.1"
@ -10491,6 +10502,14 @@ __metadata:
languageName: unknown
linkType: soft
"example-use-cookie@workspace:examples/use-cookie":
version: 0.0.0-use.local
resolution: "example-use-cookie@workspace:examples/use-cookie"
dependencies:
nuxt3: latest
languageName: unknown
linkType: soft
"example-use-fetch@workspace:examples/use-fetch":
version: 0.0.0-use.local
resolution: "example-use-fetch@workspace:examples/use-fetch"
@ -15269,14 +15288,18 @@ __metadata:
"@nuxt/schema": 3.0.0
"@nuxt/vite-builder": 3.0.0
"@nuxt/webpack-builder": 3.0.0
"@types/cookie": ^0.4.1
"@types/hash-sum": ^1.0.0
"@vue/reactivity": ^3.2.22
"@vue/shared": ^3.2.22
"@vueuse/head": ^0.7.2
chokidar: ^3.5.2
consola: ^2.15.3
cookie: ^0.4.1
defu: ^5.0.0
destr: ^1.1.0
globby: ^11.0.4
h3: ^0.3.3
hash-sum: ^2.0.0
hookable: ^5.0.0
ignore: ^5.1.9