Before the introduction

This article is from the Push ah front end team Winter97 students, mainly introduces how to use the Webpack build tool to solve some of the development pain points of writing Shadow DOM components. Read for about 5-8 minutes. If there are any errors, please note them in the comments section 👏

preface

Shadow DOM is one of the main technical Components of Web Components, which can attach a hidden, independent DOM to an element [1], combined with Custom Elements. You can create an isolated Web component (hereinafter referred to as a Shadow DOM component) without having to worry about conflict with the rest of the page, which is highly reusable. Its basic concept and basic usage can be learned from the MDN document, and will not be repeated here.

This article mainly discusses the pain points of developing a complex Shadow DOM component, and provides a way to reduce the complexity of writing a Shadow DOM component.

The development of pain points

Using popup info-box🌰 on MDN, you can see that the complexity of Shadow DOM is mainly reflected in the following aspects:

1. Create Shadow DOM structure

The Shadow DOM is created by mounting a Shadow DOM with element.attachShadow () and returning a reference to ShadowRoot. Then add child nodes and set properties for the shadow root node just like normal DOM. AppendChild creates a Shadow DOM structure by simply appendChild the ShadowRoot object:

// Create shadow root
var shadow = this.attachShadow({mode: 'open'});
/ / create a span
var wrapper = document.createElement('span');
wrapper.setAttribute('class'.'wrapper');
var icon = document.createElement('span');
icon.setAttribute('class'.'icon');
icon.setAttribute('tabindex'.0);
var info = document.createElement('span');
info.setAttribute('class'.'info');

// Get the content of the attribute and add it to the info element
var text = this.getAttribute('text');
info.textContent = text;

/ / insert icon
var imgUrl;
if(this.hasAttribute('img')) {
  imgUrl = this.getAttribute('img');
} else {
  imgUrl = 'img/default.png';
}
var img = document.createElement('img');
img.src = imgUrl;
icon.appendChild(img);

// Add the created element to the Shadow DOM
shadow.appendChild(wrapper);
wrapper.appendChild(icon);
wrapper.appendChild(info);
Copy the code

An imperative bulk appendChild operation like this takes a lot of code to write. A better approach is to create a root node and perform only one appendChild operation if it is just a static HTML structure:

const template = document.createElement('template');
template.innerHTML = `    `;

shadow.appendChild(template.content.cloneNode(true));
Copy the code

Using innerHTML and template strings, you can declaratively describe the structure of the Shadow DOM, making it much more readable. But because it’s just a string, the editor doesn’t highlight its syntax, and as DOM structures get more complex, maintainability and readability get worse.

2. Add styles to the Shadow DOM

To style the Shadow DOM, you can create a

// Add some CSS styles to the Shadow DOM
var style = document.createElement('style');

style.textContent = ` .wrapper { position: relative; }.info {font-size: 0.8rem; width: 200px; display: inline-block; border: 1px solid black; padding: 10px; background: white; border-radius: 10px; opacity: 0; The transition: 0.6 s all; position: absolute; bottom: 20px; left: 10px; z-index: 3; } img {width: 1.2rem; } .icon:hover + .info, .icon:focus + .info { opacity: 1; } `;
Copy the code

Then add it to Shadow root again with appendChild:

shadow.appendChild(style);
Copy the code

This is almost identical to the way we created the DOM structure mentioned earlier, with no guarantee of maintainability and readability, and no SUPPORT for a CSS preprocessor, resulting in a significant reduction in development efficiency.

In addition to adding styles to the Shadow DOM with inline

// Add the external referenced styles to the Shadow DOM
const linkElem = document.createElement('link');
linkElem.setAttribute('rel'.'stylesheet');
linkElem.setAttribute('href'.'style.css');

// Add the created element to the Shadow DOM

shadow.appendChild(linkElem);
Copy the code

But because the element does not interrupt the drawing of the Shadow root, there may be unadded Style content (FOUC) when loading the stylesheet, causing a flicker [1]. To some extent, referencing external styles will also cause the performance of Shadow DOM to be dependent on external styles, thus losing the meaning of encapsulation and independence. The in-line

The solution

In order to solve the above pain points, I decided to use Webpack to help build Shadow DOM components. The general idea is to use traditional Web development thinking to separate structure (HTML) and presentation (CSS) and then assemble them into the Shadow DOM components we need. Before the makeover, create a Webpack project:

npm init
npm install --save-dev webpack
npm install --save-dev webpack-cli
Copy the code

1. Use the HTml-loader to separate the Shadow DOM structure

Html-loader is a Webpack loader that exports the HTML as a string, imports it in the entry file, and assigns the value to innerHTML to form the Shadow DOM structure. First, install the dependency:

npm install --save-dev html-loader
Copy the code

The plug-in is then configured in the Webpack configuration file. Because innerHTML can set the descendants of the element represented by the HTML syntax, we don’t need to create a full HTML structure for this, and it’s not even mandatory that the element has a root. Webpack configuration is as follows:

// wepback.config.js
module.exports = {
  mode: 'production'.entry: './src/index.js'.output: {
    filename: 'popup-info.js'
  },
  module: {
    rules: [{test: /\.html$/i,
        loader: 'html-loader'},],}};Copy the code

