服务

服务是提供某种功能的长期存活的代码片段。它们可以被组件(通过 useService)或其他服务导入,也可以声明一组依赖项。从这个意义上说,服务基本上就是一个 DI 依赖注入 系统。例如,notification 服务提供显示通知的方式,rpc 服务则是向 Odoo 服务器发起请求的正确方式。

下面的示例注册了一个每隔 5 秒显示一条通知的简单服务:

import { registry } from "@web/core/registry";

const myService = {
    dependencies: ["notification"],
    start(env, { notification }) {
        let counter = 1;
        setInterval(() => {
            notification.add(`Tick Tock ${counter++}`);
        }, 5000);
    }
};

registry.category("services").add("myService", myService);

启动时,网页客户端会启动 services 注册表中存在的所有服务。注意,注册表中使用的名称即为服务的名称。

Note

大多数不是组件的代码都应 打包 为服务,尤其是当它会产生某些副作用时。这对测试非常有用:测试可以选择哪些服务处于激活状态,从而减少不必要的副作用干扰被测代码的机会。

定义服务

服务需要实现以下接口:

dependencies

可选的字符串列表。它是该服务所需的全部依赖项(其他服务)的列表

start(env, deps)
参数
  • env (Environment()) – 应用环境

  • deps (Object()) – 所有请求的依赖项

返回

服务的值或 Promise<服务的值>

这是服务的主要定义。它可以返回一个值或一个 Promise。在后一种情况下,服务加载器只需等待该 Promise 解析为一个值,该值即为服务的值。

有些服务不导出任何值。它们可能只是执行自己的工作,而不需要被其他代码直接调用。在这种情况下,它们的值会在 env.services 中被设为 null。

async

可选值。如果提供,应为 true 或一个字符串列表。

有些服务需要提供异步 API。例如,rpc 服务是一个异步函数,或者 orm 服务提供一组调用 Odoo 服务器的函数。

在这种情况下,使用该服务的组件可能会在异步函数调用结束之前被销毁。大多数时候,异步函数调用应当被忽略。否则可能非常危险,因为底层组件已不再活动。async 标志正是为此而设:它向服务创建者发出信号,表示来自组件的所有异步调用在组件被销毁后应保持挂起。

使用服务

依赖其他服务并正确声明了 dependencies 的服务,只需在 start 方法的第二个参数中接收相应服务的引用即可。

useService 钩子是在组件中使用服务的正确方式。它简单地返回服务值的引用,组件随后可以使用该引用。例如:

import { useService } from "@web/core/utils/hooks";

class MyComponent extends Component {
  setup() {
    const rpc = useService("rpc");

    onWillStart(async () => {
      this.someValue = await rpc(...);
    });
  }
}

参考列表

技术名称

简介

cookie

读取或修改 cookie

effect

显示图形效果

http

执行低层级的 http 调用

notification

显示通知

router

管理浏览器 URL

rpc

向服务器发送请求

scroller

处理锚点元素上的点击

title

读取或修改窗口标题

user

提供与当前用户相关的一些信息

概述

  • 技术名称:cookie

  • 依赖项:无

提供操作 cookie 的方式。例如:

cookieService.setCookie("hello", "odoo");

API

current

表示各 cookie 及其值(如有)的对象(或空字符串)

setCookie(name[, value, ttl])
参数
  • name (string()) – 要设置的 cookie 的名称

  • value (any()) – 可选。如果提供,cookie 将被设为该值

  • ttl (number()) – 可选。cookie 被删除前的时间(秒)(默认=1 年)

将 cookie name 设为值 value,最大有效期为 ttl

deleteCookie(name)
参数
  • name (string()) – cookie 的名称

删除 cookie name。

Effect 服务

概述

  • 技术名称:effect

  • 依赖项:无

特效(effect)是可以临时显示在页面顶层的图形元素,通常用于向用户反馈发生了值得注意的事情。

一个典型的例子是彩虹人(rainbow man):

彩虹人特效

下面是显示它的方法:

const effectService = useService("effect");
effectService.add({
  type: "rainbow_man", // can be omitted, default type is already "rainbow_man"
  message: "Boom! Team record for the past 30 days.",
});

Warning

钩子 useEffect 与 effect 服务无关。

API

effectService.add(options)
参数
  • options (object()) – 特效的选项。它们会被传递给底层的特效组件。

显示一个特效。

选项定义如下:

interface EffectOptions {
  // The name of the desired effect
  type?: string;
  [paramName: string]: any;
}

可用特效

目前唯一的特效是彩虹人。

RainbowMan
effectService.add({ type: "rainbow_man" });

名称

类型

说明

params.Component

owl.Component?

要在 RainbowMan 中实例化的组件类(将替代消息)。

params.props

object?={}

