vue之微信公众号开发
最近使用vue开发了一个购物类的微信公众号项目,将遇到的问题及大致的流程都给记录一下。
使用的是vue+vuex+vue-router+axios,样式用的是YDUI。
项目是需要做一个扫码之后通过微信openid做用户标识,然后让用户能够购买商品的小型购物网站,开始是仿照的慕课上某老师的回龙观项目,后来就加入了许多东西。
正文之前写说句感慨,没有系统学习vue就直接做vue项目简直是一直折磨。还有就是基础知识学的不扎实导致遇到一些问题浪费大量时间也不一定能搞出来。
首先先来看vue-router文件:
import Vue from ‘vue‘ import Router from ‘vue-router‘ import goodsDetail from ‘@/components/goodsDetail‘ import goodsList from ‘@/components/goodsList‘ import Mine from ‘@/components/mine‘ import mainPage from ‘@/components/mainPage‘ import submitOrder from ‘@/components/submitOrder‘ import confirmbuy from ‘@/components/confirmbuy‘ import skip from ‘@/components/skip‘ import subuserinfo from ‘@/components/subuserinfo‘ Vue.use(Router) const router = new Router({ // mode: ‘history‘, routes: [ { path: ‘/‘, component: mainPage, name: ‘mainPage‘, children: [ { path: ‘/‘, component: goodsList, name: ‘goodsList‘ }, { path: ‘/Mine‘, component: Mine, name: ‘Mine‘ } ] }, { path: ‘/skip‘, component: skip, name: ‘skip‘ }, { path: ‘/goodsDetail‘, component: goodsDetail, name: ‘goodsDetail‘ }, { path: ‘/submitOrder‘, name: ‘submitOrder‘, component: submitOrder }, { path: ‘/confirmbuy‘, name: ‘confirmbuy‘, component: confirmbuy }, { path: ‘/subuserinfo‘, name: ‘subuserinfo‘, component: subuserinfo } ] }) export default router
贴vuerouter代码的目的主要是为了展示一下目录结构,这里面是点击了购物车组件之后跳转到提交界面,因为我最开始做的时候,把订单提交组件放到了购物车组件的子组件中,导致了样式发生了很low的错误,需要通过z-index去设置组件的显示级别,后来就都弄成兄弟组件就好了。
mainjs中主要是一些公共方法,下面是一段封装了axios请求,并能够排序进行md5校验的代码,
import axios from ‘axios‘ import querystring from ‘querystring‘ import crypto from ‘crypto‘ Vue.use(YDUI) // Vue.use(axios) Vue.config.productionTip = false Vue.prototype.$domain = ‘http://xianghuali.cn‘ //接口域名/ip Vue.prototype.$http = function (options) { var defaultParam = { url: ‘‘, method: ‘post‘, baseURL: `http://sbf.zzcyi.cn`, transformRequest: [function (data) { var q = querystring.stringify(data) return q }], // headers: {‘Content-Type‘: ‘application/x-www-form-urlencoded‘}, params: {}, data: {}, timeout: 10000, responseType: ‘json‘ } var ajaxParam = {} for (var k in defaultParam) { ajaxParam[k] = options[k] ? options[k] : defaultParam[k] } console.log(ajaxParam) if (ajaxParam.url === ‘‘) { alert(‘请传入请求URL‘) return null } return axios(ajaxParam) } Vue.prototype.$md5 = function (str) { var md5 = crypto.createHash(‘md5‘) md5.update(str) return md5.digest(‘hex‘) }
我使用vuex主要就是为了缓存一些全局参数,例如用户添加的商品数据等信息。其实使用vue做公众号并没有与做pc端有很大的区别,主要是有一些需要主要的地方。
首先是微信请求之后的回调路径,因为我没搞清楚本地调试ip+端口怎么通过微信验证,并且能够回跳回来,所以在开始做支付之后就写上了服务器地址,下面是微信获取用户openid的代码,
function GetOpenId (that, wxappid) {//wxappid是微信公众号后台的appid var code = getQueryString(‘code‘) var redirecturl = encodeURIComponent(`${that.$domain}/index.html`) //这里是回调地址,可以看到我在组件中调这个方法的时候,把vue的this对象传递了尽量,然后把vue原型链上挂载的地址显示出来 var imei = window.location.href.split(‘?‘)[1].split(‘#‘)[0].replace(‘imei=‘, ‘‘) if (!code && !window.sessionStorage.getItem(‘openidobj‘)) {//如果没有code,并且sessionstorage中没有用户的openid,就跳转到微信授权页,然后回跳回来走else if window.location.href = ‘http://open.weixin.qq.com/connect/oauth2/authorize?appid=‘ + wxappid + ‘&redirect_uri=‘ + redirecturl + ‘&response_type=code&scope=snsapi_userinfo&state=‘ + imei + ‘#wechat_redirect‘ } else if (code && !window.sessionStorage.getItem(‘openidobj‘)) {//请求接口获取openid that.$http({ url: `${that.$domain}/WxPay/QryWebAccessToken?code=${code}`, method: ‘get‘ }).then((res) => { if (res.data.openid) { window.sessionStorage.setItem(‘openidobj‘, res.data.openid) that.$http({ method: ‘post‘, url: `${that.$domain}/WxPay/GetWxUserInfo`, data: { openid: res.data.openid } }).then((res) => { if (res.data.status === ‘ok‘) { window.sessionStorage.setItem(‘wxuserinfo‘, res.data.memo) that.$router.replace(‘/‘) } else { that.$dialog.toast({mes: res.data.memo}) } }) } }) } else { that.$http({ method: ‘post‘, url: `${that.$domain}/WxPay/GetWxUserInfo`, data: { openid: window.sessionStorage.getItem(‘openidobj‘) } }).then((res) => { if (res.data.status === ‘ok‘) { window.sessionStorage.setItem(‘wxuserinfo‘, res.data.memo) that.$router.replace(‘/‘) } else { that.$dialog.toast({mes: res.data.memo}) } }) } }
这里牵扯到一个问题就是界面需要加载进来,然后跳转到微信,然后在跳回来,如果直接显示页面会让用户体验很不好,参考了网上的做法,做了一个等待页,专门用来显示给用户并获取用户信息的,获取信息之后在跳回来进行后面的操作。
其实我感觉vue与之前我们习惯的dom操作最大的区别就是,vue没有直接对父子兄弟元素进行操作的方法,而是通过传值,flag,通过函数改变变量来间接的操作目标元素。开始不习惯的时候感觉做起来很吃力,写多了之后也就习惯了,只要能记得变量要及时清除重新赋值即可。项目中使用的微信支付,主要是掉后台接口,没有什么要记录的。下面说一下socket以及百度地图,
首先是socket,因为需要通过socket与设备通讯,所以在界面一进来就要建立一个长连接,参考上面vue-router中的路由配置,这里就是在用户逗留时间最多的商品列表组件goodsList的父组件mainPage中创建的,mainPage.vue代码如下
<template> <div> <v-header></v-header> <div class="tab"> <div class="tab-item"> <router-link to="/" replace>商品</router-link> </div> <div class="tab-item"> <router-link to="/Mine" replace>订单</router-link> </div> </div> <keep-alive> <router-view /> </keep-alive> </div> </template> <script> import header from ‘./header‘ import ‘../../static/boundleProto‘ // import ‘../../static/CySocket‘ import {GetOpenId} from ‘../../static/common‘ import {CySocket} from ‘../../static/CySocket‘ import {GetSignature} from ‘../../static/common‘ import Vue from ‘vue‘ export default { name: ‘mainPage‘, components: { ‘v-header‘: header }, created () { // var imei=‘ZZCY5ccf7fdf754c‘; this.$http({ url: `${this.$domain}/Home/QryConfig`, method: ‘get‘ }).then((res) => { // GetOpenId(this,res.data.WxAppId) this.formatdate() var that=this; this.$store.state.refundTimeRange=res.data.refundTimeRange if(!window.sessionStorage.getItem(‘wxuserinfo‘)){ var t=setInterval(function(){ if(window.sessionStorage.getItem(‘wxuserinfo‘)){ that.creatsocket(res.data.ServerIpEp); clearInterval(t) } },10) }else{ that.creatsocket(res.data.ServerIpEp); } }) }, beforeRouteEnter (to, from, next){ next(vm=> { if(!window.sessionStorage.getItem("wxuserinfo")){ next("/skip"); } }) }, methods: { //实例化socket creatsocket(serverIp){ var wxuserinfo=JSON.parse(window.sessionStorage.getItem(‘wxuserinfo‘)).LoginNo if(!wxuserinfo){ this.$dialog.toast({mes: ‘获取参数错误‘}); }; var that=this Vue.prototype.cySocket = new CySocket({ server:`ws://${serverIp}`, autoReconnect: true, openSocket: function() { //连接socket后立即发送登录包 var user = new window.proto.SocketCmd.CmdHeader(); user.setCmdcode("0202"); //命令号 user.setIdentity(wxuserinfo); //客户端标识 user.setTimetoken(new Date().format("yyyy-MM-dd hh:mm:ss")); //命令时间 this.sendMsg(user.serializeBinary()); var imei=window.sessionStorage.getItem(‘imei‘); //查询设备状态 if(!imei||!wxuserinfo){ that.$dialog.toast({mes: ‘出现异常···请重新扫码‘}); }else{ var qrydevice = new window.proto.SocketCmd.CmdHeader(); qrydevice.setCmdcode("0203"); qrydevice.setIdentity(wxuserinfo); qrydevice.setOppositeid(imei); qrydevice.setTimetoken(new Date().format("yyyy-MM-dd hh:mm:ss.S")); //命令时间 this.sendMsg(qrydevice.serializeBinary()); var time_=0; var t=setInterval(function(){ time_+=1; if(that.$store.state.socketbacksocket){ clearInterval(t) }else if(time_>=1000){ clearInterval(t) } },10) } }, receiveMsg: function(data) { var commBk = window.proto.SocketCmd.ComCmdBkPb.deserializeBinary(data); var msg = ""; switch (commBk.getCmdcode()) { case "0202": //客户端登录 msg = "登录返回:" + commBk.toString(); break; case "0103": msg = "状态查询返回:" + commBk.toString(); if(commBk){ that.$store.state.socketbacksocket=true } break; case "0104": //控制返回消息 var lockBk = window.proto.SocketCmd.OpenLockBkPb.deserializeBinary(data); msg = "控制开锁返回:" + lockBk.toString(); that.$store.state.deviceState = true if(that.$store.state.opendoorfrom == ‘mineorder‘){ that.$http({ method: ‘post‘, url: `${that.$domain}/WxPay/TxUpdateOrderState`, data: { orderId: that.$store.state.orderId, state: ‘SUCCESSU‘ } }).then((res) => { }) } that.$dialog.toast({mes: ‘开锁成功‘}); break; case "0105": msg = "补货开大门返回:" + commBk.toString(); break; } } }); Vue.prototype.cySocket.init(); },
//格式化时间 formatdate(){ Date.prototype.format = Date.prototype.format||function (fmt) { var o = { "M+": this.getMonth() + 1, //月份 "d+": this.getDate(), //日 "h+": this.getHours(), //小时 "m+": this.getMinutes(), //分 "s+": this.getSeconds(), //秒 "q+": Math.floor((this.getMonth() + 3) / 3), //季度 "S": this.getMilliseconds() //毫秒 }; if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length)); for (var k in o) if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length))); return fmt; } } } } </script> <style> @import "../common/index.css"; .tab{ display:flex; width:100%; height:40px; line-height:40px; border-bottom: 1px solid rgba(7,17,27,0.1); } .tab .tab-item{ flex:1; text-align:center; } .tab .tab-item a{ display:block; font-size:14px; color:rgb(77,85,93); } .tab .tab-item a:hover , .tab .tab-item a.router-link-exact-active{ font-size:14px; color:#f01414; } </style>
其中socket这部分因为要传bype,而之前做的项目已经被bype传伤了,所以后台大佬用google巨佬们开源的protobuffer封装了前后台及硬件c的protobuf代码,不用拼bype可以直接传~~~~,因为使用了vue的keep-alive组件,所以组件可以缓存,socket在切换了之后也不会断,暂时没有研究不使用keep-alive组件,切换兄弟组件之后上个组件注销会不会导致socket断开。其他子组件发送socket只需要将数据塞到挂载在vue原型链上的cysocket对象就行了
opendoor(){ var imei=window.sessionStorage.getItem(‘imei‘); var wxuserinfo=JSON.parse(window.sessionStorage.getItem(‘wxuserinfo‘)).LoginNo var boxInfo = new window.proto.SocketCmd.OpenLockPb(); boxInfo.setCmdcode("");//从这里开始 boxInfo.setIdentity(wxuserinfo); boxInfo.setOppositeid(imei); boxInfo.setOrderid(this.orderId); boxInfo.setLockstatus(this.lockstate); boxInfo.setTimetoken(new Date().format("yyyy-MM-dd hh:mm:ss.S"));//命令时间 //到这里结束,都是自己定义的参数,因为是protobuffer封装过的,所以可以直接传了··· this.cySocket.sendMsg(boxInfo.serializeBinary()); this.$store.state.opendoorfrom=‘suborder‘ },
其实这段socket代码并没有前端太多事,编译ptobuf我也不懂,以后有时间,学习了后端之后再去研究一下,js操作二进制数据可以参考我的另一篇博客
下面是微信jsapi获取用户坐标并在百度地图中显示出来,
首先是算签名的公共方法,并传进去callback获取用户坐标
function GetSignature (that, callback) { that.$http({ url: `${that.$domain}/WxPay/QryWxSignature?url=${window.location.href.split(‘#‘)[0]}`, method: ‘get‘ }).then((data) => { AppId = data.data.appId Timestamp = data.data.timestamp Signature = data.data.signature Noncestr = data.data.nonceStr wx.config({ beta: true, debug: false, appId: AppId, timestamp: Timestamp, nonceStr: Noncestr, signature: Signature, jsApiList: [ ‘checkJsApi‘, ‘onMenuShareTimeline‘, ‘onMenuShareAppMessage‘, ‘onMenuShareQQ‘, ‘onMenuShareWeibo‘, ‘hideMenuItems‘, ‘showMenuItems‘, ‘hideAllNonBaseMenuItem‘, ‘showAllNonBaseMenuItem‘, ‘translateVoice‘, ‘startRecord‘, ‘stopRecord‘, ‘onRecordEnd‘, ‘playVoice‘, ‘pauseVoice‘, ‘stopVoice‘, ‘uploadVoice‘, ‘downloadVoice‘, ‘chooseImage‘, ‘previewImage‘, ‘uploadImage‘, ‘downloadImage‘, ‘getNetworkType‘, ‘openLocation‘, ‘getLocation‘, ‘hideOptionMenu‘, ‘showOptionMenu‘, ‘closeWindow‘, ‘scanQRCode‘, ‘chooseWXPay‘, ‘openProductSpecificView‘, ‘addCard‘, ‘chooseCard‘, ‘openCard‘, ‘openWXDeviceLib‘, ‘closeWXDeviceLib‘, ‘configWXDeviceWiFi‘, ‘getWXDeviceInfos‘, ‘sendDataToWXDevice‘, ‘startScanWXDevice‘, ‘stopScanWXDevice‘, ‘connectWXDevice‘, ‘disconnectWXDevice‘, ‘getWXDeviceTicket‘, ‘WeixinJSBridgeReady‘, ‘onWXDeviceBindStateChange‘, ‘onWXDeviceStateChange‘, ‘onScanWXDeviceResult‘, ‘onReceiveDataFromWXDevice‘, ‘onWXDeviceBluetoothStateChange‘ ] }) wx.ready(function (res) { // alert(‘调用微信jsapi返回的状态1:‘ + JSON.stringify(res)) if (callback) callback() wx.error(function (res) { // alert(‘调用微信jsapi返回的状态2:‘ + JSON.stringify(res)) }) }) }) }
然后是在地图组件map中调用该方法,并在获取到坐标之后实例化地图
<template> <div id="mymap"> </div> </template> <script> import {GetSignature} from ‘../../static/common‘ import wx from ‘weixin-js-sdk‘ import Vue from ‘vue‘ export default{ name:‘maps‘, data() { return{ lng_:null, lat_:null } }, created () { // GetSignature(this) }, mounted() { this.$nextTick(function () { var that=this;
//校验签名并获取坐标 GetSignature(this,function(){ wx.getLocation({ type: ‘gcj02‘, // 默认为wgs84的gps坐标,如果要返回直接给openLocation用的火星坐标,可传入‘gcj02‘ success: function (res) { var latitude = res.latitude // 纬度,浮点数,范围为90 ~ -90 var longitude = res.longitude // 经度,浮点数,范围为180 ~ -180。 var lng = longitude var lat = latitude var xpi = 3.14159265358979324 * 3000.0 / 180.0 var x = lng var y = lat var z = Math.sqrt(x * x + y * y) + 0.00002 * Math.sin(y * xpi) var theta = Math.atan2(y, x) + 0.000003 * Math.cos(x * xpi) lng = z * Math.cos(theta) + 0.0065 lat = z * Math.sin(theta) + 0.006 that.$store.state.lng = lng that.$store.state.lat = lat var map = new BMap.Map("mymap",{enableMapClick:true}); // 初始化地图,设置中心点坐标和地图级别 var point; var marker; point=new BMap.Point(that.$store.state.lng,that.$store.state.lat) map.centerAndZoom(point, 16); var movemarkerIcon = new BMap.Icon("/reserve/static/img/moveicon.png", new BMap.Size(36,36),{ imageSize: new BMap.Size(36,36) }); var movemarker; map.enableDragging(); //地图拖拽 getneardevice() // 添加带有定位的导航控件 var navigationControl = new BMap.NavigationControl({ // 靠左上角位置 anchor: BMAP_ANCHOR_TOP_LEFT, // LARGE类型 type: BMAP_NAVIGATION_CONTROL_LARGE, // 启用显示定位 enableGeolocation: true }); map.addControl(navigationControl); //地图拖动事件 // map.addEventListener("dragend",getneardevice); // 添加定位控件 var geolocationControl = new BMap.GeolocationControl(); geolocationControl.addEventListener("locationSuccess", function(e){ // 定位成功事件 that.$store.state.lng = e.point.lng; that.$store.state.lat = e.point.lat; that.lng_ = e.point.lng that.lat_ = e.point.lat point=new BMap.Point(e.point.lng,e.point.lat) map.centerAndZoom(point, 16); getneardevice() }); geolocationControl.addEventListener("locationError",function(e){ // 定位失败事件 that.$dialog.toast({mes: e.message,timeout:500}); }); map.addControl(geolocationControl); // map.enableScrollWheelZoom(true); function attribute(){ var p = movemarker.getPosition(); //获取marker的位置 getneardevice(p.lng,p.lat); } //获取设备 function getneardevice(lng,lat){ map.clearOverlays(); that.$dialog.loading.open(‘正在加载点位数据‘); point=new BMap.Point(that.$store.state.lng,that.$store.state.lat) marker = new BMap.Marker(point);// 创建标注 if(lng&&lat){ that.lng_ = lng that.lat_ = lat movemarker = new BMap.Marker(new BMap.Point(lng,lat),{icon:movemarkerIcon});// 创建标注 }else{ that.lng_ = that.$store.state.lng that.lat_ = that.$store.state.lat movemarker = new BMap.Marker(point,{icon:movemarkerIcon});// 创建标注 } map.addOverlay(marker); // 将标注添加到地图中 map.addOverlay(movemarker); // 将可移动点 movemarker.enableDragging() movemarker.addEventListener("dragend",attribute); that.$http({ method: ‘get‘, url: `${that.$domain}/ProductModule/QryNearbyImeiInfo?lng=${that.lng_}&lat=${that.lat_}` }).then((res) => { that.$dialog.loading.close(); if(res.data){ var myIcon = new BMap.Icon("/reserve/static/img/mapicon.png", new BMap.Size(36,36),{ imageSize: new BMap.Size(36,36) }); for(var i=0;i<res.data.length;i++){ var point_=new BMap.Point(res.data[i].ImeiLongitude,res.data[i].ImeiLatitude) var marker_ = new BMap.Marker(point_,{icon:myIcon});// 创建标注 marker_.imei=res.data[i].ImeiMac map.addOverlay(marker_); //点击坐标点事件 marker_.addEventListener("click",function(e){ that.$store.state.imei=e.target.imei; window.sessionStorage.setItem(‘imei‘,e.target.imei) that.$http({ method: ‘get‘, url: `${that.$domain}/ApiDevice/QryImeiInfo?imeiMac=${e.target.imei}` }).then((res) => { if(res.data.data[0].ImeiStatusNo==‘On‘){ if(res.data.data[0].ImeiIsPreOrder==‘Y‘){ if(JSON.parse(window.sessionStorage.getItem(‘wxuserinfo‘)).UserInfo.IsApprove){ that.$router.push(‘/goodsList‘) }else{ that.$router.push(‘/statement‘) } }else{ that.$dialog.toast({mes: ‘该设备未开启预订功能‘,timeout:500}); } }else{ that.$dialog.toast({mes: ‘该设备已离线‘,timeout:500}); } }) }) } } }) } } }) }) }) }, methods: { getlng(){ return this.$store.state.lng }, getlat(){ return this.$store.state.lat },
//坐标转换 getTrasSite(longitude,latitude){ var lng = longitude var lat = latitude var xpi = 3.14159265358979324 * 3000.0 / 180.0 var x = lng var y = lat var z = Math.sqrt(x * x + y * y) + 0.00002 * Math.sin(y * xpi) var theta = Math.atan2(y, x) + 0.000003 * Math.cos(x * xpi) lng = z * Math.cos(theta) + 0.0065 lat = z * Math.sin(theta) + 0.006 // var x = lng - 0.0065, y = lat - 0.006; // var z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * xpi); // var theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * xpi); // lng = z * Math.cos(theta); // lat = z * Math.sin(theta); return{ lng:lng, lat:lat } } } } </script> <style lang="scss" scoped> #mymap{ width: 100vw; height: 100vh; } </style>
其中获取设备是类似摩拜的那种需求。
在手机端需要面对的一个问题就是,用户点击home键而不去点你设置好的按钮,那么路由就会非常乱,现在有几种处理方法,但是都不是很完美,
首先是vue-router的replace方法,这个方法与push方法的不同就在于push是向浏览器地址栈中一直添加,[a,b,c,d],这样子,也可能是[a,b,c,d,a,a,a,a]这样,而replace是将当前的路径替换,但是并不是消除,例如[a,b,c],下一个路由你用this.$router.replace(‘/d‘),路由就变成了[a,b,d],暂时没找到方法清除路径记录的,可能js中有这个方法,下去有时间再研究一下。
然后是vuerouter的导航守卫。分别使用beforeRouteEnter和beforeRouteLeave来监听,其中需要注意的就是beforeRouteEnter中没有this对象,因为这时候组件还没有开始创建,可以在回调方法中访问:
beforeRouteEnter (to, from, next) {
//两个方法都可以在这里进行判断,如果离开要去的路由或者路由来源不是自己想要的,就进行中断或者跳转 if(from.path == ‘‘||to.path == ‘‘) { next(vm=>{ //这里的vm就是this,在这里可以进行一些操作 }) } next() },
具体内容参考官方文档 https://router.vuejs.org/zh-cn/advanced/navigation-guards.html。
--END
整篇文章并没有太多高技术含量的内容,但是因为当初并没有系统学习vue,只是看了看官网文档,看了点视频就开始做项目了,导致很多东西都不懂,做项目很吃力。以后学习如果有时间还是要系统的学习。
再此做记录备忘,以及给和我一样遇到同样问题的人一些帮助