1. 程式人生 > >微信小程式模組化開發框架

微信小程式模組化開發框架

微信小程式元件化開發框架

特性

  • 使用Labrador框架可以使微信開發者工具支援載入海量NPM包
  • 支援ES6/7標準程式碼,使用async/await能夠有效避免回撥地獄
  • 元件重用,對微信小程式框架進行了二次封裝,實現了元件重用和巢狀
  • 自動化測試,非常容易編寫單元測試指令碼,不經任何額外配置即可自動化測試
  • 使用Editor Config及ESLint標準化程式碼風格,方便團隊協作

安裝

首先您的系統中安裝Node.js和npm v3 下載Node.js,然後執行下面的命令將全域性安裝Labrador命令列工具。

npm install -g labrador-cli

初始化專案

mkdir demo           # 新建目錄
cd demo # 跳轉目錄 npm init # 初始化npm包 labrador init # 初始化labrador專案

專案目錄結構

demo                 # 專案根目錄
├── .labrador        # Labrador專案配置檔案
├── .babelrc         # babel配置檔案
├── .editorconfig    # Editor Config
├── .eslintignore    # ESLint 忽略配置
├── .eslintrc        # ESLint 語法檢查配置
├── package.json ├── dist/ # 目標目錄 ├── node_modules/ └── src/ # 原始碼目錄 ├── app.js ├── app.json ├── app.less ├── components/ # 通用元件目錄 ├── pages/ # 頁面目錄 └── utils/

注意 dist目錄中的所有檔案是由labrador命令編譯生成,請勿直接修改

配置開發工具

專案初始化後使用WebStorm或Sublime等你習慣的IDE開啟專案根目錄。然後開啟 微信web開發者工具

 新建專案,本地開發目錄選擇 dist 目標目錄。

開發流程

在WebStorm或Sublime等IDE中編輯 src 目錄下的原始碼,然後在專案根目錄中執行labrador build 命令構建專案,然後在 微信web開發者工具 的除錯介面中點選左側選單的 重啟 按鈕即可檢視效果。

我們在開發中, 微信web開發者工具 僅僅用來做除錯和預覽,不要在 微信web開發者工具 的編輯介面修改程式碼。

微信web開發者工具 會偶爾出錯,表現為點選 重啟 按鈕沒有反應,除錯控制檯輸出大量的無法require檔案的錯誤,編輯 介面中程式碼檔案不顯示。這是因為 labrador build 命令會更新整個dist 目錄,而 微信web開發者工具 在監測程式碼改變時會出現異常,遇到這種情況只需要關掉 微信web開發者工具 再啟動即可。

我們還可以使用 labrador watch 命令來監控 src 目錄下的程式碼,當發生改變後自動構建,不用每一次編輯程式碼後手動執行 labrador build 。

所以最佳的姿勢是:

  1. 在專案中執行 labrador watch
  2. 在WebStorm中編碼,儲存
  3. 切換到 微信web開發者工具 中除錯、預覽
  4. 再回到WebStorm中編碼
  5. ...

labrador 命令

labrador init 初始化專案命令

注意此命令會初始化當前的目錄為專案目錄。

labrador build 構建當前專案

  Usage: labrador build [options]

  Options:

    -h, --help     output usage information
    -V, --version  output the version number
    -c, --catch    在載入時自動catch所有JS指令碼的錯誤
    -t, --test     執行測試指令碼
    -d, --debug    DEBUG模式
    -m, --minify   壓縮程式碼
    -f, --force    強制構建,不使用快取

labrador watch 監測檔案變化

  Usage: labrador watch [options]

  Options:

    -h, --help     output usage information
    -V, --version  output the version number
    -c, --catch    在載入時自動catch所有JS指令碼的錯誤
    -t, --test     執行測試指令碼
    -d, --debug    DEBUG模式

labrador 庫

labrador 庫對全域性的 wx 變數進行了封裝,將大部分 wx 物件中的方法進行了Promise支援, 除了以 on* 開頭或以 *Sync 結尾的方法。在如下程式碼中使用 labrador 庫。

