微信小程序模塊化開發實踐

前言: 省略...css

準備

  • 瞭解微信小程序是什麼? 微信小程序官方文檔html

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

  • 瞭解Javascript打包工具: webpackwebpack

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

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

TL;DR;

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

同時, 微信小程序框架自己專一於交互和UI的實現, 並未提供內置的狀態管理, 若是衆多的異步操做都直接在App或者Page中一一實現, 相信寫起來會是一場噩夢, 並且不易於測試, 筆者又所以針對微信小程序實現了一個基於Redux方案的狀態管理模塊, 用以方便的在小程序中實現應用狀態管理 redux-weapp.web

特別地, 微信小程序構建(編譯)時不支持從App scope以外require文件, npm在此就很差用了. 因此, 咱們須要實時build依賴到應用本地, 在微信小程序中引用本地的modules, 對於這種構建場景, 筆者認爲webpack算是最方便的方案. 你們都說COPY到本地是最最最方便的方式~~shell

安裝工具和依賴模塊

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

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

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上.

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

參考

相關文章
相關標籤/搜索