mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-25 07:05:11 +00:00
feat(nuxt): refreshCookie
+ experimental CookieStore support (#25198)
This commit is contained in:
parent
c446602529
commit
034d1aaa6f
@ -374,3 +374,19 @@ import { Buffer } from 'node:buffer'
|
|||||||
globalThis.Buffer = globalThis.Buffer || Buffer
|
globalThis.Buffer = globalThis.Buffer || Buffer
|
||||||
```
|
```
|
||||||
::
|
::
|
||||||
|
|
||||||
|
## cookieStore
|
||||||
|
|
||||||
|
Enables CookieStore support to listen for cookie updates (if supported by the browser) and refresh `useCookie` ref values.
|
||||||
|
|
||||||
|
```ts [nuxt.config.ts]
|
||||||
|
export defineNuxtConfig({
|
||||||
|
experimental: {
|
||||||
|
cookieStore: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
::read-more{icon="i-simple-icons-mdnwebdocs" color="gray" to="https://developer.mozilla.org/en-US/docs/Web/API/CookieStore" target="_blank"}
|
||||||
|
Read more about the **CookieStore**.
|
||||||
|
::
|
||||||
|
46
docs/3.api/3.utils/refresh-cookie.md
Normal file
46
docs/3.api/3.utils/refresh-cookie.md
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
title: "refreshCookie"
|
||||||
|
description: "Refresh useCookie values manually when a cookie has changed"
|
||||||
|
navigation:
|
||||||
|
badge: New
|
||||||
|
links:
|
||||||
|
- label: Source
|
||||||
|
icon: i-simple-icons-github
|
||||||
|
to: https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/app/composables/cookie.ts
|
||||||
|
size: xs
|
||||||
|
---
|
||||||
|
|
||||||
|
::callout{icon="i-ph-info-duotone" color="blue"}
|
||||||
|
This utility is available since [Nuxt v3.10](/blog/v3-10).
|
||||||
|
::
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
The `refreshCookie` function is designed to refresh cookie value returned by `useCookie`.
|
||||||
|
|
||||||
|
This is useful for updating the `useCookie` ref when we know the new cookie value has been set in the browser.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```vue [app.vue]
|
||||||
|
<script setup lang="ts">
|
||||||
|
const tokenCookie = useCookie('token')
|
||||||
|
|
||||||
|
const login = async (username, password) => {
|
||||||
|
const token = await $fetch('/api/token', { ... }) // Sets `token` cookie on response
|
||||||
|
refreshCookie('token')
|
||||||
|
}
|
||||||
|
|
||||||
|
const loggedIn = computed(() => !!tokenCookie.value)
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
::callout{to="/docs/guide/going-further/experimental-features#cookiestore"}
|
||||||
|
You can enable experimental `cookieStore` option to automatically refresh `useCookie` value when cookie changes in the browser.
|
||||||
|
::
|
||||||
|
|
||||||
|
## Type
|
||||||
|
|
||||||
|
```ts
|
||||||
|
refreshCookie(name: string): void
|
||||||
|
```
|
6
packages/nuxt/index.d.ts
vendored
6
packages/nuxt/index.d.ts
vendored
@ -11,6 +11,12 @@ declare global {
|
|||||||
effectiveType: 'slow-2g' | '2g' | '3g' | '4g'
|
effectiveType: 'slow-2g' | '2g' | '3g' | '4g'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Window {
|
||||||
|
cookieStore?: {
|
||||||
|
onchange: (event: any) => void
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {}
|
export {}
|
||||||
|
@ -10,6 +10,9 @@ import { klona } from 'klona'
|
|||||||
import { useNuxtApp } from '../nuxt'
|
import { useNuxtApp } from '../nuxt'
|
||||||
import { useRequestEvent } from './ssr'
|
import { useRequestEvent } from './ssr'
|
||||||
|
|
||||||
|
// @ts-expect-error virtual import
|
||||||
|
import { cookieStore } from '#build/nuxt.config.mjs'
|
||||||
|
|
||||||
type _CookieOptions = Omit<CookieSerializeOptions & CookieParseOptions, 'decode' | 'encode'>
|
type _CookieOptions = Omit<CookieSerializeOptions & CookieParseOptions, 'decode' | 'encode'>
|
||||||
|
|
||||||
export interface CookieOptions<T = any> extends _CookieOptions {
|
export interface CookieOptions<T = any> extends _CookieOptions {
|
||||||
@ -29,6 +32,8 @@ const CookieDefaults = {
|
|||||||
encode: val => encodeURIComponent(typeof val === 'string' ? val : JSON.stringify(val))
|
encode: val => encodeURIComponent(typeof val === 'string' ? val : JSON.stringify(val))
|
||||||
} satisfies CookieOptions<any>
|
} satisfies CookieOptions<any>
|
||||||
|
|
||||||
|
const store = import.meta.client && cookieStore ? window.cookieStore : undefined
|
||||||
|
|
||||||
/** @since 3.0.0 */
|
/** @since 3.0.0 */
|
||||||
export function useCookie<T = string | null | undefined> (name: string, _opts?: CookieOptions<T> & { readonly?: false }): CookieRef<T>
|
export function useCookie<T = string | null | undefined> (name: string, _opts?: CookieOptions<T> & { readonly?: false }): CookieRef<T>
|
||||||
export function useCookie<T = string | null | undefined> (name: string, _opts: CookieOptions<T> & { readonly: true }): Readonly<CookieRef<T>>
|
export function useCookie<T = string | null | undefined> (name: string, _opts: CookieOptions<T> & { readonly: true }): Readonly<CookieRef<T>>
|
||||||
@ -58,7 +63,7 @@ export function useCookie<T = string | null | undefined> (name: string, _opts?:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (import.meta.client) {
|
if (import.meta.client) {
|
||||||
const channel = typeof BroadcastChannel === 'undefined' ? null : new BroadcastChannel(`nuxt:cookies:${name}`)
|
const channel = store || typeof BroadcastChannel === 'undefined' ? null : new BroadcastChannel(`nuxt:cookies:${name}`)
|
||||||
const callback = () => {
|
const callback = () => {
|
||||||
if (opts.readonly || isEqual(cookie.value, cookies[name])) { return }
|
if (opts.readonly || isEqual(cookie.value, cookies[name])) { return }
|
||||||
writeClientCookie(name, cookie.value, opts as CookieSerializeOptions)
|
writeClientCookie(name, cookie.value, opts as CookieSerializeOptions)
|
||||||
@ -67,6 +72,13 @@ export function useCookie<T = string | null | undefined> (name: string, _opts?:
|
|||||||
channel?.postMessage(opts.encode(cookie.value as T))
|
channel?.postMessage(opts.encode(cookie.value as T))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleChange = (data: { value?: any, refresh?: boolean }) => {
|
||||||
|
const value = data.refresh ? readRawCookies(opts)?.[name] : opts.decode(data.value)
|
||||||
|
watchPaused = true
|
||||||
|
cookies[name] = cookie.value = value
|
||||||
|
nextTick(() => { watchPaused = false })
|
||||||
|
}
|
||||||
|
|
||||||
let watchPaused = false
|
let watchPaused = false
|
||||||
|
|
||||||
if (getCurrentScope()) {
|
if (getCurrentScope()) {
|
||||||
@ -77,12 +89,13 @@ export function useCookie<T = string | null | undefined> (name: string, _opts?:
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (channel) {
|
if (store) {
|
||||||
channel.onmessage = (event) => {
|
store.onchange = (event) => {
|
||||||
watchPaused = true
|
const cookie = event.changed.find((c: any) => c.name === name)
|
||||||
cookies[name] = cookie.value = opts.decode(event.data)
|
if (cookie) handleChange({ value: cookie.value })
|
||||||
nextTick(() => { watchPaused = false })
|
|
||||||
}
|
}
|
||||||
|
} else if (channel) {
|
||||||
|
channel.onmessage = ({ data }) => handleChange(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.watch) {
|
if (opts.watch) {
|
||||||
@ -109,6 +122,12 @@ export function useCookie<T = string | null | undefined> (name: string, _opts?:
|
|||||||
|
|
||||||
return cookie as CookieRef<T>
|
return cookie as CookieRef<T>
|
||||||
}
|
}
|
||||||
|
/** @since 3.10.0 */
|
||||||
|
export function refreshCookie(name: string) {
|
||||||
|
if (store || typeof BroadcastChannel === 'undefined') return
|
||||||
|
|
||||||
|
new BroadcastChannel(`nuxt:cookies:${name}`)?.postMessage({ refresh: true })
|
||||||
|
}
|
||||||
|
|
||||||
function readRawCookies (opts: CookieOptions = {}): Record<string, unknown> | undefined {
|
function readRawCookies (opts: CookieOptions = {}): Record<string, unknown> | undefined {
|
||||||
if (import.meta.server) {
|
if (import.meta.server) {
|
||||||
|
@ -378,6 +378,7 @@ export const nuxtConfigTemplate: NuxtTemplate = {
|
|||||||
`export const renderJsonPayloads = ${!!ctx.nuxt.options.experimental.renderJsonPayloads}`,
|
`export const renderJsonPayloads = ${!!ctx.nuxt.options.experimental.renderJsonPayloads}`,
|
||||||
`export const componentIslands = ${!!ctx.nuxt.options.experimental.componentIslands}`,
|
`export const componentIslands = ${!!ctx.nuxt.options.experimental.componentIslands}`,
|
||||||
`export const payloadExtraction = ${!!ctx.nuxt.options.experimental.payloadExtraction}`,
|
`export const payloadExtraction = ${!!ctx.nuxt.options.experimental.payloadExtraction}`,
|
||||||
|
`export const cookieStore = ${!!ctx.nuxt.options.experimental.cookieStore}`,
|
||||||
`export const appManifest = ${!!ctx.nuxt.options.experimental.appManifest}`,
|
`export const appManifest = ${!!ctx.nuxt.options.experimental.appManifest}`,
|
||||||
`export const remoteComponentIslands = ${typeof ctx.nuxt.options.experimental.componentIslands === 'object' && ctx.nuxt.options.experimental.componentIslands.remoteIsland}`,
|
`export const remoteComponentIslands = ${typeof ctx.nuxt.options.experimental.componentIslands === 'object' && ctx.nuxt.options.experimental.componentIslands.remoteIsland}`,
|
||||||
`export const selectiveClient = ${typeof ctx.nuxt.options.experimental.componentIslands === 'object' && ctx.nuxt.options.experimental.componentIslands.selectiveClient}`,
|
`export const selectiveClient = ${typeof ctx.nuxt.options.experimental.componentIslands === 'object' && ctx.nuxt.options.experimental.componentIslands.selectiveClient}`,
|
||||||
|
@ -62,7 +62,7 @@ const granularAppPresets: InlinePreset[] = [
|
|||||||
from: '#app/composables/fetch'
|
from: '#app/composables/fetch'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
imports: ['useCookie'],
|
imports: ['useCookie', 'refreshCookie'],
|
||||||
from: '#app/composables/cookie'
|
from: '#app/composables/cookie'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -287,6 +287,12 @@ export default defineUntypedSchema({
|
|||||||
* })
|
* })
|
||||||
*/
|
*/
|
||||||
sharedPrerenderData: false,
|
sharedPrerenderData: false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables CookieStore support to listen for cookie updates (if supported by the browser) and refresh `useCookie` ref values.
|
||||||
|
* @see [CookieStore](https://developer.mozilla.org/en-US/docs/Web/API/CookieStore)
|
||||||
|
*/
|
||||||
|
cookieStore: false,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This allows specifying the default options for core Nuxt components and composables.
|
* This allows specifying the default options for core Nuxt components and composables.
|
||||||
|
@ -488,10 +488,14 @@ describe('nuxt composables', () => {
|
|||||||
return JSON.parse(decodeURIComponent(raw))
|
return JSON.parse(decodeURIComponent(raw))
|
||||||
}
|
}
|
||||||
expect(await extractCookie()).toEqual({ foo: 'bar' })
|
expect(await extractCookie()).toEqual({ foo: 'bar' })
|
||||||
await page.getByRole('button').click()
|
await page.getByText('Change cookie').click()
|
||||||
expect(await extractCookie()).toEqual({ foo: 'baz' })
|
expect(await extractCookie()).toEqual({ foo: 'baz' })
|
||||||
await page.getByRole('button').click()
|
await page.getByText('Change cookie').click()
|
||||||
expect(await extractCookie()).toEqual({ foo: 'bar' })
|
expect(await extractCookie()).toEqual({ foo: 'bar' })
|
||||||
|
await page.evaluate(() => document.cookie = 'updated=foobar')
|
||||||
|
await page.getByText('Refresh cookie').click()
|
||||||
|
const text = await page.innerText('pre')
|
||||||
|
expect(text).toContain('foobar')
|
||||||
await page.close()
|
await page.close()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
7
test/fixtures/basic/pages/cookies.vue
vendored
7
test/fixtures/basic/pages/cookies.vue
vendored
@ -4,6 +4,8 @@ useCookie('accessed-with-default-value', () => 'default')
|
|||||||
useCookie('set').value = 'set'
|
useCookie('set').value = 'set'
|
||||||
useCookie('set-to-null').value = null
|
useCookie('set-to-null').value = null
|
||||||
useCookie('set-to-null-with-default', () => 'default').value = null
|
useCookie('set-to-null-with-default', () => 'default').value = null
|
||||||
|
|
||||||
|
const updated = useCookie('updated')
|
||||||
// the next set are all sent by browser
|
// the next set are all sent by browser
|
||||||
useCookie('browser-accessed-but-not-used')
|
useCookie('browser-accessed-but-not-used')
|
||||||
useCookie('browser-accessed-with-default-value', () => 'default')
|
useCookie('browser-accessed-with-default-value', () => 'default')
|
||||||
@ -19,9 +21,12 @@ const objectCookie = useCookie('browser-object-default', {
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div>cookies testing page</div>
|
<div>cookies testing page</div>
|
||||||
<pre>{{ objectCookie }}</pre>
|
<pre>{{ updated }}</pre>
|
||||||
<button @click="objectCookie.foo === 'baz' ? objectCookie.foo = 'bar' : objectCookie.foo = 'baz'">
|
<button @click="objectCookie.foo === 'baz' ? objectCookie.foo = 'bar' : objectCookie.foo = 'baz'">
|
||||||
Change cookie
|
Change cookie
|
||||||
</button>
|
</button>
|
||||||
|
<button @click="refreshCookie('updated')">
|
||||||
|
Refresh cookie
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
Loading…
Reference in New Issue
Block a user