网页事件接口规范 0.2.0
本包为浏览器(及能提供同义事件队列的宿主)里的豫言应用提供统一等待点:界面事件、页面消息、定时、事件流、请求、编译、可见性、联机、历史和关闭都进入同一个先进先出队列,应用用一个阻塞调用 等待网页事件 取下一个事件,再自行分派。另外提供 订阅界面事件:由应用在订阅时预先声明“同步决定”(阻止默认动作、停止传播、按键规则),宿主在原生回调里当场执行,因为豫言只能事后处理事件,无法改变本次事件的默认行为。应用依赖本包后,全部函数都必须实现,缺少任何一个函数时不得装载。
0.2.0 相对 0.1.0 只增不减(六个函数的签名不变):订阅界面事件 的策略 JSON 新增三个字段——选择器(只对匹配某个 CSS 选择器的元素产生事件并执行阻止默认等同步动作)、带属性(事件载荷里带匹配元素的若干属性)、带矩形(载荷里带匹配元素的位置矩形);事件载荷相应新增 目标标识、属性、矩形 三个字段。同时把“定时”“事件流”“请求”“编译”四类事件明确指给各自的接口包:[网页定时](../网页定时/规范.汉语.md)、[网页事件源](../网页事件源/规范.汉语.md)、[网页请求](../网页请求/规范.汉语.md)、[网页编译](../网页编译/规范.汉语.md)。
事件队列
宿主为每个应用实例维护一个队列。每个事件有类型,队列内按到达顺序排列,宿主给每个事件一个单调递增的序号(从 1 起)。类型取下列固定集合:
| 类型 | 含义 | 来源 |
| --- | --- | --- |
| 界面 | 点击、输入、变更、提交、键盘、组字、焦点、滚动、对话框 cancel/close 等 DOM 事件 | 本包 订阅界面事件;网页界面包的旧式隐式监听 |
| 消息 | 当前文档里以“豫言”开头的应用消息 | 网页消息包 |
| 定时 | 一次或周期定时到期 | 网页定时包 |
| 事件流 | 服务器推送(SSE)事件:打开、成批的消息、错误 | 网页事件源包 |
| 请求 | 非阻塞网页请求的完成或失败 | 网页请求包 |
| 编译 | 本地编译的进度(成批)与结果 | 网页编译包 |
| 可见性 | 页面可见性变化(visibilitychange) | 本包 订阅界面事件 |
| 联机 | 联机状态变化(online、offline) | 本包 订阅界面事件 |
| 历史 | 历史遍历(popstate、hashchange) | 本包 订阅界面事件 |
| 关闭 | 宿主关闭队列 | 宿主 |
| 宿主 | 低层宿主库的原始事件(动画帧、空闲回调、工作线程、广播频道、套接字、公开操作、文树变动等) | 宿主库;应用一般不使用 |
事件流、请求、编译、定时四类事件的字段与产生时机由各自的包规定,本包只规定它们共用的队列、序号、背压与取法。
背压与丢弃
- 每个类型有独立的队列上限,默认 1024,可用
设置网页事件队列上限调整为 1 至 65536;宿主创建时也可传入初始上限。 - 某类型的队列满时,新事件到达会丢弃该类型最旧的事件,并计数;别的类型不受影响。
读取网页事件丢弃数返回自宿主启动以来的丢弃总数,读取网页事件类型丢弃数返回某个类型的丢弃数。被丢事件留下的序号缺口可供应用发现丢失并重新同步。 宿主类型例外:为免丢失待答的公开操作调用,它满时拒绝新事件并报错,不丢旧事件(保持旧宿主库的行为)。- 合并:高频事件(
scroll、resize、mousemove、pointermove、touchmove、wheel、dragover、selectionchange)默认合并——同一订阅、同一目标标识的事件若紧接在队列尾部,则新事件就地替换旧事件,不占新位置,也不算丢弃。订阅策略里的合并可覆盖默认。 - 批事件:事件流与编译进度由宿主合成批事件(
详情里带批数组,见网页事件源、网页编译包),并配合背压保证不丢失;界面与消息事件不合批。
与旧函数的关系
网页界面包的 等待界面事件 和网页消息包的 等待网页消息 保留,但改为按类型从同一队列取事件:前者只取 界面 类型,后者只取 消息 类型,其余类型的事件原样留在队列里,不被吞掉。同一应用可以混用 等待网页事件 与这两个旧函数;混用时同一个事件只会被取走一次,先调用者先得。底层宿主库的 等待浏览器事件(原始 JSON)仍取任意类型的下一个事件,仅供旧应用继续运行,新应用不应与本包混用。
函数
等待网页事件 : 有 → 字符串
阻塞直到队列里有事件,返回下一个事件(不分类型,按到达顺序)的 JSON 文字。宿主关闭后,每次调用都返回 {"类型":"关闭","订阅号":0,"名称":"关闭"}。调用可挂起,应用不得由此取得原生 DOM 对象。
事件 JSON 是对象,含下列字段;各类事件只填相关字段,其余省略:
| 字段 | 类型 | 含义 |
| --- | --- | --- |
| 类型 | 文字 | 上表之一,恒有 |
| 序 | 整数 | 宿主分配的序号,恒有(关闭事件除外) |
| 订阅号 | 整数 | 产生事件的订阅号;旧式隐式监听与宿主自身产生的事件为 0,定时事件为定时号 |
| 标识 | 文字 | 触发元素的 ID,无 ID 时为空文字(界面类型恒有) |
| 名称 | 文字 | 事件名(click、keydown、visibilitychange……;消息为消息名;定时为“定时”;关闭为“关闭”) |
| 操作键 | 文字 | 触发元素及其祖先(止于订阅目标,含目标自身)中最近的 data-yy 属性值;没有则省略 |
| 目标标识 | 文字 | 仅当订阅策略带 选择器 时出现:原始事件目标的 ID,无 ID 时为空文字(此时 标识 是匹配元素的 ID) |
| 属性 | 对象 | 仅当订阅策略带 带属性 时出现:匹配元素上存在的、被请求的属性(名称→文字);不存在的属性省略,值超过 4096 个 UTF-16 码元的省略 |
| 矩形 | 对象 | 仅当订阅策略带 带矩形 时出现:匹配元素的 getBoundingClientRect(),取整后的 {"左":整数,"上":整数,"右":整数,"下":整数}(视口坐标) |
| 值 | 文字 | 目标是 input、textarea、select 时的当前值;值长于 65536 个 UTF-16 码元时省略并带 "值过长":true(改用网页界面包的 读取节点值);type=password 的输入框、scroll、mousemove 等无值事件不带 |
| 选中 | 爻 | 目标是 checkbox 或 radio 时的 checked(旧式隐式监听的事件恒带 值 与 选中) |
| 键 代码 | 文字 | 键盘事件的 key 与 code |
| ctrl meta alt shift | 爻 | 键盘与鼠标事件的修饰键 |
| 重复 | 爻 | 键盘事件的 repeat |
| 组字中 | 爻 | 键盘事件的 isComposing;input 事件的 isComposing;compositionstart、compositionupdate 为真,compositionend 为假 |
| 按钮 | 整数 | 鼠标事件的 button |
| 已阻止默认 | 爻 | 宿主在原生回调里是否已对该事件调用 preventDefault(界面类型恒有) |
| 可取消 | 爻 | 仅当订阅要求阻止默认而浏览器不允许取消该事件时出现,恒为假 |
| 详情 | 任意 JSON | 见下表 |
| 详情错误 | 文字 | 仅消息类型:详情超过 1 MiB 或不是有效 JSON 时,详情 为 null 并带此字段 |
详情 的形状:
| 事件 | 详情 |
| --- | --- |
| input | {"输入类型":inputType,"数据":data 或 null} |
| compositionstart/update/end | {"数据":data} |
| focus blur focusin focusout | {"关联标识":relatedTarget 的 ID 或空文字} |
| scroll | {"顶":整数,"总高":整数,"可视高":整数}(目标是文档时取滚动元素) |
| dialog 的 cancel close | {"返回值":returnValue} |
| 可见性 | {"可见":爻} |
| 联机 | {"联机":爻} |
| 历史 popstate | {"网址":当前地址,"状态":JSON 化的 state 或 null}(state 不能 JSON 化或超过 64 KiB 时为 null) |
| 历史 hashchange | {"旧网址":文字,"新网址":文字} |
| 消息 | 消息的 JSON 详情 |
| 定时 | {"种类":"一次"或"重复","标记":文字,"时刻":整数} |
| 宿主 | 原始事件对象 |
订阅界面事件 : 字符串 → 字符串 → 字符串 → 整数
参数依次为目标、事件名、策略 JSON,返回订阅号(正整数)。
- 目标:页面里已有元素的 ID,或保留字
文档、窗口(因此 ID 为“文档”或“窗口”的元素不能作为目标)。目标元素须在订阅时存在;元素以后被替换掉,订阅随之失效。 - 事件名:ASCII,
[A-Za-z][A-Za-z0-9_.:-]*,至多 64 个字符。visibilitychange产生可见性类型事件,online、offline产生联机类型,popstate、hashchange产生历史类型,其余产生界面类型。以“豫言”开头的消息名不能在这里订阅,应使用网页消息包。 - 委托:目标下任意后代触发的同名事件都会产生事件,
标识为触发元素的 ID(没有则为空文字),操作键为最近带data-yy的祖先(含自身)的值。因此动态生成的按钮不必有 ID,只要带data-yy。不冒泡的事件(focus、blur、scroll、close、cancel、load、error、toggle、invalid、mouseenter、mouseleave、pointerenter、pointerleave)宿主一律在捕获阶段监听,同样可以委托。 - 同一目标、同一事件名可以订阅多次,每次得到不同的订阅号,各自产生事件。
- 订阅总数至多 256。目标不存在、事件名或策略非法、数量超限时抛豫言事故(可被捕获);失败的订阅不留下任何痕迹。
策略 JSON是对象,字段全部可省略;出现未知字段或类型不对一律报错,避免笔误被默默忽略:
| 字段 | 类型 | 含义 |
| --- | --- | --- |
| 阻止默认 | 爻 | 在原生回调里调用 preventDefault()(事件可取消时);不能与 被动 同时为真 |
| 停止传播 | 爻 | 调用 stopPropagation() |
| 停止同处 | 爻 | 调用 stopImmediatePropagation()(同目标的后续监听器也收不到) |
| 捕获 | 爻 | 在捕获阶段监听(不冒泡事件总是捕获) |
| 被动 | 爻 | 被动监听;scroll、wheel、touchstart、touchmove 在没有上面三个动作时默认被动 |
| 合并 | 爻 | 是否合并紧邻的同源事件,默认仅高频事件为真 |
| 一次 | 爻 | 首个事件之后自动取消订阅(该事件仍会送达) |
| 仅命中 | 爻 | 有键规则时只把命中的事件送入队列 |
| 键规则 | 数组 | 至多 32 条,见下;仅适用于 keydown、keyup、keypress |
| 选择器 | 文字 | CSS 选择器(选择器列表亦可),至多 512 个字符,订阅时校验,非法则报错;见下文“选择器过滤” |
| 排除选择器 | 文字 | CSS 选择器,至多 512 个字符,订阅时校验;原始事件目标(含自身)落在匹配它的元素之内时,本次事件被忽略:既不投递,也不执行任何同步动作;见下文“选择器过滤” |
| 带属性 | 数组 | 至多 8 个属性名([a-z][a-z0-9:_-]*,至多 64 字符,不得以 on 开头,不得是 style、srcdoc),不得重复;事件载荷带 属性 |
| 带矩形 | 爻 | 为真时事件载荷带 矩形 |
没有键规则时,阻止默认、停止传播、停止同处 对该订阅的每个事件生效。有键规则时,动作只在某条规则命中时才执行:若三个动作字段一个都没给,命中即 阻止默认(沿用宿主库“键盘阻止默认”规则的含义);若给了其中任何一个,则只执行所给的动作。
键规则是对象,同一条规则内各条件取“且”,多条规则取“或”;字段:
| 字段 | 含义 |
| --- | --- |
| 键 | 与 event.key 相同;单个字母不分大小写(l 也匹配 L)。键 与 代码 至少给一个 |
| 代码 | 与 event.code 相同 |
| ctrl meta alt shift | 布尔;省略表示不参与匹配,给出则必须与事件相同 |
| 含组字 | 布尔;默认假:输入法组字期间(isComposing 或 keyCode 为 229)的按键不命中,以免破坏输入法;为真则组字期间也匹配 |
| 仅选区为空 | 布尔;为真时,若输入框内或页面上有选中文字则不命中(例如只在没有选区时拦截 Ctrl+C) |
| 仅目标标识 | 文字;事件目标须是该 ID 的元素本身或其后代 |
| 仅当属性 | 对象 {"标识":ID,"属性":名,"值":文字或 null}:该元素的属性须等于给定值;值 为 null 表示该属性不存在;hidden、disabled、readonly、open、checked、selected 等布尔属性用 "true"(存在)与 "false"(不存在)表示 |
规范强调:所有同步条件都写在订阅时;键规则、阻止默认与停止传播在宿主的原生事件回调里当场判定和执行,豫言收到事件时本次事件的默认行为早已决定,事后无法改变。要按运行时状态改变拦截行为,应用应取消并重新订阅,或使用 仅当属性,让宿主读取页面里的属性现值。
浏览器的实际约束:dialog 的 cancel(Esc)只有在页面已获用户激活时才允许取消;否则事件不可取消,preventDefault 无效,此时事件载荷带 可取消:false、已阻止默认:false,dialog 照常关闭。
选择器过滤(0.2.0):策略带 选择器 时,宿主在原生回调里先做“委托匹配”,再谈其余:
- 取事件的原始目标(若是文本节点则取其父元素);目标不是元素(例如
scroll的目标是文档)则不匹配。 - 取“最近匹配祖先”
m= 从原始目标起向上(含自身)第一个匹配选择器的元素,且必须在订阅边界之内(订阅目标是元素时,m须是该元素或其后代;订阅目标是文档或窗口时,不限)。 - 没有 `m`:本次事件既不投递,也不执行阻止默认、停止传播、停止同处——原生事件不受这次订阅的影响。
- 有
m:照常检查键规则并执行动作;事件载荷里所有关于“目标元素”的字段——标识、操作键(从m向上找data-yy,止于订阅边界)、值、选中、属性、矩形——都取m(focus 类事件详情里的关联标识仍是relatedTarget的 ID,不受影响);原始目标的 ID 由目标标识报告。
典型用法:订阅界面事件("文档", "click", {"捕获":true,"选择器":".豫菜单, .豫导航 [data-lang], a[href]","阻止默认":true,"停止同处":true,"带属性":["href","data-lang"]})——只拦截导航链接与语言按钮的点击并读到被点链接的 href,页面上其他点击丝毫不受影响;订阅界面事件("文档", "mouseover", {"选择器":".类型引用[title]","带属性":["title"],"带矩形":true})——悬停在带标题的引用上才产生事件,并带回标题与位置,用于摆放浮层。选择器的语法错误在订阅时报错;选择器 与 键规则 可以并用(先选择器,后键规则)。
排除选择器(0.2.0):策略带 排除选择器 时,先检查原始目标(文本节点取其父元素):若它或它的某个祖先匹配该选择器(closest,不受订阅边界限制),本次事件被忽略,然后才做上面的选择器匹配。用来表达“点在某区域之外”——例如收起下拉面板:订阅界面事件("文档", "click", {"排除选择器":".文件目录"}) 只在点击落在 .文件目录 之外时产生事件。(选择器 用的是“最近匹配祖先”,无法表达“之外”::not(...) 会向上匹配到 body。)
没有 选择器 时,带属性、带矩形 作用于原始目标本身(目标是元素时)。带属性 读取的是属性(getAttribute,原样文字)而不是 DOM 属性;type=password 的输入框上名为 value 的属性不带出。
取消订阅界面事件 : 整数 → 有
取消订阅并移除该订阅尚未取走的事件。订阅号无效或已取消时什么也不做。已经被 等待网页事件 取走的事件不受影响。
读取网页事件丢弃数 : 有 → 整数、读取网页事件类型丢弃数 : 字符串 → 整数
见“背压与丢弃”。类型名不属于上表时,后者返回 0。
设置网页事件队列上限 : 字符串 → 整数 → 有
设置某类型的队列上限,1 至 65536;类型名不在上表或上限越界时抛事故。调低上限时,多出的旧事件在该类型下一次有新事件到达时被丢弃并计数。
限额与失败分类
| 项 | 限额 |
| --- | --- |
| 队列上限(每类型) | 默认 1024,可配 1 至 65536 |
| 订阅数 | 界面订阅至多 256,消息订阅至多 128(见网页消息包) |
| 键规则条数 | 至多 32 |
| 事件名长度 | 至多 64 个字符 |
| 事件里的 值 | 至多 65536 个 UTF-16 码元,超过则省略并标记 |
| 消息详情 | 至多 1 MiB |
失败分类:违规调用(目标不存在、事件名或策略非法、订阅超限、队列上限越界、类型名无效)抛豫言事故,消息为中文;队列满不是失败,按上文丢弃并计数;宿主关闭不是失败,等待返回“关闭”事件。
跨宿主一致性
- 事件按宿主收到的顺序排队,同一订阅号的事件保持先后。
- 序号在宿主实例内单调递增;丢弃只发生在队列头部(最旧)方向,不会打乱余下事件的顺序。
- 键规则匹配、
操作键搜索和委托规则必须与本文一致;字段缺省与省略规则必须一致,应用应把缺失字段视为“不适用”。 - 不能提供同步拦截的宿主不得声明本包。
一致性样例
- 订阅
容器的click;点击容器内无 ID、带data-yy="打开:7"按钮里的span;等待网页事件返回类型为“界面”、标识为空、操作键为打开:7。 - 订阅
文档的keydown,策略{"键规则":[{"键":"Enter","shift":false}]};派发 Enter 后,dispatchEvent一返回,事件的defaultPrevented即为真;派发 Shift+Enter 则为假;两者的事件都送达,已阻止默认分别为真、假。 - 把界面队列上限设为 5,连续点击 20 次而不取事件:队列里留下最新 5 个,
读取网页事件丢弃数为 15,留下事件的序号连续且大于 15。 - 应用正阻塞在
等待界面事件时到达一条页面消息:消息留在队列里,随后等待网页消息(或等待网页事件)取到,界面事件到达后等待界面事件返回。 - 订阅
文档的click,策略{"捕获":true,"选择器":"a[href]","阻止默认":true,"带属性":["href"]};点击<a href="/x"><span>字</span></a>里的span:事件到达,标识为a的 ID(可为空)、目标标识为span的 ID、属性为{"href":"/x"},且dispatchEvent一返回,defaultPrevented即为真;点击页面上的普通按钮:没有事件,defaultPrevented为假。 - 订阅策略
{"选择器":"button["}:订阅时抛事故,宿主不留下任何监听。 - 订阅
文档的click,策略{"排除选择器":".文件目录"}:点击.文件目录内的任意后代没有事件;点击页面上其他位置有事件(标识等仍取原始目标)。
边界
本包不规定:网络请求、定时器的创建与取消、SSE 连接、本地编译(它们分属网页请求、网页定时、网页事件源、网页编译包,只借用本包的事件类型);不提供跨窗口、跨 Worker 事件;不提供“事后阻止默认”;不向应用暴露原生事件对象或 DOM 句柄;不保证高频事件(滚动、指针移动)逐个送达(默认合并);触屏、拖放、剪贴板 paste 的载荷字段留待后续版本。