欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

为女朋友写一个小程序(四)— —前端小程序的设计与实现

程序员文章站 2024-03-12 16:59:38
...

为女朋友写一个小程序(一)— —目的与需求
为女朋友写一个小程序(二)— —数据库设计
为女朋友写一个小程序(三)— —基于springboot的服务器端接口设计与实现
为女朋友写一个小程序(四)— —前端小程序的设计与实现(本文)
为女朋友写一个小程序(五)— —如何用docker简化部署
为女朋友写一个小程序(六)— —结合docker实现devOps
为女朋友写一个小程序(七)— —优化引进redis(未编码,未写)
为女朋友写一个小程序(八)— —基于moongodb实现即时通讯(未编码,未写)

2018年后半年一直出差,几乎没时间书写博客,趁现在空档期,把
拖了许久的文章继续写完吧…还是要保持写文章的习惯呀,做过的项目很久没回顾回顾起来确实也需要一定时间…记忆力这东西

一、前端实现结果展示

(以首页,任务页,商城列表,兑换列表为例)
为女朋友写一个小程序(四)— —前端小程序的设计与实现为女朋友写一个小程序(四)— —前端小程序的设计与实现为女朋友写一个小程序(四)— —前端小程序的设计与实现为女朋友写一个小程序(四)— —前端小程序的设计与实现

二、技术选型方案

1、为什么选择微信小程序?

因为2018年开发的时候想到小程序是那个时候的风口,把玩一下小程序是一个技术人的乐趣。
对于这样一个简单的程序适合于寄生于一个平台,以平台为入口进行开发,可以节省许多其他不必要的环节(如使用原生需要考虑如何被下载,应用上下架的问题)。
小程序其实也是基于B/S结构,其开发的使用的自身的框架,但是其实说白了跟使用HTML+JS+CSS开发区别其实不是很大,因为之前在工作中有过前端开发的基础,尚于对前端框架的使用,熟悉JS,入门起来可以比较快速。
所以考虑、了解了几天之后决定使用小程序作为该程序的前端交互。

2、使用的技术栈是什么?

技术栈使用的是:wxss+weUI
使用wxss这是没办法,开发小程序是腾讯限死一定要使用这样的框架(不像我司、支持我司开发的框架,同时支持普通的HTML+CSS,原生等),开发起来具有一定的局限性,且要开发一定要先过一次小程序开发文档,需要消耗一定的时间,但是因为之前玩过VUE这样MVVC前端框架,一通百通,所以接触起来也不困难。
使用weUI是因为之前用VUE开发的时候已经有很多开源的UI框架,最初的版本也是自己用原生的wxss的组件去画,但是因为前端基础还是比较薄弱,所以找到了小程序的UI框架,weUI,其界面简洁,语法简单,真是居家旅行,外包必备的一个好框架。

3、根据技术栈如何进行技术储备?

3.1、认识前端开发
如果你对于前段开发还是不熟悉的话,那做起来肯定会比较吃力,博主提过,之前是玩过VUE开发,参考文章,所以具备一定的前端开发能力是必要的,最好是在玩过VUE等这样的MVVC框架之后,接触小程序就相当简单了,因为其思想都是差不多的,开发“全家桶”也是差不多,只是语法不同罢了。

3.2、认识微信小程序
对小程序的开发首先肯定要对小程序进行了解,了解的时候主要还是以官方文档为主,无论如何一定要过一遍官方文档,里面会提及许许多多的细节,是你在设计方案的时候想不到的,如:接服务器端时,服务器端一定是要使用https协议,且服务器端地址展示出来一定要是一个域名,否则无法使用其原生接口发起请求…

3.3、认识weUI
weUI是一个UI框架官方文档,UI框架的入手过官方文档帮助其实不大,像我就直接下demo,了解一下如何接入该框架,然后根据我设计的界面找到响应的组件,然后demo代码直接copy上,然后再进行调整,这样对一个小项目来说是最快的。

