feat(nuxt): refreshCookie + experimental CookieStore support (#25198)

This commit is contained in:
Enkot 2024-01-29 12:37:32 +02:00 committed by GitHub
parent c446602529
commit 034d1aaa6f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 113 additions and 10 deletions

View File

@ -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**.
::

View 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
```

View File

@ -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 {}

View File

@ -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) {

View File

@ -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}`,

View File

@ -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'
}, },
{ {

View File

@ -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.

View File

@ -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()
}) })
}) })

View File

@ -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>