preface

WebComponent is a solution to realize componentization. At present, there are many mature schemes in the community, such as Omi and StenCIL. Among them, the Omi scheme of Tencent front-end team is quite perfect. In this article the blogger intends to build on the Omi solution and make the elementUI framework WebComponent.

Application Scenarios:

A large Web project (jquery stack) requires UI upgrades to keep in line with other projects (react stack, Angular stack). A common scenario might require multiple UIs based on different stacks, but the WebComponent solution can be used once and for all. Compatible with multiple sets of technology stack.

Through this article, we can learn these things:

  • High-frequency apis in WebComponent;
  • Define a simple WebComponent;
  • Write a Web-core package;
  • Write the first WebComponent Button component with ElementUI + Web-core.

The relevant code in the article has been submitted to Github, welcome star.

The code address

Results the preview

Basic button styles are shown below:

Basic radio styles are shown below:

WebComponent

concept

Web Component is a W3C solution for componentization with the following key metrics:

  1. Shadow DOM
  2. Custom Elements
  3. HTML Imports
  4. HTML Templates

Shadow DOM

Shadow DOM is a new SPECIFICATION for HTML that allows developers to encapsulate their HTML tags, CSS styles, and JavaScript code, most importantly with natural isolation of scope and style.

Custom Elements

Can allow developers to defined in the document and use the new dom element types, namely custom elements, such as window. CustomElements. Define (‘ test – element, TestElement); You can customize an HTML tag (test-element) that can be used directly.

HTML Imports

HTML imports provides a way to include and reuse HTML documents in one HTML document. With HTML imports, you can easily import other HTML documents into one HTML document for reuse, but I haven’t tried this yet.

HTML Templates

HTML Templates is a literal that allows developers to directly customize the content of components.

The life cycle

WebComponent custom elements, like components in Vue and React, have a life cycle in which states run in several phases.

  1. ConnectedCallback: called when custom elemen t is first inserted into the DOM;
  2. DisconnectedCallback: called when a Custom Element is removed from the DOM;
  3. AdoptedCallback: called when a Custom Element is moved to a new document;
  4. AttributeChangedCallback: called when a Custom Element adds, deletes, or modifies its attributes; But attributeChangedCallback needs to be used with observedAttributes.

I found a picture from the Internet, which can be used as a reference:

How do YOU define a Component

class TestComponent extends HTMLElement {
  constructor() {
    super(a)// Use attachShadow to isolate external styles
    const sd = this.attachShadow({ mode: 'open' })
    sd.appendChild(this.initTemplate().content)
  }

  /** * Custom component content */
  public initTemplate() {
     const template = document.createElement('template')
     template.innerHTML = `  
      
webComponent
`
returntemplate; }}// Define the test-component tag. is used in subsequent HTML. customElements.define('test-component', TestComponent) Copy the code

This code implements a simple webComponent that implements natural style isolation as follows:

The Web – the core packages

The Web-core package is a separate Omi stripped wrapper rewritten in typeScript,

CustomWebComponent

This class encapsulates the WebComponent lifecycle and introduces a virtual DOM design to avoid invalid updates to components.

connectedCallback

This method makes a more detailed division of component mounted life cycle nodes, such as component mounted before (property conversion), mounting, after mounting, etc.

export class CustomWebComponent  extends HTMLElement {
    /*** * Mount custom components */
    public connectedCallback() {
        const that: any = this;
        // Convert attrs to props
        this.attrsToProps();
        // Components before mounting
        this.beforeInstall();
        // Component mount
        this.install();
        // After the component is mounted
        this.afterInstall();
        // Initialize ShadowRoot
        let shadowRoot = this.initShadowRoot();
        // Initialize the CSS
        shadowRoot = this.initCssStyle(shadowRoot);
        // Call render function to support JSX layout UI
        const rendered = (this as any).render(this.props);
        // Introduce the virtual DOM to diff the old and new DOM
        this.rootNode = diff(null, rendered, null.this);
        // The UI is rendered
        this.rendered();
        if (that.css) {
            // Insert the CSS into the template
            shadowRoot.appendChild(cssToDom(typeof that.css === 'function' ? that.css() : that.css));
        }
        // If there is a style written inline, further processing is done
        if (this.props.css) {
            this._customStyleElement = cssToDom(this.props.css);
            this._customStyleContent = this.props.css;
            shadowRoot.appendChild(this._customStyleElement);
        }
        if (isArray(this.rootNode)) {
            this.rootNode.forEach(function (item: HTMLElement) {
                shadowRoot.appendChild(item);
            });
        } else {
            this.rootNode && shadowRoot.appendChild(this.rootNode);
        }
        // this.shadowRoot = shadowRoot;
        // The component is fully mounted
        this.installed();
        this.isInstalled = true; }}Copy the code