3.4、IDE选择
小程序IDE是我见过的最烂的IDE没有之一,除了一个好处就是支持预览与远程调试。但是对于经常使用webStom的开发者来说非常不习惯,快捷键极少,习惯难以切换,最开始一段时间开发起来是比较慢和吃力。

后面我直接用webStom打开整个工程,在webStom进行编码,然后在小程序IDE进行调试,效率提升了不止一倍。我建议大家也可以这样玩。

三、前端代码的实现

1、整体开发架构规划,模块划分

小程序开发时,项目标准结构腾讯已经帮我们规划好了,这边开发是根据一些对象功能定义不同而简单划分出各个模块。小程序项目整体架构如下图所示:
为女朋友写一个小程序(四)— —前端小程序的设计与实现
1.1、小程序全局对象
整个“小程序”在项目中就是一个全局对象,所有的逻辑定义都在该全局对象中的,这个大家可以细看官方文档。这边主要使用到全局对象中(app.js)的东西是globalData用来装一些全局使用的变量,还有启动时一些操作、如获取高度、宽度,自动登录等操作。关于app.json就不解析,这个是关于布局亦可细看官方文档。
1.2、页面展示与交互逻辑模块
为女朋友写一个小程序(四)— —前端小程序的设计与实现
页面展示、交互逻辑这块曾经重构过一次、最开始的版本是所有页面都在/page目录下,到后面页面层次一多起来,维护起来看起来非常复杂,所以下了决心重构了一次,使用目录的层级体现页面的层级。
页面展示、交互逻辑模块这一块就是小程序说的MVVC结构,中规中举,下面给出登录页面的代码,简单展示一下MVVC结构。
展示页面login.wxml

<view class='login-wrapper' style='height:{{viewHeight}}px;width:{{viewWidth}}px'>
  <view class="login-icon">
    <image class="login-img" src="../../static/images/icon-logo.png"></image>
  </view>
  <view class="login-from">
    <!--账号-->
    <view class="inputView">
      <image class="nameImage" src="../../static/images/icon-account.png"></image>
      <label class="loginLab">账号</label>
      <input class="inputText" value="{{account}}" placeholder="请输入账号" maxlength="11" bindinput="handleInputAccount" />
    </view>
    <view class="line"></view>
    <!--密码-->
    <view class="inputView">
      <image class="keyImage" src="../../static/images/icon-password.png"></image>
      <label class="loginLab">密码</label>
      <input class="inputText" password="true" value="{{password}}" maxlength="20" placeholder="请输入密码" bindinput="handleInputPassword" />
    </view>
    <!--按钮-->
    <view class="loginBtnView">
      <button type="primary"   bindtap="handleTapLogin">登录</button>
    </view>
  </view>
    <view class="weui-footer weui-footer_fixed-bottom">
            <view class="weui-footer__text">粤ICP备18035307号</view>
        </view>
</view>

类CSS的wxss,为wxml穿衣服login.wxss

/*登录图片*/
.login-wrapper {
  background-color: white
}
.login-icon {
  text-align: center;
  background-color: #fff
}

.login-img {
  width: 250px;
  height: 250px;
}

/*表单内容*/

.login-from {
  flex: auto;
}

.inputView {
  background-color: #fff;
  line-height: 44px;
}

/*输入框*/

.nameImage, .keyImage {
  margin-left: 22px;
  width: 14px;
  height: 14px;
}

.loginLab {
  margin: 15px 15px 15px 10px;
  color: #545454;
  font-size: 14px;
}

.inputText {
  flex: block;
  float: right;
  text-align: right;
  margin-right: 22px;
  margin-top: 11px;
  color: #ccc;
  font-size: 14px;
}

.line {
  width: 100%;
  height: 1px;
  background-color: #ccc;
  margin-top: 1px;
}

/*按钮*/