import wx from 'labrador';

console.log(wx.version);

wx.app;         // 和全域性的 getApp() 函式效果一樣,程式碼風格不建議粗暴地訪問全域性物件和方法
wx.Component;   // Labrador 自定義元件基類
wx.List;        // Labrador 自定義元件列表類
wx.PropTypes;   // Labrador 資料型別校驗器集合

wx.login;       // 封裝後的微信登入介面
wx.getStorage;  // 封裝後的讀取快取介面
//... 更多請參見 https://mp.weixin.qq.com/debug/wxadoc/dev/api/

我們建議不要再使用 wx.getStorageSync() 等同步阻塞方法,而在 async 函式中使用 await wx.getStorage()非同步非阻塞方法提高效能,除非遇到特殊情況。

app.js

src/app.js 示例程式碼如下:

import wx from 'labrador';
import {sleep} from './utils/util';

export default class {
  globalData = {
    userInfo: null
  };

  async onLaunch() {
    //呼叫API從本地快取中獲取資料
    let res = await wx.getStorage({ key: 'logs' });
    let logs = res.data || [];
    logs.unshift(Date.now());
    await wx.setStorage({ key: 'logs', data: logs });
    this.timer();
  }

  async timer() {
    while (true) {
      console.log('hello');
      await sleep(10000);
    }
  }

  async getUserInfo() {
    if (this.globalData.userInfo) {
      return this.globalData.userInfo;
    }
    await wx.login();
    let res = await wx.getUserInfo();
    this.globalData.userInfo = res.userInfo;
    return res.userInfo;
  }
}

程式碼中全部使用ES6/7標準語法。程式碼不必宣告 use strict ,因為在編譯時,所有程式碼都會強制使用嚴格模式。

程式碼中並未呼叫全域性的 App() 方法,而是使用 export 語法預設匯出了一個類,在編譯後,Labrador會自動增加App() 方法呼叫,所有請勿手動呼叫 App() 方法。這樣做是因為程式碼風格不建議粗暴地訪問全域性物件和方法。

自定義元件

Labrador的自定義元件,是基於微信小程式框架的元件之上,進一步自定義組合,擁有邏輯處理和樣式。這樣做的目的請參見 微信小程式開發三宗罪和解決方案

專案中通用自定義元件存放在 src/compontents 目錄,一個元件一般由三個檔案組成,*.js 、 *.xml 和*.less 分別對應微信小程式框架的 js 、 wxml 和 wxss 檔案。在Labardor專案原始碼中,我們特意採用了 xml和 less 字尾以示區別。如果元件包含單元測試,那麼在元件目錄下會存在一個 *.test.js 的測試指令碼檔案。

自定義元件示例

下面是一個簡單的自定義元件程式碼例項:

邏輯 src/compontents/title/title.js

import wx from 'labrador';
import randomColor  from '../../utils/random-color';

const { string } = wx.PropTypes;

export default class Title extends wx.Component {

  propTypes = {
    text: string
  };

  props = {
    text: ''
  };

  data = {
    text: '',
    color: randomColor()
  };

  onUpdate(props) {
    this.setData('text', props.text);
  }

  handleTap() {
    this.setData({
      color: randomColor()
    });
  }
}

自定義元件的邏輯程式碼和微信框架中的page很相似,最大的區別是在js邏輯程式碼中,沒有呼叫全域性的 Page() 函式宣告頁面,而是用 export 語法匯出了一個預設的類,這個類必須繼承於 labrador.Component 元件基類。

相對於微信框架中的page,Labrador自定義元件擴充套件了 propTypes 、 props 、 children 選項及 onUpdate 生命週期函式。children 選項代表當前元件中的子元件集合,此選項將在下文中敘述。

Labrador的目標是構建一個可以重用、巢狀的自定義元件方案,在現實情況中,當多個元件互相巢狀組合,就一定會遇到父子元件件的資料和訊息傳遞。因為所有的元件都實現了 setData 方法,所以我們可以使用this.children.foobar.setData(data) 或 this.parent.setData(data) 這樣的程式碼呼叫來解決父子元件間的資料傳遞問題,但是,如果專案中出現大量這樣的程式碼,那麼資料流將變得非常混亂。

