微信小程序如何渲染html内容「示例讲解」

1. 引言

微信小程序里目前原生的RichText组件只支持部分HTML标签的显示。但是,在实际开发中,我们可能需要渲染更为复杂的HTML内容。本文将通过一个实际案例,介绍如何在微信小程序中渲染HTML内容。

2. 业务需求与方案设计

2.1 业务需求

在我们的项目中,有一个需求是显示来自服务器的HTML格式的富文本内容,如图所示:

![需求截图](https://s1.ax1x.com/2020/10/28/BYYJFO.png)

为了实现这个需求,我们做了如下的方案设计:

2.2 方案设计

我们需要将HTML内容转化成小程序所支持的nodes列表来进行渲染。我们选择了第三方库[htmlparser](https://github.com/blowsie/Pure-JavaScript-HTML5-Parser)来将HTML字符串转化成AST,再通过遍历AST生成小程序所支持的nodes列表。

3. HTML字符串转成AST

3.1 安装htmlparser

在项目中安装htmlparser:

npm install htmlparser --save

3.2 使用htmlparser解析HTML字符串

使用htmlparser解析HTML字符串会返回一个AST,具体使用方式如下:

const { parse } = require('htmlparser');

/**

* @description 将HTML字符串转成AST

* @param {String} html HTML字符串

* @returns {Object} AST

*/

function html2ast(html) {

const handler = new htmlparser.DomHandler(function (error, dom) {

if (error) {

console.error('解析HTML字符串出错', error);

throw error;

}

return dom;

});

const parser = new htmlparser.Parser(handler);

parser.parseComplete(html);

const ast = handler.dom;

return ast;

}

4. AST转成nodes列表

4.1 AST的遍历

我们需要按照一定的顺序遍历AST,将AST转成nodes列表。遍历AST的方式有以下三种:

1. 深度优先遍历

2. 广度优先遍历

3. 前序遍历

由于深度优先遍历和广度优先遍历都需要借助队列和栈等数据结构,而前序遍历则不需要,且符合我们的视图生成需求,所以我们选择前序遍历AST。

4.2 AST节点类型与属性的处理

不同类型的AST节点对应着微信小程序中不同的节点类型和属性,我们需要将AST的节点类型和属性转成相应的小程序节点类型和属性。

以下是我们实现的AST节点类型转小程序节点类型和属性的映射表:

const Tag2Node = {

'html': 'view',

'head': 'view',

'body': 'view',

'base': 'aria-component',

'link': 'aria-component',

'meta': 'aria-component',

'style': 'view',

'script': 'view',

'noscript': 'view',

'template': 'view',

'slot': 'view',

'img': 'image',

'a': 'navigator',

'button': 'button',

'input': 'input',

'textarea': 'textarea',

'select': 'picker',

'option': 'picker-view-column',

'form': 'form',

'label': 'label',

'fieldset': 'view',

'legend': 'view',

'datalist': 'picker',

'optgroup': 'view',

'output': 'view',

'progress': 'progress',

'meter': 'view',

'details': 'view',

'summary': 'view',

'menu': 'view',

'menuitem': 'view',

'dialog': 'view',

'h1': 'view',

'h2': 'view',

'h3': 'view',

'h4': 'view',

'h5': 'view',

'h6': 'view',

'p': 'view',

'hr': 'view',

'pre': 'view',

'blockquote': 'view',

'ol': 'view',

'ul': 'view',

'li': 'view',

'dl': 'view',

'dt': 'view',

'figure': 'view',

'figcaption': 'view',

'div': 'view',

'caption': 'view',

'thead': 'view',

'tbody': 'view',

'tfoot': 'view',

'tr': 'view',

'th': 'view',

'td': 'view',

'col': 'view',

'colgroup': 'view'

};

function getTagByNodeType(nodeType) {

if (nodeType === 'text') {

return 'text';

} else if (nodeType === 'comment') {

return 'comment';

} else {

return Tag2Node[nodeType] || 'view';

}

}

function getAttrsByNode(node) {

const attrs = {};

for (let key in node.attribs) {

if (key === 'class') {

attrs['class'] = node.attribs[key];

} else if (key === 'style') {

attrs['style'] = node.attribs[key].replace(/ /g, '').replace(/;$/g, '');

} else if (/^data-/.test(key)) {

attrs[key] = node.attribs[key];

} else if (/^on/.test(key)) {

attrs[key] = node.attribs[key];

} else if (/^id/.test(key)) {

attrs[key] = node.attribs[key];

} else if (/^aria-/.test(key)) {

attrs[key] = node.attribs[key];

}

}

return attrs;

}

4.3 AST节点转节点对象

将AST节点转成节点对象的代码如下:

function nodeToObj(node) {

const tag = getTagByNodeType(node.type);

if (tag === 'view') {

return null;

}

const attrs = getAttrsByNode(node);

const children = [];

const obj = {

tag,

attrs,

children

};

if (tag === 'text') {

obj['text'] = node.data.trim();

}

if (node.children && node.children.length > 0) {

node.children.forEach(childNode => {

const childObj = nodeToObj(childNode);

if (childObj) {

children.push(childObj);

}

});

}

return obj;

}

4.4 AST遍历与节点转换

最终的代码如下:

function traverseAST(ast) {

const nodes = [];

const stack = [];

ast.forEach(node => {

stack.unshift(node);

});

while (stack.length) {

const node = stack.shift();

const obj = nodeToObj(node);

if (obj) {

if (nodes.length > 0) {

const parent = nodes[nodes.length - 1];

parent.children.push(obj);

}

nodes.push(obj);

} else {

if (nodes.length > 0) {

const lastNode = nodes[nodes.length - 1];

if (lastNode.tag === getTagByNodeType(node.type)) {

nodes.pop();

} else {

throw new Error(`节点类型不匹配,期望类型为${lastNode.tag}, 实际类型为${node.type}`);

}

}

}

if (node.children && node.children.length > 0) {

node.children.forEach(childNode => {

stack.unshift(childNode);

});

}

}

return nodes;

}

5. 将nodes列表渲染到视图中

5.1 创建template和渲染数据

我们需要在视图中创建一个template用来渲染nodes列表,具体代码如下:

<template name="customRichText">

<block wx:if="{{nodes.length > 0}}">

<block wx:for="{{nodes}}" wx:key="{{index}}">

<block wx:if="{{item.tag === 'text'}}">

<text>{{item.text}}</text>

</block>

<block wx:else="{{item.tag !== 'text'}}">

<{{item.tag}}

wx:for="{{item.attrs}}"

wx:key="{{index}}"

wx:if="{{value}}"

{{key}}="{{value}}"

></{{item.tag}}>

<template is="customRichText" wx:if="{{item.children}}" data="{{nodes: item.children}}"></template>

</block>

</block>

</block>

</template>

渲染数据是一个nodes数组。

5.2 渲染数据转换

我们还需要将AST转化的nodes数组转成渲染数据格式,用于渲染到视图中,具体代码如下:

function nodes2data(nodes) {

const data = {};

data['nodes'] = nodes;

return data;

}

5.3 完整渲染代码

将渲染模板和渲染数据结合,并在视图中引入即可。

完整代码如下:

const { parse } = require('htmlparser');

/**

* @description 将HTML字符串转成AST

* @param {String} html HTML字符串

* @returns {Object} AST

*/

function html2ast(html) {

const handler = new htmlparser.DomHandler(function (error, dom) {

if (error) {

console.error('解析HTML字符串出错', error);

throw error;

}

return dom;

});

const parser = new htmlparser.Parser(handler);

parser.parseComplete(html);

const ast = handler.dom;

return ast;

}

const Tag2Node = {

'html': 'view',

'head': 'view',

'body': 'view',

'base': 'aria-component',

'link': 'aria-component',

'meta': 'aria-component',

'style': 'view',

'script': 'view',

'noscript': 'view',

'template': 'view',

'slot': 'view',

'img': 'image',

'a': 'navigator',

'button': 'button',

'input': 'input',

'textarea': 'textarea',

'select': 'picker',

'option': 'picker-view-column',

'form': 'form',

'label': 'label',

'fieldset': 'view',

'legend': 'view',

'datalist': 'picker',

'optgroup': 'view',

'output': 'view',

'progress': 'progress',

'meter': 'view',

'details': 'view',

'summary': 'view',

'menu': 'view',

'menuitem': 'view',

'dialog': 'view',

'h1': 'view',

'h2': 'view',

'h3': 'view',

'h4': 'view',

'h5': 'view',

'h6': 'view',

'p': 'view',

'hr': 'view',

'pre': 'view',

'blockquote': 'view',

'ol': 'view',

'ul': 'view',

'li': 'view',

'dl': 'view',

'dt': 'view',

'figure': 'view',

'figcaption': 'view',

'div': 'view',

'caption': 'view',

'thead': 'view',

'tbody': 'view',

'tfoot': 'view',

'tr': 'view',

'th': 'view',

'td': 'view',

'col': 'view',

'colgroup': 'view'

};

function getTagByNodeType(nodeType) {

if (nodeType === 'text') {

return 'text';

} else if (nodeType === 'comment') {

return 'comment';

} else {

return Tag2Node[nodeType] || 'view';

}

}

function getAttrsByNode(node) {

const attrs = {};

for (let key in node.attribs) {

if (key === 'class') {

attrs['class'] = node.attribs[key];

} else if (key === 'style') {

attrs['style'] = node.attribs[key].replace(/ /g, '').replace(/;$/g, '');

} else if (/^data-/.test(key)) {

attrs[key] = node.attribs[key];

} else if (/^on/.test(key)) {

attrs[key] = node.attribs[key];

} else if (/^id/.test(key)) {

attrs[key] = node.attribs[key];

} else if (/^aria-/.test(key)) {

attrs[key] = node.attribs[key];

}

}

return attrs;

}

function nodeToObj(node) {

const tag = getTagByNodeType(node.type);

if (tag === 'view') {

return null;

}

const attrs = getAttrsByNode(node);

const children = [];

const obj = {

tag,

attrs,

children

};

if (tag === 'text') {

obj['text'] = node.data.trim();

}

if (node.children && node.children.length > 0) {

node.children.forEach(childNode => {

const childObj = nodeToObj(childNode);

if (childObj) {

children.push(childObj);

}

});

}

return obj;

}

function traverseAST(ast) {

const nodes = [];

const stack = [];

ast.forEach(node => {

stack.unshift(node);

});

while (stack.length) {

const node = stack.shift();

const obj = nodeToObj(node);

if (obj) {

if (nodes.length > 0) {

const parent = nodes[nodes.length - 1];

parent.children.push(obj);

}

nodes.push(obj);

} else {

if (nodes.length > 0) {

const lastNode = nodes[nodes.length - 1];

if (lastNode.tag === getTagByNodeType(node.type)) {

nodes.pop();

} else {

throw new Error(`节点类型不匹配,期望类型为${lastNode.tag}, 实际类型为${node.type}`);

}

}

}

if (node.children && node.children.length > 0) {

node.children.forEach(childNode => {

stack.unshift(childNode);

});

}

}

return nodes;

}

function nodes2data(nodes) {

const data = {};

data['nodes'] = nodes;

return data;

}

module.exports = {

html2ast,

traverseAST,

nodes2data

};

使用的时候,只需要将HTML字符串转成AST,然后再通过AST转成需渲染的nodes数组,最后将nodes数组转成渲染数据,然后在视图上引入即可。

免责声明:本文来自互联网,本站所有信息(包括但不限于文字、视频、音频、数据及图表),不保证该信息的准确性、真实性、完整性、有效性、及时性、原创性等,版权归属于原作者,如无意侵犯媒体或个人知识产权,请来电或致函告之,本站将在第一时间处理。猿码集站发布此文目的在于促进信息交流,此文观点与本站立场无关,不承担任何责任。