Componentization is an important direction of front-end development, which improves development efficiency on the one hand and reduces maintenance cost on the other. Mainstream vue. js, React and its extensions Ant Design, UniApp, Taro, etc., are component frameworks. Web Components is an umbrella term for a set of Web native apis that allow us to create reusable custom Components and use them in our Web applications as if they were native HTML tags. Many front-end frameworks/libraries already support Web Components.

This article will review the Web Components core API and implement a business component library based on the Web Components API from 0 to 1.

End result: blog.pingan8787.com/exe-compone… Warehouse address: github.com/pingan8787/…

Review Web Components

In the history of front-end development, from the beginning of repeated business copying the same code everywhere, to the advent of Web Components, we use native HTML tag custom Components, reuse component code, improve development efficiency. Components created through Web Components can be used in almost any front-end framework.

1. Core API review

Web Components consists of three core apis:

  • Custom elements: labels that allow you to define Custom elements and their behavior and provide components externally;
  • Shadow DOM: Encapsulates the internal structure of a component to avoid external conflicts.
  • HTML TemplatesIncluding:<template>and<slot>Element, which allows you to define HTML templates for various components that can then be reused elsewhere. Those of you who have used frameworks such as Vue/React will be familiar with this.

There are also HTML imports, which are deprecated so I won’t go into details, but are used to control dependency loading of components.

2. Getting started

Let’s take a quick look at creating a simple Web Components with the following simple example.

  • Using the component
<! DOCTYPEhtml>
<html lang="en">
<head>
    <script src="./index.js" defer></script>
</head>
<body>
    <h1>custom-element-start</h1>
    <custom-element-start></custom-element-start>
</body>
</html>
Copy the code
  • Define the components
/ * * * use CustomElementRegistry. Define () method is used to register a custom element * parameters are as follows: * - Element name, which complies with DOMString specification, cannot be a single word and must be separated by a dash * - element behavior, must be a class * - Inherited element, optional configuration, a configuration object containing the extends property, specifying which built-in element the element created inherits from, You can inherit any built-in element. * /

class CustomElementStart extends HTMLElement {
    constructor(){
        super(a);this.render();
    }
    render(){
        const shadow = this.attachShadow({mode: 'open'});
        const text = document.createElement("span");
        text.textContent = 'Hi Custom Element! ';
        text.style = 'color: red';
        shadow.append(text);
    }
}

customElements.define('custom-element-start', CustomElementStart)
Copy the code

The code above does three things:

  1. Implementing component classes

The component is defined by implementing the CustomElementStart class.

  1. Define the components

Define the component using the customElements. Define method, taking its label and component class as parameters.

  1. Using the component

After importing the component, use the custom component < custom-elemental-start >
as you would with normal HTML tags.

Subsequent browser accessindex.htmlYou can see the following:

3. Compatibility

In the MDN | Web Components section introduces the compatibility conditions:

  • Firefox(version 63), Chrome, and Opera all support Web components by default.
  • Safari supports many Web component features, but fewer than those mentioned above.
  • Edge is developing an implementation.

For compatibility, look at the following figure:Photo source:www.webcomponents.org/

There are many excellent projects to learn about Web Components on this site.

4. Summary

This section focuses on a brief overview of the basics through a simple example. You can read the documentation for details:

  • Use the custom elements
  • The use of shadow DOM
  • Use templates and slots

Analysis and design of EXe-Components library

1. Background

Suppose we need to implement an exe-Components library. The Components of the library fall into two categories:

  1. Types of components

Mainly general simple components, such as exe-Avatar avatar component, exe-button button component, etc.

  1. Types of modules

The components are mainly complex and composite components, such as exe-user-Avatar user profile picture component (including user information) and exe-attachment-list attachment list component.

Details can be seen below:

Next, we will design and develop the EXe-Components library based on the above figure.

2. Component library design

When designing a component library, consider the following:

  1. Component naming, parameter naming and other specifications, convenient component maintenance;
  2. Component parameter definition;
  3. Component style isolation;

Of course, these are the most basic points to consider, and as the real business gets more complex, there are more to consider, such as engineering dependencies, component decoupling, component themes, and so on.

For the three points mentioned above, here are some naming conventions:

  1. Component name withExe - Function nameName, as inexe-avatarRepresents the head component;
  2. Attribute parameter nameE - Parameter nameName, as ine-srcsaidsrcAddress attribute;
  3. The event parameter name starts withOn - Event typeName, as inon-clickRepresents the click event;

3. Component library Component design

