當學習成為了習慣,知識也就變成了常識。感謝各位的 點贊、收藏和評論。
新視頻和文章會第一時間在微信公眾號發送,歡迎關注:李永寧lyn
文章已收錄到 github,歡迎 Watch 和 Star。
簡介
從問題定位開始,到給框架(uni-app)提 issue、出解決方案(PR),再到最后的思考,詳細記錄了整個過程。
前序
當你在業務中不幸踩了開源框架的某些坑,這是你的不幸,但這同時也是你的幸運,因為這是你給自己簡歷中增加亮點的絕佳機會。
而給開源社區貢獻 PR 是你證明自己技術側擁有 P7 實力的絕佳方式,P7 的評判標准無非是業務和技術,業務上有收益,技術上有深度和廣度(別人有的你能做的更好,別人沒有的你能有)。
這次整個過程歷時 3-4 天,在此之前我也沒讀過 uni-app 和 ucharts 的源碼,所以這里把整個過程分享出來也是給大家一個解決問題的思路。
環境
- uni-app cli 版本 3.0.0-alpha-3030820220114011
- hbuilder 版本 3.3.8.20220114-alpha
- ucharts 版本 uni-modules 2.3.7-20220122
現象
uni-app、vue3 + ucharts 繪制圖表,開發環境正常,但是打包上線后,H5 無法繪制圖表,也不報任何錯誤。
開發 | 線上 | |
---|---|---|
APP | 正常 | 正常 |
H5 | 正常 | 無法繪制 |
問題定位
給 ucharts 的社區提 issue,經過交流,維護者 “懷疑“ 是 uni-app 的 vue3 的 renderjs 有問題,但是他也給不了一個肯定的答復,讓去 uni-app 的社區提 issue 而且示例中不能用 ucharts。個人對於該回答持懷疑態度,於是決定自己去定位問題。
懷疑是 ucharts 的 bug
- ucharts 視圖部分的關鍵代碼
<view ...其它屬性 :prop="uchartsOpts" :change:prop="rdcharts.ucinit">
<canvas ...屬性 />
</view>
這里有一個知識點需要補充:當 prop 發生改變,change:prop 的回調會被調用,這是 uni-app 框架提供的能力,但官方文檔沒有提及,從源碼中可以看到。
- 看了 ucharts 的源碼,繪制圖表時的代碼執行過程如下:
可是打包后的 H5 線上環境,當執行 this.uchartsOpts = newConfig
之后卻沒有觸發 change:prop
事件,所以這看起來似乎是 uni-app 的 view 組件有問題
感謝 ucharts 官方,在定位問題過程中,和社區進行交流后,ucharts 免費贈送了一個永久超級會員,感謝 🙏 🙏 !!
view 組件的 prop 和 change:prop
提供如下示例:
<template>
<view>
<view :prop="counter" :change:prop="changeProp"></view>
<view>{{ msg }}</view>
</view>
</template>
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from "vue";
const counter = ref(1)
const msg = ref('hello')
function changeProp() {
msg.value = 'hello' + counter.value
}
// @ts-ignore
let timer = null
onMounted(() => {
timer = setInterval(() => {
counter.value += 1
}, 1000)
})
onBeforeUnmount(() => {
// @ts-ignore
clearInterval(timer)
})
</script>
<style>
</style>
H5 開發環境 | H5 打包后 | |
---|---|---|
vue2 | 正常 | 正常 |
vue3 | 正常 | change:prop 未執行 |
因為開發環境沒有問題,所以在開發環境中通過在 change:prop 方法中打斷點,查看調用棧,找到觸發 change:prop 回調的方法,再一步步往上看,終於發現了 uni-app 重寫渲染器(render 函數)的地方,在 @dcloudio/uni-h5-vue/dist/vue.runtime.esm.js 中。
通過閱讀 uni-app 的源碼,得到如下內容:
響應式數據發生變化,觸發 vue 的響應式更新。比如你的響應式數據作為元素的 prop 屬性傳遞,則在 patch 階段會觸發 patchProps 方法, 觸發該方法后,方法內判斷新老 props 是否發生改變,如果變了,則遍歷新的 props 對象,將其中的每個屬性、值和老的對比,如果不相等 或者 props 的 key 為 change:xx 則直接調用 patchProp 方法,如果 __UNI_FEATURE_WXS__
為真並且 props 的 key 為 change: 開頭,則調用 patchWxs,patchWxs 方法最終會通過 nextTick 調用 change:prop 的回調方法。
以下為上述執行過程的流程圖:
最終定位到問題就出在 __UNI_FEATURE_WXS__
上,發現開發環境中它是 true,但是打包后就變成了 false。
__UNI_FEATURE_WXS__
__UNI_FEATURE_WXS__
是一個全局變量,所以肯定是通過 vite 的 define 選項進行設置的。
於是接下來的目的就是需要找到 __UNI_FEATURE_WXS__
是在什么地方進行設置的。可以全局搜該變量,然后找到在 @dcloudio/uni-cli-shared
包中找到一個叫 initFeatures
的方法,該方法中聲明了一個 features
對象:
const {
wx,
wxs,
// ...其它變量
} = extend(
initManifestFeature(options),
// ... 其它方法
)
const features = {
// vue
__VUE_OPTIONS_API__: vueOptionsApi, // enable/disable Options API support, default: true
__VUE_PROD_DEVTOOLS__: vueProdDevTools, // enable/disable devtools support in production, default: false
// uni
__UNI_FEATURE_WX__: wx, // 是否啟用小程序的組件實例 API,如:selectComponent 等(uni-core/src/service/plugin/appConfig)
__UNI_FEATURE_WXS__: wxs, // 是否啟用 wxs 支持,如:getComponentDescriptor 等(uni-core/src/view/plugin/appConfig)
// ... 其它屬性
}
看了該對象的設置沒什么問題,wxs
在開發和生產環境下都是 true。那接下來就需要找到誰調用了 initFeatures 方法,而且可能調用完了以后通過判斷當前命令,比如:執行 build 時,將 __UNI_FEATURE_WXS__
設置為了 false。
剛開始想正向推導。vite-plugin-uni 是 uni-app 提供給 vite 的一個插件框架,uni-app 中的 vite 配置都來自於這里。
插件當中的 uni 插件提供了 config 選項,config 選項的值是調用 createConfig 方法返回的函數,該函數會返回一個對象,該對象會和 vite 的配置做深度合並;該對象有 define 選項,該選項的值為 createDefine 函數的返回值,該返回值是一個對象,其中調用了 initDefine,再往下看發現不對,然后路 走死了。
發現上面正向推導的方式走不通以后,於是開始反向推導,即全局搜索,都有哪些地方調用了 initFeatures,然后一步步的往下推,得到如下正確的流程圖:
經過最終的調試,發現 啟動開發環境和打包時最終的調用路徑是:uniH5Plugin -> createConfig -> configDefine -> initFeatures。
而最終的問題也就是出在了 initFeatures 方法調用的 initManifestFeature 方法中。
答案
最終定位到出問題的地方在 @dcloudio/uni-cli-shared/src/vite/features.ts
文件的 initManifestFeature
方法中。有如下對比:
- github 倉庫的最新代碼,版本號:3.0.0-alpha-3030820220114011
if (command === 'build') {
// TODO 需要預編譯一遍?
// features.wxs = false
// features.longpress = false
}
- 已發版的代碼,最高版本號:3.0.0-alpha-3031120220208001
if (command === 'build') {
// TODO 需要預編譯一遍?
features.wxs = false;
features.longpress = false;
}
已發版的版本居然高於倉庫內的最新版本號。查看 npm 上的發布版本信息:
發現版本號發生了回退。這幾次回退的版本號都是不符合規范的版本號,而且其中可能攜帶了 bug,比如上面提到的最高版本。
發版出現版本號不符合規范的情況是由於項目還沒有一個規范的發版流程導致的,但是已經是 alpha 版本了,這種低級錯誤還是應該避免的。
更致命的操作是,回退版本號。uni-app 目前每次升級都是升級的最小版本號后面的數值,而業務項目的 package.json 都是 "@dcloudio/uni-app": "^xxx"
的形式,這就意味着,你每次重新裝包(比如自動化部署時)或者升級包時,都會更新到這個存在 bug 的高版本,這就會導致線上系統報 bug。
解決方案
所以這里正確的處理方式是重新發一個更高版本的包,而不是回退版本。因為該操作會導致用戶線上的系統出 bug,即以下代碼無法正常執行:
<view :prop="msg" :change:prop="cb"></view>
當正常情況下,當 msg 改變后,change:prop 的回調會執行。但是這個攜帶 bug 的高版本包,在打包時(npm run build)將 __UNI_FEATURE_WXS__
設置為了 false,導致 change:prop 的回調不會被調用。
總結
代碼可以回退,但是版本號不要回退,應該基於當前穩定版本,重新發一版版本號更高的版本。
於是就給官方提了 issue 和 解決方案。
結果
官方已采納該解決方案,基於當前穩定版重新發布一版版本號更高的版本。
思考
針對 uni-app 這種處於 alpha 版本的框架,項目內部也確實不應該繼續使用 ^ 符號,還是應該將版本號寫死為最新的 tag 版本,因為總跟隨 alpha 的最新版,確實可能會踩坑。
鏈接
感謝各位的:點贊、收藏和評論,我們下期見。
當學習成為了習慣,知識也就變成了常識。感謝各位的 點贊、收藏和評論。
新視頻和文章會第一時間在微信公眾號發送,歡迎關注:李永寧lyn
文章已收錄到 github,歡迎 Watch 和 Star。