小程序从开发到部署(Mpvue + Vuex + MongoDB + Express)

随着小程序的持续发酵,用来开发小程序的框架也越来越多;从原生到WePY到Taro再到最近刚开源的Mpvue各有各的特点。 简单看下上面提到的三个框架:

  • WePY: 让小程序支持组件化开发的框架
    • 官方出品
    • 类Vue.js语法
  • Taro: 一套遵循React语法规范的多端开发解决方案
    • 凹凸实验室出品
    • React语法
    • 一套代码适配多端(小程序/H5/ReactNative...)
  • Mpvue: 是一个使用Vue.js开发小程序的前端框架
    • 美团出品
    • Vue.js语法

关于WePY和Mpvue更详细的对比可以看这里

这里看下使用Mpvue做一个简单的TODOLIST小程序;从服务器、域名、SSL证书开始到提交线上版本发布小程序,一步一步把过程记录下来。

目录

  • 微信公众平台
  • 小程序开发
    • mpvue
    • vuex
    • wecaht
    • request
  • 服务器
  • 域名
  • SSL证书
  • Nginx
  • api服务

微信公众平台

开发小程序的第一步,你需要拥有一个小程序帐号,通过这个帐号你就可以管理你的小程序。

1、到微信公众平台上注册小程序,获得小程序的AppID(小程序ID)、AppSecret(小程序密钥)等信息。详细步骤参考小程序开发文档|起步

2、有了小程序开发者账号之后,需要下载微信开发者工具

3、用微信开发者工具创建一个小程序项目,点击开发者工具的编译按钮即可以看到一个官方Demo啦。详细步骤参考我的第一个小程序

小程序开发

这里将采用Mpvue这个框架进行开发;结合Vuex进行状态管理,简单封装微信的内置api。

Mpvue

mpvue是一个使用Vue.js开发小程序的前端框架。框架基于Vue.js核心,mpvue修改了Vue.js的runtime和compiler实现,使其可以运行在小程序环境中,从而为小程序开发引入了整套Vue.js开发体验。

1、初始化项目

# 全局安装 vue-cli
$ npm install --global vue-cli

# 创建一个基于 mpvue-quickstart 模板的新项目: todolist
$ vue init mpvue/mpvue-quickstart todolist

# 安装依赖
$ cd todolist
$ npm install
# 启动构建
$ npm run dev

成功启动后,打开微信开发者工具,引入项目(项目目录)即可看到第一个Mpvue小程序啦。

Mpvue示例小程序: mpvue_demoe_index 下面这个Counter是Vuex的示例: mpvue_demoe_counter

Vuex

Vuex 是一个专为Vue.js应用程序开发的状态管理模式,它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

Vuex的作用就是用来管理不同的组件之间的状态数据。Vuex并不是必须使用的,如果应用足够简单的话可以直接使用Prop进行数据传递会更加方便快捷。关于该什么时候使用vuex,可以看看官方的解释

Vuex的核心概念有:State、Getter、Mutation、Action、Module。

1、State state里面存储了所有状态数据,不同的module之间可以通过rootState获取数据。

// todo/store.js
const state = {
  todoList: [],
  userInfo: {}
}

// done/store.js
const state = {
  doneList: []
}

const mutations = {  // mutation用来更新状态值
  setDoneList(state, { doneList }) {
    state.doneList = doneList
  }
}

const actions = {
  listTodo({ state, commit, rootState }) {
    const todoList = rootState.todo.todoList  // actions里面通过rootState即可访问到done/store.js的state了
    const doneList = todoList.filter(todo => todo.status === 2)
    commit('setDoneList', { doneList })  // 用commit来调用mutation里面的方法,进行更新状态
  }
}

done/store.js的actions里面通过rootState即可访问到所有的state了,如果要改变state里面的值,使用mutation进行操作。

2、Getter getter可以简单的理解为用来做中间过滤操作的。

// todo/store.js
const state = {
  todoList: [],
  userInfo: {}
}

const getters = {
  doneTodos: state => {
    return state.todoList.filter(todo => todo.status === 2)
  }
}

通过属性访问getter就可以调用了。

3、Mutation 更改Vuex的store中的状态的唯一方法是提交mutation。也就是说要更改状态,必须是通提交mutation,但是要注意Mutation不支持异步函数

// todo/store.js
const state = {
  todoList: [],
  userInfo: {}
}

const mutations = {
  setTodoList: (state, { todoList }) {
    state.todoList = todoList
    /*
    // 在异步函数的回调中更新状态,让状态变得不可追踪,不推荐!
    request.getTodoListAsync().then((todoList) => {
      statte.todoList = todoList
    })
    */
  }
}

因为Mutation不能是异步函数,所以我们可以简单的理解为Mutation就是用来更新数据状态;那要获得数据怎么办呢?接下来看看Action。

