Debug 指南

2020-05-12 17:47 更新

Debug 指南

和所有框架一樣,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)一下流程均已完成:

  1. ESLint 已經(jīng)開(kāi)啟并且沒(méi)有報(bào)錯(cuò);
  2. 大致過(guò)了一遍包括最佳實(shí)踐在內(nèi)的文檔,文檔里沒(méi)有對(duì)應(yīng)問(wèn)題的描述;
  3. 搜索過(guò)相關(guān)的 issue,issue 沒(méi)有提到相關(guān)解決方案;
  4. 按項(xiàng)目使用的 Taro 版本往上查看 changelog,changelog 中沒(méi)有意見(jiàn)修復(fù)相關(guān)問(wè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 的思路:

安裝

使用 yarn 安裝完 CLI 報(bào)錯(cuò)

由于 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)解決。

項(xiàng)目依賴一直安裝不下來(lái)

由于 Taro 的 @tarojs/webpack-runner 包默認(rèn)依賴了 node-sass,倒是有些時(shí)候依賴一直安裝不了,在此,建議直接使用淘寶的 cnpm 進(jìn)行安裝依賴,或者嘗試一下這個(gè)包

小程序

沒(méi)有任何報(bào)錯(cuò),但顯示的結(jié)果不如預(yù)期

被 diff 邏輯過(guò)濾

此問(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 與表單顯示值保持一致。

debug diff

開(kāi)發(fā)者可以在開(kāi)發(fā)者工具中找到 taro 運(yùn)行時(shí)庫(kù),在 diff 方法前后打斷點(diǎn)或 log,觀察 state、小程序 data 和 diff 后將要被 setData 的數(shù)據(jù),這種排查有助定位很多視圖更新問(wèn)題。

qq20190305-151951

微信小程序,增加數(shù)組元素?zé)o法正確更新數(shù)組 length

增加數(shù)組元素時(shí),經(jīng) diff 后會(huì)按路徑更新。但由于微信小程序自身 bug,按路徑更新數(shù)組時(shí),數(shù)組 length 不會(huì)正確更新。詳見(jiàn) #882

此問(wèn)題只出現(xiàn)于微信小程序,微信官方說(shuō)法是暫不修復(fù)

推薦做法是新開(kāi)一個(gè) state 值來(lái)同步 length 變化。

編譯模板出錯(cuò)

這時(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>
);

運(yùn)行時(shí)在小程序開(kāi)發(fā)者工具報(bào)錯(cuò)

有時(shí)候我們會(huì)在運(yùn)行時(shí)遇到這樣錯(cuò)誤:

debug.png

調(diào)試這樣的問(wèn)題也很簡(jiǎn)單,只需要點(diǎn)擊調(diào)用棧從調(diào)用棧最上層的鏈接,點(diǎn)進(jìn)去我們可以發(fā)現(xiàn)是這樣的代碼:

debug2.png

這時(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 開(kāi)發(fā)者工具

生命周期/路由/setState 出錯(cuò)

在 #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á)到同等的需求,其中就包括但不限于:

  1. 使用 this.$scope.triggerEvent 調(diào)用通過(guò) props 傳遞的函數(shù);
  2. 通過(guò) this.$scope.selectComponent 和 wx.createSelectorQuery 實(shí)現(xiàn) ref;
  3. 通過(guò) getCurrentPages 等相關(guān)方法訪問(wèn)路由;
  4. 修改編譯后文件 createComponent 函數(shù)創(chuàng)建的對(duì)象

雖然使用小程序原生方法也能做很多同樣的事,但當(dāng) Taro 運(yùn)行時(shí)框架出現(xiàn)問(wèn)題時(shí),我們還是強(qiáng)烈建議開(kāi)發(fā)者向 Taro 官方 提交 issue,有能力的開(kāi)發(fā)者朋友也可以 提交 PR。一方面使用 Taro API 實(shí)現(xiàn)可以幫助你抹平多端差異,另一方面尋找甚至是修復(fù) bug 也有助于加強(qiáng)你對(duì) Taro 和小程序底層的理解。

微信小程序表單組件問(wèn)題

微信小程序表單組件不是受控組件,當(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)更新視圖。詳見(jiàn) #2642

小程序 Input 組件文檔截圖:

inputdoc

其它組件需要立即 setState({ value: e.detail.value }) 以立即更新同步 data.value 值,然后再 setState 真正需要表單改變的值。詳見(jiàn) #1981、#2257

API 問(wèn)題

API 調(diào)用結(jié)果不符合預(yù)期

Taro 小程序端的 API 只是對(duì)小程序原生 API 簡(jiǎn)單地進(jìn)行了 promise 化,并沒(méi)有做什么額外操作。因此開(kāi)發(fā)者在遇到這種情況時(shí)可以試試直接使用小程序 API,如微信小程序中直接使用 wx.xxx。如果有同樣的報(bào)錯(cuò),證明是小程序方面的問(wèn)題。否則則可能是 Taro 的問(wèn)題,可以給我們提相關(guān) issue。

API 調(diào)用報(bào)錯(cuò)

假設(shè)開(kāi)發(fā)者在調(diào)用某個(gè) API Taro.xxx,出現(xiàn)類似以下報(bào)錯(cuò):

image

證明 Taro 還沒(méi)兼容此 API,比如一些小程序平臺(tái)最新更新的 API。這時(shí)可以給我們提 issue 要求添加,或者修改此文件 native-apis.js 后,給我們提 PR。

H5

運(yùn)行時(shí)報(bào) DOM 相關(guān)錯(cuò)誤

在 #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)題:

Edit Taro sandbox

組件

jsEnginScriptError

Component is not found in path "xxx/xxx/xxx" 解決辦法:

1、檢查有沒(méi)有編譯報(bào)錯(cuò)

2、檢查編譯后的文件是否正確

3、步驟 1 和 步驟 2 如果檢查沒(méi)有問(wèn)題,重啟開(kāi)發(fā)者工具,否則跳到步驟 4

4、提供具體編譯報(bào)錯(cuò)信息與編譯后文件信息的截圖


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)