如果给定了 params.Component,可通过此参数传入其 props。

params.message

string?="Well Done!"

message 是彩虹人显示的提示信息。

如果用户禁用了特效,彩虹人将不会出现,取而代之会显示一条简单通知作为回退。

如果启用了特效且给定了 params.Component,则不会使用 params.message。

message 是一个简单字符串或表示 html 的字符串(如果需要在 DOM 中进行交互,建议使用 params.Component)。

params.messageIsHtml

boolean?=false

如果 message 表示 html,则设为 true,以便将其正确插入 DOM。

params.img_url

string?=/web/static/img/smile.svg

在彩虹中显示的图片的 URL。

params.fadeout

("slow"|"medium"|"fast"|"no")?="medium"

彩虹人消失前的延迟时间。

"fast" 会让彩虹人快速消失。

"medium" 和 "slow" 会在消失前等待稍长一点时间(当 params.message 较长时可以使用)。

"no" 会让彩虹人一直停留在屏幕上,直到用户点击彩虹人之外的任意位置。

如何添加特效

特效存储在一个名为 effects 的注册表中。您可以通过提供一个名称和一个函数来添加新的特效。

const effectRegistry = registry.category("effects");
effectRegistry.add("rainbow_man", rainbowManEffectFunction);

该函数必须遵循以下 API:

<newEffectFunction>(env, params)
参数
  • env (Env()) – 服务接收到的环境(environment)

  • params (object()) – 从服务的 add 函数接收到的参数。

返回

({Component, props} | void) 一个组件及其 props,或不返回任何内容。

该函数必须创建一个组件并将其返回。该组件会被挂载到特效组件容器内。

示例

假设我们想添加一个为页面添加棕褐色(sepia)效果的特效。

import { registry } from "@web/core/registry";
import { Component, xml } from "@odoo/owl";

class SepiaEffect extends Component {
  static template = xml`
    <div style="
        position: absolute;
        left: 0;
        top: 0;
        width: 100%;
        height: 100%;
        pointer-events: none;
        background: rgba(124,87,0, 0.4);
    "></div>
  `;
}

export function sepiaEffectProvider(env, params = {}) {
    return {
        Component: SepiaEffect,
    };
}

const effectRegistry = registry.category("effects");
effectRegistry.add("sepia", sepiaEffectProvider);

然后,在您想要的地方调用它,就能看到结果。本例中它在 webclient.js 中被调用,以便示例效果在各处可见。

const effectService = useService("effect");
effectService.add({ type: "sepia" });
棕褐色调的 Odoo

Http 服务

概述

  • 技术名称:http

  • 依赖项:无

虽然 odoo 中客户端与服务器之间的大多数交互都是 RPCs (XMLHTTPRequest),但有时可能需要对请求进行更底层的控制。

该服务提供发送 get 和 post http requests 请求的方式。

API

async get(route[, readMethod = "json"])
参数
  • route (string()) – 请求发送到的 URL

  • readMethod (string()) – 响应内容类型。可以为 “text”、”json”、”formData”、”blob”、”arrayBuffer”。

返回

请求的结果,格式由 readMethod 参数定义。

发送 get 请求。

async post(route[, params = {}, readMethod = "json"])
参数
  • route (string()) – 请求发送到的 URL

  • params (object()) – 要设置到请求表单数据(form data)部分的键值对数据

  • readMethod (string()) – 响应内容类型。可以为 “text”、”json”、”formData”、”blob”、”arrayBuffer”。

返回

请求的结果,格式由 readMethod 参数定义。

发送 post 请求。

示例

const httpService = useService("http");
const data = await httpService.get("https://something.com/posts/1");
// ...
await httpService.post("https://something.com/posts/1", { title: "new title", content: "new content" });

Notification 服务

概述

  • 技术名称:notification

  • 依赖项:无

notification 服务允许在屏幕上显示通知。

const notificationService = useService("notification");
notificationService.add("I'm a very simple notification");

API

add(message[, options])
参数
  • message (string()) – 要显示的通知消息

  • options (object()) – 通知的选项

返回

用于关闭通知的函数

显示一条通知。

选项定义如下:

名称

类型

说明

title

string

为通知添加标题

type

warning | danger | success | info

根据类型更改背景色

sticky

boolean

通知是否应一直保留直到被关闭

className

string

将添加到通知上的额外 css 类

onClose

函数

通知关闭时执行的回调

buttons

button[](见下文)

要在通知中显示的按钮列表

autocloseDelay

number

通知自动关闭前的时长(毫秒)

按钮定义如下:

名称

类型

说明

name

string

按钮文本

onClick

函数

点击按钮时执行的回调

primary

boolean

按钮是否应设置为主要按钮样式

示例

一条销售成交时的通知,带有跳转至佣金页面的按钮。

