本文介绍了odoo javascript框架。从代码行的角度来看,这个框架不是一个大的应用程序,但它是非常通用的,因为它基本上是一个将声明性接口描述转换为活动应用程序的机器,能够与数据库中的每个模型和记录交互。甚至可以使用Web客户端修改Web客户端的接口。
这里有一个有用的html版本的文档:Javascript API
这个Javascript框架主要设计用于三个地方使用:
单页应用
简而言之,webclient实例是整个用户界面的根组件。它的职责是协调所有的子组件,并提供服务,如RPC、本地存储等等。
在运行时,Web客户端是单页应用程序。每次用户执行操作时,它不需要从服务器请求整页。相反,它只请求它所需要的,然后替换/更新视图。此外,它还管理URL:它与Web客户机状态保持同步。
这意味着,当用户在处理odoo时,Web客户机类(和动作管理器)实际上创建并销毁了许多子组件。状态是高度动态的,每个小部件都可以随时销毁。
这里,我们在web/static/src/js插件中快速概述了web客户机代码。注意,这是故意不详尽的,我们只涉及最重要的文件/文件夹。
在Odoo中管理资源并不像在其他应用程序中那样简单。其中一个原因是,在其中一些情况中我们有各种各样的状态,但不是所有的资源都是必需的。例如,Web客户端、销售点、网站甚至移动应用程序的需求是不同的。此外,有些资源可能很大,但很少需要。在这种情况下,我们有时希望它们被懒惰地加载。
主要思想是我们用XML定义一组包。捆绑包在这里定义为一组文件(javascript、css、scss)。在odoo中,最重要的包在addons/web/views/webclient_templates.xml文件中定义。看起来是这样的:
<template id="web.assets_common" name="Common Assets (used in backend interface and website)">
<link rel="stylesheet" type="text/css" href="/web/static/lib/jquery.ui/jquery-ui.css"/>
...
<script type="text/javascript" src="/web/static/src/js/boot.js">script>
...
template>
然后,可以使用t-call-assets指令将捆绑包中的文件插入到模板中:
<t t-call-assets="web.assets_common" t-js="false"/>
<t t-call-assets="web.assets_common" t-css="false"/>
下面是当服务器使用以下指令呈现模板时发生的情况:
请注意,资源文件是缓存的,因此从理论上讲,浏览器应该只加载它们一次。
当odoo服务器启动时,它检查包中每个文件的时间戳,如果需要,它将创建/重新创建相应的包。
以下是大多数开发人员需要知道的一些重要包:
将位于addons/web中的文件添加到bundle的正确方法很简单:只需将脚本或样式表标记添加到文件webclient_templates.xml中的bundle即可。但是当我们使用不同的插件(addon)时,我们需要从该插件添加一个文件。在这种情况下,应分三步进行:
<template id="assets_backend" name="helpdesk assets" inherit_id="web.assets_backend">
<xpath expr="//script[last()]" position="after">
<link rel="stylesheet" type="text/scss" href="/helpdesk/static/src/scss/helpdesk.scss"/>
<script type="text/javascript" src="/helpdesk/static/src/js/helpdesk_dashboard.js">script>
xpath>
template>
请注意,当用户加载odoo web客户端时,包中的所有文件都会立即加载。这意味着每次通过网络传输文件(浏览器缓存处于活动状态时除外)。在某些情况下,最好使用Lazyload的一些资产。例如,如果一个小部件需要一个大的库,而这个小部件不是体验的核心部分,那么在实际创建小部件时,最好只加载库。widget类实际上已经为这个用例内置了支持。(查阅QWeb模板引擎部分)
文件可能无法正确加载有许多不同的原因。您可以尝试以下几点来解决此问题:
重新创建资源文件后,需要刷新页面,重新加载正确的文件(如果不起作用,文件可能被缓存了)。
一旦我们能够将我们的javascript文件加载到浏览器中,我们就需要确保以正确的顺序加载它们。为了实现这一点,odoo定义了一个小模块系统(位于addons/web/static/src/js/boot.js文件中,需要首先加载该文件)。
在AMD的启发下,odoo模块系统通过在全局odoo对象上定义函数define来工作。然后我们通过调用该函数来定义每个javascript模块。在odoo框架中,模块是一段将尽快执行的代码。它有一个名称,可能还有一些依赖项。当它的依赖项被加载时,模块也将被加载。模块的值就是定义模块的函数的返回值。
一个例子,看起来像这样:
// in file a.js
odoo.define('module.A', function (require) {
"use strict";
var A = ...;
return A;
});
// in file b.js
odoo.define('module.B', function (require) {
"use strict";
var A = require('module.A');
var B = ...; // something that involves A
return B;
});
定义模块的另一种方法是在第二个参数中明确地给出依赖项列表。
odoo.define('module.Something', ['module.A', 'module.B'], function (require) {
"use strict";
var A = require('module.A');
var B = require('module.B');
// some code
});
如果某些依赖项丢失/未就绪,那么模块将不会被加载。几秒钟后控制台中将出现警告。
请注意,不支持循环依赖项。这是有道理的,但这意味着需要谨慎。
odoo.define 方法给了三个参数:
模块可能需要在准备就绪之前执行一些工作。例如,它可以做一个RPC来加载一些数据。在这种情况下,模块只需返回一个deferred(promise)。在这种情况下,模块系统只需等待deferred完成,然后注册模块。
odoo.define('module.Something', ['web.ajax'], function (require) {
"use strict";
var ajax = require('web.ajax');
return ajax.rpc(...).then(function (result) {
// some code here
return something;
});
});
Odoo是在ECMAScript 6类可用之前开发的。在ECMAScript 5中,定义类的标准方法是定义一个函数并在其原型对象上添加方法。这很好,但是当我们想要使用继承、混合时,它稍微复杂一些。
出于这些原因,Odoo决定使用自己的类系统,这是受到约翰·雷西格的启发。基类位于web.class文件class.js中。
让我们讨论如何创建类。主要机制是使用extend方法(这或多或少相当于ES6类中的extend)。
var Class = require('web.Class');
var Animal = Class.extend({
init: function () {
this.x = 0;
this.hunger = 0;
},
move: function () {
this.x = this.x + 1;
this.hunger = this.hunger + 1;
},
eat: function () {
this.hunger = 0;
},
});
在本例中,init函数是构造函数。它将在创建实例时调用。通过使用new关键字创建实例。
可以方便地继承现有的类。这只需在超类上使用extend方法即可完成。当调用一个方法时,框架会秘密地将一个特殊的方法_super重新绑定到当前调用的方法中。这允许我们在需要调用父方法时使用this._super。
var Animal = require('web.Animal');
var Dog = Animal.extend({
move: function () {
this.bark();
this._super.apply(this, arguments);
},
bark: function () {
console.log('woof');
},
});
var dog = new Dog();
dog.move()
Odoo类系统不支持多重继承,但是对于那些需要共享某些行为的情况,我们有一个混合系统:extend方法实际上可以接受任意数量的参数,并将它们组合到新的类中。
var Animal = require('web.Animal');
var DanceMixin = {
dance: function () {
console.log('dancing...');
},
};
var Hamster = Animal.extend(DanceMixin, {
sleep: function () {
console.log('sleeping');
},
});
在这个例子中,Hamter 类是Animal的子类,但是它也混合了DanceMixin.。
修改现有的类
这并不常见,但有时我们需要在适当的位置修改另一个类。目标是有一个机制来改变一个类和所有未来/现在的实例。这是通过使用include方法完成的:
var Hamster = require('web.Hamster');
Hamster.include({
sleep: function () {
this._super.apply(this, arguments);
console.log('zzzz');
},
});
这显然是一个危险的操作,应该小心操作。但是,按照odoo的结构,有时需要在一个插件中修改在另一个插件中定义的widget/class的行为。请注意,它将修改类的所有实例,即使它们已经创建。
widget类实际上是用户界面的一个重要构建块。几乎用户界面中的所有内容都在小部件(widget)的控制之下。widget类在widget.js中的module web.widget中定义。
简而言之,widget类提供的特性包括:
var Widget = require('web.Widget');
var Counter = Widget.extend({
template: 'some.template',
events: {
'click button': '_onClick',
},
init: function (parent, value) {
this._super(parent);
this.count = value;
},
_onClick: function () {
this.count++;
this.$('.val').text(this.count);
},
});
对于本例,假设模板some.template(并且正确加载:模板位于一个文件中,该文件在模块清单中的qweb键中正确定义)如下:
<div t-name="some.template">
<span class="val"><t t-esc="widget.count"/></span>
<button>Increment</button>
</div>
这个例子说明了小部件类的一些特性,包括事件系统、模板系统、带有初始父参数的构造函数。
与许多组件系统一样,widget类有一个定义良好的生命周期。通常的生命周期如下:调用init,然后willStart,然后rendering,然后start,最后destroy。
Widget.init(parent)
这是构造函数。init方法应该初始化小部件的基本状态。它是同步的,可以被重写以从小部件的创建者/父对象获取更多参数。
Arguments :
parent(Widget())–新的widget的父级,用于处理自动销毁和事件传播。对于没有父级的小部件,可以为null。
Widget.willStart()
当一个小部件被创建并被附加到DOM的过程中,框架将调用这个方法一次。willstart方法是一个钩子,它应该返回一个deferred。JS框架将等待这个deferred完成,然后再继续渲染步骤。注意,此时小部件没有dom根元素。willstart钩子主要用于执行一些异步工作,例如从服务器获取数据。
[Rendering]()
此步骤由框架自动完成。框架会检查小部件上是否定义了template键。如果定义了,那么它将在呈现上下文中使用绑定到小部件的widget键呈现该模板(请参见上面的示例:我们在QWeb模板中使用widget.count来读取小部件的值)。如果没有定义模板,则读取 tagName 键并创建相应的DOM元素。渲染完成后,我们将结果设置为小部件的$el属性。在此之后,我们将自动绑定events和custom_events键中的所有事件。
Widget.start()
渲染完成后,框架将自动调用Start方法。这对于执行一些特殊的后期渲染工作很有用。例如,设置库。
必须返回deferred以指示其工作何时完成。
Returns: deferred 对象
Widget.destroy()
这始终是小部件生命周期中的最后一步。当小部件被销毁时,我们基本上执行所有必要的清理操作:从组件树中删除小部件,取消绑定所有事件,…
当小部件的父级被销毁时自动调用,如果小部件没有父级,或者如果它被删除但父级仍然存在,则必须显式调用。
请注意,不必调用willstart和start方法。可以创建一个小部件(将调用init方法),然后销毁(destroy方法),而不需要附加到DOM。如果是这种情况,将不会调用will start和start。
Widget.tagName
如果小部件没有定义模板,则使用。默认为DIV,将用作标记名来创建要设置为小部件的dom根的dom元素。可以使用以下属性进一步自定义生成的dom根目录:
Widget.id
用于在生成的dom根上生成id属性。请注意,这是很少需要的,如果一个小部件可以多次使用,这可能不是一个好主意。
Widget.className
用于在生成的dom根上生成class属性。请注意,它实际上可以包含多个css类:“some-class other-class”
Widget.attributes
属性名到属性值的映射(对象文本)。这些k:v对中的每一个都将被设置为生成的dom根上的dom属性。
Widget.el
将原始dom元素设置为小部件的根(仅在start lifecyle方法之后可用)
Widget.$el
jquery封装的el,(仅在Start Lifecyle方法之后可用)
Widget.template
应设置为QWeb模板的名称。如果设置了,模板将在小部件初始化之后但在其启动之前呈现。模板生成的根元素将被设置为小部件的dom根。
Widget.xmlDependencies
呈现小部件之前需要加载的XML文件的路径列表。这不会导致加载已加载的任何内容。如果您想延迟加载模板,或者想要在网站和Web客户机界面之间共享一个小部件,这很有用。
var EditorMenuBar = Widget.extend({
xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],
...
Widget.events
事件是事件选择器(由空格分隔的事件名称和可选CSS选择器)到回调的映射。回调可以是小部件方法或函数对象的名称。在这两种情况下,这都将设置为小部件:
'click p.oe_some_class a': 'some_method',
'change input': function (e) {
e.stopPropagation();
}
},
选择器用于jquery的事件委托,回调只对与选择器匹配的dom根的后代触发。如果选择器被省略(只指定了一个事件名),那么事件将直接设置在小部件的dom根上。
注意:不鼓励使用内联函数,将来可能会删除它。
Widget.custom_events
returns: true 如果小部件正在或者已经被销毁,否则false
Widget.$(selector)
将指定为参数的CSS选择器应用于小部件的dom根:
this.$(selector);
功能上与以下相同:
this.$el.find(selector);
arguments: selector(string)-CSS选择器
return:jQuery 对象
这个助手方法类似于Backbone.View.$
Widget.setElement(element)
将小部件的dom根重新设置为提供的元素,还处理重新设置dom根的各种别名以及取消设置和重新设置委托事件。
arguments: element(Element) -一个DOM元素或者jQuery对象设置为小部件的根DOM
Widget.appendTo(element)
渲染小部件并将它作为子元素插入到目标元素后面,使用.appentTo()
Widget.prependTo(element)
渲染小部件并将它作为子元素插入到目标元素前面,使用.prependTo()
Widget.insertAfter(element)
渲染小部件并将它作为目标元素的前一个同级插入,使用.insertAfter()
Widget.insertBefore(element)
渲染小部件并将其作为目标的后一个同级插入,使用.insertBefore()
所有这些方法都接受相应jquery方法接受的任何内容(css选择器、dom节点或jquery对象)。他们都会返回一个 deferred,并承担三个任务:
Web客户端使用QWeb模板引擎来呈现小部件(除非它们重写renderelement方法来执行其他操作)。QWebJS模板引擎基于XML,主要与Python实现兼容。
现在,让我们解释如何加载模板。每当Web客户端启动时,都会对/web/web client/qweb路由进行RPC。然后,服务器将返回在每个已安装模块的数据文件中定义的所有模板的列表。正确的文件列在每个模块清单的QWeb条目中。
在启动第一个小部件之前,Web客户机将等待加载该模板列表。
这个机制可以很好地满足我们的需求,但有时我们希望懒加载模板。例如,假设我们有一个很少使用的小部件。在这种情况下,我们可能不希望将其模板加载到主文件中,以便使Web客户机稍微轻一些。在这种情况下,我们可以使用小部件的xmlpendencies键:
var Widget = require('web.Widget');
var Counter = Widget.extend({
template: 'some.template',
xmlDependencies: ['/myaddon/path/to/my/file.xml'],
...
});
有了这个,计数器小部件将以willstart方法加载xmlpendencies文件,这样在执行呈现时模板就可以准备好了。
目前,odoo支持两个事件系统:一个允许添加侦听器和触发事件的简单系统,以及一个更完整的系统,它还可以使事件“冒泡”。
这两个事件系统都在文件mixins.js的eventspatchemixin中实现。这个mixin包含在widget类中。
这是历史上第一个事件系统。它实现了一个简单的总线模式。我们有4种主要方法:
var Widget = require('web.Widget');
var Counter = require('myModule.Counter');
var MyWidget = Widget.extend({
start: function () {
this.counter = new Counter(this);
this.counter.on('valuechange', this, this._onValueChange);
var def = this.counter.appendTo(this.$el);
return $.when(def, this._super.apply(this, arguments);
},
_onValueChange: function (val) {
// do something with val
},
});
// in Counter widget, we need to call the trigger method:
... this.trigger('valuechange', someValue);
{警告} 不鼓励使用此事件系统,我们计划用扩展事件系统中的trigger-up方法替换每个trigger方法。
自定义事件小部件是一个更高级的系统,它模拟DOM事件API。每当一个事件被触发时,它将“冒泡”组件树,直到它到达根小部件,或者停止。
var Widget = require('web.Widget');
var Counter = require('myModule.Counter');
var MyWidget = Widget.extend({
custom_events: {
valuechange: '_onValueChange'
},
start: function () {
this.counter = new Counter(this);
var def = this.counter.appendTo(this.$el);
return $.when(def, this._super.apply(this, arguments);
},
_onValueChange: function(event) {
// do something with event.data.val
},
});
// in Counter widget, we need to call the trigger_up method:
... this.trigger_up('valuechange', {value: someValue});
Odoo生态系统的一个常见需求是从外部扩展/更改基本系统的行为(通过安装应用程序,即不同的模块)。例如,可能需要在某些视图中添加新的小部件类型。在这种情况下,以及其他许多情况下,通常的过程是创建所需的组件,然后将其添加到注册表(注册步骤),以使Web客户机的其余部分知道它的存在。
一下是一些在系统中可用的注册:
var fieldRegistry = require('web.field_registry');
var FieldPad = ...;
fieldRegistry.add('pad', FieldPad);
注意,每个值都应该是AbstractField的子类
视图注册表:此注册表包含Web客户端已知的所有JS视图
(尤其是视图管理器)。此注册表的每个值都应该是AbstractView的子类
动作注册表:我们跟踪此注册表中的所有客户端动作。这个是动作管理器在需要创建客户端操作时查找的位置。在版本11中,每个值应该只是小部件的一个子类。但是,在版本12中,值必须是abstractAction。
有许多组件之间的通信方式
从父级到它的子级
一个简单的例子。父不见可以简单的调用子部件方法:
this.someWidget.update(someInfo);
从一个小部件到它的父/某个祖先
在这种情况下,小部件的工作只是通知其环境发生了什么事情。由于我们不希望小部件具有对其父部件的引用(这将使小部件与其父部件的实现相结合),因此继续操作的最佳方法通常是使用触发器trigger_up方法触发一个事件,该事件将冒泡到组件树中:
this.trigger_up(‘open_record’, { record: record, id: id});
此事件将在小部件上触发,然后将冒泡并最终被某些上游小部件捕获:
var SomeAncestor = Widget.extend({
custom_events: {
'open_record': '_onOpenRecord',
},
_onOpenRecord: function (event) {
var record = event.data.record;
var id = event.data.id;
// do something with the event.
},
});
// in WidgetA
var core = require('web.core');
var WidgetA = Widget.extend({
...
start: function () {
core.bus.on('barcode_scanned', this, this._onBarcodeScanned);
},
});
// in WidgetB
var WidgetB = Widget.extend({
...
someFunction: function (barcode) {
core.bus.trigger('barcode_scanned', barcode);
},
});
在本例中,我们使用web.core导出的总线,但这不是必需的。可以为特定目的创建总线。
在11.0版中,我们引入了服务的概念。主要的想法是给子组件一种受控制的方式来访问它们的环境,这种方式允许框架进行足够的控制,并且是可测试的。
服务系统围绕三个理念进行组织:services、service providers和widget。它的工作方式是小部件触发(使用trigger_up)事件,这些事件冒泡到服务提供者,服务提供者将要求服务执行任务,然后可能返回一个答案。
服务是AbstractService类的实例。它基本上只有一个名称和一些方法。它的工作是执行一些工作,通常是一些依赖于环境的工作。
例如,我们有Ajax服务(任务是执行RPC)、本地存储(与浏览器本地存储交互)和许多其他服务。
以下是有关如何实现Ajax服务的简化示例:
var AbstractService = require('web.AbstractService');
var AjaxService = AbstractService.extend({
name: 'ajax',
rpc: function (...) {
return ...;
},
});
这个服务被叫做‘ajax’而且定义了一个方法,rpc.
为了使服务正常工作,有必要让一个服务提供者准备好分派定制事件。在后端(Web客户端),这是由主Web客户端实例完成的。请注意,服务提供程序的代码来自ServiceProviderMin。
小部件是请求服务的部分。为了做到这一点,它只需触发一个事件调用服务(通常通过使用helper函数调用)。此事件将冒泡并将意图传达给系统的其余部分。
在实践中,有些函数被频繁地调用,以至于我们有一些助手函数使它们更容易使用。例如,_rpc方法是帮助生成rpc的助手。
var SomeWidget = Widget.extend({
_getActivityModelViewID: function (model) {
return this._rpc({
model: model,
method: 'get_activity_view_id'
});
},
});
{警告}
如果一个小部件被销毁,它将从主组件树中分离出来,并且没有父组件。在这种情况下,事件不会冒泡,这意味着工作不会完成。这通常正是我们从一个被破坏的小部件中想要的。
RPC功能由Ajax服务提供。但大多数人可能只会与_rpc助手进行交互。
在处理odoo时,通常有两个用例:一个需要在(python)模型上调用方法(这需要通过控制器call_kw),或者一个需要直接调用控制器(在某些路由上可用)。
return this._rpc({
model: 'some.model',
method: 'some_method',
args: [some, args],
});
return this._rpc({
route: '/some/route/',
params: { some: kwargs},
});
odoo框架有一种标准的方式来向用户传递各种信息:通知,它显示在用户界面的右上角。
通知有两种类型:
此外,通知还可以用于向用户询问问题,而不会干扰其工作流。想象一下,通过VoIP接收到的一个电话呼叫:可以显示一个带有两个按钮的粘性通知:接受和拒绝。
Odoo中的通知系统设计有以下组件:
显示通知的最常见方法是使用来自ServiceMixin的两种方法:
do_notify(title, message, sticky, className):
显示一个通知类型的通知:
do_warn(title, message, sticky, className):
显示一个警告类型的通知。
// note that we call _t on the text to make sure it is properly translated.
this.do_notify(_t("Success"), _t("Your signature request has been sent."));
this.do_warn(_t("Error"), _t("Filter name is required."));
Systray是界面菜单栏的右侧部分,Web客户端在其中显示一些小部件,如消息菜单。
当菜单创建SystrayMenu时,它将查找所有已注册的小部件,并将它们作为子小部件添加到适当的位置。
目前没有针对Systray小工具的特定API。它们应该是简单的小部件,并且可以像使用trigger-up方法的其他小部件一样与环境通信。
没有Systray注册表。添加小部件的正确方法是将其添加到类变量systraymenu.items中。
var SystrayMenu = require('web.SystrayMenu');
var MySystrayWidget = Widget.extend({
...
});
SystrayMenu.Items.push(MySystrayWidget);
在将小部件添加到自己之前,Systray菜单将按Sequence属性对项目进行排序。如果原型上不存在该属性,则将使用50。因此,要将Systray项目定位在靠后,可以设置一个非常高的序列号(反之,将其放在靠前的是一个较低的序列号)。
MySystrayWidget.prototype.sequence = 100;
有些翻译是在服务器端进行的(基本上是由服务器呈现或处理的所有文本字符串),但是静态文件中有需要翻译的字符串。它目前的工作方式如下:
请注意,在文档翻译模块中,从服务器的角度对翻译进行了更详细的解释。
javascript中的翻译有两个重要功能:_t和_lt。区别在于_lt是以懒惰的方式进行的。
var core = require('web.core');
var _t = core._t;
var _lt = core._lt;
var SomeWidget = Widget.extend({
exampleString: _lt('this should be translated'),
...
someMethod: function () {
var str = _t('some text');
...
},
});
在本例中,由于在加载模块时翻译尚未准备就绪,因此必须使用_lt。
注意,翻译功能需要注意。参数中给定的字符串不应是动态的。
Web客户端提供了一个特定的模块,其中包含一些特定于用户当前会话的信息。一些显著的键是:
加载/web路由后,服务器将在模板中插入一些会话信息和脚本标记。信息将从模型ir.http的方法session_info中读取。因此,如果要添加特定信息,可以通过重写session_info方法并将其添加到字典中来完成。
from odoo import models
from odoo.http import request
class IrHttp(models.AbstractModel):
_inherit = 'ir.http'
def session_info(self):
result = super(IrHttp, self).session_info()
result['some_key'] = get_some_value_from_db()
return result
现在,通过在会话中读取该值,可以在javascript中获取该值:
var session = require('web.session');
var myValue = session.some_key;
...
请注意,此机制旨在减少Web客户端准备就绪所需的通信量。它更适合于计算成本较低的数据(缓慢的session_info调用将延迟为每个人加载Web客户端),以及在初始化过程早期需要的数据。
“视图”一词有多种含义。本节是关于视图的JavaScript代码的设计,而不是Arch的结构或其他任何内容。
2017年,Odoo用新架构替换了先前的视图代码。主要需要将呈现逻辑与模型逻辑分开。
视图(一般意义上)现在用4个部分描述:视图、控制器、渲染器和模型。这4个部分的API在AbstractView、AbstractController、AbstractRenderer和AbstractModel类中进行了描述。
视图结构.png
视图的JS代码已设计为可在视图管理器/操作管理器的上下文之外使用。它们可以用于客户端操作,也可以显示在公共网站上(对资源进行一些处理)
该AbstractField类是在一个视图中的所有控件的基类,用于支持他们所有的视图(目前为:表格,列表,看板)。
v11字段小部件与先前版本之间存在许多差异。让我们提一下最重要的一些:
有效的装饰名字有:
每个装饰decoration-X将映射到css类text-X,这是一个标准的bootstrap css类(text-it和text-bf除外,它们由odoo处理并分别对应于斜体和粗体)。请注意,decoration属性的值应该是一个有效的python表达式,它将使用记录作为评估上下文进行评估。