我們借鑑了 React.js 的思想,為元件增加了 props 機制。子元件通過 this.props 得到父元件給自己傳達的引數資料。父元件怎樣將資料傳遞給子元件,我們下文中敘述。

onUpdate 生命週期函式是當元件的 props 發生變化後被呼叫,類似React.js中的 componentWillReceiveProps所以我們可以在此函式體內監測 props 的變化。

元件定義時的 propTypes 選項是對當前元件的props引數資料型別的定義。 props 選項代表的是當前元件預設的各項引數值。propTypes 、 props 選項都可以省略,但是強烈建議定義 propTypes,因為這樣可以使得程式碼更清晰易懂,另外還可以通過Labrador自動檢測props值型別,以減少BUG。為優化效能,只有在DEBUG模式下才會自動檢測props值型別。

編譯時加上 -d 引數時即可進入DEBUG模式,在程式碼中任何地方都可以使用魔術變數 __DEBUG__ 來判斷是否是DEBUG模式。

另外,Labrador自定義元件的 setData 方法,支援兩種傳參方式,第一種像微信框架一樣接受一個 object 型別的物件引數,第二種方式接受作為KV對的兩個引數,setData 方法將自動將其轉為 object

佈局 src/compontents/title/title.xml

<view class="text-view">
  <text class="title-text" catchtap="handleTap" style="color:{{color}};">{{text}}</text>
</view>

XML佈局檔案和微信WXML檔案語法完全一致,只是擴充了兩個自定義標籤 <component/> 和 <list/>,下文中詳細敘述。

樣式 src/compontents/title/title.less

.title-text {
  font-weight: bold;
  font-size: 2em;
}

雖然我們採用了LESS檔案,但是由於微信小程式框架的限制,不能使用LESS的層級選擇及巢狀語法。但是我們可以使用LESS的變數、mixin、函式等功能方便開發。

頁面

我們要求所有的頁面必須存放在 pages 目錄中,每個頁面的子目錄中的檔案格式和自定義元件一致,只是可以多出一個 *.json 配置檔案。

頁面示例

下面是預設首頁的示例程式碼:

邏輯 src/pages/index/index.js

import wx from 'labrador';
import List from '../../components/list/list';
import Title from '../../components/title/title';
import Counter from '../../components/counter/counter';

export default class Index extends wx.Component {
  data = {
    userInfo: {},
    mottoTitle: 'Hello World',
    count: 0
  };

  children = {
    list: new List(),
    motto: new Title({ text: '@mottoTitle', hello: '@mottoTitle' }),
    counter: new Counter({ count: '@count', onChange: '#handleCountChange' })
  };

  handleCountChange(count) {
    this.setData({ count });
  }

  //事件處理函式
  handleViewTap() {
    wx.navigateTo({
      url: '../logs/logs'
    });
  }

  async onLoad() {
    try {
      //呼叫應用例項的方法獲取全域性資料
      let userInfo = await wx.app.getUserInfo();
      //更新資料
      this.setData({ userInfo });
      this.update();
    } catch (error) {
      console.error(error.stack);
    }
  }

  onReady() {
    this.setData('mottoTitle', 'Labrador');
  }
}

頁面程式碼的格式和自定義元件的格式一模一樣,我們的思想是 頁面也是元件

js邏輯程式碼中同樣使用 export default 語句匯出了一個預設類,也不能手動呼叫 Page() 方法,因為在編譯後,pages 目錄下的所有js檔案全部會自動呼叫 Page() 方法宣告頁面。

我們看到元件類中,有一個物件屬性 children ,這個屬性定義了該元件依賴、包含的其他自定義元件,在上面的程式碼中頁面包含了三個自定義元件 list 、 title 和 counter ,這個三個自定義元件的 key 分別為 list 、motto 和 counter

