vscode 扩展 本地
When standard HTML is not enough
如果标准HTML不够用
Ever since the days of XML we have tried to extend HTML with our own tags.
从XML时代开始,我们就尝试使用我们自己的标签扩展HTML。
The standard library of HTML tags is fairly limited and intentionally consists of low-level building blocks, meant to be composed by developers into more high-level functionality.
HTML标签的标准库是相当有限的,有意地由低级构建块组成,意在由开发人员组成更高级的功能。
Now that all modern browsers support Web Components (or more specifically Custom Elements) you can create your very own HTML elements that you can use anywhere by just loading a script and adding the tag to the document.
现在,所有现代浏览器都支持Web组件 (或更具体地讲,是Custom Elements ),您可以创建自己HTML元素,只需加载脚本并将标签添加到文档中,即可在任何地方使用。
It’s really as simple as that.
就这么简单。
If you have created your own image gallery, you can use it by just loading the script and adding <image-gallery></image-gallery>
to the document:
如果您创建了自己的图片库,则可以通过加载脚本并将<image-gallery></image-gallery>
到文档中来使用它:
class ImageGallery extends HTMLElement {
constructor() {
super();
} ...}customElements.define('image-gallery', ImageGallery);<image-gallery></image-gallery> // presto!
Here the ImageGallery
class contains all the functionality for the <image-gallery>
HTML element and we register it throughcustomElements.define
with the 'image-gallery'
tag name.
这里的ImageGallery
类包含<image-gallery>
HTML元素的所有功能,我们通过customElements.define
使用'image-gallery'
标签名对其进行注册。
Now frameworks like React, Angular and Vue.js also allow you to create your own HTML tags, but contrary to framework components, Custom Elements are real first-class HTML elements.
现在,像React,Angular和Vue.js这样的框架也允许您创建自己HTML标签,但是与框架组件相反,自定义元素是真正的一流HTML元素。
In this case the ImageGallery
class extends HTMLElement
, which is the base interface of all HTML elements. This means that it will inherit all the functionality that is common to all HTML elements.
在这种情况下, ImageGallery
类扩展了HTMLElement
, HTMLElement
是所有HTML元素的基本接口。 这意味着它将继承所有HTML元素共有的所有功能。
For example, you can attach event listeners to it through addEventListener
, use CSS to style it through its style
property or interact with it in the browser devtools like any other HTML element.
例如,您可以通过addEventListener
将事件侦听器附加到该事件侦听器,使用CSS通过其style
属性设置其style
或在浏览器devtool中与其他HTML元素进行交互。
And it doesn’t stop there.
而且不止于此。
站在巨人的肩膀上 (Standing On The Shoulders Of Giants)
Instead of extending HTMLElement
, Custom Elements can also extend other built-in HTML elements like <button>
, <img>
and <a>
for example.
自定义元素还可以扩展其他内置HTML元素,例如<button>
, <img>
和<a>
,而不是扩展HTMLElement
。
Let’s say we want to create a lazy loading image that will not load until it’s scrolled into the viewport. We could do this by searching for all images in the page and attach a IntersectionObserver
to each image that makes sure the image will only load when it becomes visible.
假设我们要创建一个延迟加载图像,直到将其滚动到视口后才加载。 我们可以通过搜索页面中的所有图像并在每个图像上附加一个IntersectionObserver
来确保此图像仅在可见时才加载。
But we could also extend the built-in image element itself and use that enhanced image element instead of the regular <img>
HTML element.
但是我们也可以扩展内置图像元素本身,并使用增强的图像元素代替常规的<img>
HTML元素。
We can do this by creating a Custom Element that doesn’t extend HTMLElement
but instead extends the interface of the <img>
element, which is HTMLImageElement
:
我们可以通过创建一个自定义元素来做到这一点,该自定义元素不扩展HTMLElement
而是扩展<img>
元素的接口,即HTMLImageElement
:
class LazyImg extends HTMLImageElement {
constructor() {
super();
}...}customElements.define('lazy-img', LazyImg, {extends: 'img'});
The Custom Element is registered with the usual call to customElement.define
but now it takes a third argument, {extends: 'img'}
, that specifies which HTML element will be extended.
自定义元素已通过对customElement.define
的常规调用进行注册,但是现在它使用了第三个参数{extends: 'img'}
,该参数指定将扩展哪个HTML元素。
Now instead of using a new HTML tag, we can just use our enhanced image element with the regular <img>
tag but we add the new functionality to it through the is
attribute:
现在,不用新HTML标签,我们可以将增强的图像元素与常规的<img>
标签一起使用,但是可以通过is
属性为它添加新功能:
<img is="lazy-img" src="/path/to/image.png">
This image is now an enhanced image that gets all the functionality we defined in the LazyImg
class.
现在,此图像是增强的图像,具有我们在LazyImg
类中定义的所有功能。
The complete implementation of LazyImg
is too large for this article but you can find the source code on my Github.
LazyImg
的完整实现对于 LazyImg
来说太大了,但是您可以在 我的Github 上找到源代码 。
The beauty of this approach is that any browser that doesn’t support extending built-in HTML elements will simply ignore the is
attribute and just render a regular image.
这种方法的优点在于,任何不支持扩展内置HTML元素的浏览器都将仅忽略is
属性,而仅呈现常规图像。
Progressive enhancement at its finest.
最好的逐步增强。
示例:客户端路由 (Example: client-side routing)
This way, we can also easily enhance ordinary links to become links that work with a client-side router.
这样,我们还可以轻松地增强普通链接,使其成为可与客户端路由器一起使用的链接。
Normally, we would need to loop through all these links and write some code to prevent that we navigate to another page when the link is clicked, because we want to handle the routing on the client-side.
通常,我们需要遍历所有这些链接并编写一些代码,以防止单击链接时导航到另一个页面,因为我们要在客户端处理路由。
By extending the native <a>
tag, we can simply add an is
attribute to indicate it is a client-side link, so it won’t make the browser go to the page specified in its href
attribute when clicked.
通过扩展本机的<a>
标记,我们可以简单地添加一个is
属性以指示它是客户端链接,因此单击它不会使浏览器转到其href
属性中指定的页面。
We do this by extending the HTMLAnchorElement
which is the interface for the <a>
tag:
为此,我们扩展了HTMLAnchorElement
,它是<a>
标签的接口:
class RouterLink extends HTMLAnchorElement {
constructor() {
super();
}
connectedCallback() {
this.addEventListener('click', e => {
e.preventDefault();
this.dispatchEvent(new CustomEvent('route-change', {
composed: true,
detail: {link: this}
}));
})
}
}
In the connectedCallback
we set an event handler to intercept the click
event. By calling e.preventDefault
, we prevent the browser from following the link so nothing happens when a user clicks the link.
在connectedCallback
我们设置了一个事件处理程序来拦截click
事件。 通过调用e.preventDefault
,我们可以防止浏览器跟踪链接,因此当用户单击链接时什么也不会发生。
Then we throw a new route-change
event with the link as the payload in the link
property. A parent element can listen for this event and perform the client-side routing, for example a nav
tag that has also been extended:
然后,我们在link
属性中引发一个新的route-change
事件,该链接具有链接作为有效负载。 父元素可以侦听此事件并执行客户端路由,例如也已扩展的nav
标签:
<nav is="client-side-router">
<a href="/path/to/page1" is="router-link">Page 1</a>
<a href="/path/to/page2" is="router-link">Page 2</a>
<a href="/path/to/page3" is="router-link">Page 3</a>
</nav>
This way, we can build a navigation component that will work perfectly fine in older, not supporting browsers and that will be enhanced to a client-side router in modern browsers.
这样,我们可以构建一个导航组件,该组件在较旧的浏览器中可以完美运行,不支持浏览器,并且可以在现代浏览器中增强为客户端路由器。
Let’s look at how we could implement the router itself by extending the <nav>
tag.
让我们看看如何通过扩展<nav>
标签实现路由器本身。
The <nav>
tag doesn’t have its own interface so it simply extends HTMLElement
. Although it is a built-in element we can still add Shadow DOM to it which will make interacting with the child element, the links, a bit easier and robust:
<nav>
标签没有自己的接口,因此仅扩展了HTMLElement
。 尽管它是一个内置元素,我们仍然可以向其添加Shadow DOM,这将使它与子元素,链接进行交互变得更加容易和健壮:
class ClientSideRouter extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
<slot name="link"></slot>
`;
}
connectedCallback() {
const slot = this.shadowRoot.querySelector('slot');
const links = slot.assignedNodes();
links.forEach(link => {
link.addEventListener('route-change', e => {
this.handleRouteChange(e.detail.link);
});
});
}
}
The Shadow DOM of the router will only contain a named <slot>
element which will serve as the insertion point for the links. We can then get all links through the assignedNodes
method of the <slot>
and add an event listener to each link, so we can handle the route change when a link is clicked.
路由器的Shadow DOM仅包含一个名为<slot>
元素,该元素将用作链接的插入点。 然后,我们可以通过<slot>
的assignedNodes
方法获取所有链接,并向每个链接添加一个事件侦听器,以便单击链接时可以处理路由更改。
The only thing we need to add on the links is a slot
attribute to make sure they are inserted in the correct slot:
我们需要在链接上添加的唯一内容是slot
属性,以确保将其插入正确的插槽中:
<a href="/path/to/page1" is="router-link" slot="link">Page 1</a>
We could omit the name
attribute on the slot and the slot
attribute on the link. That would also work but then any newlines inside the nav
components would also be returned by assignedNodes()
as empty text nodes and we would need to filter them out.
我们可以省略 插槽上 的 name
属性和 链接上的 slot
属性。 这也将起作用,但是 nav
组件 内的所有换行符 也将由 assignedNodes()
作为空文本节点返回,我们需要将其过滤掉。
This is nice, but the fact that we need to add a separate event handler to each link is a bit unfortunate and inefficient. It would be better if we could just add a single event handler to the router itself.
很好,但是事实是我们需要为每个链接添加一个单独的事件处理程序,这有点不幸且效率低下。 如果我们只向路由器本身添加一个事件处理程序,那就更好了。
We can do this by adding bubbles: true
to the config object of the route-change
event thrown by RouterLink
:
我们可以通过添加bubbles: true
来做到这一点bubbles: true
由RouterLink
引发的route-change
事件的config对象:
this.dispatchEvent(new CustomEvent('route-change', {
composed: true,bubbles: true, // <-- add this to make the event bubble up
detail: {url}
}));
The event will now bubble up and we can listen to it on the router itself:
现在该事件将冒泡,我们可以在路由器本身上监听它:
class ClientSideRouter extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
<slot name="link"></slot>
`;
}
connectedCallback() {
this.outlet = document.querySelector(this.dataset.outlet);this.addEventListener('route-change', e => {
this.handleRouteChange(e.detail.link)
});
}
handleRouteChange(link) {
// handle route change
}
}
customElements.define('client-side-router', ClientSideRouter, {extends: 'nav'});
We could now also make a simple implementation of the handleRouteChange
method. We could add a data-
attribute to the router, containing a CSS selector to specify where the templates should be rendered and a data-
attribute to each router link to specify which template should be rendered:
现在,我们还可以对handleRouteChange
方法进行简单的实现。 我们可以向路由器添加一个data-
attribute属性,其中包含一个CSS选择器以指定应在何处呈现模板,而为每个路由器链接提供一个data-
attribute以指定应呈现哪个模板:
<nav is="client-side-router" data-outlet="#main">
<a href="/path/to/page1" is="router-link" slot="link"data-template="./page1.html">Page 1</a>
...</nav><!-- templates are rendered here -->
<div id="main"></div>
In the handleRouteChange
method we then fetch the template, render it inside the outlet and add and entry to the browser’s history so the url will change to reflect the route change:
然后,在handleRouteChange
方法中,获取模板,将其呈现在出口内,并添加和输入浏览器的历史记录,以便更改URL以反映路线更改:
async handleRouteChange(link) {
const template = link.dataset.template;
const url = link.getAttribute('href');
const state = {template, url};
const html = await (await fetch(template)).text();
history.pushState(state, null, url);
this.outlet.innerHTML = html;
}
Now this is obviously a naive and very basic implementation, but I hope you got an idea of what is possible by extending built-in HTML elements.
现在,这显然是一个幼稚且非常基本的实现,但是我希望您对扩展内置HTML元素的可能实现有所了解。
You can find the source code including a demo page on my Github.
您可以在我的Github上找到包括演示页面在内的源代码。
You can follow me on Twitter where I regularly write about the PWAs, Web Components and the capabilities of the modern web.
您可以在Twitter上关注我,在那里我定期撰写有关PWA,Web组件和现代Web功能的文章。
翻译自: https://itnext.io/how-to-extend-a-native-html-element-1d4674e09c22
vscode 扩展 本地