http://www.ifanr.com/minapp/790017javascript
微信小程序的 API 實現須要兼顧方方面面,因此仍然使用 callback 寫法。java
衆所周知,Callback-Hell(回調地獄)是傳統 JS 語法上的歷史問題。但畢竟稱手的工具是開發效率的源泉,所以筆者對當前版本的微信小程序 API 作了簡單的封裝——weapp。node
同時,微信小程序框架自己專一於交互和 UI 的實現,並未提供內置的狀態管理。若是衆多的異步操做都直接在 App
或 Page
中一一實現,相信開發起來會很困難,並且不易於測試。webpack
所以,我又所以針對微信小程序實現了一個基於 Redux 方案的狀態管理模塊,用以方便的在小程序中實現應用狀態管理 redux-weapp。git
特別地,微信小程序構建(編譯)時不支持從 App scope 以外 require 文件,npm 在此就很差用了。es6
因此,咱們須要實時 build 依賴到應用本地,在微信小程序中引用本地的 modules。github
對於這種構建場景,我認爲 webpack 算是最方便的方案。web
開發者工具是用 NW.js 模擬的環境,在微信中,則是 JavascriptCore 環境。typescript
不過不用擔憂, 只是兩個不一樣的 VM,本質是同樣的。npm
NW.js 可能存在一些小 bug,寫代碼的時候注意一下就好。
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') } }) ] }
首先是代碼測試命令 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 的開源項目,並顯示在小程序中。
咱們首先定義 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/5bf1cde16f898faff2e652dbd08cf669weapp 項目地址:https://github.com/xixilive/weapp