disconnectedCallback

This method handles actions such as side effects after component uninstallation.

export class CustomWebComponent  extends HTMLElement {
    /*** * Component destruction */
    public disconnectedCallback() {
        // The component is uninstalled
        this.uninstall();
        this.isInstalled = false; }}Copy the code

Virtual DOM and Diff

The details of virtual DOM and DIff will not be introduced here. The core can refer to implementation methods such as Vue and React.

Event handling mechanism

CustomEvent is used at the bottom of the framework to implement custom events.

export class CustomWebComponent  extends HTMLElement {
    /** * event agent *@param name
     * @param data
     * @private* /
    public fire(name: string, data: any) {
        const handler = this.props[`on${capitalize(name)}`];
        if (handler) {
            handler(
                new CustomEvent(name, {
                    detail: data
                })
            );
        } else {
            this.dispatchEvent(
                new CustomEvent(name, {
                    detail: data }) ); }}}Copy the code

Web-ui

jsx

JSX can define the UI functionally.

export default class WuIcon extends CustomWebComponent {
    constructor() {
        super(a); }public render(props: Props) {
        return (
           <i class="wu-icon" />); }}Copy the code

To realize the button

import { CustomWebComponent, h, CustomTag, extractClass, WebUiConfig, UISize } from "@canyuegongzi/web-core";
import * as css from './index.scss';

interfaceProps { size? : UISizetype? :'primary' | 'success' | 'warning' | 'danger' | 'info' | 'text'plain? :booleanround? :booleancircle? :booleanloading? :booleandisabled? :booleanicon? :stringnativeType? :'button' | 'submit' | 'reset'text? :string
}
// The decorator defines the component name
@CustomTag({ name: 'wu-button' })
export default class WuButton extends CustomWebComponent{
    static css = css.default ? css.default : css
    static defaultProps = {
        size: WebUiConfig.size,
        plain: false.round: false.circle: false.loading: false.disabled: false.nativeType: 'button'
    }

    static propTypes = {
        size: String.type: String.plain: Boolean.round: Boolean.circle: Boolean.loading: Boolean.disabled: Boolean.icon: String.nativeType: String.text: String,}constructor() {
        super(a); }public render(props: Props) {
        return (
            <button
                disabled={props.disabled}
                {. extractClass(props, 'wu-button'{['wu-button-'+props.type]: props.typeAnd ['wu-button-'+props.size]: props.size,
                    'is-plain': props.plain,
                    'is-round': props.round,
                    'is-circle': props.circle,
                    'is-disabled': props.disabled
                })}
                type={props.nativeType}
            >
                {props.loading && [
                    <svg
                        class="loading"
                        viewBox="0 0 1024 1024"
                        focusable="false"
                        data-icon="loading"
                        width="1em"
                        height="1em"
                        fill="currentColor"
                        aria-hidden="true"
                    >
                        <path d="M988 548c-19.9 0-36-16.1-36-36 0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 00-94.3-139.9 437.71 437.71 0 00-139.9-94.3C629 83.6 571.4 72 72c-19.9 0-36-16.1-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.3C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7 26.7 63.1 40.2 130.2 40.2 199.3.1 19.9-16 36-35.9 36z"></path>
                    </svg>,
                    ' ',
                ]}
                {props.text}
                <slot></slot>
            </button>); }}Copy the code

thinking

The componentization of webComponent is no different from the componentization of mainstream frameworks like Vue and React in terms of results, but webComponent is still not perfect in terms of development experience. There is also a difference in focus between webComponent and mainstream frameworks, as front-end frameworks have the added value of data binding, state management, and fairly standardized code bases.

The article only serves as an introduction. If you are interested in this direction, you can directly pull up the Github code to read it, and also refer to Omi related materials.

For those of you who like to make a fuss, ask PR and bloggers to work together to improve the library.