If mode is production, the HTml-Loader will compress the HTML in production mode. Create index.js and template-html files in the SRC directory as the entry file and Shadow DOM structure description file, respectively.

<! -- template.html -->
<span class="wrapper">
  <span class="icon" tabindex="0"></span>
  <span class="info"></span>
</span>
Copy the code
// index.js
import html from './template.html';

const template = document.createElement('template');
template.innerHTML = html

class PopUpInfo extends HTMLElement {
  constructor() {
    super(a);this.attachShadow({ mode: 'open' });
    this.shadowRoot.appendChild(template.content.cloneNode(true));
  }
}

customElements.define('popup-info', PopUpInfo);
Copy the code

This separates the structure and gives you editor syntax highlighting. Add the package script to the scripts in package.json:

"build": "webpack --config webpack.config.js"
Copy the code

Run NPM run build in the project root directory to get the packaged popup info.js file and import it in the page:

<popup-info></popup-info>
<script src="./dist/popup-info.js"></script>
Copy the code

Check Element:

But this only creates the static structure. For some dynamic behaviors, you still need to write code to control them:

class PopUpInfo extends HTMLElement {
  constructor() {
    super(a);const shadowRoot = this.attachShadow({ mode: 'open' });
    // Get the content of the attribute and add it to the info element
    shadowRoot.appendChild(template.content.cloneNode(true));
    const text = this.getAttribute('text');
    shadowRoot.querySelector('.info').textContent = text;
    / / insert icon
    let imgUrl;
    if (this.hasAttribute('img')) {
      imgUrl = this.getAttribute('img');
    } else {
      imgUrl = 'img/default.png';
    }
    const img = document.createElement('img');
    img.src = imgUrl;
    shadowRoot.querySelector('.icon').appendChild(img); }}Copy the code

Modify the element:

<popup-info img="img/alt.png" text="Your card Validation code (CVC) is an extra security feature -- it is the last 3 or 4 numbers on the back of Your card."></popup-info>
Copy the code

Check Element again:

2. Separate styles and use a CSS preprocessor

Once the structure is done, let’s see how the styles are separated. As mentioned earlier, to style the Shadow DOM, you need to create a style element and fill it with plain CSS text. In this mode, there is no CSS preprocessor and the editor cannot highlight its syntax. In this article, Web Components with Shadow DOM and Sass.[2], we use Sass to write the Shadow DOM component style.

First install raw-Loader and Sass-Loader:

npm install --save-dev raw-loader
npm install --save-dev sass-loader
npm install --save-dev node-sass
Copy the code

Modify webpack.config.js to add rule:

// webpack.config.js
const path = require('path');

module.exports = {
  mode: 'production'.entry: './src/index.js'.output: {
    filename: 'popup-info.js',},module: {
    rules: [{
        test: /\.html$/i,
        loader: 'html-loader'.options: {
          minimize: true.sources: false}}, {test: /\.scss$/,
        use: [
          'raw-loader',
          {
            loader: 'sass-loader'.options: {
              sassOptions: {
                includePaths: [path.resolve(__dirname, 'node_modules')]}}}]}],},};Copy the code

You can then create a separate style file, popup-info.scss:

$img-width: 1.2 rem;

.wrapper {
  position: relative;
}

.info {
  font-size: 0.8 rem;
  width: 200px;
  display: inline-block;
  border: 1px solid black;
  padding: 10px;
  background: white;
  border-radius: 10px;
  opacity: 0;
  transition: 0.6 s all;
  position: absolute;
  bottom: 20px;
  left: 10px;
  z-index: 3;
}

img {
  width: $img-width;
}

.icon:hover+.info..icon:focus+.info {
  opacity: 1;
}
Copy the code

Import in the entry file:

// index.js
import styleText from './popup-info.scss';

const style = document.createElement('style');
style.appendChild(document.createTextNode(styleText));

class PopUpInfo extends HTMLElement {
  constructor() {
    super(a);const shadowRoot = this.attachShadow({ mode: 'open' });
    shadowRoot.appendChild(style);
    // ...}}Copy the code

Check Element again:

<style>Element has been added and only takes effect inside the Shadow DOM. At this point, a Shadow DOM component is written.

thinking

The basic idea behind the above process is to separate the structure and presentation and reassemble them, improving the maintainability of the components to some extent. However, the processing of custom element attributes and adding event listeners to the Shadow DOM still needs to be done in the constructor. And because the DOM structure is separated from the CSS style, the concerns are separated, and the development process needs to switch back and forth between different files. Referring to Vue SFC, a more reasonable component structure would look like this:

<template>.</template>

<script>.<script>

<style>.</style>
Copy the code

To write the Shadow DOM component in this structure, you need to implement a Webpack Loader that can export the Shadow DOM component to meet the requirements. We’ll look into it when we have more energy. 🧑 🏻 💻

reference

[1] Using shadow DOM – Web Components | MDN

[2] Web Components with Shadow DOM and Sass.

Contribution source: juejin.cn/post/697593…