Here we mainly designexe-avatarexe-buttonexe-user-avatarThree components, the first two are simple components, the latter is a complex component, which uses the first two components for internal composition. Here we define the properties supported by these three components:

The naming of attributes looks complicated, so you can name them according to your own and your team’s habits.

This way we can think a lot clearer, implement the corresponding components.

3, exe-Components component library preparation

The examples in this article will eventually apply to the implemented componentsUse a combination of, implement the following”List of users“The effect:Experience Address:Blog.pingan8787.com/exe-compone…

1. Unified development specifications

First of all, we will unify the development specifications, including:

  1. Directory specification

  1. Defining component specifications

  1. Component development templates

Component development templates include index.js component entry file and template.js component HTML template file:

/ / index. Js template
const defaultConfig = {
    // The component is configured by default
}

const Selector = "exe-avatar"; // Component label name

export default class EXEAvatar extends HTMLElement {
    shadowRoot = null;
    config = defaultConfig;

    constructor(){
        super(a);this.render(); // Handle component initialization logic uniformly
    }

    render() {
        this.shadowRoot = this.attachShadow({mode: 'closed'});
        this.shadowRoot.innerHTML = renderTemplate(this.config); }}// Define the component
if(! customElements.get(Selector)) { customElements.define(Selector, EXEAvatar) }Copy the code
/ / template. Js template

export default config => {
    // Read the configuration uniformly
    const { avatarWidth, avatarRadius, avatarSrc } = config;
    return ` < style > / * * / CSS content < / style > < div class = "exe - avatar" > / * * / HTML content < / div > `
}
Copy the code

2. Development environment construction and engineering treatment

To facilitate the use of the exe-Components library, and closer to the actual use of the component library, we need to package the component library into a UMD type JS file. Here we use rollup to build and package the exe-components.js file as follows:

<script src="./exe-components.js"></script>
Copy the code

Generate package.json file via NPM init-y and install rollup and HTTP-server globally:

npm init -y
npm install --global rollup http-server
Copy the code

Then add the “dev” and “build” scripts under package.json:

{
	// ...
  "scripts": {
    "dev": "http-server -c-1 -p 1400"."build": "rollup index.js --file exe-components.js --format iife"}},Copy the code

Among them:

  • "dev"Command: Start the static server using http-server as the development environment. add-c-1This parameter is used to disable caching to avoid a page refreshThe HTTP server document;
  • "build"Command: output index.js as rollup package entry fileexe-components.jsFile, and the file is of iIFE type.

This completes the simple local development and engineered configuration of component library builds, and development is ready.

Fourth, exe-Components component library development

1. Configure the component library entry file

In front of thepackage.jsonConfigured in the file"build"Command, will use the root directoryindex.jsAs an entry file, and to facilitate the introduction of the components common base component and modules common complex component, we create threeindex.js, the directory structure is as follows:The contents of the three entry files are as follows:

// EXE-Components/index.js
import './components/index.js';
import './modules/index.js';

// EXE-Components/components/index.js
import './exe-avatar/index.js';
import './exe-button/index.js';

// EXE-Components/modules/index.js
import './exe-attachment-list/index.js.js';
import './exe-comment-footer/index.js.js';
import './exe-post-list/index.js.js';
import './exe-user-avatar/index.js';
Copy the code

2. Develop the exe-Avatar component index.js file

Based on the previous analysis, we know that exe-Avatar components need to support the following parameters:

  • E – avatar – SRC: avatar picture address, for example: / testAssets/images/avatars – 1. The PNG
  • E-avatar-width: indicates the width of the avatar. The default value is the same as the height, for example, 52px
  • E-button-radius: rounded head, for example, 22px. Default: 50%
  • On-avatar-click: indicates the event that an avatar clicks. The default value is none

Next, use the previous template to develop the entry file index.js:

// EXE-Components/components/exe-avatar/index.js
import renderTemplate from './template.js';
import { Shared, Utils } from '.. /.. /utils/index.js';

const { getAttributes } = Shared;
const { isStr, runFun } = Utils;

const defaultConfig = {
    avatarWidth: "40px".avatarRadius: "50%".avatarSrc: "./assets/images/default_avatar.png".onAvatarClick: null,}const Selector = "exe-avatar";

export default class EXEAvatar extends HTMLElement {
    shadowRoot = null;
    config = defaultConfig;

    constructor(){
        super(a);this.render();
    }

    render() {
        this.shadowRoot = this.attachShadow({mode: 'closed'});
        this.shadowRoot.innerHTML = renderTemplate(this.config);// Generate HTML template content
    }

