-
了解微信小程序是什么? 微信小程序官方文档
-
了解Javascript打包工具: webpack
-
了解ES6/7代码转译(transcompile)工具: Babel, 原理大致是借助语法分析工具(Esprima之类的), 将代码解析成抽象语法树, 再"重写"成最终的代码.
-
Javascript测试工具: jest, mocha等等, 请根据需要选择.
微信小程序目前版本的API实现需要兼顾方方面面, 所以仍然使用callback写法, 众所周知的Callback-Hell
是传统js语法上的历史问题, 但毕竟称手的工具是开发效率的源泉. 因此笔者对当前版本的微信小程序API做了简单的封装 weapp.
同时, 微信小程序框架本身专注于交互和UI的实现, 并未提供内置的状态管理, 如果众多的异步操作都直接在App或者Page中一一实现, 相信写起来会是一场噩梦, 而且不易于测试, 笔者又因此针对微信小程序实现了一个基于Redux方案的状态管理模块, 用以方便的在小程序中实现应用状态管理 redux-weapp.
特别地, 微信小程序构建(编译)时不支持从App scope之外require文件, npm在此就不好用了. 所以, 我们需要实时build依赖到应用本地, 在微信小程序中引用本地的modules, 对于这种构建场景, 笔者认为webpack算是最方便的方案. 大家都说COPY到本地是最最最方便的方式~~
开发者工具是用nwjs模拟的环境, 实际在微信中是JavascriptCore环境, 不过不用担心, 只是两个不同的vm, 本质是一样的.
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](http://redux.js.org/docs/advanced/AsyncActions.html)
.
安装开发小程序的辅助模块
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, 希望你会感到很舒服~~.
在这个范例中, 我们目标是去查询 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 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上.
最后, 谢谢您耐心阅读至此!
博主的范例整理好了么 😁