乐于分享
好东西不私藏

插件里用provide/inject,让全应用都能拿到你的数据

插件里用provide/inject,让全应用都能拿到你的数据

扫描二维码关注或者微信搜一搜:编程智域 前端至全栈交流与成长

发现1000+提升效率与开发的AI工具和实用程序:https://tools.cmdragon.cn/zh/apps?category=ai_chat

一、globalProperties的痛点

上一篇文章咱们用app.config.globalProperties挂了$translate方法,确实能用,但在组合式API中有个大问题——<script setup>里没有this,你得这么搞:

import { getCurrentInstance } from"vue";
const { proxy } = getCurrentInstance();
const msg = proxy.$translate("greetings.hello");

说实话,这写法看着就别扭。而且getCurrentInstance官方都说"只在开发调试时使用",拿来当正经API用总觉得不太对劲。

那有没有更优雅的方式?有——app.provide() + inject()

二、app.provide():插件给全应用发"广播"

app.provide()的作用就是往整个应用里"广播"一个数据,任何组件都能通过inject()接收到。这就像学校广播站播了一条消息,所有教室都能听到。

在插件中使用provide

// plugins/i18n.js
exportdefault {
install(app, options) {
// 把翻译字典提供给整个应用
    app.provide("i18n", options);
  },
};

就这么一行代码!app.provide('i18n', options)的意思是:以'i18n'为key,把options(翻译字典)注入到整个应用中。

在组件中用inject接收

<script setup>
import { inject } from "vue";

const i18n = inject("i18n");

console.log(i18n.greetings.hello); // '你好'
</script>

getCurrentInstance清爽多了吧?

选项式API中也能用:

exportdefault {
inject: ["i18n"],
created() {
console.log(this.i18n.greetings.hello);
  },
};
插件 install
app.providekey: 'i18n'value: 翻译字典
组件A: inject获取
组件B: inject获取
组件C: inject获取
任意组件都能拿到数据

三、provide/inject vs globalProperties

两种方式都能实现"插件给组件传数据",但差别不小:

特性
globalProperties
provide/inject
选项式API模板中
直接用$xxx
需要inject后用
组合式API中
需要getCurrentInstance
直接inject ✅
TypeScript支持
需要额外声明
更好的类型推断 ✅
作用域
全局唯一
可按key区分 ✅
命名冲突
容易冲突
用Symbol避免 ✅
响应式
需要额外处理
直接传ref/reactive ✅
官方推荐
谨慎使用
推荐方式 ✅

结论很明确——在组合式API中,provide/inject是更好的选择

四、用Symbol做注入键,避免命名冲突

上面咱们用的是字符串'i18n'作为provide的key。这有个隐患——万一另一个插件也用了'i18n'这个key,就冲突了。

Vue推荐用Symbol作为注入键,因为Symbol是唯一的,不可能重复:

// plugins/i18n.js
exportconst i18nKey = Symbol("i18n");

exportdefault {
install(app, options) {
    app.provide(i18nKey, options);
  },
};

组件中:

<script setup>
import { inject } from "vue";
import { i18nKey } from "./plugins/i18n.js";

const i18n = inject(i18nKey);
</script>

这样即使有别的插件也叫i18n,因为Symbol是唯一的,所以不会冲突。

更规范的做法:把key和类型放一起

// plugins/i18n.js
import { inject } from'vue'

