2020年,該如何選擇小程序框架
寫在開頭,這不是一篇廣告文。
微信小程序橫空出世,到現(xiàn)在已經(jīng)有 4 年時(shí)間。從一開始只能選擇原生語法,到如今小程序框架 Rax/Taro/uni-app 百花齊放。這背后是小程序原生語法造成的生態(tài)割裂,也是業(yè)務(wù)對(duì)「一碼多端」的強(qiáng)烈訴求,更是前端現(xiàn)在繁榮的生態(tài)體系。
NO.1
小程序的誕生
微信開了一個(gè)頭
微信并不是第一個(gè)做小程序的 App,而是做小程序最有優(yōu)勢的 App,比如高流量、用戶較長的停留時(shí)間等等。站在微信的視角,小程序從業(yè)務(wù)形式上更像是公眾號(hào)開發(fā)的演變產(chǎn)物。在更早的時(shí)候,微信通過 sdk 的形式,增強(qiáng)了開發(fā)者開發(fā)公眾號(hào)網(wǎng)頁的能力。小程序的誕生是微信本身邁向平臺(tái)化超級(jí) App 的業(yè)務(wù)行為,并且?guī)椭脩舾玫膶?shí)現(xiàn)了「輕量級(jí) Web App」。
微信小程序誕生之初就自己定義了一套”標(biāo)準(zhǔn)“,與前端已有的生態(tài)格格不入,最開始的框架甚至沒有組件、沒有 npm,和 Web 生態(tài)嚴(yán)重脫節(jié)。由于特殊的雙線程模型與四不像的語法,開發(fā)者苦不堪言。小程序的開放只是對(duì)三方業(yè)務(wù)的開放而已。
蜂擁而至的效仿
其它廠商看到了小程序業(yè)務(wù)的開放性,試圖也能夠做成平臺(tái)型 App。支付寶小程序、百度小程序、淘寶小程序、360小程序、快應(yīng)用……它們中的大多數(shù)都不約而同的選擇了和微信類似的架構(gòu)、框架,而這更多的不是從技術(shù)角度去做的決定,而是想盡可能蹭微信小程序的福利,讓開發(fā)者可以更快的投放到自己的平臺(tái)。當(dāng)然,其中有兩個(gè)稍顯不同。一個(gè)是早期的淘寶小程序,它不僅支持 axml 的寫法,同時(shí)還支持 sfc -- 用 Vue 來開發(fā),這個(gè)架構(gòu)更大程度上讓開發(fā)者有了選擇的權(quán)利,并且能夠更好地連接已有的前端生態(tài)。另一個(gè)是快應(yīng)用,也是用類似 Vue 的語法來開發(fā),但是略顯畸形的是它自己又造了一套標(biāo)準(zhǔn),更像是對(duì) Vue 進(jìn)行了魔改,開發(fā)者的開發(fā)成本并沒有得到有效的提升。
NO.2
小程序框架選擇
小程序原生語法 and 增強(qiáng)型框架
小程序原生語法
是不是原生語法一定是被唾棄的。站在 2020 年這個(gè)時(shí)間節(jié)點(diǎn)上來說,并不是這樣。單純就微信小程序 or 支付寶小程序而言,目前的小程序生態(tài)是完完全全足夠開發(fā)者利用前端已有的部分生態(tài)來開發(fā)出符合預(yù)期的應(yīng)用的。
與早期 npm 能力的缺失、只能通過模板渲染實(shí)現(xiàn)組件化相比而言?,F(xiàn)在的小程序已經(jīng)能夠做到前端工程化,并且植入前端生態(tài)中已有的一些理念,例如狀態(tài)管理、CLI 工程化等等。
也就是說,當(dāng)業(yè)務(wù)的需求只有投放到微信小程序或者支付寶小程序的時(shí)候,原生語法完完全全可以成為前端程序員們的一個(gè)選擇。你可以組件化你的項(xiàng)目,你可以手寫一個(gè)或者使用社區(qū)已有的狀態(tài)管理庫來顆?;芾斫M件狀態(tài),你甚至還可以直接用 TypeScript 來編寫你的應(yīng)用??傊銕缀蹩梢园涯闼?xí)慣的東西都帶到小程序這個(gè)域里。
漸進(jìn)增強(qiáng)型框架
所謂漸進(jìn)增強(qiáng)型框架,更多的還是在小程序引入 npm 之后,有了更加開放的能力所帶來的收益。這類框架一般依然是以小程序原生語法為主,只是在邏輯層引入了增強(qiáng)語法來優(yōu)化應(yīng)用性能或者提供更便捷的使用方法,
以騰訊開源的?omix(https://github.com/Tencent/omi/tree/master/packages/omix)?框架為例,先簡單看一下它的用法:
邏輯層
create.Page(store, {
// 聲明依賴
use: <a href="https://astexplorer.net/">'logs'],
computed: {
logsLength() {
return this.logs.length
}
},
onLoad: function () {
//響應(yīng)式,自動(dòng)更新視圖
this.store.data.logs = (wx.getStorageSync('logs') || []).map(log => {
return util.formatTime(new Date(log))
})
setTimeout(() => {
//響應(yīng)式,自動(dòng)更新視圖
this.store.data.logs[0] = 'Changed!'
}, 1000)
}
})
視圖層
<view class="container log-list">
<block wx:for="{{logs}}" wx:for-item="log">
<text class="log-item">{{index + 1}}. {{log}}text>
block>
view>
先不談它的語法是否符合直覺或者好用。簡單來說,它整體保留小程序已有的語法。但是在此基礎(chǔ)之上,對(duì)它進(jìn)行了擴(kuò)充和增強(qiáng),比如引入了 Vue 中比較有代表性的computed,比如能夠直接通過this.store.data.logs[0] = 'Changed'?修改狀態(tài)??梢哉f是在小程序原生半 Vue 半 React 的語法(此處半只是數(shù)量詞)背景下,徹底將其 Vue 化的一種方案。
使用增強(qiáng)型框架最大的好處是,你可以在只引入極少依賴,并且保留對(duì)小程序認(rèn)知的情況下,用更加舒爽的語法來寫代碼。這類框架對(duì)于目標(biāo)只投放到特定平臺(tái)小程序的開發(fā)者或者非專業(yè)前端而言是比較好的選擇之一。因?yàn)槟阒恍枰P(guān)注很少的新增文檔和小程序自身的文檔就足夠了。畢竟在推動(dòng)某項(xiàng)技術(shù)的過程中,團(tuán)隊(duì)的學(xué)習(xí)成本也是需要考慮的。
轉(zhuǎn)換類框架
相比于漸進(jìn)增強(qiáng)型框架,轉(zhuǎn)換類框架的使命是完全不同的。轉(zhuǎn)換類框架的使命是讓開發(fā)者幾乎不用感受小程序原生語法,更大程度對(duì)接前端已有生態(tài),并且可以實(shí)現(xiàn)「一碼多端」的業(yè)務(wù)訴求,只是最后的構(gòu)建產(chǎn)物為小程序代碼。隨著這幾年的發(fā)展,轉(zhuǎn)換類框架大的方面分為兩種 -- 編譯時(shí)/運(yùn)行時(shí)。下文會(huì)分別針對(duì)兩種方案進(jìn)行分析。
Rax 編譯時(shí)/Taro 2.0
顧名思義,編譯時(shí)方案的核心是通過編譯分析的方式,將開發(fā)者寫的代碼轉(zhuǎn)換成小程序原生語法。這里以 Rax 編譯時(shí)和 Taro 2.0 為例,面向開發(fā)者的語法是類 React 語法,開發(fā)者通過寫有一定語法限制的 React 代碼,最后轉(zhuǎn)換產(chǎn)物 1:1 轉(zhuǎn)換成對(duì)應(yīng)的小程序代碼。
以一段簡單的代碼為例:
Rax:
import { createElement, useEffect, useState } from 'rax';
import View from 'rax-view';
export default function Home() {
const [name, setName] = useState('world');
useEffect(() => {
console.log('Here is effect.');
}, [])
return Hello {name} ;
}
轉(zhuǎn)換之后的小程序代碼:
邏輯層
import { __create_component__, useEffect, useState } from 'jsx2mp-runtime/dist/ali.esm.js'
function Home() {
const [name, setName] = useState('world');
useEffect(() => {
console.log('Here is effect.');
}, []);
this._updateData({
_d0: name
});
}
Component(__create_component__(Home));
視圖層
<block a:if="{{$ready}}">
<view class="__rax-view">{{_d0}}view>
block>
編譯時(shí)方案最大的特點(diǎn)就是,開發(fā)者雖然寫的是類 React 語法,但是轉(zhuǎn)換后的代碼和漸進(jìn)增強(qiáng)型框架非常類似。開發(fā)者可以比較清晰的看出編譯前后代碼的對(duì)應(yīng)關(guān)系
簡單來說,編譯時(shí)方案會(huì)通過 AST 分析,將開發(fā)者寫的 JSX 中return?的模板部分構(gòu)建到視圖層,剩余部分代碼保留,然后通過運(yùn)行時(shí)墊片模擬 React 接口的表現(xiàn)。
以一個(gè)簡單的return ?為例,如果想要提取到,你可以寫這段解析代碼:
// 省略定義 babel parser 和包裝 traverse 的部分
const code = fs.readFileSync(FILE_PATH);
const ast = parser(code);
traverse(ast, {
ReturnStatement(path) {
const targetNodePath = path.get('argument');
if (targetNodePath.isJSXElement()) {
// 如果 return 的子元素是一個(gè) JSX Element 就收集 or 處理一下
}
}
})
targetNodePath?就是?的節(jié)點(diǎn)路徑,關(guān)于 AST 相關(guān)的文章可以自行搜索一下,babel 的 handle book 已經(jīng)比較詳細(xì)了,再加上這個(gè)[輔助網(wǎng)站(https://astexplorer.net/)基本是沒有什么門檻的。
但是這個(gè)方案其實(shí)是存在明顯的優(yōu)勢和劣勢的。
優(yōu)勢
運(yùn)行時(shí)性能損耗低
目標(biāo)代碼明確,開發(fā)者所寫即所得
運(yùn)行時(shí)、編譯時(shí)優(yōu)化
在這個(gè)方案中,你可以輕易的做到和漸進(jìn)增強(qiáng)型框架一樣的效果,即給予開發(fā)者更多的語法支持以及默認(rèn)的性能優(yōu)化處理,比如避免多次setData,亦或是長列表優(yōu)化等等。
劣勢
語法限制高
由于需要完全命中開發(fā)者在模板部分所用到的所有語法,這就對(duì)編譯時(shí)框架的實(shí)現(xiàn)者有相當(dāng)高的要求。因?yàn)榇蟛糠智岸碎_發(fā)者們其實(shí)已經(jīng)對(duì)靈活的語法有一定的依賴性,比如會(huì)使用高階組件,比如在條件判斷的時(shí)候?qū)懞芏?code style="font-family:monospace, monospace;color:rgb(255,185,15);">return?等等。進(jìn)一步的,由于是 1:1 編譯轉(zhuǎn)換,開發(fā)者在開發(fā)的時(shí)候還是不得不去遵循小程序的開發(fā)規(guī)范,比如一個(gè)文件中定義只能定義一個(gè)組件之類的。
目前在阿里巴巴集團(tuán)內(nèi)部,Rax 的這套編譯時(shí)方案已經(jīng)落地了很多業(yè)務(wù),包括「電影演出」小程序等,從開發(fā)者的實(shí)踐來看,如果能夠掌握編譯時(shí)開發(fā)的技巧,即保證最終return?的模板的簡潔性,語法限制其實(shí)還是在可以接受的范圍內(nèi)的。
Rax 運(yùn)行時(shí)/Remax/Taro Next
運(yùn)行時(shí)方案相比于上面的編譯時(shí)方案,最大的優(yōu)勢是可以幾乎沒有任何語法約束的去完成代碼編寫。這對(duì)于開發(fā)者而言,無疑是最大的吸引力,高階組件用起來!createProtal?用起來!但是在小程序領(lǐng)域上暫時(shí)還不可能存在這么好的事情,這也是小程序原生語法最后的執(zhí)拗。沒有語法限制帶來的更多的性能上的犧牲,這個(gè)與運(yùn)行時(shí)方案的實(shí)現(xiàn)方式有很大的關(guān)系,接下來我詳細(xì)介紹一下。
從渲染的角度來看,這套方案更貼近于富文本渲染。邏輯層將一個(gè)和節(jié)點(diǎn)渲染信息相關(guān)的組件樹傳遞給視圖層,視圖層通過節(jié)點(diǎn)類型判斷然后進(jìn)行視圖渲染。下面這個(gè)圖簡要的描述了一下整個(gè)過程:
雖然只用了維護(hù)兩個(gè)字,但是邏輯層做的事情其實(shí)比較復(fù)雜。首先要做的是,去處理節(jié)點(diǎn)間的關(guān)系,去模擬appendChild/removeChild/updateNode?等各個(gè)行為來操作 VDOM 樹。其次比較重要的是去模擬事件,在邏輯層每一個(gè)節(jié)點(diǎn)類會(huì)繼承自EventTarget?基類,這個(gè)和 W3C 是一樣的,然后通過nodeId?作為標(biāo)識(shí)去收集需要監(jiān)聽的事件,當(dāng)視圖層通過 action 觸發(fā)了某個(gè)節(jié)點(diǎn)的事件之后,再通過原生小程序事件中的event.currentTarget.dataset.nodeId獲取到目標(biāo)節(jié)點(diǎn)的 id,最終觸發(fā)目標(biāo)回調(diào)。
由于本文篇幅問題,不會(huì)詳細(xì)的介紹其中的各個(gè)部分更加具體的實(shí)現(xiàn),感興趣的同學(xué)可以通過 ?Rax(github.com/alibaba/rax)?的源碼或者npm init rax demo?起一個(gè)項(xiàng)目通過最終的產(chǎn)物來研究整個(gè)原理。
在目前這個(gè)階段,即使是運(yùn)行時(shí)方案,也有不同的實(shí)現(xiàn)思路。以 Kbone (Rax 運(yùn)行時(shí)方案是從 Kbone 改造而來) 和 Taro Next 都是通過模擬 Web 環(huán)境來徹底對(duì)接前端生態(tài),而 Remax 只是簡單的通過 react reconciler 連接 React 和小程序。
從業(yè)務(wù)訴求來看,筆者認(rèn)為,Rax 和 Taro Next 可能會(huì)比 Remax 更加開放。首先,需要考慮是三部分的訴求,(1)毫無語法限制,既然已經(jīng)沒有了語法限制,為什么不能用前端更加熟悉的方式來開發(fā),即擁有操作 DOM 的權(quán)利;(2)不和 DSL 耦合,盡管在阿里巴巴集團(tuán)內(nèi),對(duì) React 的認(rèn)可度更高,但是從實(shí)現(xiàn)原理上來看,和某個(gè)框架進(jìn)行強(qiáng)綁定一定不是最優(yōu)解;(3)舊有的 Web 業(yè)務(wù)遷移,今天我們所面對(duì)的開發(fā)者,很多都是因?yàn)闃I(yè)務(wù)壓力或者其他情況需要將原有的 Web 頁面遷移到小程序上,那么用模擬 Web 環(huán)境這套方案是最好不過了,根據(jù)我們的測試,大部分業(yè)務(wù)幾乎可以無縫遷移過來。
害!說了這么多漂亮話,運(yùn)行時(shí)方案真的很香,但這并不是救世主,我來說說它的劣勢。劣勢 1:數(shù)據(jù)傳輸量大,我們需要將完整的組件樹在邏輯層傳輸?shù)揭晥D層;劣勢 2:頁面上存在大量的監(jiān)聽器,每一個(gè)組件都需要無時(shí)無刻監(jiān)聽所有的事件,在事件不斷觸發(fā)的過程中,通過nodeId?篩選出真正需要觸發(fā)的事件;劣勢 3:模板遞歸渲染,如果使用原生語法,原生框架可以在渲染前就知道頁面大概的結(jié)構(gòu),來對(duì)渲染進(jìn)行優(yōu)化,但是如果僅僅只是通過類似?這樣的信息,是很難判斷頁面的真實(shí)結(jié)構(gòu)的。
組合
魚和熊掌雖然不能兼得,但是可以各要一半~再次強(qiáng)調(diào),本文不是廣告文。如果編譯時(shí)和運(yùn)行時(shí)方案共存呢?基于淘系前端高度現(xiàn)代的工程化積累,開發(fā)者已經(jīng)習(xí)慣通過區(qū)塊來組建項(xiàng)目。更得益于,Rax 在編譯時(shí)和運(yùn)行時(shí)方案都有所積累,我們希望能夠?qū)⑦\(yùn)行時(shí)方案和編譯時(shí)方案組合使用。對(duì)于基礎(chǔ)復(fù)雜或者對(duì)于性能有要求的模塊通過編譯時(shí)實(shí)現(xiàn)。然后再通過 npm 包的形式,引入到運(yùn)行時(shí)的項(xiàng)目中去,從而有效降低了運(yùn)行時(shí)方案的性能損耗,并且能保證絕大部分的業(yè)務(wù)場景可以用無限制的語法完成,而開發(fā)者所面對(duì)的都是 Rax DSL。
用一個(gè) Demo 來看下:
// 這是一個(gè)倒計(jì)時(shí)組件,通過編譯時(shí)實(shí)現(xiàn),然后發(fā)布為 rax-taobao-countdown
import { createElement } from 'rax';
import View from 'rax-view';
function CountDown(props) {
// 省略各種邏輯...
return {day}:{hours}:{minutes}:{seconds}
}
export default CountDown;
// 運(yùn)行時(shí)項(xiàng)目
import { createElement } from 'rax';
import CountDown from 'rax-taobao-countdown';
function Home() {
return
}
假設(shè),我們這個(gè)倒計(jì)時(shí)組件結(jié)構(gòu)非常復(fù)雜,并且要求極高的交互性。那么,開發(fā)者可以通過編譯時(shí)方案開發(fā)一個(gè)高性能CountDown?組件,然后在運(yùn)行時(shí)項(xiàng)目中引入使用。此時(shí),視圖層所得到的節(jié)點(diǎn)樹信息大致如下:
{
"tagName": "custom-component",
"type": "element",
"behavior": "CountDown",
"children": []
}
而不會(huì)有更多更深層結(jié)構(gòu)的節(jié)點(diǎn)信息,有效避免剛剛說的運(yùn)行時(shí)方案中存在的劣勢。
NO.3
Web 才是未來
小程序原生語法絕對(duì)不是小程序 or 下一代的渲染方案。通過微信小程序現(xiàn)有的語法規(guī)范來對(duì)開發(fā)者進(jìn)行綁架,只會(huì)讓更多的人想突破圍城。微信小程序似乎已經(jīng)意識(shí)到了這一點(diǎn),從目前的迭代來看,微信小程序引入了越來越多 Web 已有的東西,包括通過wxs在視圖層就可以一定程度上操作 DOM,甚至獲取到邏輯層組件實(shí)例等等,這個(gè)可以給現(xiàn)有的轉(zhuǎn)換類框架提供更多的可能性。但是,如果小程序如果一開始設(shè)計(jì)的不這么糟糕呢,我們可能會(huì)失業(yè)(開個(gè)玩笑)?
對(duì)于業(yè)務(wù)的開發(fā)者而言,「一碼多端」才是效率最大化的。今天的業(yè)務(wù)需求可能只是投放到小程序容器,明天的需求可能就是投放到 Web,未來甚至 是 Flutter。Web 是最貼近前端開發(fā)者的,有組織保障(W3C)的規(guī)范。所以,站在 2020 年這個(gè)時(shí)間點(diǎn),無論是框架提供者,還是業(yè)務(wù)開發(fā)者都應(yīng)該更多的從標(biāo)準(zhǔn)的角度思考問題,這樣才能讓業(yè)務(wù)代碼有更多的可能性。
NO.4
總結(jié)
距離小程序誕生已經(jīng)過去很多年,2020 年應(yīng)該如何選擇業(yè)務(wù)合適的小程序框架,這個(gè)需要開發(fā)者衡量利弊之后再做出選擇。因?yàn)槊總€(gè)業(yè)務(wù)的形式不同,應(yīng)用的存活時(shí)間也不相同,根據(jù)自己的需要選擇可能才是最好的,本文只是從一個(gè)全局的視角對(duì)所有類型的框架進(jìn)行分析,希望能夠讓正在看文章的你,不那么糾結(jié)~
??看完三件事
如果你覺得這篇內(nèi)容對(duì)你挺有啟發(fā),我想邀請你幫我三個(gè)小忙:
- 點(diǎn)贊,讓更多的人也能看到介紹內(nèi)容(收藏不點(diǎn)贊,都是耍流氓-_-)
- 關(guān)注公眾號(hào)“前端勸退師”,不定期分享原創(chuàng)知識(shí)。
- 也看看其他文章
勸退師個(gè)人微信:huab119
也可以來我的GitHub博客里拿所有文章的源文件:
前端勸退指南:https://github.com/roger-hiro/BlogFN一起玩耍呀。
