微信小程式模組化開發實踐

87654367發表於2017-01-03

前言: 省略…

準備

  • 瞭解微信小程式是什麼? 微信小程式官方文件

  • 瞭解應用狀態管理方案: Redux, 也是Flux架構的具體實現

  • 瞭解Javascript打包工具: webpack

  • 瞭解ES6/7程式碼轉譯(transcompile)工具: Babel, 原理大致是藉助語法分析工具(Esprima之類的), 將程式碼解析成抽象語法樹, 再”重寫”成最終的程式碼.

  • Javascript測試工具: jest, mocha等等, 請根據需要選擇.

TL;DR;

微信小程式目前版本的API實現需要兼顧方方面面, 所以仍然使用callback寫法, 眾所周知的Callback-Hell是傳統js語法上的歷史問題, 但畢竟稱手的工具是開發效率的源泉. 因此筆者對當前版本的微信小程式API做了簡單的封裝 weapp.

同時, 微信小程式框架本身專注於互動和UI的實現, 並未提供內建的狀態管理, 如果眾多的非同步操作都直接在App或者Page中一一實現, 相信寫起來會是一場噩夢, 而且不易於測試, 筆者又因此針對微信小程式實現了一個基於Redux方案的狀態管理模組, 用以方便的在小程式中實現應用狀態管理 redux-weapp.

特別地, 微信小程式構建(編譯)時不支援從App scope之外require檔案, npm在此就不好用了. 所以, 我們需要實時build依賴到應用本地, 在微信小程式中引用本地的modules, 對於這種構建場景, 筆者認為webpack算是最方便的方案. 大家都說COPY到本地是最最最方便的方式~~

安裝工具和依賴模組

下載微信小程式開發者工具

開發者工具是用nwjs模擬的環境, 實際在微信中是JavascriptCore環境, 不過不用擔心, 只是兩個不同的vm, 本質是一樣的.

nwjs可能存在一些小bug, 寫程式碼的時候注意一下就好.

下載 微信小程式開發者工具

用npm命令開始一個微信小程式專案

mkdir myapp
cd myapp
npm init

開始安裝必要的依賴模組

由於除了小程式執行時需要的模組, 還有構建所需要的模組, 看起來會比較多, 不過不用擔心, 大多數都是宣告性的, 不需要你直接呼叫.

為了方便經驗少些的同學理解, 我將這些依賴分步安裝.

程式碼轉譯工具, Babel

npm install --save-dev babel-cli babel-core babel-loader babel-plugin-add-module-exports babel-polyfill babel-preset-es2015 babel-preset-stage-0

有了上面這些模組, 就可以在構建時將ES6/7的程式碼轉譯為ES5的程式碼了(其實直譯器都只認ES5).

安裝打包工具, webpack

npm install webpack --save-dev

在此, 我們只需要對程式碼進行打包, 不需要dev server和hot module replace功能, 因此只需要安裝webpack module本身, 無需安裝其他擴充套件和外掛.

安裝Redux

npm install redux redux-thunk --save-dev

由於在實際應用中, 我們經常會需要非同步呼叫API伺服器的介面, 所以需要redux-thunk這個模組來處理 非同步action.

安裝開發小程式的輔助模組

npm install xixilive/weapp xixilive/redux-weapp --save-dev

其中, weapp模組是對微信小程式API的wrapper, 提供了更易於使用的API, redux-weapp是基於Redux對微信小程式進行狀態管理.

建立專案目錄結構如下

myapp
 |- es6                # 原始碼
   |- myapp.js         # 在app.js檔案中require此檔案
 |- lib                # 存放編譯之後的js檔案
 |- pages              # 小程式頁面定義
   |- projects
     |- projects.js
     |- projects.json
     |- projects.wxml
     |- projects.wxss
   ...
 |- app.js             # 小程式入口檔案
 |- app.json
 |- app.wxss
 |- webpack.config.js  # webpack配置檔案

