使用 Taro 開發微信小程式的實踐 + 踩坑合集

松鼠桂魚發表於2019-03-11

我和這篇文章

我是一名前端愛好者,現在是大三學生。大二開始接觸小程式開發,目前自己唯一還在弄的專案是校內面向學生的一款課程評測小程式 uCourse。

使用過微信小程式原生語言開發過小程式,也用過一系列後來緊隨其上的第三方框架,如 WePY (1.x), mpvue (1.x),以及 Taro (0.x 至今)。

出現這麼多第三方框架的原因,在我看來,微信小程式原生語言的不合理性和缺陷「功不可沒」,同時後期湧現的一批「xx 小程式」加強了多端編譯需求的重要性。而我有幸體驗到了這一批框架從早期到現在的發展過程,也有了一些自己的對比感受,最終選擇了 Taro。

這篇文章我想簡單總結一下我在使用 Taro 開發小程式時的一些經驗和踩坑,以及我在與微信小程式糾纏時的各方面心路歷程……(主要是微信小程式,我還暫時沒有開發多端。)

另外除了 Taro 本身,也會帶一些小程式的開發實踐。總之亂七八糟說啦,希望能對各位有所幫助。

原生小程式開發 vs 第三方框架

原生小程式開發的痛點

原生小程式開發有哪些痛點?Taro 這篇 《為什麼我們要用 Taro 來寫小程式 - Taro 誕生記》 裡,點明得相當到位。

總結來說:

  • 單頁面的檔案結構繁瑣(四個之多)
  • 語法像 React、像 Vue、又像風(四不像)
  • 組建/方法命名規範不統一(中劃線分割/單詞連寫/駝峰寫法 混雜)
  • 不完整的前端開發體驗(webpack、ES 6+ 語法、CSS 前處理器 等的缺失)

當然,微信小程式官方團隊也在不斷完善和發展。不過迭代速度、以及開發者社群內反饋問題的積極性實在讓人不敢恭維……

第三方框架一覽

目前(2019 年 3 月)——

  • WePY (類 Vue.js)
  • mpvue (Vue.js + H5/百度/位元組/支付寶)
  • Min (類 Vue.js)
  • Taro (React.js + H5/百度/位元組/支付寶/RN)
  • nanachi(React.js + H5/百度/位元組/支付寶)
  • uni-app (Vue.js + H5/百度/位元組/支付寶/Native)

至於哪一種更好,由於各個框架都在迭代,我現在無法很全面地比較各個框架。建議各位開發者可以看看社群活躍、再看對多端編譯的需求、再看技術棧符不符合團隊技能。

去年七八月份的時候,後兩款框架還沒推出,我對比了前四款:由於 WePY 的坑太多,Min 社群並不活躍,mpvue 開源後幾乎再無動靜,我個人又比較喜歡 React,Taro 的社群活躍度和版本迭代速度可喜,所以毫無意外選擇了 Taro。但現在前兩款框架又啟動了 2.x 的開發計劃,並且又有另兩款框架冒出……所以各位可以再折騰對比下看看。_(:з」∠)_

Taro 起步

所以閒話少敘,我們說回 Taro……

Taro 的開發體驗可以說和 React 別無二致。如果你有過 React 的開發經驗,可以毫無困難地順滑上手;如果沒有,直接看 Taro 的 官方文件 來入門也是沒有問題的。

從安裝到建立一個新專案使用——

$ npm install -g @tarojs/cli
$ taro init myApp
$ cd myApp
$ npm install
# 開發
$ npm run dev:weapp
# 編譯
$ npm run build:weapp
複製程式碼

這裡的開發和編譯指令中的 weapp 換成 h5 / swan / alipay / tt / rn 後,即可在對應的其他端編譯執行。多端的程式碼邏輯可以不同,詳情請看 跨平臺開發

專案結構

官方有一篇基於 Redux 的專案最佳實踐文章:《Taro深度開發實踐》

官方推薦的專案結構——

