- 前端
- UI组件
【免费下载链接】naive-ui
A Vue 3 Component Library. Fairly Complete. Theme Customizable. Uses TypeScript. Fast.
Naive UI 从 v2.29.0 起提供了createDiscreteApi,用于在组件setup之外(如工具函数、路由守卫、事件回调、HTTP 拦截器等纯 TypeScript/JavaScript 环境中)创建useMessage、useDialog、useNotification、useLoadingBar、useModal五类命令式 API。本文将完整讲解其签名、配置项、底层运行原理与注意事项,并给出可直接复制运行的完整示例,帮助你彻底告别"脱离上下文的 API 调用"这一痛点。
为什么需要 Discrete API
在 Naive UI 中,useMessage、useDialog、useNotification、useLoadingBar、useModal这些 API 都是基于 Vue 的依赖注入(inject)实现的,它们必须从组件树中最近的n-xxx-provider上读取注入值。以useMessage为例,其源码(use-message.ts)如下:
export function useMessage(): MessageApiInjection { const api = inject(messageApiInjectionKey, null) if (api === null) { throwError( 'use-message', 'No outer <n-message-provider /> founded.' ) } return api }也就是说:只要在setup之外调用useMessage,inject就取不到值并抛出 "No outer provider founded" 错误。useDialog(composables.ts)、useNotification(use-notification.ts)、useLoadingBar(use-loading-bar.ts)、useModal(composables.ts)的实现机制完全相同。
createDiscreteApi正是为解决这一问题而生的:它内部创建一个独立的 Vue 应用,把所需的 Provider 挂载其中,再把注入出的 API 以普通对象的形式暴露出来,从而让命令式 API 可以在任何地方使用。
createDiscreteApi 基础用法
官方演示(basic.demo.vue)展示了最典型的用法:一次性创建五种 API,并通过configProviderProps传入一个computed引用来实现主题的动态切换。
<script lang="ts" setup> import type { ConfigProviderProps } from 'naive-ui' import { createDiscreteApi, darkTheme, lightTheme } from 'naive-ui' import { computed, ref } from 'vue' const theme = ref<'light' | 'dark'>('light') const configProviderPropsRef = computed<ConfigProviderProps>(() => ({ theme: theme.value === 'light' ? lightTheme : darkTheme })) const { message, notification, dialog, loadingBar, modal } = createDiscreteApi( ['message', 'dialog', 'notification', 'loadingBar', 'modal'], { configProviderProps: configProviderPropsRef } ) function handleThemeChangeClick() { if (theme.value === 'light') theme.value = 'dark' else theme.value = 'light' } function handleMessageTriggerClick() { message.info('Message') } function handleNotificationTriggerClick() { notification.create({ title: 'Notification' }) } function handleDialogTriggerClick() { dialog.info({ title: 'Dialog' }) } function handleModalTriggerClick() { modal.create({ preset: 'card', title: 'Modal' }) } function handleLoadingBarTriggerClick() { loadingBar.start() setTimeout(() => { loadingBar.finish() }, 1000) } </script> <template> <n-space> <n-button @click="handleThemeChangeClick"> theme: {{ theme }} </n-button> <n-button @click="handleMessageTriggerClick"> message </n-button> <n-button @click="handleNotificationTriggerClick"> notification </n-button> <n-button @click="handleDialogTriggerClick"> dialog </n-button> <n-button @click="handleLoadingBarTriggerClick"> loadingBar </n-button> <n-button @click="handleModalTriggerClick"> modal </n-button> </n-space> </template>要点如下:
createDiscreteApi与darkTheme、lightTheme一样,直接从naive-ui包名导入;- 返回对象中只包含
includes数组里声明的 API,未声明的类型不会出现在结果中(也不会被创建); configProviderProps可以传普通对象,也可以传Ref/computed引用——传入响应式引用时,后续修改主题、locale、date-locale等配置会自动同步到离散应用中。
API 签名详解
createDiscreteApi的完整 TypeScript 签名如下(见 index.demo-entry.md,英文版见 enUS/index.demo-entry.md):
function createDiscreteApi( includes: Array<'message' | 'dialog' | 'notification' | 'loadingBar' | 'modal'>, options: { configProviderProps: Ref<ConfigProviderProps> | ConfigProviderProps messageProviderProps: Ref<MessageProviderProps> | MessageProviderProps dialogProviderProps: Ref<DialogProviderProps> | DialogProviderProps notificationProviderProps: Ref<NotificationProviderProps> | NotificationProviderProps loadingBarProviderProps: Ref<LoadingBarProviderProps> | LoadingBarProviderProps modalProviderProps: Ref<ModalProviderProps> | ModalProviderProps } ): { // 只有 includes 中包含的 API 才会被创建 message: MessageApi dialog: DialogApi notification: NotificationApi loadingBar: LoadingBarApi modal: ModalApi // Vue app app: App unmount: () => void } {}参数 1:includes
一个字符串数组,可选值为'message'、'notification'、'loadingBar'、'dialog'、'modal'。在 discrete.ts 的源码中,includes.forEach会逐个把对应的 Provider 组件(NMessageProvider、NNotificationProvider、NLoadingBarProvider、NDialogProvider、NModalProvider)及对应 props 收集起来,组成providersAndProps数组——这就是"按需创建"的实现基础。
includes.forEach((type) => { switch (type) { case 'message': providersAndProps.push({ type, Provider: NMessageProvider, props: messageProviderProps }) break // case 'notification' / 'dialog' / 'loadingBar' / 'modal' 同理 } })说明:官方中文版文档中
includes的类型示例未列出'modal',但英文版文档与源码(interface.ts 中DiscreteApiType)均确认支持'modal',且官方演示也使用了modal,因此实际使用以源码与英文文档为准。
参数 2:options
所有属性均为可选(DiscreteApiOptions,见 interface.ts),每个 Provider 的 props 既可以是普通对象,也可以是Ref<T>(MaybeRef<T> = Ref<T> | T):
| 属性 | 类型 | 说明 |
|---|---|---|
configProviderProps | MaybeRef<ConfigProviderProps> | 传递给内部NConfigProvider的 props,用于配置主题(theme)、locale、date-locale、theme-overrides等 |
messageProviderProps | MaybeRef<MessageProviderProps> | 传递给NMessageProvider的 props,如placement、max、duration、closable等 |
dialogProviderProps | MaybeRef<DialogProviderProps> | 传递给NDialogProvider的 props |
notificationProviderProps | MaybeRef<NotificationProviderProps> | 传递给NNotificationProvider的 props |
loadingBarProviderProps | MaybeRef<LoadingBarProviderProps> | 传递给NLoadingBarProvider的 props,如loadingBarStyle |
modalProviderProps | MaybeRef<ModalProviderProps> | 传递给NModalProvider的 props |
返回值
返回值是一个对象(DiscreteApi<T>,见 interface.ts),包含:
message/dialog/notification/loadingBar/modal:对应的命令式 API 实例,仅includes中声明的类型存在;app: App:内部创建的独立 Vue 应用实例;unmount: () => void:卸载离散应用并移除其 DOM 容器。
运行原理:一个独立的小型 Vue 应用
createDiscreteApi的核心实现位于 discrete.ts,它把所有 Provider 交给createDiscreteApp处理。而 discreteApp.ts 揭示了底层机制:
export function createDiscreteApp({ providersAndProps, configProviderProps }: DiscreteAppOptions): DiscreteApp { let app: App<Element> | null = createApp(App) const extractedApi: Omit<DiscreteApp, 'unmount'> = { app } function App(): VNode { return h(NConfigProvider, unref(configProviderProps), { default: () => providersAndProps.map(({ type, Provider, props }) => { return h(Provider, unref(props), { default: () => h(NInjectionExtractor, { onSetup: () => (extractedApi[type] = injectionFactoryMap[type]()) }) }) }) }) } let hostEl: Element | null if (isBrowser) { hostEl = document.createElement('div') document.body.appendChild(hostEl) app.mount(hostEl) } // ... }整个流程可以拆解为四步:
创建独立应用:
createApp(App)新建一个与主应用完全隔离的 Vue 应用实例;组装组件树:内部组件树为
NConfigProvider → NXxxProvider → NInjectionExtractor,即把用户要求的所有 Provider 嵌套挂载在NConfigProvider之下;提取注入值:
NInjectionExtractor(InjectionExtractor.tsx)是一个极简组件,在自身的setup阶段同步调用onSetup回调:export const NInjectionExtractor = defineComponent({ name: 'InjectionExtractor', props: { onSetup: Function as PropType<() => void> }, setup(props, { slots }) { props.onSetup?.() return () => slots.default?.() } })回调内容
extractedApi[type] = injectionFactoryMap[type]()正是调用useMessage()等注入函数(映射表见 discreteApp.ts)。由于此时组件正处于 Provider 子孙节点的setup中,inject能正确取到值——这就解释了为什么 Discrete API 能"脱离上下文"工作;挂载到独立容器:在浏览器环境下,代码会
document.createElement('div')创建一个全新的容器节点并追加到document.body,再将应用挂载进去。因此离散 API 弹出的消息、对话框等渲染在独立的 DOM 容器中,与主应用的n-xxx-provider互不干扰。
unmount()会依次执行app.unmount()、从body移除容器节点,并将内部引用置空;若重复调用,会通过warn给出提示(discreteApp.ts)。
注意事项(务必阅读)
官方文档在 index.demo-entry.md 中特别强调了两点,这是最容易踩坑的地方:
注意
- 脱离上下文的 API 不会受
n-xxx-provider的影响,并且和应用上下文中对应组件会使用不同的 DOM 容器。如果需要的话,你需要手动同步这些信息。并且最好不要混用两类 API。- 不要在
setup中调用createDiscreteApi,可能会有一些意外的问题出现。
结合源码可以更深入地理解这两条告诫:
- 配置需要手动同步:由于离散应用是独立的,主应用中
n-config-provider、n-message-provider等配置不会自动传递到离散 API。若希望两者的主题、文案、位置等保持一致,必须通过options中的xxxProviderProps显式传入,且应尽量与主应用的 Provider props 保持一致; - 不要混用两类 API:
useMessage()与createDiscreteApi().message使用的是两套不同的容器与实例,混用可能导致消息位置、堆叠顺序不一致等难以排查的视觉问题,建议在项目中统一选择其中一种; - 不要在
setup中调用:createDiscreteApi会创建应用、操作document.body,属于有副作用的重操作,在setup中反复执行可能引发挂载/卸载时序问题。正确做法是在模块顶层(如工具函数文件)或事件回调中调用一次并复用结果。
实战:在纯 TS 工具模块中使用
下面是推荐的生产级用法——在setup之外、模块顶层一次性创建并导出 API:
// utils/discrete.ts import { createDiscreteApi, darkTheme, type ConfigProviderProps } from 'naive-ui' const configProviderProps: ConfigProviderProps = { theme: darkTheme } export const { message, notification, dialog, loadingBar, modal } = createDiscreteApi( ['message', 'notification', 'dialog', 'loadingBar', 'modal'], { configProviderProps } )之后在任何工具函数、路由守卫或事件处理中直接使用:
// 例如在 axios 拦截器中 import { message } from '@/utils/discrete' export function handleRequestError(error: unknown): void { message.error('请求失败,请稍后重试') }若你的场景不再需要这些 API(例如在测试环境中收尾),可调用返回对象上的unmount()释放资源:
const discrete = createDiscreteApi(['message'], {}) // ...使用后 discrete.unmount()小结
createDiscreteApi(v2.29.0+)是 Naive UI 官方提供的"脱离上下文 API"方案,用于在setup外获取message、dialog、notification、loadingBar、modal五类命令式 API;- 其本质是创建一个挂载在
document.body独立容器上的微型 Vue 应用,通过NInjectionExtractor在 Provider 的setup阶段提取注入值(见 discreteApp.ts); - 使用时要牢记两点:离散 API 的配置需通过
xxxProviderProps手动同步、不要与正常 Provider API 混用、不要在setup中调用; - 相关类型
DiscreteApi、DiscreteApiOptions从naive-ui包中导出(见 index.ts),完整的实现与演示代码可分别查阅 discrete.ts、discreteApp.ts 与 basic.demo.vue。
- 前端
- UI组件
【免费下载链接】naive-ui
A Vue 3 Component Library. Fairly Complete. Theme Customizable. Uses TypeScript. Fast.
相关推荐
Naive UI 跨框架使用:在 React 项目中集成 Naive UI 组件
Naive UI 跨框架使用:在 React 项目中集成 Naive UI 组件 Naive UI 作为一款基于 Vue 3 的高质量组件库,以其丰富的组件集、
前端UI组件终极指南:一劳永逸解决Cursor自动更新的烦恼
终极指南:一劳永逸解决Cursor自动更新的烦恼 你是否曾经历过这样的场景?辛辛苦苦配置好Cursor编辑器,享受着流畅的AI编程体验,结果第二天一打开,软件自
开发工具CLI本地免费生成可编辑 PPT:Presenton AI 演示文稿完整指南
本地免费生成可编辑 PPT:Presenton AI 演示文稿完整指南 每次提案要交稿,先卡大纲,再卡排版。订阅制的 AI 工具,出来的第一页常常版式崩掉、还不
AI 应用人工智能大模型后端前端桌面应用MCP 服务AI AgentRAG本地部署企业应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考