☰
Brython 自定义 HTML 元素实战:browser.webcomponent 模块完全指南
2026/10/8 14:11:13 网站建设 项目流程
  • 编程语言
  • 语言运行时
  • 编译器
  • 前端

【免费下载链接】brython

Brython (Browser Python) is an implementation of Python 3 running in the browser

项目地址:https://gitcode.com/gh_mirrors/br/brython
点击查看免费下载

导读

browser.webcomponent是 Brython(运行在浏览器中的 Python 3 实现)提供的官方模块,用于基于标准 DOM 的 Web Components 为骨架,结合英文版 browser.webcomponent 文档、底层 JS 实现 与 测试用例,系统讲解define()/get()两个函数、Shadow DOM 的用法、生命周期回调以及属性监听机制。读完本文,你将能够在 Brython 项目中用纯 Python 定义、注册并动态使用自己的 HTML 组件。


一、模块概览:用 Python 定义自己的 HTML 标签

browser.webcomponent模块封装了浏览器原生的 Web Components 标准(Custom Elements),让开发者用 Python 类定义自定义 HTML 标签的行为。一个自定义元素(custom element)在 HTML 页面中可以这样使用:

<popup-window>¡Hola!</popup-window>

也就是说,你可以在 HTML 中直接书写一个尚未内置的标签名,并在 Brython 脚本里通过 Python 类告诉浏览器:当遇到<popup-window>标签时,应该如何渲染、如何响应生命周期事件。

该模块从 www/src/Lib/browser/webcomponent.py 导入,而该文件本身只是对底层 JavaScript 实现的一行再导出:

from _webcomponent import *

真正实现逻辑位于 www/src/libs/_webcomponent.js,它以$B.addToImported('_webcomponent', module)的方式挂载到 Brython 运行时,最终暴露给 Python 侧两个函数:define和get。


二、define():注册自定义元素的核心函数

模块对外暴露的唯一核心函数是:

define(tag_name, component_class[, options])

2.1 tag_name:自定义标签名

tag_name是自定义标签的名称。Web Component 规范强制要求标签名必须包含一个连字符-(例如popup-window、bold-italic、observed-element)。这是为了与原生 HTML 元素区分开,避免冲突。

这一点在底层实现中有硬性校验(www/src/libs/_webcomponent.js 第 49-55 行):

if (typeof tag_name != "string") { $B.RAISE(_b_.TypeError, "first argument of define() must be a string, ...") } else if (tag_name.indexOf("-") == -1) { $B.RAISE(_b_.ValueError, "custom tag name must contain a hyphen (-)") }

因此,如果标签名不含-,define()会抛出ValueError;如果第一个参数不是字符串,则抛出TypeError。

2.2 component_class:定义组件行为的 Python 类

component_class是定义组件行为的类。类的__init__方法用于创建组件;其中的self参数引用的是该自定义组件对应的 DOM 元素。

换句话说,self就是一个 Brython 的 DOM 节点对象,因此你可以在__init__里访问self.attrs(元素属性字典)、self.attachShadow()(创建影子根)、self.textContent、self.style等所有 DOM 能力。底层实现在构造 JS 类时通过$B.DOMNode.$factory(this)将原生元素包装成 Python 可操作的 DOMNode,并执行$B.$call(init, _self)调用 Python 的__init__(见 www/src/libs/_webcomponent.js 第 80-90 行)。

component_class必须是类(class),否则同样会抛出TypeError(源码第 56-59 行)。

2.3 options:可选的 "extends" 选项

define()的第三个参数options是一个字典,对应原生 API CustomElementRegistry.define 的选项。目前唯一可用的选项是"extends",用于创建自定义化内置元素(customized built-in element),即基于某个现有 HTML 标签(如p、div)扩展出的自定义元素:

class MyParagraph: def __init__(self): self.shadow = self.attachShadow({'mode': 'open'}) self.shadow <= html.B('hello') define('my-paragraph', MyParagraph, {'extends': 'p'})

2.4 继承 browser.html 类时自动推断 extends

