1. 程式人生 > >高大上的微信小程式中渲染html內容—技術分享

高大上的微信小程式中渲染html內容—技術分享

大部分Web應用的富文字內容都是以HTML字串的形式儲存的,通過HTML文件去展示HTML內容自然沒有問題。但是,在微信小程式(下文簡稱為「小程式」)中,應當如何渲染這部分內容呢?

解決方案

wxParse

小程式剛上線那會兒,是無法直接渲染HTML內容的,於是就誕生了一個叫做「 wxParse 」的庫。它的原理就是把HTML程式碼解析成樹結構的資料,再通過小程式的模板把該資料渲染出來。

rich-text

後來,小程式增加了「rich-text」元件用於展示富文字內容。然而,這個元件存在一個極大的限制: 元件內遮蔽了所有節點的事件 。也就是說,在該元件內,連「預覽圖片」這樣一個簡單的功能都無法實現。

web-view

再後來,小程式允許通過「web-view」元件巢狀網頁,通過網頁展示HTML內容是相容性最好的解決方案了。然而,因為要多載入一個頁面,效能是較差的。

當「WePY」遇上「wxParse」

基於使用者體驗和功能互動上的考慮,我們拋棄了「rich-text」和「web-view」這兩個原生元件,選擇了「wxParse」。然而,用著用著卻發現,「wxParse」也不能很好地滿足需要:

  • 我們的小程式是基於「WePY」框架開發的,而「wxParse」是基於原生的小程式編寫的。要想讓兩者相容,必須修改「wxParse」的原始碼。
  • 「wxParse」只是簡單地通過image元件對原img元素的圖片進行顯示和預覽。而在實際使用中,可能會用到雲端儲存的介面對圖片進行縮小,達到「 用小圖顯示,用原圖預覽
    」的目的。
  • 「wxParse」直接使用小程式的video元件展示視訊,但是video元件的 層級問題 經常導致UI異常(例如把某個固定定位的元素給擋了)。

此外,圍觀一下「wxParse」的程式碼倉庫可以發現,它已經兩年沒有迭代了。所以就萌生了基於「WePY」的元件模式重新寫一個富文字元件的想法,其成果就是「WePY HTML」專案。

#實現過程

###解析HTML
首先仍然是要把HTML字串解析為樹結構的資料,我採用的是「特殊字元分隔法」。HTML中的特殊字元是「<」和「>」,前者為開始符,後者為結束符。

•如果待解析內容以開始符開頭,則擷取 開始符到結束符之間 的內容作為節點進行解析。
•如果待解析內容不以開始符開頭,則擷取 開頭到開始符之前 (如果開始符不存在,則為末尾)的內容作為純文字解析。
•剩餘內容進入下一輪解析,直到無剩餘內容為止。
正如下圖所示:

image.png

為了形成樹結構,解析過程中要維護一個上下文節點(預設為根節點):

•如果截取出來的內容是開始標籤,則根據匹配出的標籤名和屬性,在當前上下文節點下建立一個子節點。如果該標籤不是自結束標籤(br、img等),就把上下文節點設為新節點。
•如果截取出來的內容是結束標籤,則根據標籤名關閉當前上下文節點(把上下文節點設為其父節點)。
•如果是純文字,則在當前上下文節點下建立一個文字節點,上下文節點不變。
過程正如下面的表格所示:

image.png

經過上述流程,HTML字串就被解析為節點樹了。

對比
把上述演算法與其他類似的解析演算法進行對比(效能以「解析10000長度的HTML程式碼」進行測定):

image.png

可見,在不考慮容錯性(產生錯誤的結果,而非丟擲異常)的情況下,本元件的演算法與其餘兩者相比有壓倒性的優勢,符合小程式「 小而快 」的需要。而一般情況下,富文字編輯器所生成的程式碼也不會出現語法錯誤。因此,即使容錯性較差,問題也不大(但這是需要改進的)。

#模板渲染

樹結構的渲染,必然會涉及到子節點的 遞迴 處理。然而,小程式的模板並不支援遞迴,這下彷彿掉入了一個大坑。

