在做完了两个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.$showMsg()方法
   
使用原生uni.showToast({})来提示用户,要配置配置对象,所以封闭全局方法来简化之后的使用
// 封装的展示消息提示的方法
uni.$showMsg = function (title = '数据加载失败!', duration = 1500) {
  uni.showToast({
    title,
    duration,
    icon: 'none',
  })
}
    分类导航区域
   
渲染然后再使用uni.switchTab()跳转页面
<!-- 分类导航区域 -->
<view class="nav-list">
  <view class="nav-item" v-for="(item, i) in navList" :key="i" @click="navClickHandler(item)">
    <image :src="item.image_src" class="nav-img"></image>
  </view>
</view>// nav-item 项被点击时候的事件处理函数
navClickHandler(item) {
  // 判断点击的是哪个 nav
  if (item.name === '分类') {
    uni.switchTab({
      url: '/pages/cate/cate'
    })
  }
}
    三级分类,左右侧滚动
   
<template>
  <view>
    <view class="scroll-view-container">
      <!-- 左侧的滚动视图区域 -->
      <scroll-view class="left-scroll-view" scroll-y :style="{height: wh + 'px'}">
        <view class="left-scroll-view-item active">xxx</view>
        <view class="left-scroll-view-item">xxx</view>
        <view class="left-scroll-view-item">xxx</view>
        <view class="left-scroll-view-item">xxx</view>
        <view class="left-scroll-view-item">xxx</view>
        <view class="left-scroll-view-item">多复制一些节点,演示纵向滚动效果...</view>
      </scroll-view>
      <!-- 右侧的滚动视图区域 -->
      <scroll-view class="right-scroll-view" scroll-y :style="{height: wh + 'px'}">
        <view class="left-scroll-view-item">zzz</view>
        <view class="left-scroll-view-item">zzz</view>
        <view class="left-scroll-view-item">zzz</view>
        <view class="left-scroll-view-item">zzz</view>
        <view class="left-scroll-view-item">多复制一些节点,演示纵向滚动效果</view>
      </scroll-view>
    </view>
  </view>
</template>
     
   
这里有一个bug,就是每点一次左边的一级分类,再滚动右侧的二三级分类,再点一点一级分类,会发现右边的滚动条并不是在顶部,所以要动态绑定一个scroll-y属性,然后每一次点击了一级分类,重置为0
data() {
  return {
    // 滚动条距离顶部的距离
    scrollTop: 0
  }
}<!-- 右侧的滚动视图区域 -->
<scroll-view class="right-scroll-view" scroll-y :style="{height: wh + 'px'}" :scroll-top="scrollTop"></scroll-view>// 选中项改变的事件处理函数
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
}
    封闭my-search组件
   
由于搜索组件要在很多页面都要进行重用,所以这里可以封闭为一个组件
1、在根目录下新建一个components,再新建组件
     
   
2、直接在结构中以标签的方式直接使用自定义组件
<my-search></my-search>在分类页面中,加上了自定义搜索组件后,右侧分类下面会不能滑动到底部,因为样式的原因,所以还需要在挂载的时候就要减去搜索组件的大小
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 class="my-search-container" :style="{'background-color': bgcolor}">
  <view class="my-search-box" :style="{'border-radius': radius + 'px'}">
    <uni-icons type="search" size="17"></uni-icons>
    <text class="placeholder">搜索</text>
  </view>
</view>
    搜索框自动获取焦点
   
这个项目的搜索框的实现是先用一个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') || '[]')
}
    再练习vuex
   
在根目录下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: {},
}
    在页面中把store映射到当前页面
   
// 按需导入 mapMutations 这个辅助方法
import { mapMutations } from 'vuex'
export default {
  methods: {
    // 把 m_cart 模块中的 addToCart 方法映射到当前页面使用
    ...mapMutations('m_cart', ['addToCart']),
  },
}
    在store中mutations使用commit调用mutations中的方法
   
 // 通过 commit 方法,调用 m_cart 命名空间下的 saveToStorage 方法
   this.commit('m_cart/saveToStorage')
    为tabBar设置数字徽标
   
先把映射一下
再把使用方法更新下标为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效果
   
      <!-- 滑动删除效果 -->
      <uni-swipe-action>
        <block v-for="(goods, i) in cart" :key="i">
          <!-- uni-swipe-action-item 可以为其子节点提供滑动操作的效果。需要通过 options 属性来指定操作按钮的配置信息 -->
          <uni-swipe-action-item :right-options="options" @click="swipeActionClickHandler(goods)">
            <my-goods :goods="goods" :show-radio="true" :show-num="true" @radio-change="radioChangeHandler" @num-change="numChangeHandler"></my-goods>
          </uni-swipe-action-item>
        </block>
      </uni-swipe-action>
用ui-swipe-action组件,这里的item项,用:right-options来设置,之前是options属性
 options: [{
      text: '删除', // 显示的文本内容
      style: {
        backgroundColor: '#C00000' // 按钮的背景颜色
      }
    }]
    实现选择收货地址
   
<!-- 选择收货地址的盒子 -->
<view class="address-choose-box" v-if="JSON.stringify(address) === '{}'">
  <button type="primary" size="mini" class="btnChooseAddress" @click="chooseAddress">请选择收货地址+</button>
</view>// 选择收货地址
  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没法试,大概就是发请求判断是否支付成功再进行下一步操作
    git的简单使用
   
1、创建分支
git checkout -b settle2、写完该分支的代码后,先提交到暂存区,再提交到本地
git add .
git commit -m "完成了登录和支付功能的开发"3、先转到mastwr分支,再合并分支,再push到远程
git checkout master
git merge settle
git push4、删除本地的分支
git branch -d settle
    分布
   
1、在HBuilderX,上面的分布,选微信小程序
     
   
2、从微信开发者工具右上点发布
     
   
3、完成微信小程序版本管理的基本信息步骤,即可。审核过后可正式上线
    发布为android App
   
1、HBuilder X上,点开mainifest.json文件进行配置
     
   
2、点击App图标配置,设置图标
     
   
3、点分布中原生app-云打包
     
   
4、勾选一下打包的配置
     
   
5、在控制台看打包进度,比较慢一点,然后会有一个链接,打开目录,下载里面的apk安装包,安装到android手机中即可。
不过没有做多端适配,所以app的一些功能不能完整的正常运行,比如微信支付,收货地址等
其实上这个小程序也差不多半个月的时间完整了,很多都是之前vue的一些语法,只不过在一些细节上实现有不同。
 
