preface

Before introducing Taro, let’s talk a little bit about applet first.

When it comes to small programs, the first thing that comes to mind should be wechat small programs. Three years ago, wechat took advantage of its high traffic, high user stay time and other advantages, on the basis of the wechat public number, open more capabilities, contributed to the birth of small programs, wechat also from the application level App to the platform of super App, this way to better meet the needs of users lightweight App.

Other vendors have also seen the benefits of the applets business and are following suit, trying to make platform-based apps as well. For example, headlines small program, Alipay small program, jingdong small program. And these small programs are adopted with WeChat applet similar structure and syntax, from a technical point of view, perhaps is not wise, but from the point of view of the business and standardization, this is the choice of the ROI is higher, already has a really feasible scheme, and can rub a wave WeChat applet heat, also can let developers more quickly into their own platform, Kill many birds with one stone.

But there are problems with such an approach. Wechat small program defined its own set of standards at the beginning of its birth, which was incompatible with the existing ecology of the front end. At the beginning, wechat small program did not even have NPM or engineering, which was the real slash-and-burn era. Due to its special two-thread model (rendering layer and logic layer) and the syntax of the four impossibility, the developer’s development cost is high and the development experience is poor. While the small programs of various manufacturers adopt the same architecture and syntax as the small programs of wechat, they also make various extensions based on the characteristics of their own platform, leading to the differentiation between various small programs, and the small program ecology is split.

Applets framework [1]

In this context, “access to the existing front-end ecology, good development experience and multi-terminal” has become the core appeal of small program developers. In response to such an appeal, many small program frameworks have been born, such as Uniapp of DCloud, Taro of JD.com, raX of Ali, etc. Among them, Taro is the leader of these frameworks and is also the topic of this sharing.

This article does not discuss the comparison between these applets frameworks, but you can read for yourself if you are interested:

  • How to choose applets framework in 2020
  • Small program open source framework selection

Introduction of Taro

Taro[2]

Taro is an open source multi-terminal unified development framework, so that we only need to write a set of code, can run on multiple terminals, to a large extent to do write once, Run Everywhere. We don’t call it a applet framework here, because it doesn’t just support applet cross-ends, it supports H5 and even RN.

In terms of development experience, Taro supports Nerv (a React framework), React, and Vue development, and can fully connect with the front-end ecosystem (such as Webpack, NPM, Typescript, etc.), which greatly improves the development experience and development efficiency of developers. Taro has gained a lot of attention in the industry since it was open source for more than two years. Many teams are using Taro for development, and the community is very active. Taro is the most reliable cross-end framework at present.

Recently, the Taro team has launched a year-long redesign of Taro Next, which is the main content of this article, focusing on the small program part of Taro Next.

Taro past

explore

Before introducing Taro Next, I’d like to tell you the story of Taro’s birth.

In 2017, the Taro team abandoned the historical baggage and began to fully embrace the React development approach. They also developed their own React framework Nerv, which further integrated the Taro team’s development ideas with React. When they were developing wechat applets, they were thinking about whether they could use React to write wechat applets. (Here we can see that the Taro team’s starting point is to use React to write wechat applets instead of cross-ends.)

So they began to compare React and applets, and found many similarities, such as life cycles, data updates, event binding, etc., but also significant differences, such as React’s JSX syntax and applets’ template syntax. JSX’s expressiveness and operability are much stronger than template syntax, so how to use JSX to write small program templates has become an urgent problem to be solved.

One of my favorite sayings is that when a language is not powerful enough and there is no other option in the current environment, it will eventually become the target language for compilation. Wechat could not support JSX directly, so they were thinking about how to compile JSX into a small program template. At this point, they thought of the compilation principle, and they thought of Babel. Babel’s core compiler, Babylon, supports JSX syntax parsing. You can use the AST generated by Babel to convert into applets templates.

JSX AST analysis and transformation

implementation

Take wechat mini program as an example, it consists of four parts:

  • Configuration (JSON)
  • View Layer Template (WXML)
  • View Layer Style (WXSS)
  • Logic Layer Logic (JS)

