依赖注入

当组件层级较深时,通过 props 一层层传递数据会非常繁琐。Vitarx 提供了 provide / inject 依赖注入机制,让祖先组件可以直接向任意后代组件传递数据,无需中间组件逐层转发。

provide — 提供依赖

provide 在当前组件中注册一个依赖,后代组件可以通过 inject 获取。

typescript
declare function provide(name: string | symbol, value: unknown): void
  • name:依赖的唯一标识,推荐使用字符串或 Symbol
  • value:要提供的数据值
tsx
import { provide, ref } from 'vitarx'

function Parent() {
  // 提供静态值
  provide('theme', 'dark')

  // 提供响应式值
  const count = ref(0)
  provide('count', count)

  return <Child />
}

provide 必须在组件函数的顶层调用(组件初始化阶段),不能在异步回调中使用。

inject — 注入依赖

inject 在后代组件中获取祖先组件提供的依赖数据。

typescript
// 不带默认值,返回 T | undefined
declare function inject<T>(name: string | symbol): T | undefined

// 带默认值
declare function inject<T>(name: string | symbol, defaultValue: T): T

// 默认值作为工厂函数
declare function inject<T>(
  name: string | symbol,
  defaultValue: () => T,
  treatDefaultAsFactory: true
): T
tsx
import { inject } from 'vitarx'

function Child() {
  // 注入依赖,不提供默认值时返回值可能为 undefined
  const theme = inject<string>('theme')

  // 注入依赖,提供默认值
  const size = inject<string>('size', 'medium')

  return <div class={`theme-#123;theme ?? 'light'} size-#123;size}`}>子组件</div>
}

inject 同样必须在组件函数的顶层调用,不能在异步回调中使用。

组件级注入

组件级注入是最常见的用法。祖先组件通过 provide 提供数据,后代组件通过 inject 获取数据。查找顺序是从当前组件向上逐层查找父组件,直到找到为止。

tsx
import { provide, inject, ref } from 'vitarx'

// 祖先组件
function GrandParent() {
  const userInfo = ref({ name: '小明', age: 18 })
  provide('userInfo', userInfo)

  return <Parent />
}

// 中间组件 — 无需关心 userInfo 的传递
function Parent() {
  return <Child />
}

// 后代组件 — 直接注入使用
function Child() {
  const userInfo = inject<{ name: string; age: number }>('userInfo')

  return (
    <div>
      <p>姓名:{userInfo?.name}</p>
      <p>年龄:{userInfo?.age}</p>
    </div>
  )
}

应用级注入

除了组件级注入,还可以在 App 实例上提供全局依赖。当组件级注入找不到数据时,会继续在应用级依赖中查找。

tsx
import { createApp, inject } from 'vitarx'

function App() {
  // 组件中可以通过 inject 获取 app 级别的依赖
  const apiBase = inject<string>('apiBase')
  return <div>API 地址:{apiBase}</div>
}

// 在创建应用时提供全局依赖
const app = createApp(App)
app.provide('apiBase', 'https://api.example.com')
app.mount(document.getElementById('root')!)

应用级注入的查找优先级低于组件级注入:组件级 > 应用级

响应式注入

provide 提供的是响应式数据(如 refreactive)时,inject 获取到的也是同一个响应式引用,因此数据变化会自动同步。

tsx
import { provide, inject, ref } from 'vitarx'

// 祖先组件
function ThemeProvider() {
  const theme = ref('light')

  const toggleTheme = () => {
    theme.value = theme.value === 'light' ? 'dark' : 'light'
  }

  provide('theme', theme)
  provide('toggleTheme', toggleTheme)

  return <Child />
}

// 后代组件
function ThemedButton() {
  const theme = inject<import('vitarx').Ref<string>>('theme')
  const toggleTheme = inject<() => void>('toggleTheme')

  return (
    <button class={`btn btn-#123;theme?.value ?? 'light'}`} onClick={toggleTheme}>
      切换主题
    </button>
  )
}

treatDefaultAsFactory 参数

当默认值是一个函数时,inject 会把它当作普通值而不是工厂函数。如果你希望默认值作为工厂函数执行,需要将第三个参数 treatDefaultAsFactory 设为 true

tsx
import { inject, reactive } from 'vitarx'

function Child() {
  // ❌ 问题:inject 会把函数当作默认值,而不是执行它
  // const config = inject('config', () => ({ debug: false }))

  // ✅ 正确:使用 treatDefaultAsFactory 让默认值作为工厂函数执行
  const config = inject('config', () => ({ debug: false }), true)

  return <div>调试模式:{config.debug ? '开启' : '关闭'}</div>
}

使用 Symbol 作为注入键

在大型项目中,推荐使用 Symbol 作为注入键,避免命名冲突:

tsx
import { provide, inject, type Ref } from 'vitarx'

// 在共享模块中定义 Symbol 键
export const ThemeKey = Symbol('theme')
export const UserKey = Symbol('user')

// 提供方
function App() {
  provide(ThemeKey, 'dark')
  provide(UserKey, { name: '小明', age: 18 })
  return <Child />
}

// 注入方
function Child() {
  const theme = inject<string>(ThemeKey, 'light')
  const user = inject<{ name: string; age: number }>(UserKey)
  return (
    <div>
      {theme} - {user?.name}
    </div>
  )
}

完整示例

下面是一个完整的主题切换示例,展示了依赖注入的核心用法:

tsx
import { provide, inject, ref, type Ref } from 'vitarx'

// 用 Symbol 作为注入键,避免命名冲突
const ThemeKey = Symbol('theme')
const ToggleThemeKey = Symbol('toggleTheme')

// 主题提供者 — 在顶层提供主题数据
function ThemeProvider() {
  const theme = ref<'light' | 'dark'>('light')

  const toggleTheme = () => {
    theme.value = theme.value === 'light' ? 'dark' : 'light'
  }

  // 提供响应式主题和切换函数
  provide(ThemeKey, theme)
  provide(ToggleThemeKey, toggleTheme)

  return (
    <div class={`app theme-#123;theme}`}>
      <Header />
      <Main />
      <Footer />
    </div>
  )
}

// 头部组件 — 深层嵌套,但可以直接注入主题
function Header() {
  const theme = inject<Ref<'light' | 'dark'>>(ThemeKey)
  const toggleTheme = inject<() => void>(ToggleThemeKey)

  return (
    <header>
      <h1>我的应用</h1>
      <button onClick={toggleTheme}>当前主题:{theme?.value},点击切换</button>
    </header>
  )
}

// 主内容区
function Main() {
  const theme = inject<Ref<'light' | 'dark'>>(ThemeKey)

  return (
    <main>
      <p>当前使用 {theme?.value} 主题</p>
    </main>
  )
}

// 底部组件
function Footer() {
  const toggleTheme = inject<() => void>(ToggleThemeKey)

  return (
    <footer>
      <button onClick={toggleTheme}>切换主题</button>
    </footer>
  )
}

下一步