30
头图

Componentization is an important direction of front-end development. It improves development efficiency on the one hand and reduces maintenance costs on the other. The mainstream Vue.js, React and its extended Ant Design, uniapp, Taro, etc. are all component frameworks.

Web Components is a general term for a set of web native APIs, allowing us to create reusable custom components and use them in our web applications like native HTML tags. Currently, many front-end frameworks/libraries support Web Components .

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

Final effect: https://blog.pingan8787.com/exe-components/demo.html
Warehouse address: https://github.com/pingan8787/Learn-Web-Components

1. Review Web Components

In the history of front-end development, from the beginning of repeating the business, copying the same code everywhere, to the emergence of Web Components, we use custom components of native HTML tags, reuse component codes, and improve development efficiency. The components created by Web Components can be used in almost any front-end framework.

1. Core API review

Web Components consists of 3 core APIs:

  • Custom elements (custom elements) : used to allow us to define custom elements and behavior , to provide external component labels;
  • Shadow DOM (Shadow DOM) : used to encapsulate the internal structure of the component to avoid external conflicts;
  • HTML templates (HTML templates) : Including <template> and <slot> elements, so that we can define HTML templates of various components, and then reused in other places, students who have used Vue/React and other frameworks should be familiar.
In addition, there are HTML imports, but they are currently obsolete, so they are not introduced in detail. Their function is to control the dependency loading of components.

2. Getting started example

Next, take a quick look at how creates a simple Web Components component through the following simple example.

  • Use components
<!DOCTYPE html>
<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>
  • Define components
/**
 * 使用 CustomElementRegistry.define() 方法用来注册一个 custom element
 * 参数如下:
 * - 元素名称,符合 DOMString 规范,名称不能是单个单词,且必须用短横线隔开
 * - 元素行为,必须是一个类
 * - 继承元素,可选配置,一个包含 extends 属性的配置对象,指定创建的元素继承自哪个内置元素,可以继承任何内置元素。
 */

class CustomElementStart extends HTMLElement {
    constructor(){
        super();
        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)

The above code mainly does 3 things:

  1. Implement component class

Define the component by implementing the CustomElementStart

  1. Define components

Set the component label and component class as parameters, and define the component customElements.define

  1. Use components

After importing the component, use the custom component <custom-element-start></custom-element-start> directly like ordinary HTML tags.

Then the browser visits index.html to see the following content:

3. Introduction to Compatibility

Its compatibility is described in the chapter MDN | Web Components

  • Firefox (version 63), Chrome and Opera all support web components by default.
  • Safari supports many web component features, but less than the above browsers.
  • Edge is developing an implementation.

Regarding compatibility, you can see the picture below:

Image source: https://www.webcomponents.org/

In this website, there are many excellent projects about Web Components to learn from.

4. Summary

This section mainly uses a simple example to briefly review the basic knowledge, and you can read the document for details:

2. EXE-Components component library analysis and design

1. Background introduction

Suppose we need to implement an EXE-Components component library, the components of the component library are divided into 2 categories:

  1. components type

general simple component , such as exe-avatar avatar component, exe-button button component, etc.;

  1. modules type

complex and combined component , such as exe-user-avatar user avatar component (including user information), exe-attachement-list attachment list component and so on.

You can see the following picture for details:

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

2. Component library design

When designing the component library, the following points need to be considered:

  1. Component naming, parameter naming and other specifications are convenient for subsequent maintenance of components;
  2. Component parameter definition;
  3. Component style isolation;

Of course, these are the most basic points that need to be considered. As the actual business becomes more complex, more needs to be considered, such as: engineering-related, component decoupling, component themes, and so on.

In response to the three points mentioned above, here are a few naming conventions:

  1. The component name exe-function name, for example, exe-avatar represents the avatar component;
  2. The attribute parameter name e-parameter name, for example, e-src represents the address attribute of src
  3. The event parameter name on-event type, for example, on-click represents a click event;

3. Component library component design

Here we mainly design exe-avatar , exe-button and exe-user-avatar . The first two are simple components, and the latter is a complex component. The first two components are used inside to combine. First define the attributes supported by these three components:

The attribute naming here seems to be more complicated, you can name it according to your own and team habits.

In this way, our thinking is much clearer, and the corresponding components can be realized.

Three, EXE-Components component library preparation

The example in this article will finally combine the to achieve the following " user list " effect:

Experience address: https://blog.pingan8787.com/exe-components/demo.html

1. Unified development specifications

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