The configuration and style transformations are relatively simple, but the difficulty lies in the transformation of templates and logic

Template conversion

The template conversion essentially converts the Render function in JSX code into a string template that the applet runs.

Here’s an example:

2.x Demo code

Taro’s Babel plugin (@tarojs/transformer-wx) will output the following wechat applet template:

Compiled applets template code

These View and Text are built-in components of Taro. Taro has customized a set of component library specifications based on the wechat small program component library. Taro has built into his own component library some components that are unique to other platforms’ applets, and implemented them in different applets.

But as we saw above, we used a custom component, my-test-component. This component does not exist in the applet, so it cannot run directly. A cross-side component definition is required to compile the custom component into a form that is supported on each side. To this end, Taro offers two things:

  • Cross-end Component Library (@taro/components) : The Taro built-in component we mentioned above (View,TextEtc.), smoothed out the differences between components on different ends
  • Multi-side packaging of custom components: Depending on the platform (applets, Web, etc.) on which you are compiling, program custom components into a form supported by each side.

In fact, the implementation of the first cross-side component library utilizes the capabilities of the second point, only the first point was completed by Taro for us to deal with general application scenarios. The second point is to open up a custom capability to meet the needs of customization.

Logical transformation

Similar component libraries need to be configured for multiple ends, and the differences in capabilities of each end need to be adapted.

Taro has developed a runtime framework that is responsible for adapting the capabilities of each end. This framework has three main functions:

  • ADAPTS basic apis such as componentization schemes and configuration options
  • ADAPTS platform capability related apis, such as calling network requests, taking photos, etc
  • Provides application capabilities such as event bus (Taro.Events), runtime environment-related API (Taro.getEnv) etc.

Platform apis such as Taro. Request exist in the @tarojs/ Taro package, which essentially provides a unified portal that can be replaced by platform at compile time. For example, if you need to package the toutiao code, you would actually introduce @tarojs/taro-tt in @tarojs/taro at compile time to call the toutiao API.

Let’s take a look at the previous Demo code:

2.x Demo code

In fact, in addition to views generated using the render function, when using the component we will use some state, life cycle, etc., Taro at compile time to compile this part of logic, and through the runtime framework provides the ability, make its can perform well in the small program, the following is Taro comparison before and after this part of logic compilation:

Before and after compilation

As you can see, the Component changes from inheriting Component to inheriting BaseComponent, the Render method is replaced by the _createData method, and the entire Component is wrapped in createComponent. Taro 2.x follows the React syntax for development, but when the code is compiled, it has nothing to do with React.

Why did Taro convert this part of logic to look like a Page instance of a small program at compile time, rather than a template conversion, instead of using a runtime solution?

For example, an applet creates a Page by passing in a literal object using the Page method, and does not support classes. If it all depended on compile time, the things we would probably do would be convert classes to objects, convert state to data, convert life cycles like componentDidMount to onReady, convert events from class functions and class property functions to literal object methods, and so on. Like this:

Static compilation

This will make our compile time work very heavy, and the probability of errors in a class that is extremely complex will be higher. A better idea is to implement a createPage method that takes a class as a parameter and returns a literal object needed by the Page method of a small program. Not only does this simplify compile time, but you can also do various manipulations and optimizations on the createPage classes produced at compile time. Having separated the work from the runtime, all you need to do when compiling is to add a line of code Page(CreatentName) at the bottom of the file. This approach is very simple to implement at run time and takes some of the work out of compile time.

For other implementation principles of Taro 2.x, you can check out the Taro Principle, which is described in detail, and will not be repeated here.

The Taro team has converted the React syntax code into runnable applet code. An overall architecture of Taro2 is as follows:

architecture

Taro 2 x architecture

It is divided into two parts, the first part is compile time and the second part is run time. Compile time to React to the user code to compile, into a small program can be run on each side of the code, and then in every small program end with a corresponding runtime framework (ability, because the small program interface is different) ADAPTS, finally let the code to run on the small program end.

Thus, Taro was born (not to mention Taro’s adaptation of H5 and RN and other platforms).

So what are the problems with this architecture?

Existing problems

Compile time

  1. DSL is strongly bound and can only be used with React
  2. Limited JSX support (lots of code to support various JSX syntax)
  3. Does not support the source – the map
  4. Maintaining iterations is difficult

The runtime

  1. Each applet maintains a corresponding runtime framework that needs to be modified synchronously when bugs exist or new features are added

Taro Next life

After analyzing the implementation principles and architectural features of other applets such as MPVue and Remax, Taro’s team gave them some thoughts:

  • Compile time OR runtime: The main reason Taro chose recompile time was for performance reasons. After all, the more work done at compile time, the less work done at runtime, the better performance. But in the long run, computer hardware has a plethora of performance, and it’s worth sacrificing a little more tolerable performance for greater flexibility and better adaptability throughout the framework.
  • Statically compiled OR dynamically built templates: Although Taro’s and MPvue’s templates are statically compiled, the community has plenty of examples of dynamically built templates, such as Remax.
  • DSL constraints: Can you implement a small program development framework that is free of DSL constraints?

With this in mind, the Taro team has shifted its focus from compile time to runtime, where more can be done to extend the flexibility of the framework, but how?

Taro’s team was inspired by the MPVue and Remax frameworks. Here’s a simple illustration of how MPvue and Remax work.

mpvue

Mpvue is strongly bound to Vue, compiling Vue templates into applet templates at compile time (similar to Taro, except that Taro compiles JSX into applet templates), and running Vue on applet templates at run time. Mpvue running Vue is modified to add some handling of small programs.

Vue update rendering process [1]

As we know, Vue also works at compile time and run time. During compilation, the TEMPLATE is ast analyzed by vue-loader to generate a render function. The render function is executed to generate the virtual DOM, and the nodes in the virtual DOM are called vNodes. After getting the virtual DOM, it will do the diff patch operation with the previous virtual DOM to get the minimum VNode that needs to be updated. The view is then updated by calling methods that operate on the real DOM (such as appendChild) to modify the real DOM node.

In the green path below, when Vue is instantiated, data is processed responsefully, and when data changes are detected, the render function is called again and the process is repeated.

However, in the small program, DOM cannot be operated directly, that is, DOM node modification cannot be operated after diff patch. The only way to update the view is to modify the data using the setData method provided by the applet, which allows the applet to update the view based on the changed data. So what does MPvue do?

Mpvue schematic [1]

As we said earlier, the Vue used in MPvue is actually modified to hang an instantiated applet Page on the root instance while the Vue is instantiated. The Page maintains the view’s state data, which is synchronized with the reactive data in the Vue instance. The data in the Page is updated when the Vue triggers a data update, thus updating the view.

In the above figure, mpVue does not call (or can’t call) DOM methods during patch. Instead, it empties all the relevant DOM methods and actually only calls the updateDataToMp function to compute data (data, computed, Props, etc.) and synchronize the changes to the Page (call setData on the Page), so that the data is synchronized and the view is updated (with some optimizations, such as throttling).

The DOM method is null

The Page instance also triggers Vue lifecycle hooks during the applet specific lifecycle (these hooks are injected into the Vue instance at initialization).

Applets hook injection
Applet hook call

In this way, MPVue makes Vue run more completely on small programs (but in reality, MPvue still has a lot of problems, which is left to the reader to think about).

Remax

Remax is strongly bound to React compared to MPVue, and data synchronization and view updates in Remax are quite different from mpVue. As you can see from our previous introduction to MPVue, MPvue can be half compiled (Vue templates are compiled into applet templates) and half run (modified Vue), while Remax is basically a fully run applet framework. Let me take a quick look at how Remax works in applets development.

The renderer

React Architecture [10]

React – Core (React -core) contains the React global API. The React-Reconciler does the virtual DOM generation and patch diff, and notifies the next layer (the renderer) to render the virtual DOM into the corresponding host view.

We see a layer of renderer between the React-Reconciler and the target hosting platform (browsers, clients, shells, etc.), the Renderer, which acts as a bridge between the target hosting platform and the virtual DOM, enabling the details of rendering. It renders after receiving some instructions from the React-Reconciler by calling the rendering interface of the target host platform.

