Vue开发三年,我才发现依赖注入的TypeScript正确打开方式
<h1 data-id="heading-0">🧑💻 写在开头</h1><p>点赞 + 收藏 === 学会🤣🤣🤣</p>
<div>
<div>
<p>你是不是也遇到过这样的场景?</p>
<p>在Vue项目里,为了跨组件传递数据,你用<code>provide</code>和<code>inject</code>写了一套祖孙通信逻辑。代码跑起来没问题,但TypeScript编辑器总给你画红线,要么是“类型any警告”,要么就是“属性不存在”的错误提示。</p>
<p>你看着一片飘红的代码区,心里想着:“功能能用就行,类型标注太麻烦了。”于是,你默默地加上了<code>// @ts-ignore</code>,或者干脆把注入的值断言成<code>any</code>。项目在跑,但心里总觉得不踏实,像是在代码里埋下了一个个“类型地雷”。</p>
<p>别担心,这几乎是每个Vue + TypeScript开发者都会经历的阶段。今天这篇文章,就是来帮你彻底拆掉这些地雷的。</p>
<p>我会带你从最基础的<code>any</code>警告开始,一步步升级到类型安全、重构友好的最佳实践。读完这篇文章,你不仅能解决眼下的类型报错,更能建立一套完整的、类型安全的Vue依赖注入体系。无论你是维护大型中后台系统,还是开发独立的组件库,这套方法都能让你的代码更可靠、协作更顺畅。</p>
<h2 data-id="heading-0">为什么你的Provide/Inject总在报类型错误?</h2>
<p>让我们先看一个非常典型的“反面教材”。相信不少朋友都写过,或者见过下面这样的代码:</p>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 祖辈组件 - Grandparent.vue
<script setup lang="ts">
import { provide } from 'vue'
// 提供一些配置和方法
const appConfig = {
theme: 'dark',
apiBaseUrl: 'https://api.example.com'
}
const updateTheme = (newTheme: string) => {
console.log(`切换主题到:${newTheme}`)
}
// 简单粗暴的provide
provide('appConfig', appConfig)
provide('updateTheme', updateTheme)
</script></pre>
</div>
然后在子孙组件里这样注入:<br>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 子孙组件 - Child.vue
<script setup lang="ts">
import { inject } from 'vue'
// 问题来了:类型是什么?编辑器不知道!
const config = inject('appConfig')
const updateFn = inject('updateTheme')
// 当你尝试使用的时候
const switchTheme = () => {
// 这里TypeScript会抱怨:updateFn可能是undefined
// 而且config也是any类型,没有任何类型提示
updateFn('light')// ❌ 对象可能为"undefined"
console.log(config.apiBaseUrl) // ❌ config是any,但能运行
}
</script></pre>
</div>
<div>
<div>
<p>看出来问题在哪了吗?</p>
<ol>
<li><strong>字符串键名容易写错</strong>:<code>'appConfig'</code>和<code>'appconfig'</code>大小写不同,但TypeScript不会帮你检查这个拼写错误</li>
<li><strong>注入值的类型完全丢失</strong>:<code>inject</code>返回的类型默认是<code>any</code>或者<code>unknown</code>,你辛辛苦苦定义的类型信息在这里断掉了</li>
<li><strong>缺乏安全性</strong>:如果上游没有提供对应的值,<code>inject</code>会返回<code>undefined</code>,但TypeScript无法确定这种情况</li>
</ol>
<p>这就是为什么我们需要给Provide/Inject加上“类型安全带”。</p>
<h2 data-id="heading-1">从基础到进阶:四种类型标注方案</h2>
<h3 data-id="heading-2">方案一:使用泛型参数(基础版)</h3>
<p>这是最直接的方式,直接在<code>inject</code>调用时指定期望的类型。</p>
</div>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 子孙组件
<script setup lang="ts">
import { inject } from 'vue'
// 使用泛型告诉TypeScript:我期望得到这个类型
const config = inject<{ theme: string; apiBaseUrl: string }>('appConfig')
const updateFn = inject<(theme: string) => void>('updateTheme')
// 现在有类型提示了!
const switchTheme = () => {
if (config && updateFn) {
updateFn('light')// ✅ 正确识别为函数
console.log(config.apiBaseUrl) // ✅ 知道apiBaseUrl是string
}
}
</script></pre>
</div>
<div>
<div>
<p>这种方法像是给TypeScript递了一张“期望清单”:“我希望拿到一个长这样的对象”。但缺点也很明显:</p>
<ul>
<li>类型定义是重复的(祖辈组件定义一次,每个注入的子孙组件都要写一次)</li>
<li>键名还是字符串,容易拼写错误</li>
<li>每次都要手动做空值检查</li>
</ul>
<h3 data-id="heading-3">方案二:定义统一的注入键(进阶版)</h3>
<p>我们可以定义专门的常量来管理所有的注入键,就像管理路由名称一样。</p>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 首先,在一个单独的文件里定义所有注入键
// src/constants/injection-keys.ts
export const InjectionKeys = {
APP_CONFIG: Symbol('app-config'), // 使用Symbol确保唯一性
UPDATE_THEME: Symbol('update-theme'),
USER_INFO: Symbol('user-info')
} as const// as const 让TypeScript知道这是字面量类型</pre>
</div>
<p>然后在祖辈组件中使用:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// Grandparent.vue
<script setup lang="ts">
import { provide } from 'vue'
import { InjectionKeys } from '@/constants/injection-keys'
interface AppConfig {
theme: 'light' | 'dark'
apiBaseUrl: string
}
const appConfig: AppConfig = {
theme: 'dark',
apiBaseUrl: 'https://api.example.com'
}
const updateTheme = (newTheme: AppConfig['theme']) => {
console.log(`切换主题到:${newTheme}`)
}
// 使用Symbol作为键
provide(InjectionKeys.APP_CONFIG, appConfig)
provide(InjectionKeys.UPDATE_THEME, updateTheme)
</script></pre>
</div>
在子孙组件中注入:<br>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// Child.vue
<script setup lang="ts">
import { inject } from 'vue'
import { InjectionKeys } from '@/constants/injection-keys'
// 类型安全地注入
const config = inject(InjectionKeys.APP_CONFIG)
const updateFn = inject(InjectionKeys.UPDATE_THEME)
// TypeScript现在知道config的类型是AppConfig | undefined
const switchTheme = () => {
if (config && updateFn) {
updateFn('light')// ✅ 正确:'light'在主题范围内
// updateFn('blue') // ❌ 错误:'blue'不是有效主题
}
}
</script></pre>
</div>
<div>
<div>
<p>这个方法解决了键名拼写错误的问题,但类型定义仍然分散在各处。而且,如果你修改了<code>AppConfig</code>接口,需要在多个地方更新类型引用。</p>
<h3 data-id="heading-4">方案三:类型安全的注入工具函数(专业版)</h3>
<p>这是我在大型项目中推荐的做法。我们创建一组工具函数,让Provide/Inject变得像调用API一样类型安全。</p>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// src/utils/injection-utils.ts
import { InjectionKey, provide, inject } from 'vue'
// 定义一个创建注入键的工具函数
export function createInjectionKey<T>(key: string): InjectionKey<T> {
return Symbol(key) as InjectionKey<T>
}
// 再定义一个类型安全的provide函数
export function safeProvide<T>(key: InjectionKey<T>, value: T) {
provide(key, value)
}
// 以及类型安全的inject函数
export function safeInject<T>(key: InjectionKey<T>): T
export function safeInject<T>(key: InjectionKey<T>, defaultValue: T): T
export function safeInject<T>(key: InjectionKey<T>, defaultValue?: T): T {
const injected = inject(key, defaultValue)
if (injected === undefined) {
throw new Error(`注入键 ${key.toString()} 没有被提供`)
}
return injected
}</pre>
</div>
如何使用这套工具?<br>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 首先,在一个集中位置定义所有注入类型和键
// src/types/injection.types.ts
import { createInjectionKey } from '@/utils/injection-utils'
export interface AppConfig {
theme: 'light' | 'dark'
apiBaseUrl: string
}
export interface UserInfo {
id: number
name: string
avatar: string
}
// 创建类型安全的注入键
export const APP_CONFIG_KEY = createInjectionKey<AppConfig>('app-config')
export const USER_INFO_KEY = createInjectionKey<UserInfo>('user-info')
export const UPDATE_THEME_KEY = createInjectionKey<(theme: AppConfig['theme']) => void>('update-theme')</pre>
</div>
<p>在祖辈组件中提供值:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// Grandparent.vue
<script setup lang="ts">
import { safeProvide } from '@/utils/injection-utils'
import { APP_CONFIG_KEY, USER_INFO_KEY, UPDATE_THEME_KEY, type AppConfig } from '@/types/injection.types'
const appConfig: AppConfig = {
theme: 'dark',
apiBaseUrl: 'https://api.example.com'
}
const userInfo = {
id: 1,
name: '张三',
avatar: 'https://example.com/avatar.jpg'
}
const updateTheme = (newTheme: AppConfig['theme']) => {
console.log(`切换主题到:${newTheme}`)
}
// 现在provide是类型安全的
safeProvide(APP_CONFIG_KEY, appConfig)
safeProvide(USER_INFO_KEY, userInfo)// ✅ 自动检查userInfo是否符合UserInfo接口
safeProvide(UPDATE_THEME_KEY, updateTheme)
</script></pre>
</div>
<p>在子孙组件中注入:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// Child.vue
<script setup lang="ts">
import { safeInject } from '@/utils/injection-utils'
import { APP_CONFIG_KEY, UPDATE_THEME_KEY } from '@/types/injection.types'
// 看!这里没有泛型参数,但类型完全正确
const config = safeInject(APP_CONFIG_KEY)
const updateFn = safeInject(UPDATE_THEME_KEY)
// 直接使用,不需要空值检查
const switchTheme = () => {
updateFn('light')// ✅ 完全类型安全,且不会undefined
console.log(`当前API地址:${config.apiBaseUrl}`)
}
</script></pre>
</div>
<div>
<div>
<p>这种方案的优点是:</p>
<ol>
<li><strong>类型推导自动完成</strong>:不需要手动写泛型</li>
<li><strong>编译时检查</strong>:如果你提供的值类型不对,TypeScript会在<code>safeProvide</code>那行就报错</li>
<li><strong>运行时安全</strong>:如果注入键没有被提供,会抛出清晰的错误信息</li>
<li><strong>重构友好</strong>:修改接口定义时,所有使用的地方都会自动更新</li>
</ol>
<h3 data-id="heading-5">方案四:组合式API风格(现代最佳实践)</h3>
<p>Vue 3的组合式API让我们的代码可以更好地组织和复用。对于依赖注入,我们可以创建专门的<code>useXxx</code>函数。</p>
<div class="code-block-extension-header">
<div class="code-block-extension-headerLeft">
<div class="code-block-extension-foldBtn">
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// src/composables/useAppConfig.ts
import { safeProvide, safeInject } from '@/utils/injection-utils'
import { APP_CONFIG_KEY, UPDATE_THEME_KEY, type AppConfig } from '@/types/injection.types'
// 提供者逻辑封装
export function useProvideAppConfig(config: AppConfig, updateThemeFn: (theme: AppConfig['theme']) => void) {
safeProvide(APP_CONFIG_KEY, config)
safeProvide(UPDATE_THEME_KEY, updateThemeFn)
// 返回一些可能需要的方法
return {
// 这里可以添加一些基于config的衍生逻辑
getThemeColor() {
return config.theme === 'dark' ? '#1a1a1a' : '#ffffff'
}
}
}
// 消费者逻辑封装
export function useAppConfig() {
const config = safeInject(APP_CONFIG_KEY)
const updateTheme = safeInject(UPDATE_THEME_KEY)
// 计算属性:自动响应式
const isDarkTheme = computed(() => config.theme === 'dark')
// 方法:封装业务逻辑
const toggleTheme = () => {
const newTheme = config.theme === 'dark' ? 'light' : 'dark'
updateTheme(newTheme)
}
return {
config,
updateTheme,
isDarkTheme,
toggleTheme
}
}</pre>
</div>
</div>
</div>
</div>
</div>
</div>
<p>在祖辈组件中使用:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// Grandparent.vue
<script setup lang="ts">
import { useProvideAppConfig } from '@/composables/useAppConfig'
const appConfig = {
theme: 'dark' as const,
apiBaseUrl: 'https://api.example.com'
}
const updateTheme = (newTheme: 'light' | 'dark') => {
console.log(`切换主题到:${newTheme}`)
}
// 一行代码完成所有provide
const { getThemeColor } = useProvideAppConfig(appConfig, updateTheme)
</script></pre>
</div>
<p>在子孙组件中使用:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// Child.vue
<script setup lang="ts">
import { useAppConfig } from '@/composables/useAppConfig'
// 像使用Vue内置的useRoute、useRouter一样
const { config, isDarkTheme, toggleTheme } = useAppConfig()
// 直接使用,所有类型都已正确推断
const handleClick = () => {
toggleTheme()
console.log(`当前主题:${config.theme}`)
}
</script></pre>
</div>
<div>
<div>
<p>这种方式的强大之处在于:</p>
<ol>
<li><strong>逻辑高度复用</strong>:注入逻辑被封装起来,可以在多个组件中复用</li>
<li><strong>开箱即用</strong>:使用者不需要关心注入的实现细节</li>
<li><strong>类型完美推断</strong>:所有返回的值都有正确的类型</li>
<li><strong>易于测试</strong>:可以单独测试<code>useAppConfig</code>的逻辑</li>
</ol>
<h2 data-id="heading-6">实战:在组件库中应用类型安全注入</h2>
<p>假设你正在开发一个UI组件库,需要提供主题配置、国际化、尺寸配置等全局设置。依赖注入是完美的解决方案。</p>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 组件库的核心注入类型定义
// ui-library/src/injection/types.ts
export interface Theme {
primaryColor: string
backgroundColor: string
textColor: string
borderRadius: string
}
export interface Locale {
language: string
messages: Record<string, string>
}
export interface Size {
small: string
medium: string
large: string
}
export interface LibraryConfig {
theme: Theme
locale: Locale
size: Size
zIndex: {
modal: number
popover: number
tooltip: number
}
}
// 创建注入键
export const LIBRARY_CONFIG_KEY = createInjectionKey<LibraryConfig>('library-config')
// 组件库的provide函数
export function provideLibraryConfig(config: Partial<LibraryConfig>) {
const defaultConfig: LibraryConfig = {
theme: {
primaryColor: '#1890ff',
backgroundColor: '#ffffff',
textColor: '#333333',
borderRadius: '4px'
},
locale: {
language: 'zh-CN',
messages: {}
},
size: {
small: '24px',
medium: '32px',
large: '40px'
},
zIndex: {
modal: 1000,
popover: 500,
tooltip: 300
}
}
const mergedConfig = { ...defaultConfig, ...config }
safeProvide(LIBRARY_CONFIG_KEY, mergedConfig)
return mergedConfig
}
// 组件库的inject函数
export function useLibraryConfig() {
const config = safeInject(LIBRARY_CONFIG_KEY)
return {
config,
// 一些便捷的getter
theme: computed(() => config.theme),
size: computed(() => config.size),
locale: computed(() => config.locale),
// 主题相关的方法
setPrimaryColor(color: string) {
// 这里可以实现主题切换逻辑
config.theme.primaryColor = color
}
}
}</pre>
</div>
<p>在应用中使用你的组件库:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// App.vue - 应用入口
<script setup lang="ts">
import { provideLibraryConfig } from 'your-ui-library'
// 配置你的组件库
provideLibraryConfig({
theme: {
primaryColor: '#ff6b6b',// 自定义主题色
borderRadius: '8px' // 更大的圆角
},
locale: {
language: 'en-US',
messages: {
'button.confirm': 'Confirm',
'button.cancel': 'Cancel'
}
}
})
</script></pre>
</div>
在组件库的按钮组件中使用:<br>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// ui-library/src/components/Button/Button.vue
<script setup lang="ts">
import { useLibraryConfig } from '../../injection'
const { theme, size } = useLibraryConfig()
// 使用注入的配置
const buttonStyle = computed(() => ({
backgroundColor: theme.value.primaryColor,
borderRadius: theme.value.borderRadius,
height: size.value.medium
}))
</script>
<template>
<button :style="buttonStyle" class="library-button">
<slot></slot>
</button>
</template></pre>
</div>
<div>
<div>
<p>这样,你的组件库就拥有了完全类型安全的配置系统。使用者可以享受完整的TypeScript支持,包括智能提示、类型检查和自动补全。</p>
<h2 data-id="heading-7">避坑指南:常见问题与解决方案</h2>
<p>在实践过程中,你可能会遇到一些特殊情况。这里我总结了几种常见问题的解法。</p>
<h3 data-id="heading-8">问题一:注入值可能是异步获取的</h3>
<p>有时候,我们需要注入的值是通过API异步获取的。这时候直接注入Promise不是一个好主意,因为每个注入的组件都需要处理Promise。</p>
<p>更好的做法是使用响应式状态:</p>
</div>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 祖辈组件
<script setup lang="ts">
import { ref, provide } from 'vue'
import { USER_INFO_KEY } from '@/types/injection.types'
// 使用ref来管理异步状态
const userInfo = ref<{ id: number; name: string } | null>(null)
// 异步获取数据
fetchUserInfo().then(data => {
userInfo.value = data
})
// 直接注入ref,子孙组件可以响应式地访问
provide(USER_INFO_KEY, userInfo)
</script>
// 子孙组件
<script setup lang="ts">
import { inject } from 'vue'
import { USER_INFO_KEY } from '@/types/injection.types'
const userInfoRef = inject(USER_INFO_KEY)
// 使用计算属性来安全访问
const userName = computed(() => userInfoRef?.value?.name ?? '加载中...')
</script></pre>
</div>
<h3 data-id="heading-9">问题二:需要注入多个同类型的值</h3>
<p>如果需要在同一个应用中注入多个同类型的对象(比如多个数据源),可以使用工厂函数模式:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 创建带标识符的注入键
export function createDataSourceKey(id: string) {
return createInjectionKey<DataSource>(`data-source-${id}`)
}
// 在祖辈组件中
provide(createDataSourceKey('user'), userDataSource)
provide(createDataSourceKey('product'), productDataSource)
// 在子孙组件中
const userSource = safeInject(createDataSourceKey('user'))
const productSource = safeInject(createDataSourceKey('product'))</pre>
</div>
<h3 data-id="heading-10">问题三:类型循环依赖问题</h3>
<p>在大型项目中,可能会遇到类型之间的循环依赖。这时可以使用TypeScript的<code>interface</code>前向声明:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// types/moduleA.ts
import type { ModuleB } from './moduleB'
export interface ModuleA {
name: string
b: ModuleB// 引用ModuleB类型
}
// types/moduleB.ts
import type { ModuleA } from './moduleA'
export interface ModuleB {
id: number
a?: ModuleA// 可选,避免强制循环
}</pre>
</div>
<p>或者在注入键中使用泛型:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">export function createModuleKey<T>() {
return createInjectionKey<T>('module')
}
// 使用时各自指定具体类型
provide(createModuleKey<ModuleA>(), moduleAInstance)</pre>
</div>
<div>
<div>
<h2 data-id="heading-11">结语:拥抱类型安全的Vue开发</h2>
<p>回顾我们今天的旅程,我们从最开始的<code>any</code>类型警告,一步步升级到了类型安全、工程化的依赖注入方案。</p>
<p>让我为你总结一下关键要点:</p>
<ol>
<li>
<p><strong>永远不要忽略类型</strong>:那些<code>// @ts-ignore</code>注释就像是代码中的定时炸弹,总有一天会爆炸</p>
</li>
<li>
<p><strong>选择合适的方案</strong>:</p>
<ul>
<li>小项目:方案一或方案二就足够</li>
<li>中大型项目:强烈推荐方案三或方案四</li>
<li>组件库开发:方案四的组合式API模式是最佳选择</li>
</ul>
</li>
<li>
<p><strong>建立代码规范</strong>:在团队中统一依赖注入的写法,会让协作顺畅很多</p>
</li>
<li>
<p><strong>利用工具函数</strong>:花点时间封装<code>safeProvide</code>和<code>safeInject</code>这样的工具函数,长期来看会节省大量时间</p>
</li>
</ol></div>
<div>
<h3 id="tid-D8HBxE">如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。</h3>
</div>
<p><em><img src="https://img2024.cnblogs.com/blog/2149129/202501/2149129-20250122165814748-630765389.png" alt="" loading="lazy"></em></p>
</div><br><br>
来源:https://www.cnblogs.com/smileZAZ/p/19470105
頁:
[1]