Odoo WebClient加载

507 阅读9分钟

Odoo WebClient 是 Odoo 系统前端的核心模块,负责管理页面加载、用户操作及视图的动态渲染。本教程将系统性地讲解 WebClient 的加载过程,分阶段详细解析每个技术环节,并提供实用的代码示例与技术说明,助力开发者全面掌握并复现 WebClient 的加载机制,满足对odoo前端的入门及其高级开发。

1. 加载流程概览

Odoo WebClient 的加载过程可分为以下五个阶段:

  1. 浏览器请求
    浏览器向后端发起 HTTP 请求,后端解析路由、验证会话并生成 HTML 响应。
  2. 模板渲染
    使用 QWeb 模板动态生成 HTML 页面,并注入会话信息和静态资源。
  3. JavaScript 加载
    前端加载 main.js 启动入口,初始化 WebClient 应用。
  4. WebClient 挂载
    使用 OWL 框架动态挂载 WebClient 组件,并根据路由加载菜单和视图。

2. 浏览器请求

浏览器通过 HTTP 请求访问 Odoo URL(例如 /web)。后端使用路由解析请求,验证会话状态,并生成 HTML 响应。

步骤 1: web路由

使用 @http.route 定义 WebClient 的主入口路由,该路由是我们访问Odoo的默认地址;函数主要功能是通通过odoo自带的模版Qweb(后端模版)渲染HTML页面:

@http.route(['/web', '/odoo', '/odoo/<path:subpath>', '/scoped_app/<path:subpath>'], type='http', auth="none", readonly=_web_client_readonly)
# 定义一个 HTTP 路由,匹配 /web, /odoo, /odoo/<path:subpath>, /scoped_app/<path:subpath> 的请求。
# `auth="none"` 表示该路由不要求用户登录认证。
# `readonly=_web_client_readonly` 是控制是否只允许读取操作。

def web_client(self, s_action=None, **kw):
    # 定义 web_client 函数,处理用户请求

    # 确保数据库已连接
    ensure_db()

    # 如果会话中没有用户 ID,重定向到登录页面
    if not request.session.uid:
        return request.redirect_query('/web/login', query={'redirect': request.httprequest.full_path}, code=303)

    # 如果 URL 中包含 redirect 参数,重定向到该 URL
    if kw.get('redirect'):
        return request.redirect(kw.get('redirect'), 303)

    # 检查会话是否有效(例如是否过期)
    if not security.check_session(request.session, request.env, request):
        raise http.SessionExpiredException("Session expired")

    # 如果当前用户不是内部用户,重定向到登录成功页面
    if not is_user_internal(request.session.uid):
        return request.redirect('/web/login_successful', 303)

    # 刷新会话的生命周期,延长会话有效期
    request.session.touch()

    # 恢复用户环境,之前因为 auth="none" 导致用户环境丢失
    request.update_env(user=request.session.uid)

    try:
        # 获取渲染 Web 客户端页面所需的上下文数据
        context = request.env['ir.http'].webclient_rendering_context()

        # 渲染 Web 客户端的 bootstrap 页面,传递上下文数据
        response = request.render('web.webclient_bootstrap', qcontext=context)

        # 设置响应头,防止页面被嵌入到其他网站的 iframe 中,提高安全性
        response.headers['X-Frame-Options'] = 'DENY'

        # 返回渲染的页面
        return response
    except AccessError:
        # 如果访问出错(如权限不足),重定向到登录页面并显示错误信息
        return request.redirect('/web/login?error=access')

技术详解

  1. HTTP 路由
    • 使用 @http.route 装饰器将 URL 映射到控制器方法。
    • 参数 type='http' 指定返回 HTML 响应。
    • 参数 auth="none" 表示不进行默认的认证,手动处理会话验证。
  1. 多租户架构
    • ensure_db() 检查请求数据库的合法性,确保支持多租户。
    • 通过捕获异常,处理数据库连接失败的情况,提供友好的错误页面。
  1. 会话管理
    • 通过 request.session.uid 验证用户是否已登录。
    • 调用 request.session.touch() 刷新会话有效期,防止会话过期。
    • 通过 request.update_env(user=request.session.uid) 更新请求环境,确保后续操作的用户上下文正确。
  1. 渲染上下文
    • 使用 webclient_rendering_context() 获取前端渲染所需的动态数据,包括用户信息、权限、菜单等。
  1. 安全性
    • 设置 X-Frame-Options 头为 DENY,防止页面被嵌入到其他网站,增强安全性。

图示:路由解析与会话验证


3. 模板渲染

Odoo 使用 QWeb 模板引擎渲染 HTML 页面。web.webclient_bootstrap 是 WebClient 的入口模板,定义了页面结构和初始化脚本。该模版渲染完成后加载静态资源文件,使得后续的webclient挂载到页面。

