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