小程式折騰記 - Taro(1.2.x)開發一個微信小程式下來的要點梳理及爬坑姿勢(篇幅有點長)

CRPER發表於2019-01-16

前言

Taro 0.x的時候就寫過一個小玩意,雖然沒什麼人玩. 最近正好看到Taro 1.2.x已經發布了

React風格的小程式框架,部分地方針對小程式做了妥協.找點東西試試水,看看改進如何了.

剛好公司有個需求做個手機端的舉報管理的程式, 開會上攬了過來;

對於這個框架,你除了需要有React的基本功外, 還需要這兩方面的知識;

雖這個專案(僅微信端)不是很大,梳理下還是有挺多東東的,其他廢話不多說,直入主題


問題彙總

生命週期及JSX{}不支援箭頭函式

用箭頭函式會出現不可控的結果;

最常見的就是報錯;

  • JSX裡面僅支援onClick={this.xxxx.bind(this,args)這種寫法
  • 生命週期用了會導致,store亦或者render的結果異常(比如執行順序和值的錯誤)
  • 至於其他常規函式則支援箭頭函式的寫法

動態樣式

雖說Taro官方支援CSS Module,若是你考慮多平臺的話..還是建議用常規的命名規劃來維護好一些

至於className的動態css class處理..我傾向於使用classnames這個庫

classname: 最普通的用法如下


// 在Taro的用法跟在React的用法差不多..Taro上需要放在{} ,不然會直接報錯(就是= classnames物件這種寫法)

import classnames from 'classnames'

 <View className={classnames({
  "tab-operator": true,
  "show": userIdList.length>0,
  "hide": userIdList.length === 0 
})}>
  <View className="tab-content-top-counter">
    <View className="left-text">{userIdList.length === 0 ?'如需操作條目請勾選下面條目!':`選中了${userIdList.length}條`}</View>
    {userIdList.length === 0 ? null : <View className="unselect" onClick={this.unselect.bind(this)}>取消</View>}
   
</View>

複製程式碼

自己封裝的元件提示型別缺失的(TS)

比如你封裝的元件裡面依賴了Taro封裝的一些元件,這時候暴露這個元件,

就會缺失你自己加進去的特性,導致編輯器會提示有錯誤資訊..

最簡便的就是用type或者interface,這樣就不會報錯了.比如下面

//方式一
type staticPropsSearchPanel={
  open: boolean,
  onClose?: () => void
} 

// 也可以用interface ,與上面的區別,比較明顯的是這個可以繼承其他的
// 方式二
interface staticPropsSearchPanel {
     open: boolean,
     onClose?: () => void
}


class SearchPanel extends Component<staticPropsSearchPanel>{}

複製程式碼

元件支援程度

  • 不支援函式式元件:具體看官方說法 截止1.2.x依舊不支援,只能寫成 class xx extends Component這種

  • 不支援同個檔案內直接多個class xx extends且被引用


允許幾種狀態管理器的接入?

dva,mobx,redux 都有對應taro接入方案,後兩者是taro官方維護


是否支援alias

最新版是支援的(可用),在config目錄暴露了配置檔案,當然很多其他webpack的配置也有一些直接暴露

小程式折騰記 - Taro(1.2.x)開發一個微信小程式下來的要點梳理及爬坑姿勢(篇幅有點長)

至於eslint不識別alias符號的,這個暫時無解,我試過社群的一些方案,好像沒啥用!


路由跳轉註意點

  • 中劃線的坑 跳轉的路由不支援中劃線(目前),以後未知

開發模式和真機除錯可以正常編譯,打包上傳就不能識別了...浪費我很多時間..

  • 路徑的坑

跳轉的url必須全路徑!!!!!,比如

// 重定向,會提供返回按鈕
Taro.redirectTo({ url: '/pages/list/index' })

// 過載整個程式,關閉其他所有頁面(清除堆疊的頁面),然後開啟你指定的頁面
// 這個非常適合鑑權失敗或者過期的時候.只開啟註冊頁面
Taro.reLaunch({ url:'/pages/login/index'})

//還有其他`navigate`這些,基本都是微信文件寫到的東西,taro封裝了下

複製程式碼

鑑權頁面渲染突兀的改善姿勢!

若是你在第一個頁面做鑑權跳轉,很容易就遇到渲染部分再跳轉的

給人的視覺反饋不是很好,對於此,寫一箇中間鑑權頁面作為第一頁,跳轉會改善很多(視覺上)

因為效果可以定製,而不渲染很多沒必要的元件

比如我的,我的入口頁面就是auth

import './index.less';

import { View } from '@tarojs/components';
import Taro, { Component, Config } from '@tarojs/taro';

class Auth extends Component {

  /**
   * 指定config的型別宣告為: Taro.Config
   *
   * 由於 typescript 對於 object 型別推導只能推出 Key 的基本型別
   * 對於像 navigationBarTextStyle: 'black' 這樣的推匯出的型別是 string
   * 提示和宣告 navigationBarTextStyle: 'black' | 'white' 型別衝突, 需要顯示宣告型別
   */
  config: Config = {
    navigationBarTitleText: 'xx小助手'
  }


  static options = {
    addGlobalClass: true
  }

  // 有token就可以進入內容區域,至於token是否有效是在裡面去判斷的;
  // 沒有token乖乖去登入
  componentDidShow() {
    const token = Taro.getStorageSync('token');
    if (!!token) {
      Taro.redirectTo({ url: '/pages/list/index' })
      return
    }
    Taro.redirectTo({ url: '/pages/login/index' })
  
  }

  render() {
    return (
      <View className='auth-page'>loading....</View>
    )
  }
}

export default Auth
複製程式碼

componentDidShow的注意點

previewImage(圖片的點選全屏預覽),在關掉後會再次觸發該生命週期..

所以把請求放這裡的需要自己權衡下..比如我的列表展開後,點選圖片關閉後導致列表重刷;

挪到了componentWillMount就不會受previewImage的影響


mobx的接入及資料觀察?

mobx的接入和常規的接入差不多,用法基本也一致..

就是從mobx-react變成@tarojsw/mobx,由taro封裝來提供

至於devtools這種.小程式目前只能從開發者工具看到,

雖然沒專業的devtools那麼清晰,但是總體上可以看到資料的組織和響應,如圖

小程式折騰記 - Taro(1.2.x)開發一個微信小程式下來的要點梳理及爬坑姿勢(篇幅有點長)


結合mobx在跳轉前預請求?

比如詳情頁,展示類的頁面,我們一般都是通過typeId去拿到具體的詳情,再來展示

常規做法都是進到頁面後在componentDidMount去觸發請求,然後把結果集渲染到頁面,

但這樣一進去就會展示預設資料再替換,有點突兀;我們肯定想改善使用者體驗,那就把資料預請求

我們可以根據實際場景在跳轉之前的生命週期入手,比如redirecTo可以在componentDidHide內呼叫函式dispatch

reLuanch可以在componentWillUnmount內觸發;

跳轉過去的頁面,可以直接從props拿到渲染,不會那麼突兀


時間戳及常見日期格式轉換

對於日期的處理,我們最常用的是兩種姿勢的傳遞的時候用時間戳,展示的時候用可讀性較強的YYYY-MM-DD這種

所以就沒必要引入moment這個大庫了用的是dayjs,很小功能比較全面的庫,apimoment,用過都說好.

當然,你自己用函式封裝一個轉換也行,就不用引入多一個庫了,見仁見智了.


獲取結點資訊注意點

若是要指定元件自身內的結點,this必須為this.$scope

微信小程式官方的this代表例項,在tarothis.$scope代表元件自身(例項)


  componentDidMount() {
    const query = Taro.createSelectorQuery().in(this.$scope);
    query.select('#list-card').boundingClientRect((res) => {
      console.log('res: ', res);
    }).exec()
  }
複製程式碼

變動專案基礎資訊(微信小程式)

直接在開發者工具的選項裡面勾選不會儲存到專案內,比如基礎庫的切換;

有效的是直接操作根目錄下的project.config.json


// 這份配置的引數可以具體看微信官方給出的解釋,會更加全面
// https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html?search-key=%E9%A1%B9%E7%9B%AE%E9%85%8D%E7%BD%AE

{
	"miniprogramRoot": "打包輸出路徑",
	"projectname": "專案名稱",
	"description": "聲兮管理後臺小程式",
	"appid": "xxxx",
	"setting": {
		"urlCheck": true, // 是否檢查安全域名和 TLS 版本
		"es6": false,  // 是否啟用es6轉換
		"postcss": true,  // 啟用postcss的支援
		"minified": false,  // 是否壓縮程式碼
		"newFeature": true  // 是否啟用新特性的支援
	},
	"compileType": "miniprogram",  // 編譯型別
	"libVersion": "2.5.0", // 基礎庫版本的指定
	"condition": {}
}

複製程式碼

其他小程式有對應的配置檔案,看官方連結


封裝的一些小玩意

請求封裝(TS)

  • request.tsx
    • 支援路由prefix
    • header的合併
    • 響應的攔截

/*
 * @Author: CRPER
 * @LastEditors: CRPER
 * @Github: https://github.com/crper
 * @Motto: 折騰是一種樂趣,求知是一種追求。不懂就學,懂則分享。
 * @Description:請求介面封裝
 */
import Taro from '@tarojs/taro';
import '@tarojs/async-await';

interface options {
  header: any,
  method?: string,
  dataType?: string,
  responseType?: string,
  success?: Function,
  fail?: Function,
  complete?:Function
}

/**
 * 
 * @param url : 介面路徑
 * @param method : 請求方法(RESTFUL,但是沒有PATCH,看微信文件支援)
 * @param data : 傳遞的資料
 * @param options : 可以覆蓋header這些
 * @param prefix : 介面額外的字首
 */
export default async function(url: string, method?:string,data?: string | [any] | Object, options?: options, prefix?: string){
  
  // 不支援patch!!!!!微信自家的請求本身就不支援patch!!!

  // 微信端自己快取token
  const wxToken:string|void =await Taro.getStorage({ key: 'token' })
    .then(res => res.data).catch(err => {
      if(err)  return 
    } )
 
  // 預設值
  const defaultOtions: options = {
    method: 'GET',
    header:{}
  }




  // 若是存在token則賦予
  if (wxToken) {
    defaultOtions.header.Authorization = wxToken
  }

  const baseUrl: string = process.env.NODE_ENV === 'development' ? 'https://xxx.com/api/web' : 'https://xxx.com/api/web';
  const newUrl = prefix ? `${baseUrl}${prefix}${url}` : `${baseUrl}${url}`

  const requestObject: any = {
    url: newUrl,
    ...defaultOtions,
    ...options,
    method,
    data
  }

  const codeMessage: Object = {
    200: '伺服器成功返回請求的資料。',
    201: '新建或修改資料成功。',
    202: '一個請求已經進入後臺排隊(非同步任務)。',
    204: '刪除資料成功。',
    400: '發出的請求有錯誤,伺服器沒有進行新建或修改資料的操作。',
    401: '使用者沒有許可權(令牌、使用者名稱、密碼錯誤)。',
    403: '使用者得到授權,但是訪問是被禁止的。',
    404: '發出的請求針對的是不存在的記錄,伺服器沒有進行操作。',
    406: '請求的格式不可得。',
    410: '請求的資源被永久刪除,且不會再得到的。',
    412: '訪問被拒絕,請重新登入',
    422: '當建立一個物件時,發生一個驗證錯誤。',
    500: '伺服器發生錯誤,請檢查伺服器。',
    502: '閘道器錯誤。',
    503: '服務不可用,伺服器暫時過載或維護。',
    504: '閘道器超時。',
  };



  // 檢測請求狀態
  const checkStatusAndFilter = (response):Promise<any> | undefined => {
    if (response.statusCode >= 200 && response.statusCode < 300) {
      if (response.statusCode === 200 || response.statusCode === 304) {
        return response.data
      }
      return response;
    }

    // 除此之外的錯所有遍歷上面的錯誤資訊丟擲異常
    const errortext = codeMessage[response.statusCode] || response.errMsg;
    Taro.showToast({
      title: errortext,
      mask: true,
      icon: 'none',
      duration: 2000
    })
    return Promise.reject(response)
  };



  try {
     return await Taro.request(requestObject)
      .then(checkStatusAndFilter)
      .then(res => {
        // 這一塊是我和後端協商的,介面內部為1則出錯的,為0才有資料回來
        if (res.code === 1) {
          const errMsg = res.msg ? res.msg : '介面錯誤了';
          Taro.showToast({
            title: errMsg,
            mask: true,
            icon: 'none',
            duration: 2000
          })
          Promise.reject(errMsg)
        }
        if (res.code === 0) {
          if (res.data) {
            return res.data
          }
          return null
        }
        return res
      }).catch(errRes => {
        if (errRes.statusCode === 412) {
          Taro.reLaunch({ url:'/pages/login/index'})
        }
      })
  } catch (err) {
    Taro.showToast({
      title: '程式碼執行異常',
      mask: true,
      icon: 'none',
      duration: 2000
    })
  }
 }


複製程式碼
  • 用法
//  我配置了alias
import wxfetch from '@utils/request';

  // 比如我程式碼中的其中一個請求,處理行為
  // 切割列表資料
  spliceList = (dataIdArr: Array<string | number> = []) => {
    const {list, paginate: {total}} = this.state;
    // 若是隻有一條,幹掉後嘗試請求列表判斷是否還有新的資料
    if (list.length <= 1) {
      this.getList()
    }
    let tempArr: Array<Object> = list.filter((item) => {
      for (let i = 0; i < dataIdArr.length; i++) {
        let innerItemId = Number(dataIdArr[i]);
        if (item.id !== innerItemId) {
          return item
        }
      }
    })
    this.setState({
      list: tempArr,
      paginate: {
        total: total - dataIdArr.length
      },
      dataIdArr: []
    })
  }

  // 處理行為
  handleActionSheetClick = async (e: number): Promise<any> => {
    try {
      const actionParam = {operationType: e};
      const {dataIdArr, operationNote} = this.state;
      const isActionNoValid: boolean = !e || e === 0 || (Array.isArray(dataIdArr) && dataIdArr.length === 0);

      if (isActionNoValid) {
        Taro.atMessage({
          'message': '請再次您的行為是否正常,比如勾選資料!',
          'type': 'error',
          'duration': 1000
        })
        return false;
      }

      await wxfetch('/suspiciousDatas', 'POST', {
        dataIdArr,
        operationNote,
        ...actionParam
      });

      // 切割陣列且關閉遮罩層
      this.spliceList(dataIdArr);
      this.handleActionSheetClose();
    } catch (err) {
      console.log(err);
    }
  }

複製程式碼

簡化版的節流器(TS)

  • throttle.tsx

/*
 * @Author: CRPER
 * @LastEditors: CRPER
 * @Github: https://github.com/crper
 * @Motto: 折騰是一種樂趣,求知是一種追求。不懂就學,懂則分享。
 * @Description: 簡易版的節流函式
 */

 
/**
 * @param fn : 回撥函式
 * @param threshold : 時間,單位毫秒
 */
export default function throttle(fn: Function, threshold: number = 1500) {
  if (threshold === null) {
    threshold = 1500
  }
  let _lastExecTime: null | number = null;
  let context = this
  return function (...args: any[]): void {
    let _nowTime: number = new Date().getTime();
    if (_nowTime - Number(_lastExecTime) > threshold || !_lastExecTime) {
      fn.apply(context, args);
      _lastExecTime = _nowTime
    }
  }
}

複製程式碼
  • 用法

this.xxx.bind的基礎上

import throttle from '@utils/throttle';

// 滾動到頂部觸發
onScrolltoupper = throttle(() => {
    console.log('1111');
},3000)

複製程式碼

下拉重新整理顯示內建的loading.

小程式折騰記 - Taro(1.2.x)開發一個微信小程式下來的要點梳理及爬坑姿勢(篇幅有點長)

就是微信自家的三個小點, 這個需要配置下頁面的一些自有屬性.

Taro只要引入Config,即可在元件內宣告頁面屬性


import Taro, { Component, Config } from '@tarojs/taro';
class ReportList extends Component {

  /**
   * 指定config的型別宣告為: Taro.Config
   *
   * 由於 typescript 對於 object 型別推導只能推出 Key 的基本型別
   * 對於像 navigationBarTextStyle: 'black' 這樣的推匯出的型別是 string
   * 提示和宣告 navigationBarTextStyle: 'black' | 'white' 型別衝突, 需要顯示宣告型別
   */
  config: Config = {
    navigationBarTitleText: '可疑資料彙總',
    enablePullDownRefresh: true,   // 這個是啟用下拉重新整理特性
    backgroundTextStyle: "dark",   // 把顯示的文字顏色改成暗色調,亮色的話.你背景不改看不到,因為同色
    backgroundColor:'#f7f7f7' // 頁面的背景色
  }

}

// 啟用後,記得加對應的條件關閉,不然會一直顯示
 // 下拉重新整理
 onPullDownRefresh = () :void => {
    // 這個loading是 導航欄,頁面標題那塊顯示一個loading  , 微信內建的
    Taro.showLoading({
      title: 'loading....'
    })
    
    // 因為我的介面請求都是 async await的姿勢,所以可以佇列執行
    this.getList(); 
    this.unselect();
    
    // 介面請求完畢後隱藏兩個loading , 標題和下拉區域
    Taro.hideLoading();
    Taro.stopPullDownRefresh();
  }


複製程式碼

實現元件樣式過渡?

實現一個元件過渡可以一定程度上增強體驗,本質就是CSS3來寫過渡,

比如看我這邊實現的一個效果,自己感覺還看得過去

小程式折騰記 - Taro(1.2.x)開發一個微信小程式下來的要點梳理及爬坑姿勢(篇幅有點長)

  • 樣式
//若是要產生視覺效應,那元素有偏移才能看出來,所以一般被作用的元素都不會在預設位置
// 這個專案用了less ,主要過渡

.testdiv{
    opacity: 0;
    transform: translateY(100vh) rotate(270deg) scale(0.5);
    &.fadeIn{
      opacity: 1;
      transform: translateY(0) rotate(0deg);
      transition:all 0.3s ease-in-out; 
    }
    &.fadeOut{
      opacity: 0;
      transform:  rotate(-270deg) scale(0.2) translateX(-100vw);
      transition:all 0.3s ease-in-out; 
    }
    
}

複製程式碼
  • 作用區域

這邊用了classnames來動態追加class

<View className={classnames({ "search-panel": true, 'fadeIn': open, 'fadeOut': !open})} >
</View>
複製程式碼

節點元素高度的過渡(CSS3)

就是讓展開和收起有個過渡效果,

經過N多次的嘗試(不能給元素設定height!!), 把元素初始化的高度設定max-height:0,

其他過渡設定合適的max-height即可解決

小程式折騰記 - Taro(1.2.x)開發一個微信小程式下來的要點梳理及爬坑姿勢(篇幅有點長)


Taro裡面對事件的支援

有些文件沒說到,只能去翻原始碼...看common.d.ts一目瞭然,比如長按事件這些

github.com/NervJS/taro…


css3 loading 引入

小程式折騰記 - Taro(1.2.x)開發一個微信小程式下來的要點梳理及爬坑姿勢(篇幅有點長)

其實跟在普通開發模式上寫法差不,基本還是CSS3的功能,DIV換成能識別的節點而已..比如Taro


// 樣式部分

 .no-data-text {
    background-color: rgba(233, 228, 228, 0.726);
    color: #333;
    height: 100vh;
    width: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
    flex-direction: column;
    font-size: 50px;
    font-weight: 700;
    .loading-text{
      font-size:28px;
      color:#555;
    }
  }

.spinner {
  width: 200px;
  height: 70px;
  text-align: center;
  font-size: 10px;
}
 
.spinner .rect {
  background-color: rgb(123, 176, 225);
  height: 100%;
  width: 10px;
  margin:0 5px;
  display: inline-block;
   
  -webkit-animation: stretchdelay 1.2s infinite ease-in-out;
  animation: stretchdelay 1.2s infinite ease-in-out;
}
 
.spinner .rect2 {
  -webkit-animation-delay: -1.1s;
  animation-delay: -1.1s;
}
 
.spinner .rect3 {
  -webkit-animation-delay: -1.0s;
  animation-delay: -1.0s;
}
 
.spinner .rect4 {
  -webkit-animation-delay: -0.9s;
  animation-delay: -0.9s;
}
 
.spinner .rect5 {
  -webkit-animation-delay: -0.8s;
  animation-delay: -0.8s;
}
 
@-webkit-keyframes stretchdelay {
  0%, 40%, 100% { -webkit-transform: scaleY(0.4) } 
  20% { -webkit-transform: scaleY(1.0) }
}
 
@keyframes stretchdelay {
  0%, 40%, 100% {
    transform: scaleY(0.4);
    -webkit-transform: scaleY(0.4);
  }  20% {
    transform: scaleY(1.0);
    -webkit-transform: scaleY(1.0);
  }
}

複製程式碼
<!--節點部分-->
<View className="no-data-text">
    <View className="spinner">
      <View className="rect rect1"></View>
      <View className="rect rect2"></View>
      <View className="rect rect3"></View>
      <View className="rect rect4"></View>
      <View className="rect rect5"></View>
    </View>
    <View className="loading-text">正在載入中......</View>
</View>
複製程式碼

總結

截止該文章輸出的時候,Taro的版本

? Taro v1.2.7


  Taro CLI 1.2.7 environment info:
    System:
      OS: macOS 10.14.2
      Shell: 5.3 - /bin/zsh
    Binaries:
      Node: 10.14.2 - /usr/local/bin/node
      Yarn: 1.13.0 - /usr/local/bin/yarn
      npm: 6.5.0 - /usr/local/bin/npm
    npmPackages:
      @tarojs/async-await: 1.2.7 => 1.2.7 
      @tarojs/components: 1.2.7 => 1.2.7 
      @tarojs/mobx: 1.2.7 => 1.2.7 
      @tarojs/mobx-h5: 1.2.7 => 1.2.7 
      @tarojs/mobx-rn: 1.2.7 => 1.2.7 
      @tarojs/plugin-babel: 1.2.7 => 1.2.7 
      @tarojs/plugin-csso: 1.2.7 => 1.2.7 
      @tarojs/plugin-less: 1.2.7 => 1.2.7 
      @tarojs/plugin-sass: 1.2.7 => 1.2.7 
      @tarojs/plugin-uglifyjs: 1.2.7 => 1.2.7 
      @tarojs/rn-runner: 1.2.7 => 1.2.7 
      @tarojs/router: 1.2.7 => 1.2.7 
      @tarojs/taro: 1.2.7 => 1.2.7 
      @tarojs/taro-alipay: 1.2.7 => 1.2.7 
      @tarojs/taro-h5: 1.2.7 => 1.2.7 
      @tarojs/taro-swan: 1.2.7 => 1.2.7 
      @tarojs/taro-tt: 1.2.7 => 1.2.7 
      @tarojs/taro-weapp: 1.2.7 => 1.2.7 
      @tarojs/webpack-runner: 1.2.7 => 1.2.7 
      eslint-config-taro: 1.2.7 => 1.2.7 
      eslint-plugin-taro: 1.2.7 => 1.2.7 

複製程式碼

TaroTaro UI目前版本對ts的支援還有待提高,會偶爾碰到缺少types

若是專案不大,對於想省心的,建議直接擼JS版本;

Taro社群目前還是很活躍的, 照這樣的情況下去,再迭代兩三個X.Y.Z(Y位)版本應該會好用很多.

ts的好處很明顯,編輯器可以直接懸浮顯示推斷的型別,很多錯誤可以在開發過程避免了;

水文到此結束,有不對之處請留言,會及時修正,謝謝閱讀.

相關文章