Vue: Project/Inject
2022-03-28
0. Intro
Prop Drilling
当我们想要把数据从 parent 传入 child 时,我们通常会使用 props。
但是当我们有一个 large component tree and a deeply nested component needs something from a distant ancestor component(也就是要传很多层时),我们如果用以前的方法,就要把这个 prop pass across the entire parent chain,这种现象被称为 Props Drilling。
Example
Note: 虽然 <Footer>
不 care 这些 props,它还是要声明且传递这些 props。
解决方法 - provide and inject
Parent component 可以作为所有 children 的 dependency provider。在这个 descendant tree 里面的任何 component,不论深度多少,都可以 inject dependencies provided by components up in its parent chain.
1. Provide
如果一个 component 需要 provide data to its descendants, 那么可以使用 provide
option。
export default {
provide: {
message: "hello!",
},
};
这个 provide
object 里面的每个 property 都是 key + value,child 可以通过 key 来查找需要的 value,所以 value 就被 inject 到 child 里面了。
Provide per-instance state
如果我们要提供的 state 是 instance level 的,比如我们要提供 data()
里面声明的数据,那么 provide
就必须用 function value。
注意:这么做的问题是,injection 变得不 reactive 了(接下来会详细讨论)。
export default {
data() {
return {
message: "hello!",
};
},
provide() {
// use function syntax so that we can access `this`
return {
message: this.message,
};
},
};
App-level Provide
我们还可以定义 app-level 的 dependency injection,让所有 component 都能使用。
常用情况:plugin。因为 plugins 无法定义一个 component 来提供 value。
import { createApp } from "vue";
const app = createApp({});
app.provide(/* key */ "message", /* value */ "hello!");
2. Inject
想要 inject data provided by an ancestor component,我们可以用 inject
option。
export default {
inject: ["message"],
created() {
console.log(this.message); // injected value
},
};
When can access?
这些 injection 通常会在 component 自己的 state resolve 之前就 resolve,所以我们可以在 data()
里面直接 access injected properties。
export default {
inject: ["message"],
data() {
return {
// initial data based on injected value
fullMessage: this.message,
};
},
};
Injection Aliasing
当我们用 array syntax 来获取 injected properties 时,这些 key 就会成为当前的 instance level property。
假如我们想换个 local key,可以用 object syntax,下面的例子里面,我们把 message
改名成了 localMessage
。
export default {
inject: {
/* local key */ localMessage: {
from: /* injection key */ "message",
},
},
};
Injection Default Values
inject
默认会假设 injected key 存在于 parent chain 的某个位置,假如找不到的话,就会用 runtime warning。
如果我们想把 injected property 变成 optional,就要声明默认值。
export default {
// object syntax is required
// when declaring default values for injections
inject: {
message: {
from: 'message', // this is optional if using the same key for injection
default: 'default value'
},
user: {
// use a factory function for non-primitive values that are expensive
// to create, or ones that should be unique per component instance.
default: () => ({ name: 'John' })
}
}
3. Working with Reactivity
想要让 injection reactively linked to the provider,我们必须提供一个 computed property。
import { computed } from "vue";
export default {
data() {
return {
message: "hello!",
};
},
provide() {
return {
// explicitly provide a computed property
message: computed(() => this.message),
};
},
};
The
computed()
function is typically used in Composition API components, but can also be used to complement certain use cases in Options API. You can learn more about its usage by reading the Reactivity Fundamentals and Computed Properties with the API Preference set to Composition API.
Vue 3.3 版本之前需要加额外的 config
The above usage requires setting app.config.unwrapInjectedRef = true
to make injections automatically unwrap computed refs. This will become the default behavior in Vue 3.3 and this config is introduced temporarily to avoid breakage. It will no longer be required after 3.3.
4. Working with Symbol Keys
什么时候需要?- 大型 app,多个 DI,需要协作
If you are working in a large application with many dependency providers, or you are authoring components that are going to be used by other developers, it is best to use Symbol injection keys to avoid potential collisions.
怎么用 Symbol?
1. create a dedicated file to export the Symbols
// keys.js
export const myInjectionKey = Symbol();
2. import in provider component
// in provider component
import { myInjectionKey } from "./keys.js";
export default {
provide() {
return {
[myInjectionKey]: {
/* data to provide */
},
};
},
};
3. import in injector component
// in injector component
import { myInjectionKey } from "./keys.js";
export default {
inject: {
injected: { from: myInjectionKey },
},
};