編寫構建指令碼

首先得寫webpack.config.js, 這個是必須的, 由於這個構建是為了本地化微信小程式的依賴, 因此只處理js檔案, 若需要打包其他諸如css, image等資源, 請讀者自行研究. 實際上, 微信小程式包有1MB的上限.

// webpack.config.js

var path = require(`path`), webpack = require(`webpack`)

var jsLoader = {
  test: /.js$/, // 你也可以用.es6做副檔名, 然後在這裡定義相應的pattern
  loader: `babel`,
  query: {
    // 程式碼轉譯預設, 並不包含ES新特性的polyfill, polyfill需要在具體程式碼中顯示require
    presets: ["es2015", "stage-0"]
  },
  // 指定轉譯es6目錄下的程式碼
  include: path.join(__dirname, `es6`),
  // 指定不轉譯node_modules下的程式碼
  exclude: path.join(__dirname, `node_modules`)
}

module.exports = {
  // sourcemap 選項, 建議開發時包含sourcemap, production版本時去掉(節能減排)
  devtool: null,

  // 指定es6目錄為context目錄, 這樣在下面的entry, output部分就可以少些幾個`../`了
  context: path.join(__dirname, `es6`),

  // 定義要打包的檔案
  // 比如: `{entry: {out: [`./x`, `./y`,`./z`]}}` 的意思是: 將x,y,z等這些檔案打包成一個檔案,取名為: out
  // 具體請參看webpack文件
  entry: {
    myapp: `./myapp`
  },

  output: {
    // 將打包後的檔案輸出到lib目錄
    path: path.join(__dirname, `lib`),

    // 將打包後的檔案命名為 myapp, `[name]`可以理解為模板變數
    filename: `[name].js`,

    // module規範為 `umd`, 相容commonjs和amd, 具體請參看webpack文件
    libraryTarget: `umd`
  },

  module: {
    loaders: [jsLoader]
  },

  resolve: {
    extensions: [``, `.js`],
    // 將es6目錄指定為載入目錄, 這樣在require/import時就會自動在這個目錄下resolve檔案(可以省去不少../)
    modulesDirectories: [`es6`, `node_modules`]
  },

  plugins: [
    new webpack.NoErrorsPlugin(),

    // 通常會需要區分dev和production, 建議定義這個變數
    // 編譯後會在global中定義`process.env`這個Object
    new webpack.DefinePlugin({
      `process.env`: {
        `NODE_ENV`: JSON.stringify(`development`)
      }
    })
  ]
}

定義npm命令

  • test 筆者比較喜歡jest, 所以在此就用jest做範例了.

// package.json

"scripts": {
  "pretest": "eslint es6", //推薦進行靜態檢查
  "test": "jest",
  ...
},
...,
// jest允許在package.json中定義配置
"jest": {
  "automock": false,
  "bail": true,
  "transform": {
    ".js": "<rootDir>/node_modules/babel-jest" //用babel轉譯
  },
  "testPathDirs": [
    "<rootDir>/__tests__/"
  ],
  "testRegex": ".test.js$",
  "unmockedModulePathPatterns": [
    "/node_modules/"
  ],
  "testPathIgnorePatterns": [
    "/node_modules/"
  ]
}
  • build 這裡就是構建的命令了, 成敗在此一舉 ?

// package.json

"scripts": {
  ...,
  // 帶上watch選項, 實時編譯修改, 由於小程式開發工具也監視應用檔案的修改, 所以es6目錄下的js檔案修改, 將導致小程式開發工具自動重新載入
  "build": "webpack --watch --progress --colors --config webpack.config.js"
},

寫應用程式碼

總算進入正題了(工欲善其事,…), 藉助上述的 weapp 和 redux-weapp, 希望你會感到很舒服~~.

在這個範例(myapp)中, 我們目標是去查詢 github/octokit 的開源專案, 並顯示在小程式中.

