构建自定义小部件 - 邮箱小部件#
本教程展示了如何使用TypeScript小部件cookiecutter构建一个简单的电子邮件小部件:https://github.com/jupyter-widgets/widget-ts-cookiecutter

设置开发环境#
使用 miniconda 安装 conda#
我们推荐使用 miniconda 安装 conda。
说明可在conda安装文档中找到。
创建新的conda环境并附带依赖项#
接下来创建一个包含以下内容的conda环境:
JupyterLab的最新版本或经典笔记本
cookiecutter: 你将用来引导自定义小部件的工具
NodeJS: 你将要使用的JavaScript运行时,用于编译自定义部件的Web资源(例如:TypeScript、CSS)
要创建环境,请执行以下命令:
conda create -n ipyemail -c conda-forge jupyterlab cookiecutter nodejs yarn python
然后使用以下命令激活环境:
conda activate ipyemail
创建新项目#
构建并安装该微件用于开发#
生成的项目应该已经包含一个README.md文件,其中包含本地开发小部件的说明。
由于该小部件包含Python部分,需要以可编辑模式安装该包:
python -m pip install -e .
您还需要启用小部件前端扩展。
如果您正在使用 JupyterLab(版本3.x或更高):
# link your development version of the extension with JupyterLab
jupyter labextension develop . --overwrite
# rebuild extension TypeScript source after making changes
yarn build
也可以使用watch脚本在有新更改时自动重新构建小部件:
# watch the source directory in one terminal, automatically rebuilding when needed
yarn watch
如果您正在使用经典版的Notebook:
jupyter nbextension install --sys-prefix --symlink --overwrite --py ipyemail
jupyter nbextension enable --sys-prefix --py ipyemail
测试安装#
此时,你应该能够打开一个笔记本并创建一个新的ExampleWidget。
要测试它,请在终端中执行以下命令:
# if you are using the classic notebook
jupyter notebook
# if you are using JupyterLab
jupyter lab
并打开 examples/introduction.ipynb。
默认情况下,该组件显示字符串Hello World,并带有彩色背景:

接下来的步骤将引导您如何修改现有代码,将小部件转换为电子邮件小部件。
实现该widget#
该组件框架构建于通信框架(简称Comm)之上。通信框架是一种允许内核与前端发送/接收JSON消息的框架(如下所示)。

