为女朋友写一个小程序(一)— —目的与需求
为女朋友写一个小程序(二)— —数据库设计
为女朋友写一个小程序(三)— —基于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+字,如果对您有帮助,希望给我一分鼓励~愿你我皆不忘初心!