看了一下「wxParse」模板的實現,它採用簡單粗暴的方式解決這個問題:通過13個長得幾乎一模一樣的模板進行巢狀呼叫(1呼叫2,2呼叫3,……,12呼叫13),也就是說最多可以支援12次巢狀。一般來說,這個深度也足夠了。

由於「WePY」框架本身是有構建機制的,所以不必手寫十來個幾乎一模一樣的模板,通過一個構建的外掛去生成即可。

以下為需要重複巢狀的模板(精簡過),在其程式碼的開始前和結束後分別插入特殊註釋進行標識,並在需要嵌入下一層模板的地方以另一段特殊註釋(「」)標識:

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

以下是對應的構建程式碼(需要安裝「 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 = '';
					// 反正不要錢,直接寫個20層巢狀
					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;
				}
			}
		}
	}
}

然而,執行起來後發現,第二層及更深層級的節點都沒有渲染出來,說明巢狀失敗了。再看一下dist目錄下生成的wxml檔案可以發現,變數名與元件原始碼的並不相同:

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

「WePY」在生成元件程式碼時,為了避免元件資料與頁面資料的變數名衝突,會 根據一定的規則給元件的變數名增加字首 (如上面程式碼中的「htmlContenthtmlContentwepyHtml$」)。所以在生成巢狀模板時,也必須使用帶字首的變數名。

先在元件程式碼中增加一個變數「thisIsMe」用於識別字首:

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

然後修改構建程式碼:

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

    // 匹配 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;
}

至此,渲染問題就解決了。

圖片
為了節省流量和提高載入速度,展示富文字內容時,一般都會按照所需尺寸對裡面的圖片進行縮小,點選小圖進行預覽時才展示原圖。這主要涉及節點屬性的修改:

•把圖片原路徑(src屬性值)存到自定義屬性(例如「data-src」)中,並將其新增到預覽圖陣列。
•把圖片的src屬性值修改為縮小後的圖片URL(一般雲服務商都有提供此類URL規則)。
•點選圖片時,使用自定義屬性的值進行預覽。
為了實現這個需求,本元件在解析節點時提供了一個鉤子( onNodeCreate ):

onNodeCreate(name, attrs) {
	if (name === 'img') {
		attrs['data-src'] = attrs.src;
		// 預覽圖陣列
		this.previewImgs.push(attrs.src);
		// 縮圖
		attrs.src = resizeImg(attrs.src, 640);
	}
}

對應的模板和事件處理邏輯如下:

<template name="wepyhtml-img">
	<image class="wepyhtml-tag-img" mode="widthFix" src="{{ elem.attrs.src }}" data-src="{{ elem.attrs['data-src'] || elem.attrs.src }}" @tap="imgTap"></image>
</template>
// 點選小圖看大圖
imgTap(e) {
	wepy.previewImage({
		current: e.currentTarget.dataset.src,
		urls: this.previewImgs
	});
}

#視訊

在小程式中,video元件的層級是較高的(且無法降低)。如果頁面設計上存在著可能擋住視訊的元素,處理起來就需要一些技巧了:

•隱藏video元件,用image元件(視訊封面)佔位;
•點選圖片時,讓視訊全屏播放;
•如果退出了全屏,則暫停播放。
相關程式碼如下:

<template name="wepyhtml-video">
	<view class="wepyhtml-tag-video" @tap="videoTap" data-nodeid="{{ elem.nodeId }}">
		<!-- 視訊封面 -->
		<image class="wepyhtml-tag-img wepyhtml-tag-video__poster" mode="widthFix" src="{{ elem.attrs.poster }}"></image>
		<!-- 播放圖示 -->
		<image class="wepyhtml-tag-img wepyhtml-tag-video__play" src="./imgs/icon-play.png"></image>
		<!-- 視訊元件 -->
		<video style="display: none;" src="{{ elem.attrs.src }}" id="wepyhtml-video-{{ elem.nodeId }}" @fullscreenchange="videoFullscreenChange" @play="videoPlay"></video>
	</view>
