background

In the project, we used Taro3 for cross-terminal development. Here we share some cross-terminal and cross-framework principles of Taro3, hoping to help us better understand the things behind Taro and accelerate the positioning of problems. This article also plays a casting brick to draw jade effect, let oneself understand a bit more deeply.

The overall architecture of the past, it is divided into two parts, the first part is compile time, the second part is run time. When compiling, the React code of the user is first compiled and converted into the code that can be run by small programs on all sides. Then, each small program side is equipped with a corresponding runtime framework for adaptation, and finally the code is run on each small program side.

Before Taro3 (recompile time, light run time) :

Compile time is usedbabel-parserThe Taro code is parsed into an abstract syntax tree and then passedbabel-typesThe abstract syntax tree is modified, transformed, and finally passedbabel-generateGenerate the corresponding object code.

Such an implementation has three disadvantages:

  • JSX support is not perfect. Taro’s support for JSX is achieved through compile-time adaptation, but JSX is too flexible to support 100% of all JSX syntax. JSX is a syntax extension for JavaScript, which can be written in a variety of ways and is very flexible. Taro’s team used an exhaustive method to adapt the possible writing methods of JSX one by one, which was a lot of work.
  • Source-map is not supported. After Taro made a series of transformations to the source code, source-Map was no longer supported, making it difficult for users to debug and use the project.
  • Maintenance and iteration are difficult. Taro code is complex and discrete at compile time, making it difficult to maintain iterations.

Take a look at the runtime defects. For each applet platform, a runtime framework is provided for adaptation. When fixing bugs or adding features, you need to change multiple runtime frameworks at the same time.

In general, the previous Taro3.0 had the following problems:

After Taro3 (rerun) :

Taro 3 can be roughly understood as an interpretative architecture (compared with Taro 1/2). It mainly implements DOM and BOM API on the small program side to make the front-end framework run directly in the small program environment, so as to achieve the unification of small programs and H5. Differences in lifecycle, component libraries, apis, routing, etc., can still be smoothest by defining uniform standards and implementing ways for each end. Taro 3 also supports frameworks like React, Vue, and jQuery. It also allows developers to customize and extend the support of other frameworks, such as Angular. The overall architecture of Taro 3 is as follows:

Taro applet:

Taro 3 after the overall architecture of small programs. First, the user’s React or Vue code will be webpacked via CLI. Second, the React and Vue adaptors will be provided at run time. Then, the DOM and BOM apis provided by Taro will be called. Finally, the entire program is rendered to all small programs.

React is a bit special because react-DOM contains a lot of code for browser-compatible classes, which makes the package too large, and this part of the code is not needed, so some customization and optimization is done.

In React 16+, React architecture is as follows:

At the top is the React-Core, the core part of React, and in the middle is the React-Reconciler, which is responsible for maintaining the VirtualDOM tree, implementing the Diff/Fiber algorithm internally and deciding when and what to update.

The Renderer is responsible for platform-specific rendering, providing host components, handling events, and so on. For example, react-dom is a renderer that handles DOM nodes and DOM events.

Taro implements the Taro – React package to connect the React-Reconciler to the Taro – Runtime BOM/DOM API. The React Renderer is a small program-specific react renderer based on the React-Reconciler and connects to @tarojs/ Runtime DOM instances. It is equivalent to the react-DOM for small programs and exposes the same API as the React-DOM.

There are two steps to creating a custom renderer:

Step 1: Implement the host configuration (implement the ** React-Reconciler **hostConfig** ** configuration)** These are some of the adapter methods and configuration items that the React-Reconciler requires the host to provide. These configuration items define how to create a node instance, build a node tree, commit, update, and so on. That is to call the corresponding Taro BOM/DOM API in the method of hostConfig.

const Reconciler = require('react-reconciler');



const HostConfig = {

  / /... Implement adapter methods and configuration items

};
# @tarojs/react reconciler.ts



/* eslint-disable @typescript-eslint/indent */

import Reconciler, { HostConfig } from 'react-reconciler'

import * as scheduler from 'scheduler'

import { TaroElement, TaroText, document } from '@tarojs/runtime'

import { noop, EMPTY_ARR } from '@tarojs/shared'

import { Props, updateProps } from './props'