// in setup
this.notificationService = useService("notification");
this.actionService = useService("action");

// later
this.notificationService.add("You closed a deal!", {
  title: "Congrats",
  type: "success",
  buttons: [
      {
          name: "See your Commission",
          onClick: () => {
              this.actionService.doAction("commission_action");
          },
      },
  ],
});
通知示例

一秒后自动关闭的通知:

const notificationService = useService("notification");
const close = notificationService.add("I will be quickly closed");
setTimeout(close, 1000);

路由服务

概述

  • 技术名称:router

  • 依赖项:无

router 服务提供三项功能:

  • 当前路由的信息

  • 应用根据自身状态更新 URL 的方式

  • 监听每一次哈希变化,并通知应用的其他部分

API

current

可以通过 current 键访问当前路由。它是一个包含以下信息的对象:

  • pathname (string):当前地址的路径(很可能是 /web )

  • search (object):一个字典,将 URL 中的每个搜索关键字(查询字符串)映射到其对应的值。如果未明确给出值,则为空字符串

  • hash (object):与上面相同,但针对的是哈希中描述的值。

例如:

// url = /web?debug=assets#action=123&owl&menu_id=174
const { pathname, search, hash } = env.services.router.current;
console.log(pathname); //   /web
console.log(search); //   { debug="assets" }
console.log(hash); //   { action:123, owl: "", menu_id: 174 }

更新 URL 可通过 pushState 方法完成:

pushState(hash: object[, replace?: boolean])
参数
  • hash (Object()) – 包含若干键到值映射的对象

  • replace (boolean()) – 若为 true,则替换 URL;否则仅更新 hash 中的键/值对。

使用 hash 对象中的每个键/值对更新 URL。如果某个值被设置为空字符串,则该键会以不带对应值的形式添加到 URL 中。

若为 true,replace 参数会告知路由器应完全替换 URL 哈希(因此 hash 对象中不存在的值将被移除)。

此方法调用不会重新加载页面,也不会触发 hashchange 事件,也不会在 main bus 中触发 ROUTE_CHANGE。这是因为该方法的目的仅在于更新 URL。调用此方法的代码有责任确保屏幕也得到相应更新。

例如:

// url = /web#action_id=123
routerService.pushState({ menu_id: 321 });
// url is now /web#action_id=123&menu_id=321
routerService.pushState({ yipyip: "" }, replace: true);
// url is now /web#yipyip

最后,redirect 方法会将浏览器重定向到指定的 URL:

redirect(url[, wait])
参数
  • url (string()) – 一个有效的 URL

  • wait (boolean()) – 若为 true,则等待服务器就绪后再重定向

将浏览器重定向到 url。该方法会重新加载页面。wait 参数很少使用:它在某些情况下很有用,比如我们已知服务器将在短时间内不可用,通常是在插件更新或安装操作之后。

Note

每当当前路由发生变化时,路由器服务就会在 main bus 上发出 ROUTE_CHANGE 事件。

RPC 服务

概述

  • 技术名称:rpc

  • 依赖项:无

rpc 服务提供一个异步函数,用于向服务器发送请求。调用控制器非常简单:路由应作为第一个参数,也可以将 params 对象作为第二个参数传入。

// in setup
this.rpc = useService("rpc");

// somewhere else, in an async function:
const result = await this.rpc("/my/route", { some: "value" });

Note

请注意,rpc 服务被视为底层服务。它只应用于与 Odoo 控制器交互。若要与模型打交道(这是迄今最重要的用例),应改用 orm 服务。

API

rpc(route, params, settings)
参数
  • route (string()) – 请求所指向的路由

  • params (Object()) – (可选)发送到服务器的参数

  • settings (Object()) – (可选)请求设置(见下文)

settings 对象可以包含:

  • xhr,应为 XMLHTTPRequest 对象。在这种情况下,rpc 方法将直接使用它,而不是创建一个新的。这在需要访问 XMLHTTPRequest API 的高级功能时很有用。

  • silent (boolean) 如果设置为 true,Web 客户端将不会提示存在待处理的 rpc。

rpc 服务通过 XMLHTTPRequest 对象与服务器通信,该对象配置为使用 application/json 内容类型。因此,请求的内容显然必须可进行 JSON 序列化。该服务发出的每个请求都使用 POST HTTP 方法。

服务器错误实际上会以 HTTP 代码 200 返回响应。但 rpc 服务会将其视为错误。

错误处理

rpc 失败主要有两个原因:

  • 要么是 Odoo 服务器返回了错误(我们称之为 server 错误)。在这种情况下,HTTP 请求会以 HTTP 代码 200 返回,但响应对象中包含 error 键。

  • 要么是某种其他类型的网络错误