  1. Directory specification

  1. Define component specifications

  1. Component development template

The component development templates are divided into index.js component entry files and template.js component HTML template files :

// index.js 模版
const defaultConfig = {
    // 组件默认配置
}

const Selector = "exe-avatar"; // 组件标签名

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

    constructor(){
        super();
        this.render(); // 统一处理组件初始化逻辑
    }

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

// 定义组件
if (!customElements.get(Selector)) {
    customElements.define(Selector, EXEAvatar)
}
// template.js 模版

export default config => {
    // 统一读取配置
    const { avatarWidth, avatarRadius, avatarSrc } = config;
    return `
        <style>
            /* CSS 内容 */
        </style>
        <div class="exe-avatar">
            /* HTML 内容 */
        </div>
    `
}

2. Development environment construction and engineering processing

In order to facilitate the use of the EXE-Components component library, which is closer to the use of the actual component library, we need to package the component library into a UMD type js file. Here we use rollup to build, and finally package it into exe-components.js . The usage is as follows:

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

Next, generate the package.json npm init -y , and then install rollup and http-server (used to start the local server for easy debugging):

npm init -y
npm install --global rollup http-server

Then package.json of script added under "dev" and "build" script:

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

in:

  • "dev" command: start a static server through http-server, as a development environment. Add the -c-1 to disable caching to avoid caching when refreshing the page. For details, see http-server document ;
  • "build" command: use index.js as the entry file for rollup packaging, and output the exe-components.js file, which is an iife type file.

This completes the engineering configuration of simple local development and component library construction, and then you can proceed with development.

Four, EXE-Components component library development

1. Component library entry file configuration

"build" command configured in the previous package.json file will use index.js root directory as the entry file, and in order to facilitate the introduction of components common basic components and modules common complex components, we create three index.js , the directory structure after creation 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';

2. Develop exe-avatar component index.js file

Through the previous analysis, we can know that the exe-avatar component needs to support parameters:

  • e-avatar-src: the address of the avatar image, for example: ./testAssets/images/avatar-1.png
  • e-avatar-width: avatar width, the default is the same as the height, for example: 52px
  • e-button-radius: rounded corners of the avatar, for example: 22px, default: 50%
  • on-avatar-click: Avatar click event, default none

Then follow 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();
        this.render();
    }

    render() {
        this.shadowRoot = this.attachShadow({mode: 'closed'});
        this.shadowRoot.innerHTML = renderTemplate(this.config);// 生成 HTML 模版内容
    }

        // 生命周期:当 custom element首次被插入文档DOM时,被调用。
    connectedCallback() {
        this.updateStyle();
        this.initEventListen();
    }

    updateStyle() {
        this.config = {...defaultConfig, ...getAttributes(this)};
        this.shadowRoot.innerHTML = renderTemplate(this.config); // 生成 HTML 模版内容
    }

    initEventListen() {
        const { onAvatarClick } = this.config;
        if(isStr(onAvatarClick)){ // 判断是否为字符串
            this.addEventListener('click', e => runFun(e, onAvatarClick));
        }
    }
}

if (!customElements.get(Selector)) {
    customElements.define(Selector, EXEAvatar)
}

Several of these methods are extracted public methods, and their functions are briefly introduced. For details, you can see the source code:

  • renderTemplate Method

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

  • getAttributes method

e- an HTMLElement element and return all the attribute key-value pairs on the element. Among them, the attributes starting with 061c48f03050f4 and on- will be processed into ordinary attributes and event attributes respectively. Examples are as follows:

// 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()"
}
  • runFun Method

Since the method passed in through the attribute is a string, it is encapsulated, passing in event and the event name as parameters, calling this method, the example is the same as the previous step, and the avatarClick() method will be executed.

In addition, the life cycle of Web Components can be viewed in detail in the document: uses the life cycle callback function .

3. Develop exe-avatar component template.js file

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