.loginBtnView {
  width: 100%;
  height: auto;
  background-color: #f2f2f2;
  margin-top: 0px;
  margin-bottom: 0px;
  padding-bottom: 0px;
}

.loginBtn {
  width: 80%;
  margin-top: 35px;
}

主要的交互逻辑,控制层login.js

// pages/login/login.js
let userLoginObj = require('../../request/user/login.js')
Page({

  /**
   * 页面的初始数据
   */
  data: {
    account:'',
    password:'',
    viewHeight: 0,
    viewWidth: 0,
    requestBuilder: {},
    userDao:{},
    router: {}
  },
  //登录控制
  handleTapLogin(){
    if (this.validate()) { //数据校验
      userLoginObj.data = { account: this.data.account,password:this.data.password}
      let that = this 
      wx.request(this.data.requestBuilder(userLoginObj,(res)=>{
        if(res.data.status){
          console.log('登录成功')
          //存储帐号与密码
          let security = { account: that.data.account, password: that.data.password}
          that.data.userDao.setSecurity(security)
          //存储用户信息
          that.data.userDao.setUser(res.data.data)
          console.log('页面跳转')
          that.data.router.toTapTargetTargetList()
          // that.data.router.toTapShopRewardList()
          // that.data.router.toTapShopExchangeDetailList()
          // that.data.router.toTapSupervisionRewardList()
          // that.data.router.toTapSupervisionRewardAdd()
          //that.data.router.toTapSupervisionTargetList()
        }else{//失败了
          wx.showToast({
            title: res.data.message,
            icon:'none'
          })
          return
        }
      }))
    }else{

    }
  },
  //form校验
  validate(){
    if(this.data.account==''){
      wx.showToast({
        title: '账户不能为空',
        icon: 'none'
      })
      return false
    }
     
    if(this.data.password==''){
      wx.showToast({
        title: '密码不能为空',
        icon: 'none'
      })
      return false
    }
     return true
  },
  handleInputAccount(even){
    this.setData({account:even.detail.value})
  },
  handleInputPassword(even){
    this.setData({password:even.detail.value})
  },
  onShow(){
    let account = this.data.userDao.getAccount()
    let password = this.data.userDao.getPassword()
    if (account != null && password != null) {
      this.setData({
        account:account,
        password:password
      })
      this.handleTapLogin()
    } 
  },
  onLoad(){
    let app = getApp()
    //定义高度与宽度
    this.setData({
      viewHeight: app.globalData.viewHeight,
      viewWidth: app.globalData.viewWidth,
      requestBuilder: app.globalData.requestBuilder,
      userDao: app.globalData.userDao,
      router: app.globalData.router
    })
  }
})

login.json没有对页面定义什么内容,故不做展示

1.3、请求、与服务器端交互模块
在上述的login.js中大家应该也看到与服务器端请求逻辑,这里不外乎也是使用wx.request(obj)进行请求,这里我用了类VUE axios的思想设计,把每个请求都定义成一个对象,称为请求对象。再由一个工厂类,对请求对象进行封装一层,成为wx.request(obj)要求的标准对象,这样设计的一个考虑,就是为了把每个不同请求进行解耦。
为女朋友写一个小程序(四)— —前端小程序的设计与实现

为了更好说明刚刚的设计理念,以登录接口为例进行代码展示,先来看看请求对象工厂类
requestObjBuilder.js

let config = require('../../config/config.js')
let userDao = require('../../store/user-dao.js')
let router = require('../../router/router.js')

