微信小程序自開(kāi)放出來(lái)到現(xiàn)在也有一段時(shí)間了,相信其底層架構(gòu)也被琢磨得差不多了。微信小程序本身是雙線程運(yùn)行的結(jié)構(gòu),而 h5 頁(yè)面是單線程的運(yùn)行模式,因此兩者無(wú)法直接互通。微信小程序的運(yùn)行模式如下:
微信小程序本身提供了 web-view 組件來(lái)支持在微信小程序中嵌入 h5 頁(yè)面,但是 web-view 組件在使用上還是有一些限制:不支持個(gè)人類(lèi)型與海外類(lèi)型的小程序、不支持全屏、頁(yè)面與小程序通信不方便、很多小程序接口無(wú)法直接調(diào)用等。
如果無(wú)法使用 web-view,這里還有一條路可以走,利用 h5-to-miniprogram 工具來(lái)將 h5 頁(yè)面轉(zhuǎn)換成小程序。
假設(shè)你已經(jīng)有一個(gè) h5 頁(yè)面,包含四個(gè)文件:
h5 頁(yè)面 |---- index.html |---- index.css |---- index.js |---- index.png 復(fù)制代碼
這種結(jié)構(gòu)我們?cè)偈煜げ贿^(guò)了,具體每個(gè)文件的內(nèi)容可參考這里: github.com/wechat-mini…。頁(yè)面渲染出來(lái)的效果如下:
頁(yè)面很簡(jiǎn)單,但是值得一提的時(shí),這個(gè)頁(yè)面引入了 jQuery 庫(kù),所以 index.html 和 index.js 是這樣的:
<!doctype html> <html lang="zh"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, minimal-ui"> <meta content="yes" name="apple-mobile-web-app-capable"> <meta content="black" name="apple-mobile-web-app-status-bar-style"> <meta name="format-detection" content="telephone=no, email=no"> <title>demo</title> <style type="text/css"> html, body { width: 100%; height: 100%; } </style> <link rel="stylesheet" href="./index.css"> </head> <body> <img class="logo" src="./index.png"> <div class="cnt"></div> <script src="https://juneandgreen.github.io/test/h5-to-miniprogram-demo/demo2/js/jquery-1.12.4.min.js"></script> <script src="./index.js"></script> </body> </html> 復(fù)制代碼
$(document).ready(function() { $('.cnt').text('h5 to miniprogram'); }); 復(fù)制代碼
微信小程序里是不暴露 dom/bom 接口的,說(shuō)想要使用 jQuery 是非常困難的。盡管難以置信,但是確實(shí)是有辦法的,后面會(huì)簡(jiǎn)述一下原理,讓我先繼續(xù)看下要如何操作。
因?yàn)檫\(yùn)行環(huán)境的不同,為了在編譯時(shí)和運(yùn)行時(shí)對(duì)兩者進(jìn)行一些兼容操作,我們需要一份配置文件:
module.exports = { index: 'h5', // 首頁(yè) urlMap: { // 每個(gè)頁(yè)面對(duì)應(yīng)的初始 url h5: 'https://weixin.qq.com/index?a=1&b=2#hash', }, resFilter(src, pageKey) { // 資源過(guò)濾,用于替換 h5 中使用到的資源路徑 return pageKey === 'h5' && src === './index.png' ? 'https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram-demo/master/h5/index.png' : src }, } 復(fù)制代碼
配置文件很簡(jiǎn)單,就是一個(gè) js 文件,里面包含各種配置項(xiàng)。例如 index 配置項(xiàng)用于配置首頁(yè); urlMap 用于配置每個(gè)頁(yè)面的初始 url,這個(gè) url 會(huì)被解析到 window.location 中,通常用于頁(yè)面跳轉(zhuǎn)或單頁(yè)系統(tǒng)中; resFilter 配置項(xiàng)用于調(diào)整資源路徑,這里是因?yàn)榭紤]到微信小程序包大小有限制,默認(rèn)不會(huì)去處理圖片等資源,所以需要提供一個(gè)方法來(lái)替換資源路徑為網(wǎng)絡(luò)路徑。
因?yàn)榕渲梦募枰截惖轿⑿判〕绦蝽?xiàng)目中執(zhí)行,所以配置文件必須是一個(gè)純凈的沒(méi)有額外依賴(lài)的文件(比如 require('fs') 在配置文件中是不允許的)。
有了原始的 h5,有了配置文件,那就可以開(kāi)始進(jìn)行轉(zhuǎn)換并生成微信小程序項(xiàng)目了。我們來(lái)編寫(xiě)一個(gè)構(gòu)建腳本,起名為 build.js:
const path = require('path') const toMiniprogram = require('h5-to-miniprogram') toMiniprogram({ entry: { // 入口 h5 頁(yè)面路徑 h5: path.join(__dirname, './h5/index.html'), }, output: path.join(__dirname, './miniprogram'), // 輸出目錄 config: path.join(__dirname, './config.js'), // 配置文件路徑 }).then(res => { console.log('done') }).catch(err => { console.error(err) }) 復(fù)制代碼
構(gòu)建腳本也很簡(jiǎn)單,引入 h5-to-miniprogram 工具,此工具直接暴露一個(gè) async 方法,調(diào)用時(shí)將必須的參數(shù)傳入即可。
可以看到參數(shù)中的入口配置是一個(gè) key-value 對(duì)象,這里的 value 不能理解,就是頁(yè)面的路徑,key 則是頁(yè)面的名稱(chēng)。例子中這個(gè)頁(yè)面的 key 就是 h5,我們回到上面的配置文件那里就會(huì)發(fā)現(xiàn),很多個(gè)地方都需要用到這個(gè) key,這個(gè) key 可以作為頁(yè)面的唯一標(biāo)識(shí)。
寫(xiě)完構(gòu)建腳本后,后續(xù)就簡(jiǎn)單很多了,執(zhí)行:
node build.js 復(fù)制代碼
然后就會(huì)看到構(gòu)建腳本中指定的輸出目錄—— miniprogram 目錄被生成出來(lái)。完整的 demo 在這里: github.com/wechat-mini…
使用官方提供的開(kāi)發(fā)者工具打開(kāi) miniprogram 目錄,可以看到已經(jīng)基本達(dá)到我們想要的效果了:
原理其實(shí)很簡(jiǎn)單,h5 頁(yè)面在瀏覽器運(yùn)行的過(guò)程就是解析 html 到渲染 dom 樹(shù)的過(guò)程,然后提供一些 dom/bom 接口給 js 調(diào)用。那么在小程序中我們把這一套給模擬一遍就行了,方法很暴力,但是卻意外的有效:因?yàn)榻o h5 頁(yè)面提供了類(lèi)似瀏覽器的環(huán)境,實(shí)現(xiàn)了最底層的適配,所以理論上來(lái)說(shuō)那些通用的框架和庫(kù)也能支持運(yùn)行。上面的例子中就表明了 jQuery 是能夠運(yùn)行的,像 react、vue 也是可以做到支持的。
微信小程序是雙線程的運(yùn)行模式,視圖層專(zhuān)注于渲染,邏輯層專(zhuān)注于邏輯。邏輯層是在一個(gè)純凈的 js 線程中跑,那里沒(méi)有 dom/bom 接口,只能運(yùn)行頁(yè)面邏輯層的代碼。要模擬瀏覽器環(huán)境,最基本的就是要在邏輯層里模擬出一棵 dom 樹(shù),本質(zhì)上和建立一棵虛擬樹(shù)類(lèi)似,因?yàn)樗⒉皇钦鎸?shí)的 dom 樹(shù)。整個(gè)流程簡(jiǎn)單來(lái)說(shuō)是這樣的:
不管是頁(yè)面中的靜態(tài) html 內(nèi)容還是使用 innerHTML 等接口動(dòng)態(tài)插入的 html 內(nèi)容都可以走上面的流程來(lái)進(jìn)行 dom 樹(shù)的創(chuàng)建。dom 樹(shù)創(chuàng)建比較簡(jiǎn)單,只是細(xì)節(jié)比較多,此處的關(guān)鍵是將創(chuàng)建好的 dom 節(jié)點(diǎn)映射到微信小程序的自定義組件,利用自定義組件的特性可以輕易的將我們創(chuàng)建好的 dom 樹(shù)給渲染出來(lái)。
如果你還不清楚微信小程序的自定義組件是什么的話,可以戳官方文檔了解一下。