实战|使用微信小程序+Vant-ui开发的共享项目

1,731 阅读13分钟

项目是干嘛的?


首先这是一个以小区为单位的业主与业主之间的自由交换物品的网站,共享者通过共享自己的物品来得到相应的7豆,通过7豆来在网站上兑换到生活用品. 项目中我实现了可以通过pc端和微信小程序访问同一个后台,实现了PC端的用户与小程序的用户可以相互互动,但由于这项目属于个人开发,加上腾讯那方面限制,导致pc端暂时无法实现用微信账号登陆。

另一篇文章,web前端部分的介绍: 7享网



技术栈


  • 微信小程序

  • vant-ui



项目架构

| app
| |-api // 存放各类请求接口
| |-common // 存放公共样式或者js文件或者文字
| |-components // 小程序组件
| |-image // 静态图片
| |-models //处理接收回来的数据
| |-pages  //小程序页面
| |-util   // 存放各类封装好工具
| app.js   
| app.json
| app.wxss
| config.js //存放设置

项目展示


图片


你也可以访问 pc端网址

路由


我的小程序是需要用户通过授权登陆之后才能正常运行的里面的功能项,但是小程序官方并没有提供像 vue-route那样的路由卫士,唯独提供了 wx.getSetting() 用来查看用户是否登陆。一开始我想在每个页面onShow()的时候用该方法判断用户是否已经登陆,但是一旦你的页面多了话,每次都需要导入这个方法,确实不太方便。

app.js 是调用 App 方法注册小程序唯一个实例,是全部页面共享的,开发者可以通过 getApp 方法获取到全局唯一的 App 实例,获取App上的数据或调用开发者注册在 App 上的函数。我也就可以通过在 app.js 封装一个路由监测的来实现全局使用。

  navigateTo(arg) {
    globalData.url = arg.url
    globalData.success = arg.success
  }

这是引用函数,而引用函数内部通过arg 传入你的路径以及回调函数,并统一放置在一个变量 globalData中,但是这个变量并不是响应式。通过大家熟知的方法Object.defineProperty() 来让变量变成响应式,以监测到传入的路径发生变化便做出相应的处理。为什么要用这个api而不用 proxy 了呢,由于 proxy 是属于ES6语言,在打包时,往往需要将ES6语言编译为ES5语言,从而使打包后的体积变大。为了优化小程序,在编写时尽量使用ES5语言,这也成了优化小程序的一个方法。

proxy () {
    Object.defineProperty(globalData, 'url', {
      get: function (val) {
        return val
      },
      set: function (newValue) {
        wx.getSetting({
          success: (res) => {
            if (res.authSetting['scope.userInfo']) {
              wx.navigateTo({
                url: newValue,
                success: globalData['success']
              })
            } else {
              wx.navigateTo({
                url: '/pages/login/login',
              })
            }
          }
        })
      }
    })
  },

我把获取用户权限放置在 set 里面,一旦监测到用户没有授权的话,就直接跳转到登陆页面那里。

const app = getApp()
 onWeather() {
    const that = this
    app.navigateTo({
      url: '/pages/weather/weather',
      success: (res) => {
        res.eventChannel.emit('sendTempFormMain', {
          data: that.data.temp
        })
      }
    })
  },

直接在想跳转路由的页面上调用该方法即可实现路由监控。

用户


在用户登陆的页面里,一旦用户授权后,通过wx.login 接口获取临时登录凭证code,发送至后台服务器以获取到用户的令牌并缓存起来。

onGotUserInfo() {
    wx.login({
      success: (res) => {
        if (res.code) {
          getToken(res.code).then((res) => {
            wx.setStorage({
              key: 'token',
              data: res.token
            })
          }).then(() => {
            wx.navigateBack()
          })
        }
      }
    })
  }

通过此 wx.getUserInfo 接口来获取到用户昵称和头像等信息,这里通过封装了一个异步函数来获取用户授权后的信息和获取不到用户一系类的动作提示。

function _getUserInfo () {
  return new Promise((resolve, reject) => {
    wx.getSetting({
      success: res => {
        if (res.authSetting['scope.userInfo']) {
          wx.getUserInfo({
            success: res => {
              resolve(res)
            }
          })
        } else {
          wx.showModal({
            title: '提示',
            content: '若不授权微信登录,则无法使用小程序。点击"头像"按钮并允许使用"用户信息"方可正常使用。',
            showCancel: false,
            confirmText: '知道了',
            success: (res => {})
          })
        }
      },
      fail: rej => {
        wx.showModal({
          title: '失败',
          content: '无法获取个人信息',
          showCancel: false,
          confirmText: '知道了',
          success: (res => {})
        })
      }
    })
  })
}

