当前位置: 首页
前端开发
VueUse createInjectionState 类型安全封装 provide inject

VueUse createInjectionState 类型安全封装 provide inject

热心网友 时间:2026-06-20
转载

一、痛点:原生 provide/inject 类型安全写法为何如此繁琐?

跨层级共享状态时,Vue 官方提供了三种解决方案:props 逐层透传、provide/inject 依赖注入、以及 Pinia 全局状态管理。其中 provide/inject 最容易被低估——它专精于“组件子树内共享上下文”的场景,既比 props 透传简洁,又比 Pinia 轻量许多。

VueUse createInjectionState —— provide/inject 的类型安全封装

不过,但凡你亲手写过一次类型安全的 provide/inject,就一定会同意它也是最冗长的

来看一段教科书式的实现:

 复制代码// keys.ts
import type { InjectionKey, Ref } from 'vue'export interface CounterContext {
  count: Ref<number>
  double: Readonly<Ref<number>>
  increment: () => void
}// 1) 必须手动声明 InjectionKey 并配合 Symbol
export const counterKey: InjectionKey<CounterContext> = Symbol('counter')
 复制代码

 复制代码

具体问题集中于三个方面:

  1. 必须手动声明 InjectionKey,每增加一个上下文就多一个 Symbol;
  2. 类型需要在 key、provide、inject 三处保持一致,修改一处而忘了另外两处,就会静默失效;
  3. inject 的返回值默认是 T | undefined,业务代码中到处充斥着 !if (!ctx),要么接受不安全的非空断言,要么编写大量防御性代码。

VueUse 的 createInjectionState 正是针对这三点而生。它用一个 composable 函数,把“声明上下文 + 提供 + 注入”三项操作打包成 [useProvideXxx, useXxxState] 的配对 API。下面逐项详细拆解。

本文基于 VueUse v14.3.0(2026 年 5 月发布,要求 Vue 3.5+),所有代码示例均可直接运行。

二、API 详解:一对配对函数搞定所有事

2.1 最小示例

直接看官方的计数器示例(来自 VueUse 官方文档):

 复制代码// useCounterStore.ts
import { computed, shallowRef } from 'vue'
import { createInjectionState } from '@vueuse/core'const [useProvideCounterStore, useCounterStore] = createInjectionState(
  (initialValue: number) => {
    const count = shallowRef(initialValue)
    const double = computed(() => count.value * 2)
    function increment() {
      count.value++
    }
    return { count, double, increment }
  },
)export { useProvideCounterStore, useCounterStore }
 复制代码

 复制代码

与原生写法对比,差异一目了然:

  • 不再需要手动定义 InjectionKey——内部自动生成 Symbol;
  • 无需手动调用 provide / inject——这些逻辑封装在 useProvideXxx / useXxxState 内部;
  • 类型从 composable 返回值自动推导——你怎么编写,类型就怎么生成;
  • 支持传参——useProvideCounterStore(0)initialValue 直接透传给 composable,参数类型也由泛型推导。

2.2 配对设计哲学:明确角色边界

createInjectionState 返回的两个函数命名带有强烈的语义暗示:

  • useProvideXxx:在祖先组件中调用,表示“我来提供这套状态”;
  • useXxxState:在后代组件中调用,表示“我要消费这套状态”。

数组解构出的两个函数本质上是配对使用的:调用 useProvideXxx 时,内部执行 composable,并将返回值通过 provideLocal 注入到组件子树;调用 useXxxState 时,通过 injectLocal 取出。这种配对模式比单一 hook 更清晰——你一看代码就知道当前组件在状态树中扮演什么角色。

设计上还有一个常被忽略的细节:useProvideXxx 也会把 composable 的返回值作为普通函数返回。也就是说,provider 自身也可以直接使用这些状态:

 复制代码

这项特性从 v10.5.0 PR #3387 开始支持:允许 provide 和 inject 在同一组件中同时发生。在原生 API 中要做到这一点,你需要在 setup() 里手动 provide 完再 inject 一次,既繁琐又反直觉。

2.3 自定义 InjectionKey

