在做完了两个vue项目之后,我开始了小程序的学习,由于开学等因素影响进度一直都是断断续续的。最终在开学一周多的时间结束了uniapp项目的练习。于是我选择了黑马商场做为微信小程序uniapp的练手。
uniapp是一个基于vue.js开发的一个前端框架,可以发布各个平台,本项目是开发一个微信小程序,使用HbuilderX中uniapp的内置的uni.ui模块。使用了sass,练习了对微信小程序开发的一套相对完整的流程,还有用git提交代码到gitee。
小程序中一个不同于网页的地址在于tabBar栏,微信小程序中,tabBar栏可以通过配置生成,也方便管理。只要在根目录中的 pages.json
配置文件,新增 tabBar
的配置节点即可
{
"tabBar": {
"selectedColor": "#C00000",
"list": [
{
"pagePath": "pages/home/home",
"text": "首页",
"iconPath": "static/tab_icons/home.png",
"selectedIconPath": "static/tab_icons/home-active.png"
},
{
"pagePath": "pages/cate/cate",
"text": "分类",
"iconPath": "static/tab_icons/cate.png",
"selectedIconPath": "static/tab_icons/cate-active.png"
},
{
"pagePath": "pages/cart/cart",
"text": "购物车",
"iconPath": "static/tab_icons/cart.png",
"selectedIconPath": "static/tab_icons/cart-active.png"
},
{
"pagePath": "pages/my/my",
"text": "我的",
"iconPath": "static/tab_icons/my.png",
"selectedIconPath": "static/tab_icons/my-active.png"
}
]
}
}
之后再修改一个导航条的模式效果,也是在pages.json中,修改globalStyle节点
{
"globalStyle": {
"navigationBarTextStyle": "white",
"navigationBarTitleText": "黑马优购",
"navigationBarBackgroundColor": "#C00000",
"backgroundColor": "#FFFFFF"
}
}
然后就是配置网络请求了,因为小程序中是不支持axios的,wx.request()又功能简单,不能支持拦截器等,所以这个项目老师用了一个自己写的第三方包来发起网络请求的,官方文档如下:
@escook/request-miniprogram - npm
再main.js入口文件中配置,引入再绑定到uni顶级对象上,再加个请求和响应拦截器
import { $http } from '@escook/request-miniprogram'
uni.$http = $http
// 配置请求根路径
$http.baseUrl = 'https://www.uinav.com'
// 请求开始之前做一些事情
$http.beforeRequest = function (options) {
uni.showLoading({
title: '数据加载中...',
})
}
// 请求完成之后做一些事情
$http.afterRequest = function () {
uni.hideLoading()
}
发起请求,再动态把图片的src等属性渲染到页面上,用小程序自带的swiper标签。
考虑到首次启动的加载时间,可以采用分包,而且微信小程序也对分包的大小有检测,所以我们是把tabBar相关的页面(home,cate,cart,my)放在主包,再把其它页面放在分包里(goods_detail,goods_list,search)。配置分包如下:
1、先在根目录中,创建分包的根目录,命名subpkg
2、再在pages.json中,与pages平级配置subPackages节点
{
"pages": [
{
"path": "pages/home/home",
"style": {}
},
{
"path": "pages/cate/cate",
"style": {}
},
{
"path": "pages/cart/cart",
"style": {}
},
{
"path": "pages/my/my",
"style": {}
}
],
"subPackages": [
{
"root": "subpkg",
"pages": []
}
]
}
3、之后可在分包目录下,新建页面,并选择小程序分包,即可自动配置
使用原生uni.showToast({})来提示用户,要配置配置对象,所以封闭全局方法来简化之后的使用
// 封装的展示消息提示的方法
uni.$showMsg = function (title = '数据加载失败!', duration = 1500) {
uni.showToast({
title,
duration,
icon: 'none',
})
}
渲染然后再使用uni.switchTab()跳转页面
// nav-item 项被点击时候的事件处理函数
navClickHandler(item) {
// 判断点击的是哪个 nav
if (item.name === '分类') {
uni.switchTab({
url: '/pages/cate/cate'
})
}
}
xxx
xxx
xxx
xxx
xxx
多复制一些节点,演示纵向滚动效果...
zzz
zzz
zzz
zzz
多复制一些节点,演示纵向滚动效果
这里有一个bug,就是每点一次左边的一级分类,再滚动右侧的二三级分类,再点一点一级分类,会发现右边的滚动条并不是在顶部,所以要动态绑定一个scroll-y属性,然后每一次点击了一级分类,重置为0
data() {
return {
// 滚动条距离顶部的距离
scrollTop: 0
}
}
// 选中项改变的事件处理函数
activeChanged(i) {
this.active = i
this.cateLevel2 = this.cateList[i].children
// 让 scrollTop 的值在 0 与 1 之间切换
this.scrollTop = this.scrollTop === 0 ? 1 : 0
// 可以简化为如下的代码:
// this.scrollTop = this.scrollTop ? 0 : 1
}
由于搜索组件要在很多页面都要进行重用,所以这里可以封闭为一个组件
1、在根目录下新建一个components,再新建组件
2、直接在结构中以标签的方式直接使用自定义组件
在分类页面中,加上了自定义搜索组件后,右侧分类下面会不能滑动到底部,因为样式的原因,所以还需要在挂载的时候就要减去搜索组件的大小
onLoad() {
const sysInfo = uni.getSystemInfoSync()
// 可用高度 = 屏幕高度 - navigationBar高度 - tabBar高度 - 自定义的search组件高度
this.wh = sysInfo.windowHeight - 50
}
为了增加组件的通用性,我们可以通过props来定义属性,以方便以后使用的时候可以通过传入参数,让组件更个性化
1、通过props来定义两个变量
props: {
// 背景颜色
bgcolor: {
type: String,
default: '#C00000'
},
// 圆角尺寸
radius: {
type: Number,
// 单位是 px
default: 18
}
}
2、再动态的绑定stype属性
搜索
这个项目的搜索框的实现是先用一个view然后再点击跳转到搜索页面实现的,所以跳转后自动获取焦点,以增强用户体验
在components中的uni-search-bar中uni-search-bar.vue中,data中的show与showSync的值改为true即可,但是这里直接改源码不好
经典防抖,每次输入后,500毫秒内要是有新的输入事件,再不断重启延时器
input(e) {
// 清除 timer 对应的延时器
clearTimeout(this.timer)
// 重新启动一个延时器,并把 timerId 赋值给 this.timer
this.timer = setTimeout(() => {
// 如果 500 毫秒内,没有触发新的输入事件,则为搜索关键词赋值
this.kw = e.value
console.log(this.kw)
}, 500)
}
最近搜索的应该放在前面,可以用计算属性,再把这个数组复制一下再反转
computed: {
historys() {
// 注意:由于数组是引用类型,所以不要直接基于原数组调用 reverse 方法,以免修改原数组中元素的顺序
// 而是应该新建一个内存无关的数组,再进行 reverse 反转
return [...this.historyList].reverse()
}
}
在保存关键词为历史记录的方法中,把这个数组转为set对象,因为set对象没有重复的元素
再移除对应元素,再添加元素,再转为数组即可
// 保存搜索关键词为历史记录
saveSearchHistory() {
// this.historyList.push(this.kw)
// 1. 将 Array 数组转化为 Set 对象
const set = new Set(this.historyList)
// 2. 调用 Set 对象的 delete 方法,移除对应的元素
set.delete(this.kw)
// 3. 调用 Set 对象的 add 方法,向 Set 中添加元素
set.add(this.kw)
// 4. 将 Set 对象转化为 Array 数组
this.historyList = Array.from(set)
}
把对象用JSON.stringify转为json,存在本地
uni.setStorageSync('kw', JSON.stringify(this.historyList))
把json用JSON.parse转为对象,拿到数据
onLoad() {
this.historyList = JSON.parse(uni.getStorageSync('kw') || '[]')
}
在根目录下store中store.js
导入vue与vuex,再安装为vue插件,创建store实例对象,向外暴露store对象,也可以使用别的模块中的数据
// 1. 导入 Vue 和 Vuex
import Vue from 'vue'
import Vuex from 'vuex'
// 2. 将 Vuex 安装为 Vue 的插件
Vue.use(Vuex)
// 3. 创建 Store 的实例对象
const store = new Vuex.Store({
// TODO:挂载 store 模块
modules: {
m_cart: moduleCart,
},
})
// 4. 向外共享 Store 的实例对象
export default store
最后再从入口文件中把store挂载到vue实例上
// 1. 导入 store 的实例对象
import store from './store/store.js'
const app = new Vue({
...App,
// 2. 将 store 挂载到 Vue 实例上
store,
})
app.$mount()
为了模块化,我们可以再建cart.js,为了语义化,可以开启命名空间
export default {
// 为当前模块开启命名空间
namespaced: true,
// 模块的 state 数据
state: () => ({
// 购物车的数组,用来存储购物车中每个商品的信息对象
// 每个商品的信息对象,都包含如下 6 个属性:
// { goods_id, goods_name, goods_price, goods_count, goods_small_logo, goods_state }
cart: [],
}),
// 模块的 mutations 方法
mutations: {},
// 模块的 getters 属性
getters: {},
}
// 按需导入 mapMutations 这个辅助方法
import { mapMutations } from 'vuex'
export default {
methods: {
// 把 m_cart 模块中的 addToCart 方法映射到当前页面使用
...mapMutations('m_cart', ['addToCart']),
},
}
// 通过 commit 方法,调用 m_cart 命名空间下的 saveToStorage 方法
this.commit('m_cart/saveToStorage')
先把映射一下
再把使用方法更新下标为2,即第3个tabBar的上徽标
// 按需导入 mapGetters 这个辅助方法
import { mapGetters } from 'vuex'
export default {
data() {
return {}
},
computed: {
// 将 m_cart 模块中的 total 映射为当前页面的计算属性
...mapGetters('m_cart', ['total']),
},
}
methods: {
setBadge() {
// 调用 uni.setTabBarBadge() 方法,为购物车设置右上角的徽标
uni.setTabBarBadge({
index: 2, // 索引
text: this.total + '' // 注意:text 的值必须是字符串,不能是数字
})
}
}
然后在挂载的时候和更新数量的时候用一下就行
因为很多页面都要用,所以这里可以使用混入,在根目标下新建一个mixins文件夹,然后把代码封装到一个单独的js文件,在四个tabBar页面中导入即可
也可再加一个监听属性,可以全局改变tabBar的值
watch: {
// 监听 total 值的变化
total() {
// 调用 methods 中的 setBadge 方法,重新为 tabBar 的数字徽章赋值
this.setBadge()
},
},
// 导入自己封装的 mixin 模块
import badgeMix from '@/mixins/tabbar-badge.js'
export default {
// 将 badgeMix 混入到当前的页面中进行使用
mixins: [badgeMix],
// 省略其它代码...
}
商品数量 = 每一项已勾选的商品所选数量相加
这里使用了reduce方法
// 勾选的商品的总数量
checkedCount(state) {
// 先使用 filter 方法,从购物车中过滤器已勾选的商品
// 再使用 reduce 方法,将已勾选的商品总数量进行累加
// reduce() 的返回值就是已勾选的商品的总数量
return state.cart.filter(x => x.goods_state).reduce((total, item) => total += item.goods_count, 0)
}
用ui-swipe-action组件,这里的item项,用:right-options来设置,之前是options属性
options: [{
text: '删除', // 显示的文本内容
style: {
backgroundColor: '#C00000' // 按钮的背景颜色
}
}]
// 选择收货地址
async chooseAddress() {
// 1. 调用小程序提供的 chooseAddress() 方法,即可使用选择收货地址的功能
// 返回值是一个数组:第 1 项为错误对象;第 2 项为成功之后的收货地址对象
const [err, succ] = await uni.chooseAddress().catch(err => err)
// 2. 用户成功的选择了收货地址
if (err === null && succ.errMsg === 'chooseAddress:ok') {
// 为 data 里面的收货地址对象赋值
this.address = succ
}
}
之前的API点击取消后,再次点选择地址不会再弹出是否确认授权,但是现在本来也就没有弹出框了,所以就没有了这个问题
若在没有登录情况下结算,会提示先登录,并会自动3秒后跳转到登录页面
1、展示倒计时的信息
// 展示倒计时的提示消息
showTips(n) {
// 调用 uni.showToast() 方法,展示提示消息
uni.showToast({
// 不展示任何图标
icon: 'none',
// 提示的消息
title: '请登录后再结算!' + n + ' 秒后自动跳转到登录页',
// 为页面添加透明遮罩,防止点击穿透
mask: true,
// 1.5 秒后自动消失
duration: 1500
})
}
2、在data中声明秒数
data() {
return {
// 倒计时的秒数
seconds: 3
}
}
3、延迟导航到my页面
两个问题,一个是跳转之后,计时器还在,所以要在跳转之前清楚定时器,二是为了防止之后的数据不出错,重置秒数
// 延迟导航到 my 页面
delayNavigate() {
// 把 data 中的秒数重置成 3 秒
this.seconds = 3
this.showTips(this.seconds)
this.timer = setInterval(() => {
this.seconds--
if (this.seconds <= 0) {
clearInterval(this.timer)
uni.switchTab({
url: '/pages/my/my'
})
return
}
this.showTips(this.seconds)
}, 1000)
}
在vuex中备好一个重定向对象
redirectInfo: null
然后在跳转之前
把重定向的对象给vuex
mutations: {
// 更新重定向的信息对象
updateRedirectInfo(state, info) {
state.redirectInfo = info
}
}
在映射之后,使用方法
// 跳转到 my 页面
uni.switchTab({
url: '/pages/my/my',
// 页面跳转成功之后的回调函数
success: () => {
// 调用 vuex 的 updateRedirectInfo 方法,把跳转信息存储到 Store 中
this.updateRedirectInfo({
// 跳转的方式
openType: 'switchTab',
// 从哪个页面跳转过去的
from: '/pages/cart/cart'
})
}
})
然后在my-login组件中,在调用接口登录成功后,调用返回页面方式
// 调用登录接口,换取永久的 token
async getToken(info) {
// 省略其它代码...
// 判断 vuex 中的 redirectInfo 是否为 null
// 如果不为 null,则登录成功之后,需要重新导航到对应的页面
this.navigateBack()
}
// 返回登录之前的页面
navigateBack() {
// redirectInfo 不为 null,并且导航方式为 switchTab
if (this.redirectInfo && this.redirectInfo.openType === 'switchTab') {
// 调用小程序提供的 uni.switchTab() API 进行页面的导航
uni.switchTab({
// 要导航到的页面地址
url: this.redirectInfo.from,
// 导航成功之后,把 vuex 中的 redirectInfo 对象重置为 null
complete: () => {
this.updateRedirectInfo(null)
}
})
}
}
个人开发者是不能用微信支付相关的api的,所以这方面的几个api没法试,大概就是发请求判断是否支付成功再进行下一步操作
1、创建分支
git checkout -b settle
2、写完该分支的代码后,先提交到暂存区,再提交到本地
git add .
git commit -m "完成了登录和支付功能的开发"
3、先转到mastwr分支,再合并分支,再push到远程
git checkout master
git merge settle
git push
4、删除本地的分支
git branch -d settle
1、在HBuilderX,上面的分布,选微信小程序
2、从微信开发者工具右上点发布
3、完成微信小程序版本管理的基本信息步骤,即可。审核过后可正式上线
1、HBuilder X上,点开mainifest.json文件进行配置
2、点击App图标配置,设置图标
3、点分布中原生app-云打包
4、勾选一下打包的配置
5、在控制台看打包进度,比较慢一点,然后会有一个链接,打开目录,下载里面的apk安装包,安装到android手机中即可。
不过没有做多端适配,所以app的一些功能不能完整的正常运行,比如微信支付,收货地址等
其实上这个小程序也差不多半个月的时间完整了,很多都是之前vue的一些语法,只不过在一些细节上实现有不同。