<template id="web.webclient_bootstrap">
        <t t-call="web.layout">
            <t t-set="head_web">
                <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"/>
                <meta name="theme-color" content="#71639e"/>
                <link rel="manifest" href="/web/manifest.webmanifest" crossorigin="use-credentials"/>
                <link rel="apple-touch-icon" href="/web/static/img/odoo-icon-ios.png"/>
                <script type="text/javascript">
                    // Block to avoid leaking variables in the script scope
                    {
                        odoo.__session_info__ = <t t-out="json.dumps(session_info)"/>;
                        const { user_context,  cache_hashes } = odoo.__session_info__;
                        const lang = new URLSearchParams(document.location.search).get("lang");
                        let menuURL = `/web/webclient/load_menus/${cache_hashes.load_menus}`;
                        if (lang) {
                            user_context.lang = lang;
                            menuURL += `?lang=${lang}`
                        }
                        odoo.reloadMenus = () => fetch(menuURL).then(res => res.json());
                        odoo.loadMenusPromise = odoo.reloadMenus();
                        // Prefetch translations to speedup webclient. This is done in JS because link rel="prefetch"
                        // is not yet supported on safari.
                        fetch(`/web/webclient/translations/${cache_hashes.translations}?lang=${user_context.lang}`);
                    }
                </script>
                <t t-call-assets="web.assets_web_print" media="print" t-js="false"/>

                <t t-if="request.cookies.get('color_scheme') == 'dark'">
                    <t t-call-assets="web.assets_web_dark" media="screen"/>
                </t>
                <t t-else="">
                    <t t-call-assets="web.assets_web" media="screen"/>
                </t>
                <t t-call="web.conditional_assets_tests" media="screen"/>
            </t>
            <t t-set="head" t-value="head_web + (head or '')" media="screen"/>
            <t t-set="body_classname" t-value="'o_web_client'"/>
        </t>
    </template>

技术详解

  1. QWeb 模板引擎
    • 采用 XML 语法定义模板,支持动态数据绑定和嵌套模板调用。
    • <t t-call> 用于调用其他模板(如 web.layout),实现模板复用。
  1. 动态数据注入
    • 使用 <t t-out> 将 Python 数据(如 session_info)转换为 JSON 格式,供前端 JavaScript 使用。
    • 通过 odoo.__session_info__ 全局变量注入会话信息,供前端脚本访问。
  1. 条件渲染
    • 使用 <t t-if> 指令,根据 user_context 中的 has_dashboard 属性决定是否加载额外的 JavaScript 文件 dashboard.js
    • 提升页面的灵活性和性能,避免不必要的资源加载。
  1. 静态资源加载
    • <t t-call-assets> 加载前端资源包 web.assets_web,包括 CSS 和 JavaScript 文件。
  1. 全局脚本初始化
    • 定义 odoo.__session_info__ 存储会话信息。
    • 提供全局函数 odoo.reloadMenus 动态加载菜单,增强用户体验。

4. JavaScript 加载

main.js 是 WebClient 的主入口脚本,负责启动前端应用并初始化组件。在main.js中,加载 startWebClient 方法,代码路径 web/static/src/main.js,该文件代码是核心入口

import { startWebClient } from "./start";
import { WebClient } from "./webclient/webclient";

/**
 * This file starts the webclient. It is in its own file to allow its replacement
 * in enterprise. The enterprise version of the file uses its own webclient import,
 * which is a subclass of the above Webclient.
 */

startWebClient(WebClient);

start.js 中实现 WebClient 的启动逻辑:

// static/src/js/start.js
import { mountComponent, whenReady } from "@odoo/owl";

/**
 * 启动 WebClient 应用
 * @param {Component} WebClientClass - WebClient 组件类
 */
export async function startWebClient(WebClientClass) {
    odoo.isReady = false;

    try {
        // 确保 DOM 加载完成
        await whenReady();

        // 挂载 WebClient 到页面
        const app = await mountComponent(WebClientClass, document.body, {
            name: "Odoo Web Client",
        });

        // 注入全局环境变量
        app.env.session = odoo.__session_info__;
        odoo.isReady = true;

        // 触发应用已准备就绪的事件
        app.env.bus.trigger("APP_READY");
    } catch (error) {
        console.error("Error starting WebClient:", error);
        // 显示错误页面或通知用户
        document.body.innerHTML = "<h1>加载应用失败,请稍后重试。</h1>";
    }
}