当 rpc 失败时:

  • 代表该 rpc 的 Promise 被拒绝,因此调用代码会崩溃,除非它处理了这种情况

  • 主应用总线上会触发 RPC_ERROR 事件。事件负载包含对错误原因的描述:

    如果是服务器错误(服务器代码抛出了异常)。在这种情况下,事件负载将是一个包含以下键的对象:

    • type = 'server'

    • message(string)

    • code(number)

    • name(string) 可选,错误服务在处理错误时用它查找合适的对话框)

    • subType(string) 可选,常用于确定对话框标题)

    • data(object) 可选对象,可包含各种键,其中 debug 是主要的调试信息,包含调用堆栈)

    如果是网络错误,则错误描述只是对象 {type: 'network'}。当发生网络错误时,会显示一条 notification,并定期联系服务器直到其响应。服务器一旦响应,该通知就会关闭。

Scroller 服务

概述

  • 技术名称:scroller

  • 依赖项:无

每当用户在 Web 客户端中点击锚点时,该服务都会自动滚动到目标位置(如果合适)。

该服务添加了一个事件监听器来获取文档上的 click 事件。服务会检查其 href 属性中包含的选择器是否有效,以区分锚点和 Odoo 动作(例如 <a href="#target_element"></a>)。如果不是这种情况,则不执行任何操作。

如果点击似乎指向某个元素,主应用总线上会触发 SCROLLER:ANCHOR_LINK_CLICKED 事件。该事件包含一个自定义事件,其中包含匹配的 element 及其 id 作为引用。这可以让其他部分自行处理与锚点相关的行为。原始事件也会一并给出,因为可能需要阻止它。如果该事件未被阻止,用户界面将滚动到目标元素。

API

上面解释的 anchor-link-clicked 自定义事件包含以下值。

名称

类型

说明

element

HTMLElement | null

href 所指向的锚点元素

id

string

href 中包含的 id

originalEv

Event

原始的点击事件

Note

Scroller 服务会在 main bus 上发出 SCROLLER:ANCHOR_LINK_CLICKED 事件。若要避免 Scroller 服务的默认滚动行为,必须对传递给监听器的事件调用 preventDefault(),这样才能在监听器中正确实现您自己的行为。

Title 服务

概述

  • 技术名称:title

  • 依赖项:无

title 服务提供了一个简单的 API,可用于读取/修改文档标题。例如,如果当前文档标题为 “Odoo”,我们可以使用以下命令将其更改为 “Odoo 15 - Apple”:

// in some component setup method
const titleService = useService("title");

titleService.setParts({ odoo: "Odoo 15", fruit: "Apple" });

API

title 服务操作以下接口:

interface Parts {
    [key: string]: string | null;
}

每个键代表标题某一部分的标识,每个值是显示的字符串,如果已被移除则为 null。

其 API 为:

current

这是一个表示当前标题的字符串。它的结构如下:value_1 - ... - value_n,其中每个 value_i 是 Parts 对象(由 getParts 函数返回)中找到的一个(非空)值

getParts()
返回

Parts 由 title 服务维护的当前 Parts 对象

setParts(parts)
参数
  • parts (Parts()) – 表示所需更改的对象

setParts 方法允许添加/替换/删除标题的多个部分。删除某个部分(值)的方法是将对应的键值设置为 null。

请注意,可以只修改单个部分而不影响其他部分。例如,如果标题由以下部分组成:

{ odoo: "Odoo", action: "Import" }

且 current 值为 Odoo - Import,那么

setParts({
  action: null,
});

会将标题更改为 Odoo。

User 服务

概述

  • 技术名称:user

  • 依赖:rpc

user 服务提供与当前登录用户相关的一批数据和若干辅助函数。

API

名称

类型

说明

context

Object

user context

db

Object

数据库相关信息

home_action_id

(number | false)

作为用户主页使用的动作的 ID

isAdmin

boolean

用户是否为管理员(属于 base.group_erp_manager 群组或超级用户)

isSystem

boolean

用户是否属于系统群组(base.group_system)

lang

string

所使用的语言

name

string

用户名称

partnerId

number

用户对应业务伙伴记录的 ID

tz

string

用户的时区

userId

number

用户的 ID

userName

string

用户的备用昵称

updateContext(update)
参数
  • update (object()) – 用于更新上下文的对象

使用给定的对象更新 user context

userService.updateContext({ isFriend: true })
removeFromContext(key)
参数
  • key (string()) – 目标属性的键

从 user context 中移除给定键的值

userService.removeFromContext("isFriend")
hasGroup(group)
参数
  • group (string()) – 要查找的群组的 xml_id

返回

Promise<boolean> 用户是否在该群组中

检查用户是否属于某个群组

const isInSalesGroup = await userService.hasGroup("sale.group_sales")