如果组件类继承了 browser.html 模块中定义的类(如html.P、html.DIV),extends选项会被自动添加,其值取为父类的类名。上面的代码可以简化为:

from browser import html class MyParagraph(html.P): def __init__(self): self.shadow = self.attachShadow({'mode': 'open'}) self.shadow <= html.B('hello') define('my-paragraph', MyParagraph)

底层实现会在未显式传入options时,沿着类的 MRO(方法解析顺序)向上查找,一旦发现某个基类定义在browser.html模块中,就以该基类名(转为小写)作为extends值(www/src/libs/_webcomponent.js 第 24-34 行):

let stack = [...cls.tp_bases]; while (stack.length) { base = stack.pop(); if (base.__module__ === 'browser.html') { _extends = base.__name__.toLowerCase() break } stack.push(...base.tp_bases); }

extends值必须是字符串,且必须是合法标签名——底层会通过document.createElement(_extends)验证,若结果为HTMLUnknownElement则抛出ValueError(源码第 36-48 行)。测试用例 www/tests/test_webcomponent.py 中的 PR 2295 用例即验证了从html.DIV继承并多层继承的场景:

class MyDivA2295(html.DIV): def __init__(self): self <= "A" class MyDivB2295(MyDivA2295): def __init__(self): self <= "B" webcomponent.define("my-div_a_2295", MyDivA2295) webcomponent.define("my-div_b_2295", MyDivB2295)

三、get():按标签名查询组件类

get(tag_name)返回与tag_name关联的组件类;如果该标签名尚未注册任何组件,则返回None。

底层实现(www/src/libs/_webcomponent.js 第 174-178 行)直接查询浏览器原生的customElements注册表:

function get(name) { var ce = customElements.get(name) if (ce && ce.$cls) {return ce.$cls} return _b_.None }

注意:define()在注册时会把 Python 类引用挂在 JS 类上(webcomp.$cls = cls),因此get()返回的是原始的 Python 类对象,而非内部的 JS 类。若注册表里没有对应条目,则返回 Brython 的None(即_b_.None)。


四、完整示例:定义<bold-italic>组件

假设我们要定义一个带data-val属性的自定义标签<bold-italic>:

<bold-italic>from browser import webcomponent class BoldItalic: def __init__(self): # Create a shadow root shadow = self.attachShadow({'mode': 'open'}) # Insert the value of attribute "data-val" in bold italic # in the shadow root shadow <= html.B(html.I(self.attrs['data-val'])) # Tell the browser to manage <bold-italic> tags with the class BoldItalic webcomponent.define("bold-italic", BoldItalic)

这段代码做了三件事:

  1. self.attachShadow({'mode': 'open'})为元素创建一个影子根(ShadowRoot),mode: 'open'表示外部脚本可以通过element.shadowRoot访问该子树;
  2. 通过self.attrs['data-val']读取自定义元素上的属性值;
  3. 使用html.B(html.I(...))构建 "粗体 + 斜体" 的嵌套节点,并用<=运算符(Brython 中向节点追加子节点的语法)插入影子根。

4.1 关于 ShadowRoot

这里用到了另一种 DOM 技术——ShadowRoot(影子根),它的作用是定义一个与主 DOM 树相互隔离的子树(见 英文文档 对 ShadowRoot 的说明)。影子树内的样式与结构不会泄漏到主文档,主文档中的样式也不会意外作用于影子树内部,这是 Web Component 实现样式封装的基石。


五、生命周期回调管理

Web Component 标准定义了一组生命周期回调函数(lifecycle callbacks),在组件的不同阶段被浏览器自动调用,例如元素被插入文档、从文档移除、属性变化等。

在 Brython 中实现它们,只需在组件类中直接添加同名的方法即可,底层会把 Python 类中的函数逐一拷贝到生成的 JS 类原型上(www/src/libs/_webcomponent.js 第 144-163 行会遍历类 MRO 上的所有函数,挂载为webcomp.prototype[key])。

5.1 connectedCallback:元素被接入文档时

