• 媒体品牌
    爱范儿
    关注明日产品的数字潮牌
    APPSO
    先进工具,先知先行,AIGC 的灵感指南
    董车会
    造车新时代,明日出行家
    玩物志
    探索城市新生活方式,做你的明日生活指南
  • 知晓云
  • 制糖工厂
    扫描小程序码,了解更多

开发 | 效率提升 100%,小程序开发应该这样做

小程序

2017-02-21 13:39

文 | xixilive

微信小程序的 API 实现需要兼顾方方面面,所以仍然使用 callback 写法。

众所周知,Callback-Hell(回调地狱)是传统 JS 语法上的历史问题。但毕竟称手的工具是开发效率的源泉,因此笔者对当前版本的微信小程序 API 做了简单的封装——weapp。

同时,微信小程序框架本身专注于交互和 UI 的实现,并未提供内置的状态管理。如果众多的异步操作都直接在 AppPage 中一一实现,相信开发起来会很困难,而且不易于测试。

因此,我又因此针对微信小程序实现了一个基于 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

往期精选文章

本文由知晓程序授权转载,关注微信号 zxcx0101,可获得以下内容和服务:

  • 在微信后台回复「1228」,获取全网第一本《微信小程序入门指南》。
  • 在微信后台回复「加群」,加入「一起发现小程序」微信交流群。
  • 在微信后台回复任意关键词,还能获得相关小程序推荐,赶紧试试吧!

zxcx_0208

登录,参与讨论前请先登录

评论在审核通过后将对所有人可见

正在加载中

小程序商店 minapp.com,一扫即用的小程序大全。微信公众号「知晓程序」,做中国最好的小程序报道。

本篇来自栏目

解锁订阅模式,获得更多专属优质内容