组件部分


在滚动部分我将小程序官方的 scroll-view 组件进行第二次封装,经过第二次的封装之后我可以从外面传入data、total、page 来判断是否更多加载以及自定义自己加载动画,而不用官网提供的,并且这组件成为我的小程序是使用到最多的组件。scroll组件

在购物页面那里,我封装第二个 shop-scroll组件。在 shop-scroll 中,我用了大量 wxs 来弥补 JavaScript 不能直接页面结构那里,官方宣称WXS(WeiXin Script)是小程序的一套脚本语言,结合 WXML,可以构建出页面的结构。WXS 与 JavaScript 是不同的语言,有自己的语法,并不和 JavaScript 一致。但是能使用的语法不够多,基本满足自己的需要,希望日后会增加多点吧。

<wxs module="normalTool">
	var tool = function(data, type) {
		var temp = []
		for (var i = 0; i < data.length; i++) {
			var d = data[i]
			if (d.type === type) {
				temp.push(d)
			}
		}
		return temp
	}
	var shopCartComputeNum = function(shopCart, id) {
		var num = 0
		for (var i = 0; i < shopCart.length; i++) {
			var currentCart = shopCart[i]
			if (id === currentCart.id) {
				num = currentCart.num
				break
			}
		}
		return num
	}
	var id = function(type) {
		switch (type) {
			case '蔬菜':
				return 'veg';
				break;
			case '水果':
				return 'fru';
				break;
		}
	}
	var type = ['蔬菜', '水果']
	module.exports = {
		tool: tool,
		shopCartComputeNum: shopCartComputeNum,
		type: type,
		id: id
	}
</wxs>

其实自己开发的组件远远不止这些,我只是挑出具有代表性的组件出来讲一下,其他组件都是简单类型的我这里就不进行细讲,感兴趣的话,可以去我的代码仓库进行查看。

数据请求


在数据请求和获取方面,我并没有使用大家都知道axios组件,转而使用官网提供的 wx.request(Object object)组件来发起 HTTPS 网络请求。这组件本身已经能满足日常使用,但不足的是每次写起来都要写一坨东西和没有异步,远远不能满足业务的需要,所以我就对其进行第二次封装,来让这个api更好使用。
http.js
我使用类方法来对这api进行封装,并使用ES6promise语言进行异步处理,根据业务情况我只封装了getpost这两个模块,并设置了防止重复提交的限制。

class HTTP {
  get({url}) {
    return new Promise((resolve, reject) => {
      this._request(resolve, reject, url, "GET")
    })
  }
  post({url,data}){
    return new Promise((resolve,reject)=>{
      this._request(resolve,reject,url,"POST",data)
    })
  }
  _request(resolve, reject, url, method, data = {}) {
    let user = wx.getStorageSync('token')
    // 防止重复提交
    if (method === 'POST') {
      wx.showLoading({
        title: '加载中',
        mask: true
      })
    }
    wx.request({
      url: config.api_base_url + url,
      method: method,
      data: data,
      header: {
        'content-type': 'application/json',
        'Authorization': 'Basic ' + Base64.encode(user + ':')
      },
      success: (res) => {
        if (res.data.error_code) {
          let error_code = res.data.error_code
          if (error_code === 1003) {
            setTimeout( ()=> {
              wx.navigateTo({
                url: '/pages/login/login',
              })
            }, 500)
          }
          reject(tips[error_code])
        } else {
          resolve(res.data)
        }
        if (method === 'POST') {
          wx.hideLoading()
        }
      },
      fail: (error) => {
        wx.showModal({
          title: '失败',
          content: '服务器有问题,请稍候再试',
          showCancel: false,
          success: (res => {
            reject(res)
          })
        })
      }
    })
  }
}

当数据请求服务器时所返回的错误代码,我把他放置在tips中,这样方便自己在日常中能随时添加的错误代码上去。

const tips = {
  1000: '验证失败',
  1001: '用户名已经被注册了',
  1002: '备注名已经被注册了',
  1003: 'token是非法的',
  1004: '该房号已经有人注册了',
  1005: '拒绝访问',
  1006: '没找到任何东西',
  1007: '亲,重复提交了',
  1008: '亲,发现有相同的物品'
}

