深度解析:在Odoo 18中基于原生Owl框架為PWA定制功能豐富的底部導航欄

本文旨在提供一個從架構設計、核心功能實現到高級用戶體驗優化的全面指南,詳細闡述如何在Odoo 18中,完全利用其原生的Owl前端框架,為漸進式網絡應用(PWA)從零開始開發一個功能完備、數據驅動且高度可定制的底部導航欄。我們將深入探討組件注入、路由集成、后端實時通信、動態權限渲染、全局狀態管理及PWA離線生命周期等關鍵技術領域。


第一部分:架構設計與核心組件構建

本部分將奠定整個項目的基礎,闡述導航欄Owl組件的基礎結構設計,包括如何定義組件的模板(XML)、樣式(CSS)和邏輯(JS),并確立其在Odoo Web客戶端整體布局中的位置和集成方式。成功的關鍵在于找到一個穩定且無侵入性的注入點,以確保導航欄作為持久性UI元素存在,獨立于主內容區的視圖切換。

1.1 模塊結構與資產定義

首先,所有定制開發都應封裝在一個獨立的Odoo模塊中,以保證代碼的模塊化和可維護性。

模塊文件結構示例 (custom_pwa_navbar):

custom_pwa_navbar/
├── __init__.py
├── __manifest__.py
├── static/
│   └── src/
│       ├── components/
│       │   └── pwa_navbar/
│       │       ├── pwa_navbar.js
│       │       ├── pwa_navbar.xml
│       │       └── pwa_navbar.scss
│       └── js/
│           └── navbar_patch.js
└── views/└── assets.xml

__manifest__.py 文件配置:

清單文件是模塊的入口點,必須在assets字典中聲明所有前端資源,以便Odoo的資產管理器能夠正確打包和加載它們。

