mirror of
https://github.com/nuxt/nuxt.git
synced 2025-01-18 09:25:54 +00:00
feat(nuxt3, bridge): useCookie
universal composable (#2085)
This commit is contained in:
parent
cc342eb6c4
commit
9920181df3
156
docs/content/3.docs/1.usage/6-cookies.md
Normal file
156
docs/content/3.docs/1.usage/6-cookies.md
Normal 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 }
|
||||
}
|
||||
```
|
23
examples/use-cookie/app.vue
Normal file
23
examples/use-cookie/app.vue
Normal 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>
|
4
examples/use-cookie/nuxt.config.ts
Normal file
4
examples/use-cookie/nuxt.config.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { defineNuxtConfig } from 'nuxt3'
|
||||
|
||||
export default defineNuxtConfig({
|
||||
})
|
12
examples/use-cookie/package.json
Normal file
12
examples/use-cookie/package.json
Normal 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"
|
||||
}
|
||||
}
|
3
examples/use-cookie/tsconfig.json
Normal file
3
examples/use-cookie/tsconfig.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
}
|
@ -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",
|
||||
|
@ -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'
|
||||
|
||||
|
1
packages/bridge/src/runtime/cookie.ts
Symbolic link
1
packages/bridge/src/runtime/cookie.ts
Symbolic link
@ -0,0 +1 @@
|
||||
../../../nuxt3/src/app/composables/cookie.ts
|
@ -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",
|
||||
|
77
packages/nuxt3/src/app/composables/cookie.ts
Normal file
77
packages/nuxt3/src/app/composables/cookie.ts
Normal 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))
|
||||
}
|
||||
}
|
@ -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'
|
||||
|
@ -13,7 +13,8 @@ export const Nuxt3AutoImports: AutoImportSource[] = [
|
||||
'useRuntimeConfig',
|
||||
'useState',
|
||||
'useFetch',
|
||||
'useLazyFetch'
|
||||
'useLazyFetch',
|
||||
'useCookie'
|
||||
]
|
||||
},
|
||||
// #meta
|
||||
|
23
yarn.lock
23
yarn.lock
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user