/**
* Vue 3 Provide/Inject 安全管ç†å·¥å…·
*
* è¿™ä¸ªæ¨¡å—æä¾›äº†ä¸€å¥—å®Œæ•´çš„ provide/inject 管ç†è§£å†³æ–¹æ¡ˆï¼Œè§£å†³äº†ä»¥ä¸‹é—®é¢˜ï¼š
* 1. 检查æŸä¸ª key 是å¦å·²ç»è¢«æ³¨å…¥è¿‡
* 2. ç¡®ä¿å¤–部注入的值具有更高优先级
* 3. 类型安全的注入和æä¾›
* 4. 防æ¢é‡å¤æ³¨å…¥å’Œæ„外覆盖
*
* @example 基本使用
* ```typescript
* // 在应用åˆå§‹åŒ–æ—¶
* appProvide(app, LayoutKey, MyLayout);
*
* // 在组件ä¸ä½¿ç”¨
* const Layout = useLayoutRequired(DefaultLayout);
* ```
*
* @example 外部覆盖
* ```typescript
* // 外部å¯ä»¥è¦†ç›–内部æä¾›çš„值
* appProvide(app, LayoutKey, CustomLayout, { override: true });
* ```
*/
import { inject, type InjectionKey, type App, type Component } from 'vue';
// 全局跟踪已æä¾›çš„ keys,用于é¿å…é‡å¤æ³¨å…¥å’Œæ£€æŸ¥æ³¨å…¥çжæ€
const globalProvidedKeys = new Set<string | symbol>();
/**
* 检查æŸä¸ª key 是å¦å·²ç»è¢« provide
*
* è¿™ä¸ªå‡½æ•°ç”¨äºŽåˆ¤æ–æŸä¸ªæ³¨å…¥é”®æ˜¯å¦å·²ç»åœ¨åº”用ä¸è¢«æä¾›è¿‡ï¼Œ
* å¯ä»¥é¿å…é‡å¤æ³¨å…¥æˆ–åœ¨éœ€è¦æ—¶è¿›è¡Œæ¡ä»¶æ€§æ³¨å…¥ã€‚
*
* @param key - è¦æ£€æŸ¥çš„æ³¨å…¥é”®ï¼Œå¯ä»¥æ˜¯å—符串或 Symbol
* @returns 如果键已ç»è¢«æä¾›åˆ™è¿”回 true,å¦åˆ™è¿”回 false
*
* @example
* ```typescript
* if (!hasProvided(LayoutKey)) {
* appProvide(app, LayoutKey, DefaultLayout);
* }
* ```
*
* @example 在é…置函数ä¸ä½¿ç”¨
* ```typescript
* const configure = (app, { hasProvided, provide }) => {
* if (hasProvided(LayoutKey)) {
* console.log('Layout already provided');
* } else {
* provide(app, LayoutKey, CustomLayout);
* }
* };
* ```
*/
export function hasProvided(key: string | symbol): boolean {
return globalProvidedKeys.has(key);
}
/**
* 安全地使用 inject,æä¾›é»˜è®¤å€¼å’Œç±»åž‹å®‰å…¨
*
* 这个函数是 Vue çš„ inject 函数的安全包装器,æä¾›äº†æ›´å¥½çš„类型安全性
* 和错误处ç†ã€‚它å¯ä»¥å¤„ç†å·¥åŽ‚å‡½æ•°ä½œä¸ºé»˜è®¤å€¼çš„æƒ…å†µã€‚
*
* @template T - 注入值的类型
* @param key - 注入键,å¯ä»¥æ˜¯ InjectionKeyã€å—符串或 Symbol
* @param defaultValue - 默认值,当注入失败时使用
* @param options - é…置选项
* @param options.treatDefaultAsFactory - 是å¦å°†é»˜è®¤å€¼å½“作工厂函数处ç†
* @returns 注入的值或默认值,å¯èƒ½ä¸º undefined
*
* @example 基本使用
* ```typescript
* const theme = safeInject(ThemeKey, { color: 'blue' });
* ```
*
* @example 使用工厂函数
* ```typescript
* const config = safeInject(
* ConfigKey,
* () => ({ mode: 'development' }),
* { treatDefaultAsFactory: true }
* );
* ```
*
* @example 在组件ä¸ä½¿ç”¨
* ```typescript
* const Layout = safeInject(LayoutKey, DefaultLayout);
* if (Layout) {
* // 安全地使用 Layout 组件
* }
* ```
*/
export function safeInject<T>(
key: InjectionKey<T> | string | symbol,
defaultValue?: T,
options?: {
treatDefaultAsFactory?: boolean;
}
): T | undefined {
const treatDefaultAsFactory = options?.treatDefaultAsFactory ?? false;
if (treatDefaultAsFactory && typeof defaultValue === 'function') {
return inject(key, defaultValue, true);
} else {
return inject(key, defaultValue);
}
}
/**
* 安全地使用 inject,确ä¿è¿”回值ä¸ä¸º undefined
*
* 这个函数是 safeInject çš„ä¸¥æ ¼ç‰ˆæœ¬ï¼Œå®ƒä¿è¯è¿”回值ä¸ä¼šæ˜¯ undefined。
* 如果注入失败且没有有效的默认值,它会抛出错误。
*
* @template T - 注入值的类型
* @param key - 注入键,å¯ä»¥æ˜¯ InjectionKeyã€å—符串或 Symbol
* @param defaultValue - 默认值,当注入失败时使用(必须æä¾›ï¼‰
* @param options - é…置选项
* @param options.treatDefaultAsFactory - 是å¦å°†é»˜è®¤å€¼å½“作工厂函数处ç†
* @returns 注入的值或默认值,ä¿è¯ä¸ä¸º undefined
* @throws {Error} 当注入失败且没有有效默认值时抛出错误
*
* @example 基本使用
* ```typescript
* // ç¡®ä¿æ€»æ˜¯æœ‰ Layout 组件å¯ç”¨
* const Layout = safeInjectRequired(LayoutKey, DefaultLayout);
* // Layout 现在ä¿è¯ä¸ä¸º undefined
* ```
*
* @example 使用工厂函数
* ```typescript
* const config = safeInjectRequired(
* ConfigKey,
* () => ({ mode: 'production' }),
* { treatDefaultAsFactory: true }
* );
* ```
*
* @example 在关键组件ä¸ä½¿ç”¨
* ```typescript
* const router = safeInjectRequired(RouterKey, createRouter());
* // 如果没有æä¾› router 且 createRouter() 返回 undefined,会抛出错误
* ```
*/
export function safeInjectRequired<T>(
key: InjectionKey<T> | string | symbol,
defaultValue: T,
options?: {
treatDefaultAsFactory?: boolean;
}
): T {
const result = safeInject(key, defaultValue, options);
if (result === undefined) {
throw new Error(`No provider found for injection "${String(key)}" and no valid default value provided`);
}
return result;
}
/**
* 应用级别的安全 provide
*
* 这个函数用于在应用级别安全地æä¾›ä¾èµ–注入值。它æä¾›äº†é‡å¤æ£€æŸ¥ã€
* 覆盖控制和跟踪功能,é¿å…æ„外的é‡å¤æ³¨å…¥æˆ–未授æƒçš„覆盖。
*
* @param app - Vue 应用实例
* @param key - 注入键,å¯ä»¥æ˜¯å—符串或 Symbol
* @param value - è¦æä¾›çš„å€¼
* @param options - é…置选项
* @param options.override - 是å¦å…许覆盖已å˜åœ¨çš„值,默认为 false
* @param options.track - 是å¦è·Ÿè¸ªè¿™ä¸ªé”®åˆ°å…¨å±€è®°å½•ä¸ï¼Œé»˜è®¤ä¸º true
* @returns 如果æˆåŠŸæä¾›åˆ™è¿”回 true,如果被阻æ¢ï¼ˆå·²å˜åœ¨ä¸”ä¸å…许覆盖)则返回 false
*
* @example 基本使用
* ```typescript
* // æä¾›é»˜è®¤å¸ƒå±€
* appProvide(app, LayoutKey, DefaultLayout);
* ```
*
* @example å…许覆盖
* ```typescript
* // 外部å¯ä»¥è¦†ç›–内部æä¾›çš„布局
* appProvide(app, LayoutKey, CustomLayout, { override: true });
* ```
*
* @example ä¸è·Ÿè¸ªçš„临时æä¾›
* ```typescript
* // æä¾›ä¸´æ—¶å€¼ï¼Œä¸è®°å½•到全局跟踪ä¸
* appProvide(app, 'temp-config', tempConfig, { track: false });
* ```
*
* @example 在é…置函数ä¸ä½¿ç”¨
* ```typescript
* const configure = (app, { provide }) => {
* // å°è¯•æä¾›è‡ªå®šä¹‰ä¸»é¢˜
* const success = provide(app, ThemeKey, customTheme);
* if (!success) {
* console.log('Theme already provided by framework');
* }
* };
* ```
*/
export function appProvide(
app: App,
key: string | symbol,
value: unknown,
options?: {
override?: boolean;
track?: boolean;
}
): boolean {
const { override = false, track = true } = options || {};
if (!override && hasProvided(key)) {
console.warn(`Key "${String(key)}" has already been provided. Use override: true to replace it.`);
return false;
}
app.provide(key, value);
if (track) {
globalProvidedKeys.add(key);
}
return true;
}
/**
* 创建类型安全的 injection key
*
* 这个函数用于创建类型安全的注入键,返回一个带有类型信æ¯çš„ Symbol。
* 使用 InjectionKey å¯ä»¥æä¾›æ›´å¥½çš„ TypeScript 类型推æ–和编译时检查。
*
* @template T - 注入值的类型
* @param description - 键的æè¿°ï¼Œç”¨äºŽè°ƒè¯•和日志记录
* @returns 类型安全的 InjectionKey
*
* @example 创建基本类型键
* ```typescript
* const ConfigKey = createInjectionKey<AppConfig>('AppConfig');
* ```
*
* @example 创建组件类型键
* ```typescript
* const ModalKey = createInjectionKey<Component>('ModalComponent');
* ```
*
* @example åˆ›å»ºå¤æ‚类型键
* ```typescript
* interface ThemeConfig {
* colors: Record<string, string>;
* fonts: string[];
* }
* const ThemeKey = createInjectionKey<ThemeConfig>('Theme');
* ```
*/
export function createInjectionKey<T>(description: string): InjectionKey<T> {
return Symbol(description) as InjectionKey<T>;
}
/**
* 预定义的 injection keys
*
* 这些是框架内置的常用注入键,为常è§çš„用例æä¾›äº†å¼€ç®±å³ç”¨çš„类型安全性。
*
* @example 使用预定义键
* ```typescript
* // åœ¨åº”ç”¨ä¸æä¾›å¸ƒå±€
* appProvide(app, LayoutKey, MyLayout);
*
* // 在组件ä¸ä½¿ç”¨
* const Layout = useLayoutRequired(DefaultLayout);
* ```
*/
/** 布局组件注入键 - 用于æä¾›å’Œæ³¨å…¥åº”用的主è¦å¸ƒå±€ç»„ä»¶ */
export const LayoutKey = createInjectionKey<Component>('Layout');
/** 主题é…置注入键 - 用于æä¾›å’Œæ³¨å…¥åº”用的主题é…ç½® */
export const ThemeKey = createInjectionKey<Record<string, unknown>>('Theme');
/** 应用é…置注入键 - 用于æä¾›å’Œæ³¨å…¥åº”用的全局é…ç½® */
export const ConfigKey = createInjectionKey<Record<string, unknown>>('Config');
/**
* Layout 相关的专用 composable
*
* 这个函数æä¾›äº†ä¸€ä¸ªä¾¿æ·çš„æ–¹å¼æ¥æ³¨å…¥å¸ƒå±€ç»„件,支æŒå¯é€‰çš„默认值。
* 如果没有æä¾›å¸ƒå±€ä¸”没有默认值,将返回 undefined。
*
* @template T - 布局组件的类型,继承自 Component
* @param defaultLayout - å¯é€‰çš„默认布局组件
* @returns 注入的布局组件或默认布局组件,å¯èƒ½ä¸º undefined
*
* @example 基本使用
* ```typescript
* const Layout = useLayout();
* if (Layout) {
* // 使用布局组件
* }
* ```
*
* @example 使用默认布局
* ```typescript
* const Layout = useLayout(DefaultLayout);
* // Layout 现在是注入的布局或 DefaultLayout
* ```
*
* @example åœ¨ç»„ä»¶ä¸æ¡ä»¶æ¸²æŸ“
* ```typescript
* <template>
* <component :is="Layout" v-if="Layout">
* <slot />
* </component>
* <div v-else>
* <!-- æ— å¸ƒå±€æ—¶çš„å›žé€€å†…å®¹ -->
* </div>
* </template>
*
* <script setup>
* const Layout = useLayout();
* </script>
* ```
*/
export function useLayout<T extends Component = Component>(defaultLayout?: T): T | undefined {
return safeInject(LayoutKey, defaultLayout) as T | undefined;
}
/**
* Layout 相关的专用 composable - 必须返回值版本
*
* 这个函数是 useLayout çš„ä¸¥æ ¼ç‰ˆæœ¬ï¼Œä¿è¯æ€»æ˜¯è¿”回一个布局组件。
* 如果没有注入布局且没有æä¾›æœ‰æ•ˆçš„默认值,将抛出错误。
*
* @template T - 布局组件的类型,继承自 Component
* @param defaultLayout - 必需的默认布局组件
* @returns 注入的布局组件或默认布局组件,ä¿è¯ä¸ä¸º undefined
* @throws {Error} å½“æ²¡æœ‰æ³¨å…¥å¸ƒå±€ä¸”é»˜è®¤å€¼æ— æ•ˆæ—¶æŠ›å‡ºé”™è¯¯
*
* @example 基本使用
* ```typescript
* const Layout = useLayoutRequired(DefaultLayout);
* // Layout ä¿è¯ä¸ä¸º undefined
* ```
*
* @example 在组件ä¸å®‰å…¨ä½¿ç”¨
* ```typescript
* <template>
* <component :is="Layout">
* <slot />
* </component>
* </template>
*
* <script setup>
* import DefaultLayout from './DefaultLayout.vue';
*
* const Layout = useLayoutRequired(DefaultLayout);
* // å¯ä»¥å®‰å…¨åœ°ä½¿ç”¨ Layoutï¼Œæ— éœ€æ£€æŸ¥ undefined
* </script>
* ```
*
* @example 在路由守å«ä¸ä½¿ç”¨
* ```typescript
* router.beforeEach((to, from, next) => {
* try {
* const Layout = useLayoutRequired(DefaultLayout);
* // ç¡®ä¿æœ‰å¯ç”¨çš„布局
* next();
* } catch (error) {
* console.error('No layout available:', error);
* next('/error');
* }
* });
* ```
*/
export function useLayoutRequired<T extends Component = Component>(defaultLayout: T): T {
return safeInjectRequired(LayoutKey, defaultLayout) as T;
}
/**
* èŽ·å–æ‰€æœ‰å·²æä¾›çš„ keys
*
* 这个函数返回一个åªè¯»æ•°ç»„ï¼ŒåŒ…å«æ‰€æœ‰å·²ç»é€šè¿‡ appProvide 函数æä¾›çš„æ³¨å…¥é”®ã€‚
* 主è¦ç”¨äºŽè°ƒè¯•ã€æ—¥å¿—记录或è¿è¡Œæ—¶æ£€æŸ¥ã€‚
*
* @returns 所有已æä¾›çš„æ³¨å…¥é”®çš„åªè¯»æ•°ç»„
*
* @example 调试使用
* ```typescript
* console.log('Provided keys:', getProvidedKeys());
* // 输出: ['Layout', 'Theme', 'Config']
* ```
*
* @example è¿è¡Œæ—¶æ£€æŸ¥
* ```typescript
* const providedKeys = getProvidedKeys();
* const hasTheme = providedKeys.includes(ThemeKey);
* if (!hasTheme) {
* console.warn('Theme not provided, using default');
* }
* ```
*
* @example 在开å‘工具ä¸ä½¿ç”¨
* ```typescript
* // 开呿¨¡å¼ä¸‹æš´éœ²åˆ°å…¨å±€å¯¹è±¡
* if (process.env.NODE_ENV === 'development') {
* window.__DEBUG_PROVIDED_KEYS__ = getProvidedKeys;
* }
* ```
*/
export function getProvidedKeys(): readonly (string | symbol)[] {
return Array.from(globalProvidedKeys);
}
|