Odoo WebClient 是 Odoo 系统前端的核心模块,负责管理页面加载、用户操作及视图的动态渲染。本教程将系统性地讲解 WebClient 的加载过程,分阶段详细解析每个技术环节,并提供实用的代码示例与技术说明,助力开发者全面掌握并复现 WebClient 的加载机制,满足对odoo前端的入门及其高级开发。
1. 加载流程概览
Odoo WebClient 的加载过程可分为以下五个阶段:
- 浏览器请求
浏览器向后端发起 HTTP 请求,后端解析路由、验证会话并生成 HTML 响应。 - 模板渲染
使用 QWeb 模板动态生成 HTML 页面,并注入会话信息和静态资源。 - JavaScript 加载
前端加载main.js启动入口,初始化 WebClient 应用。 - 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')
技术详解
- HTTP 路由:
-
- 使用
@http.route装饰器将 URL 映射到控制器方法。 - 参数
type='http'指定返回 HTML 响应。 - 参数
auth="none"表示不进行默认的认证,手动处理会话验证。
- 使用
- 多租户架构:
-
ensure_db()检查请求数据库的合法性,确保支持多租户。- 通过捕获异常,处理数据库连接失败的情况,提供友好的错误页面。
- 会话管理:
-
- 通过
request.session.uid验证用户是否已登录。 - 调用
request.session.touch()刷新会话有效期,防止会话过期。 - 通过
request.update_env(user=request.session.uid)更新请求环境,确保后续操作的用户上下文正确。
- 通过
- 渲染上下文:
-
- 使用
webclient_rendering_context()获取前端渲染所需的动态数据,包括用户信息、权限、菜单等。
- 使用
- 安全性:
-
- 设置
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>
技术详解
- QWeb 模板引擎:
-
- 采用 XML 语法定义模板,支持动态数据绑定和嵌套模板调用。
<t t-call>用于调用其他模板(如web.layout),实现模板复用。
- 动态数据注入:
-
- 使用
<t t-out>将 Python 数据(如session_info)转换为 JSON 格式,供前端 JavaScript 使用。 - 通过
odoo.__session_info__全局变量注入会话信息,供前端脚本访问。
- 使用
- 条件渲染:
-
- 使用
<t t-if>指令,根据user_context中的has_dashboard属性决定是否加载额外的 JavaScript 文件dashboard.js。 - 提升页面的灵活性和性能,避免不必要的资源加载。
- 使用
- 静态资源加载:
-
<t t-call-assets>加载前端资源包web.assets_web,包括 CSS 和 JavaScript 文件。
- 全局脚本初始化:
-
- 定义
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>";
}
}
技术详解
- Odoo模块化:
-
- 使用
import和export实现模块化,便于代码管理和复用。 - 将启动逻辑和组件定义分离,提升代码可维护性。
- 使用
- OWL 框架:
-
mountComponent是 OWL 提供的组件挂载方法,用于将组件渲染到指定的 DOM 元素。whenReady确保在 DOM 加载完成后启动应用,防止操作未加载完成的 DOM 元素。
- 全局状态管理:
-
app.env.session共享会话信息,供子组件访问,确保前端应用中的用户信息一致。- 通过
odoo.isReady状态标识应用是否已准备就绪,便于在其他部分判断应用状态。
- 错误处理与优化建议:
-
- 使用
try-catch捕获启动过程中的异常,确保应用在遇到错误时能够优雅地处理。 - 在捕获到错误后,记录错误日志并显示用户友好的错误提示,提升用户体验。
- 使用
错误处理与优化建议
在启动过程中可能会遇到各种错误,如网络问题、组件加载失败等。以下是一些错误处理和优化建议:
- 错误处理:
-
- 在
startWebClient函数中使用try-catch捕获启动过程中的异常。 - 在捕获到错误后,记录详细的错误信息,便于调试和问题追踪。
- 显示用户友好的错误提示,避免直接暴露技术细节。
- 在
- 性能优化:
-
- 懒加载:对于不在初始页面展示的模块,采用懒加载策略,按需加载,减少初始资源的体积。
- 代码分割:将大型 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: "加载视图失败,请稍后重试。" });
}
}
}
技术详解
- OWL 框架:
-
- 组件化开发:通过定义组件类,利用 OWL 的组件系统实现模块化开发,提升代码可维护性。
- 生命周期管理:使用
onMounted生命周期钩子,在组件挂载后执行初始化逻辑。 - 事件总线:利用 OWL 的事件总线
useBus,实现组件间的事件通信。
- 事件驱动设计:
-
- 路由变化监听:通过
useBus监听ROUTE_CHANGE事件,当路由变化时,触发loadRouterState方法加载新的路由状态。 - 全局事件触发:在组件挂载后触发
WEB_CLIENT_READY事件,通知其他模块应用已准备就绪。
- 路由变化监听:通过
- 服务管理:
-
- useService:通过
useService加载menuService和actionService,管理菜单和用户动作,提供统一的服务接口。 - 菜单管理:
menuService.setCurrentMenu(menuId)设置当前菜单,根据菜单 ID 加载相应的内容。 - 动作管理:
actionService.loadState()加载当前动作状态,确保视图和数据的正确加载。
- useService:通过
- 生命周期钩子:
-
- onMounted:在组件挂载后执行
loadRouterState方法,加载初始路由状态。 - 错误处理:在
loadRouterState方法中使用try-catch捕获加载过程中的异常,记录错误日志并显示用户友好的错误提示。
- onMounted:在组件挂载后执行
图示:组件挂载与事件流
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