📊 组件通信方式总览
|----------|-----------------|------------------|----------|
| 通信方式 | 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,
// actions
increment() {
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
});
}
// Getters
get activeUsers() {
return this.state.users.filter(u => u.isActive);
}
// Actions
async 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: ''
});
}
// Getters
get 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
);
}
// Actions
addItem(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