默认情况下,key 是 Symbol(composable.name || 'InjectionState')——从 v10.8.0 之后,会自动使用 composable 的函数名作为 Symbol 描述符(PR #3788),便于调试时识别。

注意,每次调用 createInjectionState 都会生成新的 Symbol。模块级别声明一次,所有组件共用同一个 Symbol,这通常没有问题。但如果你需要:

  • 在多个独立打包产物之间共享同一个上下文(例如微前端场景);
  • 使用字符串 key 方便调试或 DevTools 识别;

可以传入 injectionKey 选项:

 复制代码import type { InjectionKey } from 'vue'const counterKey: InjectionKey<CounterStore> = Symbol('counter-store')const [useProvideCounterStore, useCounterStore] = createInjectionState(
  (initialValue: number) => { /* ... */ },
  { injectionKey: counterKey },
)// 也支持字符串
// { injectionKey: 'counter-store' }

字符串 key 是 Vue 原生 inject 所支持的,但这样一来会失去类型推导的桥梁,因此优先使用 InjectionKey 强类型

2.4 默认值与 throw 兜底:三种错误处理策略

当 provide 尚未调用时就去 inject,是最常见的运行时陷阱。createInjectionState 提供了三档应对策略:

策略 A:不做任何处理(默认)

 复制代码const counter = useCounterStore() // 类型: CounterStore | undefined
counter?.increment()

useXxxState 的返回类型默认包含 undefined,调用方需要自行处理。

策略 B:传入 defaultValue,自动兜底

该功能从 v10.10.0(PR #3902)开始新增。v14.3.0 进一步改进了类型(PR #5306)——一旦传了 defaultValue,返回类型就不再包含 undefined

 复制代码const [useProvideCounterStore, useCounterStore] = createInjectionState(
  (initialValue: number) => ({ /* ... */ }),
  {
    defaultValue: {
      count: shallowRef(0),
      double: shallowRef(0),
      increment: () => {},
    },
  },
)const { count, double, increment } = useCounterStore() // ✅ 类型中不再包含 undefined

适合“祖先没 provide 也能正常工作”的可选上下文场景。

策略 C:包裹一层抛错或回退函数

官方推荐的工程实践是:不要直接 export useCounterStore,而是额外封装一层

 复制代码// useCounterStore.ts
const [useProvideCounterStore, useCounterStoreRaw] = createInjectionState(
  (initialValue: number) => { /* ... */ },
)export { useProvideCounterStore }// 强制存在:祖先未提供上下文时直接抛错
export function useCounterStoreOrThrow() {
  const ctx = useCounterStoreRaw()
  if (ctx == null) {
    throw new Error(
      '[CounterStore] 请先在祖先组件调用 useProvideCounterStore',
    )
  }
  return ctx
}// 默认兜底:祖先未提供时返回缺省实现
export function useCounterStoreWithDefault() {
  return useCounterStoreRaw() ?? {
    count: shallowRef(0),
    double: shallowRef(0),
    increment: () => {},
  }
}

通过封装,让 inject 端彻底摆脱 undefined,业务代码中再也不需要到处都是 ! 断言。这正是 VueUse 官方文档推荐的模式。

三、源码原理浅析:本质是 provide/inject 的薄包装

打开 packages/shared/createInjectionState/index.ts,核心实现非常简短:

 复制代码import type { InjectionKey } from 'vue'
import { injectLocal } from '../injectLocal'
import { provideLocal } from '../provideLocal'export function createInjectionState<Arguments extends Array<any>, Return>(
  composable: (...args: Arguments) => Return,
  options?: CreateInjectionStateOptions<Return>,
) {
  // 1) 默认使用 composable 的函数名作为 Symbol 描述符
  const key: string | InjectionKey<Return> =
    options?.injectionKey || Symbol(composable.name || 'InjectionState')
  const defaultValue = options?.defaultValue  // 2) provider 端:执行 composable + provide
  const useProvidingState = (...args: Arguments) => {
    const state = composable(...args)
    provideLocal(key, state)
    return state
  }  // 3) consumer 端:从同一个 key 注入
  const useInjectedState = () => injectLocal(key, defaultValue)  return [useProvidingState, useInjectedState]
}

总共不到 20 行。但有两个值得关注的细节:

1. provideLocal / injectLocal,并非原生的 provide / inject

这是 VueUse 自己的小封装。原生的 Vue provide 不允许在同一个组件内,先 provide 完再 inject 同一个 key(取出来的仍然是上层的 provide 值)。provideLocal 通过将 provide 内容直接挂载到当前实例的本地缓存,使得“自身组件 inject 自身 provide 的值”成为可能。

这项能力来自 PR #3387,官方文档中“provider 自己也能用 store”那一段就是基于这一机制。

2. 函数重载提供两套类型签名。

 复制代码// 重载 1:传了 defaultValue,返回类型不含 undefined
export function createInjectionState<Args, Return>(
  composable: (...args: Args) => Return,
  options: { defaultValue: Return } & CreateInjectionStateOptions<Return>,
): CreateInjectionStateReturn<Args, Return, Return>// 重载 2:没传 defaultValue,返回类型含 undefined
export function createInjectionState<Args, Return>(
  composable: (...args: Args) => Return,
  options?: CreateInjectionStateOptions<Return>,
): CreateInjectionStateReturn<Args, Return, Return | undefined>

通过函数重载,defaultValue 的存在与否会精确反映在 useXxxState 的返回类型上。这是 v14.3.0 PR #5306 新增的能力——之前的版本无论是否传 defaultValue,类型都会带上 undefined,开发者不得不反复断言;现在终于“传了就保证有”。

理解到这一层,你就能感受到 VueUse 对类型安全的执着:接口未变,但开发体验提升了一个台阶

四、实战:四个跨层级状态共享的真实场景

4.1 表单上下文:让任意嵌套字段共享校验状态

复杂表单经常面临“嵌套区域 + 全局校验”的需求。比如下面这个用户资料表,外层控制整体禁用、是否正在提交、以及字段错误集合:

 复制代码// useFormContext.ts
import { computed, reactive, shallowRef } from 'vue'
import { createInjectionState } from '@vueuse/core'interface FormContextOptions {
  initialValues?: Record<string, any>
}const [useProvideFormContext, useFormContextRaw] = createInjectionState(
  (options: FormContextOptions = {}) => {
    const values = reactive<Record<string, any>>({ ...options.initialValues })
    const errors = reactive<Record<string, string>>({})
    const submitting = shallowRef(false)
    const disabled = shallowRef(false)    const isValid = computed(() => Object.keys(errors).length === 0)    function setValue(key: string, value: any) {
      values[key] = value
      delete errors[key]
    }    function setError(key: string, message: string) {
      errors[key] = message
    }    return { values, errors, submitting, disabled, isValid, setValue, setError }
  },
)export { useProvideFormContext }export function useFormContext() {
  const ctx = useFormContextRaw()
  if (!ctx) throw new Error('请在 
组件内使用表单字段') return ctx }
 复制代码

 复制代码

无论 嵌套多深、外层套了多少层 layout 容器,它始终能拿到表单上下文。整套机制比 Pinia 轻量得多——它只在这棵子树中生效,多个 实例互不干扰。

4.2 Tab 容器:父子组件解耦的经典场景

是 provide/inject 的经典用法。用 createInjectionState 改写:

 复制代码// useTabsContext.ts
import { shallowRef } from 'vue'
import { createInjectionState } from '@vueuse/core'const [useProvideTabsContext, useTabsContextRaw] = createInjectionState(
  (initial?: string) => {
    const active = shallowRef(initial ?? '')
    const tabs = shallowRef<{ name: string; label: string }[]>([])    function register(name: string, label: string) {
      if (!tabs.value.some(t => t.name === name)) {
        tabs.value = [...tabs.value, { name, label }]
        if (!active.value) active.value = name
      }
    }    function unregister(name: string) {
      tabs.value = tabs.value.filter(t => t.name !== name)
    }    return { active, tabs, register, unregister }
  },
)export { useProvideTabsContext }
export function useTabsContext() {
  const ctx = useTabsContextRaw()
  if (!ctx) throw new Error(' 必须在  内使用')
  return ctx
}
 复制代码

 复制代码

使用示例:

 复制代码
  
    
  
  
    
  

不需要了解 的内部实现,注册/注销都通过上下文完成。相比 的“声明式” props 配置,组合式 + provide/inject 更接近“插件化”的思维方式。

4.3 Stepper 步骤器:当前步、能否前进/后退

 复制代码// useStepperContext.ts
import { computed, shallowRef } from 'vue'
import { createInjectionState } from '@vueuse/core'const [useProvideStepperContext, useStepperContextRaw] = createInjectionState(
  (totalSteps: number) => {
    const current = shallowRef(0)    const isFirst = computed(() => current.value === 0)
    const isLast = computed(() => current.value === totalSteps - 1)    function next() {
      if (!isLast.value) current.value++
    }
    function prev() {
      if (!isFirst.value) current.value--
    }
    function goto(index: number) {
      if (index >= 0 && index < totalSteps) current.value = index
    }    return { current, isFirst, isLast, next, prev, goto }
  },
)export { useProvideStepperContext }
export function useStepperContext() {
  const ctx = useStepperContextRaw()
  if (!ctx) throw new Error(' 必须在  内使用')
  return ctx
}
 复制代码

 复制代码

与 Tab 同理, 完全不需要 props,它从上下文中自动获取所需信息。关键在于这种状态绝对不该上 Pinia——它与当前 实例强绑定,全局化反而成为负担。

4.4 表格行选:跨表头/行/批量操作栏共享选中集合

 复制代码// useSelectionContext.ts
import { computed, reactive } from 'vue'
import { createInjectionState } from '@vueuse/core'interface SelectionContextOptions {
  itemKey: (item: T) => string
}const [useProvideSelectionContext, useSelectionContextRaw] =
  createInjectionState((options: SelectionContextOptions) => {
    const selected = reactive(new Set<string>())    function isSelected(item: T) {
      return selected.has(options.itemKey(item))
    }    function toggle(item: T) {
      const key = options.itemKey(item)
      selected.has(key) ? selected.delete(key) : selected.add(key)
    }    function clear() {
      selected.clear()
    }    function selectAll(items: T[]) {
      items.forEach(item => selected.add(options.itemKey(item)))
    }    const count = computed(() => selected.size)    return { selected, isSelected, toggle, clear, selectAll, count }
  })export { useProvideSelectionContext }
export function useSelectionContext() {
  const ctx = useSelectionContextRaw()
  if (!ctx) throw new Error('请在  内使用')return ctx as ReturnType<typeof useSelectionContextRaw> & {} // 类型见下文
}
 复制代码

表头中的“全选”复选框、行中的“单选”、批量操作栏中的“已选 N 条”——三个部分分别在三个不同的 slot 中渲染,但都共享同一个选中状态。这正是 provide/inject 模式擅长的场景。

五、对比矩阵:跟 Pinia / props / createSharedComposable 怎么选

每隔半年就会出现“Pinia 和 provide/inject 选哪个”的争论。createInjectionState 的定位其实非常清晰——它是 provide/inject 的现代封装,而非 Pinia 的替代品。

方案作用范围实例隔离类型推导SSR 安全适合的场景
props 透传父子直接每实例独立✅ 显式定义✅ 安全一两层、字段有限
createInjectionState子树每个 provider 子树独立✅ 完整推导✅ 安全表单/Tab/Stepper/表格行选等“组件族”
createSharedComposable整个客户端所有调用共享同一份✅ 推导✅ SSR 自动降级为非共享全局鼠标位置、滚动状态等“单例资源”
createGlobalState整个客户端所有调用共享✅ 推导✅ 同上与 SharedComposable 类似,更轻量
Pinia应用级单例 store✅ 一流✅(带 hydration)用户身份、购物车、跨路由的业务状态

几条选型直觉:

  1. 状态生命周期跟随某个父组件createInjectionState 卸载时,所有内部字段的状态一同被回收,自然且安全。
  2. 状态需要在多个互相独立的实例间隔离createInjectionState。同一页面中的两个 绝不能共享 active tab;如果使用 Pinia,反而需要给每个 store 加 id,徒增复杂度。
  3. 状态确实是“全局唯一资源”createSharedComposable。例如 useMouse() 希望全局只注册一个 mousemove 监听器,用 createSharedComposable 包裹一层即可。
  4. 状态是业务状态、跨路由、需要持久化或调试 → 选用 Pinia。createInjectionState 不是 store,没有 devtools 集成、没有 SSR 序列化、没有时间旅行能力。

特别区分 createInjectionStatecreateSharedComposable,这两者最容易混淆——名字都带 “create”,都来自 @vueuse/shared

 复制代码// createSharedComposable:全应用单例
import { createSharedComposable, useMouse } from '@vueuse/core'
const useSharedMouse = createSharedComposable(useMouse)// CompA.vue
const { x, y } = useSharedMouse() // 第一次调用:注册 mousemove 事件
// CompB.vue
const { x, y } = useSharedMouse() // 复用上一次的状态,不再重复注册// createInjectionState:组件树作用域
const [useProvideXxx, useXxx] = createInjectionState(/* ... */)
// 必须先调用祖先组件的 useProvideXxx() 才能在后代组件中使用 useXxx()

核心区别createSharedComposable 是“全应用一份”,没有 provider 概念;createInjectionState 是“每个 provider 子树一份”,必须有祖先 provide 才能在后代中使用。生命周期上,createSharedComposable 在最后一个订阅者卸载时停止 effectScope;createInjectionState 的生命周期跟随 provider 组件。

createSharedComposable 官方文档:

createInjectionState 因为基于 provide/inject,天然就是请求隔离的,不需要此类降级。SSR 项目中更推荐 createInjectionState

六、坑与解

6.1 别忘了 provider 必须先挂载

createReusableTemplate 一样,使用顺序是硬性约束:

 复制代码

   
       


  
    
  

编写工具组件时,养成习惯:永远封装一个 useXxxOrThrow,让缺失 provider 时直接报错而非静默失败。这条建议来自官方文档。

6.2 异步 setup 中的 inject 时机

如果某个组件的 setupasync 的(例如包含 top-level await),需要确保 useXxxState() 调用发生在第一个 await 之前,否则会丢失当前组件实例:

 复制代码

这是 Vue 3 inject 自身的限制,并非 VueUse 的特殊行为。

6.3 别在 createInjectionState 内部用 onMounted 等 hooks 期望它跟 consumer 绑定

composable 函数体是在 provider 组件的 setup 阶段执行:

 复制代码const [useProvideXxx] = createInjectionState(() => {
  onMounted(() => {
    // 这是 provider 组件的 onMounted,并非 consumer
  })
  return { /* ... */ }
})

这与普通 composable 的行为一致,但由于 createInjectionState 的“消费”动作延迟到后代组件执行,新手容易误认为生命周期会跟着 consumer 走。记住:composable 体在 useProvideXxx() 被调用时立即执行,且只执行一次

6.4 多个 provider 的优先级

如果在祖先链上多次调用同一个 useProvideXxx,inject 拿到的是最近的祖先那一份。这是 Vue 原生行为,可以利用它做“覆盖式上下文”(例如某个区域内的表单禁用规则覆盖父级),但也容易在不经意间产生 bug。建议使用 DevTools 的 Provided / Injected 面板检查实际拿到的是哪一份。

6.5 SSR 下不要在 composable 里直接访问 window

createInjectionState 本身 SSR 安全,但你的 composable 函数体一旦访问 window / document,就会像原生 composable 一样引起错误。该写的 if (typeof window !== 'undefined') 检查必不可少。

七、版本要求与变更速览

据 VueUse createInjectionState Changelog:

版本关键变更
v14.3.0(2026-05-01)defaultValue 时返回类型不再含 undefined(#5306)
v14.0.0(2025-10)要求 Vue 3.5+,构建产物迁移
v13.6.0(2025-07-28)添加 @__NO_SIDE_EFFECTS__ 注解,改善 tree-shaking
v12.0.0-beta.1(2024-11-21)不再支持 Vue 2
v10.10.0(2024-05-27)新增 defaultValue 选项(#3902)
v10.8.0(2024-02-20)injectionKey 默认使用 composable 名称(#3788)
v10.5.0(2023-10-07)新增 injectionKey 选项(#3404);允许在同一组件中同时 provide 和 inject(#3387)

新项目直接安装 v14.3.0 最为稳妥:

 复制代码npm i @vueuse/core

如果你的项目仍使用 Vue 2,需要锁定在 v9.x 版本。

八、小结

createInjectionState 的价值不在于它能做什么——这些功能原生 provide/inject 也同样能做到。它的价值在于将那些重复的样板代码全部砍掉,并将 TypeScript 类型推导推进到极致

可以这样记忆它的定位:

  • 想让 provide/inject 更好写?createInjectionState
  • 想让一棵子树共享状态?createInjectionState,不要轻易上 Pinia。
  • 想让全局只有一份资源?createSharedComposable
  • 想做应用级业务状态? 用 Pinia。

它的 API 设计——[useProvideXxx, useXxxState] 配对——精准填补了“抽象成 store 又怕过度工程”的灰色地带:你既无需手写 InjectionKey 三件套,也不必引入完整的 store 库。该有类型有类型,该有错误处理有错误处理,该轻量轻量,完全符合 VueUse 一贯的“够用就好”哲学。

下次再遇到跨层级状态共享的场景,先问自己:这状态真的需要 Pinia 吗?还是它只跟某个父组件相关? 如果是后者,createInjectionState 应该是你的第一选择。


参考资料:

  • VueUse createInjectionState 官方文档:vueuse.org/shared/crea…
  • 源码(GitHub):github.com/vueuse/vueu…
  • 同组件 provide+inject PR:github.com/vueuse/vueu…
  • defaultValue 选项 PR:github.com/vueuse/vueu…
  • 默认值非 undefined 类型 PR(v14.3.0):github.com/vueuse/vueu…
  • createSharedComposable 官方文档:vueuse.org/shared/crea…
  • Vue 官方 Provide / Inject 指南:cn.vuejs.org/guide/compo…
  • Composables vs. Provide/Inject vs. Pinia — When to Use What:vueschool.io/articles/vu…
来源:https://juejin.cn/post/7652194092413845542

游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。

同类文章
更多
深入理解调用栈与异步任务的状态同步机制

深入理解调用栈与异步任务的状态同步机制

调用栈仅记录同步执行流,异步回调由事件循环调度、任务队列存放,闭包维持作用域。执行时新建上下文入栈,不复用旧栈帧。调用栈不主动同步异步状态,事件循环是真正的协调者。

时间:2026-07-08 06:57
HTML自定义元素实现跨组件通信的完整指南

HTML自定义元素实现跨组件通信的完整指南

自定义元素跨组件通信需依赖CustomEvent,且bubbles和composed必须同时设为true才能穿透ShadowDOM。事件监听应挂载在document或父容器等合适节点,避免使用DOM查找或data ARIA属性替代事件通信,从而确保事件正确穿越边界,保持跨组件通信的可靠性。

时间:2026-07-08 06:56
JavaScript static关键字在工具库构建中的应用

JavaScript static关键字在工具库构建中的应用

在JavaScript工具库构建中,static关键字将函数或常量挂载到类名上,无需实例化即可调用,适用于无状态操作如日期格式化、字符串截断、深克隆,并统一管理配置常量与实现工厂方法,但需避免依赖实例状态。

时间:2026-07-08 06:56
如何避免静态初始化块异常导致应用冷启动中断

如何避免静态初始化块异常导致应用冷启动中断

防范静态初始化失败导致应用中断,需用try-catch兜住异常并提供安全默认值;将高风险逻辑移出static块,改用延迟加载或Holder模式;显式控制初始化顺序并增强可观测性;诊断时直击ExceptionInInitializerError的根因。

时间:2026-07-08 06:56
虚拟DOM标签级diff比对优化过程详解

虚拟DOM标签级diff比对优化过程详解

虚拟DOMdiff以节点为单位,按层级同标签优先判断复用,通过key精准识别节点身份,配合细粒度属性更新及短路优化,避免全量递归比较,只更新变化的节点,跳过冗余计算,将时间复杂度从O(n³)降至接近O(n),从而大幅提升渲染性能。

时间:2026-07-08 06:56
热门专题
更多
刀塔传奇破解版无限钻石下载大全 刀塔传奇破解版无限钻石下载大全
洛克王国正式正版手游下载安装大全 洛克王国正式正版手游下载安装大全
思美人手游下载专区 思美人手游下载专区
好玩的阿拉德之怒游戏下载合集 好玩的阿拉德之怒游戏下载合集
不思议迷宫手游下载合集 不思议迷宫手游下载合集
百宝袋汉化组游戏最新合集 百宝袋汉化组游戏最新合集
jsk游戏合集30款游戏大全 jsk游戏合集30款游戏大全
宾果消消消原版下载大全 宾果消消消原版下载大全
  • 日榜
  • 周榜
  • 月榜