exportconstI18N_KEY = Symbol('i18n'asInjectionKey<I18nOptions>

// 提供一个useI18n函数,组件里直接调用就行
exportfunctionuseI18n() {
const i18n = inject(I18N_KEY)
if (!i18n) {
thrownewError('i18n plugin is not installed!')
  }
return i18n
}

exportdefault {
install(app, options) {
    app.provide(I18N_KEY, options)
  }
}

组件中使用:

<script setup>
import { useI18n } from "./plugins/i18n.js";

const { translate, locale, setLocale } = useI18n();
</script>

你看,这跟Composable的用法一模一样!useI18n()看起来就像个Composable函数,但它的数据来源是插件通过app.provide注入的。这就是插件和Composable的完美结合。

五、provide响应式数据

app.provide()提供的数据默认不是响应式的。如果你需要组件中能响应式地获取更新,就要传ref或reactive:

// plugins/i18n.js
import { ref, reactive } from"vue";

exportconstI18N_KEY = Symbol("i18n");

exportfunctionuseI18n() {
const i18n = inject(I18N_KEY);
if (!i18n) {
thrownewError("i18n plugin is not installed!");
  }
return i18n;
}

exportdefault {
install(app, options) {
const locale = ref("zh");
const messages = options;

functiontranslate(key, defaultValue = "") {
if (typeof key !== "string"return defaultValue;
const currentMessages = messages[locale.value] || {};
const result = key.split(".").reduce((obj, k) => {
if (obj) return obj[k];
      }, currentMessages);
return result !== undefined ? result : defaultValue;
    }

functionsetLocale(lang) {
if (messages[lang]) {
        locale.value = lang;
      }
    }

// 提供响应式数据
    app.provide(I18N_KEY, {
      locale,
      translate,
      setLocale,
availableLocalesObject.keys(messages),
    });
  },
};

组件中:

<script setup>
import { useI18n } from "./plugins/i18n.js";

const { locale, translate, setLocale } = useI18n();
</script>

<template>
  <h1>{{ translate("greetings.hello") }}</h1>
  <p>当前语言:{{ locale }}</p>
  <button @click="setLocale('en')">English</button>
  <button @click="setLocale('zh')">中文</button>
</template>

切换语言时,locale是ref,translate函数内部读取了locale.value,所以翻译内容会自动更新。

插件 install
创建 ref: locale
创建 translate 函数
app.provide I18N_KEY
locale / translate / setLocale
组件: useI18n
setLocale切换语言
locale.value变化
translate自动返回新翻译
模板自动更新 ✅

六、inject的默认值

如果插件没安装,inject会返回undefined。你可以给inject传第二个参数作为默认值:

// 如果没安装i18n插件,返回一个空的翻译对象
const i18n = inject(I18N_KEY, {
localeref("zh"),
translate() =>"",
setLocale() => {},
});

或者传一个工厂函数(当默认值需要计算时):

const i18n = inject(
I18N_KEY,
() => ({
localeref("zh"),
translate() =>"",
setLocale() => {},
  }),
true,
); // 第三个参数true表示默认值是工厂函数

不过更好的做法是在useI18n里直接抛错,这样没装插件的时候你能第一时间发现:

exportfunctionuseI18n() {
const i18n = inject(I18N_KEY);
if (!i18n) {
thrownewError(
"[i18n] Plugin is not installed! Did you forget app.use(i18nPlugin)?",
    );
  }
return i18n;
}

七、一个完整的插件模板

把上面说的最佳实践合在一起,来一个完整的插件模板:

// plugins/myPlugin.js
import { ref, inject } from"vue";

constPLUGIN_KEY = Symbol("myPlugin");

exportfunctionuseMyPlugin() {
const plugin = inject(PLUGIN_KEY);
if (!plugin) {
thrownewError("MyPlugin is not installed!");
  }
return plugin;
}

exportdefault {
install(app, options = {}) {
const state = ref(options.initialValue || null);

functiondoSomething() {
// ...
    }

    app.provide(PLUGIN_KEY, {
      state,
      doSomething,
    });
  },
};

使用:

// main.js
import myPlugin from"./plugins/myPlugin.js";
app.use(myPlugin, { initialValue"hello" });
<!-- 组件中 -->
<script setup>
import { useMyPlugin } from "./plugins/myPlugin.js";
const { state, doSomething } = useMyPlugin();
</script>

课后 Quiz

问题 1

为什么在插件中推荐用app.provide()而不是globalProperties

答案解析

三个主要原因:

  1. 1. 组合式API友好inject()可以直接在<script setup>中使用,不需要getCurrentInstance
  2. 2. TypeScript支持更好:配合InjectionKey可以获得完整的类型推断
  3. 3. 避免命名冲突:用Symbol做key,不会跟其他插件冲突

问题 2

为什么推荐用Symbol而不是字符串作为provide的key?

答案解析

因为Symbol是唯一的。如果用字符串'i18n'作为key,另一个插件也用了'i18n',后注册的会覆盖先注册的。而Symbol即使名字一样,每次创建的都是不同的值,所以不可能冲突。

问题 3

app.provide()提供的数据默认是响应式的吗?

答案解析

不是。app.provide()本身不会让数据变成响应式的。如果你需要组件中能响应式地获取更新,需要传入ref或reactive对象。比如app.provide('count', ref(0)),组件中inject('count')拿到的就是一个ref,可以响应式地追踪变化。

常见报错解决方案

报错 1:inject() can only be used inside setup()

错误场景

// 在组件外面调用inject
const i18n = inject("i18n"); // 💥 不在组件上下文中

报错原因
inject必须在组件的setup上下文中调用,就像Composable一样。

解决方案
把inject封装在use函数里,在组件中调用:

// 插件文件
exportfunctionuseI18n() {
returninject(I18N_KEY);
}

// 组件中
<script setup>const {translate} = useI18n() // ✅ 在setup上下文中</script>;

报错 2:inject返回undefined

错误场景

const i18n = inject("i18n"); // undefined

报错原因
可能的原因:

  1. 1. 插件没安装(没调用app.use
  2. 2. provide的key和inject的key不一致
  3. 3. 用了字符串key但拼写不一致

解决方案

  1. 1. 确认app.use()已调用
  2. 2. 使用Symbol key确保一致性
  3. 3. 给inject加默认值或错误提示:
exportfunctionuseI18n() {
const i18n = inject(I18N_KEY);
if (!i18n) {
thrownewError("[i18n] Plugin not installed!");
  }
return i18n;
}

报错 3:provide的数据变了但组件不更新

错误场景

// 插件中provide普通对象
app.provide("config", { theme"dark" });

// 组件中inject
const config = inject("config");
config.theme = "light"// 改了但页面没更新

报错原因
provide的是普通对象,不是响应式的。修改普通对象的属性不会触发视图更新。

解决方案
用reactive或ref包装数据:

// 插件中
const config = reactive({ theme"dark" });
app.provide("config", config);

// 或者用ref
const theme = ref("dark");
app.provide("theme", theme);