module.exports=function(baseRequestObj,success,fail,complete){
  //复制一个传递进来的请求对象
  baseRequestObj = JSON.parse(JSON.stringify(baseRequestObj))
  //定义请求头
  baseRequestObj.header.KIKI_AUTH_TOKEN = userDao.getToken()
  //解耦域名基础路径
  baseRequestObj.url = config.BASE_SERVICE_PATH + baseRequestObj.url


//定义全局错误策略,与成功策略
  let baseSuccess = (res)=>{
    if (res.statusCode!=200){
      wx.showToast({
        title: '请求失败了:' + res.statusCode,
        icon: 'none',
        duration: 2000
      })
    }else{
      //res.data!=undefined 下载接口是没有data的
      if (res.data!=undefined && res.data.code == 401){//尚未登录
        router.toLogin()
      }else{
        if (typeof success === "function") {
          success(res)
        } 
      }
    }
  }

  baseRequestObj.success = baseSuccess
 


  if (typeof fail === "function"){
    baseRequestObj.fail = fail
  }else{
    let defaultfail = (err)=>{
      console.log(err)
      wx.showToast({
        title: '服务器挂了:' + err.errMsg,
        icon:'none',
        duration:2000
      })
    }
    baseRequestObj.fail = defaultfail
  }


  if (typeof complete === "function")
    baseRequestObj.complete = complete
  return baseRequestObj
}

再来看看设计的请求对象是怎样的
login.js

let requestObj = {//请求实体
  url: '/user/login',//请求地址
  data:null,//请求的参数
  header:{
    'content-type': 'application/json' // 默认值
  },//请求头
  method: "POST"//请求方法
}
module.exports = requestObj

把requestBuilder注入data中,当我需要对用户进行登录时,我可以使用以下方法进行登录,成功的解耦

 wx.request(this.data.requestBuilder(userLoginObj,(res)=>{
        if(res.data.status){
          console.log('登录成功')
          //存储帐号与密码
          let security = { account: that.data.account, password: that.data.password}
          that.data.userDao.setSecurity(security)
          //存储用户信息
          that.data.userDao.setUser(res.data.data)
          console.log('页面跳转')
          that.data.router.toTapTargetTargetList()
          // that.data.router.toTapShopRewardList()
          // that.data.router.toTapShopExchangeDetailList()
          // that.data.router.toTapSupervisionRewardList()
          // that.data.router.toTapSupervisionRewardAdd()
          //that.data.router.toTapSupervisionTargetList()
        }else{//失败了
          wx.showToast({
            title: res.data.message,
            icon:'none'
          })
          return
        }
      }))
    }else{

    }

1.4、页面之间的路由跳转模块
页面之间的路由跳转小程序是提供了标准的接口,参考导航,但是我感觉这个处理不是很优雅,因为这样需要在不同页面里面写入其他页面的地址,所以我干脆定义一个全局对象,使用方法进行跳转,见代码:
router.js

