Hey! Look at the past few years, the development of the Web front end is really fast ah!

Think of a few years ago, when HTML was a basic skill for front-end developers, it was possible to build a usable website with a variety of tags, and basic interaction was not a problem. If you get a little bit more CSS, well, it’s golden and crispy and delicious. Throw in a few handfuls of JavaScript at this point and you can’t stop.

As the need for HTML grew and the structure of HTML became more complex, the amount of repetitive code made it extremely difficult to change pages, spawning dozens of template tools that extracted common parts into common components. Later, as the performance of JavaScript improved, JavaScript became more and more important than just a side dish, and the introduction of front-end rendering took the pressure off the server to parse the template by simply providing static files and an API. Then, the front-end rendering tools were moved back to the server, and the back-end rendering appeared.

In summary, componentization makes the complex front-end structure clear, the parts are independent, high cohesion and low coupling, resulting in significantly lower maintenance costs.

So, have you ever heard of native HTML components?

Four Web component standards

Before we get to native HTML components, let’s take a quick look at the four Web component standards: HTML Template, Shadow DOM, Custom Elements, and HTML Imports. One of them has actually been abandoned, so it’s become the Big Three.

HTML Template is the < Template > tag in HTML5. Under normal circumstances, it is colorless, tasteless and invisible. Even the img below it will not be downloaded, and script will not be executed.

Shadow DOM is a basic tool for native component encapsulation, which enables component-to-component independence.

Custom Elements is a container that wraps native components so that you can write a label and get a complete component.

HTML Imports are HTML equivalents to ES6 Modules. You can import another HTML file and use its DOM nodes. However, HTML Imports are so similar to the ES6 Module that no browser other than Chrome wants to implement it, so it has been deprecated and is not recommended. The ES6 Module will be used to replace it in the future, but there seems to be no plan to replace it at the moment, as it has been removed from the new Chrome and will be warned in the Console when it is used. The warning says to use ES Modules instead, but I tested that ES Module in Chrome 71 forces the MIME type of files to be JavaScript, which is not supported yet.

Shadow DOM

To talk about native HTML components, let’s talk about what Shadow DOM is.

You are familiar with the DOM, which exists as a basic skeleton in HTML. It is a tree structure, and each node in the tree is a part of HTML. DOM, as a tree, has a hierarchical relationship between superiors and subordinates. We usually use “parent node”, “child node”, “brother node” and so on to describe it (of course, some people think that these appellations emphasize gender, so we also create some gender-independent appellations). Children inherit from their parents to some extent, but also have some influence from their siblings, notably when CSS Style is applied, children inherit from their parents.

And Shadow DOM, which is also a kind of DOM, is also a tree, except it’s a special purple potato on a DOM tree 🍠, oh no, subtree.

What? Isn’t the DOM itself made up of subtrees? What’s so special about this Shadow DOM?

What makes Shadow DOM special is that it strives to create a relatively independent space. Although it also grows on the DOM tree, its environment is isolated from the outside world. In this isolation space, you can selectively inherit properties from the parent node of the DOM tree. Or even inherit a DOM tree.

Using the isolation of the Shadow DOM, we can create native HTML components.

In fact, browsers already implement some components through Shadow DOM, but we use them without realizing it, which is part of the beauty of Shadow DOM wrapped components: You just write an HTML tag and I’ll do the rest. (Isn’t it like React JSX?)

Let’s take a look at an example of a browser implementation using Shadow DOM, which is the video tag:

<video controls src="./video.mp4" width="400" height="300"></video>
Copy the code

Let’s take a look at the browser rendering:

Wait a minute! What happened to Shadow DOM? How is this different from normal DOM?

In Chrome, Elements does not display the Shadow DOM node implemented internally by default and needs to be enabled in Settings:

Note: By default, the browser hides its own Shadow DOM implementation, but it will not hide Shadow DOM created by a user script.

Then we can see what the video tag looks like:

Here, you can adjust the contents of the Shadow DOM exactly as you would debug a normal DOM (just like a normal DOM, it will be refreshed).

We can see that most of the nodes in the shadow DOM above have pseudo properties, with which you can write CSS styles outside to control the node styles. For example, change the background color of the input button from pseudo=”-webkit-media-controls-overlay-play-button” to orange:

video::-webkit-media-controls-overlay-play-button {
  background-color: orange;
}
Copy the code

Since Shadow DOM is actually a kind of DOM, you can continue to nest Shadow DOM in Shadow DOM, as above.

There are many other elements in the browser that are wrapped in the Shadow DOM, such as ,

Because of the isolation of the Shadow DOM, even if you write a style outside: div {background-color: red! important; }, div inside Shadow DOM will not be affected at all.

In other words, when writing styles, use ID when it should be id, use class when it should be class, and the class of a button should be.button as.button. Never mind that ids and classes in the current component might conflict with other components, just make sure one component doesn’t conflict internally — that’s easy to do.

This solves a problem that most componentized frameworks face today: How to write Element’s class(className)? Using prefixed namespaces results in class names that are too long, like this:. Header-nav-list-sublist-button-icon; With some CSS-in-JS tools, you can create unique class names like.nav__welcomewrapper___lkxTG, which are still a bit long with redundant information.

ShadowRoot

ShadowRoot is the root underneath the Shadow DOM. You can treat it like a in the DOM, but it’s not a , so you can’t use attributes on , or even a node.

You can manipulate the entire Shadow DOM tree with attributes and methods like appendChild and querySelectorAll under ShadowRoot.

For a normal Element, such as

, you can create a ShadowRoot by calling the attachShadow method on it (there is also a createShadowRoot method, which is outdated and not recommended). AttachShadow Accepts an object for initialization: {mode: ‘open’}, this object has a mode attribute that has two values: ‘open’ and ‘closed’. This attribute is initialized when ShadowRoot is created and becomes a read-only attribute after ShadowRoot is created.

What is the difference between mode: ‘open’ and mode: ‘closed’? After calling attachShadow to create ShadowRoot, the attachShdow method returns the ShadowRoot object instance, which you can use to construct the entire Shadow DOM. When mode is set to ‘open’, there is a ShadowRoot property on the external normal node used to create ShadowRoot (such as

). This property is the created ShadowRoot. After ShadowRoot is created, you can use this property to retrieve ShadowRoot anywhere and continue to modify it. When mode is ‘closed’, you can no longer get this property, it will be set to null, that is, you can only get the ShadowRoot object after attachShadow, which is used to construct the Shadow DOM. Once you lose the reference to this object, you can no longer modify the Shadow DOM.

#shadow-root (user-agent) shadow-root (user-agent) shadow-root (user-agent) If the script created ShadowRoot is used, the parentheses show the mode of Shadow DOM as open or closed.

The mode of user-Agent implemented inside the browser is closed, so you cannot obtain the ShadowRoot object of the node through the ShadowRoot attribute, which means you cannot modify the Shadow DOM implemented inside the browser through scripts.

HTML Template

