小夥伴遇到這個問題說不想幹前端了——一次Chrome翻譯形成的玄學bug

忽然有一個海外用戶反饋問題,說有一個頁面點擊新增按鈕就白屏。對方不會說中文,因此全程英文交流,用上了我摳腳的6級啞吧英語,溝經過程稍微麻煩一點。一開始聽到白屏,內心仍是毫無波動的,這種問題呢,無非就是某個接口數據返回不太科學,而後前端沒有容錯。只須要看見報錯信息必然能夠秒解決css

前排提示,如今掘金髮文的時候有違禁詞會發不出去。因此花了半小時發文章,反覆使用二分法排除定位違禁詞語,能發出去說明前面內容沒問題,而後再加一點內容繼續試。我還在發文章的時候,就看見200瀏覽了,給200個小夥伴道歉,那時候還在試敏感詞中,文章內容不完整,如今已經好了,能夠回頭看了。例如"報錯"有時候須要改爲"錯誤"才能過、頁面不能有emoji、"jiechi"也是違禁的(之後叫hijack吧)。這裏強烈建議,給出違禁詞清單、或者監測到違禁詞的時候彈出來提示一下html

讓用戶打開控制檯

先讓用戶刷新再復現一遍,保持一直打開console的狀態下操做。再手把手截圖指導,如何打開console切到哪一個面板,再讓對方截圖,結果是這樣的報錯:前端

這個就忽然讓我有點懵逼了,居然不是 cannot read property xx of undefined 這種報錯。細看一下,是react源碼的報錯:dispatch後setstate、觸發批量更新、執行調度。估計是中途有其餘操做把dom節點改了,react瞬間懵逼。即便知道大概是這樣,但怎麼排查呢?那就先直接來撈接口數據,放本地跑一下看看能不能復現吧vue

引導用戶發response過來

通過一番摳腳英語交流和步驟截圖,終於讓用戶把相關接口的返回數據都發過來了。拿到了數據,那就到我表演了。我本地開始跑dev,再把這些接口所有代理到剛剛拿到的數據上node

結果,竟然正常運行,一切問題都沒有發生react

接着我嘗試看看對方的錄屏,結果發現也沒什麼錯誤操做,惟獨就是點一下按鈕,就報錯了,並且仍是一樣的react源碼內部的報錯,接口都正常。最後,決定讓用戶掃我電腦的碼,在我電腦登陸帳號chrome

在我電腦登上了別人的號,開始一頓操做,來到一樣的頁面,點一下按鈕,結果又正常,什麼都沒有發生......小朋友,你是否有不少問號api

遠程桌面

實在沒辦法了,我直接視頻通話打過去並要求屏幕分享。打通了,開始全程口語交流,摳腳的英語口語水平只能慢慢的講,估計對方勉強聽得懂吧。我重複了以前的操做,果真又出現了,來到一樣的頁面,點了按鈕,立刻報錯了。仍是同樣的問題瀏覽器

因而開始打斷點,隨便操做了幾下,竟然本身好了!??app

後面刷新頁面,全都天然好了........

心累,暫時無論那麼多了,沒事就行了吧,事情就此爲止。

"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

純寫需求寫業務無聊?那一塊兒搞事情鴨。關注公衆號《不同的前端》,以不同的視角學習前端,快速成長,一塊兒把玩最新的技術、探索各類黑科技

相關文章
相關標籤/搜索