4、Action Action类似Mutation,区别在于:

  • Action不是直接改变状态,而是通过提交(commit)Mutation来改变状态
  • Action支持异步函数操作

看起来好像Action跟Mutation有点混乱的感觉,其实不然;简单的说:

  • mutation是更像状态(state)的唯一方式
  • mutation是不关心业务逻辑怎么处理的,只是关注状态(state)的变更
  • action的关注点在业务逻辑的处理,不关注状态(state)的变化
  • action可以同时分发(dispatch)多个mutation,更好地去实现业务逻辑

参考stackoverflow: vuex action vs mutation

// 还是这个例子
// done/store.js
const state = {
  doneList: []
}

const mutations = {  // mutation不支持异步函数,用来更新状态值
  setDoneList(state, { doneList }) {
    state.doneList = doneList
  }
}

const actions = { // action里面支持异步函数,进行逻辑处理
  listTodo({ state, commit, rootState }) {
    const todoList = rootState.todo.todoList
    const doneList = todoList.filter(todo => todo.status === 2)
    commit('setDoneList', { doneList })  // 用commit来调用mutation里面的方法,进行更新状态
  }
}

5、Module 模块化store,让每个模块都有自己的state/mutation/action/getter等,这样可以使得我们的应用更容易管理。上面提到的例子都是模块化后的代码。

// 官方示例
const moduleA = { // 模块A
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = { // 模块B
  state: { ... },
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({ // 统一初始化
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态

如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。

// 官方示例 改进版
// store/index.js
const moduleA = { // 模块A
  state: {
    name: ''
  },
  mutations: {
    setName (state, { name }) {
      state.name = name
    }
  },
  actions: {
    async getName: ({ state, commit, rootState }) {
      const orderId = rootState.orderId
      const name = await getNameByOrderIdAsync(orderId)
      commit('setName', { name })
    }
  },
  getters: { ... }
}

const moduleB = { // 模块B
  state: {
    orderId: ''
  },
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({ // 统一初始化
  modules: {
    a: {
      namespaced: true,
      ...moduleA
    },
    b: {
      namespaced: true,
      ...moduleB
    }
  }
})

store.state.a.name    // -> 模块A 状态里面的name
store.state.b.orderId // -> 模块B 状态里面的orderId

// pages/demo/index.vue
import { mapState, mapActions } from 'vuex'
import store from '@/store/index'
export default {
  store,
  computed: {
    ...mapState({
      name: state => state.a.name,
      orderId: state => state.b.orderId
    })
  },
  created () {
    this.getName().then(() => {
      console.log('I am Groot!')
    })
  },
  methods: {
    ...mapActions('a', [
      'getName'
    ])
  }
}

上面这个Vuex模块化例子包含了State、Action、Mutation、Namespaced、mapState、mapActions等常用方法;文章末尾也会提供示例项目源码,可以对比参照。

6、注意事项

  • 表单处理

    • 在严格模式中使用Vuex时,在属于Vuex的state上使用v-model指令会抛错,详细解决方案可以查看这里
  • 对象展开运算符: ...

    • 上面示例中常见的...是可以把目标对象展开到当前对象里面
    • ECMAScripe提案
    const a = { x: 1, y: 2 }
    const b = { z: 3 }
    
    const c = { ...a, ...b }
    // c = { x: 1, y: 2, z: 3 }
    
  • 对象风格参数

    • 参数以对象形式传递
    • 获取的时候也可以更简洁
let myName = ''
function setName ({ name }) { myName = name }
function start () { setName({ name: 'small_white' }) }

封装微信自带API

微信小程序提供了一些API用于获取用户信息等,这部分API我们会接触得较多,但是并不是Promise的API。所以我们可以简单的封装一下,使得更易用。

微信小程序API一般为这样:

wx.getUserInfo({
  success: (res) => {},
  fail: (failRes) => {}
})

简单封装成一个Promise函数:

export class wechat {
  static getUserInfo () {
    return new Promise((resolve, reject) => {
      wx.getUserInfo({ success: resolve, fail: reject })
    })
  }
}

封装HTTP请求

小程序要发起HTPP请求,可以通过自带的wx.request(),也可以使用第三方支持的HTPP请求包;这里使用了fly.js:一个支持所有JavaScript运行环境的基于Promise的、支持请求转发、强大的http请求库。

// request.js
export default async getTodoList () {
  const resp = await fly.get('https://www.yourdomain.com/api/todos')
  return resp.data
}

通过这样封装,可以把http请求尽量与业务代码分离,同时提高代码的复用性。

感谢您的耐心,看到了这里,是时候附上源码,动手实操一下啦:todoList


当你把程序写好了,在本地机器可以成功运行了,这时候可以想想如何放到服务器,让大家可以用。