</template>

{
	// 點選封面圖,播放視訊
	videoTap(e) {
		const nodeId = e.currentTarget.dataset.nodeid;
		const context = wepy.createVideoContext('wepyhtml-video-' + nodeId);
		context.play();
		// 在安卓微信下,如果視訊不可見,則呼叫play()也無法播放
		// 需要再呼叫全屏方法
		if (wepy.getSystemInfoSync().platform === 'android') {
			context.requestFullScreen();
		}
	},
	// 視訊層級較高,為防止遮擋其他特殊定位元素,造成介面異常,
	// 強制全屏播放
	videoPlay(e) {
		wepy.createVideoContext(e.currentTarget.id).requestFullScreen();
	},
	// 退出全屏則暫停
	videoFullscreenChange(e) {
		if (!e.detail.fullScreen) {
			wepy.createVideoContext(e.currentTarget.id).pause();
		}
	}
}

本文分享就到這裡了。

#最後

這裡推薦一下我的前端學習交流群:784783012,裡面都是學習前端的,如果你想製作酷炫的網頁,想學習程式設計。自己整理了一份2018最全面前端學習資料,從最基礎的HTML+CSS+JS【炫酷特效,遊戲,外掛封裝,設計模式】到移動端HTML5的專案實戰的學習資料都有整理,送給每一位前端小夥伴,有想學習web前端的,或是轉行,或是大學生,還有工作中想提升自己能力的,正在學習的小夥伴歡迎加入學習。

相關推薦

高大程式渲染html內容技術分享

大部分Web應用的富文字內容都是以HTML字串的形式儲存的,通過HTML文件去展示HTML內容自然沒有問題。但是,在微信小程式(下文簡稱為「小程式」)中,應當如何渲染這部分內容呢? 解決方案 wxParse 小程式剛上線那會兒,是無法直接渲染HTML內容的,於

淺談高大程序渲染html內容技術分享

ren const num 前端工程 變量 前後臺 用戶 視頻 idt 大部分Web應用的富文本內容都是以HTML字符串的形式存儲的,通過HTML文檔去展示HTML內容自然沒有問題。但是,在微信小程序(下文簡稱為「小程序」)中,應當如何渲染這部分內容呢? 解決方案 wxPa

程式渲染HTML內容

大部分Web應用的富文字內容都是以HTML字串的形式儲存的,通過HTML文件去展示HTML內容自然沒有問題。但是,在微信小程式(下文簡稱為「小程式」)中,應當如何渲染這部分內容呢? 解決方案 wxParse 小程式剛上線那會兒,是無法直接渲染HTML內容的,於是就誕生了

程式顯示HTML格式內容的例項

因為個人註冊的開發者不允許在微信小程式中載入網頁,因此開發者都遇到過需要將網頁中的圖文內容完整載入到微信小程式中的情況,如果網頁數目較多,逐個編輯wxml程式碼往往過於麻煩,因此這篇文章將介紹如何藉助Bmob雲後端的圖文素材功能和大神編寫的wxParse元件實現網頁內容在微信

程式實現傳視訊的開發程式碼

index.wxml <view class="image-plus image-plus-nb" bindtap="chooseVideo"> <view class="image-plus-horizontal"></view> &l

程式如何獲取使用者當前的地理位置以及在頁面如何給在map元件的圖示繫結事件

效果圖 元件 <map id="map" longitude="{{longitude}}" latitude="{{latitude}}" scale="15" controls="{{controls}}" bindcontroltap="controlta

程式懸浮窗功能的實現(主要探討和解決在原生元件的拖動)

問題場景 所謂懸浮窗就是圖中微信圖示的按鈕,採用fixed定位,可拖動和點選。 這算是一個比較常見的實現場景了。 為什麼要用cover-view做懸浮窗?原生元件出來背鍋了~ 最初我做懸浮窗用的不是cover-view,而是view。 這是簡化的程式碼結構: index.wxml: <view cl