技术详解

  1. Odoo模块化
    • 使用 importexport 实现模块化,便于代码管理和复用。
    • 将启动逻辑和组件定义分离,提升代码可维护性。
  1. OWL 框架
    • mountComponent 是 OWL 提供的组件挂载方法,用于将组件渲染到指定的 DOM 元素。
    • whenReady 确保在 DOM 加载完成后启动应用,防止操作未加载完成的 DOM 元素。
  1. 全局状态管理
    • app.env.session 共享会话信息,供子组件访问,确保前端应用中的用户信息一致。
    • 通过 odoo.isReady 状态标识应用是否已准备就绪,便于在其他部分判断应用状态。
  1. 错误处理与优化建议
    • 使用 try-catch 捕获启动过程中的异常,确保应用在遇到错误时能够优雅地处理。
    • 在捕获到错误后,记录错误日志并显示用户友好的错误提示,提升用户体验。

错误处理与优化建议

在启动过程中可能会遇到各种错误,如网络问题、组件加载失败等。以下是一些错误处理和优化建议:

  1. 错误处理
    • startWebClient 函数中使用 try-catch 捕获启动过程中的异常。
    • 在捕获到错误后,记录详细的错误信息,便于调试和问题追踪。
    • 显示用户友好的错误提示,避免直接暴露技术细节。
  1. 性能优化
    • 懒加载:对于不在初始页面展示的模块,采用懒加载策略,按需加载,减少初始资源的体积。
    • 代码分割:将大型 JavaScript 文件拆分成更小的模块,提升加载速度。
    • 缓存策略:利用浏览器缓存机制,减少重复请求,提高资源加载效率。
    • 压缩与混淆:在生产环境中,对 JavaScript 和 CSS 文件进行压缩和混淆,减少文件大小,提高加载速度。

5. WebClient

WebClient 是 Odoo 前端的核心组件,负责加载菜单、视图和用户动作;在以前的odoo版本中也有类似的组件,在后续版本中用owl进行了替代。webclient/webclient.js 文件大体代码机构如下,其中包含了很多加载项,包含国际化,初始化第一个Action等。

// static/src/js/webclient/webclient.js
import { Component, onMounted } from "@odoo/owl";
import { useService, useBus } from "@odoo/owl";

export class WebClient extends Component {
    static template = "web.WebClient";
    static components = {
        ActionContainer,
        NavBar,
        MainComponentsContainer,
    };

    setup() {
        // 使用服务管理菜单和动作
        this.menuService = useService("menu");
        this.actionService = useService("action");

        // 路由监听,监听路由变化事件
        useBus(routerBus, "ROUTE_CHANGE", this.loadRouterState);

        // 生命周期钩子,在组件挂载后执行
        onMounted(() => {
            this.loadRouterState();
            this.env.bus.trigger("WEB_CLIENT_READY");
        });
    }

    /**
     * 加载路由状态,根据当前路由加载相应的菜单和视图
     */
    async loadRouterState() {
        try {
            const menuId = router.current.menu_id;
            if (menuId) {
                this.menuService.setCurrentMenu(menuId);
            }
            await this.actionService.loadState();
        } catch (error) {
            console.error("Error loading router state:", error);
            // 显示错误通知或跳转到错误页面
            this.env.bus.trigger("SHOW_ERROR", { message: "加载视图失败,请稍后重试。" });
        }
    }
}

技术详解

  1. OWL 框架
    • 组件化开发:通过定义组件类,利用 OWL 的组件系统实现模块化开发,提升代码可维护性。
    • 生命周期管理:使用 onMounted 生命周期钩子,在组件挂载后执行初始化逻辑。
    • 事件总线:利用 OWL 的事件总线 useBus,实现组件间的事件通信。
  1. 事件驱动设计
    • 路由变化监听:通过 useBus 监听 ROUTE_CHANGE 事件,当路由变化时,触发 loadRouterState 方法加载新的路由状态。
    • 全局事件触发:在组件挂载后触发 WEB_CLIENT_READY 事件,通知其他模块应用已准备就绪。
  1. 服务管理
    • useService:通过 useService 加载 menuServiceactionService,管理菜单和用户动作,提供统一的服务接口。
    • 菜单管理menuService.setCurrentMenu(menuId) 设置当前菜单,根据菜单 ID 加载相应的内容。
    • 动作管理actionService.loadState() 加载当前动作状态,确保视图和数据的正确加载。
  1. 生命周期钩子
    • onMounted:在组件挂载后执行 loadRouterState 方法,加载初始路由状态。
    • 错误处理:在 loadRouterState 方法中使用 try-catch 捕获加载过程中的异常,记录错误日志并显示用户友好的错误提示。

图示:组件挂载与事件流

sequenceDiagram
    participant WebClient
    participant MenuService
    participant ActionService
    participant RouterBus
    participant EventBus

    WebClient->>MenuService: setCurrentMenu(menuId)
    WebClient->>ActionService: loadState()
    RouterBus->>WebClient: ROUTE_CHANGE
    WebClient->>RouterBus: loadRouterState()
    WebClient->>EventBus: trigger("WEB_CLIENT_READY")
    EventBus-->>OtherModules: Notifies readiness

参考资料