// 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>
  `
}

The final effect is as follows:

After developing the first component, we can briefly summarize the steps to create and use the component:

4. Develop exe-button components

According to the previous development ideas of the exe-avatar exe-button component can be quickly realized.
The following parameters need to be supported:

  • e-button-radius: button rounded corners, for example: 8px
  • e-button-type: Button type, for example: default, primary, text, dashed
  • e-button-text: button text, default: open
  • on-button-click: 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: "打开",
    disableButton: false,
    onButtonClick: null,
}

const Selector = "exe-button";

export default class EXEButton extends HTMLElement {
    // 指定观察到的属性变化,attributeChangedCallback 会起作用
    static get observedAttributes() { 
        return ['e-button-type','e-button-text', 'buttonType', 'buttonText']
    }

    shadowRoot = null;
    config = defaultConfig;

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

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

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

    attributeChangedCallback (name, oldValue, newValue) {
        // console.log('属性变化', 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)
}

The template is defined as follows:

// EXE-Components/components/exe-button/tempalte.js
// 按钮边框类型
const borderStyle = { solid: 'solid', dashed: 'dashed' };

// 按钮类型
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>
    `
}

The final effect is as follows:

5. Develop exe-user-avatar components

This component is a combination of the previous exe-avatar component and the exe-button component. It not only needs to support the click event , but also needs to support the slot function .

Since it is a combination, it is relatively simple to develop~Look at the entry file first:

// 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();
        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)
}

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>
            ${
                !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>
    `
}

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' "

The final effect is as follows:

6. Implement a user list business

Next, let's take a look at the effect of our components through a real business:


In fact, the implementation is also very simple. According to the given data, the components can be recycled. Assuming the following user data:

const users = [
  {"name":"前端早早聊","desc":"帮 5000 个前端先跑 @ 前端早早聊","level":6,"avatar":"qdzzl.jpg","home":"https://juejin.cn/user/712139234347565"}
  {"name":"来自拉夫德鲁的码农","desc":"谁都不救我,谁都救不了我,就像我救不了任何人一样","level":2,"avatar":"lzlfdldmn.jpg","home":"https://juejin.cn/user/994371074524862"}
  {"name":"黑色的枫","desc":"永远怀着一颗学徒的心。。。","level":3,"avatar":"hsdf.jpg","home":"https://juejin.cn/user/2365804756348103"}
  {"name":"captain_p","desc":"目的地很美好,路上的风景也很好。今天增长见识了吗","level":2,"avatar":"cap.jpg","home":"https://juejin.cn/user/2532902235026439"}
  {"name":"CUGGZ","desc":"文章联系微信授权转载。微信:CUG-GZ,添加好友一起学习~","level":5,"avatar":"cuggz.jpg","home":"https://juejin.cn/user/3544481220801815"}
  {"name":"政采云前端团队","desc":"政采云前端 ZooTeam 团队,不掺水的原创。 团队站点:https://zoo.team","level":6,"avatar":"zcy.jpg","home":"https://juejin.cn/user/3456520257288974"}
]

We can splice HTML fragments through a simple for loop, and then add them to an element on the page:

// 测试生成用户列表模版
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}"
    e-avatar-width="36px"
    e-button-type="primary"
    e-button-text="关注"
    on-avatar-click="toUserHome('${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;

At this point, we have implemented a user list business. Of course, the actual business may be more complicated and needs to be optimized.

Five, summary

This article first briefly reviews the core API of Web Components, and then analyzes and designs the component library requirements, and then builds and develops the environment. The content is relatively large, and it may not be covered in every point. Please also take a look at the source code of my warehouse. What are the problems? Welcome to discuss with me.

Several core purposes of writing this article:

  1. When we receive a new task, we need to start from the analysis and design, and then to the development, rather than blindly start the development;
  2. Take a look at how to use Web Components to develop a simple business component library;
  3. Experience the shortcomings of the Web Components development component library (that is, there are too many things to write).

After reading this article, do you think it is a bit complicated to develop component libraries with Web Components? Too much to write.
It doesn't matter, in the next article, I will take everyone to use the Stencil framework to develop the Web Components standard component library. After all, the entire ionic has been Stencil , and the general trend of Web Components~!

Further reading


pingan8787
3.2k 声望4.1k 粉丝

🌼 前端宝藏小哥哥