uniapp黑马项目总结

  • Post author:
  • Post category:uniapp


在做完了两个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 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、完成微信小程序版本管理的基本信息步骤,即可。审核过后可正式上线

发布为android App

1、HBuilder X上,点开mainifest.json文件进行配置

2、点击App图标配置,设置图标

3、点分布中原生app-云打包

4、勾选一下打包的配置

5、在控制台看打包进度,比较慢一点,然后会有一个链接,打开目录,下载里面的apk安装包,安装到android手机中即可。

不过没有做多端适配,所以app的一些功能不能完整的正常运行,比如微信支付,收货地址等

其实上这个小程序也差不多半个月的时间完整了,很多都是之前vue的一些语法,只不过在一些细节上实现有不同。



版权声明:本文为qq_42765776原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。