扫描二维码关注或者微信搜一搜:编程智域 前端至全栈交流与成长
发现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);
},
};三、provide/inject vs globalProperties
两种方式都能实现"插件给组件传数据",但差别不小:
$xxx | ||
结论很明确——在组合式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,
availableLocales: Object.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,所以翻译内容会自动更新。
六、inject的默认值
如果插件没安装,inject会返回undefined。你可以给inject传第二个参数作为默认值:
// 如果没安装i18n插件,返回一个空的翻译对象
const i18n = inject(I18N_KEY, {
locale: ref("zh"),
translate: () =>"",
setLocale: () => {},
});或者传一个工厂函数(当默认值需要计算时):
const i18n = inject(
I18N_KEY,
() => ({
locale: ref("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. 组合式API友好: inject()可以直接在<script setup>中使用,不需要getCurrentInstance2. TypeScript支持更好:配合 InjectionKey可以获得完整的类型推断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. 插件没安装(没调用 app.use)2. provide的key和inject的key不一致 3. 用了字符串key但拼写不一致
解决方案:
1. 确认 app.use()已调用2. 使用Symbol key确保一致性 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);
夜雨聆风