数据处理

在数据处理方面,由于后台返回的数据不一定就符合前端要求,所以我都对那些数据会做一些优化,已达到前端的要求.通常我的做法就是写成一个类,在类的方法里面写成各种类的方法,然后数据直接在类里面完成转换,提高代码的复用率. 附上pending.js

当然这样也还是不够完美的,因为在通过后台返回的数据虽然经过各自class的处理后会漂亮些,但是这些class也只是处理单一一个,假如遇到多个了呢?我们每次就要像如下那样处理我从后台拿到的数据.

  _pending(page, type) {
    pending(page, type).then((res) => {
      let pending = this.data.pending.concat(this.normalPending(res.data))
      this.setData({
        select: type,
        pending,
        page: page,
        total: res.total,
        pages: res.pages
      })
    })
  },
  normalPending(data){
    let temp = []
    data.forEach((d)=>{
      temp.push(createPending(d))
    })
    return temp
  },

这看上去多麻烦,能不能让代码更加灵活和优美了呢?答案就是要对normalPending写一个封装函数,

export function normallArray (fn) {
  const wrapped = function (arg) {
    let temp = []
    arg.forEach((d) => {
      temp.push(fn(d))
    })
    return temp
  }
  return wrapped
}

normalArray 本身是一个函数,它接受 fn 作为函数传入,返回一个新的函数 wrapped当wrapped 执行的时候,通过传入的data,来自动执行wrapped函数

const normalPending = normallArray(createPending)
 
_pending(page, type) {
    pending(page, type).then((res) => {
      let pending = this.data.pending.concat(normalPending(res.data))
      this.setData({
        select: type,
        pending,
        page: page,
        total: res.total,
        pages: res.pages
      })
    })
  },

也就这样每次都减少代码重复使用的次数,来让自己写起来更轻松.

天气动画


Canvas 开发中,经常会提到粒子系统,使用粒子系统可以模拟出火、雾、云、雪、尘埃、烟气等抽象视觉效果,在我的小程序里,我使用粒子系统做了雨、雾效果,其他我利用画布来构成晴、星星、云等天气效果。

一开始,我先把画布放在所要显示的页面那里

<canvas type="2d" id="effect"></canvas>

然后,创建出画布的实例出来

const query = wx.createSelectorQuery()
query.select('#effect')
.fields({ node: true, size: true })
.exec((res)=>{
   const canvas = res[0].node
   const dpr = wx.getSystemInfoSync().pixelRatio
   canvas.width = res[0].width * dpr
   canvas.height = res[0].height * dpr
   const ctx = canvas.getContext('2d')
   ctx.scale(dpr, dpr)
   this.judge(ctx, this.data.air.weather)
})      

粒子系统由基类和子类组成。Particle 是基类,定义了子类统一的方法,如 run()、stop()等。基类负责整个粒子系统动画周期和流程的维护,子类负责具体实现的粒子效果,比如下雨有雾有太阳的效果是子类实现的,而下雨有阳光的开关和公共处理流程是基类控制的。

基类实现效果如下:

const STATUS_STOP = 'stop'
const STATUS_RUNNING = 'running'
const INTERVAL = true
class Particle{
  constructor(ctx,width,height,options){
    this._timer = null
    this.options = options || {}
    this.ctx = ctx
    this.width = width
    this.height = height
    this.status = STATUS_STOP
    // 是否执行动画,默认是执行
    this.interval = INTERVAL
    this.particles = []
    // 这是用作物品的移动,每次动画执行的步数
    this.step=0
    this._init()
  }
  _init(){
  // 实例化时第一执行的方法;空,由子类具体实现
  }
  _draw() {
  // 每个动效周期内画图用的方法;空,由子类具体实现
  }
  run(){
  // 设置定时器,定时执行 _draw(),实现动画周期
  }
  stop(){
  // 停止动画
  }
}

动画执行部分我是放在 run() ,在这里我设置可以是否执行动画效果开关,而且也设置检查是否是在执行过程中,以免动画多次执行。

run() {
   if (this.status !== STATUS_RUNNING) {
      if (this.interval === true) {
        this._timer = setInterval(() => {
          this._draw()
        }, 30)
      } else {
        this._draw()
      }
      this.status = STATUS_RUNNING
    }
    return this
  }