要了解更多关于底层Widget协议的工作原理,请查看Low Level Widget文档。
要创建自定义小部件,您需要在浏览器和 Python 内核中定义该小部件。
Python 内核#
DOMWidget、ValueWidget与Widget#
要定义一个小部件,你必须继承自 DOMWidget、ValueWidget 或 Widget 基类。如果你希望你的小部件能够显示,你将需要继承自 DOMWidget。如果你希望你的小部件用作 interact 的输入,你将需要继承自 ValueWidget。如果你的小部件具有单一明确输出(例如,一个 IntSlider 的输出显然是滑块的当前值),那么你的小部件应该继承自 ValueWidget。
DOMWidget 和 ValueWidget 类均继承自 Widget 类。当 widget 无需直接在 notebook 中展示,而是作为另一种渲染环境的子元素时,Widget 类就非常有用。以下是一些示例:
如果您想要创建一个three.js小部件(three.js是一个流行的WebGL库),您可以将渲染窗口实现为
DOMWidget,并将任何在该窗口中渲染的3D对象或光源实现为Widget如果你想创建一个直接在笔记本中显示的小部件以用于
interact(比如IntSlider),你应该同时从DOMWidget和ValueWidget进行多重继承。如果你想创建一个提供给
interact但不需显示的小部件,你应该仅继承ValueWidget
视图名称#
继承自DOMWidget并不会告诉小部件框架你的后端小部件要关联哪个前端小部件。
相反,您必须通过定义特殊命名的特性属性来自行告知它,_view_name、_view_module 和 _view_module_version(如下所示),以及可选的 _model_name 和 _model_module。
首先将ipyemail/example.py重命名为ipyemail/widget.py。
在ipyemail/widget.py中,将示例代码替换为以下内容:
from ipywidgets import DOMWidget, ValueWidget, register
from traitlets import Unicode, Bool, validate, TraitError
from ._frontend import module_name, module_version
@register
class Email(DOMWidget, ValueWidget):
_model_name = Unicode('EmailModel').tag(sync=True)
_model_module = Unicode(module_name).tag(sync=True)
_model_module_version = Unicode(module_version).tag(sync=True)
_view_name = Unicode('EmailView').tag(sync=True)
_view_module = Unicode(module_name).tag(sync=True)
_view_module_version = Unicode(module_version).tag(sync=True)
value = Unicode('example@example.com').tag(sync=True)
在 ipyemail/__init__.py 中,将导入从:
from .example import ExampleWidget
致:
from .widget import Email
sync=True 特征属性#
Traitlets 是一个 IPython 库,用于在可配置对象上定义类型安全的属性。在本教程中,您无需担心 traitlets 机制中的可配置部分。sync=True 关键字参数告诉小部件框架处理将该值同步到浏览器的操作。如果没有 sync=True,小部件的属性将不会与前端的属性同步。
请记住,可变类型在修改时不一定会同步。例如,向一个list添加元素不会导致更改同步。相反,必须创建一个新列表并将其分配给特性,以使更改同步。
另一种选择是使用诸如spectate这样的第三方库,它跟踪可变数据类型的更改。
其他 traitlet 类型#
Unicode, 用于_view_name, 并不是唯一的Traitlet类型, 还有更多一些类型在下面列出:
任意
布尔
字节
CBool
CBytes
C复杂类型
浮点数
整数型转换
CLong
CRegExp
统一码
大小写无差别的字符串枚举
复杂
字典
带点对象名称
枚举
浮点数
函数类型
实例
实例类型
整型
列表
长整型
设置
TCP地址
元组
类型
统一码
并集
并非所有这些特质都可以通过网络同步,仅支持JSON格式的特质和小部件实例会被同步。
前端 (TypeScript)#
模型与视图#
IPython 小部件框架前端严重依赖 Backbone.js。Backbone.js 是一个 MVC(模型视图控制器)框架。在后端定义的小部件会自动与前端中的 Backbone.js Model 同步。每个前端 Model 处理小部件数据和状态,并可以有任意数量的关联 View。在小部件的上下文中,Views 是渲染对象以供用户交互的部分,而 Model 则处理与 Python 对象的通信。
在第一次从python推送状态时,同步的traitlets会自动添加。您之前定义的_view_name特征被widget框架用于创建相应的Backbone.js视图并将该视图链接到模型。
TypeScript cookiecutter 生成一个文件 src/widget.ts。打开该文件并将 ExampleModel 重命名为 EmailModel,将 ExampleView 重命名为 EmailView:
export class EmailModel extends DOMWidgetModel {
defaults() {
return {...super.defaults(),
_model_name: EmailModel.model_name,
_model_module: EmailModel.model_module,
_model_module_version: EmailModel.model_module_version,
_view_name: EmailModel.view_name,
_view_module: EmailModel.view_module,
_view_module_version: EmailModel.view_module_version,
value : 'Hello World'
};
}
static serializers: ISerializers = {
...DOMWidgetModel.serializers,
// Add any extra serializers here
}
static model_name = 'EmailModel';
static model_module = MODULE_NAME;
static model_module_version = MODULE_VERSION;
static view_name = 'EmailView';
static view_module = MODULE_NAME;
static view_module_version = MODULE_VERSION;
}
export class EmailView extends DOMWidgetView {
render() {
this.el.classList.add('custom-widget');
this.value_changed();
this.model.on('change:value', this.value_changed, this);
}
value_changed() {
this.el.textContent = this.model.get('value');
}
}
渲染方法#
现在,重写视图的基础render方法来自定义渲染逻辑。
可以通过this.el获取控件默认DOM元素的句柄。el属性是与视图关联的DOM元素。
在 src/widget.ts 中,定义 _emailInput 属性:
export class EmailView extends DOMWidgetView {
private _emailInput: HTMLInputElement;
render() {
// .....
}
// .....
}
然后,为render方法添加以下逻辑:
render() {
this._emailInput = document.createElement('input');
this._emailInput.type = 'email';
this._emailInput.value = 'example@example.com';
this._emailInput.disabled = true;
this.el.appendChild(this._emailInput);
this.el.classList.add('custom-widget');
this.value_changed();
this.model.on('change:value', this.value_changed, this);
},
测试#
首先,运行以下命令重新构建前端包:
npm run build
如果您使用 JupyterLab,您可能希望使用 jlpm 作为 npm 客户端。jlpm 在底层使用 yarn 作为包管理器。与 npm 的主要区别是,jlpm 会生成一个 yarn.lock 文件用于依赖管理,而不是 package-lock.json。使用 jlpm 时的命令是:
jlpm build
重新加载页面后,您现在应该能够像显示任何其他小部件一样显示您的小部件:
from ipyemail import Email
Email()
使小部件具备状态#
上述例子中,你能做到的与IPython显示框架能实现的几乎没有差别。为了改变这一点,你将使该widget具有状态。它将显示一个由后端设置的地址,而不是一个静态的“example@example.com”邮箱地址。首先,你需要在后端添加一个traitlet。使用value的名称,以便与widget框架的其他部分保持一致,并允许你的widget与interact一起使用。
我们希望能够避免用户输入无效的电子邮件地址,因此需要使用traitlets创建一个验证器。
from ipywidgets import DOMWidget, ValueWidget, register
from traitlets import Unicode, Bool, validate, TraitError
from ._frontend import module_name, module_version
@register
class Email(DOMWidget, ValueWidget):
_model_name = Unicode('EmailModel').tag(sync=True)
_model_module = Unicode(module_name).tag(sync=True)
_model_module_version = Unicode(module_version).tag(sync=True)
_view_name = Unicode('EmailView').tag(sync=True)
_view_module = Unicode(module_name).tag(sync=True)
_view_module_version = Unicode(module_version).tag(sync=True)
value = Unicode('example@example.com').tag(sync=True)
disabled = Bool(False, help="Enable or disable user changes.").tag(sync=True)
# Basic validator for the email value
@validate('value')
def _valid_value(self, proposal):
if proposal['value'].count("@") != 1:
raise TraitError('Invalid email value: it must contain an "@" character')
if proposal['value'].count(".") == 0:
raise TraitError('Invalid email value: it must contain at least one "." character')
return proposal['value']
从视图访问模型#
要访问与视图实例关联的模型,请使用视图的 model 属性。get 和 set 方法用于与 Backbone 模型交互。get 很简单,但在使用 set 时需小心。调用模型的 set 后,需要调用视图的 touch 方法。这将 set 操作与特定视图关联,以便输出被路由到正确的单元格。模型还有一个 on 方法,允许您监听模型触发的事件(如值更改)。
渲染模型内容#
通过将字符串字面量替换为对model.get的调用,视图现在将在显示时展示后端的值。然而,当值发生变化时,它不会自动更新为新值。
export class EmailView extends DOMWidgetView {
render() {
this._emailInput = document.createElement('input');
this._emailInput.type = 'email';
this._emailInput.value = this.model.get('value');
this._emailInput.disabled = this.model.get('disabled');
this.el.appendChild(this._emailInput);
}
private _emailInput: HTMLInputElement;
}
动态更新#
为了让视图动态更新自身,当模型的value属性改变时,注册一个函数来更新视图的值。这可以通过使用model.on方法完成。on方法接受三个参数:事件名称、回调句柄和回调上下文。每当模型发生改变时,名为change的Backbone事件将触发。通过在其后追加:value,你告诉Backbone只监听value属性的变化事件(如下所示)。
export class EmailView extends DOMWidgetView {
render() {
this._emailInput = document.createElement('input');
this._emailInput.type = 'email';
this._emailInput.value = this.model.get('value');
this._emailInput.disabled = this.model.get('disabled');
this.el.appendChild(this._emailInput);
// Python -> JavaScript update
this.model.on('change:value', this._onValueChanged, this);
this.model.on('change:disabled', this._onDisabledChanged, this);
}
private _onValueChanged() {
this._emailInput.value = this.model.get('value');
}
private _onDisabledChanged() {
this._emailInput.disabled = this.model.get('disabled');
}
private _emailInput: HTMLInputElement;
}
这使我们能够从Python内核更新值到视图。现在,若要将从前端更新后的值传回Python内核(当输入未禁用时),我们使用model.set在前端模型上设置值,然后使用model.save_changes将前端模型与Python对象同步。
export class EmailView extends DOMWidgetView {
render() {
this._emailInput = document.createElement('input');
this._emailInput.type = 'email';
this._emailInput.value = this.model.get('value');
this._emailInput.disabled = this.model.get('disabled');
this.el.appendChild(this._emailInput);
// Python -> JavaScript update
this.model.on('change:value', this._onValueChanged, this);
this.model.on('change:disabled', this._onDisabledChanged, this);
// JavaScript -> Python update
this._emailInput.onchange = this._onInputChanged.bind(this);
}
private _onValueChanged() {
this._emailInput.value = this.model.get('value');
}
private _onDisabledChanged() {
this._emailInput.disabled = this.model.get('disabled');
}
private _onInputChanged() {
this.model.set('value', this._emailInput.value);
this.model.save_changes();
}
private _emailInput: HTMLInputElement;
}
测试#
实例化一个新控件:
email = Email(value='john.doe@domain.com', disabled=False)
email
要获取该小部件的值:
email.value
设置小部件的值:
email.value = 'jane.doe@domain.com'
最终结果应如下所示:

传递URL#
在上面的示例中,我们已经看到了如何将简单的Unicode字符串传递到HTML输入元素。然而,
某些HTML元素,例如 <img/>、<iframe/> 或 <script/> 需要URL作为输入。考虑
一个嵌入 <iframe/> 的小部件。该小部件具有一个 src 属性,该属性连接到 <iframe/>
的 src 属性。它是内置 IPython.display.IFrame(...) 的ipywidget版本。
与内置版本一样,我们希望支持两种形式:
from ipyiframe import IFrame
remote_url = IFrame(src='https://jupyter.org') # full HTTP URL
local_file = IFrame(src='./local_file.html') # local file
注意,第二种形式是相对于笔记本文件的路径。使用此字符串作为src
<iframe/>的属性将无法工作,因为浏览器会将其解释为相对URL,相对于浏览器的地址栏。要将相对路径转换为有效的文件URL,我们在我们的JavaScript视图类中使用
工具函数resolveUrl(...):
export class IFrameView extends DOMWidgetView {
render() {
this.$iframe = document.createElement('iframe');
this.el.appendChild(this.$iframe);
this.src_changed();
this.model.on('change:src', this.src_changed, this);
}
src_changed() {
const url = this.model.get('src');
this.model.widget_manager.resolveUrl(url).then(resolvedUrl => {
this.$iframe.src = resolvedUrl;
});
}
}
调用this.model.widget_manager.resolveUrl(...)返回一个解析为正确URL的承诺。
了解更多#
正如我们在本教程中所见,从cookiecutter项目开始对于快速原型化自定义小部件非常有用。
当前提供以下两个Cookiecutter项目:
widget-ts-cookiecutter: 使用TypeScript创建自定义小部件
widget-cookiecutter: 用于在JavaScript中创建自定义小部件
如果您想了解更多关于构建自定义组件的信息,也可以查看丰富的第三方组件生态系统: