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

《微信小程序》入门小程序-商城小demo的实现

程序员文章站 2022-06-14 22:44:51
...

前言

承接上一篇《入门小程序》,希望通过这个简单的商城demo可以更好的学习并理解微信小程序的开发;
(PS:本文接口和图片资源来自于某位老师的网课,为了防止说打广告,因此不放链接了,布局和实现这两部分与课程不尽相同,虽然是学习但是学习还是要融入自己的思考才能有所收货,因此只能说最终效果是一致的);

分析

在开始码代码前我个人习惯先分析一下接下来要做什么,理一下逻辑,同时看看有可能会涉及到哪些知识点,这样在准备阶段大致心里就有了一个底,对于重复度高的部分可以提前预计到并封装成组件,公共样式也尽量提前抽离,不然要吃好多亏…真的…

另外在移动端布局,我问的几乎所有大佬都推荐使用flex进行布局,因为移动端没有那么多老版本需要兼容,因此本demo中也是跟着大佬的意见走,使用flex布局,当然如果是响应式的页面,那就不仅仅会在移动端进行查看,还要考虑到pc端,使用flex就需要比较谨慎了;

项目下载

demo代码已经上传到csdn的资源库了,这里是下载链接:下载demo

思维导图

看到最终效果后,简单分析了一下页面以及可能涉及到的功能点,页面大致有这么几个
《微信小程序》入门小程序-商城小demo的实现

页面功能点及效果

首页
首页涉及的功能大致有:

  • 组件:轮播图组件,滚动视图组件;
  • 布局:flex布局;
  • 功能:wx.loading(请求时的加载动画),wx.request(wx的数据请求);
    《微信小程序》入门小程序-商城小demo的实现
    分类
    分类涉及的功能大致有:
  • 组件:滚动视图组件;
  • 布局:flex布局,wx:for循环,tap点击事件;
  • 功能:wx.loading(请求时的加载动画),wx.request(微信中的数据请求方法),wx.navigateTo(页面跳转);
    《微信小程序》入门小程序-商城小demo的实现
    商品列表
    商品列表涉及的功能大致有:
  • 组件:滚动视图组件;
  • 布局:flex布局,wx:for循环,tap点击事件;
  • 功能:wx.loading(请求时的加载动画),wx.request(微信中的数据请求方法),wx.navigateTo(页面跳转),onPullDownRefresh(下拉刷新),onReachBottom(触底上拉加载);
    《微信小程序》入门小程序-商城小demo的实现

商品详情
商品详情涉及的功能大致有:

  • 组件:滚动视图组件;
  • 布局:flex布局,wx:for循环,tap点击事件,wx:if渲染;
  • 功能:wx.loading(请求时的加载动画),wx.request(微信中的数据请求方法),wx.navigateTo(页面跳转)storage本地存储,wx.switchTab(tab页面跳转)
  • 自定义组件:支付方式的弹窗组件,数量的弹窗组件,数量加减的按钮组件;
    《微信小程序》入门小程序-商城小demo的实现
    购物车
    购物车涉及的功能大致有:
  • 组件:滚动视图组件;
  • 布局:flex布局,wx:for循环,tap点击事件,wx:if渲染;
  • 功能:wx.navigateTo(页面跳转)storage本地存储,wx.setTabBarBadge(tab右上角红色标记设置)
  • 自定义组件:数量加减的按钮组件;
    《微信小程序》入门小程序-商城小demo的实现
    我的
    我的涉及的功能大致有:
  • 组件:视图组件;
  • 布局:flex布局,tap点击事件,wx:if渲染;
  • 功能:userInfoReadyCallback(获取用户信息);

准备

小程序的项目和普通的web项目不大一样,微信官方为了数据的按钮,向服务器请求的地址,必须进行登记,如果向没有登记的服务器地址发送请求,那么是会报错的;

登记服务器地址需要登录微信公众平台,打开“开发”->“开发设置”->“服务器域名”,输入并保存服务器地址;保存之后在开发工具的“详情”->“项目配置”->“域名信息”可以查看到配置的域名,如图所示
《微信小程序》入门小程序-商城小demo的实现

功能点

感觉没有必要将每一行代码都放在博客里,那样的话感觉整篇博客都是长篇代码而且效果也不好,不能调试,要看整体代码直接下载项目就是了,还能一边看一边调试,本文主要记录一下涉及到的功能点的代码,方便理解小程序;

页面布局、指令及组件

页面主要是通过flex进行布局,关于flex布局其实还是有一点点复杂的,这边就不多介绍了,百度Flex布局一堆指南;

指令需要使用到了wx:for(列表渲染),wx:if(条件渲染)该两个指令主要用于到数据请求到之后需要将请求到的数据进行渲染,比如数组,那么就需要通过wx:for渲染,如果是根据不同结果决定是不是要显示在页面上那么就需要通过wx:if进行判断;

组件主要涉及到了滚动视图组件轮播图组件,至于最常规的view,button这些就不多说了,因为每个页面几乎都有用到

  <!--滚动区域-->
	<!--纵向与横向的滚动默认是关闭的,通过设置scroll-x和scroll-y为true开启-->
  <scroll-view class="scroll" scroll-y="true">
    <!--轮播图-->
    <view>
      <swiper
        //是否显示轮播上的点
        indicator-dots="{{indicatorDots}}"
        //滑动方向是否为纵向
        vertical="{{vertical}}"
				//是否自动切换图片
        autoplay="{{autoplay}}"
				//自动切换时间间隔
        interval="{{interval}}"
				//滑动动画时常
        duration="{{duration}}"
				//是否采用衔接滑动
        circular="{{circular}}"
				//轮播图上原点的颜色
        indicator-color="rgba(255, 255, 255, .3)"
				//当前被选中的轮播图的小圆点颜色
        indicator-active-color="#ffffff"
        class="swiper"
      >
      //wx:for指令循环遍历数组
      <block wx:for="{{swipers}}" wx:key="index">
        <swiper-item class="swiper-item-style">
          <image class="widgets-item" src="{{item.image}}" mode="aspectFill"></image>
        </swiper-item>
      </block>
      </swiper>
    </view>
  </scroll-view>

scroll-view,官方提供的滚动视图组件,它将内容包裹起来之后,当内容超出高度或宽度,那么可纵向或横向滚动视图区域,具体请看:scroll-view滚动视图,在这里主要使用到了其一个纵向属性scroll-y,默认是false,也就是不允许滚动,因此需要设置成true;

swiper,轮播图组件,网页上常见的功能组件,微信官方命名为滑块视图容器,该容器中只可放置swiper-item组件,其内是具体内容,关于使用到的属性已注释在组件上了,更多具体请看:swiper轮播图组件

请求数据

微信官方提供了请求数据的方法,并不需要自己引入axios或者ajax等工具,在微信中请求的方法叫做wx.request()方法,具体请看微信数据请求request,当然请求的接口地址的域名必须是在该小程序的后台登记过的,没有登记过是会报错的,即使接口正确;

当页面进行加载(也就是生命周期处于onload)的时候,向服务器请求数据,同时显示加载中的提示loading提示框,等到请求成功,返回了数据,那么就将加载中的提示关闭掉;

当请求成功之后,将请求到的数据存储到本地的data中,存储变量的方式和react类似,是通过setData()的方式存储的;

//引入接口地址,接口地址是统一写在一个文件中的,通过require引入
const interfaces = require("../../utils/urlconfig.js");

	/**
   * 生命周期函数--监听页面加载
   */
  onLoad: function (options) {
    const _this = this;
    //显示加载中的提示框,文字为:加载中
    wx.showLoading({
      title: '加载中',
    })
    //发起一个请求
    wx.request({
      //请求的服务器地址,例子的地址是统一写的,因此这里只是引入urlconfig.js中写好的地址
      url:interfaces.homepage,
      header:{
        // 返回的数据类型为json格式,当然默认是application/json格式
        "content-type":"application/json"
      },
      //请求成功后的回调函数
      success(res){
        // 请求成功代表有返回数据,那么通过setData保存数据,保存的数据会实时更新
        _this.setData({
          swipers:res.data.swipers,
          logos:res.data.logos,
          quicks:res.data.quicks,
          pageRow:res.data.pageRow
        })
        //隐藏加载中的提示框
        wx.hideLoading();
      }
    })
  }

这里通过setData设置数据,刚开始有疑问,为什么要通过setData设置,直接通过类似下例的方法不可以吗?

this.data.swipers = res.data.swipers;

这种方式设置数据难道不行吗,这种设置当然是可以的,但是这种设置有一个致命缺陷,就是设置后的数据不会实时的响应式的渲染到页面上,比如;swipers初始时是一个空数组,页面上通过wx:for循环也没有结果,当使用setData()的方式设置值后,那么页面会实时的渲染新结果,如果是通过直接赋值的方式保存结果,那么此时页面是不会对其作出响应的,仍然是渲染的空数组;

页面跳转

在小程序中,页面A跳转到页面B,是通过官方提供的跳转方法:wx.navigateTo(),这个方法通常和事件绑定在一起(当然这个不是绝对的,比如需要根据用户是否登录过作为跳转依据,跳转到不同的页面),具体看示例

页面A,对view绑定了一个点击事件,事件类型是bindtap(点击事件),函数是showShopDetail,代表的是当点击这个view之后触发了showShopDetail这个函数

<view 
	wx:for="{{item.desc}}" 
	class="right-shop" 
	wx:key="index" 
	data-txt="{{shop.text}}"
	wx:for-item="shop" 
	bindtap="showShopDetail"> 
    <image src="{{shop.img}}" class="shop-msg" mode="aspectFill"></image>
    <text class="shop-title">{{shop.text}}</text>
</view>

这个函数调用了wx.navigateTo进行页面跳转,跳转的同时携带了一个参数title,值是绑定在view上的自定义属性txt的值

showShopDetail(e){
  wx.navigateTo({
    //url是需要进行跳转的地址
    //例子中是跳转到pages目录下shopList目录下的index页面,并携带了一个参数title
    url: '/pages/shopList/index?title='+e.currentTarget.dataset.txt,
  })
}

新跳转的页面,比如这里是跳转到shopList这个页面,在shoplist这个页面中的onLoad生命周期内可以接收到传递过来的参数,

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad: function (options) {
    //options中会携带传递过来的参数title
    this.setData({
      title:options.title
    })
    //设置页面标题
    wx.setNavigationBarTitle({
      title: options.title,
    })
  }

自定义组件

概述

小程序同样也支持将功能模块抽象成自定义组件,具体查看自定义组件,类似于页面,自定义组件类似于页面,由 json、wxml、wxss、js 4个文件组成,其中必须要在json文件中声明这是一个自定义组件

{
  "component": true
}

之后,在wxml中编写模版,wxss中编写样式,如果有事件需要传递,那么可以通过triggerEvent()方法进行传递

this.triggerEvent("事件名",obj)

如果是接收父组件的属性,那么需要在子组件的js文件中定义名字

Component({
  /**
   * 组件的属性列表
   */
  properties: {
    //定义了一个count属性,值的类型是Number类型,这个值是父组件上传递过来的
    count:Number
  },
  /**
   * 组件的初始数据
   */
  data: {},
  /**
   * 组件的方法列表
   */
  methods: {}
})

接着,在需要调用组件的json文件中新定义一个字段usingComponents,key是调用组件时的标签名,值是对应的组件地址

{
  "usingComponents": {
    "number":"/components/number/index"
  }
}

案例

自定义组件创建

新建一个组件,在根目录下新建了一个componnents文件夹,之后新建了一个number文件夹,右击number文件夹,选择新建Component,输入名字后会字段生成4个标准文件
《微信小程序》入门小程序-商城小demo的实现
在index.wxml中输入结构,同时通过catch绑定两个点击事件

<view class="number">
  <view class="number-subtract" catchtap="subtractCount">-</view>
  <view class="number-num">{{count}}</view>
  <view class="number-add" catchtap="addCount">+</view>
</view>

当然,json文件中必须先声明这是一个组件

{
  "component": true,
  "usingComponents": {}
}

之后就是事件传递了,当点击绑定的两个事件时,将事件往父组件传递

Component({
  /**
   * 组件的属性列表
   */
  properties: {
    //定义了一个count属性,值的类型是Number类型
    count:Number
  },
  /**
   * 组件的初始数据
   */
  data: {},
  /**
   * 组件的方法列表
   */
  methods: {
    addCount(){
      this.triggerEvent("addCount")
    },
    subtractCount(){
      this.triggerEvent("subtractCount")
    }
  }
})

父组件调用

首先是在需要调用number组件的页面,进行组件的注册,组件的注册简单的说就是在调用的页面的json文件中的usingComponents字段中添加组件

{
  "usingComponents": {
    //"/components/number/index"是组件所在的路径
    "number":"/components/number/index"
  }
}

这样就算是注册了,之后就可以直接在wxml中调用了

<view class="item-container">
    <view class="item-title">数量:</view>
    <view class="item-num">
      //调用组件
      <number 
				//这个值就是会传递到子组件中
				count="{{partData.count}}" 
				//绑定了一个事件,事件名就是子组件传递过来的addCount,触发后执行js文件中的addCount函数
				catchaddCount="addCount" 
        //绑定了一个事件,事件名就是子组件传递过来的subtractCount,触发后执行js文件中的subtractCount函数
				catchsubtractCount="subtractCount"></number>
    </view>
</view>

这样基本就是一套完整的流程,和vue不同的是,vue是不允许直接修改父组件传递过来的属性的,小程序不同,当子组件接收到父组件传递过来的属性,如果对其做修改,修改的是在data中的副本,换句话说,当子组件接收到父组件的属性后,会在data中创建一个副本,如果对属性修改,那么修改的是副本并不是传递过来的属性;

数据缓存

这个场景是这样的,当把产品加入购物车后,关闭小程序,此时如果再打开,显然加入过购物车的商品必须是依旧存在的,那么为了获取之前加入到购物车的商品,就需要商品加入购物车的同时,将商品列表加入本地缓存中;
小程序的缓存是storage,类似web中的localStorage,具体可以查看官方文档数据缓存

概述

查看缓存

《微信小程序》入门小程序-商城小demo的实现

设置缓存

通过wx.setStorage()设置缓存内容,比如

wx.setStorage({
  //key就是属性名
  key:"key",
  //data就是对应的属性值
  data:"value"
})

读取缓存

通过wx.getStorage()读取缓存内容,比如

wx.getStorage({
  //key就是需要读取的缓存的属性名
  key: 'key',
  //当读取成功之后,会执行success回调函数
  success (res) {
    console.log(res.data)
  }
})

删除缓存

通过wx.removeStorage()读取缓存内容,比如

wx.removeStorage({
  //需要删除的属性名
  key: 'key',
  //删除成功之后的回调函数
  success (res) {
    console.log(res)
  }
})

案例

当打开购物车页面时,在onShow生命周期阶段,读取缓存中的商品列表,并且假如有商品列表,那么在tab的右上角显示商品数量

效果

《微信小程序》入门小程序-商城小demo的实现

代码

  /**
   * 生命周期函数--监听页面显示
   */
  onShow: function () {
    //获取缓存数据
    wx.getStorage({
      //缓存的属性名叫做cartInfo
      key: 'cartInfo',
      //读取成功的时候执行success回调函数
      success(res){
        const arr = res.data;
        //判断获得的数组的长度是否大于0,如果大于0,那么代表有数据的
        arr.length > 0 ?
          //显示右上角文本
          wx.setTabBarBadge({
          	//下标为2的tabBar
            index: 2,
          	//文本内容是数组的长度
            text: String(arr.length)
          }):
        	//如果长度不大于0,那么就删除下标为2的tabBar的右上角文本
          wx.removeTabBarBadge({
            index: 2,
          })
      }
    })
  },

