Author: Bump man – JJ

Taro is a multi-end development framework. Developers write a single piece of code to generate applications for each small program side, H5, and React Native.

Taro Next has recently released a beta version, fully improving the support for small programs and H5, welcome to experience!

background

Taro Next will support development using multiple frameworks

In the past Taro 1 and Taro 2 could only be developed using React syntax, but the next generation Taro framework has upgraded the overall architecture, supporting the development of multi-terminal applications using React, Vue, Nerv and other frameworks.

In order to support development using multiple frameworks, Taro needs to modify its adaptability. This paper will focus on the transformation of Taro H5 terminal component library.

Taro H5

Taro follows components and API specifications that are dominated by wechat applets and supplemented by other applets.

However, browsers do not have applet specification components and apis to use. For example, we cannot use applet view component and getSystemInfo API in the browser. Therefore, we need to implement a set of component libraries and API libraries based on the small program specification on the H5 end.

In Taro 1 and Taro 2, the component libraries for Taro H5 were developed using React syntax. However, if a developer uses Vue to develop an H5 application in Taro Next, it will not be compatible with the existing H5 component library.

Therefore, the core problem to be faced in this article is that we need to implement component libraries that can be used by React, Vue and other frameworks on the H5 end.

Scheme selection

Our first thought was to use Vue to develop a new set of component libraries, which was the safest and not too much work.

However, considering the following two points, we abandoned this idea:

  1. Component libraries are not maintainable and extensible enough. Whenever a problem needs to be fixed or a new feature needs to be added, we need to make changes to the component libraries for the React and Vue versions respectively.
  2. Taro Next’s goal is to support the development of multi-end applications using any framework. If we’re going to support development using frameworks like Angular, then we need to develop component libraries that support Angular.

So is there a way to make a component library built from a single copy of code compatible with all web development frameworks?

The answer is Web Components.

However, the process of transforming the component library into Web Components is not smooth, and we have encountered many problems, so borrow this article to describe to you.

Introduction of Web Components

Web Components consists of a set of technical specifications that allow developers to develop Components that browsers support natively.

The technical specification

The main technical specifications of Web Components are:

  • Custom Elements
  • Shadow DOM
  • HTML Template

Custom Elements allow developers to customize HTML tags with specific behavior.

Shadow DOM wraps the structure and style inside the tag.

The

The sample

Define a template:

<template id="template">
  <h1>Hello World!</h1>
</template>
Copy the code

Construct a Custom Element:

class App extends HTMLElement {
  constructor () {
    super(... arguments)// Enable Shadow DOM
    const shadowRoot = this.attachShadow({ mode: 'open' })

    // Reuse the structure defined by 
      
    const template = document.querySelector('#template')
    const node = template.content.cloneNode(true)
    shadowRoot.appendChild(node)
  }
}
window.customElements.define('my-app', App)
Copy the code

Use:

<my-app></my-app>
Copy the code

Stencil

Writing Web Components using native syntax can be tedious, so we need a framework to help us improve our development efficiency and experience.

There are many mature Web Components frameworks in the industry, and we chose Stencil for two reasons:

  1. Stencil, built by the Ionic team and used to build Ionic’s component library, has proven to be industry-proven.
  2. Stencil supports JSX, reducing the cost of migrating existing component libraries.

Stencil is a compiler that generates Web Components. It incorporates some of the best concepts of industry front-end frameworks, such as support for Typescript, JSX, and virtual DOM.

Example:

Create Stencil Component:

import { Component, Prop, State, h } from '@stencil/core'

@Component({
  tag: 'my-component'
})
export class MyComponent {
  @Prop() first = ' '
  @State() last = 'JS'

  componentDidLoad () {
    console.log('load')
  }

  render () {
    return (
      <div>
        Hello, my name is {this.first} {this.last}
      </div>)}}Copy the code

Using components:

<my-component first='Taro' />
Copy the code

Use Stencil in React and Vue

So far, so good: Web Components are written in Stencil, so you can use them directly in React and Vue.

However, there will be some problems in the actual use. Custom Elements Everywhere lists the compatibility issues and related issues of the industry front-end framework to Web Components through a series of test cases. The following is a brief introduction to the Taro H5 component library compatible with React and Vue, respectively.

Compatible with the React

1. Props

1.1 the problem

React passes parameters to Web Components using the form setAttribute. This works when the arguments are primitive types, but when the arguments are objects or arrays, since the attribute values of HTML elements can only be strings or null, The final attribute set to WebComponents will be attr=”[Object object]”.

