動態

詳情 返回 返回

從白屏問題重學模塊機制 - 動態 詳情

背景

公司App使用的是跨平台技術H5+原生混合開發,雙方通信的協議是Jsbridge。
為了獲取用户行為以及跟蹤產品在用户端的使用情況,並且能自動監控到App的所有H5頁面,由Native引入了隔壁部門研發的前端監控SDK。
由於被任命為【推動隔壁部門完善監控SDK事項】負責人,於是在後續使用過程不斷結合實際情況,發現SDK存在問題並進行梳理和方案輸出,但是由於對方不開放SDK源碼,只能把問題和方案告知對方後進行推動和跟進。
在某個版本修復了部分問題並擴展了一些新功能後,推動Native對SDK進行升級,並通過郵件通知各組進行驗證,一切正常,但是隔壁部門上報某個頁面白屏了。後隔壁部門一頓操作總算解決了問題,但是給出的理由是同時引入兩個監控SDK文件衝突導致的問題。後面我對他給的原因進行分析,發現邏輯串不起來,也就不相信了,於是自己研究了起來。

現象

經不斷測試,發現Vue項目均正常,而Backbone.js + Require.js項目頁面安卓一直白屏,ios偶現白屏。經調試,發現報錯 Mismatched anonymous define() module 的問題。

定位之旅

1. 從報錯信息入手

image.png
image.png

根據提示信息訪問官網錯誤信息頁面:https://requirejs.org/docs/errors.html#mismatch,官網給出的解釋如下:
If you manually code a script tag in HTML to load a script with an anonymous define() call, this error can occur.
If you use the loader plugins or anonymous modules (modules that call define() with no string ID) but do not use the RequireJS optimizer to combine files together, this error can occur.

2. 理解官網解釋

  • anonymous defines 解釋
define(function() {
    return { helloWorld: function() { console.log('hello world!') } };
})
  • define with string id 解釋
define('moduleOne',function() {
    return { helloWorld: function() { console.log('hello world!') } };
})
  • call define() with no string ID 解釋
define(function() {
    return { helloWorld: function() { console.log('hello world!') } };
})

3. 分析定位

通過第2步瞭解到報錯原因可能是調用了define函數,而且傳入的第一個參數不是字符串導致的。於是,搜代碼定位到監控SDK文件採用UMD打包模式,裏面有一句關鍵代碼

typeof define === 'function' && define.amd ? define(factory) : factory()

為了驗證猜想,我把define函數調用刪除後,重新運行代碼,果然頁面渲染出來了,也就是問題就在這裏。

4. 解決方案

  • 方案一
    這也是隔壁部門實施的方案,就是SDK直接不用UMD模式,改成IIFE模式,只能感嘆真簡單粗暴!至於為什麼能想到,後面給答案。
  • 方案二
    讓SDK優先於require.js下載執行,理由後面給出,但是因為當前項目屬於老破大的屎山項目,而且領導對頁面性能也有要求,所以不太適合。
  • 方案三
    使用require.js方式加載SDK,而不是用script標籤方式引入,這應該是最合理的方案,但是猜測目前維護性能監控SDK的人太年輕,根本不懂requirejs是啥,怎麼用,所以沒想到這個方案(其實我沒研究之前也不知道,因為之前我也沒接觸過Backbone.js + Require.js項目,也沒研究過Require.js)。示例代碼

      require(['http://some.domain.dom/path/to/papm.js'],
    function (papm) {});
  • 方案四
    網上所謂奇淫技巧,本質跟方案二是一樣的,即加載SDK時不受AMD模式影響,相關代碼參考

      <script>
          window.__define = window.define;
          window.__require = window.require;
          window.define = undefined;
          window.require = undefined;
      </script>
      <script src="your-script-file.js"></script>        
      <script>
          window.define = window.__define;
          window.require = window.__require;
          window.__define = undefined;
          window.__require = undefined;
      </script>

5. 快問快答

  1. 為什麼升級前沒問題,升級後有問題?

    • 因為升級前就是IIFE模式,這也是為什麼他們最終能解決而且用的是IIFE的原因。
  2. AMD模式和requirejs到底怎麼個衝突法?怎麼定位到的?

    • 一句話,擼requirejs源碼。
      具體分析如下:

      當先加載requirejs,後加載UMD模式SDK時,
      執行requirejs會依次調用req({})和req(cfg),
      每次調用req首先會調用intakeDefines函數,它會做2件事,

      • 將globalDefQueue隊列任務放到defQueue隊列,
      • 檢查defQueue隊列成員合法性;

      接着會調用context.nextTick啓動一個定時器,回調函數也是intakeDefines函數;
      而執行UMD模式SDK時,調用define(fn)會觸發requirejs源碼define定義,即把fn函數push進globalDefQueue;
      而當context.nextTick回調函數intakeDefines在這之後從宏任務隊列取出執行的話,當檢查defQueue隊列成員合法性時,由於 define(fn) 第一參數為 function而不是string,所以 name = null,則觸發錯誤"Mismatched anonymous define() module: "。

    • 執行流程代碼可簡化如下

      req({}) -> context.nextTick(cb1)
      req(cfg) -> context.nextTick(cb2)
      next script tag run -> define(fn) -> globalDefQueue.push(fn);
      cb1() -> check fn -> "Mismatched anonymous define() module: "
      cb2()
    • 瀏覽器運行日誌也驗證此流程
      截屏2023-12-19 下午11.54.36.png
  • 為什麼安卓大概率白屏,ios偶現白屏?

    • 為了驗證 requirejs裏的nextTick回調函數 和 通過script標籤加載SDK哪個先執行問題,寫了一個測試demo頁面scriptOrSettimeout.html。通過測試(不斷刷新),當 requirejs 和 SDK 都是通過 src 加載時,且 requirejs 在前面,chrome 和 safari 瀏覽器都有報錯概率,特別還和 settimeout 設置的延時有關,如設 1 和 5,結果也不大一樣;也和 nextTick 和 define()中間代碼執行時間有關係;所以因為執行順序存在不確定性,所以白屏也是概率出現。
    • 白屏時候瀏覽器日誌截圖請看上一張圖
    • 沒有白屏時候的瀏覽器日誌截圖
      截屏2023-12-19 下午11.54.24.png

Add a new 評論

Some HTML is okay.