自定义懒加载Select元素的value setter在浏览器扩展页面快速重载时的异常问题咨询
大家好,我正在开发一款浏览器扩展的自定义UI元素,目标是实现一个和原生HTMLSelectElement的value setter行为一致的下拉选择器,同时保留模板懒加载的特性来优化性能。目前99%的场景下功能都正常,但当我按住F5快速多次重载页面时,这个自定义select的value会莫名其妙跳回第一个默认选项,折腾了半天没找到完美的解决办法,想请教下社区的各位大佬。
先给大家梳理下我的实现逻辑和相关代码:
基础自定义元素基类(DSElement)
我写了一个基类来缓存同类型元素的模板,避免重复加载模板资源:
const templateCache = new Map(); export class DSElement extends HTMLElement { constructor() { super(); this._name = this.constructor._type; this.attachShadow({ mode: 'open' }); this._internals = this.attachInternals(); } async connectedCallback() { let template = templateCache.get(this._name); if (!template) { const templateUrl = chrome.runtime.getURL(`interface/templates/${this._name}.html`); const response = await fetch(templateUrl); const html = await response.text(); const wrapper = document.createElement('template'); wrapper.innerHTML = html; const inner = wrapper.content.getElementById(`${this._name}-template`); if (inner) { template = inner; templateCache.set(this._name, template); } requestAnimationFrame(() => this._after()); } if (template) this.shadowRoot.appendChild(template.content.cloneNode(true)); } async _after() { // 留给子类重写的虚拟方法,用于完成元素的后续初始化逻辑 } }
自定义Select元素(DSSelect)
继承自上面的基类,在_after方法里完成选择器的交互逻辑和DOM绑定:
import { DSElement } from './ds-element.js'; class DSSelect extends DSElement { static _type = 'ds-select'; constructor() { super(); this._value = null; this._firstOption = null; this._label = null; this._internals.role = 'select'; } async _after() { this._select = this.shadowRoot.getElementById('select'); this._options = this.shadowRoot.getElementById('options'); this._labelSpan = this.shadowRoot.getElementById('label'); this.setInitialSelection(); this._select.addEventListener('click', () => this.toggle()); this.addEventListener('click', e => this.handleOptionClick(e)); } setInitialSelection() { let firstOption; if (this._firstOption !== null) firstOption = this._firstOption; else firstOption = this.querySelector('ds-opt'); if (firstOption) this.selectOption(firstOption); } selectOption(option) { this._value = option.getAttribute('value'); this._label = option.textContent; this._labelSpan.innerText = this._label; this.dispatchEvent(new Event('change', {bubbles: true})); } get value() { return this._value; } set value(value) { const match = Array.from(this.querySelectorAll('ds-opt')).find( opt => opt.getAttribute('value') === value ); if (match) this._firstOption = match; } } customElements.define( DSSelect._type, DSSelect );
页面初始化代码
我在页面的DOMContentLoaded事件里设置这个select的初始value:
const reloadOptions = () => { const elMode = document.getElementById('mode'); elMode.value = 'selected'; }; document.addEventListener('DOMContentLoaded', reloadOptions);
模板与页面HTML结构
ds-select对应的模板文件内容:
<template id="ds-select-template"> <style> :host { display: inline-block; position: relative; user-select: none; } #select { cursor: pointer; } #options { display: none; position: absolute; top: 100%; left: 0; right: 0; } #options.open { display: block; } </style> <div id="select"> <span id="label">n/a</span> </div> <div id="options"> <slot></slot> </div> </template>
页面中使用ds-select的方式:
<ds-select id="mode"> <ds-opt value="all"> Parse all </ds-opt> <ds-opt value="selected"> Parse selected </ds-opt> </ds-select>
问题排查与猜测
一开始我以为是reloadOptions和_after方法的异步竞争:DOMContentLoaded触发后执行的设置value操作,和用requestAnimationFrame延迟执行的_after可能时序颠倒。如果_after晚于value设置执行,那setter里的this.querySelectorAll('ds-opt')可能因为某种原因找不到对应的选项元素,导致_firstOption没被赋值,最后setInitialSelection就走了else分支,选了第一个默认选项。
后来我突然意识到这是浏览器扩展的代码,快速重载时控制台会出现Loading failed for the module with source “moz-extension://*的报错,这说明问题可能出在扩展脚本/模块的加载时序、或者页面销毁/重载的资源清理过程,而不是我自己写的函数之间的竞争。现在我觉得需要重构代码,确保即使模板或模块没完全加载完成,也不会覆盖已经设置好的value。
想请教下大家,这种场景下正确的处理方式应该是什么?怎么确保value setter的设置能被正确保留,不会因为异步加载或页面快速重载而丢失?
内容来源于stack exchange

