http://www.ifanr.com/minapp/790017
微信小程序的 API 實現需要兼顧方方面面,所以仍然使用 callback 寫法。
眾所周知,Callback-Hell(回調地獄)是傳統 JS 語法上的歷史問題。但畢竟稱手的工具是開發效率的源泉,因此筆者對當前版本的微信小程序 API 做了簡單的封裝——weapp。
同時,微信小程序框架本身專注於交互和 UI 的實現,並未提供內置的狀態管理。如果眾多的異步操作都直接在 App
或 Page
中一一實現,相信開發起來會很困難,而且不易於測試。
因此,我又因此針對微信小程序實現了一個基於 Redux 方案的狀態管理模塊,用以方便的在小程序中實現應用狀態管理 redux-weapp。
特別地,微信小程序構建(編譯)時不支持從 App scope 之外 require 文件,npm 在此就不好用了。
所以,我們需要實時 build 依賴到應用本地,在微信小程序中引用本地的 modules。
對於這種構建場景,我認為 webpack 算是最方便的方案。
在開始之前,你需要准備
- 從官方文檔,了解微信小程序是什么;
- 了解 Redux 應用狀態管理方案,同時它也是 Flux 架構的具體實現;
- 了解 JavaScript 打包工具 webpack;
- 了解 ES6/7 代碼轉譯(transcompile)工具 Babel。原理是借助語法分析工具,將代碼解析成抽象語法樹后「重寫」成最終的代碼;
- 類似 Jest、Mocha 等 JavaScript 測試工具,可以根據需要選擇。
安裝工具和依賴模塊
下載微信小程序開發者工具
開發者工具是用 NW.js 模擬的環境,在微信中,則是 JavascriptCore 環境。
不過不用擔心, 只是兩個不同的 VM,本質是一樣的。
NW.js 可能存在一些小 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
這個模塊,來處理異步行為。
然后安裝開發小程序的輔助模塊:
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 文件。若需要打包其他資源,請讀者自行研究。
而且,值得注意的是,微信小程序包有 1 MB 的上限。
// 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": "/node_modules/babel-jest" //用babel轉譯 }, "testPathDirs": [ "/__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,希望你在開發小程序的時候,會感到很舒服。
在這個范例中,我們目標是去查詢 GitHub 和 Octokit 的開源項目,並顯示在小程序中。
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 在執行時,發生的狀態變化的函數,參數總是為 (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
,它通常是個 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.js
。
// /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 上。
最后,謝謝您耐心閱讀至此!
原文地址:https://gist.github.com/xixilive/5bf1cde16f898faff2e652dbd08cf669
weapp 項目地址:https://github.com/xixilive/weapp