import browser.webcomponent class BoldItalic: def __init__(self): # Create a shadow root shadow = self.attachShadow({'mode': 'open'}) # Insert the value of attribute "data-val" in bold italic # in the shadow root shadow <= html.B(html.I(self.attrs['data-val'])) def connectedCallback(self): print("connected callback", self) webcomponent.define("bold-italic", BoldItalic)

当每个<bold-italic>元素被插入文档时,connectedCallback都会被调用一次,self同样是该元素对应的 DOM 节点。这是做初始化工作(绑定事件、加载数据、读取子节点)的典型位置。

5.2 observedAttributes 与 attributeChangedCallback:监听属性变化

要处理某些属性的变化,需要在类中定义列表observedAttributes(声明要观察哪些属性),并实现方法attributeChangedCallback(name, old, new, ns)(分别在属性名、旧值、新值、命名空间发生变化时被调用)。

下面的例子还展示了一个重要技巧:用 browser.html 模块中的maketag函数动态创建新的自定义标签类,再在运行时动态地把元素加入文档:

observed_tag = html.maketag("observed-element") class Observed: observedAttributes = ["data"] def attributeChangedCallback(self, name, old, new, ns): print(f"attribute {name} changed from {old} to {new}") webcomponent.define("observed-element", Observed) elt = observed_tag() document <= elt elt.attrs["data"] = "info"

执行流程是:

  1. html.maketag("observed-element")生成一个对应标签的 Python 构造类(其底层实现在 www/src/brython.js 第 14819-14829 行:maketag校验标签名为字符串后,用makeTagClass生成类、绑定$factory,并注册到browser.html模块命名空间与tags字典中);
  2. observed_tag()实例化元素;
  3. document <= elt把元素动态插入文档,触发connectedCallback;
  4. 修改elt.attrs["data"] = "info",由于"data"在observedAttributes中,浏览器回调attributeChangedCallback,打印attribute data changed from None to info。

注意:observedAttributes应定义为类属性(列表)。底层实现(www/src/libs/_webcomponent.js 第 109-133 行)中,静态 getterobservedAttributes优先读取类属性;若定义成属性(property)对象也能兼容(对应 issue 2454 的测试);若定义成方法则已废弃,会触发DeprecationWarning。


六、源码级解析:define() 的底层实现

www/src/libs/_webcomponent.js 中define函数(第 6-172 行)完整展示了注册一个自定义元素需要经过的步骤:

  1. 参数规范化与校验:通过$B.args处理三个参数(options默认为None);options若非字典则抛TypeError("options can only be None or a dict");tag_name必须含连字符;cls必须是类;
  2. 解析 extends:显式传入options['extends'],或沿 MRO 自动推断自browser.html基类(见 2.4 节);
  3. 注入 DOMNode:将cls.$webcomponent置为true,并把$B.DOMNode插入类 MRO 的末尾(cls.tp_mro.splice(...)),使 Python 组件实例天然具备 DOM 节点的全部方法;
  4. 动态生成 JS 类:以 Python 类名为类名,eval一段模板代码生成class Xxx extends HTMLElement(或 extends 指定的原生类)的 JS 类。其构造函数中会:调用html.maketag确保browser.html模块中存在对应标签类、用$B.DOMNode.$factory(this)包装元素、调用 Python 的__init__;
  5. 合规性检查:按 Custom Element 规范 的要求,如果__init__在构造过程中给元素新增了原本不存在的属性,会抛出TypeError("Custom element must not create attributes, found: ..."),这是自定义元素构造器的硬性约束;
  6. 拷贝 Python 方法到 JS 原型:遍历类 MRO 上的所有函数,挂载到webcomp.prototype,从而让connectedCallback、attributeChangedCallback、disconnectedCallback等原生回调能调用到 Python 实现;
  7. 注册到 customElements:调用浏览器原生customElements.define(tag_name, webcomp, {extends: extend_tag})(有 extends 时)或customElements.define(tag_name, webcomp),完成注册。

七、实战进阶:从测试与示例中提炼的组件设计模式