自定義元件類在例項化時接受一個型別為 object 的引數,這個引數就是父元件要傳給子元件的props資料。一般情況下,父元件傳遞給子元件的props屬性在其生命週期中是不變的,這是因為JS的語法和小程式框架的限制,沒有React.js的JSX靈活。但是我們可以傳遞一個以 @ 開頭的屬性值,這樣我們就可以把子組建的 props 屬性值繫結到父元件的 data 上來,當父元件的 data 發生變化後,Labrador將自動更新子元件的 props。例如上邊程式碼中,將子元件 motto 的 text 屬性繫結到了 @mottoTitle。那麼在 onReady 方法中,將父元件的 mottoTitle 設定為 Labrador,那麼子元件 motto 的 text 屬性就會自動變為 Labrador。如果屬性值以 # 開頭,則將父元件的屬性(非data的屬性)直接繫結到子元件 props,如上邊程式碼中的 #handleCountChange,會將父元件的handleCountChange 方法繫結到子元件的 props.onChange 屬性,這樣子元件中可以通過呼叫this.props.onChange(newValue) 來通知父元件資料變化。

頁面也是元件,所有的元件都擁有一樣的生命週期函式onLoad, onReady, onShow, onHide, onUnload,onUpdate 以及setData函式。

componets 和 pages 兩個目錄的區別在於,componets 中存放的元件能夠被智慧載入,pages 目錄中的元件在編譯時自動加上 Page() 呼叫,所以,pages 目錄中的元件不能被其他元件呼叫,否則將出現多次呼叫Page()的錯誤。如果某個元件需要重用,請存放在 componets 目錄或打包成NPM包。

佈局 src/pages/index/index.xml

<view class="container">
  <view class="userinfo" catchtap="handleViewTap">
    <image class="userinfo-avatar" src="{{ userInfo.avatarUrl }}" background-size="cover"/>
    <text class="userinfo-nickname">{{ userInfo.nickName }}</text>
  </view>
  <view class="usermotto">
    <component key="motto" name="title"/>
  </view>
  <component key="list"/>
  <component key="counter"/>
</view>

XML佈局程式碼中,使用了Labrador提供的 <component/> 標籤,此標籤的作用是匯入一個自定義子元件的佈局檔案,標籤有兩個屬性,分別為 key (必選)和 name (可選,預設為key的值)。key 與js邏輯程式碼中的元件 key 對應,name 是元件的目錄名。key 用來繫結元件JS邏輯物件的 children 中對應的資料, name 用於在src/componets 和 node_modules 目錄中尋找子元件模板。

樣式 src/pages/index/index.less

@import 'list';
@import 'title';
@import 'counter';

.motto-title-text {
  font-size: 3em;
  padding-bottom: 1rem;
}

/* ... */

LESS樣式檔案中,我們使用了  語句載入所有子元件樣式,這裡的  語句按照LESS的語法,會首先尋找當前目錄 src/pages/index/ 中的 list.less 檔案,如果找不到就會按照Labrador的規則智慧地嘗試尋找 src/componets 和 node_modules 目錄中的元件樣式。

接下來,我們定義了 .motto-title-text 樣式,這樣做是因為 motto key 代表的title元件的模板中(src/compontents/title/title.xml)有一個view 屬於 title-text 類,編譯時,Labrador將自動為其增加一個字首 motto- ,所以編譯後這個view所屬的類為 title-text motto-title-text (可以檢視dist/pages/index/index.xml)。那麼我們就可以在父元件的樣式程式碼中使用 .motto-title-text 來重新定義子元件的樣式。

Labrador支援多層元件巢狀,在上述的例項中,index 包含子元件 list 和 titlelist 包含子元件 title,所以在最終顯示時,index 頁面上回顯示兩個 title 元件。

詳細程式碼請參閱 labrador init 命令生成的示例專案。

自定義元件列表

Labrador 0.5版本後支援迴圈呼叫自定義元件生成一個列表。

邏輯 src/components/list/list.js

import wx from 'labrador';
import Title from '../title/title';
import Item from '../item/item';
import { sleep } from '../../utils/util';

export default class List extends wx.Component {