		// Lifecycle: Called when a Custom Element is inserted into the document DOM for the first time.
    connectedCallback() {
        this.updateStyle();
        this.initEventListen();
    }

    updateStyle() {
        this.config = {... defaultConfig, ... getAttributes(this)};
        this.shadowRoot.innerHTML = renderTemplate(this.config); // Generate HTML template content
    }

    initEventListen() {
        const { onAvatarClick } = this.config;
        if(isStr(onAvatarClick)){ // Check whether it is a string
            this.addEventListener('click'.e= >runFun(e, onAvatarClick)); }}}if(! customElements.get(Selector)) { customElements.define(Selector, EXEAvatar) }Copy the code

Several methods are extracted from the public method, about its role, specific can see the source code:

  • renderTemplatemethods

From the template.js exposed method, pass in config to generate the HTML template.

  • getAttributesmethods

Pass in an HTMLElement element and return all attribute key pairs on the element. Attributes beginning with e- and on- are treated as normal and event attributes, respectively, as shown in the following example:

// input
<exe-avatar
    e-avatar-src="./testAssets/images/avatar-1.png"
    e-avatar-width="52px"
    e-avatar-radius="22px"
    on-avatar-click="avatarClick()"
></exe-avatar>
  
// output
{
  avatarSrc: "./testAssets/images/avatar-1.png".avatarWidth: "52px".avatarRadius: "22px".avatarClick: "avatarClick()"
}
Copy the code
  • runFunmethods

Since the method passed in through the property is a string, we wrap it, pass the event and event name as arguments, call the method, and the example executes the avatarClick() method as in the previous step.

Also, the Web Components lifecycle can be documented in detail: use lifecycle callback functions.

3. Develop the exe-Avatar component template.js file

This file exposes a method that returns the component’s HTML template:

// EXE-Components/components/exe-avatar/template.js
export default config => {
  const { avatarWidth, avatarRadius, avatarSrc } = config;
  return `
    <style>
      .exe-avatar {
        width: ${avatarWidth};
        height: ${avatarWidth};
        display: inline-block;
        cursor: pointer;
      }
      .exe-avatar .img {
        width: 100%;
        height: 100%;
        border-radius: ${avatarRadius};
        border: 1px solid #efe7e7;
      }
    </style>
    <div class="exe-avatar">
      <img class="img" src="${avatarSrc}" />
    </div>
  `
}
Copy the code

The final result is as follows:

Having developed our first component, we can briefly summarize the steps to create and use it:

4. Develop exe-button components

According to the development idea of exe-Avatar component, exe-button component can be realized quickly. The following parameters need to be supported:

  • E-button-radius: rounded button corners, for example, 8px
  • E-button-type: indicates the button type, for example, default, primary, text, and dashed module
  • E-button-text: indicates the text of the button. Default: open
  • On-button-click: indicates the button click event. None by default
// EXE-Components/components/exe-button/index.js
import renderTemplate from './template.js';
import { Shared, Utils } from '.. /.. /utils/index.js';

const { getAttributes } = Shared;
const { isStr, runFun } = Utils;
const defaultConfig = {
    buttonRadius: "6px".buttonPrimary: "default".buttonText: "Open".disableButton: false.onButtonClick: null,}const Selector = "exe-button";

export default class EXEButton extends HTMLElement {
    // Specify the observed attribute change, and attributeChangedCallback takes effect
    static get observedAttributes() { 
        return ['e-button-type'.'e-button-text'.'buttonType'.'buttonText']
    }

    shadowRoot = null;
    config = defaultConfig;

    constructor(){
        super(a);this.render();
    }

    render() {
        this.shadowRoot = this.attachShadow({mode: 'closed'});
    }

    connectedCallback() {
        this.updateStyle();
        this.initEventListen();
    }

    attributeChangedCallback (name, oldValue, newValue) {
        // console.log(' property change ', name)
    }

    updateStyle() {
        this.config = {... defaultConfig, ... getAttributes(this)};
        this.shadowRoot.innerHTML = renderTemplate(this.config);
    }

    initEventListen() {
        const { onButtonClick } = this.config;
        if(isStr(onButtonClick)){
            const canClick = !this.disabled && !this.loading
            this.addEventListener('click'.e= > canClick && runFun(e, onButtonClick));
        }
    }

    get disabled () {
        return this.getAttribute('disabled')! = =null;
    }

    get type () {
        return this.getAttribute('type')! = =null;
    }

    get loading () {
        return this.getAttribute('loading')! = =null; }}if(! customElements.get(Selector)) { customElements.define(Selector, EXEButton) }Copy the code

The template definition is as follows:

