前言: 省略...css
瞭解微信小程序是什麼? 微信小程序官方文檔html
瞭解Javascript打包工具: webpackwebpack
瞭解ES6/7代碼轉譯(transcompile)工具: Babel, 原理大體是藉助語法分析工具(Esprima之類的), 將代碼解析成抽象語法樹, 再"重寫"成最終的代碼.git
Javascript測試工具: jest, mocha等等, 請根據須要選擇.es6
微信小程序目前版本的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, 寫代碼的時候注意一下就好.
下載 微信小程序開發者工具
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') } }) ] }
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
定義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.js
和 app.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上.
最後, 謝謝您耐心閱讀至此!