建議不瞭解Redux的讀者先去快速瞭解一下(2 hours) Getting started with Redux – from egghead

myapp模組

  • 定義store: /es6/store.js

這裡只是簡單的範例, 實際中會有比較複雜的store shape, 需要引入更多的middleware來處理動作和狀態的變化.

// /es6/store.js

import {createStore, applyMiddleware, bindActionCreators} from `redux`
import thunk from `redux-thunk`
import reducers from `./reducers`

export default function(initState = {}){
  return createStore(
    reducers,
    initState,
    applyMiddleware(thunk)
  )
}
  • 定義reducers: /es6/reducers.js

Reducer就是處理因Store dispatch actions時發生的狀態變化的function, 引數總是為(state, action)

// /es6/reducers.js
import { combineReducers } from `redux`

// 處理projects邏輯
const projects = (state = [], action) => {
  switch (action.type) {
    case `PROJECTS_LOADED`:
      return state.concat[action.payload]
    //other cases
  }

  return state
}

// 將多個reducer合併起來
// 這裡就可以看出store的結構了, 是不是很 predictable ?
export default combineReducers({
  projects
})
  • 定義actions: /es6/actions.js

Action通常是個Plain Object, 總是被Store dispatch, 描述了”發生了什麼, 結果是什麼”的邏輯

// /es6/actions.js

import {weapp} from `weapp`

// 更好的方法是定義一個api module, 來處理網路請求
const http = weapp.Http(`https://api.github.com`)

// 這是一個非同步action, redux-thunk會處理返回值為Function的action(可以編入繞口令大全了~~)
export const loadProjects = (org) => {
  return (dispatch) => {
    http.get(`/orgs/${org}/repos`).then(response => {
      // 讓store去廣播`PROJECTS_LOADED`這件事情發生了
      dispatch({
        type: `PROJECTS_LOADED`,
        payload: response
      })
    })
  }
}
  • myapp模組入口: /es6/myapp.js

// /es6/myapp.js
import {bindActionCreators} from `redux`
import {weapp} from `weapp`
import connect from `redux-weapp`
import store from `./store`
import actions from `./actions`

export {
  weapp,
  connect,
  bindActionCreators,
  store,
  actions
}

小程式模組

  • 入口檔案: app.jsapp.json

// /app.js
App({
  // 方便起見, 這裡不做任何life-cycle處理
})

app.json

{
  "pages": [
    "pages/projects/projects"
  ],
  "window": {
    "navigationBarTitleText": "Orchid"
  },
  "networkTimeout": {
    "request": 10000,
    "downloadFile": 10000
  },
  "debug": true
}
  • 頁面邏輯: projects.js

如上定義, 小程式的啟動頁面是projects

// /pages/projects/projects.js

// 引入編譯過的modules
import {
  weapp,
  connect,
  bindActionCreators,
  store,
  actions
} from `../../lib/app`

// 標準Page定義Object
const config = {
  data: {
    projects: [] //for init-render
  },

  onReady(){
    // 哪裡來的 loadProjects? 往下看
    this.loadProjects(`octokit`)
  },

  onStateChange(nextState){
    this.setData({projects: nextState})
  }
}

// connect store with page
const page = connect.Page(
  store, // required
  // 這個頁面只關注projects變化
  (state) => ({projects: state.projects}),

  // 將Action定義與Store.dispatch binding在一起, 這樣就是一個可以發起對github API的請求了
  (dispatch) => {
    return {
      loadProjects: bindActionCreators(actions.loadProjects, dispatch)
    }
  }
)

// 啟動被connect過的頁面
Page(page(config))
  • 頁面UI: projects.wxml

<scroll-view wx:for="{{projects}}" wx:for-item="project" class="container">
  <view>{{project.name}}</view>
</scroll-view>

後記

範例程式碼未實際執行, 僅用以表示開發步驟, 我會盡快把這個範例實現完整, 放到github上.

最後, 謝謝您耐心閱讀至此!

參考

相關文章