动画停止部分,这里我将上面提到的动画执行状态 status 设置为 false ,以便下次动画执行时再执行了

stop() {
    this.status = STATUS_STOP
    clearInterval(this._timer)
    return this
  }

下雨效果

首先,我将调用代码放在需要调用的页面上

rain(ctx,amount) {
    let {width, height} = this.data
    let rain = new Rain(ctx, width, height, {
      amount: amount,
      speedFactor: 0.03
    })
    rain.run()
},

而画布的大小是是直接通过小程序自带的api接口wx.getSystemInfo进行获取。

  wx.getSystemInfo({
      success: (res)=>{
        let width = res.windowWidth
        let height = res.windowHeight
        this.setData({
          width,
          height
        })
      }
  })

子类通过继承基类的属性后,将初始化放在 _init 中,根据雨的大小来设置粒子的数量,并把其形成的粒子放入this.particles 中。

class Rain extends Particle {
  // 初始化雨点
  _init(){
    let width = this.width
    let height = this.height
    // 雨点的数量
    let amount = this.options.amount || 100
    // 雨点的速度
    let speedFactor = this.options.speedFactor || 0.03
    let speed = height * speedFactor
    let ps = this.particles
    for (let i=0;i<amount;i++){
      let p = {
        x: width,
        y: height,
        l: 2*Math.random(),
        xs: -1,
        ys: 10*Math.random() + speed,
        color: 'rgba(255, 255, 255, 0.5)'
      }
      ps.push(p)
    }
    this.particles = ps
  }
  _draw(){
    let ps = this.particles
    let ctx = this.ctx
    // 首先清除画布上的内容
    ctx.clearRect(0, 0 , this.width,this.height)
    for(let i =0;i<ps.length;i++){
      let s = ps[i]
      ctx.beginPath()
      ctx.moveTo(s.x, s.y)
      ctx.lineTo(s.x+s.l*s.xs, s.y+s.l*s.ys)
      ctx.strokeStyle=s.color
      ctx.stroke()
    }
    return this._update()
  }
  _update() {
    let {width, height, particles} = this
    let ps = particles
    for (let i=0;i<ps.length;i++){
      let s = ps[i]
      s.x += s.xs
      s.y += s.ys
      if (s.x> width|| s.y>height){
        s.x = Math.random()*width
        s.y = -5
      }
    }
  }
}

其中:

  • x、y 代表单个粒子的位置,即雨滴开始绘图的位置

  • l 代表雨滴的长度

  • xs、ys 分别代表 x、y 方向上的加速度,即雨滴的下落速度和角度

  • _draw()的方法,是先将画布清空,然后遍历 this.particles 数组拿到单个雨滴并进行绘制,然后调用一个单独实现的 _update() 重新计算单个雨滴的位置,就这样雨滴效果就出来了。

白云效果

白云效果也是跟上面的雨滴效果写法都大致一样,首先我要设置出一朵白云的开始高度,并根据情况画出多朵白云。

class Cloud extends Particle{
  _init(){
    let width = this.width
    let height = this.height
    let cloudWidth = this.width*0.25
    // 云朵的数量
    let amount = this.options.amount || 1
    let ps = this.particles
    for (let i=0;i<amount;i++){
      let p = {
        x: Math.random()*width,
        y: height*Math.random()*0.5,
        cw: cloudWidth,
        // 云朵的高度
        ch: cloudWidth*0.6,
        // 开始
        sangle: 0,
        // 结束
        eangle: 2*Math.PI,
        color: 'rgba(255, 255, 255, 0.5)'
      }
      ps.push(p)
    }
    this.particles = ps
  }
}

一朵白云的高度出来了,我通过5个圆圈把一朵白云样子画出来,然后通过每次改变cx的值来让白云缓慢移动

_draw(){
    this.step+=Math.random()
    let ps = this.particles
    let ctx = this.ctx
    // 首先清除画布上的内容
    ctx.clearRect(0, 0 , this.width,this.height)
    for(let i =0;i<ps.length;i++){
      let s = ps[i]
      let cx = (s.x+this.step)%this.width
      ctx.beginPath()
      ctx.fill();
      ctx.arc(cx,s.y,s.cw*0.19,s.sangle,s.eangle)
      ctx.arc(cx + s.cw * 0.08, s.y - s.ch * 0.3, s.cw * 0.11, s.sangle, s.eangle);
      ctx.arc(cx + s.cw * 0.3, s.y - s.ch * 0.25, s.cw * 0.25, s.sangle, s.eangle);
      ctx.arc(cx + s.cw * 0.6, s.y, 80 * 0.21, s.sangle, s.eangle);
      ctx.arc(cx + s.cw * 0.3, s.y - s.ch * 0.1, s.cw * 0.28, s.sangle, s.eangle);
      ctx.closePath();
      ctx.fillStyle=s.color
      ctx.fill();
    }
  }