├── config                 配置目錄
|   ├── dev.js             開發時配置
|   ├── index.js           預設配置
|   └── prod.js            打包時配置
├── src                    原始碼目錄
|   ├── components         公共元件目錄
|   ├── pages              頁面檔案目錄
|   |   ├── index          index 頁面目錄
|   |   |   ├── banner     頁面 index 私有元件
|   |   |   ├── index.js   index 頁面邏輯
|   |   |   └── index.css  index 頁面樣式
|   ├── utils              公共方法庫
|   ├── app.css            專案總通用樣式
|   └── app.js             專案入口檔案
└── package.json
複製程式碼

我在專案中並沒有用到 Redux / MobX,專案「發展壯大」後的結構也比較簡單明瞭——

├── dist                                編譯結果目錄
├── config                              配置目錄
|   ├── dev.js                          開發時配置
|   ├── index.js                        預設配置
|   └── prod.js                         打包時配置
├── src                                 原始碼目錄
|   ├── assets                          公共資源目錄(內含圖示等資源)
|   ├── components                      公共元件目錄
|   |   └── Btn                         公共元件 Btn 目錄
|   |       ├── Btn.js                  公共元件 Btn 邏輯
|   |       └── Btn.scss                公共元件 Btn 樣式
|   ├── pages                           頁面檔案目錄
|   |   └── index                       index 頁面目錄
|   |       ├── components              index 頁面的獨有元件目錄
|   |       |   └── Banner              index 頁面的 Banner 元件目錄
|   |       |       ├── Banner.js       index 頁面的 Banner 元件邏輯
|   |       |       └── Banner.scss     index 頁面的 Banner 元件樣式
|   |       ├── index.js                index 頁面邏輯
|   |       └── index.scss              index 頁面樣式
|   ├── subpackages                     分包目錄(專案過大時建議分包)
|   |   └── profile                     一個叫 profile 的分包目錄
|   |       └── pages                   該分包的頁面檔案目錄
|   |           └── index               該分包的頁面 index 目錄(其下結構與主包的頁面檔案一致)
|   ├── utils                           專案輔助類工具目錄
|   |   └── api.js                      比如輔助類 api 等
|   ├── app.css                         專案總通用樣式
|   └── app.js                          專案入口檔案
└── package.json
複製程式碼

什……這也叫「簡單明瞭」嗎? (゚д゚≡゚д゚)

這是我個人比較喜歡的組織方式。我的專案已經不算小了,總計近 30 個頁面,使用上面這種方式維護,確實感覺還挺省心舒服的。當然你也可以按照你的喜好組織檔案~

編譯配置檔案

編譯配置存放於專案根目錄下 config 目錄中,包含三個檔案

  • index.js 是通用配置
  • dev.js 是專案預覽時的配置
  • prod.js 是專案打包時的配置

下面說一些使用案例和某些坑的解決方案——

路徑別名

在專案中不斷 import 相對路徑的後果就是,不能方便直觀地明白目錄結構;如果要遷移改動一個目錄,IDE 又沒有很準確的重構功能的話,需要手動更改每一個 import 的路徑,非常難受。

所以我們想把:

import A from '../../componnets/A'
複製程式碼

變成

import A from '@/componnets/A'
複製程式碼

這種引用。

方式如下:

/* config/index.js */
const path = require('path')
alias: {
  '@/components': path.resolve(__dirname, '..', 'src/components'),
  '@/utils': path.resolve(__dirname, '..', 'src/utils'),
},
複製程式碼

詳見:nervjs.github.io/taro/docs/c…

在程式碼中判斷環境

/* config/dev.js */
env: {
  NODE_ENV: '"development"', // JSON.stringify('development')
},
複製程式碼
/* config/prod.js */
env: {
  NODE_ENV: '"production"', // JSON.stringify('development')
},
複製程式碼

程式碼中可以通過 process.env.NODE_ENV === 'development' 來判斷環境。

區分開發和線上環境的 API

/* config/dev.js */
defineConstants: {
  BASE_URL: '"https://dev.com/api"',
},
複製程式碼
/* config/prod.js */
defineConstants: {
  BASE_URL: '"https://prod.com/api"',
},
複製程式碼

如此一來,可以直接在程式碼中引用 BASE_URL 來基於環境獲取不同的 API Gateway。

解決打包後樣式丟失等問題

如果你在開發中遇到了開發環境時樣式沒有問題,但是編譯打包後出現部分樣式丟失,可能是因為 csso 的 restructure 特性。可以在 plugins.csso 中將其關閉:

/* config/prod.js */
plugins: {
  csso: {
    enable: true,
    config: {
      restructure: false,
    },
  },
},
複製程式碼

解決編譯壓縮過的 js 檔案出錯的問題

如果你遇到了編譯時,壓縮過的 js 檔案過編譯器報錯,可以將其排除編譯:

/* config/index.js */
weapp: {
  compile: {
    exclude: [
      'src/utils/qqmap-wx-jssdk.js',
      'src/components/third-party/wemark/remarkable.js',
    ],
  },
},
複製程式碼

解決編譯後資原始檔找不到的問題

如果你遇到了編譯後,資原始檔(如圖片)沒有被編譯到 dist 目錄中導致找不到,可以令其直接被複制過來:

/* config/index.js */
copy: {
  patterns: [
    {
      from: 'src/assets/',
      to: 'dist/assets/',
    },
  ],
},
複製程式碼

使用微信小程式原生第三方元件和外掛

官方文件:nervjs.github.io/taro/docs/m…

需要注意的是,如果這麼做了,專案就不能再多端編譯了。

使用方式看官方文件描述即可,十分簡單。但我實際在使用的過程中,還是踩了坑的。比如我嘗試整合 wemark 來做 markdown 的渲染。發現了兩個問題:

  1. Taro 會漏編譯僅在 wxss 中引用的 wxss 檔案。解決方式是需要 copy 配置把所有檔案在編譯時全部拷貝過去。
  2. 在編譯壓縮過的 js 檔案時,會再次經過一次編譯導致出錯,且無視 copy 配置。解決方式是需要 exclude 配置把壓縮的 js 檔案排除。(如上面提到的那樣。)

所以以 wemark 為例,專案整合原生元件,需要另加配置:

/* config/index.js */
copy: {
  patterns: [
    {
      from: 'src/components/wemark',
      to: 'dist/components/wemark',
    },
  ],
},
weapp: {
  compile: {
    exclude: [
      'src/components/wemark/remarkable.js',
    ],
  },
},
複製程式碼

然後可以引用了——

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

export default class Comp extends Component {
  config = {
    usingComponents: {
      wemark: '../components/wemark/wemark'
    }
  }

  state = {
    md: '# heading'
  }

  render() {
    return (
      <View>
        <wemark md={this.state.md} link highlight type="wemark" />
      </View>
    )
  }
}
複製程式碼

簡而言之,如果你在整合原生元件中遇到了類似問題,可以試試直接 copy 整個元件目錄,並且排除掉某些 js 檔案防止過編譯。

使用圖示字型元件

我們希望在專案中擁有自己的圖示字型元件,使用方法如下:

<UIcon icon="home" />
複製程式碼

為什麼是 UIcon,而不是 Icon?因為命名不能與官方自帶的元件 Icon 衝突呀……(|||゚д゚) 你也可以管他叫 OhMyIcon 之類的。

這裡先說實踐,再說說坑……

實踐就是,如果大家沒有專業的設計師或者公司內部的圖示庫的話,可以使用 Iconfont 的圖表庫,優點是圖示多而優、CDN 開箱即用。你可以新建一個專案,選擇適合你專案的圖示後,直接獲取到 @font-face 的引用程式碼:

使用 Taro 開發微信小程式的實踐 + 踩坑合集

Unicode 和 Font class 的引用效果幾乎一樣,後者的優勢是 class name 的語義化,而由於我們需要再進行一層包裝,將 class name 變得可定製,所以推薦大家選擇 Unicode。

而 Symbol 的優勢就在於支援多色圖示,那為什麼不用它呢……踩坑啦踩坑啦,微信小程式不相容 svg 圖示 QwQ。(在官方社群搜了很多帖子,官方只會說「好的,我們看看」、「請貼一下程式碼片段」然後就沒動靜了……類似的情況還有很多,提了幾年的 bug,始終不修,留著當傳家寶……(/‵Д′)/~ ╧╧ )

