A small attempt at pure native componentization and modularization, using the following new features: Shadown-dom is an encapsulation of HTML tag structure and a component in a real sense. It can ensure that DOM elements in shadow-DOM will not be affected by the outside world, and the inside will not affect the behavior of the outside world, and become an independent module. Custom elements can be used by registering custom tags in the browser, such as
. The tag content is in two forms: 1. Es-module is a kind of modular scheme that is supported by browsers natively. It uses import and export syntax directly in the browser. Import js files as modules. A few are relatively new things, and together they can really make something fun.
shadow-DOM
Imagine a scenario where something like a profile card needs to display an avatar and the user’s name on the page. Your head should be on the left, 100px wide and high, and round; Your name should be on the right with a font size of 16px, vertically centered.
This is a very simple SECTION of CSS. It looks like this:
<style>
.info { display: flex; }
.info-avatar { width: 100px; height: 100px; border-radius: 50%; }
.info-name { display: flex; align-items: center; font-size: 16px; }
</style>
<div class="info">
<img class="info-avatar" src="https://avatars1.githubusercontent.com/u/9568094?v=4" />
<p class="info-name">Jarvis</p>
</div>
Copy the code
At this point, we have completed the requirements, there is nothing wrong with everything, but a very real problem. There’s no such thing as a simple page, even if it’s as simple as Google’s home page, which uses around 400 DOM elements. It is difficult to guarantee that CSS and JS in other resource files will not affect the DOM above. For example, if you have a main. CSS file with a line: p {color: red; }, this CSS will affect the.info-name element we wrote above, causing the text color to turn red.
This problem often occurs in pages that require a third-party plugin. It is possible that the CSS provided by the plugin will affect your DOM elements, and it is also possible that your CSS will affect the DOM in the plugin.
There is a simple way to solve this problem, that is, All with! Important, use shadow-dom.
There are some examples of shadow-dom in the browser today:
<video>
<audio>
- even
<input>
These elements are built using shadow-DOM in Chrome, but are not visible in developer tools by default.
To enable shadow-dom, use Chrome DevTools – Settings -> Default Preferences panel to find Elements -> click on Show user agent shadow DOM
At this point, you can see the actual structure of shadow-DOM using the developer tools.
A feature of shadow-DOM is that all DOM elements in a shadow are not affected by external code, which is why the UI of video and Audio is so difficult to customize.
The basic grammar
The creation of the shadow-dom must be done using JavaScript, and we need to have a real element in the document to hang in the shadow-dom, also known as host. In addition, the creation process can add, delete, and change child elements like a normal DOM tree.
let $tag = document.querySelector('XXX') // The real element to mount
let shadow = $tag.attachShadow({ mode: 'open' }) // Mount the shadow-dom element and get its root element
Copy the code
The mode parameter in attachShadow has two valid values, open and closed, which specify the encapsulation mode of a shadow-DOM structure.
When the value is open, we can get the shadow-dom from the real element used when we mount it.
$tag.shadowRoot; // shadow-dom root element
Copy the code
If the value is closed, it indicates that the outer layer cannot obtain shadow-dom.
$tag.shadowRoot; // null
Copy the code
The subsequent operations are the same as normal DOM operations, with the various appends, removers, and innerHTML available.
let $shadow = $tag.attachShadow({ mode: 'open' })
let $img = document.createElement('img')
$shadow.appendChild($img) // Add an img tag to shadow-dom
$shadow.removeChild($img) // Remove the img tag from shadow-dom
$img.addEventListener('click', _ = >console.log('click on img'))
$shadow.innerHTML = `
Some Text
`
Copy the code
It is important to note that shadow-DOM itself is not an actual tag and does not have the ability to define CSS. But it is possible to bind events
$shadow.appendChild('<p></p>') // Pretend to add a label
$shadow.appendChild('<p></p>') // Pretend to add a label
// The resulting structure is
// < outer container >
//
//
//
// There are no class-related attributes
$shadow.classList // undefined
$shadow.className // undefined
$shadow.style // undefined
// Binding events is fine
$shadow.addEventListener('click'.console.log)
Copy the code
Shadow-dom will also have CSS property inheritance, rather than completely ignoring all the outer CSS
<style>
body {
font-size: 16px; The /* attribute is inherited by the.text element
}
.host {
color: red; /* is also inherited by the.text element */
}
.text {
color: green; /* Setting elements directly within a shadow is invalid */
}
p {
font-size: 24px; /* Settings for p tags are not applied to.text */
}
/* If you set Up Flex for the outer layer, the inner elements will be applied directly (but to keep the outer elements non-invasive, it is recommended to create a container DOM internally) */
.host {
display: flex;
}
.text {
flex: 1;
}
</style>
<div class="host">
#shadow
<p class="text">Text</p>
<p class="text">Text</p>
#shadow
</div>
Copy the code
So, in the case of shadow-DOM, CSS blocks only those rules that directly hit the inner element. For example, I wrote a * {color: red; }, this rule will definitely apply, because * represents all, and shadow-dom is actually inherited from the outer host element color: red, rather than directly matching its own rule.
Simple little example
We used shadow-dom to modify the above data card.
Online Demo source address
<div id="info"></div>
<script>
let $info = document.querySelector('#info') // host
let $shadow = $info.attachShadow({mode: 'open'})
let $style = document.createElement('style')
let $wrap = document.createElement('div')
let $avatar = document.createElement('img')
let $name = document.createElement('p')
$style.textContent = ` .info { display: flex; } .info-avatar { width: 100px; height: 100px; border-radius: 50%; } .info-name { display: flex; align-items: center; font-size: 16px; } `
$wrap.className = 'info'
$avatar.className = 'info-avatar'
$name.className = 'info-name'
$avatar.src = 'https://avatars1.githubusercontent.com/u/9568094?v=4'
$name.innerHTML = 'Jarvis'
$wrap.appendChild($avatar)
$wrap.appendChild($name)
$shadow.appendChild($style)
$shadow.appendChild($wrap)
</script>
Copy the code
P.S. CSS inside the shadow-dom has no impact on the outside world, so you can use shadow-dom to name classes without worrying about collisions.
If we now want to display multiple users’ avatars and names in a page, we can wrap the above code and put operations like className and appendChild in a function like this:
Online Demo source address
function initShadow($host, { isOpen, avatar, name }) {
let $shadow = $host.attachShadow({ mode: isOpen ? 'open' : 'closed' });
/ /... Omit various operations
$avatar.src = avatar
$name.innerHTML = name
}
initShadow(document.querySelector('#info1'), {
avatar: 'https://avatars1.githubusercontent.com/u/9568094?v=4'.name: 'Jarvis'
});
initShadow(document.querySelector('#info2'), {
isOpen: true.avatar: 'https://avatars1.githubusercontent.com/u/9568094?v=4'.name: 'Jarvis'
})
Copy the code
This gives you a simple component that you can simply pass in a mounted DOM wherever you need it.
custom-elements
As with shadow-dom, the details of the component are no longer visible in the document tree, and its structure is not affected by any code (except for root operations in open mode). However, there is a root element in the document to hang in the shadow-DOM, which is still a normal HTML tag. If you have a large page with N similar components, search and find all
, the experience will be painful and almost semantically meaningless. And when we want to use this component, we have to call JavaScript in addition to get the DOM element to generate the corresponding shadow-DOM. So, we can try using custom-Elements to register our own unique tags. Simply call the custom component with
.
Custom Elements supports both normal label encapsulation and shadow-DOM encapsulation, but they cannot coexist.
The basic grammar
First we need to have a class that inherits HTMLElement. You then need to register it with the current environment.
class Info extends HTMLElement {}
customElements.define(
'cus-info'./ / tag name
Info // The corresponding constructor for the tag
)
Copy the code
An optional third parameter when you call DEFINE sets the custom tag to inherit from a native tag. There are slight differences in the subsequent use of labels:
<! Extends: 'p'} -->
<p is="cus-info" />
<script>
document.createElement('p', { is: 'cus-info' })
</script>
<! -- extends is not set -->
<info />
<script>
document.createElement('cus-info') // Must contain a '-'
</script>
Copy the code
P.S. custom tags should have at least one registration – depending on the scenario, use extends is not recommended for individuals because it looks more comfortable
Common label mode
If you encapsulate a common set of tags, you solve the problem that some components of the same function need to be pasted around the page.
Online Demo source address
<cus-info>
<p>native text</p>
<! -- The default is directly nested, unless removed from custom components -->
</cus-info>
<script>
class CusInfo extends HTMLElement {
constructor() {
super(a)let $text = document.createElement('p')
$text.innerHTML = 'Hello custom-elements.'
this.appendChild($text) // this represents the current instance of the custom element
}
}
customElements.define('cus-info', CusInfo)
</script>
Copy the code
Achieve something like this:
How to use shadow-dom
P.S. When an element is shadow-DOM enabled, normal child elements are invisible, but can still be retrieved using the DOM API
Online Demo source address
<cus-info>
<p>native text</p>
<! -- The default is directly nested, unless removed from custom components -->
</cus-info>
<script>
class CusInfo extends HTMLElement {
constructor() {
super(a)let $shadow = this.attachShadow({ mode: 'open' })
let $text = document.createElement('p')
$text.innerHTML = 'Hello custom-elements.'
$shadow.appendChild($text)
}
}
customElements.define('cus-info', CusInfo)
console.log(document.querySelector('cus-info').children[0].innerHTML) // native text
</script>
Copy the code
Life cycle function
Custom tags aren’t just a way to give you an extra tag to use. There are several lifecycle functions that can be set for registered custom tags. Currently, the valid events are:
connectedCallback
Triggered when the tag is added to the document flowdisconnectedCallback
Triggered when a label is removed from the document streamadoptedCallback
Triggered when the tag is moved,There seems to be no existing API that can trigger this event, because likeappendChild
orinsertBefore
In this category, existing DOM elements are removed and then added, so there is no direct movement behaviorattributeChangedCallback
Triggered when adding, deleting, or modifying element attributesObservedAttributes need to be set in advance to listen for attribute changes
A simple example of firing various events:
Online Demo source address
<div id="wrap">
<div id="content"></div>
</div>
<script>
class CusTag extends HTMLElement {
static get observedAttributes() { return ['id']}// Sets which property changes to listen for
connectedCallback () { console.log('DOM is added to document ') }
disconnectedCallback () { console.log('DOM removed from document ') }
adoptedCallback () { console.log('DOM is moved ') }
attributeChangedCallback () { console.log('DOM property changed ') }
}
customElements.define('cus-tag', CusTag)
let $wrap = document.querySelector('#wrap')
let $content = document.querySelector('#content')
let $tag = document.createElement('cus-tag')
$wrap.appendChild($tag)
$content.appendChild($tag)
$tag.setAttribute('id'.'tag-id')
$tag.setAttribute('id'.'tag-id2')
$tag.removeAttribute('id')
$content.removeChild($tag)
</script>
Copy the code
P.S. If you need to handle DOM structures and bind events, it is recommended that you execute the connectedCallback callback in order for the attributeChangedCallback to work, you must set observedAttributes to return which attribute changes the tag needs to listen for
Encapsulate data card components with custom tags
The next step is to complete a simple encapsulation of the data card using CUSTOMe-Elements combined with shadow-DOM. Because the shadow-dom version of the component is relatively independent, the shadow-DOM approach is used here. The general code is as follows:
Online Demo source address
<info-card name="Jarvis" avatar="https://avatars1.githubusercontent.com/u/9568094?v=4" />
<! -- P.S. this triggers a hidden bug in Chrome67 -->
<script>
class InfoCard extends HTMLElement {
connectedCallback () {
// The safe way to render is to make sure the tag has been added to the DOM
let avatar = this.getAttribute('avatar')
let name = this.getAttribute('name')
initShadow(this, { avatar, name })
}
}
customElements.define('info-card', InfoCard)
</script>
Copy the code
The call to initShadow above simply changes the source of the Avatar and name fields. Now we need to use the wrapped data card in the page, just register a custom tag and write the corresponding tag code in the HTML
Imagine again
This is one of the things that is particularly friendly to server-side template rendering, since it is done by registering HTML tags. If you have a page rendered using the server, you might dynamically concatenate some DOM elements into the return value of the request. In order to apply some style, you may need to add various classnames to the template, and it is quite possible that a shake of the hand will result in the tag not closing, the structure will be wrong, or some properties will be misspelled, etc. For example, insert some form elements, which might start with code like this:
router.get('/', ctx => {
ctx.body = `
`
})
Copy the code
The memory cost on the Server side is also significantly reduced when custom-Elements are used. The Server only needs to indicate that there is a form element, leaving it up to the front end to render it.
router.get('/', ctx => {
ctx.body = `
`
})
Copy the code
custom-events
If you use a lot of custom components on your page, you’re bound to run into communication issues between components. For example, how I can trigger the behavior of other components when I click a button. Since it is a pure native version, it naturally supports addEventListener. We can directly use custom-Events to complete communication between components.
The basic grammar
The only difference between using custom events and native DOM events is that you need to build your own Event instance and fire the Event:
document.body.addEventListener('ping', _ = >console.log('pong')) // Set the event listener
document.body.dispatchEvent(new Event('ping')) // Trigger the event
Copy the code
Use in custom components
Now there are two components in the page, a container, which contains a text box and several buttons. After clicking the button, the corresponding text of the button will be output to the text box:
Online Demo source address
<cus-list>
<input id="output" />
<cus-btn data-text="Button 1"></cus-btn>
<cus-btn data-text="Button 2"></cus-btn>
<cus-btn data-text="Button 3"></cus-btn>
</cus-list>
<script>
class CusList extends HTMLElement {
connectedCallback() {
let $output = this.querySelector('#output')
Array.from(this.children).forEach(item= > {
if (item.tagName.toLowerCase() === 'cus-btn') {
item.addEventListener('check', event => { // Register listeners for custom events
$output.value = event.target.innerText
})
}
})
}
}
class CusBtn extends HTMLElement {
connectedCallback() {
let { text } = this.dataset
let $text = document.createElement('p')
$text.innerHTML = text
$text.addEventListener('click', _ = > {this.dispatchEvent(new Event('check')) // Triggers a custom event
})
this.appendChild($text)
}
}
customElements.define('cus-list', CusList)
customElements.define('cus-btn', CusBtn)
</script>
Copy the code
Loop through a List of children and then bind events in sequence. This is inefficient and inflexible. If there are new child elements, the corresponding event cannot be fired. So, we can simplify the above code by enabling bubbling of events:
Online Demo source address
class CusList extends HTMLElement {
connectedCallback() {
let $output = this.querySelector('#output')
this.addEventListener('check', event => { // Register listeners for custom events
$output.value = event.target.innerText // The effect is the same because event.target is the DOM object that triggers a dispatchEvent}}})class CusBtn extends HTMLElement {
connectedCallback() {
let { text } = this.dataset
let $text = document.createElement('p')
$text.innerHTML = text
$text.addEventListener('click', _ = > {this.dispatchEvent(new Event('check'), {
bubbles: true // Enable event bubbling
}) // Triggers a custom event
})
this.appendChild($text)
}
}
Copy the code
ES-module
Es-module is an implementation of native modularity. Using ES-Module makes it easier to call the above components. So, without going over some module-related basics, just move the wrapped component code into a JS file, and then reference the corresponding JS file in the page to complete the call.
Online Demo source address
module.js
export default class InfoCard extends HTMLElement { }
customElements.define('info-card', InfoCard)
Copy the code
index.html
<info-card name="Jarvis" avatar="https://avatars1.githubusercontent.com/u/9568094?v=4"></info-card>
<script type="module" src="./cus-elements-info-card.js"></script>
Copy the code
At first glance, this looks like a normal JS script introduction. It really makes no difference to write this component alone.
But a real page would not have just one component. Suppose there was a page that contained three components:
<cus-tab>
<cus-list>
<cus-card />
<cus-card />
</cus-list>
<cus-list>
<cus-card />
<cus-card />
</cus-list>
</cus-tab>
Copy the code
We want to make sure that card is loaded when we use list, and we want to make sure that list is loaded when we use TAB. The easiest way to do this is to wait until all the resources have loaded before executing the code, which is what the mainstream Webpack packaging does. The consequence of this, however, is that you have to wait for the outer tabs to load before executing your code, even though you can handle your own logic and register custom tags once the list and card are loaded. This is an obvious problem with frameworks like React and Vue that use Webpack to package js files that are hundreds of KB or even megabytes too large. You need to wait until the file is all downloaded before you start running the code and building the page.
We can take advantage of the gap between downloading other components to perform some of the logic of the current component that we can’t do with a packaging tool like Webpack. This is obviously a waste of time, and es-Module has taken care of this. The Module code executes on the basis that all dependencies have been loaded.
When card and List are loaded, list will start executing the code. At this point, the TAB might still be loading, and by the time the TAB is loaded and executed, the list is already registered with the document, waiting to be called, which to some extent eliminates the problem of too centralized code execution. Maybe the previous page load was 200ms downloading files, 50ms building components, and 50ms rendering the page * (the numbers are bullshit, just for example) *. Some components are lightweight and may download files in 20ms. If it does not rely on other modules, it will execute some of its own component code, generate constructors, and register custom components into the document while the browser is downloading other modules. So there are two parallel lines, with some code execution time overlapping with network request time.
Take a real life example: you run a restaurant and employ three cooks: one cooks scrambled eggs with tomatoes, one cooks tofu with preserved eggs, and one cooks smashed cucumbers. Space is limited, so the three cooks share a set of cooking utensils. (Single thread) Today is the first day of opening. At this time, guests came and ordered these three dishes, but the dishes are still on the way. Webpack: “tomato, egg, preserved egg, bean curd and cucumber” are all put together and sent to you. After being sent to you, three chefs cook in turn and then bring them to the guests. Es-module: Distribute the dishes, cook the dishes first, and serve the dishes to the guests first.
A simple example of component nesting
Online Demo source address
cus-elements-info-list.js
import InfoCard from './cus-elements-info-card.js'
export default class InfoList extends HTMLElement {
connectedCallback() {
// load data
let data = [
{
avatar: 'https://avatars1.githubusercontent.com/u/9568094?v=4'.name: 'Jarvis'
},
{
avatar: 'https://avatars1.githubusercontent.com/u/9568094?v=4'.name: 'Jarvis'
},
{
avatar: 'https://avatars1.githubusercontent.com/u/9568094?v=4'.name: 'Jarvis'}]// laod data end
initShadow(this, { data })
}
}
function initShadow($host, { data, isOpen }) {
let $shadow = $host.attachShadow({ mode: isOpen ? 'open' : 'closed' })
let $style = document.createElement('style')
let $wrap = document.createElement('div')
$style.textContent = ` .list { display: flex; flex-direction: column; } `
$wrap.className = 'list'
// loop create
data.forEach(item= > {
let $item = new InfoCard()
$item.setAttribute('avatar', item.avatar)
$item.setAttribute('name', item.name)
$wrap.appendChild($item)
})
$shadow.appendChild($style)
$shadow.appendChild($wrap)
}
customElements.define('info-list', InfoList)
Copy the code
<info-list></info-list>
<script type="module" src="./cus-elements-info-list.js"></script>
Copy the code
The new Component works like document.createElement and can be used without knowing the Component’s registration name
conclusion
A few tips
shadow-DOM
Cannot coexist with normal child elements, setattachShadow
This later causes normal child elements to be invisible on the page, but the DOM remainscustom-elements
The registration name of the-
custom-elements
theconstructor
Function firing is not guaranteedDOM
Having rendered correctly, the DOM operations should be placed inconnectedCallback
In thecustom-elements
Component property change monitoring needs to be configured in advanceobservedAttributes
, there are no wildcard operationsES-module
Related operations can only be performed intype="module"
In theES-module
The references are shared, even though ten files areimport
The same JS file, they get the same object, do not worry about wasting network resources
A simple todo-list implementation:
Online Demo source address
Native browser support is getting richer and richer, with Es-Modules, custom-elements, shadow-dom and all sorts of new stuff; Componentization and modularization of the Web native, looking forward to the day when it will become commonplace, just as it is now possible to use QSA and FETCH without considering whether oR not jQuery is needed to help with compatibility (most of the time).
The resources
- shadow-DOM | MDN
- custom-elements | MDN
- custom-events | MDN
- ES-module | MDN
Warehouse addresses for all examples in this article
The warehouse address