Three solutions of rendering HTML content in wechat applet and their analysis and problem solving

Time:2020-8-1

Most of the rich text content of web applications is stored in the form of HTML strings. It is no problem to display HTML content through HTML documents. However, in the wechat applet (hereinafter referred to as “applet”), how to render this part of the content?

Three solutions of rendering HTML content in wechat applet

wxParse

When the applet was just launched, it could not directly render HTML content, so a library called “wxparse” was born. Its principle is to parse HTML code into tree structure data, and then render the data through the template of small program.

rich-text

Later, the applet added a “rich text” component to display rich text content. However, this component has a great limitation: the events of all nodes are masked in the component. In other words, in this component, even a simple function such as “preview image” cannot be implemented.

web-view

Later, applets allow web pages to be nested through the “web view” component. Displaying HTML content through web pages is the most compatible solution. However, the performance is poor because you have to load one more page.

When “wepy” meets “wxparse”

Based on the consideration of user experience and functional interaction, we abandoned “rich text” and “web view” as two native components and chose “wxparse”. However, it is found that “wxpasse” can not meet the needs of users

Our applet is based on the “wepy” framework, while “wxparse” is based on the native applet. To make the two compatible, the source code of “wxparse” must be modified.

“Wxparse” simply displays and previews the image of the original img element through the image component. In actual use, the cloud storage interface may be used to reduce the size of the image, so as to achieve the purpose of “displaying with small images and previewing with original images”.

“Wxparse” directly uses the video component of the applet to display video. However, the hierarchy of the video component often causes UI exceptions (for example, blocking a fixed element).

In addition, if you look around the code repository of “wxparse”, you can see that it has not been iterated for two years. Therefore, the idea of rewriting a rich text component based on the “wepy” component mode was initiated, and the result was the “wepy HTML” project.

Implementation process

Parsing HTML

First of all, I still want to parse HTML strings into tree structure data. I use special character separation method. The special characters in HTML are “<” and “>”, the former is the beginning character, and the latter is the ending character.

If the content to be parsed begins with a start character, the content between the beginning character and the ending character is intercepted and parsed as a node.

If the content to be parsed does not begin with a start character, the content from the beginning to the beginning (or the end if the starting character does not exist) is intercepted and parsed as plain text.

The remaining content goes to the next round of parsing until there is no remaining content.

In order to form a tree structure, a context node (the default is the root node) should be maintained during the parsing process

If the intercepted content is the start tag, a child node is created under the current context node according to the tag name and attribute matched. If the label is not a self ending tag (BR, IMG, etc.), the context node is set as a new node.

If the intercepted content is the end tag, the current context node is closed according to the tag name (the context node is set as its parent node).

If it is plain text, a text node is created under the current context node, and the context node remains unchanged.

Context (before parsing) Analysis content Context (after parsing)
Root node <div> div
div <p style=”text-indent: 2em;”> p
p Hello world p
p </p> div
div </div> Root node

After the above process, the HTML string is parsed into a node tree.

contrast

  This component algorithm wxParse parse5
performance 3~6ms About 20ms About 20ms
Fault tolerance difference commonly strong
File size (uncompressed) 6kb 22kb Close to 400KB

It can be seen that the algorithm of this component has an overwhelming advantage over the other two without considering fault tolerance (generating error results rather than throwing exceptions), which meets the needs of small and fast applets. In general, the code generated by the rich text editor will not have syntax errors. Therefore, even if the fault tolerance is poor, the problem is not big (but it needs to be improved).

Template rendering

The rendering of tree structure must involve recursive processing of child nodes. However, the template does not support recursion, which seems to fall into a big hole.

As like as two peas of 13 identical templates, 1 nested calls (2 calls, 2 calls 3,…) are taken to see the implementation of the wxParse template. In other words, it can support up to 12 nesting times. Generally speaking, this depth is enough.

As like as two peas, the WePY framework is built with a mechanism, so it is not necessary to write ten templates that are almost identical.


<!-- wepyhtml-repeat start -->
<template name="wepyhtml-0">
  <block wx:if="{{ content }}" wx:for="{{ content }}">
    <block wx:if="{{ item.type === 'node' }}">
      <view>
        <!-- next template -->
      </view>
    </block>
    <block wx:else>{{ item.text }}</block>
  </block>
</template>
<!-- wepyhtml-repeat end -->

The following is the corresponding build code (we need to install “wepy plugin replace”):

// wepy.config.js
{
  plugins: {
    replace: {
      filter: /\.wxml$/,
      config: {
        find: /<\!-- wepyhtml-repeat start -->([\W\w]+?)<\!-- wepyhtml-repeat end -->/,
        replace(match, tpl) {
          let result = '';
          //No money anyway, write a 20 layer nesting
          for (let i = 0; i <= 20; i++) {
            result += '\n' + tpl
              .replace('wepyhtml-0', 'wepyhtml-' + i)
              .replace(/<\!-- next template -->/g, () => {
                return i === 20 ?
                  '' :
                  `<template is="wepyhtml-${ i + 1 }" wx:if="{{ item.children }}" data="{{ content: item.children"></template>`;
              });
          }
          return result;
        }
      }
    }
  }
}

However, after running, it is found that the nodes of the second and deeper levels are not rendered, indicating that the nesting has failed. If you look at the wxml file generated in dist directory, you can find that the variable name is not the same as that of the component source code


<block wx:if="{{ $htmlContent$wepyHtml$content }}" wx:for="{{ $htmlContent$wepyHtml$content }}">

When generating component code, in order to avoid the conflict between component data and variable name of page data, wepy will add prefix to the variable name of component according to certain rules (such as “$htmlcontent $wepyhtml $” in the code above).

Therefore, variable names with prefixes must also be used when generating nested templates. First add a variable “thisisme” to the component code to identify the prefix:


<!-- wepyhtml-repeat start -->
<template name="wepyhtml-0">
  {{ thisIsMe }}
  <block wx:if="{{ content }}" wx:for="{{ content }}">
    <block wx:if="{{ item.type === 'node' }}">
      <view>
        <!-- next template -->
      </view>
    </block>
    <block wx:else>{{ item.text }}</block>
  </block>
</template>
<!-- wepyhtml-repeat end -->

Then modify the build code:

replace(match, tpl) {
  let result = '';
  let prefix = '';

  //Matches the prefix of thisisme
  tpl = tpl.replace(/\{\{\s*($.*?$)thisIsMe\s*\}\}/, (match, p) => {
    prefix = p;
    return '';
  });

  for (let i = 0; i <= 20; i++) {
    result += '\n' + tpl
      .replace('wepyhtml-0', 'wepyhtml-' + i)
      .replace(/<\!-- next template -->/g, () => {
        return i === 20 ?
          '' :
          `<template is="wepyhtml-${ i + 1 }" wx:if="{{ item.children }}" data="{{ ${ prefix }content: item.children }}"></template>`;
      });
  }

  return result;
}

At this point, the rendering problem is solved.

HTML in wechat applet contains pictures

In order to save traffic and improve the loading speed, when displaying rich text content, the images inside are generally reduced according to the required size, and the original image is displayed only when the small image is previewed.

This mainly involves the modification of node attributes: save the original path of the image (SRC attribute value) to a custom attribute (such as “data SRC”) and add it to the preview graph array.

Modify the SRC attribute value of the image to the reduced image URL (generally cloud service providers provide such URL rules).

When you click on the image, preview with the value of the custom attribute. In order to achieve this requirement, this component provides an onnode create when parsing nodes

onNodeCreate(name, attrs) {
  if (name === 'img') {
    attrs['data-src'] = attrs.src;
    //Preview image array
    this.previewImgs.push(attrs.src);
    //Miniature
    attrs.src = resizeImg(attrs.src, 640);
  }
}

The corresponding template and event processing logic are as follows:


<template name="wepyhtml-img">
  <image mode="widthFix" src="{{ elem.attrs.src }}" data-src="{{ elem.attrs['data-src'] || elem.attrs.src }}" @tap="imgTap"></image>
</template>
//Click on the small picture to see the big picture
imgTap(e) {
  wepy.previewImage({
    current: e.currentTarget.dataset.src,
    urls: this.previewImgs
  });
}

HTML contains video in wechat applet

In applets, the level of video components is high (and cannot be lowered).

If there are elements in the page design that may block the video, it needs some skills to deal with it: hide the video component and use the image component (video cover) to occupy the space; when you click on the image, let the video play in full screen; if you exit the full screen, the playback will be suspended.

The relevant codes are as follows:

<template name="wepyhtml-video">
  <view @tap="videoTap" data-nodeid="{{ elem.nodeId }}">
    Cover video
    <image mode="widthFix" src="{{ elem.attrs.poster }}"></image>
    <! -- play icon -- >
    <image src="./imgs/icon-play.png"></image>
    <! -- video components -- >
    <video style="display: none;" src="{{ elem.attrs.src }}" @fullscreenchange="videoFullscreenChange" @play="videoPlay"></video>
  </view>
</template>
{
  //Click on the cover image to play the video
  videoTap(e) {
    const nodeId = e.currentTarget.dataset.nodeid;
    const context = wepy.createVideoContext('wepyhtml-video-' + nodeId);
    context.play();
    //Under Android wechat, if the video is not visible, call play() to play it
    //You need to call the full screen method again
    if (wepy.getSystemInfoSync().platform === 'android') {
      context.requestFullScreen();
    }
  },
  //The video level is high. In order to avoid blocking other special positioning elements and causing interface abnormality,
  //Force full screen playback
  videoPlay(e) {
    wepy.createVideoContext(e.currentTarget.id).requestFullScreen();
  },
  //Exit full screen and pause
  videoFullscreenChange(e) {
    if (!e.detail.fullScreen) {
      wepy.createVideoContext(e.currentTarget.id).pause();
    }
  }
}

Open Source

Finally, post the project repository of “wepy HTML”: https://github.com/beiliao-web-frontend/wepy-html See the readme in the project for specific usage.

If you encounter problems in the use process, or have good suggestions and opinions, you can put forward them in issues.

With the continuous improvement of wechat small programs, I believe it will not be long before there will be a more perfect solution, then we will not change again and again. For more articles on wechat applet development, please click the related articles below