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
複製程式碼
// config.ts
"enable_test_mode": false
複製程式碼
可以看到,搜尋 register_test_blocks
模組裡面的相關程式碼已經搜尋不到了。