With the ShadowRoot object, we can create internal structures in code. For simple structures, maybe we can create them directly with document.createElement, but for more complex structures, creating all of them in this way is not only difficult, but also the code is not readable. You can also use the backquoted string provided by ES6 (const template = ‘……) `;) Constructing a structure with innerHTML, using the template to take advantage of the fact that you can wrap any line in a backquoted string and that HTML is not sensitive to indentation, is not elegant either. Long HTML strings are not pretty in code, even if you extract a single constant file.

This is where the HTML Template comes in. You can write the DOM structure in an HTML document and load it in ShadowRoot.

HTML Template is actually a < Template > tag in HTML. Normally, the contents of this tag are not rendered, including img, style, script, etc., which are not loaded or executed. You can use methods like getElementById in your script to get the node that corresponds to the

Using the Document. importNode method, you can create a copy of the Document -fragment object. You can then replace the template content in the copy with any DOM attribute methods and eventually insert it into the DOM or Shadow DOM.

<div id="div"></div>
<template id="temp">
  <div id="title"></div>
</template>
Copy the code
const template = document.getElementById('temp');
const copy = document.importNode(template.content, true);
copy.getElementById('title').innerHTML = 'Hello World! ';

const div = document.getElementById('div');
const shadowRoot = div.attachShadow({ mode: 'closed' });
shadowRoot.appendChild(copy);
Copy the code

HTML Imports

With HTML templates, it’s easy to create closed Web components, but there are still some imperfections: we have to define a large number of < templates > in HTML, one for each component.

At this point, we can use the deprecated HTML Imports. Although it is deprecated, it will be supported in the future via ES6 Modules, so in theory it will just be loaded in a different way.

With HTML Imports, we can define

Deprecated HTML Imports are implemented with the tag, just specify rel=”import”, like this: , which can accept onload and onError events to indicate that it has loaded. It is also possible to create a link node from a script and then specify rel and href to load on demand. After the Import is successful, there is an Import attribute on the link node, which stores the DOM tree from which the Import is stored, you can querySelector or something like that, Use the cloneNode or document.importNode method to create a copy.

Future HTML Imports will come in the form of ES6 modules that import * as template from ‘./template.html’ directly in JavaScript; , or import on demand, like this: const template = await import(‘./template.html’); . Although the browser already supports ES6 Modules, when importing other Modules, it will check that the MIME type of the file returned by the server must be the MIME type of JavaScript, otherwise it will not be allowed to load.

Custom Elements

With the three component standards above, we’re really just splitting UP HTML, splitting a large DOM tree into separate smaller DOM trees, not really components.

To implement a real component, we need Custom Elements, which, as its name suggests, is used to define native components.

The core of Custom Elements is to inherit the HTML native HTMLElement class (or a specific native Element class such as HTMLButtonElement) using object inheritance in JavaScript. You then write your own lifecycle functions that handle member attributes and user interaction events.

This looks a lot like React today. In React, you create a component like this: Class MyElement extends React.Component {… } to use native Custom Elements, you write: class MyElement extends HTMLElement {… }.

The life cycle functions of Custom Elements are limited, but sufficient to use. Here I make a simple comparison between the life cycle function of Custom Elements and React:

  • Constructor (): a constructor that initializes state, creates Shadow DOM, listens for events, and so on.

    The Mounting phase of React includes: constructor(props), static getDerivedStateFromProps(props, state), and Render ().

    In Custom Elements, the constructor() constructor is exactly what it means: initialize, similar to React initialization, but it doesn’t split it into parts as React does. At this stage, the component is only created (for example, through document.createElement()), but not yet inserted into the DOM tree.

  • ConnectedCallback (): The component instance has been inserted into the DOM tree for some show-related initialization.

    Corresponding to the last lifecycle of the Mounting phase in React: componentDidMount().

    At this stage, the component has already been inserted into the DOM tree or is already written into the DOM tree in an HTML file. This stage usually involves some display-related initialization, such as loading data, images, audio, or video, and displaying it.

  • AttributeChangedCallback (attrName, oldVal, newVal): Changes to component attributes to update the state of the component.

    Corresponding to the Updating phase in React: Static getDerivedStateFromProps(props, state), shouldComponentUpdate(nextProps, NextState), Render (), getSnapshotBeforeUpdate(prevProps, prevState) and componentDidUpdate(prevProps, prevState, Snapshot).

    This lifecycle is triggered when the properties of the component (functions in React) change. However, not all property changes are triggered. For example, class and style changes of the component generally do not generate special interactions. Performance is greatly affected. So Custom Elements requires the developer to provide a property list, and the lifecycle function is triggered only when the property in the property list changes.

    This list of properties is declared as a static read-only property on the component Class, and is implemented in the ES6 Class using a getter function that implements only the getter but not the setter. The getter returns a constant, making it read-only. Like this:

    class AwesomeElement extends HTMLElement {
      static get observedAttributes() {
        return ['awesome']; }}Copy the code
  • DisconnectedCallback (): The component is removed from the DOM tree for some cleanup.

    Corresponding to the Unmounting phase in React: componentWillUnmount().

  • AdoptedCallback (): Component instance is moved from one document to another.

    This life cycle is unique to native components; there is no similar life cycle in React. This life cycle function is also not commonly used. It is commonly encountered when operating on multiple documents, and is triggered when the document.AdoptNode () function is called to transfer the document to which the node belongs.

After defines the custom component, we need to register it to the list of HTML tags, through the window. The customElements. Define () function can be realized, this function takes two parameters and an optional parameter. The first parameter is the name of the registered tag. To avoid conflicts with the HTML tag itself, Custom Elements requires that the user-defined component name must contain at least a dash – and not start with a dash, such as my-Element or awe-button. The second argument is the class of the registered component. We can pass in the name of the inherited subclass, or we can write an anonymous class:

window.customElements.define('my-element'.class extends HTMLElement {... });Copy the code

After registration, we can use it. We can write the corresponding tag directly in the HTML document, such as:
, which can also be created with document.createElement(‘my-element’), is used almost exactly like a normal tag. Note, however, that although the HTML standard states that some tags can be unclosed or self-closed (

or

), only a few specified tags allow self-closing, so you must write a node of Custom Elements with a close tag in your HTML.

Since Custom Elements are defined by JavaScript, and js files are usually externalized by

my-element:not(:defined) {
  display: none;
}
Copy the code

Or Custom Elements also provides a function to check if the specified component has been registered: CustomElements. WhenDefined (), this function takes a component parameters, and returns a Promise, when the Promise is resolve, it means the component has been registered.

This way, we can safely use the async property on JavaScript

Custom Elements + Shadow DOM

When you use Custom Elements to create components, it is often combined with the Shadow DOM, which allows you to create separate components by taking advantage of its isolation.

The Shadow DOM is typically created in the constructor() function of Custom Elements, and event listeners are added to the nodes in the Shadow DOM, firing native Events objects for specific Events.

When writing an HTML document normally, we might add some child nodes to Custom Elements, like this:

Title

Content

In React, these child nodes are placed in the children of props, and we can choose where to place it in render(). There is a special tag in the Shadow DOM:

. This tag does exactly what it says: it places a “slot” on the Shadow DOM, and the children of Custom Elements are automatically placed into this “slot”.

Sometimes we need to be more precise about where children are placed in the Shadow DOM, and by default, all children are placed under the same

tag, even if you write multiple < slots >. So how do you control the child nodes more precisely?

By default,

Fallback
is the default

, only the first default

will be valid, put all the children in, if there are no children available, The default Fallback content will be displayed (a Fallback can be a sub-DOM tree).

The

tag has a name attribute, and when you provide a name, it becomes a “named

“. There can be more than one such

as long as the names are different. They will automatically match any child node under Custom Elements with a slot attribute that has the same name as their own, like this:


<template id="list">
  <div>
    <h1>Others</h1>
    <slot></slot>
  </div>
  <div>
    <h1>Animals</h1>
    <slot name="animal"></slot>
  </div>
  <div>
    <h1>Fruits</h1>
    <slot name="fruit"></slot>
  </div>
</template>

<my-list>
  <div slot="animal">Cat</div>
  <div slot="fruit">Apple</div>
  <div slot="fruit">Banana</div>
  <div slot="other">flower</div>
  <div>pencil</div>
  <div slot="animal">Dog</div>
  <div slot="fruit">peach</div>
  <div>red</div>
</my-list>
Copy the code
class MyList extends HTMLElement {
  constructor() {
    super(a);const root = this.attachShadow({ mode: 'open' });
    const template = document.getElementById('list');
    root.appendChild(document.importNode(template.content, true));
  }
}
customElements.define('my-list', MyList);
Copy the code

This results in the structure shown in the figure, #shadow-root (open) indicates that this is an open shadow DOM, and the following nodes are cloned directly from the template. The browser automatically places several grey

nodes under the three

tags. In fact, these grey

nodes represent “references” to their real nodes. Moving the mouse over them will show a link to jump to the real node.

Here we can see that although the children under

are placed out of order, they will be placed under the correct

tags whenever slot attributes are given. Note that there is a

flower

. This node is not displayed in the result because slot=”other” is specified but no matching

tag is found.


When styling the Shadow DOM of Custom Elements, we can place the

There are also specific selectors in the styles inside Shadow DOM, such as :host selector, which stands for ShadowRoot. This is similar to :root in the normal DOM, and it can be used in combination with other pseudo-classes, such as when the mouse is over a component: :host(:hover), when the component has a class: :host(.awesome), when the component has a disabled property: :host([disabled])… However, host has inherited attributes, so if you define styles outside of Custom Elements, the styles in :host will be overridden, making it easy to implement a variety of “theme styles.”

To implement custom themes, we can also use the :host-context() selector provided by Shadow DOM, which allows us to check whether any ancestor nodes of the Shadow DOM contain the specified selector. For example, if there is a class:. Night on the outermost DOM < HTML > or , then the Shadow DOM can use :host-context(. Night) to specify the theme of a night. This enables inheritance of theme styles.

Another way to define styles is by using CSS variables. We use variables in the Shadow DOM to specify the style, for example: background-color: var(–bg-colour, #0F0); If no –bg-colour variable is specified, the default style color #0F0 will be used.

Sometimes we need to use completely custom styles inside the Shadow DOM, such as font styles and font sizes, which can lead to layout confusion if inherited, and specifying styles outside the component every time is a bit cumbersome and breaks component encapsulation. So, Shadow DOM provides an all attribute, as long as you specify :host{all: initial; } resets all inherited attributes.

Demo

There are already many Web Components demos on the Web. This is a Demo I wrote two years ago when I first encountered ES6 and Web Components: github.com/jinliming2/… , a calendar, which was a V0 specification at the time, and had a Bug in Firefox that caused Firefox to crash. The Demo no longer runs in Firefox, as Firefox has removed the V0 specification and started to implement the V1 standard, so I will probably refactor the Demo in the near future.

conclusion

The concept of Web Components was first proposed by Alex Russell at Fronteers Conference 2011, which was very shocking at that time. In 2013, Google launched a Web Components framework called “Polymer” to promote the development of Web Components.

In 2014, Chrome released early versions of the Web Components specification, including Custom Elements V0, Shadow DOM V0, and HTML Imports. However, the specifications were experimental and are no longer recommended and have been replaced by Custom Elements V1 and Shadow DOM V1 standards, while HTML Imports are not standardized and will be replaced by ES6 Modules.

The V0 specification will be marked with a deprecation warning in Chrome 70 and removed from Chrome 73 around March 2019.

The V1 standard is already supported in Chrome 54+ and Safari 10.1+, and is scheduled to be officially supported in Firefox this month (October 2018) (previously supported in Firefox, but disabled by default, needs to be enabled in About :config).

HTML Templates has long been accepted and supported by browsers as an HTML5 feature.

Since Web Components involve many subitems, the screenshots of Can I Use are not shown here. Readers Can search “Web Components” for compatibility, or click here.

Can I Use includes HTML Templates, HTML Imports, Shadow DOM V0, Custom Elements V0, Shadow DOM v1, and Custom Elements V1 The related browser compatibility and notes are very detailed.

Web Components, based on native HTML Components, is not a single technology. It is made up of a set of browser standards defined by the W3C. Building Components in a way that browsers themselves can understand will become the front-end standard of the future.


The text/jinliming2

A salty fish full of curiosity about new things

Sound/fluorspar

Author’s past articles:

  • Quietly lift the veil of secrecy around WebAssembly
  • Dangerous target=”_blank” and opener

This article has been authorized by the author, the copyright belongs to chuangyu front. Welcome to indicate the source of this article. Link to this article: knownsec-fed.com/2018-10-05-…

To subscribe for more sharing from the front line of KnownsecFED development, please search our wechat official account KnownsecFED. Welcome to leave a comment to discuss, we will reply as far as possible.

Thank you for reading.