为naive-ui添加全局的message函数

前言

naive-ui 添加全局的 messagenotificationloaddingBardialog 函数

对于 Vue3 ,个人非常喜欢 naive-ui 这个 ui 库,并且也在工作中把它应用到了相应的项目之中

虽然它看起来有点像 ant-design , 不过国内的 ui 库大体都是以蓝色为主色,看起来就有些审美疲劳了

正文

我们都知道,像 element-uiant-design 等都有一些通用的方法可供全局使用,即可以脱离组件的上下文

比如 element-plus

我们可以在任何地方使用这些组件,比如 axios 的拦截器中,或者路由守卫( hooks )中

而在 naive-ui 中,使用 message 的方法比较特别

首先是必须包在 n-message-provider 组件下

然后使用 useMessage 来获取 message 实例

App.vue

1
2
3
4
5
6
7
8
9
10
<script setup lang="ts">
import { NMessageProvider } from 'naive-ui';
import Content from './Content.vue';
</script>

<template>
<NMessageProvider>
<Content></Content>
</NMessageProvider>
</template>

Content.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
<script setup lang="ts">
import { useMessage } from 'naive-ui';

const message = useMessage();

const open = () => {
message.info('我是消息')
}
</script>

<template>
<button @click="open">打开message</button>
</template>

看起来有点复杂

我去翻了下历史的 issues 记录, 发现有人已经有提过相关的 issue

不过作者似乎并不想提供这样的 api ,作者在第三个 issue 中回复了

If you must need to render a message before app is ready, you need to render a app outside current app and set message api globally. However remember message is a part of your app. You can’t operate a phone before you turn it on.

意思是如果确实需要在 app 被挂在前调用 message, 那么需要在原 app 外部额外渲染一个 app ,并且把相关的 api 全局化

作者的意思很简单,想用 message, 就是得 app 挂载了才能用,因为 message 就是整个 app 的一个部分

作者还举了个例子:你不能在手机还没开机的时候就操作它

所以,我们需要按照作者说的来进行 hack

干掉 useXXX

每次都要 useMessage 很麻烦,那么如何才能导出一个全局变量呢?

可以通过添加一个空的组件来把 message 提升到全局

MessageInjectWindow.vue

1
2
3
4
5
6
7
<script setup lang="ts">
import { useMessage } from "naive-ui";

window["$message"] = useMessage();
</script>

<template></template>

然后我们把上面的组件放到 App.vue

1
2
3
4
5
6
7
8
9
10
11
<script setup lang="ts">
import { NMessageProvider } from 'naive-ui';
import Content from './Content.vue';
</script>

<template>
<NMessageProvider>
<MessageInjectWindow></MessageInjectWindow>
<Content></Content>
</NMessageProvider>
</template>

然后在 Content.vue 中就可以使用 $message

1
2
3
4
5
6
7
8
9
<script setup lang="ts">
const open = () => {
$message.info('我是消息');
}
</script>

<template>
<button @click="open">打开message</button>
</template>

不过在代码的过程中总觉得缺了点啥?没错,就是代码提示

这是一个 ts 的项目,通过添加 d.ts 可以为 $message 赋予类型提示

创建 global.d.ts 然后输入以下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
declare global {
// 可以直接使用 $message
const $message: import("naive-ui").MessageApi;
const $dialog: import("naive-ui").DialogApi;
const $notification: import("naive-ui").NotificationApi;
const $loadingBar: import("naive-ui").LoadingBarApi;

interface Window {
// 挂载需要的类型提示
// 或者通过 window.$message 使用
$message: import("naive-ui").MessageApi;
$dialog: import("naive-ui").DialogApi;
$notification: import("naive-ui").NotificationApi;
$loadingBar: import("naive-ui").LoadingBarApi;
}
}
export {};

Window 下定义是因为需要通过 window['$xxx'] = useXXX() 来挂载, 不加的话在 ts 项目下就会有红线

global 下定义意味着除了 window['$xxx'] 来调用之外,也可以直接使用 $xxx 直接使用

这样子代码就有不错的提示了

干掉只能在app内使用

虽然我们已经提取了全局的 api 了,但是依然无法在没有 app 的上下文下使用

比如如果我想在 mount 之前调用一个接口,这个接口要使用 message 来显示一些信息,那么就会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
import { createApp } from "vue";
import App from "./App.vue";

const bootstrap = async () => {
// 想在挂在前调用一个接口
await Promise.resolve().then(() => {
$message.info("hello");
});
// 接口完成再挂载 app
createApp(App).mount("#app");
};

bootstrap();

结果显而易见,报错

原因很简单,都没挂载 app ,那么 MessageInjectWindow.vuesetup 不会执行,自然也就没有注入 message 实例了

那么这时候就需要构建一个空的 app ,在目标 app 之前挂载,然后全局化 api

创建 AppProvider.vue

1
2
3
4
5
6
7
8
9
10
<script lang="ts" setup>
import { NMessageProvider } from "naive-ui";
import MessageInjectWindow from "./MessageInjectWindow.vue";
</script>

<template>
<NMessageProvider>
<MessageInjectWindow></MessageInjectWindow>
</NMessageProvider>
</template>

内容基本一样,不过没有 Content.vue ,因为作为一个空的 app ,不需要渲染真正的内容节点

然后修改 main.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { createApp } from "vue";
import App from "./App.vue";
import AppProvider from './AppProvider.vue';

const bootstrap = async () => {
// 挂载一个空的 app
createApp(AppProvider).mount('#app-provider');
// 想在挂在前调用一个接口
await Promise.resolve().then(() => {
$message.info("hello");
});
// 接口完成再挂载 app
createApp(App).mount("#app");
};

bootstrap();

然后就可以愉快的使用全局的 $message

后记

这个方法是从 Naive-ui-admin 以及相关的 issues 查找到的

目前已经用在公司的项目中

如果只是讨厌每次调用接口时写 useXXX

issues 中也有用户提议可以封装通用的 useRequest

反正,萝卜青菜各有所爱吧~