藉助 Webpack 靜態分析能力實現程式碼動態載入

RingChenng發表於2019-03-04

Debugger 一個動態配置程式碼非同步載入引發的狀態錯誤問題,想起以前在某廠學習的一個解決問題的方法論:

  • 現象背後真實問題是啥?
  • 真實問題背後原因是啥?
  • 對策是要基於原因的,不是基於現象的。

最後從 Webpack 的角度利用靜態程式碼分析的能力來解決問題。

現象

父元件 kitten.tsx

componentDidMount() {
  console.log('cc kitten didMount');
  setTimeout(
    () => {
      console.log('cc kitten render_content before');
      this.render_content();
      console.log('cc kitten render_content after');
    },
    0,
  );
}
render_content() {
  console.log('cc action_fetch_bcm_by_url before');
  // 一個非同步操作,拉取到線上資源後會呼叫 workspace 上的方法
  this.props.action_fetch_bcm_by_url();
  console.log('cc action_fetch_bcm_by_url after');
}
複製程式碼

子元件 BKWorkspaceContainer.tsx

async componentDidMount() {
  console.log('cc BKWorkspaceContainer didMount');

  console.log('cc BKBridge.init before');
  // 初始化 workspace,初始化完成前為 null
  await BKBridge.init();
  console.log('cc BKBridge.init after');
}
複製程式碼

執行順序

cc BKWorkspaceContainer didMount
cc BKBridge.init before
cc kitten didMount
cc kitten render_content before
cc kitten render_content after
cc action_fetch_bcm_by_url before
cc action_fetch_bcm_by_url after

cc BKBridge.init after
複製程式碼

上面順序可能一個個看會看得眼花,而且這只是最外層的函式,裡面還很深很雜,描述一下現象:

  • 方法呼叫 workspace.xxx 報錯了,因為 workspace 還是 null
  • 子元件先 render 沒毛病,但是子元件裡面的 init 方法似乎沒有執行完就把控制權交回給父元件了
  • 父元件 componentDidMount 中使用 setTimeout 0 企圖將 render 任務推到 task 中,甚至這是個 ajax 請求操作,但是在 ajax 請求完成後還是比 BKBridge.init() 完成得要早,ajax 後面的操作用到了 workspace,但是它是 null
  • 最後 cc BKBridge.init after 列印出來了,workspace 初始化完成了

init 阻塞

// 簡化後的呼叫過程
if (config().enable_test_mode) {
  await register_test_block();
}
const workspace_panel = new WorkspacePanel(block_xml);

// register_test_block 程式碼
async function register_test_block(registry:Registry) {
  return new Promise((resolve) => {
    require.ensure([], function(require){
      const { register_test_blocks } = require('../acceptance_test');
      register_test_blocks(registry);
      registry.load_all_block_definitions_into_bk(BK);
      resolve();
    });
  });
}
複製程式碼

用了 require.ensure 這種方式來非同步載入程式碼,程式碼執行到這一段的時候才去拉 JS,所以會出現比 ajax 還慢的情況,它是非同步的。直接阻塞了後面 new WorkspacePanel(block_xml) 的執行。

更長的延時

componentDidMount() {
  setTimeout(
    () => {
      this.render_content();
    },
    100,
  );
}
複製程式碼

通過父元件設定 100ms 的延時,問題就不存在了,但是如果 require('../acceptance_test') 的時間超過了 100ms,怎麼辦呢?

現象背後的問題

程式碼寫成這樣子,最初是為了解決動態載入程式碼的問題,如果 config().enable_test_mode 設定成 true 才接入 acceptance_test 相關的程式碼,否則就連程式碼都不要進入到打出來的包中。

問題背後的原因

所以我們的初衷是為了讓某段程式碼可以通過配置決定是否打包進 boundle。這就好辦了,可以不用把精力放在如何溝通父子元件上面,不用想類似程式碼暫停這種複雜化操作,只要利用 Webpack 打包時候的靜態分析即可。

對策:Webpack 靜態分析

webpack 讀 config 檔案

const runtime_cfg = require('../config')();

module.exports = {
  // ...
  plugins: [
    new webpack.DefinePlugin({
      '__TEST_MODE__': runtime_cfg.client.enable_test_mode
    }),
  ],
  // ...
}
複製程式碼

為全域性定義一個 __TEST_MODE__ 變數,在程式碼的任何地方都可以使用,當然如果是 ts 程式碼的話需要配置:

// global.d.ts
declare var __TEST_MODE__:boolean;
複製程式碼

程式碼中直接寫 require

if (__TEST_MODE__) {
  const { register_test_blocks } = require('../acceptance_test');
  register_test_blocks(registry);
  registry.load_all_block_definitions_into_bk(BK);
}
複製程式碼

結果

// config.ts
"enable_test_mode": true
複製程式碼

01.png

// config.ts
"enable_test_mode": false
複製程式碼

02.png

可以看到,搜尋 register_test_blocks 模組裡面的相關程式碼已經搜尋不到了。

部落格原文引流

www.chenng.cn/post/webpac…

相關文章