和所有框架一樣,Taro 也可能存在 bug。當(dāng)你認(rèn)為你的代碼沒(méi)有問(wèn)題,問(wèn)題出在 Taro 時(shí),可以按照本章內(nèi)容進(jìn)行調(diào)試。
當(dāng)你在 Taro 進(jìn)行 debug 時(shí),請(qǐng)先確認(rèn)一下流程均已完成:
很多時(shí)候只要你把以上四個(gè)流程都走一遍,遇到的問(wèn)題就會(huì)迎刃而解。而作為一個(gè)多端框架,Taro 有非常多的模塊,當(dāng)出現(xiàn)問(wèn)題時(shí) Taro 也需要分模塊進(jìn)行調(diào)試,接下來(lái)我們會(huì)舉一些已經(jīng)解決了的 bug 樣例,闡述我們調(diào)試 bug 的思路:
由于 commander.js 的緣故,在 Mac 下使用 yarn 安裝 CLI,偶爾會(huì)出現(xiàn)執(zhí)行命令報(bào)錯(cuò)的情況
taro-init(1) does not exist, try --help
這時(shí)候,你可以選擇使用 npm 或者 cnpm 重新安裝 CLI,或者將 CLI 添加到環(huán)境變量中來(lái)解決。
由于 Taro 的 @tarojs/webpack-runner
包默認(rèn)依賴了 node-sass
,倒是有些時(shí)候依賴一直安裝不了,在此,建議直接使用淘寶的 cnpm 進(jìn)行安裝依賴,或者嘗試一下這個(gè)包
此問(wèn)題發(fā)生在頁(yè)面或組件更新時(shí)。
在調(diào)用小程序的 setData 方法前,Taro 會(huì)把 state 與 data 做一次 diff。
如果 state 與 data 的某個(gè)屬性值沒(méi)有變化,很有可能就不會(huì)重新 setData 該屬性,導(dǎo)致頁(yè)面或組件沒(méi)有正確更新。
這種問(wèn)題多出現(xiàn)在小程序的表單組件中,例如以下兩個(gè) issue:#1981、#2257。因?yàn)樾〕绦蛞恍┍韱谓M件為非受控組件,表單更新時(shí),對(duì)應(yīng) value 值的 data 并不會(huì)更新,導(dǎo)致 data 值還是初始值。如果再 setState 此屬性為初始值,由于 diff 邏輯判斷屬性值沒(méi)有變化,不會(huì) setData 此屬性,導(dǎo)致視圖沒(méi)有更新。正確做法是在表單組件的 update 事件中 setData value 為當(dāng)前值,保證 data 與表單顯示值保持一致。
開發(fā)者可以在開發(fā)者工具中找到 taro 運(yùn)行時(shí)庫(kù),在 diff 方法前后打斷點(diǎn)或 log,觀察 state、小程序 data 和 diff 后將要被 setData 的數(shù)據(jù),這種排查有助定位很多視圖更新問(wèn)題。
增加數(shù)組元素時(shí),經(jīng) diff 后會(huì)按路徑更新。但由于微信小程序自身 bug,按路徑更新數(shù)組時(shí),數(shù)組 length 不會(huì)正確更新。詳見 #882
此問(wèn)題只出現(xiàn)于微信小程序,微信官方說(shuō)法是暫不修復(fù)。
推薦做法是新開一個(gè) state 值來(lái)同步 length 變化。
這時(shí)候很可能是編譯模板出現(xiàn)了錯(cuò)誤。例如中 #2285 中,題主寫了兩個(gè)嵌套循環(huán),在第二個(gè)循環(huán)中無(wú)法正確地訪問(wèn)到第一個(gè)循環(huán)聲明的 index
變量:
// 假設(shè)源碼在 src/pages/index/index.js 中
rooms.map((room, index) => (
<View key={room.id}>
<View>房間</View>
<View className="men">
{room.checkInMen.map(man => (
<View onClick={this.handleRemoveMan.bind(this, man.id, index)}>
{man.name}
</View>
))}
</View>
</View>
);
而編譯出來(lái)的 wxml
將會(huì)是:
<!-- 編譯后代碼代碼至少會(huì)生成三個(gè)文件,分別是: -->
<!-- dist/pages/index/index.js,dist/pages/index/index.wxml,dist/pages/index/index.json -->
<view wx:for="{{loopArray0}}" wx:for-item="room" wx:for-index="index">
<view>房間</view>
<view class="men">
<view data-e-tap-a-b="{{index}}" bindtap="handleRemoveMan" wx:for="{{room.$anonymousCallee__0}}" wx:for-item="man" data-e-tap-so="this" data-e-tap-a-a="{{man.$original.id}}">{{man.$original.name}}
</view>
</view>
</view>
</view>
觀察編譯前后文件,我們可以發(fā)現(xiàn):由于第二個(gè)循環(huán)沒(méi)有指定 index
變量名,Taro 編譯的循環(huán)也沒(méi)有指定 index
變量名。但問(wèn)題在于微信小程序當(dāng)不指定 index
時(shí),會(huì)隱式地注入一個(gè)名為 index
的變量名作為 index
。因此這段代碼在第二個(gè)循環(huán)中訪問(wèn) index
,實(shí)際上是當(dāng)前循環(huán)的 index
,而不是上級(jí)循環(huán)的 index
。
當(dāng)我們了解到問(wèn)題所在之后,解決問(wèn)題也很容易,只要在第二個(gè)循環(huán)顯式地暴露循環(huán)的第二個(gè)變量即可,源代碼可以修改為:
rooms.map((room, index) => (
<View key={room.id}>
<View>房間</View>
<View className="men">
{room.checkInMen.map((man, _) => (
<View onClick={this.handleRemoveMan.bind(this, man.id, index)}>
{man.name}
</View>
))}
</View>
</View>
);
有時(shí)候我們會(huì)在運(yùn)行時(shí)遇到這樣錯(cuò)誤:
調(diào)試這樣的問(wèn)題也很簡(jiǎn)單,只需要點(diǎn)擊調(diào)用棧從調(diào)用棧最上層的鏈接,點(diǎn)進(jìn)去我們可以發(fā)現(xiàn)是這樣的代碼:
這時(shí)我們可以發(fā)現(xiàn)這個(gè)錯(cuò)誤的原因在于變量 url
在調(diào)用 Object.assign()
函數(shù)時(shí)找不到變量,我們可以再看一下源碼:
// 如果運(yùn)行時(shí)報(bào)錯(cuò)文件路徑是:dist/pages/test/test.js
// 那么就可以推算出源碼在:src/pages/test/test.js
// 編譯后的 js 文件已經(jīng)經(jīng)過(guò) Babel 編譯過(guò),但函數(shù)基本上還是能一一對(duì)應(yīng)的
// 除了 `render()` 函數(shù)會(huì)對(duì)應(yīng)到 `_createData()` 函數(shù),形如 `renderTest` 函數(shù)會(huì)對(duì)應(yīng)到 `createTestData` 函數(shù)
render () {
let dom = null
if (this.props.visable) {
const url = 'https://...'
dom = <Image src={url} />
}
return <Container>
{dom}
</Container>
}
通過(guò)觀察編譯前后代碼,我們可以發(fā)現(xiàn)源碼沒(méi)有任何問(wèn)題,但 Taro 在此問(wèn)題出現(xiàn)的版本沒(méi)有處理好 if 表達(dá)式作用域內(nèi)的變量,調(diào)用 Object.assign()
函數(shù)時(shí) url
變量并不存在于 render
函數(shù)的作用域中。為了解決這個(gè)問(wèn)題,我們可以修改源碼,手動(dòng)把 url
變量也放在 render
函數(shù)作用域中:
render () {
let dom = null
let url = ''
if (this.props.visable) {
url = 'https://...'
dom = <Image src={url} />
}
return <Container>
{dom}
</Container>
}
大部分運(yùn)行時(shí)錯(cuò)誤都可以通過(guò)小程序內(nèi)置的 Chrome DevTools 找到報(bào)錯(cuò)的緣由,如果當(dāng)前調(diào)用棧沒(méi)有找到問(wèn)題所在,可以往上逐層地去調(diào)試各個(gè)調(diào)用棧。Chrome DevTools 相關(guān)文檔請(qǐng)查看:Chrome 開發(fā)者工具
在 #1814 中提到了 this.$router.path
(當(dāng)前頁(yè)面路由的路徑) 有時(shí)無(wú)法訪問(wèn)。經(jīng)過(guò)調(diào)研發(fā)現(xiàn)原因在于 Taro 把獲取路徑的函數(shù)放在了小程序的 onLoad
函數(shù)上,而不是每個(gè)組件都能調(diào)用到這個(gè)函數(shù)。而解決這個(gè)問(wèn)題的方法也很簡(jiǎn)單,如果當(dāng)前頁(yè)面是組件可以直接通過(guò) this.$scope.route
訪問(wèn),更普適的方法則是通過(guò) getCurrentPages
函數(shù)訪問(wèn)到當(dāng)前頁(yè)面的示例,然后訪問(wèn)實(shí)例的 route
或 __route__
訪問(wèn)到當(dāng)前頁(yè)面路由的路徑。
通過(guò)這個(gè)例子,我們不難發(fā)現(xiàn) Taro 的生命周期/路由 和 setState
在小程序端其實(shí)是包裝成 React API 的一層語(yǔ)法糖,我們把這層包裝稱之為 Taro 運(yùn)行時(shí)框架。幾乎所有 Taro 提供的 API 和語(yǔ)法糖最終都是通過(guò)小程序本身提供的 API 實(shí)現(xiàn)的,也就是說(shuō)當(dāng) Taro 運(yùn)行時(shí)框架出現(xiàn)問(wèn)題時(shí),你基本都能使用小程序本身提供的 API 達(dá)到同等的需求,其中就包括但不限于:
this.$scope.triggerEvent
調(diào)用通過(guò) props 傳遞的函數(shù);this.$scope.selectComponent
和 wx.createSelectorQuery
實(shí)現(xiàn) ref
;getCurrentPages
等相關(guān)方法訪問(wèn)路由;createComponent
函數(shù)創(chuàng)建的對(duì)象雖然使用小程序原生方法也能做很多同樣的事,但當(dāng) Taro 運(yùn)行時(shí)框架出現(xiàn)問(wèn)題時(shí),我們還是強(qiáng)烈建議開發(fā)者向 Taro 官方 提交 issue,有能力的開發(fā)者朋友也可以 提交 PR。一方面使用 Taro API 實(shí)現(xiàn)可以幫助你抹平多端差異,另一方面尋找甚至是修復(fù) bug 也有助于加強(qiáng)你對(duì) Taro 和小程序底層的理解。
微信小程序表單組件不是受控組件,當(dāng)用戶操作表單時(shí)視圖會(huì)立即改變,但表單的 value 值還是沒(méi)有變化。
如果在表單 onChange
、onInput
此類值改變回調(diào)中 setState value 為用戶操作改變表單之前的值時(shí),Taro 的 diff 邏輯會(huì)判斷 setState 的 value 值和當(dāng)前 data.value 一致,則放棄 setData,導(dǎo)致視圖沒(méi)有正確更新。
解決辦法:
Input 組件可以通過(guò)在回調(diào)中 return 需要改變的值來(lái)更新視圖。詳見 #2642
小程序 Input 組件文檔截圖:
其它組件需要立即 setState({ value: e.detail.value })
以立即更新同步 data.value 值,然后再 setState 真正需要表單改變的值。詳見 #1981、#2257
Taro 小程序端的 API 只是對(duì)小程序原生 API 簡(jiǎn)單地進(jìn)行了 promise 化,并沒(méi)有做什么額外操作。因此開發(fā)者在遇到這種情況時(shí)可以試試直接使用小程序 API,如微信小程序中直接使用 wx.xxx
。如果有同樣的報(bào)錯(cuò),證明是小程序方面的問(wèn)題。否則則可能是 Taro 的問(wèn)題,可以給我們提相關(guān) issue。
假設(shè)開發(fā)者在調(diào)用某個(gè) API Taro.xxx
,出現(xiàn)類似以下報(bào)錯(cuò):
證明 Taro 還沒(méi)兼容此 API,比如一些小程序平臺(tái)最新更新的 API。這時(shí)可以給我們提 issue 要求添加,或者修改此文件 native-apis.js 后,給我們提 PR。
在 #1804 中提到,只要使用了 Block
組件并且有一個(gè)變量控制它的顯式時(shí),就必定會(huì)報(bào)錯(cuò):
export default class Index extends Component {
config = {
navigationBarTitleText: '首頁(yè)'
};
state = {
num: 1
};
componentDidMount() {
console.log('did');
setTimeout(() => {
this.setState({
num: 0
});
}, 2000);
}
render() {
const { num } = this.state;
return (
<View className="container">
{num === 0 && <View>已賣完</View>}
{num > 0 && (
<Block>
<View>正在銷售</View>
<View>立即購(gòu)買</View>
</Block>
)}
{/* {num > 0 && <View>正在銷售</View>} */}
</View>
);
}
}
這個(gè)時(shí)候我們可以把問(wèn)題定位到 Block
組件中,我們可以查看 @tarojs/components
的 Block
組件源碼:
const Block = (props) => props.children
export default Block
也就是說(shuō)當(dāng)變量 num > 0
時(shí),Block
組件的 children
會(huì)顯示,而當(dāng) Block
組件的 children
是一個(gè)數(shù)組時(shí),View.container
的 children
就變成 [一個(gè) View 組件, [一個(gè)數(shù)組]]
,渲染這樣的數(shù)據(jù)結(jié)構(gòu)需要 React.Fragment
的包裹才能渲染。而 Taro 目前還沒(méi)有支持 React.Fragment
語(yǔ)法,所以這樣的寫法就報(bào)錯(cuò)了。解決這個(gè)問(wèn)題也很簡(jiǎn)單,只需要修改 Block
組件,用一個(gè)元素包裹住 children
即可:
const Block = (props) => <div>{props.children}</div>
當(dāng)你遇到了相關(guān)問(wèn)題時(shí),我們準(zhǔn)備了一個(gè)快速起步的沙盒工具,你可以直接在這個(gè)工具里編輯、調(diào)試、復(fù)現(xiàn)問(wèn)題:
Component is not found in path "xxx/xxx/xxx"
解決辦法:
1、檢查有沒(méi)有編譯報(bào)錯(cuò)
2、檢查編譯后的文件是否正確
3、步驟 1 和 步驟 2 如果檢查沒(méi)有問(wèn)題,重啟開發(fā)者工具,否則跳到步驟 4
4、提供具體編譯報(bào)錯(cuò)信息與編譯后文件信息的截圖
更多建議: