新聞中心
零、何為跨端write once, run everywhere
那么在跨端方案百花齊放的今天,比如現(xiàn)在最為人們所熟知的react native、flutter、electron等,他們之間有沒有什么共同的特點(diǎn),而我們又是否能夠找到其中的本質(zhì),就是今天這篇文章想講述的問題。

站在用戶的角度思考問題,與客戶深入溝通,找到綏化網(wǎng)站設(shè)計(jì)與綏化網(wǎng)站推廣的解決方案,憑借多年的經(jīng)驗(yàn),讓設(shè)計(jì)與互聯(lián)網(wǎng)技術(shù)結(jié)合,創(chuàng)造個(gè)性化、用戶體驗(yàn)好的作品,建站類型包括:網(wǎng)站制作、網(wǎng)站設(shè)計(jì)、企業(yè)官網(wǎng)、英文網(wǎng)站、手機(jī)端網(wǎng)站、網(wǎng)站推廣、主機(jī)域名、網(wǎng)站空間、企業(yè)郵箱。業(yè)務(wù)覆蓋綏化地區(qū)。
一、主流跨端實(shí)現(xiàn)方案
1.1 h5 hybrid 方案
其實(shí),瀏覽器本就是一個(gè)跨端實(shí)現(xiàn)方案,因?yàn)槟阒恍枰斎刖W(wǎng)址,就能在任何端的瀏覽器上打開你的網(wǎng)頁。那么,如果我們把瀏覽器嵌入 app 中,再將地址欄等內(nèi)容隱藏掉,是不是就能將我們的網(wǎng)頁嵌入原生 app 了。而這個(gè)嵌入 app 的瀏覽器,我們把它稱之為 webview ,所以只要某個(gè)端支持 webview ,那么它就能使用這種方案跨端。
同時(shí)這也是開發(fā)成本最小的一種方案,因?yàn)檫@實(shí)際上就是在寫前端界面,和我們開發(fā)普通的網(wǎng)頁并沒有太大區(qū)別。
1.2 框架層+原生渲染
典型的代表是 react-native,它的開發(fā)語言選擇了 js,使用的語法和 react 完全一致,其實(shí)也可以說它就是 react,這就是我們的框架層。而不同于一般 react 應(yīng)用,它需要借助原生的能力來進(jìn)行渲染,組件最終都會(huì)被渲染為原生組件,這可以給用戶帶來比較好的體驗(yàn)。
1.3 框架層+自渲染引擎
這種方案和上面的區(qū)別就是,它并沒有直接借用原生能力去渲染組件,而是利用了更底層的渲染能力,自己去渲染組件。這種方式顯然鏈路會(huì)比上述方案的鏈路跟短,那么性能也就會(huì)更好,同時(shí)在保證多端渲染一致性上也會(huì)比上一種方案更加可靠。這類框架的典型例子就是 flutter 。
1.4 另類跨端
眾所周知,在最近幾年有一個(gè)東西變得非?;鸨盒〕绦?,現(xiàn)在許多大廠都有一套自己的小程序?qū)崿F(xiàn),但相互之間還是有不小差異的,通??梢越柚?taro ,remax 這類框架實(shí)現(xiàn)一套代碼,多端運(yùn)行的效果,這也算是一種另類的跨端,它的實(shí)現(xiàn)方式還是比較有意思的,我們后面會(huì)展開細(xì)講。
二、react-native 實(shí)現(xiàn)
2.1 rn的三個(gè)線程rn 包含三個(gè)線程:
- native thread:主要負(fù)責(zé)原生渲染和調(diào)用原生能力;
- js thread:JS 線程用于解釋和執(zhí)行我們的js代碼。在大多數(shù)情況下,react native使用的js引擎是JSC(JavaScriptCore) ,在使用 chrome 調(diào)試時(shí),所有的 js 代碼都運(yùn)行在 chrome中,并且通過 websocket與原生代碼通信。此時(shí)的運(yùn)行環(huán)境是v8。
- shadow thread:要渲染到界面上一個(gè)很重要的步驟就是布局,我們需要知道每個(gè)組件應(yīng)該渲染到什么位置,這個(gè)過程就是通過yoga去實(shí)現(xiàn)的,這是一個(gè)基于flexbox的跨平臺(tái)布局引擎。shadow thread 會(huì)維護(hù)一個(gè) shadow tree來計(jì)算我們的各個(gè)組件在 native 頁面的實(shí)際布局,然后通過 bridge 通知native thread 渲染 ui。
2.2 初始化流程
- native 啟動(dòng)一個(gè)原生界面,比如android會(huì)起一個(gè)新的activity來承載rn,并做一些初始化的操作。
- 加載 js 引擎,運(yùn)行 js 代碼,此時(shí)的流程和 react 的啟動(dòng)流程就非常相似了,我們先簡(jiǎn)單觀察調(diào)用棧,
是不是看見了一些非常熟悉的函數(shù)名,在上一講的基本原理中已經(jīng)提到過了,這里我們就不再贅述。同時(shí)再看一下FiberNode的結(jié)構(gòu),也和react的保持一致,只不過我們?cè)趈s層是無法拿到真實(shí)結(jié)點(diǎn)的,所以stateNode只是一個(gè)代號(hào)。
- js 線程通知shadow thread。在react中,走到createInstance以后我們就可以直接調(diào)用createElement來創(chuàng)建真實(shí)結(jié)點(diǎn)了,但是在rn中我們沒辦法做到這一步,所以我們會(huì)通知native層讓它來幫助我們創(chuàng)建一個(gè)對(duì)應(yīng)的真實(shí)結(jié)點(diǎn)。
- shadow thread 計(jì)算布局,通知native Thread 創(chuàng)建原生組件。
- native 在界面上渲染原生組件,呈現(xiàn)給用戶。
2.3 更新流程
比如某個(gè)時(shí)候,用戶點(diǎn)擊了屏幕上的一個(gè)按鈕觸發(fā)了一個(gè)點(diǎn)擊事件,此時(shí)界面需要進(jìn)行相應(yīng)的更新操作。
- native 獲取到了點(diǎn)擊事件,傳給了js threadjs thread根據(jù) react 代碼進(jìn)行相應(yīng)的處理,比如處理 onClick 函數(shù),觸發(fā)了 setState。
- 和 react 的更新流程一樣,觸發(fā)了 setState 之后會(huì)進(jìn)行 diff,找到需要更新的結(jié)點(diǎn)
- 通知 shadow threadshadow thread 計(jì)算布局之后通知 native thread 進(jìn)行真正的渲染。
2.4 特點(diǎn)
我們上述說的通知,都是通過 bridge 實(shí)現(xiàn)的,bridge本身是用實(shí)現(xiàn)C++的,就像一座橋一樣,將各個(gè)模塊關(guān)聯(lián)起來,整個(gè)通信是一個(gè)「異步」的過程。這樣做好處就是各自之間不會(huì)有阻塞關(guān)系,比如 不會(huì)native thread因?yàn)閖s thread而阻塞渲染,給用戶良好的體驗(yàn)。但是這種「異步」也存在一個(gè)比較明顯的問題:因?yàn)橥ㄐ胚^程花費(fèi)的時(shí)間比較長(zhǎng),所以在一些時(shí)效性要求較高場(chǎng)景上體驗(yàn)較差。
比如長(zhǎng)列表快速滾動(dòng)的時(shí)候或者需要做一些跟手的動(dòng)畫,整個(gè)過程是這樣的:
- native thread監(jiān)聽到了滾動(dòng)事件,發(fā)送消息通知
- js threadjs thread 處理滾動(dòng)事件,如果需要修改 state 需要經(jīng)過一層js diff,拿到最終需要更新的結(jié)點(diǎn)
- js thread 通知 shadow threadshadow thread 通知 native 渲染
- 當(dāng)用戶操作過快的時(shí)候,就會(huì)導(dǎo)致界面來不及更新,進(jìn)而導(dǎo)致在快速滑動(dòng)的時(shí)候會(huì)出現(xiàn)白屏、卡頓的現(xiàn)象。
2.5 優(yōu)化
我們很容易看出,這是由rn的架構(gòu)引出的問題,其實(shí)小程序的架構(gòu)也會(huì)有這個(gè)問題,所以在rn和小程序上出現(xiàn)一些需要頻繁通信的場(chǎng)景時(shí),就會(huì)導(dǎo)致頁面非常差,流暢度降低。那么如果想解決這個(gè)問題,勢(shì)必要從架構(gòu)上去進(jìn)行修改。
三、從rn看本質(zhì)
那么既然我們知道了rn是如何實(shí)現(xiàn)的跨端,那么我們就可以來探究一下它本質(zhì)上是在干什么。首先,跨端可以分為「邏輯跨端」和「渲染跨端」。
「邏輯跨端」通常通過 vm來實(shí)現(xiàn),例如利用v8 引擎,我們就能在各個(gè)平臺(tái)上運(yùn)行我們的js 代碼,實(shí)現(xiàn)「邏輯跨端」。
那么第二個(gè)問題就是「渲染跨端」,我們把業(yè)務(wù)代碼的實(shí)現(xiàn)抽象為開發(fā)層,比如 react-native中我們寫的 react 代碼就屬于開發(fā)層,再把具體要渲染的端稱為渲染層。作為開發(fā)層來說,我一定知道我想要的ui長(zhǎng)什么樣,但是我沒有能力去渲染到界面上,所以當(dāng)我聲明了一個(gè)組件之后,我們需要考慮的問題是如何把我想要什么告訴渲染層。
就像這樣的關(guān)系,那么我們最直觀的方式肯定是我能夠?qū)崿F(xiàn)一種通信方式,在開發(fā)層將消息通知到各個(gè)系統(tǒng),再由各個(gè)系統(tǒng)自己去調(diào)用對(duì)應(yīng)的 api 來實(shí)現(xiàn)最終的渲染。
function render() {
if(A) {
message.sendA('render', { type: 'View' })
}
if(B) {
message.sendB('render', { type: 'View' })
}
if(C) {
message.sendC('render', { type: 'View' })
}
}
比如這樣,我就能通過判斷平臺(tái)來通知對(duì)應(yīng)的端去渲染View組件。這一部分的工作就是跨端框架需要幫助我們做的,它可以把這一步放到 JS 層,也可以把這一步放到c++ 層。我們應(yīng)該把這部分工作盡量往底層放,也就是我們可以對(duì)各個(gè)平臺(tái)的 api 進(jìn)行一層封裝,上層只負(fù)責(zé)調(diào)用封裝的 api,再由這一層封裝層去調(diào)用真正的 api。因?yàn)檫@樣可以復(fù)用更多的邏輯,否則像上文中我們?cè)?JS層去發(fā)送消息給不同的平臺(tái),我們就需要在A\B\C三個(gè)平臺(tái)寫三個(gè)不同方法去渲染組件。
但是,歸根結(jié)底就是,一定有一個(gè)地方是通過判斷不同平臺(tái)來調(diào)用具體實(shí)現(xiàn),也就是下面這樣
有一個(gè)地方會(huì)對(duì)系統(tǒng)進(jìn)行判斷,再通過某種通信方式通知到對(duì)應(yīng)的端,最后執(zhí)行真正的方法。其實(shí),所有跨端相關(guān)操作其實(shí)都在做上圖中的這些事情。所有的跨端也可以總結(jié)為下面這句話:
「我知道我想要什么,但是我沒有能力去渲染,我要通知有能力渲染的人來幫助我渲染」
比如hybrid跨端方案中,webview其實(shí)就充當(dāng)了橋接層的角色,createElement,appendChild等api就是給我們封裝好的跨平臺(tái)api,底層最終調(diào)用到了什么地方,又是如何渲染到界面上的細(xì)節(jié)都被屏蔽掉了。所以我們利用這些api就能很輕松的實(shí)現(xiàn)跨端開發(fā),寫一個(gè)網(wǎng)頁,只要能夠加載 webview 的地方,我們的代碼就能跑在這個(gè)上面。
又比如flutter的方案通過研發(fā)一個(gè)自渲染的引擎來實(shí)現(xiàn)跨端,這種思路是不是相當(dāng)于另外一個(gè)瀏覽器?但是不同的點(diǎn)在于 flutter 是一個(gè)非常新的東西,而 webview 需要遵循大量的 w3c規(guī)范和背負(fù)一堆歷史包袱。flutter 并沒有什么歷史包袱,所以它能夠從架構(gòu),設(shè)計(jì)的方面去做的更好更快,能夠做更多的事情。
四、跨端目前有什么問題
4.1 一致性
對(duì)于跨端來說,如何屏蔽好各端的細(xì)節(jié)至關(guān)重要,比如針對(duì)某個(gè)端特有的api如何處理,如何保證渲染細(xì)節(jié)上各個(gè)端始終保持一致。如果一個(gè)跨端框架能夠讓開發(fā)者的代碼里面不出現(xiàn) isIos、isAndroid的字眼,或者是為了兼容各種奇怪的渲染而產(chǎn)生的非常詭異的hack方式。那我認(rèn)為它絕對(duì)是一個(gè)真正成功的框架。
但是按我經(jīng)驗(yàn)而言,先后寫過的 h5、rn、小程序,他們都沒有真正做到這一點(diǎn),所以項(xiàng)目里面會(huì)出現(xiàn)為了解決不同端不一致問題而出現(xiàn)的各種奇奇怪怪的代碼。而這個(gè)問題其實(shí)也是非常難解決的,因?yàn)楦鞫说牟町愡€是比較大的,所以說很難去完全屏蔽這些細(xì)節(jié)。
比如說h5中磨人的垂直居中問題,我相信只要開發(fā)過移動(dòng)端頁面的都會(huì)遇見,就不用我多說了。
4.2 為什么出現(xiàn)了這么多框架
為什么大家其實(shí)本質(zhì)上都是在干一件事情,卻出現(xiàn)了這么多的解決方案?其實(shí)大家都覺得某些框架沒能很好的解決某個(gè)問題,所以想自己去造一套。其中可能很多開發(fā)者最關(guān)心的就是性能問題,比如:
- rn因?yàn)榧軜?gòu)上的原因?qū)е履承﹫?chǎng)景性能差,所以它就想辦法從架構(gòu)上去進(jìn)行修改。
- flutter直接自己搞了一套渲染引擎,同時(shí)選用支持AOT的dart作為開發(fā)語言。
但是其實(shí)我們?cè)谶x擇框架的時(shí)候性能并不是唯一因素,開發(fā)體驗(yàn)、框架生態(tài)這些也都是關(guān)鍵因素,我個(gè)人感受是,目前rn的生態(tài)還是比其他的要好,所以在開發(fā)過程中你想要的東西基本都有。
五、小程序跨端
ok,說了這么多,對(duì)于跨端部分的內(nèi)容其實(shí)我想說的已經(jīng)說的差不多了,還記得上文提到的 Taro、Uni-app 一類跨小程序方案么。為什么說它是另類的跨端,因?yàn)樗鋵?shí)并沒有實(shí)際跨端,只是為了解決各個(gè)小程序語法之間不兼容的問題。但是它又確實(shí)是一個(gè)跨端解決方案,因?yàn)樗?「write once, run everything?!?/p>
下面我們先來了解下小程序的背景。
5.1 什么是小程序
小程序是各個(gè)app廠商對(duì)外開放的一種能力。通過廠商提供的框架,就能在他們的app中運(yùn)行自己的小程序,借助各大app的流量來開展自己的業(yè)務(wù)。同時(shí)作為廠商如果能吸引到更多的人加入到開發(fā)者大軍中來,也能給app帶來給多的流量,這可以看作一個(gè)雙贏的業(yè)務(wù)。那么最終呈現(xiàn)在app中的頁面是以什么方式進(jìn)行渲染的呢?其實(shí)還是通過webview,但是會(huì)嵌入一些原生的組件在里面以提供更好的用戶體驗(yàn),比如video組件其實(shí)并不是h5 video,而是native video。
5.2 什么是小程序跨端
那么到了這里,我們就可以來談一談關(guān)于小程序跨端的東西了。關(guān)于小程序跨端,核心并不是真正意義上的跨端,雖然小程序也做到了跨端,例如一份代碼其實(shí)是可以跑在android和Ios上的,但是實(shí)際上這和hybrid跨端十分相似。
在這里我想說的其實(shí)是,市面上現(xiàn)在有非常多的小程序:字節(jié)小程序、百度小程序、微信小程序、支付寶小程序等等等等。雖然他們的dsl十分相似,但是終歸還是有所不同,那么就意味著如果我想在多個(gè)app上去開展我的業(yè)務(wù),我是否需要維護(hù)多套十分相似的代碼?我又能否通過一套代碼能夠跑在各種小程序上?
5.3 怎么做
想通過一套代碼跑在多個(gè)小程序上,和想通過一套代碼跑在多個(gè)端,這兩件事到底是不是一件事呢?我們?cè)倩氐竭@張圖
這些平臺(tái)是否可以對(duì)應(yīng)上不同的小程序?
再回到那句話:「我知道我想要什么,但是我沒有能力去渲染,我要通知有能力渲染的人來幫助我渲染?!?/p>
現(xiàn)在來理一下我們的需求:
- 小程序的語法不好用,我希望用 react 開發(fā);
- 我希望盡可能低的成本讓小程序跑在多個(gè)平臺(tái)上。
那么從這句話來看:「我」代表了什么,「有能力渲染的人」又代表了什么?
第二個(gè)很容易對(duì)應(yīng)上,「有能力渲染的人」就是小程序本身,只有它才能幫助我們把內(nèi)容真正渲染到界面上。
而「我」又是什么呢?其實(shí)這個(gè)「我」可以是很多東西,不過這里我們的需求是想用react進(jìn)行開發(fā),所以我們回想一下第一講中react的核心流程,當(dāng)它拿到vdom的時(shí)候,是不是就已經(jīng)知道【我想要什么】了?所以我們把react拿到vdom之前的流程搬過來,這樣就能獲取到「我知道我想要什么」的信息,但是「我沒有能力去渲染」,因?yàn)檫@不是web,沒有dom api,所以我需要通知小程序來幫助我渲染,我還可以根據(jù)不同的端來通知不同的小程序幫助我渲染。
所以整個(gè)流程就是下面這樣的:
前面三個(gè)流程都在我們的js層,也就是開發(fā)層,我們寫的代碼經(jīng)歷一遍完整的 react 流程之后,會(huì)將最后的結(jié)果給到各個(gè)小程序,然后再走小程序自己的內(nèi)部流程,將其真正的渲染到界面上。
采用這種做法的典型例子有remax、taro3,他們宣稱用真正的react去開發(fā)小程序,其實(shí)并沒有錯(cuò),因?yàn)檎娴氖前裷eact的整套東西都搬了過來,和react并無差異。我們用taro寫一個(gè)非常簡(jiǎn)單的例子來看一下:
import { Component } from 'react'
import { View, Text, Button } from '@tarojs/components'
import './index.css'
export default class Index extends Component {
state = {
random: Math.random()
}
componentWillMount () { }
componentDidMount () { }
componentWillUnmount () { }
componentDidShow () { }
componentDidHide () { }
handleClick = () => {
debugger;
console.log("Math.random()", Math.random());
this.setState({random: Math.random()})
}
render () {
return (
Hello world! {this.state.random}
)
}
}
這是一個(gè)用taro寫的組件,把它編譯到字節(jié)小程序之后是這樣的效果:
根據(jù)我們之前的分析,在最后生成的文件中,一定包含了一個(gè)「小程序渲染器」。它接受的data就是整個(gè)ui結(jié)構(gòu),然后通過小程序的渲染能力渲染到界面上,我們?nèi)ist文件中找一下,就能找到一個(gè)base.ttml的文件,里面的內(nèi)容是這樣的
{{i.v}}
從名字可以看出,這是用于渲染各種組件的template,所以當(dāng)我們拿到react傳遞過來的data時(shí),將其傳給template,template就能根據(jù)對(duì)應(yīng)的組件名采用不同的模版進(jìn)行渲染。隨后再用一個(gè)for循環(huán)將其子組件進(jìn)行遞歸渲染,完成整個(gè)頁面的渲染。這個(gè)就可以理解為我們針對(duì)不同端寫的不同渲染器,如果我們編譯到wx小程序,這里面的內(nèi)容是會(huì)不同的。
總之,「在」**react**「對(duì)其處理完之后,會(huì)把數(shù)據(jù)」**setData**「?jìng)鬟f給「「小程序」」,小程序再用之前寫好的各種」**template**「將其渲染到頁面上。」
下面這張圖就是經(jīng)過react處理之后,能夠拿到頁面的數(shù)據(jù),將其傳遞給小程序之后,就能遞歸渲染出來。
那么這樣的架構(gòu)有什么問題呢,可以很明顯的看到會(huì)走兩遍diff,為什么會(huì)走兩遍diff呢?因?yàn)樵趓eact層為了獲取到我想要什么這個(gè)信息,我們必須走一遍diff,這樣才能將最后得到的data交給小程序。
而交給小程序之后,小程序?qū)τ谥暗牧鞒淌菬o感知的,所以它為了得到需要更新什么這個(gè)信息,也需要過一遍diff,或者通過一些其他的方式來拿到這個(gè)信息(并沒有深入了解過小程序的渲染流程,所以不確定是否是通過diff拿到的),所以這一整套流程就會(huì)走兩遍diff。
為什么我們不能將兩次diff合并為一次?因?yàn)樾〕绦虻匿秩緦?duì)開發(fā)者而言就是個(gè)黑盒,我們不能干擾到其內(nèi)部流程。如果我們能夠直接對(duì)接小程序的渲染sdk,那么其實(shí)根本沒必要走兩遍diff,因?yàn)榍爸玫?react的diff我們已經(jīng)能夠知道需要更新什么內(nèi)容。
這個(gè)問題的本質(zhì)和普通意義上的跨端框架沒有太大的區(qū)別,開發(fā)層也就是 react 知道自己需要什么東西,但是它沒有能力去渲染到界面上,所以需要通過小程序充當(dāng)渲染層來渲染到真正的界面上。這種開發(fā)方式有一種用 react 去寫 vue 的意思,但是為什么會(huì)出現(xiàn)這種詭異的開發(fā)方式,如果這個(gè) vue 做的足夠好的話,誰又想去這樣折騰?
5.4 組件的嵌套
其實(shí)還有一個(gè)小問題,wx的template是無法支持遞歸調(diào)用的,也就導(dǎo)致了我們想用template遞歸渲染data內(nèi)容是無法實(shí)現(xiàn)的,那么這個(gè)問題要如何解決呢..我們看一下上面的代碼在wx小程序中編譯出來的結(jié)果:
我們可以看到各種template之間多了0、1、2、3這種標(biāo)號(hào)..就是為了解決無法遞歸調(diào)用的問題,提前多造幾個(gè)名字不同功能相同的template,不就能跨過遞歸調(diào)用的限制了么...
六、另一種粗暴的跨端
上述的這些跨端都是通過某種架構(gòu)方式去實(shí)現(xiàn)的,那如果我們粗暴一點(diǎn)的想,我能不能直接把一套代碼通過編譯的方式去編譯到不同的平臺(tái)。比如我把js代碼編譯成java代碼、object-c代碼,其實(shí),個(gè)人感覺也不是不行,但是因?yàn)檫@些的差異實(shí)在太大,所以在寫js代碼的時(shí)候,可能需要非常強(qiáng)的約束性、規(guī)范性,把開發(fā)者限制在某個(gè)區(qū)域內(nèi),才能很好的編譯過去。也就是說,從js到j(luò)ava其實(shí)是一個(gè)自由度
網(wǎng)站欄目:聊聊跨端技術(shù)的本質(zhì)與現(xiàn)狀
當(dāng)前URL:http://fisionsoft.com.cn/article/dhpiehj.html


咨詢
建站咨詢