Attribute is different from property

1.2 Solutions

The parameters are passed using the DOM Property method.

We can wrap Web Components as a high-level component and set the props on the high-level component to the Web Components property:

const reactifyWebComponent = WC= > {
  return class extends React.Component {
    ref = React.createRef()

    update () {
      Object.entries(this.props).forEach(([prop, val]) = > {
        if (prop === 'children' || prop === 'dangerouslySetInnerHTML') {
          return
        }
        if (prop === 'style' && val && typeof val === 'object') {
          for (const key in val) {
            this.ref.current.style[key] = val[key]
          }
          return
        }
        this.ref.current[prop] = val
      })
    }

    componentDidUpdate () {
      this.update()
    }

    componentDidMount () {
      this.update()
    }

    render () {
      const { children, dangerouslySetInnerHTML } = this.props
      return React.createElement(WC, {
        ref: this.ref,
        dangerouslySetInnerHTML
      }, children)
    }
  }
}

const MyComponent = reactifyWebComponent('my-component')
Copy the code

Note:

  • Children, dangerouslySetInnerHTML attributes need to be passed through.
  • Style property values in React can take object form, which requires extra processing.

2. Events

2.1 the problem

Because React has a synthetic event system, it can’t listen to custom events emitted by Web Components.

The onLongPress callback for the following Web Component will not be triggered:

<my-view onLongPress={onLongPress}>view</my-view>
Copy the code
2.2 Solutions

Get the Web Component element from ref and bind the event to addEventListener manually.

Modify the above higher-order components:

const reactifyWebComponent = WC= > {
  return class Index extends React.Component {
    ref = React.createRef()
    eventHandlers = []

    update () {
      this.clearEventHandlers()

      Object.entries(this.props).forEach(([prop, val]) = > {
        if (typeof val === 'function' && prop.match(/^on[A-Z]/)) {
          const event = prop.substr(2).toLowerCase()
          this.eventHandlers.push([event, val])
          return this.ref.current.addEventListener(event, val)
        }

        ...
      })
    }

    clearEventHandlers () {
      this.eventHandlers.forEach(([event, handler]) = > {
        this.ref.current.removeEventListener(event, handler)
      })
      this.eventHandlers = []
    }

    componentWillUnmount () {
      this.clearEventHandlers() } ... }}Copy the code

3. Ref

3.1 the problem

To solve the problems with Props and Events, we introduced high-level components. So when a developer passes in a ref to a higher-order Component, it gets the higher-order Component, but we want the developer to get the Corresponding Web Component.

DomRef gets MyComponent instead of

<MyComponent ref={domRef} />
Copy the code
3.2 Solutions

Pass the ref through the forwardRef.

Change the above higher-order component to the forwardRef form:

const reactifyWebComponent = WC= > {
  class Index extends React.Component {... render () {const { children, forwardRef } = this.props
      return React.createElement(WC, {
        ref: forwardRef
      }, children)
    }
  }
  return React.forwardRef((props, ref) = >( React.createElement(Index, { ... props,forwardRef: ref })
  ))
}
Copy the code

4. Host’s className

4.1 the problem

In Stencil we can use the Host component to add the class name to the Host element.

import { Component, Host, h } from '@stencil/core';

@Component({
  tag: 'todo-list'
})
export class TodoList {
  render () {
    return (
      <Host class='todo-list'>
        <div>todo</div>
      </Host>)}}Copy the code

Then using the

element will show our built-in class name “todo-list” and the class name “polymorphism” that Stencil automatically adds:

But if we use a dynamic class name, such as

. When the dynamic class name is updated, the built-in class name “todo-list” and “polymorphism” will be erased.

About class name “polymorphism” :

Stencil adds visibility: hidden to all Web Components. Style. After each Web Component is initialized, add the class name polymorphism (DST) and change visibility to inherit. If “polymorphism” is erased, the Web Components will not be visible.

So we need to make sure that the Web Components built-in class name is not overwritten when the class name is updated.

4.2 Solutions

The higher-order Component merges the built-in class when it sets the className property for the Web Component using the REF.

Modify the above higher-order components:

const reactifyWebComponent = WC= > {
  class Index extends React.Component {
    update (prevProps) {
      Object.entries(this.props).forEach(([prop, val]) = > {
        if (prop.toLowerCase() === 'classname') {
          this.ref.current.className = prevProps
            // getClassName returns the latest class name, while retaining the built-in class name
            ? getClassName(this.ref.current, prevProps, this.props)
            : val
          return}... }) } componentDidUpdate (prevProps) {this.update(prevProps)
    }

    componentDidMount () {
      this.update()
    }

    ...
  }
  return React.forwardRef((props, ref) = >( React.createElement(Index, { ... props,forwardRef: ref })
  ))
}
Copy the code

Compatible with the Vue

Unlike React, Vue also uses setAttribute when passing parameters to Web Components, but the V-bind directive provides a.prop modifier that binds parameters as DOM property. In addition, Vue can listen for custom events emitted by Web Components.

Therefore, Vue does not need additional treatment on Props and Events, but there are still some compatibility problems with Stencil. The next three main points will be listed.

1. Host’s className

1.1 the problem

Update the host element class in Vue, which will also override the built-in class.

1.2 Solutions

Along the same lines, you need to wrap a layer of Vue’s custom Components around Web Components.

function createComponent (name, classNames = []) {
  return {
    name,
    computed: {
      listeners () {
        return { ...this.$listeners }
      }
    },
    render (createElement) {
      return createElement(name, {
        class: ['hydrated'. classNames],on: this.listeners
      }, this.$slots.default)
    }
  }
}

Vue.component('todo-list', createComponent('todo-list'['todo-list']))
Copy the code

Note:

  • We repeatedly declared the built-in class name for the Web Component in our custom Component. When developers set class names for custom components, Vue will automatically merge the class names.
  • The events bound to the custom Component need to be transparently transmitted to the Web Component via $Listeners.

2. Ref

2.1 the problem

To solve problem 1, we wrap the Web Components in Vue with a layer of custom Components. Similarly, developers using the REF get a custom Component instead of a Web Component.

2.2 Solutions

This.$parent.$refs.

Add a mixin to a custom component:

export const refs = {
  mounted () {
    if (Object.keys(this.$parent.$refs).length) {
      const refs = this.$parent.$refs

      for (const key in refs) {
        if (refs[key] === this) {
          refs[key] = this.$el
          break
        }
      }
    }
  },
  beforeDestroy () {
    if (Object.keys(this.$parent.$refs).length) {
      const refs = this.$parent.$refs

      for (const key in refs) {
        if (refs[key] === this.$el) {
          refs[key] = null
          break
        }
      }
    }
  }
}
Copy the code

Note:

  • The above code does not handle the circular ref, which requires additional judgment and processing.

3. v-model

3.1 the problem

We used the render function in the custom component for rendering, so additional v-Model processing is required for the form component.

3.2 Solutions

Use the Model option on the custom component to customize the prop and Event when the component uses the V-Model.

To modify the above custom components:

export default function createFormsComponent (name, event, modelValue = 'value', classNames = []) {
  return {
    name,
    computed: {
      listeners () {
        return { ...this.$listeners }
      }
    },
    model: {
      prop: modelValue,
      event: 'model'
    },
    methods: {
      input (e) {
        this.$emit('input', e)
        this.$emit('model', e.target.value)
      },
      change (e) {
        this.$emit('change', e)
        this.$emit('model', e.target.value)
      }
    },
    render (createElement) {
      return createElement(name, {
        class: ['hydrated'. classNames],on: {
          ...this.listeners,
          [event]: this[event]
        }
      }, this.$slots.default)
    }
  }
}

const Input = createFormsComponent('taro-input'.'input')
const Switch = createFormsComponent('taro-switch'.'change'.'checked')
Vue.component('taro-input', Input)
Vue.component('taro-switch', Switch)
Copy the code

conclusion

Web Components are a good choice when we want to create Components that are not tied to a framework. For example, when working across teams with different technology stacks, common parts are needed.

This time, the React syntax component library is transformed into Web Components, and the workload is no less than to rebuild a Vue component library. However, when Taro supports multiple applications using other frameworks in the future, it will be worth writing a glue layer for compatibility with Web Components and Stencil.

As for the glue layer, there are many solutions compatible with React in the industry, but reactify-WC can be used with Web Components, and Stencil DS Plugin can be used with Stencil. If Vue needs to be Stencil compatible, or if it needs to be more flexible when compatible, it is advisable to write a glue layer by hand.

This article briefly introduces Taro Next, Web Components, Stencil and Stencil based component library transformation process, hoping to bring some help and enlightenment for readers.


Welcome to the Bump Lab blog: aotu.io

Or follow the public account of AOTULabs and push articles from time to time: