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,
    
    // 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
相关推荐
梦6503 小时前
axios请求
vue.js
花开月正圆3 小时前
遇见docker-compose
前端
护国神蛙3 小时前
自动翻译插件中的智能字符串切割方案
前端·javascript·babel
TechFrank3 小时前
浏览器云端写代码,远程开发 Next.js 应用的简易教程
前端
PaytonD3 小时前
LoopBack 2 如何设置静态资源缓存时间
前端·javascript·node.js
snow@li3 小时前
d3.js:学习积累
开发语言·前端·javascript
vincention3 小时前
JavaScript 中 this 指向完全指南
前端
qyresearch_4 小时前
射频前端MMIC:5G时代的技术引擎与市场机遇
前端·5g
天蓝色的鱼鱼4 小时前
Next.js 渲染模式全解析:如何正确选择客户端与服务端渲染
前端·react.js·next.js