# custom_pwa_navbar/__manifest__.py
{'name': 'PWA Bottom Navigation Bar','version': '18.0.1.0.0','category': 'Productivity','summary': 'Adds a persistent PWA-style bottom navigation bar to the Odoo 18 Web Client.','author': 'Your Name','website': 'https://your.website.com','license': 'LGPL-3','depends': ['web'],'data': [],'assets': {'web.assets_backend': [# 引入組件的SCSS樣式'custom_pwa_navbar/static/src/components/pwa_navbar/pwa_navbar.scss',# 引入組件的JS邏輯和QWeb模板'custom_pwa_navbar/static/src/components/pwa_navbar/pwa_navbar.js','custom_pwa_navbar/static/src/components/pwa_navbar/pwa_navbar.xml',# 引入用于注入組件的Patch邏輯'custom_pwa_navbar/static/src/js/navbar_patch.js',],},'installable': True,'application': False,
}

1.2 核心組件:PwaNavBar

PwaNavBar是導航欄的核心Owl組件,負責渲染UI并處理用戶交互。

pwa_navbar.js (組件邏輯):

/** @odoo-module **/import { Component, useState } from "@odoo/owl";export class PwaNavBar extends Component {static template = "custom_pwa_navbar.PwaNavBar";setup() {// 使用useState管理導航項的狀態,例如激活項this.state = useState({activeItem: 'home',navItems: [{ id: 'home', icon: 'fa-home', label: '首頁', action: 'home_action' },{ id: 'messages', icon: 'fa-comments', label: '消息', action: 'message_action', badge: 0 },{ id: 'tasks', icon: 'fa-tasks', label: '任務', action: 'task_action', badge: 0 },{ id: 'profile', icon: 'fa-user', label: '我的', action: 'profile_action' },]});}onNavItemClick(itemId) {this.state.activeItem = itemId;// 后續部分將實現點擊后觸發客戶端動作console.log(`Navigating to: ${itemId}`);}
}

pwa_navbar.xml (QWeb模板):

模板定義了組件的HTML結構。遵循addon_name.ComponentName的命名約定以避免沖突。

<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve"><t t-name="custom_pwa_navbar.PwaNavBar" owl="1"><div class="o_pwa_navbar"><t t-foreach="state.navItems" t-as="item" t-key="item.id"><div t-att-class="{'o_pwa_navbar_item': true,'active': state.activeItem === item.id}"t-on-click="() => this.onNavItemClick(item.id)"><i t-att-class="'fa ' + item.icon"/><span class="o_pwa_navbar_label" t-esc="item.label"/><t t-if="item.badge > 0"><span class="o_pwa_navbar_badge" t-esc="item.badge"/></t></div></t></div></t>
</templates>

pwa_navbar.scss (組件樣式):

.o_pwa_navbar {display: none; // 默認在桌面端隱藏position: fixed;bottom: 0;left: 0;width: 100%;height: 60px;background-color: #ffffff;border-top: 1px solid #e0e0e0;display: flex;justify-content: space-around;align-items: center;z-index: 1050; // 需要仔細管理z-index,確保在內容之上,但在模態框之下box-shadow: 0 -2px 5px rgba(0,0,0,0.05);.o_pwa_navbar_item {display: flex;flex-direction: column;align-items: center;justify-content: center;color: #757575;cursor: pointer;flex-grow: 1;position: relative;padding: 5px 0;transition: color 0.2s ease-in-out;&.active {color: var(--o-brand-primary);}.fa {font-size: 22px;}.o_pwa_navbar_label {font-size: 12px;margin-top: 4px;}.o_pwa_navbar_badge {position: absolute;top: 2px;right: 20%;background-color: red;color: white;border-radius: 50%;padding: 2px 6px;font-size: 10px;font-weight: bold;line-height: 1;}}
}// 使用媒體查詢,僅在移動設備屏幕寬度下顯示導航欄
@media (max-width: 767.98px) {.o_pwa_navbar {display: flex;}// 為主內容區增加padding,防止被導航欄遮擋.o_action_manager {padding-bottom: 60px;}
}

1.3 注入點分析與patch實現

這是整個架構中最關鍵且最具挑戰性的一步。 目標是將PwaNavBar組件作為一個持久性元素注入到Odoo的WebClient中。

研究與分析:

根據研究,Odoo 18的WebClient是整個后端UI的根Owl組件,其模板為webclient_templates.xml。它被直接掛載到document.body上,并包含了navbaraction_container等主要UI區域。為了實現持久化,我們不能將導航欄注入到action_container內部,因為這會導致它隨視圖切換而被銷毀和重建。

最佳注入策略:

最佳策略是修補(patch)WebClient組件,在其DOM結構中找到一個合適的、持久的掛載點。

navbar_patch.js (注入邏輯):

/** @odoo-module **/import { patch } from "@web/core/utils/patch";
import { WebClient } from "@web/web_client/web_client";
import { PwaNavBar } from "@custom_pwa_navbar/components/pwa_navbar/pwa_navbar";
import { onMounted, onWillUnmount, useRef } from "@odoo/owl";patch(WebClient.prototype, {setup() {super.setup(...arguments);this.pwaNavBarRoot = useRef("pwaNavBarRoot");let pwaNavBarComponent = null;onMounted(async () => {// **關鍵注入邏輯**// 1. 創建一個新的DOM元素作為掛載點const navBarElement = document.createElement('div');navBarElement.setAttribute('class', 'o_pwa_navbar_container');// 2. 將掛載點附加到WebClient的根元素(this.el)上// this.el 指向WebClient渲染后的根DOM節點// **預測與假設**: 將其作為WebClient根元素的直接子節點是最可靠的持久化方式。// 這確保了它與action_manager同級,而不會被其內部的視圖切換所影響。if (this.el) {this.el.appendChild(navBarElement);// 3. 在新創建的掛載點上掛載我們的PwaNavBar組件pwaNavBarComponent = new PwaNavBar(null, {}); // 第二個參數是propsawait pwaNavBarComponent.mount(navBarElement);}});onWillUnmount(() => {// 在WebClient卸載時,確保我們的組件也被正確銷毀if (pwaNavBarComponent) {pwaNavBarComponent.destroy();}});}
});

架構決策與說明:

  1. 為什么選擇patch? patch是Odoo 18官方推薦的、用于非侵入式地修改核心組件行為的API。它比直接覆寫整個組件或通過jQuery進行DOM操作更為健壯和可維護。
  2. 為什么選擇onMounted? 我們在WebClientonMounted鉤子中執行注入。這是因為我們需要確保WebClient的根元素(this.el)已經被渲染并附加到實際的DOM中,這樣我們才能安全地向其appendChild
  3. 為什么動態創建掛載點? 直接修改webclient_templates.xml是一種方法,但通過patch在JS中動態創建和附加掛載點,可以減少對核心模板的依賴,降低未來Odoo版本升級時產生沖突的風險。這種方式更加靈活和解耦。
  4. 持久性保證: 由于我們的導航欄容器是WebClient根元素的直接子節點,并且與負責視圖切換的action_manager同級,因此它不會受到視圖切換的影響,從而實現了UI的持久性。
  5. 響應式布局: 通過SCSS中的媒體查詢,我們確保了導航欄僅在移動端視圖下顯示,并為主內容區添加了padding-bottom,避免了內容遮擋問題。這是實現良好移動端體驗的關鍵。

通過以上設計,我們構建了一個結構清晰、可維護且與Odoo核心框架緊密集成的持久性導航欄組件,為后續的功能開發奠定了堅實的基礎。


第二部分:路由與客戶端動作集成

本部分將詳細說明如何將導航欄按鈕與Odoo的前端路由系統深度集成。目標是實現點擊導航按鈕后,能夠精確地觸發特定菜單(ir.ui.menu)的加載、執行預定義的客戶端動作(ir.actions.client),并有效管理視圖狀態的切換。

2.1 Odoo前端路由與actionService

Odoo的單頁應用(SPA)體驗依賴于其內部的路由和動作服務。直接操作瀏覽器URL或History API是不可取且無效的。所有導航和視圖切換都應通過actionService來完成。

  • actionService: 這是Odoo前端的核心服務之一,負責處理所有類型的動作(窗口動作、客戶端動作、報表等)。通過useService("action")鉤子可以在Owl組件中訪問它。
  • doAction方法: 這是actionService的核心方法,用于執行一個動作。它可以接受一個動作的XML ID(字符串),或者一個描述動作的詳細對象。

2.2 定義客戶端動作 (ir.actions.client)

為了將導航欄按鈕與特定的Owl組件視圖關聯起來,我們需要定義ir.actions.client記錄。客戶端動作允許我們將一個唯一的tag與一個前端Owl組件關聯起來。

示例:為“任務”頁面定義客戶端動作 (XML):

<!-- custom_pwa_navbar/views/actions.xml -->
<odoo><record id="action_client_pwa_tasks" model="ir.actions.client"><field name="name">PWA Tasks</field><field name="tag">pwa_tasks_view</field><field name="target">current</field></record><!-- 同樣可以定義其他頁面的動作 --><record id="action_client_pwa_messages" model="ir.actions.client"><field name="name">PWA Messages</field><field name="tag">pwa_messages_view</field></record>
</odoo>
  • tag: 這是關鍵字段,它將作為前端JS代碼中識別和注冊組件的唯一標識符。
  • target: 控制動作的顯示方式。current表示在主內容區顯示,new表示在模態框中顯示,fullscreen則全屏顯示。

2.3 注冊客戶端動作組件

定義了XML動作后,我們需要在JavaScript中創建一個對應的Owl組件,并使用actionRegistry將其與tag關聯起來。

創建“任務”視圖組件 (pwa_tasks_view.js):

/** @odoo-module **/import { registry } from "@web/core/registry";
import { Component, onMounted, useState } from "@odoo/owl";class PwaTasksView extends Component {static template = "custom_pwa_navbar.PwaTasksView";setup() {this.state = useState({ tasks: [] });onMounted(() => {// 模擬獲取任務數據this.state.tasks = [{ id: 1, name: "設計導航欄UI" },{ id: 2, name: "實現路由集成" },{ id: 3, name: "連接后端API" },];});}
}// 注冊組件到動作注冊表
registry.category("actions").add("pwa_tasks_view", PwaTasksView);

對應的QWeb模板 (pwa_tasks_view.xml):

<templates xml:space="preserve"><t t-name="custom_pwa_navbar.PwaTasksView" owl="1"><div class="p-3"><h1>我的任務</h1><ul class="list-group"><t t-foreach="state.tasks" t-as="task" t-key="task.id"><li class="list-group-item" t-esc="task.name"/></t></ul></div></t>
</templates>

確保這些新的JS和XML文件也加入到__manifest__.pyweb.assets_backend中。

2.4 在導航欄中觸發動作

現在,我們可以修改PwaNavBar組件,使其在點擊時調用actionService.doAction

修改后的 pwa_navbar.js:

/** @odoo-module **/import { Component, useState } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";export class PwaNavBar extends Component {static template = "custom_pwa_navbar.PwaNavBar";setup() {this.actionService = useService("action"); // 引入action服務this.state = useState({activeItem: 'home',navItems: [// 假設'home'對應一個已存在的菜單動作{ id: 'home', icon: 'fa-home', label: '首頁', action: 'web.action_web_client_home_menu', actionType: 'xml_id' },// 'messages'和'tasks'對應我們自定義的客戶端動作{ id: 'messages', icon: 'fa-comments', label: '消息', action: { tag: 'pwa_messages_view', type: 'ir.actions.client' }, actionType: 'object' },{ id: 'tasks', icon: 'fa-tasks', label: '任務', action: { tag: 'pwa_tasks_view', type: 'ir.actions.client' }, actionType: 'object' },{ id: 'profile', icon: 'fa-user', label: '我的', action: { tag: 'pwa_profile_view', type: 'ir.actions.client' }, actionType: 'object' },]});}async onNavItemClick(item) {if (this.state.activeItem === item.id) {return; // 如果已經是激活項,則不執行任何操作}this.state.activeItem = item.id;// **核心路由邏輯**await this.actionService.doAction(item.action, {// 清除面包屑,以獲得更原生的PWA體驗clear_breadcrumbs: true,});}
}

邏輯解析:

  1. useService("action"): 我們使用此鉤子獲取actionService的實例。
  2. navItems 結構更新: navItems數組現在包含了執行動作所需的信息。我們可以混合使用動作的XML ID(對于Odoo原生動作)和動作描述對象(對于我們的自定義客戶端動作)。
  3. doAction調用: 在onNavItemClick方法中,我們調用this.actionService.doAction(item.action)。Odoo的actionService會負責解釋傳入的字符串或對象,找到并執行對應的動作,最終渲染PwaTasksView組件到主內容區。
  4. clear_breadcrumbs: true: 這是一個非常有用的選項,它可以清除頂部的面包屑導航,使得PWA的界面更加簡潔,更像一個獨立應用。

2.5 傳遞參數與上下文

doAction方法允許傳遞contextdomain等參數,這對于創建動態和數據驅動的視圖至關重要。

示例:打開“未讀消息”視圖

假設我們的pwa_messages_view組件可以根據上下文顯示所有消息或僅顯示未讀消息。

PwaNavBar中調用:

// 在PwaNavBar的某個方法中
this.actionService.doAction({tag: 'pwa_messages_view',type: 'ir.actions.client',context: {show_only_unread: true,default_user_id: odoo.session_info.user_id,}
});

pwa_messages_view.js中接收:

// ...
class PwaMessagesView extends Component {static template = "custom_pwa_navbar.PwaMessagesView";setup() {// props.action.context 包含了傳遞過來的上下文const context = this.props.action.context || {};this.showOnlyUnread = context.show_only_unread || false;console.log("Should show only unread messages:", this.showOnlyUnread);// ...后續邏輯可以根據this.showOnlyUnread來獲取不同數據}
}
// ...

通過這種方式,導航欄不僅能切換視圖,還能在切換時向目標視圖傳遞初始狀態或過濾條件,實現了強大的組件間通信和動態內容展示。

至此,我們已經成功地將底部導航欄與Odoo的客戶端動作系統連接起來,實現了流暢的、受控的單頁應用內導航。


第三部分:后端通信與數據驅動

一個功能豐富的導航欄不僅僅是靜態鏈接的集合,它還需要與后端服務器進行通信,以獲取動態數據來驅動UI的變化,例如,在“消息”圖標上顯示未讀消息的數量角標(Badge)。本部分將聚焦于導航欄與Odoo后端的交互,講解如何通過RPC調用模型方法,以及如何利用Odoo 18的實時總線服務(Bus Service)實現數據的實時更新。

3.1 通過RPC服務獲取初始數據

當導航欄組件首次加載時,我們需要從后端獲取一次初始數據,例如各個模塊的待辦事項數量。Odoo前端提供了rpc服務來調用后端Python模型的公共方法。

后端Python方法定義:

在自定義模塊中創建一個新的模型或擴展現有模型,來提供一個聚合數據的接口。

# custom_pwa_navbar/models/pwa_data_provider.py
from odoo import models, apiclass PwaDataProvider(models.AbstractModel):_name = 'pwa.data.provider'_description = 'PWA Data Provider'@api.modeldef get_navbar_badge_data(self):"""一個RPC方法,用于聚合計算導航欄角標所需的數據。"""# **這里的邏輯需要根據實際業務來替換**# 示例:獲取當前用戶未讀消息數量unread_messages = self.env['mail.message'].search_count([('needaction', '=', True),('author_id', '!=', self.env.user.partner_id.id)])# 示例:獲取分配給當前用戶的未完成任務數量# 假設我們有一個'project.task'模型open_tasks = self.env['project.task'].search_count([('user_ids', 'in', self.env.user.id),('stage_id.is_closed', '=', False)])return {'messages': unread_messages,'tasks': open_tasks,}

前端PwaNavBar組件調用RPC:

PwaNavBar組件的setup方法中,使用useService("rpc")鉤子,并在onWillStart生命周期鉤子中調用RPC。onWillStart是執行異步初始化操作的理想位置,因為組件會等待其中的Promise完成后再進行首次渲染。

/** @odoo-module **/import { Component, useState, onWillStart } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";export class PwaNavBar extends Component {static template = "custom_pwa_navbar.PwaNavBar";setup() {this.rpcService = useService("rpc");this.state = useState({// ... (其他狀態)navItems: [// ...{ id: 'messages', ..., badge: 0 },{ id: 'tasks', ..., badge: 0 },// ...]});onWillStart(async () => {await this.fetchBadgeData();});}async fetchBadgeData() {try {const result = await this.rpcService({model: 'pwa.data.provider',method: 'get_navbar_badge_data',args: [],kwargs: {},});// 更新狀態,驅動UI重新渲染const messagesItem = this.state.navItems.find(item => item.id === 'messages');if (messagesItem) messagesItem.badge = result.messages;const tasksItem = this.state.navItems.find(item => item.id === 'tasks');if (tasksItem) tasksItem.badge = result.tasks;} catch (error) {console.error("Failed to fetch navbar badge data:", error);}}// ... (其他方法)
}

3.2 利用Bus服務實現數據實時更新

僅在加載時獲取一次數據是不夠的。當后端數據發生變化時(例如,收到一條新消息),UI應該能夠實時響應。Odoo 18的bus_service為此提供了完美的解決方案。Odoo 18的Bus服務底層已重構為使用WebSocket,提供了比傳統長輪詢更高效、更低延遲的實時通信能力。

工作流程:

  1. 后端觸發: 當特定事件發生時(如創建新任務、收到新消息),后端Python代碼通過Bus服務向特定頻道發送一個通知。
  2. 前端監聽: 前端Owl組件訂閱該頻道,并注冊一個回調函數。
  3. 實時更新: 當接收到通知時,回調函數被執行,可以在其中更新組件狀態或重新調用RPC獲取最新數據,從而刷新UI。

后端觸發Bus通知:

我們需要在數據變化的地方(例如,createwrite方法)發送通知。

# 擴展 project.task 模型
from odoo import models, apiclass ProjectTask(models.Model):_inherit = 'project.task'def _send_task_update_notification(self, user_id):"""發送通知到指定用戶的私有頻道"""# 用戶的私有頻道通常是 (database_name, model_name, record_id) 或一個唯一標識符# 這里我們使用一個自定義頻道名稱channel_name = f'pwa_navbar_user_{user_id}'message = {'type': 'task_update','payload': {'message': 'Your tasks have been updated.'}}self.env['bus.bus']._sendone(channel_name, 'pwa_notification', message)@api.model_create_multidef create(self, vals_list):records = super().create(vals_list)for record in records:for user in record.user_ids:self._send_task_update_notification(user.id)return recordsdef write(self, vals):# ... (在write方法中也需要類似的邏輯)res = super().write(vals)# ...return res

前端監聽Bus通知:

PwaNavBar組件中,使用useBus鉤子來訂閱頻道和處理消息。

/** @odoo-module **/import { Component, useState, onWillStart } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
import { useBus } from "@web/core/utils/hooks";export class PwaNavBar extends Component {static template = "custom_pwa_navbar.PwaNavBar";setup() {this.rpcService = useService("rpc");const session = useService("session"); // 獲取會話服務以拿到用戶IDthis.state = useState({ /* ... */ });onWillStart(async () => {await this.fetchBadgeData();});// **實時監聽邏輯**const channelName = `pwa_navbar_user_${session.user_id}`;useBus(this.env.bus, channelName, (notification) => {// 收到通知后,重新獲取角標數據// 這里的notification參數就是后端發送的messageconsole.log("Received bus notification:", notification);if (notification.type === 'pwa_notification' && notification.payload.type === 'task_update') {// 收到任務更新通知,重新獲取所有角標數據this.fetchBadgeData();}});}async fetchBadgeData() { /* ... */ }// ...
}

解析與最佳實踐:

  1. useBus鉤子: @web/core/utils/hooks提供的useBus鉤子極大地簡化了事件監聽。它會自動處理組件掛載時的訂閱和卸載時的取消訂閱,有效防止了內存泄漏。
  2. 頻道設計: 頻道的命名至關重要。為每個用戶創建一個私有頻道(例如 pwa_navbar_user_{user_id})可以確保通知被精確地發送給目標用戶,避免了不必要的廣播和客戶端處理。
  3. 消息負載: 通知負載(message)應該設計得輕量且信息明確。一種常見的模式是,通知本身只包含一個“數據已更新”的信號,客戶端收到信號后再主動發起RPC請求獲取詳細數據。這種方式可以確保數據的一致性,并簡化后端邏輯。
  4. 性能考量: 雖然WebSocket性能優越,但頻繁的后端觸發和前端RPC調用仍需謹慎。對于變化非常頻繁的數據,可以考慮在前端進行節流(throttling)或防抖(debouncing)處理,或者在后端聚合多個更新后再一次性發送通知。

通過結合一次性的RPC拉取和持續的WebSocket推送,我們的導航欄實現了高效的數據驅動,能夠實時、準確地向用戶展示關鍵信息的動態變化。


第四部分:高級UI/UX實現:動態權限、角標與轉場動畫

本部分將集中解決高級用戶體驗(UI/UX)的實現細節,包括如何根據當前用戶的權限組動態渲染導航項,如何實現消息角標(Badges)的顯示與更新,以及如何利用Owl的鉤子和CSS技術創建平滑的視圖轉場動畫,從而提供接近原生應用的交互感受。

4.1 動態權限:根據用戶組渲染導航項

在企業級應用中,不同角色的用戶應該看到不同的導航選項。實現這一點的關鍵在于前端能夠安全地獲取用戶的權限信息,并據此進行條件渲染。

安全模型:后端驗證,前端展示

首先必須明確一個核心安全原則:前端的任何顯示/隱藏邏輯都只是為了提升用戶體驗,絕不能作為安全屏障。 真正的安全控制必須由Odoo后端的訪問權限(Access Rights)和記錄規則(Record Rules)來強制執行。即使惡意用戶通過修改前端代碼顯示了無權訪問的導航項,當他們嘗試訪問相應功能時,后端的RPC調用也必須因為權限不足而被拒絕。

實現步驟:

  1. 使用user服務檢查權限: Odoo的user服務提供了一個便捷的方法hasGroup(group_xml_id)來檢查當前用戶是否屬于某個特定的安全組。
  2. onWillStart中預加載權限: 與獲取RPC數據類似,權限檢查是異步操作,應在onWillStart鉤子中完成,以確保在組件首次渲染前權限狀態已就緒。
  3. 使用useState存儲權限狀態: 將權限檢查的結果存儲在組件的state中。
  4. 在模板中使用t-if進行條件渲染: 根據state中的權限標志來決定是否渲染某個導航項。

示例代碼 (pwa_navbar.js):

/** @odoo-module **/import { Component, useState, onWillStart } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";export class PwaNavBar extends Component {static template = "custom_pwa_navbar.PwaNavBar";setup() {this.userService = useService("user");this.state = useState({activeItem: 'home',navItems: [{ id: 'home', ..., requiredGroup: null }, // 'home'對所有用戶可見{ id: 'messages', ..., requiredGroup: null },// 'tasks'導航項只對'project.group_project_user'組的成員可見{ id: 'tasks', ..., requiredGroup: 'project.group_project_user', isVisible: false },// 'admin'導航項只對'base.group_system'組的成員(管理員)可見{ id: 'admin', icon: 'fa-cogs', label: '管理', action: 'some_admin_action', requiredGroup: 'base.group_system', isVisible: false }]});onWillStart(async () => {// ... (fetchBadgeData調用)await this.checkNavPermissions();});}async checkNavPermissions() {const promises = this.state.navItems.map(async (item) => {if (item.requiredGroup) {// hasGroup是異步的,返回一個Promiseitem.isVisible = await this.userService.hasGroup(item.requiredGroup);} else {item.isVisible = true;}});await Promise.all(promises);}// ...
}

模板修改 (pwa_navbar.xml):

<templates xml:space="preserve"><t t-name="custom_pwa_navbar.PwaNavBar" owl="1"><div class="o_pwa_navbar"><t t-foreach="state.navItems" t-as="item" t-key="item.id"><!-- 使用 t-if 指令根據 isVisible 狀態進行條件渲染 --><t t-if="item.isVisible"><div t-att-class="{...}" t-on-click="() => this.onNavItemClick(item)"><!-- ... (item content) ... --></div></t></t></div></t>
</templates>

權限變更的實時響應:

如果用戶的權限在后臺被管理員修改,理想情況下UI應能實時響應。這可以通過結合第三部分討論的Bus服務來實現。當管理員修改用戶組時,后端可以發送一個Bus通知到該用戶的私有頻道,前端收到通知后,重新調用checkNavPermissions方法即可刷新導航欄。

4.2 角標(Badges)的實現

角標的實現已經在第三部分中奠定了基礎。核心在于將從后端獲取的數據(無論是通過初始RPC調用還是通過Bus服務實時更新)綁定到UI上。PwaNavBar組件的state.navItems數組中的badge屬性就是數據模型,而QWeb模板中的t-if="item.badge > 0"<span ... t-esc="item.badge"/>則是視圖渲染。

由于Owl的響應式系統,只要this.state.navItems[...].badge的值發生變化,UI就會自動更新,無需任何手動的DOM操作。

4.3 視圖轉場動畫

為了提升PWA的用戶體驗,使其更接近原生應用,平滑的視圖轉場動畫是必不可少的。當用戶點擊導航欄切換視圖時,我們可以讓舊視圖平滑地淡出,新視圖平滑地淡入。

實現策略:

我們將利用CSS動畫Owl的生命周期鉤子來協同工作。動畫的狀態將由父組件(這里是WebClient的patch)或一個專門的動畫管理器來控制。

1. 定義CSS動畫:

// 在你的主SCSS文件中
.o_action_manager {position: relative; // 父容器需要相對定位
}.main-content { // 假設這是客戶端動作組件的根元素// 默認樣式opacity: 1;transform: scale(1);transition: opacity 300ms ease-in-out, transform 300ms ease-in-out;// 定義進入和離開的狀態&.is-entering {opacity: 0;transform: scale(0.95);}&.is-leaving {opacity: 0;transform: scale(1.05);}
}

2. 在WebClient的patch中管理動畫狀態:

我們需要一個更高級的actionService包裝器或直接在WebClient中管理視圖的切換過程,以便在舊視圖卸載前和新視圖掛載后添加/移除CSS類。

高級動畫控制邏輯 (概念性,在navbar_patch.js中擴展):

// ... (在WebClient的patch中)
patch(WebClient.prototype, {setup() {super.setup(...arguments);this.actionService = useService("action");const originalDoAction = this.actionService.doAction.bind(this.actionService);// **包裝 doAction 方法以注入動畫邏輯**this.actionService.doAction = async (action, options) => {const actionManager = this.el.querySelector('.o_action_manager');const currentView = actionManager ? actionManager.querySelector('.main-content') : null;// 1. 舊視圖離開動畫if (currentView) {currentView.classList.add('is-leaving');// 等待動畫完成await new Promise(resolve => setTimeout(resolve, 300));}// 2. 執行原始的 doAction,這會卸載舊視圖,掛載新視圖const result = await originalDoAction(action, options);// 3. 新視圖進入動畫// 需要延遲一幀以確保新視圖已渲染到DOMrequestAnimationFrame(() => {const newView = actionManager ? actionManager.querySelector('.main-content') : null;if (newView) {newView.classList.add('is-entering');// 強制重繪newView.getBoundingClientRect(); // 移除class以觸發動畫newView.classList.remove('is-entering');}});return result;};}
});

解釋與挑戰:

  • 動畫協調: 上述代碼是一個簡化的概念。真正的挑戰在于精確協調DOM操作和CSS動畫。舊視圖的is-leaving動畫必須在它被從DOM中移除之前完成。setTimeout是一種簡單但不夠精確的方法,更可靠的方式是監聽animationendtransitionend事件。
  • 組件通信: WebClient需要知道哪個是“當前視圖”。這通常需要深入理解ActionManager的內部工作原理,或者讓客戶端動作組件在onMountedonWillUnmount時向其父組件(或全局服務)注冊/注銷自己。
  • 封裝為自定義鉤子: 為了可重用性和代碼整潔,可以將這套復雜的動畫邏輯封裝成一個自定義Owl鉤子,例如useViewTransition()。這個鉤子可以在WebClient中使用,負責包裝actionService并管理CSS類。

一個更可靠的、基于生命周期鉤子的方法:

// 在你的客戶端動作組件基類或mixin中
// MyBaseClientAction.js
import { onMounted, onWillUnmount } from "@odoo/owl";export const transitionMixin = {setup() {// ...onMounted(() => {this.el.classList.add('is-entering');requestAnimationFrame(() => {this.el.classList.remove('is-entering');});});onWillUnmount(async () => {this.el.classList.add('is-leaving');// 創建一個Promise,在動畫結束后resolveawait new Promise(resolve => {const onAnimationEnd = () => {this.el.removeEventListener('transitionend', onAnimationEnd);resolve();};this.el.addEventListener('transitionend', onAnimationEnd);});});}
};// 在你的實際客戶端動作組件中使用
// pwa_tasks_view.js
class PwaTasksView extends Component {static template = "...";setup() {Object.assign(this, transitionMixin);this.setup(); // 調用mixin的setup// ... 你的組件邏輯}
}

這種基于Mixin或組合的方式將動畫邏輯內聚到組件自身,但需要Odoo的ActionManager在卸載組件時等待onWillUnmount中的異步操作完成。這需要對Odoo 18的ActionManager行為進行驗證。

通過上述高級UI/UX技術的實現,我們的PWA導航欄不僅功能強大,而且在視覺和交互層面都提供了流暢、專業且接近原生應用的用戶體驗。


第五部分:狀態管理與性能優化

當應用變得復雜,多個組件需要共享和響應同一份數據時(如用戶權限、導航狀態、角標數據),僅靠組件內部的useState進行狀態管理會變得混亂且難以維護。本部分將探討在復雜交互場景下的全局狀態管理策略,例如使用Owl Store來集中管理應用狀態,并討論確保組件在頻繁更新和復雜動畫下依然保持高性能的優化技巧。

5.1 全局狀態管理:引入Owl Store

動機:

  • 狀態共享: 導航欄需要知道當前激活的路由,而主內容區的視圖也可能需要改變導航欄的狀態(例如,在某個操作后更新角標)。
  • 單一數據源 (Single Source of Truth): 將共享狀態集中存放在一個地方,可以避免數據不一致的問題,使應用狀態更可預測。
  • 邏輯解耦: 將狀態管理邏輯從UI組件中分離出來,使組件更專注于渲染,而Store則負責業務邏輯。

Owl Store 概念 (基于其設計理念和對Vuex/Redux的借鑒):

雖然Odoo 18的官方文檔對@odoo/owl/store的API細節著墨不多,但基于Owl的設計哲學,我們可以推斷并設計一個類似于Vuex/Redux的Store。一個典型的Owl Store會包含:

  • State: 存儲應用狀態的響應式對象。
  • Actions: 處理異步操作(如RPC調用)和復雜業務邏輯的方法。Actions不能直接修改State。
  • Mutations (或直接在Action中修改): 唯一可以修改State的同步函數。在更現代的狀態管理器中,可能會允許Action直接修改State。
  • Getters: 從State中派生出的計算屬性,類似于模型的計算字段。

構建模塊化的Store:

為了保持可維護性,我們將Store按功能領域進行拆分。

Store目錄結構:

custom_pwa_navbar/
└── static/└── src/└── store/├── index.js       # 組合所有store模塊├── nav_store.js   # 導航欄相關狀態└── user_store.js  # 用戶權限和信息

nav_store.js 示例:

// custom_pwa_navbar/static/src/store/nav_store.js
export const navStoreModule = {state: {activeItem: 'home',navItems: [// ... navItems definition ...],},actions: {async setActiveItem({ state, dispatch }, itemId) {if (state.activeItem === itemId) return;state.activeItem = itemId;// 可能需要觸發其他action,例如路由切換},async fetchBadges({ state, root }, rpc) {const data = await rpc({ model: 'pwa.data.provider', method: 'get_navbar_badge_data' });state.navItems.find(i => i.id === 'messages').badge = data.messages;state.navItems.find(i => i.id === 'tasks').badge = data.tasks;},},getters: {getTaskBadge(state) {const taskItem = state.navItems.find(i => i.id === 'tasks');return taskItem ? taskItem.badge : 0;},},
};

index.js (組合Store并創建實例):

// custom_pwa_navbar/static/src/store/index.js
import { createStore } from "@odoo/owl/store"; // **假設API**
import { navStoreModule } from "./nav_store";
import { userStoreModule } from "./user_store";export function setupStore(env) {const store = createStore({modules: {nav: navStoreModule,user: userStoreModule,},env,});return store;
}

推測與假設: 上述createStore和模塊化的API是基于現代狀態管理庫的通用模式推斷的。在實際開發中,需要查閱Odoo 18或Owl的最新官方文檔或源代碼以確認其準確的API。如果Owl Store不提供模塊化,我們可以手動將多個狀態對象組合成一個大的state對象。

在組件中使用Store:

組件可以通過useStore鉤子或從env中訪問Store,并使用選擇器(selector)來訂閱狀態變化。

// pwa_navbar.js
import { useStore } from "@odoo/owl"; // **假設API**// ...
setup() {// ...this.store = useStore();// 使用選擇器訂閱狀態,當返回值變化時,組件會重渲染this.activeItem = useStore(state => state.nav.activeItem);this.navItems = useStore(state => state.nav.navItems);
}onNavItemClick(item) {// 派發action來改變狀態this.store.dispatch('nav/setActiveItem', item.id);this.actionService.doAction(item.action);
}

5.2 狀態持久化

對于PWA,即使用戶關閉并重新打開瀏覽器,某些狀態(如UI偏好)也應該被保留。我們可以使用瀏覽器存儲API將Store的某些部分持久化。

存儲機制

容量

持久性

API類型

適用場景

localStorage

~5-10MB

跨會話持久

同步

少量、非敏感數據,如主題、用戶偏好。

sessionStorage

~5-10MB

單個會話

同步

臨時數據,如表單草稿,當前標簽頁的UI狀態。

IndexedDB

幾乎無限制

跨會話持久

異步

大量、復雜、結構化數據,離線數據緩存,是PWA的首選。

實現策略:

可以創建一個Store插件或在Store的訂閱機制中,監聽狀態變化,并將需要持久化的部分同步到localStorageIndexedDB。應用啟動時,從存儲中讀取數據來初始化Store的state

// 在Store創建邏輯中
// 1. 初始化時從localStorage加載狀態
const persistedNavState = JSON.parse(localStorage.getItem('pwa_nav_state'));
if (persistedNavState) {// 合并到初始狀態Object.assign(navStoreModule.state, persistedNavState);
}// 2. 訂閱狀態變化并保存
store.subscribe((mutation, state) => {// 只持久化部分狀態const stateToPersist = { activeItem: state.nav.activeItem };localStorage.setItem('pwa_nav_state', JSON.stringify(stateToPersist));
});

5.3 性能優化技巧

當Store狀態頻繁變化時,可能會導致不必要的組件重渲染,影響性能,尤其是在執行動畫時。

  1. 使用精細化的選擇器 (useSelector-like):

組件應該只訂閱它們真正需要的數據片段,而不是整個對象。

    • 不好: const user = useStore(state => state.user);user對象中任何不相關的屬性(如user.lastLogin)變化時,組件都會重渲染。
    • 好: const userName = useStore(state => state.user.name); 只有當userName這個原始值變化時,組件才會重渲染。
  1. Memoized Selectors (記憶化選擇器):

對于需要從State中進行復雜計算派生出的數據,應該使用記憶化選擇器(類似Reselect庫的思想)。這可以緩存計算結果,只有當其依賴的原始State部分發生變化時,才重新計算。

概念示例:

import { createSelector } from 'reselect'; // 概念庫const getTasks = state => state.tasks.allTasks;
const getActiveFilter = state => state.tasks.filter;// 這個選擇器只有在allTasks或filter變化時才會重新計算
const getVisibleTasks = createSelector([getTasks, getActiveFilter],(tasks, filter) => {switch (filter) {case 'COMPLETED': return tasks.filter(t => t.completed);// ...default: return tasks;}}
);

即使Owl Store沒有內置createSelector,我們也可以手動實現這個緩存邏輯,或者使用簡單的useMemo鉤子(如果Owl提供)在組件級別緩存計算結果。

  1. 組件級別的Memoization:

對于純展示性組件,如果其props沒有變化,就不應該重新渲染。React為此提供了React.memo,Owl可能也提供了類似的機制來包裹組件,避免不必要的渲染。

  1. 對動畫性能的影響:

確保狀態更新不會不必要地觸發正在進行動畫的組件或其父組件的重渲染。將動畫狀態(如is-entering)與業務數據狀態分離,并使用精細化的選擇器,是避免動畫卡頓的關鍵。

通過引入結構化的Owl Store進行全局狀態管理,并結合持久化和一系列性能優化技術,我們可以構建一個既健壯、可維護,又高度響應和流暢的復雜PWA前端應用。


第六部分:PWA集成與生命周期管理

本部分將專門討論導航欄在PWA環境下的特殊考量,特別是如何通過定制Service Worker來實現高級離線功能,確保導航欄及其驅動的功能在網絡連接不可靠甚至完全離線的情況下依然可用。我們將超越簡單的靜態資源緩存,探索一個完整的離線請求隊列和后臺同步機制。

6.1 Service Worker定制與離線緩存

Odoo 18允許通過自定義模塊來提供manifest.jsonservice-worker.js,從而啟用和定制PWA功能。

基礎:靜態資源緩存

Service Worker的核心功能之一是作為網絡代理,攔截網絡請求并從緩存中提供響應。

service-worker.js 緩存策略示例:

// custom_pwa_navbar/static/src/js/service-worker.jsconst CACHE_NAME = 'pwa-navbar-cache-v1';
const URLS_TO_CACHE = ['/','/web',// ... Odoo核心JS/CSS資源的路徑 (需要小心管理)// ... 導航欄組件相關的JS/XML/CSS資源
];self.addEventListener('install', event => {event.waitUntil(caches.open(CACHE_NAME).then(cache => {console.log('Opened cache');return cache.addAll(URLS_TO_CACHE);}));
});self.addEventListener('fetch', event => {event.respondWith(caches.match(event.request).then(response => {// Cache-first策略if (response) {return response;}return fetch(event.request);}));
});

注意: Odoo的資源URL可能包含哈希值,簡單的靜態列表緩存可能不夠健壯。更高級的策略是使用Workbox.js庫或動態緩存成功的新請求。

6.2 高級離線功能:RPC請求隊列

真正的離線體驗意味著用戶可以執行寫操作(如創建任務、發送消息),這些操作在離線時被暫存,在網絡恢復時自動同步到服務器。

架構設計:

  1. 攔截RPC調用: 我們需要一個機制來攔截所有從前端發出的RPC請求。最佳方式是包裝或修補Odoo的rpc_service
  2. 網絡狀態檢測: 檢查navigator.onLine狀態。如果在線,正常發送RPC。
  3. 請求入隊: 如果離線,將RPC請求的詳細信息(模型、方法、參數)序列化并存入IndexedDB中的一個“待辦隊列”表。
  4. 后臺同步觸發: 注冊一個background sync事件。當網絡恢復時,瀏覽器會自動喚醒Service Worker來處理這個同步事件。
  5. Service Worker處理隊列:sync事件處理器中,Service Worker從IndexedDB讀取隊列中的請求,并按順序將它們重新發送到服務器。
  6. 結果反饋: Service Worker通過postMessage將同步結果(成功或失敗)通知給前端UI,UI可以據此更新狀態或顯示通知。

實現細節:

1. 包裝rpc_service (在navbar_patch.js或新文件中):

// **推測性實現,需要驗證Odoo服務注冊表的行為**
import { registry } from "@web/core/registry";
import { rpcService }s from "@web/core/rpc_service";
import { openDB } from 'idb'; // 使用idb庫簡化IndexedDB操作const dbPromise = openDB('pwa-request-queue-db', 1, {upgrade(db) {db.createObjectStore('requests', { keyPath: 'id', autoIncrement: true });},
});const offlineRpcService = {...rpcService,async rpc(route, params, settings) {if (!navigator.onLine) {console.log("Offline: Queuing RPC request", { route, params });const db = await dbPromise;await db.add('requests', { route, params, timestamp: new Date().getTime() });// 注冊后臺同步const registration = await navigator.serviceWorker.ready;await registration.sync.register('rpc-queue-sync');// **立即向UI返回一個樂觀的響應**// 這對于提升用戶體驗至關重要,讓用戶感覺操作已“完成”return Promise.resolve({ optimistic: true, message: "請求已加入隊列,將在網絡恢復后同步。" });}return rpcService.rpc.call(this, route, params, settings);}
};// 使用更高優先級注冊或直接patch來覆蓋默認的rpc_service
registry.category("services").add("rpc", offlineRpcService, { force: true });

2. Service Worker處理后臺同步:

// service-worker.js
import { openDB } from 'idb';self.addEventListener('sync', event => {if (event.tag === 'rpc-queue-sync') {event.waitUntil(syncRpcQueue());}
});async function syncRpcQueue() {const db = await openDB('pwa-request-queue-db', 1);const allRequests = await db.getAll('requests');for (const req of allRequests) {try {// **這里的fetch需要模擬Odoo的RPC調用結構**const response = await fetch(req.route, {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({ jsonrpc: '2.0', method: 'call', params: req.params })});if (response.ok) {await db.delete('requests', req.id);// 通知UI同步成功notifyClients({ type: 'SYNC_SUCCESS', payload: { id: req.id } });} else {// 處理錯誤情況,例如保留請求并重試notifyClients({ type: 'SYNC_ERROR', payload: { id: req.id, error: 'Server error' } });}} catch (error) {// 網絡錯誤,保留請求,下次同步時重試notifyClients({ type: 'SYNC_ERROR', payload: { id: req.id, error: 'Network error' } });break; // 停止處理隊列,等待下一次sync事件}}
}async function notifyClients(message) {const clients = await self.clients.matchAll();for (const client of clients) {client.postMessage(message);}
}

3. 前端UI監聽Service Worker消息:

PwaNavBar或一個全局的UI服務中監聽消息。

// PwaNavBar.js setup()
navigator.serviceWorker.addEventListener('message', event => {const { type, payload } = event.data;if (type === 'SYNC_SUCCESS') {// 顯示一個短暫的“同步成功”通知this.env.services.notification.add("數據已同步至服務器。", { type: "success" });// 可能需要重新獲取數據以刷新UIthis.store.dispatch('nav/fetchBadges');} else if (type === 'SYNC_ERROR') {this.env.services.notification.add(`數據同步失敗: ${payload.error}`, { type: "danger" });}
});

6.3 挑戰與高級考量

  • 請求依賴性: 如果請求之間存在依賴關系(例如,必須先創建訂單,然后才能添加訂單行),簡單的順序執行可能不夠。需要在隊列中記錄依賴關系,或者設計更復雜的同步邏輯。
  • 數據沖突: 當多個客戶端或用戶同時修改同一份數據時,可能會發生沖突。需要設計沖突解決策略(例如,最后寫入者獲勝 Last-Write-Wins,或更復雜的三向合并算法)。
  • 認證與會話: Service Worker的生命周期獨立于頁面。需要確保Service Worker在執行后臺同步時,擁有有效的用戶認證令牌或會話信息來與Odoo后端通信。

通過實施這套完整的離線請求隊列和后臺同步機制,我們的Odoo PWA將具備強大的韌性,即使在最不穩定的網絡環境下,也能為用戶提供無縫、可靠的操作體驗,真正實現了漸進式網絡應用的承諾。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/90516.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/90516.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/90516.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Java泛型初始化ArrayList<String>()和ArrayList<>()的區別

文章目錄前言Java 泛型初始化&#xff1a;ArrayList<String>() vs ArrayList<>() 的區別1. 語法差異1.1 顯式泛型初始化 (ArrayList<String>())1.2 鉆石操作符初始化 (ArrayList<>())2. 編譯與運行時的區別3. 使用場景對比3.1 顯式泛型初始化的適用情況…

ubuntu25.04+4070+cuda+docker安裝

目錄 1.4070nvidia驅動安裝 2.CUDA安裝 3.docker安裝 4.docker的GPU支持 1.4070nvidia驅動安裝 首先從軟件源獲取最新的軟件包信息&#xff0c;然后升級一下安裝好的軟件包 #從軟件源獲取最新的軟件包信息 apt update -y #將已安裝的軟件包升級到最新版本 apt upgrade -y 然…

Mac m系列 VMware Fusion虛擬機安裝ARM contos

一、下載虛擬機 VMware Fusion和 CentOS 安裝 VMware Fusion下載地址下載好鏡像文件CentOS-Stream-9-20230516.0-aarch64-boot.iso下載地址 二、打開VMware Fusion新建虛擬機 選擇從光盤或映像中安裝點擊繼 選擇剛才下載的鏡像&#xff0c;點擊繼續選擇 Linux > 其他Linu…

MYSQL中NOT IN和NOT EXISTS

NOT IN 和 NOT EXISTS 是 MySQL 中用于排除某些數據的兩種常見查詢方式。它們的功能相似&#xff0c;都用于返回不滿足某一條件的結果&#xff0c;但是它們在內部的實現方式以及某些特定場景下的行為有所不同。1. NOT INNOT IN 是用來排除在指定值集合中存在的值。通常用來與子…

數據庫關系運算之連接

在數據庫理論中&#xff0c;關系連接&#xff08;Join&#xff09; 是將兩個或多個關系&#xff08;表&#xff09;中的元組&#xff08;行&#xff09;根據一定條件組合成新關系的操作&#xff0c;是關系型數據庫中核心且高頻使用的操作。其本質是通過共享的屬性&#xff08;列…

npm全局安裝后,依然不是內部或外部命令,也不是可運行的程序或批處理文件

雖然通過 npm install -g yarn 安裝了 Yarn&#xff0c;但系統無法識別 yarn 命令。這通常是因為 npm 的全局安裝目錄沒有添加到系統的 PATH 環境變量中C:\Users\Administrator>npm install -g yarnadded 1 package in 518msC:\Users\Administrator>yarn yarn 不是內部或…

C++ Proactor 與 Reactor 網絡編程模式

&#x1f9e0; C Proactor 與 Reactor 網絡編程模式&#x1f4cc; 核心區別概述特性Reactor 模式Proactor 模式事件驅動核心監聽 I/O 就緒事件 (可讀/可寫)監聽 I/O 完成事件 (讀完成/寫完成)I/O 執行者用戶線程 主動執行 I/O 操作操作系統 異步執行 I/O 操作控制流同步非阻塞 …

從手動操作到自動化:火語言 RPA 在多系統協作中的實踐

在企業日常運營中&#xff0c;很多業務流程需要在多個系統間來回切換&#xff1a;從 A 系統導出數據&#xff0c;到 B 系統校驗格式&#xff0c;再到 C 系統錄入信息…… 這些跨系統操作步驟繁瑣、邏輯固定&#xff0c;卻往往依賴人工完成&#xff0c;不僅效率低下&#xff0c;…

Spring Security 實踐之登錄

前言Spring Security是一個功能強大且高度且可定制的身份驗證和訪問控制框架&#xff0c;包含標準的身份認證和授權。 本文主要介紹SpringBoot中如何配置使用 Spring Security 安全認證框架并簡述相關原理和步驟。核心認證流程解析請求過濾 用戶提交登錄表單AbstractAuthentica…

華為云開發者空間 × DeepSeek-R1 智能融合測評:云端開發與AI客服的協同進化

前言&#xff1a; 華為開發者空間&#xff0c;是為全球開發者打造的專屬開發者空間&#xff0c;致力于為每位開發者提供一臺云主機、一套開發工具和云上存儲空間&#xff0c;當普惠云資源遇見推理大模型&#xff0c;企業服務與開發效能的范式革命正在加速。華為云開發者空間&am…

二分查找----4.搜索旋轉排序數組

題目鏈接 /** 升序數組在某個位置被分割為前后兩部分,前后兩部分整體互換;在被改變后的數組中找到目標值 O(log n)---> 二分查找 特點: 旋轉后的數組被分割為兩個獨立的遞增區間 左半區的最小值,大于右半區的最大值(mid所在區間的判斷依據) 二分策略: 首先判斷mid落在左區間…

地球表面附近兩點之間距離、高低角和方位角的計算方法,VC++代碼實操!

書接上文&#xff0c;這篇文章介紹具體的VC編程實現&#xff0c;代碼實操。任何一個算法&#xff0c;你必須將其編寫為代碼&#xff0c;運行結果正確&#xff0c;才算真正掌握了&#xff0c;否則都是似懂非懂&#xff0c;一知半解&#xff0c;下面先給出仿真結果的截圖&#xf…

uniapp各大平臺導航組件

最近有個需求要點擊導航然后跳出各家導航軟件話不多出直接貼出代碼&#xff1a;這個可以作為組件引入<template><view><view class"nav" :style"{color: customColor}" click.stop"openMap">{{title}}</view><!-- 彈…

Access開發一鍵刪除Excel指定工作表

Hi&#xff0c;大家好&#xff01;又到了每周給大家更新的時間了&#xff0c;這周給大家講講excel的處理操作吧。在開始前&#xff0c;先給大家匯報一下我們框架的進度&#xff0c;最近兩周沒有直播&#xff0c;所以大家不太清楚目前的進度&#xff0c;框架目前就差權限了&…

無廣告終端安全產品推薦:打造純凈辦公環境的安全之選

在數字化辦公時代&#xff0c;終端安全防護是企業和個人不可忽視的重要環節。然而&#xff0c;許多傳統安全軟件往往伴隨著頻繁的廣告彈窗和推廣信息&#xff0c;不僅干擾正常工作&#xff0c;還可能成為潛在的安全隱患。本文將為您介紹幾款「無廣告、無捆綁」的終端產品&#…

使用UE5自帶節點InteriorCubemap制作假室內效果

Interior Mapping&#xff08;室內映射&#xff09;是一種用著色器方法模擬室內結構紋理的方式&#xff0c;避免了真實對室內場景建模造成的模型面數渲染開銷&#xff0c;在《蜘蛛俠》《城市天際線》等游戲中都采用了該技術。 UE自帶了節點InteriorCubemap&#xff08;Unity S…

基于單片機睡眠質量/睡眠枕頭設計

傳送門 &#x1f449;&#x1f449;&#x1f449;&#x1f449;其他作品題目速選一覽表 &#x1f449;&#x1f449;&#x1f449;&#x1f449;其他作品題目功能速覽 概述 隨著現代社會生活節奏的加快&#xff0c;睡眠質量問題日益受到人們的關注。本研究設計了一種基于…

Ajax第一天

AJAX概念&#xff1a;AJAX 是瀏覽器與服務器進行數據通信的技術&#xff08;把數據變活&#xff09;語法&#xff1a;1.引入 axios.js&#xff1a;https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js2.使用 axios 函數? 傳入配置對象? 再用 .then 回調函數接收結果&#…

AI大模型各類概念掃盲

以下內容整理自AI&#xff0c;進行一個概念掃盲&#xff1a;Prompt&#xff08;提示詞&#xff09; Prompt是用戶提供給AI模型的指令或問題&#xff0c;用于引導模型生成特定輸出。良好的Prompt設計能顯著提升模型的任務理解能力和響應質量&#xff0c;例如通過結構化提示&…

Linux系統編程——網絡

一、TCP/UDP 1、osi模型 物理層、數據鏈路層、網絡層、傳輸層、會話層、表示層、應用層&#xff08;下層為上層提供服務&#xff09; 2、TCP/IP模型&#xff08;TCP/IP協議棧&#xff09; 應用層&#xff1a; HTTP&#xff08;超文本傳輸協議&#xff09;、FTP&#xff08;文件…