那麼我們在元件的樣式檔案里加上上面這段 @font-face 的程式碼後,再寫類似下面的這段:

/* UIcon.scss */
.u-icon {
  display: inline-block;
  &:before {
    font-family: 'iconfont' !important;
    font-style: normal;
    font-weight: normal;
    speak: none;
    display: inline-block;
    text-decoration: inherit;
    width: 1em;
    text-align: center;
  }
}
複製程式碼

然後針對每個圖示,給出它的 unicode 定義:

/* UIcon.scss */
.u-icon-add::before {
  content: '\e6e0';
}
.u-icon-addition::before {
  content: '\e6e1';
}
/* ... */
複製程式碼

UIcon.js 如此包裝:

/* UIcon.js */
import Taro, { Component } from '@tarojs/taro'
import { View } from '@tarojs/components'
import './UIcon.scss'

export default class UIcon extends Component {
  static externalClasses = ['uclass']

  static defaultProps = {
    icon: '',
  }

  render() {
    return <View className={`uclass u-icon u-icon-${this.props.icon}`} />
  }
}
複製程式碼

這裡注意到我加了一個 externalClasses 的配置,以及一個 uclassclassName。原因是我想在元件外部定義內部的樣式,如此定義後,可以這麼呼叫:

<UIcon icon="home" uclass="external-class" />
複製程式碼

詳情可以看文件 元件的外部樣式和全域性樣式。(這篇文件就是我當時踩完這個坑幫忙給補上的 QwQ……)

包裝一個彙報 formId 的元件

如果你有主動傳送小程式模板訊息卡片的需求,可能需要這樣的元件。

小程式目前的策略是你只能在使用者觸發一個 button 點選事件後,彙報給你一個一次性的 7 天過期 formId,你用它來傳送一次模板訊息。所以湧現一批機智的小程式開發者,把 button 包裹在整個頁面上,使用者每一次點選都彙報一個 formId,存起來之後,七天之內反正不愁啦,慢慢發。而官方貌似也一直睜一隻眼閉一隻眼…… (●` 艸 ´)

Taro 中實現這樣的包裹器也很簡單:

/* FormIdReporter.js */
import Taro, { Component } from '@tarojs/taro'
import { Button, Form } from '@tarojs/components'
import './FormIdReporter.scss'

export default class FormIdReporter extends Component {
  handleSubmit = e => {
    console.log(e.detail.formId) // 這裡處理 formId
  }

  render() {
    return (
      <Form reportSubmit onSubmit={this.handleSubmit}>
        <Button plain formType="submit" hoverClass="none">
          {this.props.children}
        </Button>
      </Form>
    )
  }
}
複製程式碼

在呼叫時,把整個頁面包裹上即可:

<FormIdReporter>
  {/* 一些其他元件 */}
</FormIdReporter>
複製程式碼

需要注意的是,這裡的 Button 需要使用下面的樣式程式碼清除掉預設樣式,達到「隱藏」的效果:

/* FormIdReporter.scss */
button {
  width: 100%;
  border-radius: 0;
  padding: 0;
  margin: 0;
  &:after {
    border: 0;
  }
}
複製程式碼

利用 Decorator 實現快速分享/登入驗證

由於這部分內容也是我從他處學到的,並且有既成的教程,我就不再添油加醋啦。參考:


下面說的這些,更多的是關於小程式自身的一些實踐案例了。當然,也是以 Taro 為背景的。

i18n 國際化

由於專案需要實現小程式文字國際化,我找了很多案例,最終參考了這個比較簡潔的方案的思路:weapp-i18n。已經運用到兩個專案中了。在 Taro 中,可以包裝成下面這個類:

/* utils/i18n.js */
export default class T {
  constructor(locales, locale) {
    this.locales = locales
    if (locale) {
      this.locale = locale
    }
  }

  setLocale(code) {
    this.locale = code
  }

  _(line) {
    const { locales, locale } = this
    if (locale && locales[locale] && locales[locale][line]) {
      line = locales[locale][line]
    }

    return line
  }
}
複製程式碼

新建一個 locales.js,寫上你的本地化語言,key 名要和微信系統語言的叫法一致:

/* utils/locales.js */
locales.zh_CN = {
  Discover: '發現',
  Schools: '學校',
  Me: '我',
  'Courses of My Faculty': '我的院系課程',
  'Popular Evaluations Monthly': '本月熱門評測',
  'Popular Evaluations': '熱門評測',
  'Recent Evaluations': '最新評測',
  'Top Courses': '高分課程',
  /* ... */
}
locales.zh_TW = {
  ...locales.zh_CN,
  Discover: '發現',
  Schools: '學校',
  Me: '我',
  'Courses of My Faculty': '我的院系課程',
  'Popular Evaluations Monthly': '本月熱門評測',
  'Popular Evaluations': '熱門評測',
  'Recent Evaluations': '最新評測',
  'Top Courses': '高分課程',
  /* ... */
}
複製程式碼

使用方式是在 App.js 中先初始化:

/* App.js */
componentWillMount() {
  this.initLocale()
}

initLocale = () => {
  let locale = Taro.getStorageSync('locale')
  if (!locale) { // 初始化語言
    const systemInfo = await Taro.getSystemInfo()
    locale = systemInfo.language // 預設使用系統語言
    Taro.setStorage({ key: 'locale', data: locale })
  }
  Taro.T = new T(locales, locale) // 初始化本地化工具例項並注入 Taro.T
  // 手動更改 TabBar 語言(目前只能這麼做)
  Taro.setTabBarItem({
    index: 0,
    text: Taro.T._('Discover'),
  })
  Taro.setTabBarItem({
    index: 1,
    text: Taro.T._('Me'),
  })
}
複製程式碼

元件中使用:

<Button>{Taro.T._('Hello')}</Button>
複製程式碼

如果小程式提供了更改語言的功能,使用者更改後,儲存配置,然後直接 Taro.reLaunch 到首頁,並且依次如上所述更改 TabBar 的語言即可。

確實挫了一點,不過在我看來,已經是在小程式裡實現國際化的最方便可行的辦法啦……(*´ω`)人(´ω`*)

包裝 API 方法

雖然 Taro 提供了 Taro.request 這個方法,但我還是選擇了 Fly.js 這個庫,包裝了一個自己的 request 方法:

/* utils/request.js */
import Taro from '@tarojs/taro'
import Fly from 'flyio/dist/npm/wx'
import config from '../config'
import helper from './helper'

const request = new Fly()
request.config.baseURL = BASE_URL
const newRquest = new Fly() // 這是用來 lock 時用的,詳見後面

// 請求攔截器,我在這裡的使用場景是:除了某些路由外,如果沒有許可權的使用者「越界」了,就報錯給予提示
request.interceptors.request.use(async conf => {
  const { url, method } = conf
  const allowedPostUrls = [
    '/login',
    '/users',
    '/email',
  ]
  const isExcept = allowedPostUrls.some(v => url.includes(v))
  if (method !== 'GET' && !isExcept) {
    try {
      await helper.checkPermission() // 一個用來檢測使用者許可權的方法
    } catch (e) {
      throw e
    }
  }

  return conf
})

// 響應攔截器,我在這裡的使用場景是:如果使用者的 session 過期了,就鎖定請求佇列,完成重新登入,然後繼續請求佇列
request.interceptors.response.use(
  response => response,
  async err => {
    try {
      if (err.status === 0) { // 網路問題
        throw new Error(Taro.T._('Server not responding'))
      }

      const { status } = err.response
      if (status === 401 || status === 403) { // 這兩個狀態碼錶示使用者沒有許可權,需要重新登入領 session
        request.lock() // 鎖定請求佇列,避免重複請求
        const { errMsg, code } = await Taro.login() // 重新登入
        if (code) {
          const res = await newRquest.post('/login', { code }) // 使用新例項完成登入
          const { data } = res.data
          const { sessionId, userInfo } = data
          Taro.setStorageSync('sessionId', sessionId) // 儲存新 session
          if (userInfo) {
            Taro.setStorageSync('userInfo', userInfo) // 更新使用者資訊
          }
          request.unlock() // 解鎖請求佇列
          err.request.headers['Session-Id'] = sessionId // 在請求頭加上新 session
          return await request.request(err.request) // 重新完成請求
        } else {
          request.unlock()
          throw new Error(Taro.T._('Unable to get login status'), errMsg)
          return err
        }
      }
    } catch (e) {
      Taro.showToast({ title: e.message, icon: 'none' })
      throw e
    }
  },
)

export default request
複製程式碼

你可以在這個的基礎上,再包裝一層 api.js 的 SDK。用起來很舒服~σ ゚∀ ゚) ゚∀゚)σ

使用第三方統計

第三方統計我目前用過兩個,阿拉丁TalkingData。二者進行對比後,發現大同小異,阿拉丁社群更活躍一些,而 TalkingData 提供了資料獲取的 API。但使用中發現,TalkingData 並不能很好地相容 Taro,在我反饋後,得到的回覆是由於小程式第三方開發框架太多,所以沒有支援的計劃 (´c_`);阿拉丁雖然之前也有這樣的問題,但在幾個月前的版本中已經修復,而且提供了整合的參考文件。