const {

  unstable_scheduleCallback: scheduleDeferredCallback,

  unstable_cancelCallback: cancelDeferredCallback,

  unstable_now: now

} = scheduler



function returnFalse () {

  return false

}



const hostConfig: HostConfig<

  string.// Type

  Props, // Props

  TaroElement, // Container

  TaroElement, // Instance

  TaroText, // TextInstance

  TaroElement, // HydratableInstance

  TaroElement, // PublicInstance

  Record<string.any>, // HostContext

  string[].// UpdatePayload

  unknown, // ChildSet

  unknown, // TimeoutHandle

  unknown // NoTimeout

> & {

  hideInstance (instance: TaroElement): void

  unhideInstance (instance: TaroElement, props): void

} = {

  createInstance (type) {

    return document.createElement(type)

  },



  createTextInstance (text) {

    return document.createTextNode(text)

  },



  getPublicInstance (inst: TaroElement) {

    return inst

  },



  getRootHostContext () {

    return {}

  },



  getChildHostContext () {

    return {}

  },



  appendChild (parent, child) {

    parent.appendChild(child)

  },



  appendInitialChild (parent, child) {

    parent.appendChild(child)

  },



  appendChildToContainer (parent, child) {

    parent.appendChild(child)

  },



  removeChild (parent, child) {

    parent.removeChild(child)

  },



  removeChildFromContainer (parent, child) {

    parent.removeChild(child)

  },



  insertBefore (parent, child, refChild) {

    parent.insertBefore(child, refChild)

  },



  insertInContainerBefore (parent, child, refChild) {

    parent.insertBefore(child, refChild)

  },



  commitTextUpdate (textInst, _, newText) {

    textInst.nodeValue = newText

  },



  finalizeInitialChildren (dom, _, props) {

    updateProps(dom, {}, props)

    return false

  },



  prepareUpdate () {

    return EMPTY_ARR

  },



  commitUpdate (dom, _payload, _type, oldProps, newProps) {

    updateProps(dom, oldProps, newProps)

  },



  hideInstance (instance) {

    const style = instance.style

    style.setProperty('display'.'none')

  },



  unhideInstance (instance, props) {

    const styleProp = props.style

    letdisplay = styleProp? .hasOwnProperty('display')? styleProp.display :null

    display = display == null || typeof display === 'boolean' || display === ' ' ? ' ' : (' ' + display).trim()

    // eslint-disable-next-line dot-notation

    instance.style['display'] = display

  },



  shouldSetTextContent: returnFalse,

  shouldDeprioritizeSubtree: returnFalse,

  prepareForCommit: noop,

  resetAfterCommit: noop,

  commitMount: noop,

  now,

  scheduleDeferredCallback,

  cancelDeferredCallback,

  clearTimeout: clearTimeout.setTimeout: setTimeout.noTimeout: -1.supportsMutation: true.supportsPersistence: false.isPrimaryRenderer: true.supportsHydration: false

}



export const TaroReconciler = Reconciler(hostConfig)
Copy the code

Step 2: Implement a render function, similar to the reactdom.render () method. Think of it as creating the Taro DOM Tree container.

// Create the Reconciler instance and pass HostConfig to the Reconciler

const MyRenderer = Reconciler(HostConfig);



Render (
      , container, () => console.log(' Rendered ')) */

export function render(element, container, callback) {

  // Create the root container

  if(! container._rootContainer) { container._rootContainer = ReactReconcilerInst.createContainer(container,false);

  }



  // Update the root container

  return ReactReconcilerInst.updateContainer(element, container._rootContainer, null, callback);

}
Copy the code

With the steps above, the React code will actually run properly while the appletops is running and will generate Taro DOM Tree. So how to update the Taro DOM Tree to the page?

Because small programs do not provide the ability to create nodes dynamically, you need to consider how to use the relatively static WXML to render the relatively dynamic Taro DOM tree. Taro uses template concatenation. Based on the DOM tree data structure provided by the runtime, the templates recursively reference each other, and finally render the corresponding dynamic DOM tree.

First, all the components of the small program are stencil processed one by one, so as to get the corresponding template of the small program components. Here is what the view component template of the applet looks like after being stenciled. The first step is to write a view inside the template and list all of its properties (listing all properties because you can’t dynamically add properties in an applet).

The next step is to iterate over all the child nodes, rendering the whole tree dynamically “recursively” based on the component template.