  data = {
    items: [
      { title: 'Labrador' },
      { title: 'Alaska' }
    ]
  };

  children = {
    title: new Title({ text: 'The List Title' }),
    listItems: new wx.List(Item, 'items', {
      item: '>>',
      title: '>title',
      isNew: '>isNew',
      onChange: '#handleChange'
    })
  };

  async onLoad() {
    await sleep(1000);
    this.setData({
      items: [{ title: 'Collie', isNew: true }].concat(this.data.items)
    });
  }

  handleChange(component, title) {
    let item = this.data.items[component.key];
    item.title = title;
    this.setData('items', this.data.items);
  }
}

在上邊程式碼中的 children.listItems 子元件定義時,並沒有直接例項化子元件類,而是例項化了一個labrador.List 類,這個類是Labrador中專門用來管理元件列表。labrador.List 例項化時,接受三個引數:

第一個引數是列表中的自定義元件類,請將原始類傳入即可,不用例項化。

第二個引數是父元件上 data 屬性指向,指向的屬性必須是一個數組,例如上述程式碼中,第二個引數為 items ,則當前父元件的 data.items 屬性是一個數組,這個陣列又多少個元素,那麼子元件列表中就自動產生多少個子元件。子元件的數量跟隨 data.items 陣列動態變化,Labrador會自動例項化或銷燬相應的子元件。銷燬子元件時,子元件的 onUnload() 方法將會被呼叫。

第三個引數是子元件 props 資料繫結設定,如果屬性值以 > 開頭,則將 data.items 中對應元素的屬性繫結到子元件的 props。如果屬性值以 # 開頭,則將父元件的方法繫結到子元件的 props 中。注意,因為子元件是一個列表,所以為了區別,父元件對應的方法被呼叫時,第一個引數為子元件的例項,第二個及其之後的引數才是子元件中傳回的引數。如果屬性值是 >> 則將整個列表項資料繫結到對應的 props 上。

模板 src/components/list/list.xml

<view class="list">
  <component key="title" name="title"/>
  <list key="listItems" name="item"/>
</view>

在XML模板中,呼叫 <list/> 標籤即可自動渲染子元件列表。和 <component/> 標籤類似,<list/> 同樣也有兩個屬性,key 和 name。Labrador編譯後,會自動將 <list/> 標籤編譯成 wx:for 迴圈。

自動化測試

我們規定專案中所有後綴為 *.test.js 的檔案為測試指令碼檔案。每一個測試指令碼檔案對應一個待測試的JS模組檔案。例如 src/utils/util.js 和 src/utils/utils.test.js 。這樣,專案中所有模組和其測試檔案就全部存放在一起,方便查詢和模組劃分。這樣規劃主要是受到了GO語言的啟發,也符合微信小程式一貫的目錄結構風格。

在編譯時,加上 -t 引數即可自動呼叫測試指令碼完成專案測試,如果不加 -t 引數,則所有測試指令碼不會被編譯到dist 目錄,所以不必擔心專案會肥胖。

普通JS模組測試

測試指令碼中使用 export 語句匯出多個名稱以 test* 開頭的函式,這些函式在執行後會被逐個呼叫完成測試。如果test測試函式在執行時丟擲異常,則視為測試失敗,例如程式碼:

// src/util.js
// 普通專案模組檔案中的程式碼片段,匯出了一個通用的add函式
export function add(a, b) {
  return a + b;
}
// src/util.test.js
// 測試指令碼檔案程式碼片段

import assert from 'assert';

//測試 util.add() 函式
export function testAdd(exports) {
  assert(exports.add(1, 1) === 2);
}

程式碼中 testAdd 即為一個test測試函式,專門用來測試 add() 函式,在test函式執行時,會將目標模組作為引數傳進來,即會將 util.js 中的 exports 傳進來。

自定義元件測試

自定義元件的測試指令碼中可以匯出兩類測試函式。第三類和普通測試指令碼一樣,也為 test* 函式,但是引數不是exports 而是執行中的、例項化後的元件物件。那麼我們就可以在test函式中呼叫元件的方法或則訪問元件的props 和 data 屬性,來測試行為。另外,普通模組測試指令碼是啟動後就開始逐個執行 test* 函式,而元件測試指令碼是當元件 onReady 以後才會開始測試。

