忽然有一個海外用戶反饋問題,說有一個頁面點擊新增按鈕就白屏。對方不會說中文,因此全程英文交流,用上了我摳腳的6級啞吧英語,溝經過程稍微麻煩一點。一開始聽到白屏,內心仍是毫無波動的,這種問題呢,無非就是某個接口數據返回不太科學,而後前端沒有容錯。只須要看見報錯信息必然能夠秒解決css
前排提示,如今掘金髮文的時候有違禁詞會發不出去。因此花了半小時發文章,反覆使用二分法排除定位違禁詞語,能發出去說明前面內容沒問題,而後再加一點內容繼續試。我還在發文章的時候,就看見200瀏覽了,給200個小夥伴道歉,那時候還在試敏感詞中,文章內容不完整,如今已經好了,能夠回頭看了。例如"報錯"有時候須要改爲"錯誤"才能過、頁面不能有emoji、"jiechi"也是違禁的(之後叫hijack吧)。這裏強烈建議,給出違禁詞清單、或者監測到違禁詞的時候彈出來提示一下html
先讓用戶刷新再復現一遍,保持一直打開console的狀態下操做。再手把手截圖指導,如何打開console切到哪一個面板,再讓對方截圖,結果是這樣的報錯:前端
這個就忽然讓我有點懵逼了,居然不是 cannot read property xx of undefined
這種報錯。細看一下,是react源碼的報錯:dispatch後setstate、觸發批量更新、執行調度。估計是中途有其餘操做把dom節點改了,react瞬間懵逼。即便知道大概是這樣,但怎麼排查呢?那就先直接來撈接口數據,放本地跑一下看看能不能復現吧vue
通過一番摳腳英語交流和步驟截圖,終於讓用戶把相關接口的返回數據都發過來了。拿到了數據,那就到我表演了。我本地開始跑dev,再把這些接口所有代理到剛剛拿到的數據上node
結果,竟然正常運行,一切問題都沒有發生react
接着我嘗試看看對方的錄屏,結果發現也沒什麼錯誤操做,惟獨就是點一下按鈕,就報錯了,並且仍是一樣的react源碼內部的報錯,接口都正常。最後,決定讓用戶掃我電腦的碼,在我電腦登陸帳號chrome
在我電腦登上了別人的號,開始一頓操做,來到一樣的頁面,點一下按鈕,結果又正常,什麼都沒有發生......小朋友,你是否有不少問號api
實在沒辦法了,我直接視頻通話打過去並要求屏幕分享。打通了,開始全程口語交流,摳腳的英語口語水平只能慢慢的講,估計對方勉強聽得懂吧。我重複了以前的操做,果真又出現了,來到一樣的頁面,點了按鈕,立刻報錯了。仍是同樣的問題瀏覽器
因而開始打斷點,隨便操做了幾下,竟然本身好了!??app
後面刷新頁面,全都天然好了........
心累,暫時無論那麼多了,沒事就行了吧,事情就此爲止。
"looks fine for now. Thank you so much!"
過了幾天,在愉快地寫需求的時候,忽然被機器人拉羣,仍是一樣的人,仍是一樣的問題,只是不一樣的頁面連接了。先別急着動手,捋一下思路:
上次的經驗告訴我,直接遠程控制是最好的方法。因而立刻連上了遠程控制。檢查了一下瀏覽器插件,沒有什麼插件有影響——瀏覽器插件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 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包裹
繼續來做死,一塊兒看看怎麼樣才能把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> 複製代碼
話很少說,看🌰
<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
?
純寫需求寫業務無聊?那一塊兒搞事情鴨。關注公衆號《不同的前端》,以不同的視角學習前端,快速成長,一塊兒把玩最新的技術、探索各類黑科技