您可能实际上并不需要所有这些;通常有一个更简单的方法。如果有意使用,此处显示的技术可能仍然对您的工具集有用。
为了保持一致性,我们希望我们的自动加载器也成为一个自定义元素——这也意味着我们可以通过 HTML 轻松配置它。但首先,让我们逐步确定那些未解决的自定义元素:
class AutoLoader extends HTMLElement {connectedCallback() {let scope = this.parentNode;this.discover(scope);}
}
customElements.define("ce-autoloader", AutoLoader);
假设我们已经预先加载了这个模块(使用async是理想的),我们可以将一个元素放入我们的文档中。这将立即启动 的所有子元素的发现过程,这些子元素现在构成了我们的根元素。我们可以通过添加到相应的容器元素来将发现限制在文档的子树中——实际上,我们甚至可以为不同的子树设置多个实例。
当然,我们仍然必须实现该discover方法(作为上面类的一部分AutoLoader):
discover(scope) {let candidates = [scope, ...scope.querySelectorAll("*")];for(let el of candidates) {let tag = el.localName;if(tag.includes("-") && !customElements.get(tag)) {this.load(tag);}}
}
在这里,我们检查我们的根元素以及每个后代 ( *)。如果它是一个自定义元素——如带连字符的标签所示——但尚未升级,我们将尝试加载相应的定义。以这种方式查询 DOM 可能代价高昂,所以我们应该小心一点。我们可以通过推迟这项工作来减轻主线程上的负载:
connectedCallback() {let scope = this.parentNode;requestIdleCallback(() => {this.discover(scope);});
}
requestIdleCallback还没有得到普遍支持,但我们可以使用requestAnimationFrame作为后备:
let defer = window.requestIdleCallback || requestAnimationFrame;class AutoLoader extends HTMLElement {connectedCallback() {let scope = this.parentNode;defer(() => {this.discover(scope);});}// ...
}
现在我们可以继续实现缺少的load方法来动态注入元素:
load(tag) {let el = document.createElement("script");let res = new Promise((resolve, reject) => {el.addEventListener("load", ev => {resolve(null);});el.addEventListener("error", ev => {reject(new Error("failed to locate custom-element definition"));});});el.src = this.elementURL(tag);document.head.appendChild(el);return res;
}elementURL(tag) {return `${this.rootDir}/${tag}.js`;
}
请注意elementURL. 该src属性的 URL 假设有一个目录,所有自定义元素定义都位于该目录中(例如→ /components/my-widget.js)。我们可以提出更详尽的策略,但这足以满足我们的目的。将此 URL 委托给一个单独的方法允许在需要时进行特定于项目的子类化:
class FancyLoader extends AutoLoader {elementURL(tag) {// fancy logic}
}
无论哪种方式,请注意我们依赖于this.rootDir. 这就是前面提到的可配置性的用武之地。让我们添加一个相应的 getter:
get rootDir() {let uri = this.getAttribute("root-dir");if(!uri) {throw new Error("cannot auto-load custom elements: missing `root-dir`");}if(uri.endsWith("/")) { // remove trailing slashreturn uri.substring(0, uri.length - 1);}return uri;
}
您现在可能正在考虑observedAttributes,但这并没有真正让事情变得更容易。另外,root-dir在运行时进行更新似乎是我们永远不需要的。
现在我们可以——而且必须——配置我们的元素目录:.
有了这个,我们的自动加载器就可以完成它的工作了。除了它只工作一次,对于初始化自动加载器时已经存在的元素。我们可能还想考虑动态添加的元素。这就是MutationObserver发挥作用的地方:
connectedCallback() {let scope = this.parentNode;defer(() => {this.discover(scope);});let observer = this._observer = new MutationObserver(mutations => {for(let { addedNodes } of mutations) {for(let node of addedNodes) {defer(() => {this.discover(node);});}}});observer.observe(scope, { subtree: true, childList: true });
}disconnectedCallback() {this._observer.disconnect();
}
这样,每当 DOM 中出现新元素时,浏览器就会通知我们——或者更确切地说,我们各自的子树——然后我们使用它来重新启动发现过程。(你可能会争辩说我们在这里重新发明了自定义元素,你是对的。)
我们的自动装载机现在功能齐全。未来的增强可能会研究潜在的竞争条件并研究优化。但对于大多数情况来说,这可能已经足够好了。
上一篇:VSCode:添加SSH远程连接