下拉刷新和上拉加载

在微信中,其实这两种方法都已经写好了,毕竟这是移动端非常常用的功能

下拉刷新

下拉刷新这个功能,首先必须在json文件中进行配置,否则,即使在方法写了代码,也是不会生效的

{
  "usingComponents": {},
   //下拉刷新设置成true,代表需要下拉刷新这个功能
  "enablePullDownRefresh":true,
  "backgroundTextStyle":"dark"
}

之后,在页面的js文件中,和生命周期并列的有这么一个函数

 /**
   * 页面相关事件处理函数--监听用户下拉动作
   */
  onPullDownRefresh: function () {
    //微信官方提供的方法,作用是在标题上显示加载状态
    wx.showNavigationBarLoading();
    const _this = this;
    //下拉刷新代表需要重新请求数据,因此这边重新发起请求
    wx.request({
      //请求地址
      url: interfaces.productionsList,
      //请求成功以后的回调函数
      success(res){
        _this.setData({
          productionsList:res.data
        })
        //微信官方提供的方法,因为一旦请求成功有返回了,那么下拉加载动画就不需要了,可以立即隐藏掉
        wx.stopPullDownRefresh();
        //微信官方提供的方法,隐藏标题上的加载状态
        wx.hideNavigationBarLoading();
      }
    })
  },

上拉刷新

上拉刷新其实和下拉加载差不多,当内容滚动到大约距离底部50px的时候,会触发这个函数

 /**
   * 页面上拉触底事件的处理函数
   */
  onReachBottom: function () {
    //停止下拉刷新,不管有没有,防止上拉刷新的时候,还有下拉加载的动画
    wx.stopPullDownRefresh();
    //微信官方提供的方法,作用是在标题上显示加载状态
    wx.showNavigationBarLoading();

		//向服务发起请求获得数据
    wx.request({
      url: interfaces.productionsList + "/" + _this.data.page + "/" + _this.data.size,
      success(res){
        //隐藏标题的加载状态
        wx.hideNavigationBarLoading();
      }
    })
  },

用户信息

获取用户信息

微信对获取用户信息有严格的要求,具体内容见wx.getUserInfo(Object object),想要获取用户信息,必须是通过微信提供的开放能力open-type获取,而且必须是经过用户确认同意的,大致流程:

确认当前获取用户信息的api(也就是open-type上的getUserInfo)是否可用

//设置
data: {
  canIUse:wx.canIUse('button.open-type.getUserInfo')
}

canIUser,判断当前指定的Api是否可用,返回值是一个布尔值(关于canIUse的更多信息查看canIUse详情),使用的是canIUser中的:component.{component}.{attribute}.${option} 方式来调用,

  • ${component} 代表组件名字,也就是button组件
  • ${attribute} 代表组件属性,open-type属性
  • ${option} 代表组件属性的可选值,获取用户基本信息的值是getUserInfo

之后,给按钮声明一个属性open-type(关于open-type的值更多请看open-type),值是固定的,getUserInfo代表获取用户信息,之后绑定了一个事件,事件类型是getuserinfo,同时为其添加了一个函数,方法名是getUserInfo,点击按钮后执行了getUserInfo这个方法

<button wx:if="{{!hasUserInfo && canIUse}}" 
	open-type="getUserInfo" 
	bindgetuserinfo="getUserInfo">
    获取头像昵称
</button>

执行方法后,可以在该方法内获得到用户信息

getUserInfo: function(e){
  console.log(e.detail.userInfo);
  this.setData({
    userInfo: e.detail.userInfo,
    hasUserInfo:true
  })
}

保存用户信息

虽然说是保存,其实还是获取,比如某个小程序,假如用户没有授权过,那么需要用户点击按钮后授权,如果用户已经授权过了,那么下次打开的时候其实不需要再次授权了,直接可以获取到用户信息,因此在总的app.js中的onLaunch生命周期内,直接去判断当前用户有没有已经授权过,如果授权过,那么直接获取到用户信息,并保存到本地,查看wx.getSetting(Object object)

//app.js
App({
  onLaunch: function () {
    // 获取用户信息授权列表
    wx.getSetting({
      //获取成功以后执行success回调函数
      success: res => {
        //判断是否已经授权过获取用户信息了
        if (res.authSetting['scope.userInfo']) {
          // 已经授权,可以直接调用 getUserInfo 获取头像昵称,不会弹框
          wx.getUserInfo({
            success: res => {
              // 将用户信息存储到全局数据中
              this.globalData.userInfo = res.userInfo

              // 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回
              // 所以此处加入 callback 以防止这种情况
              if (this.userInfoReadyCallback) {
                this.userInfoReadyCallback(res)
              }
            }
          })
        }
      }
    })
  },
  //设置一个全局的属性,用来存储用户信息
  globalData: {
    userInfo: null
  }
})

那么新问题来了,在app.js中设置了属性,怎么在其他页面获取到,直接在需要的页面通过getApp()方法就可以获取到,比如

//获取应用实例
const app = getApp();

// pages/me/index.js
Page({
  /**
   * 生命周期函数--监听页面加载
   */
  onLoad: function (options) {
    //判断有没有用户信息,如果有那么直接存储到data中
    if(app.globalData.userInfo){
      this.setData({
        userInfo: app.globalData.userInfo,
        hasUserInfo:true
      })
    }
    /*
     * 如果没有用户信息,那么存在一种可能,就是打开这个页面的时候,网络请求还没有返回,也就是app.js中
     * 的getUserInfo这个网络请求还没有返回,因此这边使用这个固定的方法做一个处理,预防这种情况
     */
    else if(this.data.canIUse){
        app.userInfoReadyCallback = res => {
          console.log(res)
          this.setData({
            userInfo: app.globalData.userInfo,
            hasUserInfo:true
          })
        }
    }
  }
})

假如上面的onLoad生命周期内的函数都没有执行,那么就代表没有被授权过,那么用户需要点击按钮去授权;

注意点

currentTarget和target区别

这里得记录一下currentTarget和target的区别,这两个属性还是有点搞的:

  • target:当前点击的对象的上的数据;
  • currentTarget:被触发事件的组件上的数据;

看个示例

<view data-a="a" bindtap="a" class="outside">
  <view data-d="d" bindtap="d" class="outside-2">
    <button data-b="b" bindtap="b">点击b</button>
  </view>
  <button data-b="c" bcatchtap="c">点击c</button>
  点击a
</view>

//js文件
  a(e){
    console.log(e)
    console.log("这里是a------------------")
  },
  b(e){
    console.log(e)
    console.log("这里是b------------------")
  },
  c(e){
    console.log(e)
    console.log("这里是c------------------")
  },
  d(e){
    console.log(e)
    console.log("这里是d------------------")
  },

里面有四个点击事件,分别为:a、b、c、d,当点击b的时候,事件会进行冒泡,因此在触发事件b之后,还会冒泡触发事件d,之后在冒泡触发事件a;

当点击“点击b”之后,会触发3个事件:分别为b、d、a,通过打印我们知道:

  • 事件b:因为点击就是b事件,因此它的target和currentTarget的值是相同的;
  • 事件d:事件d是因为点击事件b冒泡触发,因此它的target中的属性是点击对象上的自定义属性,也就是button上的自定义属性data-b=“b”,而currentTarget的值当前绑定事件d的view上的自定义属性的值,也就是data-d=“d”;
  • 事件a:事件a也是因为点击事件b冒泡触发,因此它的target中的属性是点击对象上的自定义属性,也就是button上的自定义属性data-b=“b”,而currentTarget的值当前绑定事件a的view上的自定义属性的值,也就是data-a=“a”;

因此可以得出结论:target是当前点击的对象上的自定义属性,currentTarget是触发事件的对象上的自定义属性,如果点击对象和触发对象是同一个,那么target和currentTarget是相同的;