所以如果有這方面需求的話,可以考慮看看阿拉丁~

全域性樣式的統一配置檔案

最後,說一個統一全域性樣式的實踐吧。很簡單,比如建立一個 _scheme.scss 的檔案:

/* utils/_scheme.js */
$f1: 80px; // 阿拉伯數字資訊,如:金額、時間等
$f2: 40px; // 頁面大標題,如:結果、控狀態等資訊單一頁面
$f3: 36px; // 大按鈕字型
$f4: 34px; // 首要層級資訊,基準的,可以是連續的,如:列表標題、訊息氣泡
$f5: 28px; // 次要描述資訊,服務於首要資訊並與之關聯,如:列表摘要
$f6: 26px; // 輔助資訊,需弱化的內容,如:連結、小按鈕
$f7: 22px; // 說明文字,如:版權資訊等不需要使用者關注的資訊
$f8: 18px; // 十分小

$color-primary: #ff9800; // 品牌顏色
$color-secondary: lighten($color-primary, 10%);
$color-tertiary: lighten($color-primary, 20%);

$color-line: #ececec; // 分割線顏色
$color-bg: #ebebeb; // 背景色

$color-text-primary: #000000; // 主內容
$color-text-long: #353535; // 大段說明的主要內容
$color-text-secondary: #888888; // 次要內容
$color-text-placeholder: #b2b2b2; // 預設值

$color-link-normal: #576b96; // 連結用色
$color-link-press: lighten($color-link-normal, 10%);
$color-link-disable: lighten($color-link-normal, 20%);

$color-complete-normal: $color-primary; // 完成用色
$color-complete-press: lighten($color-complete-normal, 10%);
$color-complete-disable: lighten($color-complete-normal, 20%);

$color-success-normal: #09bb07; // 成功用色
$color-success-press: lighten($color-success-normal, 10%);
$color-success-disable: lighten($color-success-normal, 20%);

$color-error-normal: #e64340; // 出錯用色
$color-error-press: lighten($color-error-normal, 10%);
$color-error-disable: lighten($color-error-normal, 20%);
複製程式碼

之後在樣式檔案中引用這個配置檔案,使用相應變數,而不使用絕對的數值即可。

對了,Taro 中的 px 其實指的就是 rpx;如果你想要真實的 px,可以大寫 PX


以上就是這些,沒能在開發的同時做筆記導致可能也遺漏了不少點,爭取以後補上吧。寫這麼多一方面是總結,一方面也是分享。謝謝各位看到這裡。如果有任何地方說的不對,歡迎指正教導。我要學習的還有很多!

使用 Taro 開發小程式,總之還是蠻爽的啦,大家快來用(揮手~)

٩(´ ω` )۶

相關文章