The specific process is to first traverse the child elements of the root node of the Taro DOM Tree, and then select the corresponding template to render the child elements according to the type of each child element. Then, in each template, we will traverse the child elements of the current element, so as to recurse the entire node Tree.

Taro H5:

Taro follows the main components and API specifications of wechat small programs, supplemented by other small programs. However, the browser does not have the applet specification components and apis available for use. We cannot use the Applet view component and the getSystemInfo API in the browser. Therefore Taro implements a set of component libraries and API libraries based on the small program specification on the H5 end.

Looking at the H5 architecture again, it is also necessary to package the user’s React or Vue code via Webpack. It then did three things at runtime: The first thing is to implement a component library, which needs to be used by React, Vue and even more frameworks at the same time. Taro used Stencil to implement a component library based on WebComponents and in compliance with wechat small program specifications. The second and third thing is to implement the API and routing mechanism of a small program specification, and finally to run the entire program on the browser. Next, let’s focus on implementing component libraries.

Implement component libraries:

The first thing to think about is developing another set of component libraries using Vue, which is the safest and least taxing option.

But officials abandoned the idea because of two concerns:

  1. Component libraries are not maintainable and extensible enough. Each time a problem needs to be fixed or a new feature needs to be added, the React and Vue component libraries need to be reworked, respectively.
  2. Taro Next’s goal is to support the development of multi-end applications using any framework. If you’re going to support development using frameworks like Angular, you’ll 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?

Taro’s choice is Web Components.

Web Components

Web Components consists of a set of technical specifications that allow developers to develop Components that browsers support natively. Allows you to create reusable custom elements (whose functionality is encapsulated outside of your code) and use them in your Web application.

Web Components – It consists of three major technologies that can be used together to create custom elements that encapsulate functionality and can be reused anywhere you like without worrying about code conflicts.

  • Custom Elements:A set of JavaScript apis that allow you to define Custom Elements and their behavior, which can then be used as needed in your user interface. In short, it isAllows developers to customize HTML tags with specific behaviors.

  • Shadow DOM: A set of JavaScript apis for attaching an encapsulated “Shadow” DOM tree to an element (rendered separately from the main document DOM) and controlling its associated functionality. In this way, you can keep the functions of elements private so that they can be scripted and styled without fear of conflict with other parts of the document. In simple terms, the structure and style inside the tag are wrapped in a layer.

  • HTML Templates: the

The basic approach to implementing a Web Component usually looks something like this:

  1. Create a class or function to specify the functionality of a Web component.
  2. Using CustomElementRegistry. Define () method to register your new custom element, and transfer to define the elements to its name, specify the element function class, as well as its inherited from optional elements.
  3. Attach a shadow DOM to the custom Element using the element.attachShadow () method, if desired. Add child elements, event listeners, and so on to the Shadow DOM using the usual DOM methods.
  4. If necessary, use< the template > and < slot >Define an HTML template. Use the regular DOM methods again to clone the template and attach it to your shadow DOM.
  5. Use custom elements wherever you like on the page, just as you would with regular HTML elements.
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

Writing Web Components using native syntax can be tedious, so we need a framework to improve our development efficiency and experience. There are a number of mature Web Components frameworks in the industry, and Taro chose Stencil, 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.

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

Stencil in React

Custom Elements Everywhere lists the compatibility issues and related issues of the industry front-end framework to Web Components. In the React document, also slightly mentioned in using Web Components in the React note zh-hans.reactjs.org/docs/web-co…

React compatibility issues with Web Components can be seen on Custom Elements Everywhere.

Translated, that is to say.

  1. Use the ReactsetAttributePass parameters to Web Components in the form If the parameter is an object or an array, the attribute value of an HTML element can only be a string or null, so the attribute value set to WebComponents will beattr="[object Object]".

The difference between the attribute and property: stackoverflow.com/questions/6…

  1. Because React implements its own synthetic event system, it can’t listen to custom events emitted by Web Components.

reactify-wc.js

In fact, this high-level component implementation is a modified version of the open source library Reactify-WC, which connects WebComponent to React in order to be able to use WebComponent in React. This modified library is designed to solve the aforementioned problem.

props

Taro processing, using the DOM Property method to pass the parameters. Wrap the 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 Index extends React.Component {

  ref = React.createRef()

  update (prevProps) {

      this.clearEventHandlers()

      if (!this.ref.current) return



      Object.keys(prevProps || {}).forEach((key) = > {

        if(key ! = ='children'&& key ! = ='key' && !(key in this.props)) {

          updateProp(this, WC, key, prevProps, this.props)

        }

      })



      Object.keys(this.props).forEach((key) = > {

        updateProp(this, WC, key, prevProps, this.props)

      })

  }

 

  componentDidUpdate () {

    this.update()

  }

 

  componentDidMount () {

    this.update()

  }

 

  render () {

    const { children, dangerouslySetInnerHTML } = this.props

    return React.createElement(WC, {

      ref: this.ref,

      dangerouslySetInnerHTML

    }, children)

}
Copy the code
Event

Because React implements its own 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

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
ref

To solve the problems with Props and Events, high-level components were introduced. 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

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
className

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:

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.

In order not to overwrite the built-in class for host and stencil in WC, merge the built-in class.

function getClassName (wc, prevProps, props) {

  const classList = Array.from(wc.classList)

  const oldClassNames = (prevProps.className || prevProps.class || ' ').split(' ')

  let incomingClassNames = (props.className || props.class || ' ').split(' ')

  let finalClassNames = []



  classList.forEach(classname= > {

    if (incomingClassNames.indexOf(classname) > -1) {

      finalClassNames.push(classname)

      incomingClassNames = incomingClassNames.filter(name= >name ! == classname) }else if (oldClassNames.indexOf(classname) === -1) {

      finalClassNames.push(classname)

    }

  })



  finalClassNames = [...finalClassNames, ...incomingClassNames]



  return finalClassNames.join(' ')}Copy the code

And here we have our Reactify-WC. Let’s not forget that Stencil writes web Components for us, and reactify-WC is designed to make it possible to use Web Components in React. With the following packaging, we can use View, Text and other components directly in react

//packages/taro-components/h5



import reactifyWc from './utils/reactify-wc'

import ReactInput from './components/input'



export const View = reactifyWc('taro-view-core')

export const Icon = reactifyWc('taro-icon-core')

export const Progress = reactifyWc('taro-progress-core')

export const RichText = reactifyWc('taro-rich-text-core')

export const Text = reactifyWc('taro-text-core')

export const Button = reactifyWc('taro-button-core')

export const Checkbox = reactifyWc('taro-checkbox-core')

export const CheckboxGroup = reactifyWc('taro-checkbox-group-core')

export const Editor = reactifyWc('taro-editor-core')

export const Form = reactifyWc('taro-form-core')

export const Input = ReactInput

export const Label = reactifyWc('taro-label-core')

export const Picker = reactifyWc('taro-picker-core')

export const PickerView = reactifyWc('taro-picker-view-core')

export const PickerViewColumn = reactifyWc('taro-picker-view-column-core')

export const Radio = reactifyWc('taro-radio-core')

export const RadioGroup = reactifyWc('taro-radio-group-core')

export const Slider = reactifyWc('taro-slider-core')

export const Switch = reactifyWc('taro-switch-core')

export const CoverImage = reactifyWc('taro-cover-image-core')

export const Textarea = reactifyWc('taro-textarea-core')

export const CoverView = reactifyWc('taro-cover-view-core')

export const MovableArea = reactifyWc('taro-movable-area-core')

export const MovableView = reactifyWc('taro-movable-view-core')

export const ScrollView = reactifyWc('taro-scroll-view-core')

export const Swiper = reactifyWc('taro-swiper-core')

export const SwiperItem = reactifyWc('taro-swiper-item-core')

export const FunctionalPageNavigator = reactifyWc('taro-functional-page-navigator-core')

export const Navigator = reactifyWc('taro-navigator-core')

export const Audio = reactifyWc('taro-audio-core')

export const Camera = reactifyWc('taro-camera-core')

export const Image = reactifyWc('taro-image-core')

export const LivePlayer = reactifyWc('taro-live-player-core')

export const Video = reactifyWc('taro-video-core')

export const Map = reactifyWc('taro-map-core')

export const Canvas = reactifyWc('taro-canvas-core')

export const Ad = reactifyWc('taro-ad-core')

export const OfficialAccount = reactifyWc('taro-official-account-core')

export const OpenData = reactifyWc('taro-open-data-core')

export const WebView = reactifyWc('taro-web-view-core')

export const NavigationBar = reactifyWc('taro-navigation-bar-core')

export const Block = reactifyWc('taro-block-core')

export const CustomWrapper = reactifyWc('taro-custom-wrapper-core')
//packages/taro-components/src/components/view/view.tsx

// Take the View component as an example



// eslint-disable-next-line @typescript-eslint/no-unused-vars

import { Component, Prop, h, ComponentInterface, Host, Listen, State, Event, EventEmitter } from '@stencil/core'

import classNames from 'classnames'



@Component({

  tag: 'taro-view-core'.styleUrl: './style/index.scss'

})

export class View implements ComponentInterface {

  @Prop() hoverClass: string

  @Prop() hoverStartTime = 50

  @Prop() hoverStayTime = 400

  @State() hover = false

  @State() touch = false



  @Event({

    eventName: 'longpress'

  }) onLongPress: EventEmitter



  private timeoutEvent: NodeJS.Timeout

  private startTime = 0



  @Listen('touchstart')

  onTouchStart () {

    if (this.hoverClass) {

      this.touch = true

      setTimeout(() = > {

        if (this.touch) {

          this.hover = true}},this.hoverStartTime)

    }



    this.timeoutEvent = setTimeout(() = > {

      this.onLongPress.emit()

    }, 350)

    this.startTime = Date.now()

  }



  @Listen('touchmove')

  onTouchMove () {

    clearTimeout(this.timeoutEvent)

  }



  @Listen('touchend')

  onTouchEnd () {

    const spanTime = Date.now() - this.startTime

    if (spanTime < 350) {

      clearTimeout(this.timeoutEvent)

    }

    if (this.hoverClass) {

      this.touch = false

      setTimeout(() = > {

        if (!this.touch) {

          this.hover = false}},this.hoverStayTime)

    }

  }



  render () {

    const cls = classNames({

      [`The ${this.hoverClass}`] :this.hover

    })

    return (

      <Host class={cls}>

        <slot></slot>

      </Host>)}}Copy the code

At this point, the component library is implemented.

The NPM package name used in section Taro 3 and its specific functions.

NPM package describe
babel-preset-taro Babel Preset used for the Taro project
@tarojs/taro The Taro core API that is exposed to application developers
@tarojs/shared Utills used internally by Taro
@tarojs/api Public API exposed to all ends of @tarojs/ Taro
@tarojs/taro-h5 H5 API exposed to @tarojs/ Taro
@tarojs/router Taro H5 routing
@tarojs/react The React Renderer is a small program based on the React Reconciler
@tarojs/cli Taro Development Tools
@tarojs/extend Taro extension, including the jQuery API, etc
@tarojs/helper Internally use a set of helper methods for the CLI and Runner
@tarojs/service Taro plug-in kernel
@tarojs/taro-loader Expose the WebPack Loader used by @tarojs/mini-runner and @tarojs/webpack-runner
@tarojs/runner-utils Common utility functions exposed to @tarojs/mini-runner and @tarojs/webpack-runner
@tarojs/webpack-runner Taro H5 end Webpack compilation tool
@tarojs/mini-runner Taro small program side Webpack compilation tool
@tarojs/components Taro Standard component library, H5 edition
@tarojs/taroize Taro small program reverse compiler
@tarojs/with-weapp Run time adapter for reverse transformation
eslint-config-taro Taro ESLint rules
eslint-plugin-taro Taro ESLint plug-in

conclusion

Taro 3 refactoring is designed to address architectural issues and provide support for multiple frameworks. From the previous recompile time, to the current rerun time.

All things being equal, more work done at compile time means less work done at run time and better performance. In the long run, computer hardware becomes more and more redundant, and if you sacrifice a little bit of tolerable performance for greater flexibility and better adaptation of the overall framework, you can greatly improve the development experience.

reference

  1. github.com/NervJS/taro
  2. Mp.weixin.qq.com/s?__biz=MzU…
  3. www.yuque.com/zaotalk/pos…
  4. Juejin. Cn/post / 686809…
  5. Developer.mozilla.org/zh-CN/docs/…
  6. custom-elements-everywhere.com/
  7. zhuanlan.zhihu.com/p/83324871