const loginPath = '/pages/login/login'
const tapTargetTargetList = '/pages/tap-target/target-list/target-list'
const tapShopRewardList = '/pages/tap-shop/reward-list/reward-list'
const tapTargetTargetDetail = '/pages/tap-target/target-detail/target-detail'
const tapSupervisionTargetDetail = '/pages/tap-supervision/target-detail/target-detail'
const tapTargetTargetComplete = '/pages/tap-target/target-complete/target-complete'
const tapSupervisionReviewedList = '/pages/tap-supervision/reviewed-list/reviewed-list'
const tapSupervisionTargetList = '/pages/tap-supervision/target-list/target-list'
const tapSupervisionRewardList = '/pages/tap-supervision/reward-list/reward-list'
const tapSupervisionRewardAdd = '/pages/tap-supervision/reward-add/reward-add'
const persionPath = '/pages/tap-persion/persion/persion'
const tapPersionReviewedList = '/pages/tap-persion/reviewed-list/reviewed-list'
const toTapPersionExchangeList = '/pages/tap-persion/exchange-list/exchange-list'
const tapSupervisionExchangeList = '/pages/tap-supervision/exchange-list/exchange-list'
const rewardPath = '/pages/reward/reward'
const tapShopExchangeDetail = '/pages/tap-shop/exchange-detail/exchange-detail'
const tapPersionExchangeDetail = '/pages/tap-shop/exchange-detail/exchange-detail'
let router = {
    toLogin() {
        wx.reLaunch({
            url: loginPath,
        })
    },
    toTapTargetTargetList() {
        wx.switchTab({
            url: tapTargetTargetList
        })
    },
    toTapShopRewardList() {
        wx.switchTab({
            url: tapShopRewardList
        })
    },
    toTapTargetTargetComplete(params) {
        console.log(params)
        let url = tapTargetTargetComplete;
        if (params instanceof Array) {
            if (params.length > 0) {
                url = url + '?'
                let key = null;
                let value = null;
                for ({key, value} of params) {
                    url = url + key + '=' + value + '&'
                }
                url.substring(0, url.length - 1)
            }
        }
        wx.navigateTo({
            url: url,
        })
    },
    toTapSupervisionReviewedList() {
        wx.navigateTo({
            url: tapSupervisionReviewedList,
        })
    },
    toTapShopExchangeDetail(params) {
        let url = tapShopExchangeDetail;
        if (params instanceof Array) {
            if (params.length > 0) {
                url = url + '?'
                let key = null;
                let value = null;
                for ({key, value} of params) {
                    url = url + key + '=' + value + '&'
                }
                url.substring(0, url.length - 1)
            }
        }
        wx.navigateTo({
            url: url,
        })
    },
    toTapPersionExchangeDetail(params) {
        let url = tapPersionExchangeDetail;
        if (params instanceof Array) {
            if (params.length > 0) {
                url = url + '?'
                let key = null;
                let value = null;
                for ({key, value} of params) {
                    url = url + key + '=' + value + '&'
                }
                url.substring(0, url.length - 1)
            }
        }
        wx.navigateTo({
            url: url,
        })
    },
    toTapSupervisionTargetList() {
        wx.navigateTo({
            url: tapSupervisionTargetList,
        })
    },
    toTapSupervisionRewardAdd() {
        wx.navigateTo({
            url: tapSupervisionRewardAdd,
        })
    },
    toTapTargetTargetDetail(params) {
        console.log(params)
        let url = tapTargetTargetDetail;
        if (params instanceof Array) {
            if (params.length > 0) {
                url = url + '?'
                let key = null;
                let value = null;
                for ({key, value} of params) {
                    url = url + key + '=' + value + '&'
                }
                url.substring(0, url.length - 1)
            }
        }
        wx.navigateTo({
            url: url,
        })
    },
    toTapSupervisionTargetDetail(params) {
        console.log(params)
        let url = tapSupervisionTargetDetail;
        if (params instanceof Array) {
            if (params.length > 0) {
                url = url + '?'
                let key = null;
                let value = null;
                for ({key, value} of params) {
                    url = url + key + '=' + value + '&'
                }
                url.substring(0, url.length - 1)
            }
        }
        wx.navigateTo({
            url: url,
        })
    },
    toTapPersionReviewedList() {
        wx.navigateTo({
            url: tapPersionReviewedList,
        })
    },
    toTapPersionExchangeList() {
        wx.navigateTo({
            url: toTapPersionExchangeList,
        })
    },
    toTapSupervisionExchangeList() {
        wx.navigateTo({
            url: tapSupervisionExchangeList,
        })
    },
    toTapSupervisionRewardList() {
        wx.navigateTo({
            url: tapSupervisionRewardList,
        })
    },
}

module.exports = router

其他页面需要跳转时,使用以下的方式

console.log('页面跳转')
that.data.router.toTapTargetTargetList()