下雪效果

主要区别在于动画回收部分,我这里采用颜色渐变效果,以达到雪查不到底部一个消失的效果。

_update() {
    let {
      height,
      particles
    } = this
    let ps = particles
    for (let i = 0; i < ps.length; i++) {
      let p = ps[i]
      p.color = `rgba(255, 255, 255, ${1 - p.y/height})`
      p.y += p.ys
      if (p.y > height) {
        p.y = 0
        p.color = 'rgba(255, 255, 255, 1)'
      }
    }
  }

写到这里,我利用小程序的画布api实现了雨、云、雪、雾、晴朗、星星(流星)的天气效果,在这里我不一一展示了,感兴趣可以看我的代码仓库

幸运转盘

关于转盘部分我写了一篇小文章,大家直接点击链接查看 教程|在微信小程序实现一个幸运转盘小游戏

优化

数据缓存

在项目中我用到缓存有两个地方,一个是用来存放我的 token 和物品的目录。而这些东西只要在项目开始之初加载过一次就够了,减少后台服务器的压力。

App({
  onLaunch: function () {
    // 获取subs列表并放在缓存中
    getAllSubs()
  },
})

分包机制

而这里我采用小程序内置的分包机制,由于小程序的包体积大小限制已经提高到了2MB,但为了提高用户初次加载时的速度,增强用户的使用感受,于是我这里采用的小程序的分包机制。

  "pages": [
    "pages/main/main",
    "pages/my/my",
    "pages/drift/drift"
  ],
  "subpackages": [
    {
      "root": "mainpages",
      "name": "main",
      "pages": [
        "pages/help/help",
        "pages/hot/hot",
        "pages/shop/shop",
        "pages/askForHelp/askForHelp",
        "pages/use/use",
        "pages/weather/weather"
      ]
    },
    {
      "root": "mypages",
      "name": "my",
      "pages": [
        "pages/room/room",
        "pages/mobile/mobile",
        "pages/email/email",
        "pages/addGood/addGood",
        "pages/addWish/addWish",
        "pages/myWish/myWish",
        "pages/myGoods/myGoods"
      ]
    },
    {
      "root": "driftpages",
      "name": "drift",
      "pages": [
        "pages/comment/comment"
      ]
    }
  ],

经过上面的配置,pages 内的内容被打包成主包,而 subPackages 中的内容则被打包成子包。当小程序打开时,采用分包机制的小程序会先下载 主包 来展现首页内容,这样极大地提升了小程序的打开速度,并且我将那些业务性相关性强的,会分到同一个包那里。

部署

有部署经验的同学可以飘过这个章节。

当你完成所有开发时,你可以点击“真机调试”来进行真机模仿看下你编写的小程序是否有问题。

当你觉得测试没问题的时候,这时你可以点击编程软件最上面的 “工具” 找到里面的 “上传”,接着按照提示即可完成上传。

登陆 微信公众号 官网,登陆你注册小程序时的账号和密码,成功之后,在页面左边的目录里找到“管理”,再找到“版本管理”,在该页面的“开发版本”处,你就见到你刚刚上传的项目,接着点击右边“提交审核”,等待腾讯的审核。

腾讯审核完毕,会提示你审核通过,接着你再回到“版本管理”页面,发布你的小程序。

总结


这是我第一次写微信小程序,里面很多内置的api,鹅厂已经帮我弄好了,再配合UI框架,写起来更加游刃有余,我一开始有想过用 mpvue ,但是听身边朋友说不推介使用,难道原生api不香吗?

还是那句话,多看文档,要很仔细去看,因为他有些地方写的很隐蔽,一旦你看漏了,有些程序就不能正常运行,虽然有报错,但是有些报错你会看不明白的。

再次感谢大家能看到最后,小弟文笔实在太菜,写得不好的地方请多多包涵。最后分享下我的代码仓库,别忘了奉献你的小星星哦。 weapp

别忘了在另一篇web前端教程奉献出你的点赞 7享网