7.1 用_initialized守卫避免重复初始化

connectedCallback在元素每次被移入文档时都会触发(例如被移动位置、被重新插入)。测试用例(issue 2062)展示的惯用做法是用实例标志位防止重复初始化:

class App(BaseElement): def connectedCallback(self): if not self._initialized: self._get_childs() self._initialized = True def _get_childs(self): self._main_view_elements = [child for child in self.children]

7.2 基于多继承的 Mixin 组合

由于define()会把 MRO 上的方法都挂到 JS 原型,Brython 组件可以借助 Python 多继承把能力拆分为多个 Mixin(issue 1893 / 1894 / 2082 的测试覆盖了属性继承、回调中异常处理、方法解析顺序等场景)。例如:

class UIPlugin: @property def prefix(self): return self._prefix class TestComponent(UIPlugin, BaseComponent): pass webcomponent.define("ui-test1893", TestComponent)

7.3 用__init_subclass__自动注册组件

测试中还出现了一种自动化注册模式(issue 2169):在基类的__init_subclass__中把类名转成带ui-前缀的 kebab-case 标签名并自动define(),从而免去手写注册代码:

class UIPlugin2169: @classmethod def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) tag_name = "ui-" + un_camel(cls.__name__) webcomponent.define(tag_name, cls)

7.4 用自定义元素承载 JSON 配置

测试用例(issue 2075)展示了一个实用场景:自定义元素内部存放 JSON 配置文本,connectedCallback时解析并批量应用到父元素或其子元素上:

class Config(BaseComponent): def connectedCallback(self): if not self._initialized: self.style.display = "none" self._initialized = True child_config = json.loads(self.text) selector = self.attrs.get("selector", None) for child in parent.select(selector): for attr_name, value in child_config.items(): setattr(child, attr_name, value)

7.5 完整的演示页面

仓库中的 www/gallery/webcomponent.html 是一个可直接运行的完整示例,包含一个带图标与 CSS 样式封装的popup-info组件(在 Shadow DOM 内插入<style>、<span>、<img>,并实现connectedCallback),以及父/子组件继承、属性监听、动态创建、事件绑定(bind/unbind、addEventListener/removeEventListener)等多种组合用法,是学习 Brython Web Component 的最佳参考页面。


八、常见错误与注意事项

情况结果
tag_name不含连字符-抛出ValueError: custom tag name must contain a hyphen (-)
define()第一个参数非字符串抛出TypeError
第二个参数不是类抛出TypeError
options既不是None也不是字典抛出TypeError: options can only be None or a dict
extends不是字符串抛出TypeError: value for extends must be a string
extends指向不存在的 HTML 标签抛出ValueError: '...' is not a valid tag name
__init__中给元素创建了原本不存在的属性抛出TypeError: Custom element must not create attributes
observedAttributes声明为方法(而非类属性/列表)触发DeprecationWarning,建议改为类属性

此外还需注意:options中目前只有"extends"一个键可用;自定义元素构造器必须遵循规范约束,不要在__init__阶段擅自添加属性;属性监听的回调签名固定为attributeChangedCallback(self, name, old, new, ns)。


结语

browser.webcomponent用一套极简的 Python API(define+get+ 类方法约定)完整承接了浏览器原生 Web Components 能力:自定义标签、Shadow DOM 封装、生命周期回调、属性监听、动态创建一应俱全。结合 模块文档、英文文档、底层 JS 实现、测试用例 与 演示页面,你可以在 Brython 项目中把可复用的 UI 能力封装成真正的 HTML 标签,让页面代码更简洁、更语义化。

  • 编程语言
  • 语言运行时
  • 编译器
  • 前端

【免费下载链接】brython

Brython (Browser Python) is an implementation of Python 3 running in the browser

项目地址:https://gitcode.com/gh_mirrors/br/brython
点击查看免费下载
上一篇:免费解锁WeMod专业版:Wand-Enhancer完全使用指南
下一篇:城通网盘解析器:三步获取高速直连下载地址的终极指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询