【總結(jié)】1098- 來(lái)自阿里巴巴體驗(yàn)技術(shù)部的前端白屏方案分享

引用下集團(tuán)監(jiān)控的 slogan:關(guān)注業(yè)務(wù)穩(wěn)定性的人,運(yùn)氣都不會(huì)太差~
背景
不知從什么時(shí)候開始,前端白屏問(wèn)題成為一個(gè)非常普遍的話題,'白屏' 甚至成為了前端 bug 的代名詞:_喂,你的頁(yè)面白了。_而且,'白' 這一現(xiàn)象似乎對(duì)于用戶體感上來(lái)說(shuō)更加強(qiáng),回憶起 windows 系統(tǒng)的崩潰 '藍(lán)屏':


可以說(shuō)是非常相似了,甚至能明白了白屏這個(gè)詞匯是如何統(tǒng)一出來(lái)的。那么,體感如此強(qiáng)烈的現(xiàn)象勢(shì)必會(huì)給用戶帶來(lái)一些不好的影響,如何能盡早監(jiān)聽,快速消除影響就顯得很重要了。
為什么單獨(dú)監(jiān)控白屏
不光光是白屏,白屏只是一種現(xiàn)象,我們要做的是精細(xì)化的異常監(jiān)控。異常監(jiān)控各個(gè)公司肯定都有自己的一套體系,集團(tuán)也不例外,而且也足夠成熟。但是通用的方案總歸是有缺點(diǎn)的,如果對(duì)所有的異常都加以報(bào)警和監(jiān)控,就無(wú)法區(qū)分異常的嚴(yán)重等級(jí),并做出相應(yīng)的響應(yīng),所以在通用的監(jiān)控體系下定制精細(xì)化的異常監(jiān)控是非常有必要的。這就是本文討論白屏這一場(chǎng)景的原因,我把這一場(chǎng)景的邊界圈定在了 “白屏” 這一現(xiàn)象。
方案調(diào)研
白屏大概可能的原因有兩種:
js 執(zhí)行過(guò)程中的錯(cuò)誤 資源錯(cuò)誤
這兩者方向不同,資源錯(cuò)誤影響面較多,且視情況而定,故不在下面方案考慮范圍內(nèi)。為此,參考了網(wǎng)上的一些實(shí)踐加上自己的一些調(diào)研,大概總結(jié)出了一些方案:
一、onerror + DOM 檢測(cè)
原理很簡(jiǎn)單,在當(dāng)前主流的 SPA 框架下,DOM 一般掛載在一個(gè)根節(jié)點(diǎn)之下(比如 )發(fā)生白屏后通常現(xiàn)象是根節(jié)點(diǎn)下所有 DOM 被卸載,該方案就是通過(guò)監(jiān)聽全局的 onerror 事件,在異常發(fā)生時(shí)去檢測(cè)根節(jié)點(diǎn)下是否掛載 DOM,若無(wú)則證明白屏。我認(rèn)為是非常簡(jiǎn)單暴力且有效的方案。但是也有缺點(diǎn):其一切建立在 白屏 === 根節(jié)點(diǎn)下 DOM 被卸載?成立的前提下,實(shí)際并非如此比如一些微前端的框架,當(dāng)然也有我后面要提到的方案,這個(gè)方案和我最終方案天然沖突。
二、Mutation Observer Api
不了解的可以看下文檔[1]。其本質(zhì)是監(jiān)聽 DOM 變化,并告訴你每次變化的 DOM 是被增加還是刪除。為其考慮了多種方案:
搭配 onerror使用,類似第一個(gè)方案,但很快被我否決了,雖然其可以很好的知道 DOM 改變的動(dòng)向,但無(wú)法和具體某個(gè)報(bào)錯(cuò)聯(lián)系起來(lái),兩個(gè)都是事件監(jiān)聽,兩者是沒(méi)有必然聯(lián)系的。單獨(dú)使用判斷是否有大量 DOM 被卸載,缺點(diǎn):白屏不一定是 DOM 被卸載,也有可能是壓根沒(méi)渲染,且正常情況也有可能大量 DOM 被卸載。完全走不通。 單獨(dú)使用其監(jiān)聽時(shí)機(jī)配合 DOM 檢測(cè),其缺點(diǎn)和方案一一樣,而且我覺得不如方案一。因?yàn)樗鼪](méi)法和具體錯(cuò)誤聯(lián)系起來(lái),也就是沒(méi)法定位。當(dāng)然我和其他團(tuán)隊(duì)同學(xué)交流的時(shí)候他們給出了其他方向:通過(guò)追蹤用戶行為數(shù)據(jù)來(lái)定位問(wèn)題,我覺得也是一種方法。
一開始我認(rèn)為這就是最終答案,經(jīng)過(guò)了漫長(zhǎng)的心里斗爭(zhēng),最終還是否定掉了。不過(guò)它給了一個(gè)比較好的監(jiān)聽時(shí)機(jī)的選擇。
三、餓了么-Emonitor 白屏監(jiān)控方案
餓了么的白屏監(jiān)控方案,其原理是記錄頁(yè)面打開 4s 前后 html 長(zhǎng)度變化,并將數(shù)據(jù)上傳到餓了么自研的時(shí)序數(shù)據(jù)庫(kù)。如果一個(gè)頁(yè)面是穩(wěn)定的,那么頁(yè)面長(zhǎng)度變化的分布應(yīng)該呈現(xiàn)「冪次分布」曲線的形態(tài),p10、p20 (排在文檔前 10%、20%)等數(shù)據(jù)線應(yīng)該是平穩(wěn)的,在一定的區(qū)間內(nèi)波動(dòng),如果頁(yè)面出現(xiàn)異常,那么曲線一定會(huì)出現(xiàn)掉底的情況。
其他
其他都大同小樣,其實(shí)調(diào)研了一圈下來(lái)發(fā)現(xiàn)無(wú)非就是兩點(diǎn)
監(jiān)控時(shí)機(jī):調(diào)研下來(lái)常見的就三種: onerror mutation observer api 輪訓(xùn) DOM 檢測(cè):這個(gè)方案就很多了,除了上述的還可以: elementsFromPoint api 采樣 圖像識(shí)別 基于 DOM 的各種數(shù)據(jù)的各種算法識(shí)別 ...
改變方向
幾番嘗試下來(lái)幾乎沒(méi)有我想要的,其主要原因是準(zhǔn)確率 -- 這些方案都不能保證我監(jiān)聽到的是白屏,單從理論的推導(dǎo)就說(shuō)不通。他們都有一個(gè)共同點(diǎn):監(jiān)聽的是'白屏'這個(gè)現(xiàn)象,從現(xiàn)象去推導(dǎo)本質(zhì)雖然能成功,但是不夠準(zhǔn)確。所以我真正想要監(jiān)聽的是造成白屏的本質(zhì)。
那么回到最開始,什么是白屏?他是如何造成的?是因?yàn)殄e(cuò)誤導(dǎo)致的瀏覽器無(wú)法渲染?不,在這個(gè) spa 框架盛行的現(xiàn)在實(shí)際上的白屏是框架造成的,本質(zhì)是由于錯(cuò)誤導(dǎo)致框架不知道怎么渲染所以干脆就不渲染。由于我們團(tuán)隊(duì) React 技術(shù)棧居多,我們來(lái)看看 React 官網(wǎng)的一段話[2]:

React 認(rèn)為把一個(gè)錯(cuò)誤的 UI 保留比完全移除它更糟糕。我們不討論這個(gè)看法的正確與否,至少我們知道了白屏的原因:渲染過(guò)程的異常且我們沒(méi)有捕獲異常并處理。
反觀目前的主流框架:我們把 DOM 的操作托管給了框架,所以渲染的異常處理不同框架方法肯定不一樣,這大概就是白屏監(jiān)控難統(tǒng)一化產(chǎn)品化的原因。但大致方向肯定是一樣的。
那么關(guān)于白屏我認(rèn)為可以這么定義:異常導(dǎo)致的渲染失敗。
那么白屏的監(jiān)控方案即:監(jiān)控渲染異常。那么對(duì)于 React 而言,答案就是:Error Boundaries
Error Boundaries
我們可以稱之為錯(cuò)誤邊界,錯(cuò)誤邊界是什么?它其實(shí)就是一個(gè)生命周期,用來(lái)監(jiān)聽當(dāng)前組件的 children 渲染過(guò)程中的錯(cuò)誤,并可以返回一個(gè) 降級(jí)的 UI 來(lái)渲染:
class?ErrorBoundary?extends?React.Component?{
??constructor(props)?{
????super(props);
????this.state?=?{?hasError:?false?};
??}
??static?getDerivedStateFromError(error)?{
????//?更新?state?使下一次渲染能夠顯示降級(jí)后的?UI
????return?{?hasError:?true?};
??}
??componentDidCatch(error,?errorInfo)?{
????//?我們可以將錯(cuò)誤日志上報(bào)給服務(wù)器
????logErrorToMyService(error,?errorInfo);
??}
??render()?{
????if?(this.state.hasError)?{
??????//?我們可以自定義降級(jí)后的?UI?并渲染
??????return?<h1>Something?went?wrong.h1>;
????}
????return?this.props.children;?
??}
}
一個(gè)有責(zé)任心的開發(fā)一定不會(huì)放任錯(cuò)誤的發(fā)生。錯(cuò)誤邊界可以包在任何位置并提供降級(jí) UI,也就是說(shuō),一旦開發(fā)者'有責(zé)任心' 頁(yè)面就不會(huì)全白,這也是我之前說(shuō)的方案一與之天然沖突且其他方案不穩(wěn)定的情況。那么,在這同時(shí)我們上報(bào)異常信息,這里上報(bào)的異常一定會(huì)導(dǎo)致我們定義的白屏,這一推導(dǎo)是 100% 正確的。
100% 這個(gè)詞或許不夠負(fù)責(zé),接下來(lái)我們來(lái)看看為什么我說(shuō)這一推導(dǎo)是 100% 準(zhǔn)確的:
React 渲染流程
我們來(lái)簡(jiǎn)單回顧下從代碼到展現(xiàn)頁(yè)面上 React 做了什么。我大致將其分為幾個(gè)階段:render => 任務(wù)調(diào)度 => 任務(wù)循環(huán) => 提交 => 展示 我們舉一個(gè)簡(jiǎn)單的例子來(lái)展示其整個(gè)過(guò)程(任務(wù)調(diào)度不再本次討論范圍故不展示):
const?App?=?({?children?})?=>?(
??<>
????<p>hellop>
????{?children?}
??>
);
const?Child?=?()?=>?<p>I'm?childp>
const?a?=?ReactDOM.render(
??<App><Child/>App>,
??document.getElementById('root')
);
準(zhǔn)備
首先瀏覽器是不認(rèn)識(shí)我們的 jsx 語(yǔ)法的,所以我們通過(guò) babel 編譯大概能得到下面的代碼:
var?App?=?function?App(_ref2)?{
??var?children?=?_ref2.children;
??return?React.createElement("p",?null,?"hello"),?children);
};
var?Child?=?function?Child()?{
??return?React.createElement("p",?null,?"I'm?child");
};
ReactDOM.render(React.createElement(App,?null,?React.createElement(Child,?null)),?document.getElementById('root'));
babel 插件將所有的 jsx 都轉(zhuǎn)成了 createElement 方法,執(zhí)行它會(huì)得到一個(gè)描述對(duì)象 ReactElement 大概長(zhǎng)這樣子:
{
????$$typeof:?Symbol(react.element),
??key:?null,
??props:?{},?//?createElement?第二個(gè)參數(shù)?注意?children?也在這里,children?也會(huì)是一個(gè)?ReactElement?或?數(shù)組
??type:?'h1'?//?createElement?的第一個(gè)參數(shù),可能是原生的節(jié)點(diǎn)字符串,也可能是一個(gè)組件對(duì)象(Function、Class...)
}
所有的節(jié)點(diǎn)包括原生的 、 都會(huì)創(chuàng)建一個(gè) FiberNode ,他的結(jié)構(gòu)大概長(zhǎng)這樣:
FiberNode?=?{
????elementType:?null,?//?傳入?createElement?的第一個(gè)參數(shù)
??key:?null,
??type:?HostRoot,?//?節(jié)點(diǎn)類型(根節(jié)點(diǎn)、函數(shù)組件、類組件等等)
??return:?null,?//?父?FiberNode
??child:?null,?//?第一個(gè)子?FiberNode
??sibling:?null,?//?下一個(gè)兄弟?FiberNode
??flag:?null,?//?狀態(tài)標(biāo)記
}
你可以把它理解為 Virtual Dom 只不過(guò)多了許多調(diào)度的東西。最開始我們會(huì)為根節(jié)點(diǎn)創(chuàng)建一個(gè) FiberNodeRoot 如果有且僅有一個(gè) ReactDOM.render 那么他就是唯一的根,當(dāng)前有且僅有一個(gè) FiberNode 樹。
我只保留了一些渲染過(guò)程中重要的字段,其他還有很多用于調(diào)度、判斷的字段我這邊就不放出來(lái)了,有興趣自行了解
render
現(xiàn)在我們要開始渲染頁(yè)面,是我們剛才的例子,執(zhí)行 ReactDOM.render 。這里我們有個(gè)全局 workInProgress 對(duì)象標(biāo)志當(dāng)前處理的 FiberNode
首先我們?yōu)楦?jié)點(diǎn)初始化一個(gè) FiberNodeRoot,他的結(jié)構(gòu)就如上面所示,并將workInProgress= FiberNodeRoot。接下來(lái)我們執(zhí)行 ReactDOM.render方法的第一個(gè)參數(shù),我們得到一個(gè)ReactElement:
ReactElement?=?{
??$$typeof:?Symbol(react.element),
??key:?null,
??props:?{
????children:?{
??????$$typeof:?Symbol(react.element),
??????key:?null,
??????props:?{},
??????ref:?null,
??????type:???Child(),
????}
??}
??ref:?null,
??type:?f?App()
}
該結(jié)構(gòu)描述了
我們?yōu)? ReactElement生成一個(gè)FiberNode并把 return 指向父FiberNode,最開始是我們的根節(jié)點(diǎn),并將workInProgress = FiberNode
{
??elementType:?f?App(),?//?type?就是?App?函數(shù)
??key:?null,
??type:?FunctionComponent,?//?函數(shù)組件類型
??return:?FiberNodeRoot,?//?我們的根節(jié)點(diǎn)
??child:?null,
??sibling:?null,
??flags:?null
}
只要 workInProgress存在我們就要處理其指向的FiberNode。節(jié)點(diǎn)類型有很多,處理方法也不太一樣,不過(guò)整體流程是相同的,我們以當(dāng)前函數(shù)式組件為例子,直接執(zhí)行App(props)方法,這里有兩種情況該組件 return 一個(gè)單一節(jié)點(diǎn),也就是返回一個(gè) ReactElement對(duì)象,重復(fù) 3 - 4 的步驟。并將當(dāng)前 節(jié)點(diǎn)的 child 指向子節(jié)點(diǎn)CurrentFiberNode.child = ChildFiberNode并將子節(jié)點(diǎn)的 return 指向當(dāng)前節(jié)點(diǎn)ChildFiberNode.return = CurrentFiberNode該組件 return 多個(gè)節(jié)點(diǎn)(數(shù)組或者 Fragment),此時(shí)我們會(huì)得到一個(gè)ChildiFberNode的數(shù)組。我們循環(huán)他,每一個(gè)節(jié)點(diǎn)執(zhí)行 3 - 4 步驟。將當(dāng)前節(jié)點(diǎn)的 child 指向第一個(gè)子節(jié)點(diǎn)CurrentFiberNode.child = ChildFiberNodeList[0],同時(shí)每個(gè)子節(jié)點(diǎn)的 sibling 指向其下一個(gè)子節(jié)點(diǎn)(如果有)ChildFiberNode[i].sibling = ChildFiberNode[i + 1],每個(gè)子節(jié)點(diǎn)的 return 都指向當(dāng)前節(jié)點(diǎn)ChildFiberNode[i].return = CurrentFiberNode
如果無(wú)異常每個(gè)節(jié)點(diǎn)都會(huì)被標(biāo)記為待布局 FiberNode.flags = Placement
重復(fù)步驟直到處理完全部節(jié)點(diǎn) workInProgress為空。
最終我們能大概得到這樣一個(gè) FiberNode 樹:
FiberNodeRoot?=?{
??elementType:?null,
??type:?HostRoot,
??return:?null,
??child:?FiberNode,
??sibling:?null,
??flags:?Placement,?//?待布局狀態(tài)
}
FiberNode?{
??elementType:?f?App(),
??type:?FunctionComponent,
??return:?FiberNodeRoot,
??child:?FiberNode,
??sibling:?null,
??flags:?Placement?//?待布局狀態(tài)
}
FiberNode
?{
??elementType:?'p',
??type:?HostComponent,
??return:?FiberNode,
??sibling:?FiberNode,
??child:?null,
??flags:?Placement?//?待布局狀態(tài)
}
FiberNode?{
??elementType:?f?Child(),
??type:?FunctionComponent,
??return:?FiberNode,
??child:?null,
??flags:?Placement?//?待布局狀態(tài)
}
提交階段
提交階段簡(jiǎn)單來(lái)講就是拿著這棵樹進(jìn)行深度優(yōu)先遍歷 child => sibling,放置 DOM 節(jié)點(diǎn)并調(diào)用生命周期。
那么整個(gè)正常的渲染流程簡(jiǎn)單來(lái)講就是這樣。接下來(lái)看看異常處理
錯(cuò)誤邊界流程
剛剛我們了解了正常的流程現(xiàn)在我們制造一些錯(cuò)誤并捕獲他:
const?App?=?({?children?})?=>?(
??<>
??<p>hellop>
??{?children?}
??>
);
const?Child?=?()?=>?<p>I'm?child?{a.a}p>
const?a?=?ReactDOM.render(
??<App>
????<ErrorBoundary><Child/>ErrorBoundary>
??App>,
??document.getElementById('root')
);
執(zhí)行步驟 4 的函數(shù)體是包裹在 try...catch 內(nèi)的如果捕獲到了異常則會(huì)走異常的流程:
do?{
??try?{
????workLoopSync();?//?上述?步驟?4
????break;
??}?catch?(thrownValue)?{
????handleError(root,?thrownValue);
??}
}?while?(true);
執(zhí)行步驟 4 時(shí)我們調(diào)用 Child 方法由于我們加了個(gè)不存在的表達(dá)式 {a.a} 此時(shí)會(huì)拋出異常進(jìn)入我們的 handleError 流程此時(shí)我們處理的目標(biāo)是 FiberNode ,我們來(lái)看看 handleError :
function?handleError(root,?thrownValue):?void?{
??let?erroredWork?=?workInProgress;?//?當(dāng)前處理的?FiberNode?也就是異常的?節(jié)點(diǎn)
??throwException(
????root,?//?我們的根?FiberNode
????erroredWork.return,?//?父節(jié)點(diǎn)
????erroredWork,
????thrownValue,?//?異常內(nèi)容
??);
????completeUnitOfWork(erroredWork);
}
function?throwException(
??root:?FiberRoot,
??returnFiber:?Fiber,
??sourceFiber:?Fiber,
??value:?mixed,
)?{
??//?The?source?fiber?did?not?complete.
??sourceFiber.flags?|=?Incomplete;
??let?workInProgress?=?returnFiber;
??do?{
????switch?(workInProgress.tag)?{
??????case?HostRoot:?{
????????workInProgress.flags?|=?ShouldCapture;
????????return;
??????}
??????case?ClassComponent:
????????//?Capture?and?retry
????????const?ctor?=?workInProgress.type;
????????const?instance?=?workInProgress.stateNode;
????????if?(
??????????(workInProgress.flags?&?DidCapture)?===?NoFlags?&&
??????????(typeof?ctor.getDerivedStateFromError?===?'function'?||
????????????(instance?!==?null?&&
??????????????typeof?instance.componentDidCatch?===?'function'?&&
??????????????!isAlreadyFailedLegacyErrorBoundary(instance)))
????????)?{
??????????workInProgress.flags?|=?ShouldCapture;
??????????return;
????????}
????????break;
??????default:
????????break;
????}
????workInProgress?=?workInProgress.return;
??}?while?(workInProgress?!==?null);
}
代碼過(guò)長(zhǎng)截取一部分 先看 throwException 方法,核心兩件事:
將當(dāng)前也就是出問(wèn)題的節(jié)點(diǎn)狀態(tài)標(biāo)志為未完成 FiberNode.flags = Incomplete從父節(jié)點(diǎn)開始冒泡,向上尋找有能力處理異常( ClassComponent)且的確處理了異常的(聲明了getDerivedStateFromError或componentDidCatch生命周期)節(jié)點(diǎn),如果有,則將那個(gè)節(jié)點(diǎn)標(biāo)志為待捕獲workInProgress.flags |= ShouldCapture,如果沒(méi)有則是根節(jié)點(diǎn)。
completeUnitOfWork 方法也類似,從父節(jié)點(diǎn)開始冒泡,找到 ShouldCapture 標(biāo)記的節(jié)點(diǎn),如果有就標(biāo)記為已捕獲 DidCapture ,如果沒(méi)找到,則一路把所有的節(jié)點(diǎn)都標(biāo)記為 Incomplete 直到根節(jié)點(diǎn),并把 workInProgress 指向當(dāng)前捕獲的節(jié)點(diǎn)。
之后從當(dāng)前捕獲的節(jié)點(diǎn)(也有可能沒(méi)捕獲是根節(jié)點(diǎn))開始重新走流程,由于其狀態(tài) react 只會(huì)渲染其降級(jí) UI,如果有 sibling 節(jié)點(diǎn)則會(huì)繼續(xù)走下面的流程。我們看看上述例子最終得到的 FiberNode 樹:
FiberNodeRoot?=?{
??elementType:?null,
??type:?HostRoot,
??return:?null,
??child:?FiberNode,
??sibling:?null,
??flags:?Placement,?//?待布局狀態(tài)
}
FiberNode?{
??elementType:?f?App(),
??type:?FunctionComponent,
??return:?FiberNodeRoot,
??child:?FiberNode,
??sibling:?null,
??flags:?Placement?//?待布局狀態(tài)
}
FiberNode
?{
??elementType:?'p',
??type:?HostComponent,
??return:?FiberNode,
??sibling:?FiberNode,
??child:?null,
??flags:?Placement?//?待布局狀態(tài)
}
FiberNode?{
??elementType:?f?ErrorBoundary(),
??type:?ClassComponent,
??return:?FiberNode,
??child:?null,
??flags:?DidCapture?//?已捕獲狀態(tài)
}
FiberNode?{
??elementType:?f?ErrorBoundary(),
??type:?ClassComponent,
??return:?FiberNode,
??child:?null,
??flags:?Placement?//?待布局狀態(tài)
}
如果沒(méi)有配置錯(cuò)誤邊界那么根節(jié)點(diǎn)下就沒(méi)有任何節(jié)點(diǎn),自然無(wú)法渲染出任何內(nèi)容。
ok,相信到這里大家應(yīng)該清楚錯(cuò)誤邊界的處理流程了,也應(yīng)該能理解為什么我之前說(shuō)由 ErrorBoundry 推導(dǎo)白屏是 100% 正確的。當(dāng)然這個(gè) 100% 指的是由 ErrorBoundry 捕捉的異常基本上會(huì)導(dǎo)致白屏,并不是指它能捕獲全部的白屏異常。以下場(chǎng)景也是他無(wú)法捕獲的:
事件處理 異步代碼 SSR 自身拋出來(lái)的錯(cuò)誤
React SSR 設(shè)計(jì)使用流式傳輸,這意味著服務(wù)端在發(fā)送已經(jīng)處理好的元素的同時(shí),剩下的仍然在生成 HTML,也就是其父元素?zé)o法捕獲子組件的錯(cuò)誤并隱藏錯(cuò)誤的組件。這種情況似乎只能將所有的 render 函數(shù)包裹 try...catch ,當(dāng)然我們可以借助 babel 或 TypeScript 來(lái)幫我們簡(jiǎn)單實(shí)現(xiàn)這一過(guò)程,其最終得到的效果是和 ErrorBoundry 類似的。
而事件和異步則很巧,雖說(shuō) ErrorBoundry 無(wú)法捕獲他們之中的異常,不過(guò)其產(chǎn)生的異常也恰好不會(huì)造成白屏(如果是錯(cuò)誤的設(shè)置狀態(tài),間接導(dǎo)致了白屏,剛好還是會(huì)被捕獲到)。這就在白屏監(jiān)控的職責(zé)邊界之外了,需要?jiǎng)e的精細(xì)化監(jiān)控能力來(lái)處理它。
總結(jié)
那么最后總結(jié)下本文的出的幾個(gè)結(jié)論:我對(duì)白屏的定義:異常導(dǎo)致的渲染失敗。對(duì)應(yīng)方案是:資源監(jiān)聽 + 渲染流程監(jiān)聽。
在目前 SPA 框架下白屏的監(jiān)控需要針對(duì)場(chǎng)景做精細(xì)化的處理,這里以 React 為例子,通過(guò)監(jiān)聽渲染過(guò)程異常能夠很好的獲得白屏的信息,同時(shí)能增強(qiáng)開發(fā)者對(duì)異常處理的重視。而其他框架也會(huì)有相應(yīng)的方法來(lái)處理這一現(xiàn)象。
當(dāng)然這個(gè)方案也有弱點(diǎn),由于是從本質(zhì)推導(dǎo)現(xiàn)象其實(shí)無(wú)法 cover 所有的白屏的場(chǎng)景,比如我要搭配資源的監(jiān)聽來(lái)處理資源異常導(dǎo)致的白屏。當(dāng)然沒(méi)有一個(gè)方案是完美的,我這里也是提供一個(gè)思路,歡迎大家一起討論。
作者:ES2049 / 金城武
https://zhuanlan.zhihu.com/p/383686310
參考資料
文檔: https://link.zhihu.com/?target=https%3A//developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver
[2]一段話: https://link.zhihu.com/?target=https%3A//zh-hans.reactjs.org/docs/error-boundaries.html%23new-behavior-for-uncaught-errors

回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~
點(diǎn)擊“閱讀原文”查看 120+ 篇原創(chuàng)文章