自定義元件的第二類測試函式是以 on* 開頭,和元件的生命週期函式名稱一模一樣,這一類測試函式不是等到元件onReady 以後開始執行,而是當元件生命週期函式執行時被觸發。函式接收兩個引數,第一個為元件的物件引用,第二個為run 函式。比如某個元件有一個 onLoad 測試函式,那麼當元件將要執行 onLoad 生命週期函式時,先觸發 onLoad 測試函式,在測試函式內部呼叫 run() 函式,繼續執行元件的生命週期函式,run() 函式返回的資料就是生命週期函式返回的資料,如果返回的是Promise,則代表生命週期函式是一個非同步函式,測試函式也可以寫為async 非同步函式,等待生命週期函式結束。這樣我們就可以獲取run()前後兩個狀態資料,最後對比,來測試生命週期函式的執行是否正確。

第三類測試函式與生命週期測試函式類似,是以 handle* 開頭,用以測試事件處理函式是否正確,是在對應事件發生時執行測試。例如:

// src/components/counter/counter.test.js

export function handleTap(c, run) {
  let num = c.data.num;
  run();
  let step = c.data.num - num;
  if (step !== 1) {
    throw new Error('計數器點選一次應該自增1,但是自增了' + step);
  }
}

生命週期測試函式和事件測試函式只會執行一次,自動化測試的結果將會輸出到Console控制檯。

專案配置檔案

labrador init 命令在初始化專案時,會在專案根目錄中建立一個 .labrador 專案配置檔案,如果你的專案是使用 labrador-cli 0.3 版本建立的,可以手動增加此檔案。

配置檔案為JSON格式,預設配置為:

{
  "npmMap":{
  },
  "uglify":{
    "mangle": [],
    "compress": {
      "warnings": false
    }
  },
  "classNames": {
    "for-test":true
  }
}

npmMap 屬性為NPM包對映設定,例如 {"underscore":"lodash"} 配置,如果你的原始碼中有require('underscore') 那麼編譯後將成為 require('lodash')。這樣做是為了解決小程式的環境限制導致一些NPM包無法使用的問題。比如我們的程式碼必須依賴於包A,A又依賴於B,如果B和小程式不相容,將導致A也無法使用。在這總情況下,我們可以Fork一份B,起名為C,將C中與小程式不相容的程式碼調整下,最後在專案配置檔案中將B對映為C,那麼在編譯後就會繞過B而載入C,從而解決這個問題。

uglify 屬性為 UglifyJs2 的壓縮配置,在編譯時附加 -m 引數即可對專案中的所有檔案進行壓縮處理。

classNames 屬性指定了不壓縮的WXSS類名,在壓縮模式下,預設會將所有WXSS類名壓縮為非常短的字串,並拋棄所有WXML頁面中未曾使用的樣式類,如果指定了該配置項,則指定的類不會被壓縮和拋棄。這個配置在動態類名的情況下非常實用,比如XML中class="text-{{color}}",在編譯LESS時,無法確定LESS中的.text-red類是否被用到,所以需要配置此項強制保留text-red類。

ChangeLog

2016-10-09

labrador 0.3.0

  • 重構自定義元件支援繫結子元件資料和事件

2016-10-12

labrador 0.4.0

  • 增加自定義元件props機制
  • 自動化測試
  • UglifyJS壓縮整合
  • NPM包對映
  • 增加.labrador專案配置檔案

2016-10-13

labrador 0.4.2

  • 修復元件setData方法優化效能產生的資料不同步問題
  • 在DEBUG模式下輸出除錯資訊

2016-10-16

labrador 0.5.0

  • 新增元件列表
  • 重構XML模板編譯器
  • 編譯時繫結事件改為事件發生時自動分派

貢獻者

第九程式

開源協議

本專案依據MIT開源協議釋出,允許任何組織和個人免費使用。