程式資料的儲存和獲取

/儲存資料     try {       wx.setStorageSync('key',this.data.radioCheckVal2)  //key表示data中的引數

程式引入iconfont阿里巴巴向量圖示

1.訪問iconfont阿里巴巴向量圖示庫官網 2.搜尋自己想找的圖示(輸入拼音,中文或英文都可以) 3.把自己想找的圖示“新增入庫” 4.點選購物車,新增至專案(若無專案,可新建) 5.檢視線上連結 6.點選複製程式碼 7.在微信小

程式使用Echarts(可非同步請求資料)

在微信小程式中使用Echarts,主要分為以下幾步: 1.首先要下載ecomfe/echarts-for-weixin專案,下載後將ec-canvas資料夾複製到小程式專案中,假設放在根目錄下utils資料夾中。 2.在要實現echarts圖的頁面引入echarts.js檔案,例如要在i

程式 scroll-view觸底事件不觸發的解決方法

scroll-view元件是否設定了確定的高度,若沒有請設定 scroll-view元件的 lower-threshold 引數是否帶了單位,若帶了單位如 px、rpx等,請去除,只使用數值。 若設定了上面兩項還是沒有效果,將 scroll-view的高度設定為具

weui在程式如何使用

weUI 是一套同微信原生視覺體驗一致的基礎樣式庫,由微信官方設計團隊為微信 Web 開發量身設計,可以令使用者的使用感知更加統一。包含button、cell、dialog、 progress、 toast、article、actionsheet、icon等各式元素。 預覽 用微信web開發者工

程式動畫多次呼叫的問題

function hideMsg(that) { var animation = wx.createAnimation({ duration: 1500, timingFunction: 'linear', }) that.animation = animation

hooks 在程式的試驗

PS:首先,這不是一個成熟的東西,只是一個實現極其簡單的玩具而已。 前言 前段時間 react hooks 特性刷得沸沸揚揚的,看起來挺有意思的,估計不少其他框架也會逐步跟進,所以也來嘗試一下能不能用在小程式上。 react hooks 允許你在函式式元件中使用 state,用一段官方的簡單例

程式換行,空格(多個空格)寫法

在小程式中HTML的網頁實體無法正常使用,小程式中的寫法為:  一、空格,換行 <text>你好!\t七月流火啊!\n我在下一行</text> ---------------------------------------------------------

程式把頁面生成圖片

這個問題我上網搜了一下,答案有多種,但是真正能用的沒有幾何。很多答案都是雷同,有的網友也不負責任,直接拿來照抄,自己也不跑一遍看看。哎,不說了,說多了全是淚。希望我們的技術達人在分享的時候,能夠真實的走一遍程式碼,儘量能讓我等小白看的懂啊。鬧騷發過了,下面我們就進入正題吧(*^__^*) 嘻嘻……

程式手風琴摺疊選單

效果圖 1.wxml // WXML片段 <block wx:for="{{list}}" wx:for-item="list" wx:for-index="idx"> <!-- 訂單組list --> <

程式頁面間資料傳遞

一、在index.js頁面 1、index.js中的goodspagedata方法從後臺get的資料通過setData將資料shareData從邏輯層傳送到檢視層(我理解為將資料傳送到Page({})下層和goodspagedata方法的同級) 2、然後在index.js的shareP

程式的自定義元件

手把手教你實現微信小程式中的自定義元件 微信小程式中的元件 前言 之前做小程式開發的時候,對於開發來說比較頭疼的莫過於自定義元件了,當時官方對這方面的文件也只是寥寥幾句,一筆帶過而已,所以寫起來真的是非常非常痛苦!!

程式的app.js-清除快取

微信小程式中的app.js 關於小程式app.js生命週期的介紹 App(Object) App() 函式用來註冊一個小程式。接受一個 Object 引數,其指定小程式的生命週期回撥等。 App() 必須在 app.js 中呼叫,必須呼叫且只能呼叫一次。不然會出現無法預期的後果。 onLa