【愚公系列】2022年08月 微信小程序-富文本中预览图片组件封装
【摘要】 一、富文本使用 1.富文本中预览图片 1.1 组件封装使用 1.1.1 组件封装项目结构parserhandler.wxsvar inlineTags = { abbr: 1, b: 1, big: 1, code: 1, del: 1, em: 1, i: 1, ins: 1, label: 1, q: 1, small: 1, span: 1, strong:...
一、富文本使用
1.富文本中预览图片
1.1 组件封装使用
1.1.1 组件封装
项目结构parser
handler.wxs
var inlineTags = {
abbr: 1,
b: 1,
big: 1,
code: 1,
del: 1,
em: 1,
i: 1,
ins: 1,
label: 1,
q: 1,
small: 1,
span: 1,
strong: 1
}
module.exports = {
// 获取图片大小
load: function (e) {
if (e.target.dataset.auto)
e.instance.setStyle({
width: e.detail.width + 'px'
})
},
// 链接点击态
visited: function (e, owner) {
if (!e.instance.hasClass('_visited'))
e.instance.addClass('_visited')
owner.callMethod('linkpress', e);
},
// 是否通过 rich-text 显示
useRichText: function (item) {
return !item.c && !inlineTags[item.name] && (item.attrs.style || '').indexOf('display:inline') == -1
}
}
trees.js
Component({
data: {
canIUse: !!wx.chooseMessageFile
},
properties: {
nodes: Array,
lazyLoad: Boolean
},
methods: {
// 视频播放事件
play(e) {
this.top.group && this.top.group.pause(this.top.i);
if (this.top.videoContexts.length > 1 && this.top.data.autopause)
for (var i = this.top.videoContexts.length; i--;)
if (this.top.videoContexts[i].id != e.currentTarget.id)
this.top.videoContexts[i].pause();
},
// 图片点击事件
imgtap(e) {
var attrs = e.target.dataset.attrs;
if (!attrs.ignore) {
var preview = true;
this.top.triggerEvent('imgtap', {
id: e.target.id,
src: attrs.src,
ignore: () => preview = false
})
if (preview) {
if (this.top.group) return this.top.group.preview(this.top.i, attrs.i);
var urls = this.top.imgList,
current = urls[attrs.i] ? urls[attrs.i] : (urls = [attrs.src], attrs.src);
wx.previewImage({
current,
urls
})
}
}
},
// 链接点击事件
linkpress(e) {
var jump = true,
attrs = e.currentTarget.dataset.attrs;
attrs.ignore = () => jump = false;
this.top.triggerEvent('linkpress', attrs);
if (jump) {
if (attrs['app-id'])
wx.navigateToMiniProgram({
appId: attrs['app-id'],
path: attrs.path
})
else if (attrs.href) {
if (attrs.href[0] == '#')
this.top.navigateTo({
id: attrs.href.substring(1)
})
else if (attrs.href.indexOf('http') == 0 || attrs.href.indexOf('//') == 0)
wx.setClipboardData({
data: attrs.href,
success: () =>
wx.showToast({
title: '链接已复制'
})
})
else
wx.navigateTo({
url: attrs.href,
})
}
}
},
// 错误事件
error(e) {
var context, src = '',
source = e.target.dataset.source,
i = e.target.dataset.i,
node = this.data.nodes[i];
if (source == 'video' || source == 'audio') {
// 加载其他 source
var index = (node.i || 0) + 1;
if (index < node.attrs.source.length)
return this.setData({
[`nodes[${i}].i`]: index
})
if (this.top) context = this.top.getVideoContext(e.target.id);
} else if (source == 'img')
context = {
setSrc: (newSrc) => src = newSrc
}
this.top && this.top.triggerEvent('error', {
source,
target: e.target,
context,
...e.detail
})
if (source == 'img') {
var data = {
[`nodes[${i}].attrs.src`]: src
}
if (!src) data[`nodes[${i}].err`] = 1;
this.setData(data);
}
},
// 加载视频
loadVideo(e) {
var i = e.target.dataset.i;
this.setData({
[`nodes[${i}].lazyLoad`]: false,
[`nodes[${i}].attrs.autoplay`]: true
})
},
// 加载图片
loadImg(e) {
var data = e.target.dataset;
if (data.auto)
this.setData({
[`nodes[${data.i}].attrs.style`]: `${this.data.nodes[data.i].attrs.style};width:${e.detail.width}px`
})
}
}
})
trees.json
{
"component": true,
"usingComponents": {
"trees": "./trees"
}
}
trees.wxml
<!--trees 递归子组件-->
<wxs module="handler" src="./handler.wxs" />
<block wx:for="{{nodes}}" wx:key="index" wx:for-item="n">
<rich-text wx:if="{{n.en||n.svg||n.err}}" class="_svg" nodes="{{[n]}}" />
<!--图片-->
<image wx:elif="{{n.name=='img'}}" class="_img" style="{{n.attrs.style}}" src="{{n.attrs.src}}" lazy-load="{{lazyLoad}}" mode="{{n.mode||'widthFix'}}" show-menu-by-longpress="{{!n.attrs.ignore}}" webp="{{n.webp}}" data-attrs="{{n.attrs}}" data-i="{{index}}" data-auto="{{n.auto}}" data-source="img" bindtap="imgtap" bindload="{{canIUse?handler.load:'loadImg'}}" binderror="error" />
<!--文本-->
<text wx:elif="{{n.type=='text'}}" decode>{{n.text}}</text>
<text wx:elif="{{n.name=='br'}}">\n</text>
<!--链接-->
<view wx:elif="{{n.name=='a'}}" class="_a {{n.attrs.class}}" hover-class="_hover" style="{{n.attrs.style}}" data-attrs="{{n.attrs}}" bindtap="{{canIUse?handler.visited:'linkpress'}}">
<trees nodes="{{n.children}}" />
</view>
<!--视频-->
<block wx:elif="{{n.name=='video'}}">
<view wx:if="{{n.lazyLoad}}" id="{{n.attrs.id}}" class="_video {{n.attrs.class}}" style="{{n.attrs.style}}" data-i="{{index}}" bindtap="loadVideo" />
<video wx:else id="{{n.attrs.id}}" class="{{n.attrs.class}}" style="{{n.attrs.style}}" autoplay="{{n.attrs.autoplay}}" controls="{{n.attrs.controls}}" loop="{{n.attrs.loop}}" muted="{{n.attrs.muted}}" poster="{{n.attrs.poster}}" src="{{n.attrs.source[n.i||0]}}" unit-id="{{n.attrs['unit-id']}}" data-i="{{index}}" data-source="video" binderror="error" bindplay="play" />
</block>
<!--音频-->
<audio wx:elif="{{n.name=='audio'}}" id="{{n.attrs.id}}" class="{{n.attrs.class}}" style="{{n.attrs.style}}" author="{{n.attrs.author}}" autoplay="{{n.attrs.autoplay}}" controls="{{n.attrs.controls}}" loop="{{n.attrs.loop}}" name="{{n.attrs.name}}" poster="{{n.attrs.poster}}" src="{{n.attrs.source[n.i||0]}}" data-i="{{index}}" data-source="audio" binderror="error" bindplay="play" />
<!--广告-->
<ad wx:elif="{{n.name=='ad'}}" class="{{n.attrs.class}}" style="{{n.attrs.style}}" unit-id="{{n.attrs['unit-id']}}" data-source="ad" binderror="error" />
<!--列表-->
<view wx:elif="{{n.name=='li'}}" class="{{n.attrs.class}}" style="{{n.attrs.style}};display:flex">
<view wx:if="{{n.type=='ol'}}" class="_ol-bef">{{n.num}}</view>
<view wx:else class="_ul-bef">
<view wx:if="{{n.floor%3==0}}" class="_ul-p1">█</view>
<view wx:elif="{{n.floor%3==2}}" class="_ul-p2" />
<view wx:else class="_ul-p1" style="border-radius:50%">█</view>
</view>
<trees class="_node _li" lazy-load="{{lazyLoad}}" nodes="{{n.children}}" />
</view>
<!--富文本-->
<rich-text wx:elif="{{handler.useRichText(n)}}" id="{{n.attrs.id}}" class="_p __{{n.name}}" nodes="{{[n]}}" />
<!--继续递归-->
<trees wx:else id="{{n.attrs.id}}" class="_node _{{n.name}} {{n.attrs.class}}" style="{{n.attrs.style}}" lazy-load="{{lazyLoad}}" nodes="{{n.children}}" />
</block>
parser.js
var cache = {},
Parser = require('./libs/MpHtmlParser.js'),
fs = wx.getFileSystemManager && wx.getFileSystemManager();
try {
var dom = require('./libs/document.js.js');
} catch (e) {}
// 计算 cache 的 key
function hash(str) {
for (var i = str.length, val = 5381; i--;)
val += (val << 5) + str.charCodeAt(i);
return val;
}
Component({
options: {
pureDataPattern: /^[acdgtux]|W/
},
properties: {
'html': {
type: null,
observer(html) {
if (this._refresh) this._refresh = false;
else this.setContent(html, false, true);
}
},
'autosetTitle': {
type: Boolean,
value: true
},
'autopause': {
type: Boolean,
value: true
},
'compress': Number,
'domain': String,
'gestureZoom': Boolean,
'lazyLoad': Boolean,
'selectable': Boolean,
'tagStyle': Object,
'showWithAnimation': Boolean,
'useAnchor': Boolean,
'useCache': Boolean,
'xml': Boolean
},
relations: {
'../parser-group/parser-group': {
type: 'ancestor'
}
},
created() {
// 图片数组
this.imgList = [];
this.imgList.setItem = function(i, src) {
if (!i || !src) return;
// 去重
if (src.indexOf('http') == 0 && this.includes(src)) {
var newSrc = '';
for (var j = 0, c; c = src[j]; j++) {
if (c == '/' && src[j - 1] != '/' && src[j + 1] != '/') break;
newSrc += Math.random() > 0.5 ? c.toUpperCase() : c;
}
newSrc += src.substr(j);
return this[i] = newSrc;
}
this[i] = src;
// 暂存 data src
if (src.includes('data:image')) {
var info = src.match(/data:image\/(\S+?);(\S+?),(.+)/);
if (!info) return;
var filePath = `${wx.env.USER_DATA_PATH}/${Date.now()}.${info[1]}`;
fs && fs.writeFile({
filePath,
data: info[3],
encoding: info[2],
success: () => this[i] = filePath
})
}
}
this.imgList.each = function(f) {
for (var i = 0, len = this.length; i < len; i++)
this.setItem(i, f(this[i], i, this));
}
},
detached() {
// 删除暂存
this.imgList.each(src => {
if (src && src.includes(wx.env.USER_DATA_PATH) && fs)
fs.unlink({
filePath: src
})
})
clearInterval(this._timer);
},
methods: {
// 锚点跳转
navigateTo(obj) {
if (!this.data.useAnchor)
return obj.fail && obj.fail({
errMsg: 'Anchor is disabled'
})
this.createSelectorQuery()
.select('.top' + (obj.id ? '>>>#' + obj.id : '')).boundingClientRect()
.selectViewport().scrollOffset().exec(res => {
if (!res[0])
return this.group ? this.group.navigateTo(this.i, obj) :
obj.fail && obj.fail({
errMsg: 'Label not found'
});
obj.scrollTop = res[1].scrollTop + res[0].top;
wx.pageScrollTo(obj);
})
},
// 获取文本
getText(ns = this.data.html) {
var txt = '';
for (var i = 0, n; n = ns[i++];) {
if (n.type == 'text') txt += n.text.replace(/ /g, '\u00A0').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&');
else if (n.type == 'br') txt += '\n';
else {
// 块级标签前后加换行
var br = n.name == 'p' || n.name == 'div' || n.name == 'tr' || n.name == 'li' || (n.name[0] == 'h' && n.name[1] > '0' && n.name[1] < '7');
if (br && txt && txt[txt.length - 1] != '\n') txt += '\n';
if (n.children) txt += this.getText(n.children);
if (br && txt[txt.length - 1] != '\n') txt += '\n';
else if (n.name == 'td' || n.name == 'th') txt += '\t';
}
}
return txt;
},
// 获取视频 context
getVideoContext(id) {
if (!id) return this.videoContexts;
for (var i = this.videoContexts.length; i--;)
if (this.videoContexts[i].id == id) return this.videoContexts[i];
},
// 渲染富文本
setContent(html, append, _watch) {
var data = {};
if (!html) {
if (_watch || append) return;
data.html = '';
} else if (typeof html == 'string') {
let parser = new Parser(html, this.data);
// 缓存读取
if (this.data.useCache) {
var hashVal = hash(html);
if (cache[hashVal]) data.html = cache[hashVal];
else {
data.html = parser.parse();
cache[hashVal] = data.html;
}
} else data.html = parser.parse();
this._refresh = true;
this.triggerEvent('parse', data.html);
} else if (html.constructor == Array) {
// 转换不符合格式的 array
if (html.length && html[0].PoweredBy != 'Parser') {
let parser = new Parser('', this.data);
(function f(ns) {
for (var i = 0, n; n = ns[i]; i++) {
if (n.type == 'text') continue;
n.attrs = n.attrs || {};
for (var key in n.attrs)
if (typeof n.attrs[key] != 'string') n.attrs[key] = n.attrs[key].toString();
parser.matchAttr(n);
if (n.children) {
parser.STACK.push(n);
f(n.children);
parser.popNode(parser.STACK.pop());
}
}
})(html);
data.html = html;
}
if (!_watch) data.html = html;
} else if (typeof html == 'object' && html.nodes) {
data.html = html.nodes;
console.warn('错误的 html 类型:object 类型已废弃');
} else
return console.warn('错误的 html 类型:' + typeof html);
if (append) {
this._refresh = true;
data.html = (this.data.html || []).concat(data.html);
} else if (this.data.showWithAnimation) data.showAm = 'animation: show .5s';
if (data.html || data.showAm) this.setData(data);
// 设置标题
if (this.data.html.length && this.data.html[0].title && this.data.autosetTitle)
wx.setNavigationBarTitle({
title: this.data.html[0].title
})
this.imgList.length = 0;
this.videoContexts = [];
if (dom) this.document = new dom(this.data.html, 'html', this);
var ns = this.selectAllComponents('.top,.top>>>._node');
for (let i = 0, n; n = ns[i++];) {
n.top = this;
for (var j = 0, item; item = n.data.nodes[j++];) {
if (item.c) continue;
// 获取图片列表
if (item.name == 'img')
this.imgList.setItem(item.attrs.i, item.attrs.src);
// 音视频控制
else if (item.name == 'video' || item.name == 'audio') {
var ctx;
if (item.name == 'video') ctx = wx.createVideoContext(item.attrs.id, n);
else ctx = n.selectComponent('#' + item.attrs.id);
if (ctx) {
ctx.id = item.attrs.id;
this.videoContexts.push(ctx);
}
}
}
}
(wx.nextTick || setTimeout)(() => this.triggerEvent('load'), 50);
var height;
clearInterval(this._timer);
this._timer = setInterval(() => {
this.createSelectorQuery().select('.top').boundingClientRect(res => {
this.rect = res;
if (res.height == height) {
this.triggerEvent('ready', res)
clearInterval(this._timer);
}
height = res.height;
}).exec();
}, 350)
},
// 预加载
preLoad(html, num) {
if (typeof html == 'string') {
var id = hash(html);
html = new Parser(html, this.data).parse();
cache[id] = html;
}
var imgs, wait = [];
(function f(ns) {
for (var i = 0, n; n = ns[i++];) {
if (n.name == 'img' && n.attrs.src && !wait.includes(n.attrs.src))
wait.push(n.attrs.src);
f(n.children || []);
}
})(html);
if (num) wait = wait.slice(0, num);
this._wait = (this._wait || []).concat(wait);
if (!this.data.imgs) imgs = this._wait.splice(0, 15);
else if (this.data.imgs.length < 15)
imgs = this.data.imgs.concat(this._wait.splice(0, 15 - this.data.imgs.length));
imgs && this.setData({
imgs
});
},
_load(e) {
if (this._wait.length)
this.setData({
[`imgs[${e.target.id}]`]: this._wait.shift()
})
},
// 事件处理
_tap(e) {
if (this.data.gestureZoom && e.timeStamp - this._lastT < 300) {
var initY = e.detail.y - e.currentTarget.offsetTop;
if (this._zoom) {
this._scaleAm.translateX(0).scale(1).step();
wx.pageScrollTo({
scrollTop: (initY + this._initY) / 2 - e.touches[0].clientY,
duration: 400
})
} else {
var initX = e.detail.x - e.currentTarget.offsetLeft;
this._initY = initY;
this._scaleAm = wx.createAnimation({
transformOrigin: `${initX}px ${this._initY}px 0`,
timingFunction: 'ease-in-out'
});
this._scaleAm.scale(2).step();
this._tMax = initX / 2;
this._tMin = (initX - this.rect.width) / 2;
this._tX = 0;
}
this._zoom = !this._zoom;
this.setData({
scaleAm: this._scaleAm.export()
})
}
this._lastT = e.timeStamp;
},
_touchstart(e) {
if (e.touches.length == 1)
this._initX = this._lastX = e.touches[0].pageX;
},
_touchmove(e) {
var diff = e.touches[0].pageX - this._lastX;
if (this._zoom && e.touches.length == 1 && Math.abs(diff) > 20) {
this._lastX = e.touches[0].pageX;
if ((this._tX <= this._tMin && diff < 0) || (this._tX >= this._tMax && diff > 0)) return;
this._tX += diff * Math.abs(this._lastX - this._initX) * 0.05;
if (this._tX < this._tMin) this._tX = this._tMin;
if (this._tX > this._tMax) this._tX = this._tMax;
this._scaleAm.translateX(this._tX).step();
this.setData({
scaleAm: this._scaleAm.export()
})
}
}
}
})
parser.json
{
"component": true,
"usingComponents": {
"trees": "./trees/trees"
}
}
parser.wxml
<!--parser 主组件-->
<slot wx:if="{{!html[0].name&&!html[0].type}}" />
<trees class="top" style="{{selectable?'user-select:text;-webkit-user-select:text;':''}}{{showAm}}" animation="{{scaleAm}}" lazy-load="{{lazyLoad}}" nodes="{{html[0].name||html[0].type?html:[]}}" bindtap="_tap" bindtouchstart="_touchstart" bindtouchmove="_touchmove" />
<image wx:for="{{imgs}}" wx:key="index" id="{{index}}" src="{{item}}" hidden bindload="_load" />
parser.wxss
:host {
display: block;
overflow: scroll;
-webkit-overflow-scrolling: touch;
}
.top {
display: inherit;
}
@keyframes show {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
1.1.2 组件使用
{
"usingComponents": {
"parser":"../../components/parser/parser"
}
}
<parser bindimgtap="onTapImage" html="{{html}}" tag-style="{{tagStyle}}" />
data: {
tagStyle: {
img: 'font-size:0;display:block;',
},
html:"<div>小程序实践<span>message</span></div>"
},
onTapImage(e) {
console.log('iamge url', e.detail.src)
}
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)