突然有一個海外用戶反饋問題,說有一個頁面點擊新增按鈕就白屏。對方不會說中文,所以全程英文交流,用上了我摳腳的6級啞巴英語,溝通過程稍微麻煩一點。一開始聽到白屏,心裏還是毫無波動的,這種問題呢,無非就是某個接口數據返回不太科學,然後前端沒有容錯。只需要看見報錯信息必然可以秒解決
前排提示,現在掘金髮文的時候有違禁詞會發不出去。所以花了半小時發文章,反覆使用二分法排除定位違禁詞語,能發出去說明前面內容沒問題,然後再加一點內容繼續試。我還在發文章的時候,就看見200瀏覽了,給200個小夥伴道歉,那時候還在試敏感詞中,文章內容不完整,現在已經好了,可以回頭看了。例如"報錯"有時候需要改成"錯誤"才能過、頁面不能有emoji、"jiechi"也是違禁的(以後叫hijack吧)。這裏強烈建議,給出違禁詞清單、或者監測到違禁詞的時候彈出來提示一下
讓用戶打開控制檯
先讓用戶刷新再復現一遍,保持一直打開console的狀態下操作。再手把手截圖指導,如何打開console切到哪個面板,再讓對方截圖,結果是這樣的報錯:
這個就突然讓我有點懵逼了,竟然不是 cannot read property xx of undefined
這種報錯。細看一下,是react源碼的報錯:dispatch後setstate、觸發批量更新、執行調度。估計是中途有其他操作把dom節點改了,react瞬間懵逼。即使知道大概是這樣,但怎麼排查呢?那就先直接來撈接口數據,放本地跑一下看看能不能復現吧
引導用戶發response過來
經過一番摳腳英語交流和步驟截圖,終於讓用戶把相關接口的返回數據都發過來了。拿到了數據,那就到我表演了。我本地開始跑dev,再把這些接口全部代理到剛剛拿到的數據上
結果,居然正常運行,一切問題都沒有發生
接着我嘗試看看對方的錄屏,結果發現也沒什麼錯誤操作,唯獨就是點一下按鈕,就報錯了,而且還是同樣的react源碼內部的報錯,接口都正常。最後,決定讓用戶掃我電腦的碼,在我電腦登錄賬號
在我電腦登上了別人的號,開始一頓操作,來到同樣的頁面,點一下按鈕,結果又正常,什麼都沒有發生…小朋友,你是否有很多問號
遠程桌面
實在沒辦法了,我直接視頻通話打過去並要求屏幕分享。打通了,開始全程口語交流,摳腳的英語口語水平只能慢慢的講,估計對方勉強聽得懂吧。我重複了之前的操作,果然又出現了,來到同樣的頁面,點了按鈕,馬上報錯了。還是一樣的問題
於是開始打斷點,隨便操作了幾下,居然自己好了!??
後面刷新頁面,全都自然好了…
心累,暫時不管那麼多了,沒事就好了吧,事情就此爲止。
“looks fine for now. Thank you so much!”
事情再次出現
過了幾天,在愉快地寫需求的時候,突然被機器人拉羣,還是同樣的人,還是同樣的問題,只是不同的頁面鏈接了。先別急着動手,捋一下思路:
- react源碼錯誤,必然是有react之外的原生dom操作
- 確認過代碼,沒有任何其他原生dom操作
- 對方在控制檯做了dom操作?不可能,無技術背景
- 那隻能是瀏覽器插件、中間人注入(基本不可能優先級調最低)、翻譯
- 忘了上次打斷點的事情吧,不能投機取巧
上次的經驗告訴我,直接遠程控制是最好的方法。於是馬上連上了遠程控制。檢查了一下瀏覽器插件,沒有什麼插件有影響——瀏覽器插件pass。確認一下是否翻譯,問了對方說有沒有開了翻譯,對方說沒有(遠程桌面看不見彈出菜單的,所以需要人家告訴我)
ok,人家說沒有翻譯,那我就假設這是實話。既然問題發生的根本原因就是有react之外的原生dom操作,那就是dom節點數很有可能不一樣。於是我在控制檯輸入了一下$$('*')
,發現對方電腦上是2400個節點。在我電腦上輸一下,只有2000個節點。讓同事幫忙看看,一樣也是2000個節點。於是我決定對比一下第一個不一樣的節點是怎樣的,在對方的電腦控制檯上輸了一段簡單的腳本:
$$('*').reduce((acc, { tagName }) => `${acc}${tagName},`, '')
我:“could you please copy the txt and send me”
於是我拿到了用戶整個頁面所有的標籤字符串集合,在我打開的頁面的控制檯下,和我的對比一下:
var arr = otherHtml.split(',')
$$('*').findIndex(({ tagName }, i) => tagName !== arr[i])
發現index爲103,找到第103個節點,發現是一個link標籤,引入了translate.googleapis.com
下的一個css,而且html這個標籤多了一個叫做translated-ltr
的class。顧名思義,翻譯實錘了
於是,再繼續展開主內容,發現對方的頁面上多了很多font
標籤!!
果然,還是開了翻譯,只是人家“覺得沒有開”。其實,很有可能是之前設置了一律翻譯,所以後面就一直不用管,所有的網站都會自動翻譯。接着讓用戶按照我的要求,將翻譯關掉。最後,多次重複的操作,問題也沒有出現了
其實,估計之前大家都是腳手架一把刷,並沒有注意到html的lang的值,而且我們這個系統都是英文的。於是出現了一個所有的內容都是英文的“中文”頁面,到了海外Chrome翻譯的邏輯就是,這是“中文”頁面,需要自動翻譯,然後就“英文翻譯成英文”,視覺上無變化,實際上dom節點已經多了很多font
了
<html lang="zh-cn">
爲什麼上次打斷點就沒事
於是我還是想看看爲什麼上次打斷點就沒事了,打開維基百科試一下,在開啓了翻譯的條件下打斷點會發生什麼。打開source面板,勾選了load事件
自動翻譯也開啓
刷新頁面,發現一進來的時候,一切安好,html標籤是這樣
<html class="client-js" lang="en" dir="ltr">
點了兩下下一步的時候,html標籤發生了變化,核心特徵:有translated-ltr類
<html class="client-js translated-ltr ve-not-available" lang="zh-CN" dir="ltr">
再看看element面板,很多font包裹
實際上這就是一個頁面load成功後,Chrome的翻譯功能去拉css和js回來、修改頁面內容的過程。覆盤一下上次能解決問題的斷點操作:
- 我在報錯的發生前最後一個接口的返回打了斷點,勾選了error事件的斷點
- 頁面進來,有一個cors報錯,error卡住。此時已經有請求出去了,斷點卡一下爭取到了時間(你看起來是pending,實際上response已經到你家門口了)
- 再點下一步,前面的數據秒出,一瞬間又卡了,因爲最後一個接口也回來了
- 此時還沒到拉翻譯資源的時候,但頁面已經展示完整。我點一下按鈕,成功越過翻譯導致的頁面元素錯亂。這是一個創建按鈕,創建成功了後面就是用戶自己操作了
- 因爲創建是頻率稍微低一些的行爲,所以幾天內再無收到反饋
- 出現問題通常是setstate後刪掉某個元素,那個元素追溯不到報錯了。這裏點了按鈕的確是會刪掉按鈕並切換頁面內容
看看react具體怎樣纔會報錯
繼續來作死,一起看看怎麼樣才能把react玩壞
const { useState, useLayoutEffect } = React;
export default function App() {
useLayoutEffect(() => {
const font = document.createElement("font");
const app = document.querySelector(".App");
// 製造font包裹的效果,模擬翻譯的效果,破壞原有結構
while (app.firstChild) {
font.appendChild(app.firstChild);
}
app.appendChild(font);
setTimeout(() => {
// set個state看看
setShow(false);
}, 1000);
}, []);
const [show, setShow] = useState(true);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
{show && (
<>
123123
<h2>Start editing to see some magic happen!</h2>
</>
)}
</div>
);
}
預期效果出現了:
其實也不需要手動改,你只需要右鍵開啓翻譯爲中文就可以復現了。問題根源在於react提前把parentNode存起來了,所以操作的時候找不到子節點
解決方法
錯誤邊界組件
利用react的兩個生命週期來感知翻譯錯誤,然後展示兜底ui,提示用戶關掉翻譯。並給出操作文檔鏈接。使用的時候只需要用TranslateErrorBoundary包一下組件即可
class TranslateErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { translateError: false };
}
static getDerivedStateFromError() {
if (document.documentElement.classList.contains("translated-ltr")) {
return { translateError: true };
}
}
componentDidCatch(e, info) {
// 上報翻譯錯誤
report(e, info);
}
render() {
if (this.state.translateError) {
return (
<>
<strong>
translate error! you' d better to turn your google-translate off and reload. see
</strong>
<a
target="_blank"
rel="noopener noreferrer"
href="文檔鏈接"
>
文檔
</a>
</>
);
}
return this.props.children;
}
}
// usage
<TranslateErrorBoundary>
<Cpn />
</TranslateErrorBoundary>
不要讓一塊可刪改的react元素最外層存在文本節點
話不多說,看🌰
<div className="App">
<h1>Hello CodeSandbox</h1>
{show && (
<>
123123
<h2>Start editing to see some magic happen!</h2>
</>
)}
</div>
這一塊,有最外層的123123文本節點,所以翻譯了會報錯:
{show && (
<>
123123
<h2>Start editing to see some magic happen!</h2>
</>
)}
爲什麼呢?先看看翻譯後結果,發現原本想刪的節點是"123123",而他父節點卻再也找不到它了
{show && (
<>
<font><font>123123</font></font>
<h2><font><font>Start editing to see some magic happen!</font></font></h2>
</>
)}
改正措施: 加上span標籤,不要讓123123裸露
{show && (
<>
<span>123123</span>
<h2>Start editing to see some magic happen!</h2>
</>
)
}
// 翻譯後
{show && (
<>
<span><font><font>123123</font></font></span>
<h2><font><font>Start editing to see some magic happen!</font></font></h2>
</>
)
}
因爲最外層的是span,所以即使加了font,也是在span內部加了,刪除元素的時候找的是span,都不會出問題
再看一個🌰
<div>
{label !== undefined ? (
<div>
{label}
</div>
) : null}
{children}
</div>
這裏的話,label就是純文本。經過上面的例子,相信大家都知道{label}
那裏要套一個span了。但是這還是有風險:如果這個組件對外部使用,外部靠children傳進來,意味着children的內容是多變的,比如傳一個字符串進來,setstate後是一個其他節點,那麼問題再次出現
錯誤條件再次重複一遍:一塊可刪改的react元素最外層存在文本節點。此時children是一塊元素,而且是可變的,最外層就是children這個對象的最外層所有節點,其中存在一個文本節點是字符串,因此滿足出錯條件
例如children是文本節點textNode1
,那麼正常情況下setstate後如果children發生變化,刪掉textNode1
的方式就是textNode1ParentNode.removeChild(textNode1)
。如果翻譯了,文本節點包了兩層font,那麼textNode1
再也不是textNode1ParentNode
的子節點了。此外,即使把外層div換成span、section、article同理,都會出錯
推論:不要在任何元素下直接裸露可變文本節點
代碼都是自己寫的,像props.children
這種那麼靈活的,尤其是要注意一下,如果是可能有文本節點的最好包一個span,確認沒有的就可以不用包,防止外國用戶翻譯後源碼出錯。其實可以寫一個工具,掃一下ast,發現有裸露文本節點的自動包一層span
要不,提個issue問問react那邊可不可以不把parent節點先存起來,刪元素的時候直接
node.parentNode.removeChild
?
總結
- 使用數據驅動視圖的框架如react、vue,如果遇到源碼錯誤,考慮一下是不是有原生dom操作打亂了
- 如果確認不是原生dom操作導致,考慮一下瀏覽器插件、翻譯
- 確實需要在react、vue中使用原生操作,需要考慮到這個隱患
- 國際化的業務,如果出現這種問題,建議首先從瀏覽器翻譯開始排查
- 不要讓一塊可刪改的react元素最外層存在文本節點,確認會有可變文本節點,需要套一層span
純寫需求寫業務無聊?那一起搞事情鴨。關注公衆號《不一樣的前端》,以不一樣的視角學習前端,快速成長,一起把玩最新的技術、探索各種黑科技