Vue2 完整完善的 Context 封装及使用案例
下面我将提供一个更加完整、完善的 Vue2 Context 封装方案,包含响应式支持、类型提示(使用 JSDoc)和多种使用方式,然后给出详细的使用案例。
一、完整 Context 封装实现
1. 核心 Context 封装 (context.js)
/**
* 创建 Vue2 上下文
* @template T
* @param {T} defaultValue 默认值
* @param {Object} [options] 配置选项
* @param {boolean} [options.reactive=true] 是否启用响应式
* @returns {{
* Provider: VueComponent,
* Consumer: VueComponent,
* useConsumer: () => T,
* withConsumer: (propName?: string) => Object
* }}
*/
export const createContext = (defaultValue, { reactive = true } = {}) => {
const contextKey = Symbol('contextKey')
const observableValue = reactive
? Vue.observable({ value: defaultValue })
: { value: defaultValue }
/**
* Context Provider 组件
*/
const Provider = {
name: 'ContextProvider',
props: {
value: {
type: null,
required: true
}
},
provide() {
return {
[contextKey]: observableValue
}
},
watch: {
value: {
immediate: true,
handler(newVal) {
observableValue.value = newVal
}
}
},
render() {
return this.$slots.default && this.$slots.default[0]
}
}
/**
* Context Consumer 组件
*/
const Consumer = {
name: 'ContextConsumer',
inject: {
[contextKey]: {
from: contextKey,
default: () => reactive
? Vue.observable({ value: defaultValue })
: { value: defaultValue }
}
},
render() {
const contextValue = this[contextKey].value
return this.$scopedSlots.default
? this.$scopedSlots.default(contextValue)
: null
}
}
/**
* 组合式 API 风格的 Hook
* @returns {T}
*/
const useConsumer = () => {
const vm = getCurrentInstance()
if (!vm) {
console.warn('useConsumer must be called within a setup function')
return defaultValue
}
return vm.proxy[contextKey]?.value ?? defaultValue
}
/**
* 高阶组件混入
* @param {string} [propName='context'] 注入的属性名
* @returns {Object}
*/
const withConsumer = (propName = 'context') => ({
inject: {
[contextKey]: {
from: contextKey,
default: () => reactive
? Vue.observable({ value: defaultValue })
: { value: defaultValue }
}
},
computed: {
[propName]() {
return this[contextKey].value
}
}
})
return {
Provider,
Consumer,
useConsumer,
withConsumer,
_contextKey: contextKey
}
}
/**
* 获取当前组件实例 (兼容 Vue2)
* @returns {Vue|null}
*/
function getCurrentInstance() {
// 在 Vue2 中模拟 getCurrentInstance
if (typeof window !== 'undefined' && window.Vue) {
return window.Vue.prototype.$options.__currentInstance
}
return null
}
2. 辅助工具函数 (context-utils.js)
* 创建响应式 Context 快捷方法
* @template T
* @param {T} defaultValue
* @returns {ReturnType<typeof createContext>}
*/
export const createReactiveContext = (defaultValue) =>
createContext(defaultValue, { reactive: true })
/**
* 创建普通 Context 快捷方法
* @template T
* @param {T} defaultValue
* @returns {ReturnType<typeof createContext>}
*/
export const createPlainContext = (defaultValue) =>
createContext(defaultValue, { reactive: false })
/**
* 多层 Context 合并工具
* @param {...ReturnType<typeof createContext>} contexts
* @returns {VueComponent}
*/
export const mergeContextProviders = (...contexts) => ({
name: 'MergedContextProvider',
components: {
...contexts.reduce((comps, ctx) => ({
...comps,
[`ContextProvider${ctx._contextKey.description}`]: ctx.Provider
}), {})
},
render() {
return contexts.reduceRight((child, ctx) => {
const Provider = `ContextProvider${ctx._contextKey.description}`
return h(Provider, { value: this.value }, [child])
}, this.$slots.default && this.$slots.default[0])
}
})
二、完整使用案例
1. 主题切换案例
(1) 创建主题上下文 (theme-context.js)
// 默认主题配置
const defaultTheme = {
mode: 'light',
colors: {
primary: '#409EFF',
background: '#ffffff',
text: '#303133'
},
toggleMode: () => {}
}
export const ThemeContext = createReactiveContext(defaultTheme)
(2) 主题提供者组件 (ThemeProvider.vue)
<theme-provider :value="themeValue">
<slot></slot>
</theme-provider>
</template>
<script>
import { ThemeContext } from './theme-context'
export default {
components: {
'theme-provider': ThemeContext.Provider
},
data() {
return {
themeValue: {
mode: 'light',
colors: {
primary: '#409EFF',
background: '#ffffff',
text: '#303133'
},
toggleMode: this.toggleMode
}
}
},
methods: {
toggleMode() {
if (this.themeValue.mode === 'light') {
this.themeValue = {
...this.themeValue,
mode: 'dark',
colors: {
primary: '#64B5F6',
background: '#121212',
text: '#E0E0E0'
}
}
} else {
this.themeValue = {
...this.themeValue,
mode: 'light',
colors: {
primary: '#409EFF',
background: '#ffffff',
text: '#303133'
}
}
}
}
}
}
</script>
(3) 使用 Consumer 组件 (ThemeButton.vue)
<theme-consumer>
<template #default="theme">
<button
:style="{
backgroundColor: theme.colors.primary,
color: theme.colors.text,
border: 'none',
padding: '8px 16px',
borderRadius: '4px'
}"
@click="theme.toggleMode"
>
{{ theme.mode === 'light' ? '🌙 Dark Mode' : '☀️ Light Mode' }}
</button>
</template>
</theme-consumer>
</template>
<script>
import { ThemeContext } from './theme-context'
export default {
components: {
'theme-consumer': ThemeContext.Consumer
}
}
</script>
(4) 使用高阶组件 (ThemedCard.vue)
<div
:style="{
backgroundColor: theme.colors.background,
color: theme.colors.text,
padding: '16px',
borderRadius: '8px',
boxShadow: '0 2px 12px 0 rgba(0, 0, 0, 0.1)'
}"
>
<slot></slot>
<p>Current theme: {{ theme.mode }}</p>
</div>
</template>
<script>
import { ThemeContext } from './theme-context'
export default {
mixins: [ThemeContext.withConsumer('theme')]
}
</script>
(5) 使用 Hook 方式 (ThemeIndicator.vue)
<div>
<p>Current theme mode: {{ themeMode }}</p>
<p>Primary color: {{ themeColors.primary }}</p>
</div>
</template>
<script>
import { ThemeContext } from './theme-context'
export default {
computed: {
theme() {
return ThemeContext.useConsumer.call(this)
},
themeMode() {
return this.theme.mode
},
themeColors() {
return this.theme.colors
}
}
}
</script>
(6) 应用入口 (App.vue)
<theme-provider>
<div class="app" :style="{ backgroundColor: theme.colors.background }">
<header>
<theme-button />
</header>
<main>
<themed-card>
<h2>Theme Context Demo</h2>
<theme-indicator />
</themed-card>
</main>
</div>
</theme-provider>
</template>
<script>
import ThemeProvider from './ThemeProvider.vue'
import ThemeButton from './ThemeButton.vue'
import ThemedCard from './ThemedCard.vue'
import ThemeIndicator from './ThemeIndicator.vue'
import { ThemeContext } from './theme-context'
export default {
components: {
ThemeProvider,
ThemeButton,
ThemedCard,
ThemeIndicator
},
computed: {
theme() {
return ThemeContext.useConsumer.call(this)
}
}
}
</script>
<style>
.app {
min-height: 100vh;
transition: background-color 0.3s ease;
}
</style>
2. 多上下文合并案例
(1) 创建用户上下文 (user-context.js)
const defaultUser = {
isAuthenticated: false,
userInfo: null,
login: () => Promise.reject('No provider'),
logout: () => {}
}
export const UserContext = createReactiveContext(defaultUser)
(2) 合并提供者 (AppProviders.vue)
<merged-providers>
<slot></slot>
</merged-providers>
</template>
<script>
import { mergeContextProviders } from './context-utils'
import { ThemeContext } from './theme-context'
import { UserContext } from './user-context'
export default {
components: {
'merged-providers': mergeContextProviders(
ThemeContext,
UserContext
)
}
}
</script>
(3) 使用多个上下文 (UserProfile.vue)
<div>
<theme-consumer>
<template #default="theme">
<user-consumer>
<template #default="user">
<div
:style="{
backgroundColor: theme.colors.background,
color: theme.colors.text,
padding: '16px',
borderRadius: '8px'
}"
>
<h3>User Profile</h3>
<div v-if="user.isAuthenticated">
<p>Welcome, {{ user.userInfo.name }}!</p>
<button @click="user.logout">Logout</button>
</div>
<div v-else>
<p>Please login</p>
<button @click="login">Login</button>
</div>
</div>
</template>
</user-consumer>
</template>
</theme-consumer>
</div>
</template>
<script>
import { ThemeContext } from './theme-context'
import { UserContext } from './user-context'
export default {
components: {
'theme-consumer': ThemeContext.Consumer,
'user-consumer': UserContext.Consumer
},
methods: {
async login() {
try {
await this.$user.login({ username: 'demo', password: '123' })
} catch (error) {
console.error('Login failed:', error)
}
}
},
computed: {
$theme: ThemeContext.useConsumer,
$user: UserContext.useConsumer
}
}
</script>