diff --git a/docs/api/classes/Cookies.md b/docs/api/classes/Cookies.md new file mode 100644 index 0000000..ac62689 --- /dev/null +++ b/docs/api/classes/Cookies.md @@ -0,0 +1,51 @@ +[dt-utils](../globals.md) / Cookies + +# Class: Cookies + +Defined in: [cookies/index.ts:54](https://github.com/DTStack/dt-utils/blob/master/src/cookies/index.ts#L54) + +统一封装的 Cookie 操作工具集。 + +## Description + +基于 js-cookie 封装的浏览器 Cookie 操作工具,提供统一的默认配置和便捷方法。 +所有操作默认携带根路径,统一管理 Cookie 的读写与删除逻辑,简化项目中 Cookie 的使用。 + +## Methods + +| 方法名 | 描述 | 参数 | 返回值 | 使用方式 | +|------|------|------|--------|--------| +| `get` | 读取指定名称的 Cookie 值;不传名称时返回所有 Cookie | `name?: string` - Cookie 名称 | `string \| undefined \| Record` | `Cookies.get('username')` | +| `set` | 设置 Cookie,并自动合并默认配置 | `name: string` - Cookie 名称
`value: string` - Cookie 值
`options?: JSCookies.CookieAttributes` - 可选配置
`pairs: Record` - 批量键值对 | `void` | `Cookies.set('username', 'john', { expires: 7 })`
`Cookies.set({ token: '123', user: 'tom' })` | +| `remove` | 删除指定名称的 Cookie,并自动合并默认配置 | `name: string \| string[]` - Cookie 名称
`options?: JSCookies.CookieAttributes` - 可选配置 | `void` | `Cookies.remove('username')` | +| `clear` | 清除 Cookie,可以选择性保留特定键 | `except?: string[]` - 可选的要保留的键数组 | `void` | `Cookies.clear(['username'])` | + +## Example + +```typescript +import Cookies from './cookies'; + +// 读取所有 Cookie +const allCookies = Cookies.get(); + +// 读取指定 Cookie +const username = Cookies.get('username'); + +// 设置 Cookie +Cookies.set('username', 'john', { expires: 7 }); + +// 批量设置 Cookie +Cookies.set({ token: '123', user: 'tom' }); + +// 读取 Cookie +const username = Cookies.get('username'); + +// 删除单个 Cookie +Cookies.remove('username'); + +// 批量删除 Cookie +Cookies.remove(['username', 'permission']); + +// 清除除 username 外的所有 Cookie +Cookies.clear(['username'], { path: '/' }); +``` diff --git a/docs/api/globals.md b/docs/api/globals.md index 2ca232e..b3b48ec 100644 --- a/docs/api/globals.md +++ b/docs/api/globals.md @@ -2,6 +2,7 @@ ## Storage +- [Cookies](classes/Cookies.md) - [~~IndexedDB~~](classes/IndexedDB.md) - [LocalDB](classes/LocalDB.md) - [SessionDB](classes/SessionDB.md) diff --git a/docs/api/typedoc-sidebar.json b/docs/api/typedoc-sidebar.json index 60890a9..4fe6e85 100644 --- a/docs/api/typedoc-sidebar.json +++ b/docs/api/typedoc-sidebar.json @@ -3,6 +3,10 @@ "text": "Storage", "collapsed": true, "items": [ + { + "text": "Cookies", + "link": "/api/classes/Cookies.md" + }, { "text": "IndexedDB", "link": "/api/classes/IndexedDB.md" diff --git a/src/cookies/__test__/index.test.ts b/src/cookies/__test__/index.test.ts new file mode 100644 index 0000000..7ffe42c --- /dev/null +++ b/src/cookies/__test__/index.test.ts @@ -0,0 +1,271 @@ +import Cookies from '..'; + +describe('Cookies utils', () => { + beforeEach(() => { + // 清空测试 cookie + document.cookie.split(';').forEach((cookie) => { + const name = cookie.split('=')[0]?.trim(); + + if (name) { + document.cookie = `${name}=; expires=${new Date(0).toUTCString()}; path=/`; + } + }); + }); + + afterEach(() => { + window.history.replaceState({}, '', '/app'); + Cookies.remove('scopedToken', { path: '/app' }); + Cookies.remove('scopedKeep', { path: '/app' }); + window.history.replaceState({}, '', '/'); + }); + + it('should expose cookie methods as static members on class-like export', () => { + expect(typeof Cookies).toBe('function'); + expect(typeof Cookies.get).toBe('function'); + expect(typeof Cookies.set).toBe('function'); + expect(typeof Cookies.remove).toBe('function'); + expect(typeof Cookies.clear).toBe('function'); + }); + + describe('get', () => { + it('should get cookie value', () => { + document.cookie = 'token=123; path=/'; + + expect(Cookies.get('token')).toBe('123'); + }); + + it('should get all cookies when no name is provided', () => { + Cookies.set('token', '123'); + Cookies.set('user', 'tom'); + + const result = Cookies.get(); + + expect(result).toEqual({ + token: '123', + user: 'tom', + }); + }); + + it('should return undefined when cookie does not exist', () => { + expect(Cookies.get('not-exist')).toBeUndefined(); + }); + }); + + describe('set', () => { + it('should set cookie', () => { + Cookies.set('token', '123'); + + expect(document.cookie).toContain('token=123'); + expect(Cookies.get('token')).toBe('123'); + }); + + it('should set cookie with custom path', () => { + Cookies.set('user', 'tom', { + path: '/', + }); + + expect(Cookies.get('user')).toBe('tom'); + }); + + it('should set cookie with expires as number', () => { + Cookies.set('token', '123', { expires: 7 }); + + expect(Cookies.get('token')).toBe('123'); + }); + + it('should set cookie with expires as Date', () => { + const futureDate = new Date(); + futureDate.setDate(futureDate.getDate() + 7); + + Cookies.set('token', '123', { expires: futureDate }); + + expect(Cookies.get('token')).toBe('123'); + }); + + it('should set cookie with secure option without throwing', () => { + expect(() => Cookies.set('token', '123', { secure: true })).not.toThrow(); + }); + + it('should set cookie with sameSite option', () => { + Cookies.set('token', '123', { sameSite: 'Lax' }); + + expect(Cookies.get('token')).toBe('123'); + }); + + it('should set cookie with domain option', () => { + Cookies.set('token', '123', { domain: 'localhost' }); + + expect(Cookies.get('token')).toBe('123'); + }); + + it('should set cookie with multiple options', () => { + const futureDate = new Date(); + futureDate.setDate(futureDate.getDate() + 7); + + Cookies.set('token', '123', { + path: '/', + expires: futureDate, + sameSite: 'Strict', + }); + + expect(Cookies.get('token')).toBe('123'); + }); + + it('should override default path option with custom path', () => { + window.history.replaceState({}, '', '/app'); + + Cookies.set('token', '123', { path: '/app' }); + + expect(Cookies.get('token')).toBe('123'); + expect(document.cookie).toContain('token=123'); + }); + + it('should set multiple cookies from a record', () => { + Cookies.set({ token: '123', user: 'tom' }); + + expect(Cookies.get('token')).toBe('123'); + expect(Cookies.get('user')).toBe('tom'); + }); + + it('should set multiple cookies from a record with shared options', () => { + Cookies.set({ token: '123', user: 'tom' }, { sameSite: 'Lax' }); + + expect(Cookies.get('token')).toBe('123'); + expect(Cookies.get('user')).toBe('tom'); + }); + + it('should set multiple cookies from a record with expires option', () => { + const futureDate = new Date(); + futureDate.setDate(futureDate.getDate() + 7); + + Cookies.set({ token: '123', user: 'tom' }, { expires: futureDate }); + + expect(Cookies.get('token')).toBe('123'); + expect(Cookies.get('user')).toBe('tom'); + }); + }); + + describe('remove', () => { + it('should remove cookie', () => { + Cookies.set('token', '123'); + + expect(Cookies.get('token')).toBe('123'); + + Cookies.remove('token'); + + expect(Cookies.get('token')).toBeUndefined(); + }); + + it('should remove cookie with custom path option', () => { + window.history.replaceState({}, '', '/app'); + + Cookies.set('token', '123', { path: '/app' }); + + expect(Cookies.get('token')).toBe('123'); + + Cookies.remove('token', { path: '/app' }); + + expect(Cookies.get('token')).toBeUndefined(); + }); + + it('should remove cookie with domain option', () => { + Cookies.set('token', '123', { domain: 'localhost' }); + + expect(Cookies.get('token')).toBe('123'); + + Cookies.remove('token', { domain: 'localhost' }); + + expect(Cookies.get('token')).toBeUndefined(); + }); + + it('should remove multiple cookies by array', () => { + Cookies.set('token', '123'); + Cookies.set('user', 'tom'); + + expect(Cookies.get('token')).toBe('123'); + expect(Cookies.get('user')).toBe('tom'); + + Cookies.remove(['token', 'user']); + + expect(Cookies.get('token')).toBeUndefined(); + expect(Cookies.get('user')).toBeUndefined(); + }); + + it('should remove multiple cookies with shared options', () => { + Cookies.set('token', '123', { path: '/app' }); + Cookies.set('user', 'tom', { path: '/app' }); + + Cookies.remove(['token', 'user'], { path: '/app' }); + + expect(Cookies.get('token')).toBeUndefined(); + expect(Cookies.get('user')).toBeUndefined(); + }); + }); + + describe('clear', () => { + it('should clear all cookies except specified keys', () => { + Cookies.set('token', '123'); + Cookies.set('userInfo', 'tom'); + Cookies.set('permission', 'admin'); + + Cookies.clear?.(['userInfo']); + + expect(Cookies.get('token')).toBeUndefined(); + expect(Cookies.get('userInfo')).toBe('tom'); + expect(Cookies.get('permission')).toBeUndefined(); + }); + + it('should clear cookies with custom path only when matching options are provided', () => { + window.history.replaceState({}, '', '/app'); + + Cookies.set('scopedToken', '123', { path: '/app' }); + Cookies.set('scopedKeep', 'tom', { path: '/app' }); + + Cookies.clear?.(['scopedKeep']); + + expect(Cookies.get('scopedToken')).toBe('123'); + expect(Cookies.get('scopedKeep')).toBe('tom'); + + Cookies.clear?.(['scopedKeep'], { path: '/app' }); + + expect(Cookies.get('scopedToken')).toBeUndefined(); + expect(Cookies.get('scopedKeep')).toBe('tom'); + }); + + it('should clear all cookies when no keys are provided', () => { + Cookies.set('token', '123'); + Cookies.set('userInfo', 'tom'); + Cookies.set('permission', 'admin'); + + Cookies.clear?.([]); + + expect(Cookies.get('token')).toBeUndefined(); + expect(Cookies.get('userInfo')).toBeUndefined(); + expect(Cookies.get('permission')).toBeUndefined(); + }); + + it('should pass domain option through to remove when clearing', () => { + Cookies.set('token', '123', { domain: 'localhost' }); + Cookies.set('userInfo', 'tom', { domain: 'localhost' }); + + Cookies.clear?.([], { domain: 'localhost' }); + + expect(Cookies.get('token')).toBeUndefined(); + expect(Cookies.get('userInfo')).toBeUndefined(); + }); + + it('should preserve specified keys while passing options through', () => { + window.history.replaceState({}, '', '/app'); + + Cookies.set('token', '123', { path: '/app' }); + Cookies.set('keep', 'keep', { path: '/app' }); + Cookies.set('info', 'tom', { path: '/app' }); + + Cookies.clear?.(['keep'], { path: '/app' }); + + expect(Cookies.get('token')).toBeUndefined(); + expect(Cookies.get('keep')).toBe('keep'); + expect(Cookies.get('info')).toBeUndefined(); + }); + }); +}); diff --git a/src/cookies/index.ts b/src/cookies/index.ts new file mode 100644 index 0000000..78c0b98 --- /dev/null +++ b/src/cookies/index.ts @@ -0,0 +1,138 @@ +import JSCookies from 'js-cookie'; + +import getTypeOfValue from '../getTypeOfValue'; + +const DEFAULT_OPTIONS = { + path: '/', +}; + +/** + * 统一封装的 Cookie 操作工具集。 + * + * @category Storage + * @description + * 基于 js-cookie 封装的浏览器 Cookie 操作工具,提供统一的默认配置和便捷方法。 + * 所有操作默认携带根路径,统一管理 Cookie 的读写与删除逻辑,简化项目中 Cookie 的使用。 + * + * @Methods + * | 方法名 | 描述 | 参数 | 返回值 | 使用方式 | + * |------|------|------|--------|--------| + * | `get` | 读取指定名称的 Cookie 值;不传名称时返回所有 Cookie | `name?: string` - Cookie 名称 | `string \| undefined \| Record` | `Cookies.get('username')` | + * | `set` | 设置 Cookie,并自动合并默认配置 | `name: string` - Cookie 名称
`value: string` - Cookie 值
`options?: JSCookies.CookieAttributes` - 可选配置
`pairs: Record` - 批量键值对 | `void` | `Cookies.set('username', 'john', { expires: 7 })`
`Cookies.set({ token: '123', user: 'tom' })` | + * | `remove` | 删除指定名称的 Cookie,并自动合并默认配置 | `name: string \| string[]` - Cookie 名称
`options?: JSCookies.CookieAttributes` - 可选配置 | `void` | `Cookies.remove('username')` | + * | `clear` | 清除 Cookie,可以选择性保留特定键 | `except?: string[]` - 可选的要保留的键数组 | `void` | `Cookies.clear(['username'])` | + * + * @example + * ```typescript + * import Cookies from './cookies'; + * + * // 读取所有 Cookie + * const allCookies = Cookies.get(); + * + * // 读取指定 Cookie + * const username = Cookies.get('username'); + * + * // 设置 Cookie + * Cookies.set('username', 'john', { expires: 7 }); + * + * // 批量设置 Cookie + * Cookies.set({ token: '123', user: 'tom' }); + * + * // 读取 Cookie + * const username = Cookies.get('username'); + * + * // 删除单个 Cookie + * Cookies.remove('username'); + * + * // 批量删除 Cookie + * Cookies.remove(['username', 'permission']); + * + * // 清除除 username 外的所有 Cookie + * Cookies.clear(['username'], { path: '/' }); + * ``` + */ +class Cookies { + /** + * @hidden + */ + constructor() {} + /** + * @hidden + * 读取所有 Cookie + */ + static get(): Record; + /** + * @hidden + * 读取指定名称的 Cookie 值 + * @param {string} name - Cookie 名称 + */ + static get(name: string): string | undefined; + static get(name?: string): Record | string | undefined { + return name ? JSCookies.get(name) : JSCookies.get(); + } + + /** + * @hidden + * 设置 Cookie,并合并默认根路径配置。 + */ + static set(name: string, value: string, options?: JSCookies.CookieAttributes): void; + /** + * @hidden + * 批量设置 Cookie,并合并默认根路径配置。 + */ + static set(pairs: Record, options?: JSCookies.CookieAttributes): void; + static set( + nameOrPairs: string | Record, + valueOrOptions?: string | JSCookies.CookieAttributes, + options?: JSCookies.CookieAttributes + ) { + if (typeof nameOrPairs === 'string') { + JSCookies.set(nameOrPairs, valueOrOptions as string, { + ...DEFAULT_OPTIONS, + ...options, + }); + } else if (getTypeOfValue(nameOrPairs) === 'object') { + const opts = (valueOrOptions as JSCookies.CookieAttributes) ?? {}; + Object.entries(nameOrPairs).forEach(([k, v]) => + JSCookies.set(k, v, { + ...DEFAULT_OPTIONS, + ...opts, + }) + ); + } + } + + /** + * @hidden + * 删除指定名称的 Cookie,并合并默认根路径配置。 + */ + static remove(name: string | string[], options?: JSCookies.CookieAttributes) { + if (typeof name === 'string') { + JSCookies.remove(name, { + ...DEFAULT_OPTIONS, + ...options, + }); + } else if (Array.isArray(name)) { + name.forEach((k) => + JSCookies.remove(k, { + ...DEFAULT_OPTIONS, + ...options, + }) + ); + } + } + + /** + * @hidden + * 清除 Cookie,同时可以选择性保留特定的键。 + */ + static clear(except?: string[], options?: JSCookies.CookieAttributes) { + const cookies = Cookies.get(); + + const keysToRemove = Object.keys(cookies).filter((key) => !except?.includes(key)); + + keysToRemove.forEach((key) => Cookies.remove(key, options)); + } +} + +export default Cookies; diff --git a/src/index.ts b/src/index.ts index 7c0a099..73ff114 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,11 @@ // ========== export some dependencies ========== export { default as dayjs } from 'dayjs'; export * as idb from 'idb'; -export { default as Cookie } from 'js-cookie'; export * as lodash from 'lodash-es'; // ========== export utils ========== export { default as checkBrowserSupport } from './checkBrowserSupport'; +export { default as Cookies } from './cookies'; export { default as copy } from './copy'; export { default as downloadFile } from './downloadFile'; export { default as formatBytes } from './formatBytes';