1.5、其他模块
1.5.1、全局配置
主要定义了服务器的基础路径,没有其他的东西
1.5.2、页面之间的交互
这里是一个比较有趣的问题,就是比如你在添加任务的页面完成一个任务添加时,需要通知任务列表去刷新,拉取刚刚任务。小程序在页面切换的时候是不会主动刷新的,切过去是你上一次点击看到的内容,所以需要一个通知机制来做主动刷新操作,所以就设计了这个模块。
其主要也是通过globalData的字段来体现,当页面被调到栈顶的时候,主动监测一下是否需要刷新,要的话就先刷新数据再展示,否则还是展示之前内容。下面看看代码
targetTargetListInteractive.js

module.exports={
  isReload(){
    return getApp().globalData.isTargetTargetListReload;
  },
  setReload(){
    getApp().globalData.isTargetTargetListReload=true;
  },
  resetReload(){
    getApp().globalData.isTargetTargetListReload = false;
  },
  isPartRefresh(){
    return getApp().globalData.targetTargetListPartRefresh.length>0
  },
  setPartRefresh(target){
    getApp().globalData.targetTargetListPartRefresh.push(target)
  },
  resetPartRefresh(){
    let array = getApp().globalData.targetTargetListPartRefresh
    getApp().globalData.targetTargetListPartRefresh=[]
    return array
  }
}

看看任务列表页如果监听这个对象的机制
target-list.js

.....
onShow(){
        if (supervisionTargetListInteractive.isReload()) {//判断是否整个页面刷新
            //获取数据
            wx.pageScrollTo({
                scrollTop: 0,
                duration: 0
            })
            this.resetPage()
            this.loadTable()
            supervisionTargetListInteractive.resetReload()
        } else {//判断是否局部刷新
            if (supervisionTargetListInteractive.isPartRefresh()) {
                let newTargets = supervisionTargetListInteractive.resetPartRefresh()
                for (let target of newTargets) {
                    for (let i = 0; i < this.data.targets.length; i++) {
                        if (this.data.targets[i].id == target.id) {
                            this.setData({
                                ['targets[' + i + ']']: target
                            })
                        }
                    }
                }
            }
        }
    }
    ......

而在添加任务页面成功添加任务之后,需要调一下这个方法让任务列表刷新

 //让任务列表刷新
 targetListInteractive.setReload()

1.5.5、缓存模块
缓存模块主要是使用wx.setStorageSync()方法来进行本地缓存,主要是缓存用户数据。在开发的过程中,因为作者的手机网络比较慢,下载图片会很卡,每次读商城都要卡等一段时间,所以使用到了本地缓存,只要资源被下载过,就会缓存,不会二次下载。
store.js

let storeDownloadObj = require('../request/store/download-file')
let requestBuilder = require('../request/factory/requestObjBuilder')
let storeDao = {
    setStore(storeId,path){
        let stores = wx.getStorageSync('store')||{};
        stores[storeId]=path
        wx.setStorageSync('store', stores)
    },
    getStore(storeId){
        let stores = wx.getStorageSync('store')||{};
        return stores[storeId];
    },
    //下载图片资源并存储
    downloadPicture(storeId){
        let that = this
        let requetObj = JSON.parse(JSON.stringify(storeDownloadObj))
        requetObj.url = requetObj.url + "?storeId=" + storeId + ""
        wx.downloadFile(requestBuilder(requetObj, (res) => {
            console.log(res);
            console.log('下载资源:' + storeId + ' 成功,' + '临时目录:' + res.tempFilePath)
            //存储到本地
            wx.saveFile({
                tempFilePath: res.tempFilePath,
                success: function (res) {
                    console.log('存储到本地成功')
                    that.setStore(storeId, res.savedFilePath)
                }
            })

        }))
    }
}

module.exports = storeDao

1.5.6、工具
一些常用的工具对象,如时间转换等…

果然,唯有代码可以让我找回初心,那一个热爱编程的初生牛犊不怕虎的想进BAT男孩,hhhhh可是现在不是很想了,三个小时的回顾,1w+字,如果对您有帮助,希望给我一分鼓励~愿你我皆不忘初心!
为女朋友写一个小程序(四)— —前端小程序的设计与实现