// EXE-Components/components/exe-button/tempalte.js
// Button border type
const borderStyle = { solid: 'solid'.dashed: 'dashed' };

// Button type
const buttonTypeMap = {
    default: { textColor: '# 222'.bgColor: '#FFF'.borderColor: '# 222'},
    primary: { textColor: '#FFF'.bgColor: '#5FCE79'.borderColor: '#5FCE79'},
    text: { textColor: '# 222'.bgColor: '#FFF'.borderColor: '#FFF'}},export default config => {
    const { buttonRadius, buttonText, buttonType } = config;

    const borderStyleCSS = buttonType 
        && borderStyle[buttonType] 
        ? borderStyle[buttonType] 
        : borderStyle['solid'];

    const backgroundCSS = buttonType 
        && buttonTypeMap[buttonType] 
        ? buttonTypeMap[buttonType] 
        : buttonTypeMap['default'];

    return `
        <style>
            .exe-button {
                border: 1px ${borderStyleCSS} ${backgroundCSS.borderColor};
                color: ${backgroundCSS.textColor};
                background-color: ${backgroundCSS.bgColor};
                font-size: 12px;
                text-align: center;
                padding: 4px 10px;
                border-radius: ${buttonRadius};
                cursor: pointer;
                display: inline-block;
                height: 28px;
            }
            :host([disabled]) .exe-button{ 
                cursor: not-allowed; 
                pointer-events: all; 
                border: 1px solid #D6D6D6;
                color: #ABABAB;
                background-color: #EEE;
            }
            :host([loading]) .exe-button{ 
                cursor: not-allowed; 
                pointer-events: all; 
                border: 1px solid #D6D6D6;
                color: #ABABAB;
                background-color: #F9F9F9;
            }
        </style>
        <button class="exe-button">${buttonText}</button>
    `
}
Copy the code

The final effect is as follows:

5. Develop exe-User-Avatar

This component combines the exe-Avatar and exe-button components to support click events as well as slot slot functions. Because it is to do a combination, so the development is relatively simple ~ first look at the entry file:

// EXE-Components/modules/exe-user-avatar/index.js

import renderTemplate from './template.js';
import { Shared, Utils } from '.. /.. /utils/index.js';

const { getAttributes } = Shared;
const { isStr, runFun } = Utils;

const defaultConfig = {
    userName: "".subName: "".disableButton: false.onAvatarClick: null.onButtonClick: null,}export default class EXEUserAvatar extends HTMLElement {
    shadowRoot = null;
    config = defaultConfig;

    constructor() {
        super(a);this.render();
    }

    render() {
        this.shadowRoot = this.attachShadow({mode: 'open'});
    }

    connectedCallback() {
        this.updateStyle();
        this.initEventListen();
    }

    initEventListen() {
        const { onAvatarClick } = this.config;
        if(isStr(onAvatarClick)){
            this.addEventListener('click'.e= >runFun(e, onAvatarClick)); }}updateStyle() {
        this.config = {... defaultConfig, ... getAttributes(this)};
        this.shadowRoot.innerHTML = renderTemplate(this.config); }}if(! customElements.get('exe-user-avatar')) {
    customElements.define('exe-user-avatar', EXEUserAvatar)
}
Copy the code

The main content is in template.js:

// EXE-Components/modules/exe-user-avatar/template.js

import { Shared } from '.. /.. /utils/index.js';

const { renderAttrStr } = Shared;

export default config => {
    const { 
        userName, avatarWidth, avatarRadius, buttonRadius, 
        avatarSrc, buttonType = 'primary', subName, buttonText, disableButton,
        onAvatarClick, onButtonClick
    } = config;
    return `
        <style>
            :host{
                color: "green";
                font-size: "30px";
            }
            .exe-user-avatar {
                display: flex;
                margin: 4px 0;
            }
            .exe-user-avatar-text {
                font-size: 14px;
                flex: 1;
            }
            .exe-user-avatar-text .text {
                color: #666;
            }
            .exe-user-avatar-text .text span {
                display: -webkit-box;
                -webkit-box-orient: vertical;
                -webkit-line-clamp: 1;
                overflow: hidden;
            }
            exe-avatar {
                margin-right: 12px;
                width: ${avatarWidth};
            }
            exe-button {
                width: 60px;
                display: flex;
                justify-content: end;
            }
        </style>
        <div class="exe-user-avatar">
            <exe-avatar
                ${renderAttrStr({
                    'e-avatar-width': avatarWidth,
                    'e-avatar-radius': avatarRadius,
                    'e-avatar-src': avatarSrc,
                })}
            ></exe-avatar>
            <div class="exe-user-avatar-text">
                <div class="name">
                    <span class="name-text">${userName}</span>
                    <span class="user-attach">
                        <slot name="name-slot"></slot>
                    </span>
                </div>
                <div class="text">
                    <span class="name">${subName}<slot name="sub-name-slot"></slot></span>
                </div>
            </div>
            The ${! disableButton &&`<exe-button
                    ${renderAttrStr({
                        'e-button-radius' : buttonRadius,
                        'e-button-type' : buttonType,
                        'e-button-text' : buttonText,
                        'on-avatar-click' : onAvatarClick,
                        'on-button-click' : onButtonClick,
                    })}
                ></exe-button>`
            }

        </div>
    `
}
Copy the code

