OWL与VUE3 的高级组件通信全解析
📊 组件通信方式总览
通信方式 | Vue 3 | OWL | 使用场景 |
父→子 | props | props | 传递数据 |
子→父 | emit | 自定义事件 (trigger) | 子组件通知父组件 |
父访问子 | ref | t-ref | 父组件调用子方法 |
跨层级 | provide/inject | env (环境对象) | 祖先→后代传递 |
全局状态 | Pinia/Vuex | Store (useState) | 全局数据共享 |
兄弟组件 | EventBus / 状态管理 | 通过父组件 / Store | 平级通信 |
🎯 1. 父传子:Props(最基础)
Vue 3 方式
<!-- 父组件 -->
<template><ChildComponent :title="title":count="count":user="user"/>
</template><script setup>
import { ref } from 'vue';
const title = ref('标题');
const count = ref(10);
const user = ref({ name: '张三' });
</script>
<!-- 子组件 -->
<script setup>
const props = defineProps({title: String,count: Number,user: Object
});
</script>
OWL 方式 ✅
// 父组件
import { Component, useState } from "@odoo/owl";class ParentComponent extends Component {static template = "ParentTemplate";static components = { ChildComponent };setup() {this.state = useState({title: '标题',count: 10,user: { name: '张三' }});}
}
<!-- 父组件模板 -->
<templates><t t-name="ParentTemplate"><ChildComponent title="state.title"count="state.count"user="state.user"/></t>
</templates>
// 子组件
class ChildComponent extends Component {static template = "ChildTemplate";// ✅ 定义 props(类型验证)static props = {title: { type: String, optional: false },count: { type: Number, optional: true },user: { type: Object, optional: false }};setup() {console.log('接收到的 props:', this.props);// this.props.title// this.props.count// this.props.user}
}
<!-- 子组件模板 -->
<templates><t t-name="ChildTemplate"><div><h1><t t-esc="props.title"/></h1><p>数量: <t t-esc="props.count"/></p><p>用户: <t t-esc="props.user.name"/></p></div></t>
</templates>
✅ Props 类型验证:
static props = {// 字符串name: { type: String },// 数字(可选)age: { type: Number, optional: true },// 布尔值(带默认值)isActive: { type: Boolean, optional: true },// 对象user: { type: Object },// 数组items: { type: Array },// 多种类型id: { type: [String, Number] },// 任意类型data: { type: true },// 自定义验证email: { type: String, validate: (value) => value.includes('@')}
};
🎯 2. 子传父:自定义事件(重要!)
Vue 3 方式
<!-- 子组件 -->
<template><button @click="handleClick">点击</button>
</template><script setup>
const emit = defineEmits(['update', 'delete']);const handleClick = () => {emit('update', { id: 1, name: '张三' });
};
</script>
<!-- 父组件 -->
<template><ChildComponent @update="onUpdate"@delete="onDelete"/>
</template>
OWL 方式 ✅
// 子组件
import { Component } from "@odoo/owl";class ChildComponent extends Component {static template = "ChildTemplate";handleClick() {// ✅ 方法1: 使用 props 传入的回调(推荐)if (this.props.onUpdate) {this.props.onUpdate({ id: 1, name: '张三' });}// ✅ 方法2: 触发自定义事件this.trigger('update', { id: 1, name: '张三' });}handleDelete() {this.trigger('delete', { id: 1 });}
}
<!-- 子组件模板 -->
<templates><t t-name="ChildTemplate"><button t-on-click="handleClick">更新</button><button t-on-click="handleDelete">删除</button></t>
</templates>
// 父组件
class ParentComponent extends Component {static template = "ParentTemplate";static components = { ChildComponent };onUpdate(data) {console.log('子组件更新:', data);}onDelete(data) {console.log('子组件删除:', data);}
}
<!-- 父组件模板 -->
<templates><t t-name="ParentTemplate"><!-- ✅ 方法1: 通过 props 传递回调(推荐) --><ChildComponent onUpdate.bind="onUpdate"/><!-- ✅ 方法2: 监听自定义事件 --><ChildComponent t-on-update="onUpdate" t-on-delete="onDelete"/></t>
</templates>
⚠️ 注意事项:
// ❌ 错误写法
<ChildComponent onUpdate="onUpdate"/> // 传的是字符串!// ✅ 正确写法
<ChildComponent onUpdate.bind="onUpdate"/> // .bind 绑定方法
🎯 3. 父访问子:t-ref(引用子组件)
Vue 3 方式
<!-- 父组件 -->
<template><ChildComponent ref="childRef"/><button @click="callChildMethod">调用子方法</button>
</template><script setup>
import { ref } from 'vue';const childRef = ref(null);const callChildMethod = () => {childRef.value.someMethod(); // 调用子组件方法console.log(childRef.value.someData); // 访问子组件数据
};
</script>
OWL 方式 ✅
// 子组件
class ChildComponent extends Component {static template = "ChildTemplate";setup() {this.state = useState({count: 0});}// ✅ 可以被父组件调用的方法increment() {this.state.count++;console.log('子组件的 count:', this.state.count);}getData() {return { count: this.state.count };}
}
// 父组件
import { useRef, onMounted } from "@odoo/owl";class ParentComponent extends Component {static template = "ParentTemplate";static components = { ChildComponent };setup() {// ✅ 创建 ref 引用this.childRef = useRef("childRef");onMounted(() => {// ✅ 通过 .comp 访问子组件实例const child = this.childRef.comp;if (child) {child.increment(); // 调用子组件方法console.log(child.getData()); // 获取子组件数据}});}callChildMethod() {this.childRef.comp.increment();}
}
<!-- 父组件模板 -->
<templates><t t-name="ParentTemplate"><!-- ✅ 使用 t-ref 标记子组件 --><ChildComponent t-ref="childRef"/><button t-on-click="callChildMethod">调用子方法</button></t>
</templates>
🔍 ref 的其他用法:
setup() {// DOM 元素引用this.inputRef = useRef("inputRef");onMounted(() => {// ✅ 访问 DOM 元素this.inputRef.el.focus();this.inputRef.el.value = 'Hello';});
}
<input t-ref="inputRef" type="text"/>
🎯 4. 跨层级通信:env (环境对象)
Vue 3 方式
<!-- 祖先组件 -->
<script setup>
import { provide } from 'vue';
provide('theme', 'dark');
provide('user', { name: '张三' });
</script>
<!-- 后代组件 -->
<script setup>
import { inject } from 'vue';
const theme = inject('theme');
const user = inject('user');
</script>
OWL 方式 ✅
// 根组件 / App 入口
import { Component, mount } from "@odoo/owl";class App extends Component {static template = "AppTemplate";static components = { ParentComponent };setup() {// ✅ 定义全局环境变量this.env.theme = 'dark';this.env.currentUser = { name: '张三', role: 'admin' };// ✅ 定义全局方法this.env.showNotification = (message) => {console.log('通知:', message);};}
}// 挂载应用
mount(App, document.body, {env: {// 初始环境对象appName: 'My OWL App',version: '1.0.0'}
});
// 任意后代组件(无论多深)
class DeepChildComponent extends Component {static template = "DeepChildTemplate";setup() {// ✅ 访问环境变量console.log('主题:', this.env.theme);console.log('当前用户:', this.env.currentUser);console.log('应用名:', this.env.appName);}showMessage() {// ✅ 调用全局方法this.env.showNotification('操作成功!');}
}
✅ env 的常见用途:
// 1. 全局配置
this.env.config = {apiUrl: 'https://api.example.com',timeout: 5000
};// 2. 全局服务
this.env.services = {rpc: rpcService,notification: notificationService,router: routerService
};// 3. 当前用户信息
this.env.user = {id: 1,name: '张三',permissions: ['read', 'write']
};// 4. i18n 翻译
this.env._t = (key) => translations[key];
🎯 5. 全局状态管理:Store
Vue 3 (Pinia)
// store.js
import { defineStore } from 'pinia';export const useCounterStore = defineStore('counter', {state: () => ({count: 0}),actions: {increment() {this.count++;}}
});
<!-- 组件中使用 -->
<script setup>
import { useCounterStore } from './store';
const store = useCounterStore();
</script><template><div>{{ store.count }}</div>
</template>
OWL 方式 ✅
// store.js
import { reactive } from "@odoo/owl";// ✅ 创建响应式 Store
export const store = reactive({count: 0,user: null,// actionsincrement() {this.count++;},setUser(user) {this.user = user;}
});
// 组件中使用
import { Component, useState } from "@odoo/owl";
import { store } from './store';class CounterComponent extends Component {static template = "CounterTemplate";setup() {// ✅ 使用 useState 包裹 store,使其响应式this.store = useState(store);}increment() {this.store.increment();}
}
<templates><t t-name="CounterTemplate"><div><p>Count: <t t-esc="store.count"/></p><button t-on-click="increment">增加</button></div></t>
</templates>
✅ 高级 Store 模式:
// advancedStore.js
import { reactive } from "@odoo/owl";class Store {constructor() {this.state = reactive({users: [],loading: false,error: null});}// Gettersget activeUsers() {return this.state.users.filter(u => u.isActive);}// Actionsasync fetchUsers() {this.state.loading = true;try {const response = await fetch('/api/users');this.state.users = await response.json();} catch (err) {this.state.error = err.message;} finally {this.state.loading = false;}}addUser(user) {this.state.users.push(user);}deleteUser(id) {this.state.users = this.state.users.filter(u => u.id !== id);}
}export const userStore = new Store();
// 在组件中使用
import { userStore } from './advancedStore';class UserList extends Component {setup() {this.store = useState(userStore.state);// 初始化时加载数据userStore.fetchUsers();}get activeUsers() {return userStore.activeUsers;}addUser() {userStore.addUser({ id: Date.now(), name: '新用户' });}
}
🎯 6. 兄弟组件通信
方式1: 通过父组件中转(推荐)
// 父组件
class ParentComponent extends Component {static template = "ParentTemplate";static components = { ChildA, ChildB };setup() {this.state = useState({sharedData: ''});}onChildAUpdate(data) {this.state.sharedData = data;// ChildB 会自动收到更新的 props}
}
<templates><t t-name="ParentTemplate"><ChildA onUpdate.bind="onChildAUpdate"/><ChildB data="state.sharedData"/></t>
</templates>
方式2: 使用共享 Store
// 组件 A
class ComponentA extends Component {updateData() {store.sharedData = 'Hello from A';}
}// 组件 B
class ComponentB extends Component {setup() {this.store = useState(store);// 当 ComponentA 更新 store 时,这里自动响应}
}
🚀 完整实战案例:购物车系统
Store 定义
// cartStore.js
import { reactive } from "@odoo/owl";class CartStore {constructor() {this.state = reactive({items: [],couponCode: ''});}// Gettersget totalPrice() {return this.state.items.reduce((sum, item) => sum + item.price * item.quantity, 0);}get itemCount() {return this.state.items.reduce((sum, item) => sum + item.quantity, 0);}// ActionsaddItem(product) {const existing = this.state.items.find(i => i.id === product.id);if (existing) {existing.quantity++;} else {this.state.items.push({ ...product, quantity: 1 });}}removeItem(id) {this.state.items = this.state.items.filter(i => i.id !== id);}updateQuantity(id, quantity) {const item = this.state.items.find(i => i.id === id);if (item) {item.quantity = quantity;}}clearCart() {this.state.items = [];}applyCoupon(code) {this.state.couponCode = code;}
}export const cartStore = new CartStore();
产品列表组件
// ProductList.js
import { Component } from "@odoo/owl";
import { cartStore } from './cartStore';class ProductList extends Component {static template = "ProductListTemplate";setup() {this.products = [{ id: 1, name: '商品A', price: 100 },{ id: 2, name: '商品B', price: 200 }];}addToCart(product) {cartStore.addItem(product);this.env.showNotification(`${product.name} 已加入购物车`);}
}
<templates><t t-name="ProductListTemplate"><div class="product-list"><div t-foreach="products" t-as="product" t-key="product.id"><h3><t t-esc="product.name"/></h3><p>¥<t t-esc="product.price"/></p><button t-on-click="() => this.addToCart(product)">加入购物车</button></div></div></t>
</templates>
购物车组件
// CartView.js
import { Component, useState } from "@odoo/owl";
import { cartStore } from './cartStore';class CartView extends Component {static template = "CartViewTemplate";setup() {// ✅ 绑定 store,自动响应变化this.cart = useState(cartStore.state);}get totalPrice() {return cartStore.totalPrice;}get itemCount() {return cartStore.itemCount;}removeItem(id) {cartStore.removeItem(id);}updateQuantity(id, quantity) {cartStore.updateQuantity(id, parseInt(quantity));}checkout() {console.log('结算:', this.cart.items);cartStore.clearCart();}
}
<templates><t t-name="CartViewTemplate"><div class="cart-view"><h2>购物车 (<t t-esc="itemCount"/>)</h2><div t-if="cart.items.length === 0">购物车是空的</div><div t-else=""><div t-foreach="cart.items" t-as="item" t-key="item.id"><h4><t t-esc="item.name"/></h4><p>单价: ¥<t t-esc="item.price"/></p><input type="number" t-att-value="item.quantity"t-on-change="(ev) => this.updateQuantity(item.id, ev.target.value)"/><button t-on-click="() => this.removeItem(item.id)">删除</button></div><div class="total"><h3>总计: ¥<t t-esc="totalPrice"/></h3><button t-on-click="checkout">结算</button></div></div></div></t>
</templates>
购物车图标组件(兄弟组件)
// CartIcon.js
import { Component, useState } from "@odoo/owl";
import { cartStore } from './cartStore';class CartIcon extends Component {static template = "CartIconTemplate";setup() {this.cart = useState(cartStore.state);}get itemCount() {return cartStore.itemCount;}
}
<templates><t t-name="CartIconTemplate"><div class="cart-icon">🛒<span class="badge" t-if="itemCount > 0"><t t-esc="itemCount"/></span></div></t>
</templates>
💡 最佳实践总结
✅ 推荐做法
// 1. Props 传递数据(父→子)
<ChildComponent title="state.title"/>// 2. 回调函数通信(子→父)
<ChildComponent onUpdate.bind="handleUpdate"/>// 3. 使用 Store 管理全局状态
this.store = useState(globalStore.state);// 4. 使用 env 传递全局服务
this.env.notification.show('成功');// 5. 使用 t-ref 访问子组件
this.childRef.comp.someMethod();
❌ 避免的做法
// ❌ 不要直接修改 props
this.props.count++; // 错误!// ❌ 不要在子组件中访问父组件
this.__owl__.parent // 不推荐!// ❌ 不要过度使用 env
this.env.someTemporaryData // 应该用 props 或 store
🎯 组件通信决策树
需要传递数据?
├─ 父→子? → 用 Props
├─ 子→父? → 用回调函数 (onXxx.bind)
├─ 父访问子? → 用 t-ref
├─ 跨多层级? → 用 env
├─ 全局状态? → 用 Store
└─ 兄弟组件? → 通过父组件 或 Store