Here’s a little more information about the React renderer. React provides a unified way to implement renderers. Developers only need to provide two things to customize a renderer:

Renderer configuration
  • Host Configuration (hostConfig) : The React-Reconciler requires the host to provide a host configuration containing adaptation methods and configuration items that define how to create node instances, build the number of nodes, and update them. HostConfig is passed in when the Reconciler is instantiated.

    HostConfig structure
  • Render function: Render functions are used to mount the component tree to the specified target node (typically to the root element). For example, the ReactDOM renderer we use on the Web provides a render method called ReactDOM. Render will mount our React root node to the specified DOM tag. For example, reactdom.render (
    , document.querySelector(‘# App ‘). The root element is the entry to the entire tree of components, which is used to hold information and manage updates and rendering of all the following nodes.

When a render function is called to mount a root or child node rendering update, the reconciler invokes some of the adaptation methods defined in the host configuration to allow the host environment to do the actual rendering.

Remax implements a renderer that renders the virtual DOM to the applet view (targeted at the applet host platform) :

Remax mode of action [10]

So how does Remax render the virtual DOM onto the applet view?

As we have said before, the architecture of the applet is a dual-thread architecture separated from the rendering layer and the logic layer. The applet shields THE DOM. Our code runs in a worker thread and cannot directly operate the DOM of the view layer.

Applets two-threaded architecture [10]

As mentioned earlier, Remax is basically a fully run-time applet framework that does not statically compile JSX into applet templates. Applets also do not support DOM manipulation, so they cannot dynamically generate the DOM, so how to generate the applets view template at run time?

VNode

Remax introduces a tree structure called the VNode (note the difference from the virtual DOM) and lets React inform the renderer (Remax) to modify the VNode after the reconciliation (diff patch of the virtual DOM). The basic structure of a VNode is as follows:

Remax VNode structure

Regardless of what each property does, as a whole, VNode implements properties and node operations similar to those in the DOM (in this case, vNodes). When performing operations such as data updates, methods are called to update vNodes synchronously. When the update action is performed, the toJSON method of the node to be updated is called to generate a JSON object, which is updated to the data in the applet Page.

The first diagram shows the React JSX code and the second diagram shows the VNode tree structure:

The Demo JSX code
Demo VNode tree structure

The next thing to do is how to display the data in the applet template.

As we mentioned earlier, applets can’t manipulate the DOM directly, let alone generate it dynamically. So how can a relatively static applets template generate a view corresponding to a relatively dynamic VNode tree?

Template recursive call

Remax thought of a very wicked way, small program you didn’t let me dynamically generate DOM, so I write the content to generate in advance, as a template. Why don’t I just dynamically reference these templates when I need them?

Template definitions and recursive references

As you can see, the Remax template iterates through the child elements of the root node, selects a template to render the child elements based on the type of each child element, and then within each template iterates through the child elements of the current element, recursively traversing the entire node tree.

The diagram above is just a simplified schematic diagram, and there are some more complex processes in the source code. Remax actually generates these templates through EJS and adds handling for element attributes, events, and template recursion. Here’s what the actual generated template looks like (a bit long but not complicated) :

Remax applet template

During the React status update, vNodes are updated synchronously and diff operations are performed to compare the old and new VNodes to find the least changed data and update it to the data in the applet Page to update the applet view.

Life Cycle (Extended reading)

In some of the previous introductions, we didn’t cover how the applet life cycle method correctly notifies the React component in Remax, or how VNode updates the data in the applet Page. Because the latter logic is actually contained in the former implementation, and the former logic is more complex, if you want to describe clearly, you need to combine the source code to tell, not suitable for rapid absorption.

So here’s a simple description of some of the things Remax does with the lifecycle: Remax wrote a plug-in for babel-Loader that wraps the logic (instantiating a Page object) around the Page function of the applet at compile time, so that the applet can trigger the life cycle of the logic correctly. Create this logic dynamically at runtime (by calling createPageConfig) and invoke the React component’s lifecycle when the lifecycle is triggered (much like the transformation of the logic in Taro 2.x described earlier).

The part about the life cycle is for extended reading, not easy to understand, focusing on understanding the overall design, but not focusing on details for the time being.

Remax actually does something at compile time and at run time. Take Page as an example. At run time, Remax creates a Page parameter object using a createPageConfig function. This parameter object contains small program life cycle methods such as onLoad and onShow. In these methods, the React component corresponding to the Page is taken at onLoad, and the call appendInitialChild is notified to the React-Reconciler call, Remax mounts the child VNode to the specified parent VNode (when mounted, an update request logic is pushed to the update queue corresponding to the container property of the parent VNode). After mounting, it triggers a resetAfterCommit hook for the React-Reconciler, In this hook, Remax performs some logic to update the App and Page (calling the applet’s setData, $spliceData, etc.) based on the update requests in the container’s update queue. The updated VNode Tree is then updated to the data in the Page to update the view. The React component instance is then obtained by Ref and the component’s lifecycle method is called when other lifecycle methods are triggered. At compile time, Remax implements a plug-in for babel-Loader that wraps the parameter object returned by createPageConfig with the Page function at compile time, allowing the applet to trigger the Page lifecycle method normally. Call the life cycle method of the React component corresponding to Page through the above steps.

summary

By doing this, Remax is finally able to run React entirely on applets and have a complete development experience. For cross-end functionality, in addition to some common runtime code, Remax maintains a separate set of runtime code for each side. The main thing to do is compatibility of components, apis, and templates. This section will not be explored in detail (it is worth mentioning that remax-toutiao is a byte in support of PR#166).

Remax runtime directory structure at each end

Taro Next

Finally, it’s our turn to Taro Next. Let’s talk briefly about Taro Next’s design ideas.

Design ideas and architecture

The Taro team had an Epiphany after referring to the implementation and design ideas of small program frameworks such as MPvue and Remax (mainly Remax). They started to think about the nature of the front end in terms of the browser: Whatever framework you use, React or Vue, is essentially JavaScript code that ends up calling the browser’s DOM/BOM apis like appendChild and removeChild. These apis essentially invoke the capabilities provided by the browser to do something.

The nature of frameworks 1[1]

Taro Next implemented a simplified and efficient version of the DOM and BOM at runtime (in the @tarojs/ Runtime package). And through the ProvidePlugin of Webpack, the DOM, BOM and other APIS are injected into the small program logic layer at run time to call the framework. That way, any framework will run on Taro Next.

Taro Next Design Idea [2]
Taro DOM UML diagrams
Taro DOM injection method

Having said that, there is an additional set of logic to be maintained for each framework at run time due to the special logic inherent in applets, such as life cycle, App, Page, component, etc., which varies from framework to framework and requires some processing and injection of life cycle in different ways. However, this set of logic is relatively simple, and the Taro team plans to open this part of logic to the public in the form of plug-ins, so that the community can maintain and support various frameworks.

So overall, Taro Next’s architecture on applets is as follows:

Taro Next Architecture Plan [2]

Taro Next, a small program framework for the implementation of ideas on the end.

What? You said Taro Next’s view rendering update process I haven’t said yet? Actually, I said that in Remax

View rendering update process

Here’s a comparison of Remax and Taro Next’s render flow for React:

Remax vs. Taro Next rendering process

I hate to admit it, but after analyzing the source code, the similarity is as high as 90%. Just like Remax, Taro Next implements a renderer for React, called taro-React. The React-Reconciler notifies them to call an API like appendChild to update the view, and Remax is synchronized to the data in the applet Page by updating the VNode. Taro Next updates TaroElement to Page’s data, but TaroElement does an extra layer of hydrate before it is synchronized to the applet, which only results in data that will affect the view, thus decreasing the amount of data that needs to be synchronized. Optimized certain performance.

Once the data is synchronized, the view is rendered recursively using some written templates (like Remax). This step is completely consistent with Remax.

So what’s the difference between them?

Differences with Remax

The React rendering process does look the same.

In fact, the two are of different degrees of abstraction. Remax has created a data structure for the React-Reconciler and for data synchronization with small programs. It is strongly bound to React, and many core logic would need to be modified if it were replaced with another framework. Taro Next implements a simple and efficient DOM/BOM and event mechanism by referring to Element, Node and other types in the browser. Although it is similar in the React rendering process, it can be supported by other frameworks at a very low cost. This is something Remax can’t do.

In addition, Taro has many excellent features that Remax does not have.

For now, instead of discussing the implementation of Taro Next and how it compares to other frameworks, we will simply look at what Taro Next offers from a user’s perspective compared to Taro 2.x.

Taro Next new features

In conclusion, Taro Next is more powerful, faster, and more flexible than its predecessor Taro 2.x.

More powerful

No DSL restrictions: Want to write applets with React? You can! Want to write applets in Vue? Can also! Want to write applets with the latest Vue3? All can be! Taro Next is DSL free to meet the needs of our various frameworks.

Support HTML Rendering: Want to support direct rendering of tag strings? You can! The new Taro Next implementation mechanism allows us to dynamically render the tag elements we want to render.

Support for cross-framework Components: Taro leverages Web Components to provide a suite of components that can be used across multiple platforms and frameworks, and users who want to write cross-framework components can do so through the apis provided by Taro Next.

More quickly

Faster build packaging: With Taro Next’s near-full runtime architecture, builds are faster by eliminating the need for complex AST operations at compile time.

Runtime performance optimization: Taro Next has put a lot of effort into performance optimization since its new architecture became nearly fully operational:

  • Constant template size: Due to Taro Next’s use of dynamic template rendering, the template size is always fixed as the project grows;
  • Streamlined and efficient DOM/BOM: During the construction and update phase of the Taro DOM Tree, an efficient, streamlined DOM/BOM API was implemented, and only necessary. Compared to JS-DOM, the Total number of Taro DOM/BOM API code is less than 1000 lines, greatly ensuring the performance of the Taro DOM Tree build update.
  • More fine-grained updates: Taro Next’s updates are DOM level updates and are more fine-grained than Data level updates because some Data does not cause DOM updates. Taro Next does a hydrate operation to identify Data that will affect view updates, reducing the amount of Data. Improved performance.
A more flexible

Plug-in system Design: Taro Next provides a plug-in system (inspired by Remax) that allows users to write Taro plug-ins to expand their capabilities based on business scenarios.

Compile configurations exposed: Taro Next exposes some internal compile-time configurations and WebPack-related configurations for users to customize, such as the number of template layers for recursive invocation of applets, global constant injection, and more.

Custom dependency loading: Taro Next will slim down its dependencies by no longer maintaining libraries such as taro-Redux and taro-Mobx, but instead letting users install and use these libraries themselves via NPM when they need them.

conclusion

This article discusses the design idea and architecture of Taro Next, and does not describe the use of Taro Next. In my opinion, the use of Taro Next is actually not very different from that of Taro 2.x (except the use of Vue in Taro). And this kind of article already very much, the reader can search to consult by oneself.

Many people think Taro Next is just copying Remax, but I think the commonality of design ideas is very common in the programming world, and some architectural designs in programming refer to real-world practices, so it’s pointless to argue about these personal opinions. Taro Next is also more in-depth than Remax in terms of design ideas, and has many excellent features, as well as a relatively complete tool chain and ecology. The community is active, the team is reliable, and the corresponding speed of iteration and issue is also the fastest among the several small program frameworks.

Of course, Taro Next also has many problems at present, as can be seen from the number of issues on Github. However, Taro has a reliable team and an active community. I think these problems will be gradually improved and solved as time goes by.

The resources

This article refers to more information, if violated your rights and interests, please contact me in time, I will timely correct or delete, thank you!

[1] Full text of lecture on Cross-framework Development of Small Programs

[2] How to use Taro Next to span business lines

[3] Taro

[4] Taro principle summary

[5] the Taro’s official website

[6] Write your own React renderer: Use Remax as an example

[7] Analysis of Remax principle

[8] Developing wechat applets with vue.js: Open source Framework MPvue analysis

[9] React source code overview

[10] Remax – Use real React to build applets