The renderAttrStr method receives an attribute object and returns its key-value pair string:

// input
{
  'e-avatar-width': 100.'e-avatar-radius': 50.'e-avatar-src': './testAssets/images/avatar-1.png',}// output
"e-avatar-width='100' e-avatar-radius='50' e-avatar-src='./testAssets/images/avatar-1.png' "
Copy the code

The final effect is as follows:

6. Implement a user list service

Let’s take a look at the effect of our component through a real business:

The implementation is also simple, given the data, and then loop through the component, assuming the following user data:

const users = [
  {"name":"Front-end early chat."."desc":"Help 5000 front-end run first @ front-end early chat"."level":6."avatar":"qdzzl.jpg"."home":"https://juejin.cn/user/712139234347565"}
  {"name":"A coder from Loughdrew."."desc":"No one can save me, no one can save me, just as I can save no one else."."level":2."avatar":"lzlfdldmn.jpg"."home":"https://juejin.cn/user/994371074524862"}
  {"name":"Black Maple"."desc":"Always with the heart of an apprentice..."."level":3."avatar":"hsdf.jpg"."home":"https://juejin.cn/user/2365804756348103"}
  {"name":"captain_p"."desc":"The destination is beautiful and the scenery on the way is good. Have you learned something today?"."level":2."avatar":"cap.jpg"."home":"https://juejin.cn/user/2532902235026439"}
  {"name":"CUGGZ"."desc":"Article contact wechat authorized reprint. Wechat: cug-gz, add friends to learn ~"."level":5."avatar":"cuggz.jpg"."home":"https://juejin.cn/user/3544481220801815"}
  {"name":"Zhengcai Cloud Front End Team"."desc":"Zheng Mining cloud front ZooTeam team, without water original. Team site: https://zoo.team"."level":6."avatar":"zcy.jpg"."home":"https://juejin.cn/user/3456520257288974"}]Copy the code

We can concatenate HTML fragments with a simple for loop and add them to a page element:

// Test generate user list template
const usersTemp = () = > {
    let temp = ' ', code = ' ';
    users.forEach(item= > {
        const {name, desc, level, avatar, home} = item;
        temp += 
`
<exe-user-avatar 
    e-user-name="${name}"
    e-sub-name="${desc}"
    e-avatar-src="./testAssets/images/users/${avatar}"Width ="36px" e-button-type="primary" e-button-text=" margin: 0.0px 0.0px 0.0px 0.0px 0.0px 0.0px 0.0px 0.0px" width="36px" e-button-type="primary"${home}')"
    on-button-click="toUserFollow('${name}')"
>
${
    level >= 0 && '<span slot="name-slot"> <span class="medal-item"> (Lv${level}) < / span > < / span > `}
</exe-user-avatar>
`
})
    return temp;
}

document.querySelector('#app').innerHTML = usersTemp;
Copy the code

Here we have a list of users, but of course the actual business may be more complex and need to be optimized.

Five, the summary

This article first briefly reviews the Core API of Web Components, and then analyzes and designs the requirements of the component library, and then builds and develops the environment. The content is more, and may not talk about every point. Please take a look at the source code of my warehouse, and welcome to discuss with me if you have any questions. The core purposes of this article are:

  1. When we receive a new task, we need to start from analysis and design, then development, rather than blindly start development;
  2. Take a look at developing a simple business component library using Web Components.
  3. Experience the downside of the Web Components development component library (it’s just too much to write).

At the end of this article, is it a bit complicated to develop a component library using Web Components? There’s so much to write. In the next post, I will take you through the development of a standard Web Components library using Stencil. Ionic has been refactoring with Stencil.

Develop reading

  • WEBCOMPONENTS.ORG Discuss & share web components
  • Web Components as Technology
  • Stenciljs – Build. Customize. Distribute. Adopt.