微信小程序

npm i @vant/weapp -S --production

npm i --save miniprogrom-api-promise

微信小程序基础

小程序和网页开发有什么区别

  1. 运行环境不同
    1. 网页在浏览器
    2. 小程序在微信环境
  2. api不同
    1. 小程序无法调用dom和bom
  3. 开发模式不同
    1. 小程序有自己一套开发模式
    2. 申请账号
    3. 安装开发者工具
    4. 创建和配置

注册

使用浏览器打开 https://mp.weixin.qq.com/ 网址,点击右上角的“立即注册”即可进入到小程序开发账号的注册流程,主要流程截图如下:

注册完成

获取小程序的 AppID

微信小程序_第1张图片

微信开发者工具是官方推荐使用的小程序开发工具,它提供的主要功能如下:
快速创建小程序项目
代码的查看和编辑
对小程序功能进行调试
小程序的预览和发布

推荐下载和安装最新的稳定版(Stable Build)的微信开发者工具,下载页面的链接如下:
https://developers.weixin.qq.com/miniprogram/dev/devtools/stable.html

安装

创建项目

微信小程序_第2张图片

微信小程序_第3张图片

项目基本构成

 pages 用来存放所有小程序的页面
 utils 用来存放工具性质的模块(例如:格式化时间的自定义模块)
 app.js 小程序项目的入口文件
 app.json 小程序项目的全局配置文件
 app.wxss 小程序项目的全局样式文件
 project.config.json 项目的配置文件
 sitemap.json 用来配置小程序及其页面是否允许被微信索引

微信小程序_第4张图片

其中,每个页面由 4 个基本文件组成,它们分别是:
 .js 文件(页面的脚本文件,存放页面的数据、事件处理函数等)
 .json 文件(当前页面的配置文件,配置窗口的外观、表现等)
 .wxml 文件(页面的模板结构文件)
 .wxss 文件(当前页面的样式表文件)

微信小程序_第5张图片

json配置文件

JSON 是一种数据格式,在实际开发中,JSON 总是以配置文件的形式出现。小程序项目中也不例外:通过不同的 .json 配置文件,可以对小程序项目进行不同级别的配置。

小程序项目中有 4 种 json 配置文件,分别是:
项目根目录中的 app.json 配置文件
项目根目录中的 project.config.json 配置文件
项目根目录中的 sitemap.json 配置文件
每个页面文件夹中的 .json 配置文件


  1. app.json 文件
app.json 是当前小程序的全局配置,包括了小程序的所有页面路径、窗口外观、界面表现、底部 tab 等。Demo 项目里边的 app.json 配置内容如下:

简单了解下这 4 个配置项的作用:
pages:用来记录当前小程序所有页面的路径
window:全局定义小程序所有页面的背景色、文字颜色等
style:全局定义小程序组件所使用的样式版本
sitemapLocation:用来指明 sitemap.json 的位置

微信小程序_第6张图片

  1. project.config.json 文件
project.config.json 是项目配置文件,用来记录我们对小程序开发工具所做的个性化配置,例如:
 setting 中保存了编译相关的配置
 projectname 中保存的是项目名称
 appid 中保存的是小程序的账号 ID

  1. sitemap.json 文件
微信现已开放小程序内搜索,效果类似于 PC 网页的 SEO。sitemap.json 文件用来配置小程序页面是否允许微信索引。
当开发者允许微信索引时,微信会通过爬虫的形式,为小程序的页面内容建立索引。当用户的搜索关键字和页面的索引匹配成功的时候,小程序的页面将可能展示在搜索结果中。

微信小程序_第7张图片

注意:sitemap 的索引提示是默认开启的,如需要关闭 sitemap 的索引提示,可在小程序项目配置文件 project.config.json 的 setting 中配置字段 checkSiteMap 为 false

  1. 页面的 .json 配置文件
小程序中的每一个页面,可以使用 .json 文件来对本页面的窗口外观进行配置,页面中的配置项会覆盖 app.json 的 window 中相同的配置项。例如:

微信小程序_第8张图片

只需要在 app.json -> pages 中新增页面的存放路径,小程序开发者工具即可帮我们自动创建对应的页面文件,
如图所示:

微信小程序_第9张图片

wxml

WXML(WeiXin Markup Language)是小程序框架设计的一套标签语言,用来构建小程序页面的结构,其作用类似于网页开发中的 HTML。

区别
标签名称不同
HTML (div, span, img, a)
WXML(view, text, image, navigator)
属性节点不同
超链接

提供了类似于 Vue 中的模板语法
数据绑定
列表渲染
条件渲染

wxss

WXSS (WeiXin Style Sheets)是一套样式语言,用于描述 WXML 的组件样式,类似于网页开发中的 CSS。

区别
新增了 rpx 尺寸单位
CSS 中需要手动进行像素单位换算,例如 rem
WXSS 在底层支持新的尺寸单位 rpx,在不同大小的屏幕上小程序会自动进行换算
提供了全局的样式和局部样式
项目根目录中的 app.wxss 会作用于所有小程序页面
局部页面的 .wxss 样式仅对当前页面生效
WXSS 仅支持部分 CSS 选择器
.class 和 #id
element
并集选择器、后代选择器
::after 和 ::before 等伪类选择器

js交互

项目仅仅提供界面展示是不够的,在小程序中,我们通过 .js 文件来处理用户的操作。例如:响应用户的点击、获取用户的位置等等。

js分类

小程序中的 JS 文件分为三大类,分别是:
app.js
是整个小程序项目的入口文件,通过调用 App() 函数来启动整个小程序
页面的 .js 文件
是页面的入口文件,通过调用 Page() 函数来创建并运行页面
普通的 .js 文件
是普通的功能模块文件,用来封装公共的函数或属性供页面使用

宿主环境

宿主环境(host environment)指的是程序运行所必须的依赖环境。例如:
Android 系统和 iOS 系统是两个不同的宿主环境。安卓版的微信 App 是不能在 iOS 环境下运行的,所以,Android 是安卓软件的宿主环境,脱离了宿主环境的软件是没有任何意义的!

微信小程序_第10张图片

小程序宿主环境

小程序借助宿主环境提供的能力,可以完成许多普通网页无法完成的功能,例如:
微信扫码、微信支付、微信登录、地理定位、etc…

宿主环境包含内容
通信模型
运行机制
组件
API


通信主体

小程序中通信的主体是渲染层和逻辑层,其中:
WXML 模板和 WXSS 样式工作在渲染层
JS 脚本工作在逻辑层

微信小程序_第11张图片

通信模型

小程序中的通信模型分为两部分:
 渲染层和逻辑层之间的通信
由微信客户端进行转发
 逻辑层和第三方服务器之间的通信
由微信客户端进行转发

微信小程序_第12张图片

小程序启动

把小程序的代码包下载到本地
解析 app.json 全局配置文件
执行 app.js 小程序入口文件,调用 App() 创建小程序实例
渲染小程序首页
小程序启动完成

页面渲染

加载解析页面的 .json 配置文件
加载页面的 .wxml 模板和 .wxss 样式
执行页面的 .js 文件,调用 Page() 创建页面实例
页面渲染完成

组件分类

小程序中的组件也是由宿主环境提供的,开发者可以基于组件快速搭建出漂亮的页面结构。官方把小程序的组件分为了 9 大类,分别是:
视图容器
基础内容
表单组件
导航组件
媒体组件
map 地图组件
canvas 画布组件
开放能力
无障碍访问

视图类容器

view
普通视图区域
类似于 HTML 中的 div,是一个块级元素
常用来实现页面的布局效果
scroll-view
可滚动的视图区域
常用来实现滚动列表效果
swiper 和 swiper-item
轮播图容器组件 和 轮播图 item 组件

基本使用

image-20221128104944817


  A
  B
  C



 pages/list/list.wxss


.container1 view:nth-child(1){
background-color: lightgreen;
}
.container1 view:nth-child(2){
  background-color: lightblue;
  }

  .container1 view:nth-child(3){
    background-color: lightcoral;
    }

.container1 {
      display: flex;
      justify-content: space-around;
    } 

   
  1. swiper 和 swiper-item 组件的基本使用

微信小程序_第13张图片

  1. swiper 组件的常用属性

属性 类型 默认值 说明
indicator-dots boolean false 是否显示面板指示点
indicator-color color rgba(0, 0, 0, .3) 指示点颜色
indicator-active-color color #000000 当前选中的指示点颜色
autoplay boolean false 是否自动切换
interval number 5000 自动切换时间间隔
circular boolean false 是否采用衔接滑动
  1. 常用的基础内容组件

text
文本组件
类似于 HTML 中的 span 标签,是一个行内元素
rich-text
富文本组件
支持把 HTML 字符串渲染为 WXML 结构

  1. text 组件的基本使用

通过 text 组件的 selectable 属性,实现长按选中文本内容的效果:

微信小程序_第14张图片

  1. rich-text 组件的基本使用

通过 rich-text 组件的 nodes 属性节点,把 HTML 字符串渲染为对应的 UI 结构:

微信小程序_第15张图片其他常用组件

button
按钮组件
功能比 HTML 中的 button 按钮丰富
通过 open-type 属性可以调用微信提供的各种功能(客服、转发、获取用户授权、获取用户信息等)
image
图片组件
image 组件默认宽度约 300px、高度约 240px
navigator(后面课程会专门讲解)
页面导航组件
类似于 HTML 中的 a 链接

  1. button 按钮的基本使用

微信小程序_第16张图片

  1. image 组件的基本使用

image-20221128110845877

  1. image 组件的 mode 属性

image 组件的 mode 属性用来指定图片的裁剪和缩放模式,常用的 mode 属性值如下:

mode 说明
scaleToFill (默认值)缩放模式,不保持纵横比缩放图片,使图片的宽高完全拉伸至填满 image 元素
aspectFit 缩放模式,保持纵横比缩放图片,使图片的长边能完全显示出来。也就是说,可以完整地将图片显示出来。
aspectFill 缩放模式,保持纵横比缩放图片,只保证图片的短边能完全显示出来。也就是说,图片通常只在水平或垂直方向是完整的,另一个方向将会发生截取。
widthFix 缩放模式,宽度不变,高度自动变化,保持原图宽高比不变
heightFix 缩放模式,高度不变,宽度自动变化,保持原图宽高比不变

小程序中的 API 是由宿主环境提供的,通过这些丰富的小程序 API,开发者可以方便的调用微信提供的能力,例如:获取用户信息、本地存储、支付功能等。

API三大分类


小程序官方把 API 分为了如下 3 大类:
事件监听 API
特点:以 on 开头,用来监听某些事件的触发
举例:wx.onWindowResize(function callback) 监听窗口尺寸变化的事件
同步 API
特点1:以 Sync 结尾的 API 都是同步 API
特点2:同步 API 的执行结果,可以通过函数返回值直接获取,如果执行出错会抛出异常
举例:wx.setStorageSync(‘key’, ‘value’) 向本地存储中写入内容
异步 API
特点:类似于 jQuery 中的 $.ajax(options) 函数,需要通过 success、fail、complete 接收调用的结果
举例:wx.request() 发起网络数据请求,通过 success 回调函数接收数据

权限管理需求

在中大型的公司里,人员的分工非常仔细:同一个小程序项目,一般会有不同岗位、不同角色的员工同时参与设计与开发。

此时出于管理需要,我们迫切需要对不同岗位、不同角色的员工的权限进行边界的划分,使他们能够高效的进行协同工作。

微信小程序_第17张图片

小程序开发流程

微信小程序_第18张图片

成员管理

微信小程序_第19张图片

小程序成员管理体现在管理员对小程序项目成员及体验成员的管理:
项目成员:
表示参与小程序开发、运营的成员
可登录小程序管理后台
管理员可以添加、删除项目成员,并设置项目成员的角色
体验成员:
表示参与小程序内测体验的成员
可使用体验版小程序,但不属于项目成员
管理员及项目成员均可添加、删除体验成员


权限 运营者 开发者 数据分析者
开发者权限
体验者权限
登录
数据分析 ** **
微信支付 ** **
推广 ** **
开发管理 ** **
开发设置
权限 运营者 开发者 数据分析者
开发者权限
体验者权限
登录
数据分析 ** **
微信支付 ** **
推广 ** **
开发管理 ** **
开发设置

权限说明

 开发者权限:可使用小程序开发者工具及对小程序的功能进行代码开发
 体验者权限:可使用体验版小程序
 登录权限:可登录小程序管理后台,无需管理员确认
 开发设置:设置小程序服务器域名、消息推送及扫描普通链接二维码打开小程序
 腾讯云管理:云开发相关设置

添加成员

微信小程序_第20张图片

在软件开发过程中,根据时间节点的不同,会产出不同的软件版本,例如:
开发者编写代码的同时,对项目代码进行自测(开发版本)
直到程序达到一个稳定可体验的状态时,开发者把体验版本给到产品经理和测试人员进行体验测试
最后修复完程序的 Bug 后,发布正式版供外部用户使用

小程序版本

版本阶段 说明
开发版本 使用开发者工具,可将代码上传到开发版本中。 开发版本只保留每人最新的一份上传的代码。点击提交审核,可将代码提交审核。开发版本可删除,不影响线上版本和审核中版本的代码。
体验版本 可以选择某个开发版本作为体验版,并且选取一份体验版。
审核中的版本 只能有一份代码处于审核中。有审核结果后可以发布到线上,也可直接重新提交审核,覆盖原审核版本。
线上版本 线上所有用户使用的代码版本,该版本代码在新版本代码发布后被覆盖更新。

小程序上线整体步骤

一个小程序的发布上线,一般要经过上传代码 -> 提交审核 -> 发布这三个步骤。

上传代码

点击开发者工具顶部工具栏中的“上传” 按钮
填写版本号以及项目备注

微信小程序_第21张图片

后台查看上传版本

微信小程序_第22张图片

提交审核

为什么需要提交审核:为了保证小程序的质量,以及符合相关的规范,小程序的发布是需要经过腾讯官方审核的。
提交审核的方式:在开发版本的列表中,点击“提交审核”按钮之后,按照页面提示填写相关的信息,就能把小程序提交到腾讯官方进行审核。

微信小程序_第23张图片

发布

审核通过之后,管理员的微信中会收到小程序通过审核的通知,此时在审核版本的列表中,点击“发布”按钮之后,即可把“审核通过”的版本发布为“线上版本”,供所有小程序用户访问和使用。

微信小程序_第24张图片

推广

相对于普通二维码来说,小程序码的优势如下:
在样式上更具辨识度和视觉冲击力
能够更加清晰地树立小程序的品牌形象
可以帮助开发者更好地推广小程序

获取小程序码的 5 个步骤:
登录小程序管理后台 -> 设置 -> 基本设置 -> 基本信息 -> 小程序码及线下物料下载

运营

在“小程序后台”查看
登录小程序管理后台
点击侧边栏的“统计”
点击相应的 tab 可以看到相关的数据
使用“小程序数据助手”查看
打开微信
搜索“小程序数据助手”
查看已发布的小程序相关的数据

能够知道如何创建小程序项目
微信开发者工具的使用、appID 的获取
能够清楚小程序项目的基本组成结构
app.js、app.json、app.wxss、pages 文件夹
能够知道小程序页面由几部分组成
wxml、wxss、json、js
能够知道小程序中常见的组件如何使用
view、text、image
能够知道小程序如何进行协同开发和发布
成员管理、发布小程序、查看运营数据

小程序模板配置

在data中定义页面数据

在页面对应的 .js 文件中,把数据定义到 data 对象中即可:

微信小程序_第25张图片

  1. Mustache 语法的格式

把data中的数据绑定到页面中渲染,使用 Mustache 语法(双大括号)将变量包起来即可。语法格式为:

image-20221128112831339

Mustache 语法的主要应用场景如下:
  绑定内容
  绑定属性
  运算(三元运算、算术运算等)

动态绑定内容

插值语法

page:({
    data:{
        info:'init data'
    }
})

 {{info}} 

三元运算

page:({
    data:{
          randomNum:Math.randomNum
    }
})


  {{randomNum >=5 ? '大于5': '小于5'}}

算数运算

page:({
    data:{
          randomNum:Math.randomNum
    }
})


  {{randomNum*100}}

事件是渲染层到逻辑层的通讯方式。通过事件可以将用户在渲染层产生的行为,反馈到逻辑层进行业务的处理。

微信小程序_第26张图片

类型 绑定方式 事件描述
tap bindtap 或 bind:tap 手指触摸后马上离开,类似于 HTML 中的 click 事件
input bindinput 或 bind:input 文本框的输入事件
change bindchange 或 bind:change 状态改变时触发

事件对象属性列表

属性 类型 说明
type String 事件类型
timeStamp Integer 页面打开到触发事件所经过的毫秒数
target Object 触发事件的组件的一些属性值集合
currentTarget Object 当前组件的一些属性值集合
detail Object 额外的信息
touches Array 触摸事件,当前停留在屏幕中的触摸点信息的数组
changedTouches Array 触摸事件,当前变化的触摸点信息的数组
  1. target 和 currentTarget 的区别

target 是触发该事件的源头组件,而 currentTarget 则是当前事件所绑定的组件。举例如下:

微信小程序_第27张图片

点击内部的按钮时,点击事件以冒泡的方式向外扩散,也会触发外层 view 的 tap 事件处理函数。
此时,对于外层的 view 来说:
e.target 指向的是触发事件的源头组件,因此,e.target 是内部的按钮组件
e.currentTarget 指向的是当前正在触发事件的那个组件,因此,e.currentTarget 是当前的 view 组件

  1. bindtap 的语法格式
在小程序中,不存在 HTML 中的 onclick 鼠标点击事件,而是通过 tap 事件来响应用户的触摸行为。
通过 bindtap,可以为组件绑定 tap 触摸事件,语法如下:


微信小程序_第28张图片

通过调用 this.setData(dataObject) 方法,可以给页面 data 中的数据重新赋值,示例如下:

  1. 在事件处理函数中为 data 中的数据赋值

微信小程序_第29张图片

事件传参

小程序中的事件传参比较特殊,不能在绑定事件的同时为事件处理函数传递参数。例如,下面的代码将不能正常工作:

因为小程序会把 bindtap 的属性值,统一当作事件名称来处理,相当于要调用一个名称为 btnHandler(123) 的事件处理函数。

image-20221128114522330

可以为组件提供 data-* 自定义属性传参,其中 * 代表的是参数的名字,示例代码如下:

最终:
info 会被解析为参数的名字
数值 2 会被解析为参数的值

image-20221128114533937

在事件处理函数中,通过 event.target.dataset.参数名 即可获取到具体参数的值,示例代码如下:

微信小程序_第30张图片

  1. bindinput 的语法格式

在小程序中,通过 input 事件来响应文本框的输入事件,语法格式如下:
通过 bindinput,可以为文本框绑定输入事件:

image-20221128114557643

在页面的 .js 文件中定义事件处理函数:

微信小程序_第31张图片

  1. 实现文本框和 data 之间的数据同步

实现步骤:
定义数据
渲染结构
美化样式
绑定 input 事件处理函数

微信小程序_第32张图片

image-20221128114648546

微信小程序_第33张图片

绑定 input 事件处理函数:

微信小程序_第34张图片

在小程序中,使用 wx:if=“{{condition}}” 来判断是否需要渲染该代码块:

image-20221128114742130

也可以用 wx:elif 和 wx:else 来添加 else 判断:

微信小程序_第35张图片

  1. 结合 使用 wx:if
如果要一次性控制多个组件的展示与隐藏,可以使用一个  标签将多个组件包装起来,并在 标签上使用 wx:if 控制属性,示例如下:

微信小程序_第36张图片

注意:  并不是一个组件,它只是一个包裹性质的容器,不会在页面中做任何渲染。

hidden

在小程序中,直接使用 hidden=“{{ condition }}” 也能控制元素的显示与隐藏:

image-20221128114845778

  1. wx:if 与 hidden 的对比
运行方式不同
 wx:if 以动态创建和移除元素的方式,控制元素的展示与隐藏
 hidden 以切换样式的方式(display: none/block;),控制元素的显示与隐藏
使用建议
 频繁切换时,建议使用 hidden
 控制条件复杂时,建议使用 wx:if 搭配 wx:elif、wx:else 进行展示与隐藏的切换
  1. wx:for

通过 wx:for 可以根据指定的数组,循环渲染重复的组件结构,语法示例如下:

微信小程序_第37张图片

默认情况下,当前循环项的索引用 index 表示;当前循环项用 item 表示。

  1. 手动指定索引和当前项的变量名*
 使用 wx:for-index 可以指定当前循环项的索引的变量名
 使用 wx:for-item 可以指定当前项的变量名
示例代码如下:

微信小程序_第38张图片

  1. wx:key 的使用

  2. 类似于 Vue 列表渲染中的 :key,小程序在实现列表渲染时,也建议为渲染出来的列表项指定唯一的 key 值,从而提高渲染的效率,示例代码如下:
    
    

微信小程序_第39张图片

WXSS (WeiXin Style Sheets)是一套样式语言,用于美化 WXML 的组件样式,类似于网页开发中的 CSS。

WXSS 具有 CSS 大部分特性,同时,WXSS 还对 CSS 进行了扩充以及修改,以适应微信小程序的开发。
与 CSS 相比,WXSS 扩展的特性有:
 rpx 尺寸单位
 @import 样式导入

image-20221128115231125

什么是rpx

rpx(responsive pixel)是微信小程序独有的,用来解决屏适配的尺寸单位。

  1. rpx 的实现原理
rpx 的实现原理非常简单:鉴于不同设备屏幕的大小不同,为了实现屏幕的自动适配,rpx 把所有设备的屏幕,在宽度上等分为 750 份(即:当前屏幕的总宽度为 750rpx)。
在较小的设备上,1rpx 所代表的宽度较小
在较大的设备上,1rpx 所代表的宽度较大

小程序在不同设备上运行的时候,会自动把 rpx 的样式单位换算成对应的像素单位来渲染,从而实现屏幕适配。

  1. rpx 与 px 之间的单位换算*
在 iPhone6 上,屏幕宽度为375px,共有 750 个物理像素,等分为 750rpx。则:
750rpx = 375px = 750 物理像素
    1rpx = 0.5px  = 1物理像素

设备 rpx换算*px (屏幕宽度/750)** px换算rpx (750/屏幕宽度)
iPhone5 1rpx = 0.42px 1px = 2.34rpx
iPhone6 1rpx = 0.5px 1px = 2rpx
iPhone6 Plus 1rpx = 0.552px 1px = 1.81rpx

官方建议:开发微信小程序时,设计师可以用 iPhone6 作为视觉稿的标准。
开发举例:在 iPhone6 上如果要绘制宽100px,高20px的盒子,换算成rpx单位,宽高分别为 200rpx 和 40rpx。

什么是样式导入

使用 WXSS 提供的 @import 语法,可以导入外联的样式表。

  1. @import 的语法格式

@import 后跟需要导入的外联样式表的相对路径,用 ; 表示语句结束。示例如下:

微信小程序_第40张图片

  1. 全局样式

定义在 app.wxss 中的样式为全局样式,作用于每一个页面。

  1. 局部样式
在页面的 .wxss 文件中定义的样式为局部样式,只作用于当前页面。

注意:
当局部样式和全局样式冲突时,根据就近原则,局部样式会覆盖全局样式
当局部样式的权重大于或等于全局样式的权重时,才会覆盖全局的样式

全局配置文件常用配置项

小程序根目录下的 app.json 文件是小程序的全局配置文件。常用的配置项如下:
 pages
记录当前小程序所有页面的存放路径
 window
全局设置小程序窗口的外观
 tabBar
设置小程序底部的  tabBar 效果
 style
是否启用新版的组件样式

小程序窗口组成部分

微信小程序_第41张图片

常用配置项

属性名 类型 默认值 说明
navigationBarTitleText String 字符串 导航栏标题文字内容
navigationBarBackgroundColor HexColor #000000 导航栏背景颜色,如 #000000
navigationBarTextStyle String white 导航栏标题颜色,仅支持 black / white
backgroundColor HexColor #ffffff 窗口的背景色
backgroundTextStyle String dark 下拉 loading 的样式,仅支持 dark / light
enablePullDownRefresh Boolean false 是否全局开启下拉刷新
onReachBottomDistance Number 50 页面上拉触底事件触发时距页面底部距离,单位为px
  1. 设置导航栏的标题

设置步骤:app.json -> window -> navigationBarTitleText
需求:把导航栏上的标题,从默认的 “WeChat”修改为“黑马程序员”,效果如图所示:

微信小程序_第42张图片

背景色

设置步骤:app.json -> window -> navigationBarBackgroundColor
需求:把导航栏标题的背景色,从默认的 #fff 修改为 #2b4b6b ,效果如图所示:

微信小程序_第43张图片

  1. 设置导航栏的标题颜色
设置步骤:app.json -> window -> navigationBarTextStyle
需求:把导航栏上的标题颜色,从默认的 black 修改为 white ,效果如图所示:

微信小程序_第44张图片

注意: navigationBarTextStyle 的可选值只有 black 和 white
  1. 全局开启下拉刷新功能
概念:下拉刷新是移动端的专有名词,指的是通过手指在屏幕上的下拉滑动操作,从而重新加载页面数据的行为。
设置步骤:app.json -> window -> 把 enablePullDownRefresh 的值设置为 true

注意:在 app.json 中启用下拉刷新功能,会作用于每个小程序页面!


  1. 设置下拉刷新时窗口的背景色
当全局开启下拉刷新功能之后,默认的窗口背景为白色。如果自定义下拉刷新窗口背景色,设置步骤为: app.json -> window -> 为 backgroundColor 指定16进制的颜色值 #efefef。效果如下:

微信小程序_第45张图片

  1. 设置下拉刷新时 loading 的样式
当全局开启下拉刷新功能之后,默认窗口的 loading 样式为白色,如果要更改 loading 样式的效果,设置步骤为 app.json -> window -> 为 backgroundTextStyle 指定 dark 值。效果如下:
注意: backgroundTextStyle 的可选值只有 light 和 dark

微信小程序_第46张图片

  1. 设置上拉触底的距离
概念:上拉触底是移动端的专有名词,通过手指在屏幕上的上拉滑动操作,从而加载更多数据的行为。
设置步骤: app.json -> window -> 为 onReachBottomDistance 设置新的数值

注意:默认距离为50px,如果没有特殊需求,建议使用默认值即可。

全局配置tabBar

tabBar 是移动端应用常见的页面效果,用于实现多页面的快速切换。小程序中通常将其分为:
底部 tabBar
顶部 tabBar


注意:
tabBar中只能配置最少 2 个、最多 5 个 tab 页签
当渲染顶部 tabBar 时,不显示 icon,只显示文本

微信小程序_第47张图片

  1. tabBar 的 6 个组成部分
 backgroundColor:tabBar 的背景色
 selectedIconPath:选中时的图片路径
 borderStyle:tabBar 上边框的颜色
 iconPath:未选中时的图片路径
 selectedColor:tab 上的文字选中时的颜色
 color:tab 上文字的默认(未选中)颜色

微信小程序_第48张图片

属性 类型 必填 默认值 描述
position String bottom tabBar 的位置,仅支持 bottom/top
borderStyle String black tabBar 上边框的颜色,仅支持 black/white
color HexColor tab 上文字的默认(未选中)颜色
selectedColor HexColor tab 上的文字选中时的颜色
backgroundColor HexColor tabBar 的背景色
list Array tab 页签的列表,最少 2 个、最多 5 个 tab
  1. 每个 tab 项的配置选项
属性 类型 必填 描述
pagePath String 页面路径,页面必须在 pages 中预先定义
text String tab 上显示的文字
iconPath String 未选中时的图标路径;当 postion 为 top 时,不显示 icon
selectedIconPath String 选中时的图标路径;当 postion 为 top 时,不显示 icon

根据资料中提供的小图标、在小程序中配置如图所示的 tabBar 效果:

微信小程序_第49张图片

拷贝图标资源
新建 3 个对应的 tab 页面
配置 tabBar 选项

  1. 步骤1 - 拷贝图标资源
把资料目录中的 images 文件夹,拷贝到小程序项目根目录中
将需要用到的小图标分为 3 组,每组两个,其中:
图片名称中包含 -active 的是选中之后的图标
图片名称中不包含 -active 的是默认图标
        截图如下:

微信小程序_第50张图片

  1. 步骤2 - 新建 3 个对应的 tab 页面

微信小程序_第51张图片

其中,home 是首页,message 是消息页面,contact 是联系我们页面。

打开 app.json 配置文件,和 pages、window 平级,新增 tabBar 节点
tabBar 节点中,新增 list 数组,这个数组中存放的,是每个 tab 项的配置对象
在 list 数组中,新增每一个 tab 项的配置对象。对象中包含的属性如下:
pagePath 指定当前 tab 对应的页面路径【必填】
text 指定当前 tab 上按钮的文字【必填】
iconPath 指定当前 tab 未选中时候的图片路径【可选】
selectedIconPath 指定当前 tab 被选中后高亮的图片路径【可选】

  1. 步骤3 - 配置 tabBar 选项
打开 app.json 配置文件,和 pages、window 平级,新增 tabBar 节点
tabBar 节点中,新增 list 数组,这个数组中存放的,是每个 tab 项的配置对象
在 list 数组中,新增每一个 tab 项的配置对象。对象中包含的属性如下:
 pagePath 指定当前 tab 对应的页面路径【必填】
 text 指定当前 tab 上按钮的文字【必填】
 iconPath 指定当前 tab 未选中时候的图片路径【可选】
 selectedIconPath 指定当前 tab 被选中后高亮的图片路径【可选】

完整配置代码

微信小程序_第52张图片

{
    "pages": [
        "pages/home/home",
        "pages/messages/messages",
        "pages/contact/contact",
        "pages/shoplist/shoplist"
    ],
    "window": {
        "backgroundTextStyle": "light",
        "navigationBarBackgroundColor": "#2b4b6b",
        "navigationBarTitleText": "本地生活",
        "navigationBarTextStyle": "white"
       
    },
   "tabBar": {
     "list": [
       {
       "pagePath": "pages/home/home",
       "text": "首页",
       "iconPath": "images/home.png",
       "selectedIconPath": "images/home-active.png"
     },
     {
      "pagePath": "pages/messages/messages",
      "text": "消息",
      "iconPath": "images/message.png",
      "selectedIconPath": "images/message-active.png"
    },
     {
      "pagePath": "pages/contact/contact",
      "text": "联系我们",
      "iconPath": "images/contact.png",
      "selectedIconPath": "images/contact-active.png"
    }
   
    ]
   },
    "style": "v2",
    "sitemapLocation": "sitemap.json"
}

页面配置

小程序中,每个页面都有自己的 .json 配置文件,用来对当前页面的窗口外观、页面效果等进行配置。

  1. 页面配置和全局配置的关系
小程序中,app.json 中的 window 节点,可以全局配置小程序中每个页面的窗口表现。
如果某些小程序页面想要拥有特殊的窗口表现,此时,“页面级别的 .json 配置文件”就可以实现这种需求。

注意:当页面配置与全局配置冲突时,根据就近原则,最终的效果以页面配置为准。

页面常用配置项

属性 类型 默认值 描述
navigationBarBackgroundColor HexColor #000000 当前页面导航栏背景颜色,如 #000000
navigationBarTextStyle String white 当前页面导航栏标题颜色,仅支持 black / white
navigationBarTitleText String 当前页面导航栏标题文字内容
backgroundColor HexColor #ffffff 当前页面窗口的背景色
backgroundTextStyle String dark 当前页面下拉 loading 的样式,仅支持 dark / light
enablePullDownRefresh Boolean false 是否为当前页面开启下拉刷新的效果
onReachBottomDistance Number 50 页面上拉触底事件触发时距页面底部距离,单位为 px

出于安全性方面的考虑,小程序官方对数据接口的请求做出了如下两个限制:
只能请求 HTTPS 类型的接口
必须将接口的域名添加到信任列表中

微信小程序_第53张图片

  1. 配置 request 合法域名

  2. 需求描述:假设在自己的微信小程序中,希望请求 https://www.escook.cn/ 域名下的接口
    配置步骤:登录微信小程序管理后台 -> 开发 -> 开发设置 -> 服务器域名 -> 修改 request 合法域名
    
    注意事项:
    域名只支持 https 协议
    域名不能使用 IP 地址或 localhost
    域名必须经过 ICP 备案
    服务器域名一个月内最多可申请 5 次修改
    
    
    
  3. 发起 GET 请求

调用微信小程序提供的 wx.request() 方法,可以发起 GET 数据请求,示例代码如下:

微信小程序_第54张图片

  1. 发起 POST 请求

微信小程序_第55张图片

调用微信小程序提供的 wx.request() 方法,可以发起 POST 数据请求,示例代码如下:

  1. 在页面刚加载时请求数据
    在很多情况下,我们需要在页面刚加载的时候,自动请求一些初始化的数据。此时需要在页面的 onLoad 事件
    中调用获取数据的函数,示例代码如下
    

微信小程序_第56张图片

  1. 跳过 request 合法域名校验
如果后端程序员仅仅提供了 http 协议的接口、暂时没有提供 https 
协议的接口。
此时为了不耽误开发的进度,我们可以在微信开发者工具中,临时
开启「开发环境不校验请求域名、TLS 版本及 HTTPS 证书」选项,
跳过 request 合法域名的校验。
注意:
跳过 request 合法域名校验的选项,仅限在开发与调试阶段使用!


微信小程序_第57张图片

  1. 关于跨域和 Ajax 的说明

    跨域问题只存在于基于浏览器的 Web 开发中。由于小程序的宿主环境不是浏览器,而是微信客户端,所以小
    程序中不存在跨域的问题。
    Ajax 技术的核心是依赖于浏览器中的 XMLHttpRequest 这个对象,由于小程序的宿主环境是微信客户端,所
    以小程序中不能叫做“发起 Ajax 请求”,而是叫做“发起网络数据请求”。
    

案例

本地生活

① 新建项目并梳理项目结构
② 配置导航栏效果
③ 配置 tabBar 效果
④ 实现轮播图效果
⑤ 实现九宫格效果
⑥ 实现图片布局


微信小程序_第58张图片

2. 接口地址
① 获取轮播图数据列表的接口
⚫ 【GET】https://www.escook.cn/slides
② 获取九宫格数据列表的接口
⚫ 【GET】https://www.escook.cn/categories

总结

① 能够使用 WXML 模板语法渲染页面结构
⚫ wx:if、wx:elif、wx:else、hidden、wx:for、wx:key
② 能够使用 WXSS 样式美化页面结构
⚫ rpx 尺寸单位、@import 样式导入、全局样式和局部样式
③ 能够使用 app.json 对小程序进行全局性配置
⚫ pages、window、tabBar、style
④ 能够使用 page.json 对小程序页面进行个性化配置
⚫ 对单个页面进行个性化配置、就近原则
⑤ 能够知道如何发起网络数据请求
⚫ wx.request() 方法、onLoad() 事件

视图与逻辑

1. 什么是页面导航
页面导航指的是页面之间的相互跳转。例如,浏览器中实现页面导航的方式有如下两种:
①  链接
② location.href
2. 小程序中实现页面导航的两种方式
① 声明式导航
⚫ 在页面上声明一个  导航组件
⚫ 通过点击  组件实现页面跳转
② 编程式导航
⚫ 调用小程序的导航 API,实现页面的跳转
1. 导航到 tabBar 页面
tabBar 页面指的是被配置为 tabBar 的页面。
在使用  组件跳转到指定的 tabBar 页面时,需要指定 url 属性和 open-type 属性,其中:
⚫ url 表示要跳转的页面的地址,必须以 / 开头
⚫ open-type 表示跳转的方式,必须为 switchTab

image-20221201161104966

2. 导航到非 tabBar 页面
非 tabBar 页面指的是没有被配置为 tabBar 的页面。
在使用  组件跳转到普通的非 tabBar 页面时,则需要指定 url 属性和 open-type 属性,其中:
⚫ url 表示要跳转的页面的地址,必须以 / 开头
⚫ open-type 表示跳转的方式,必须为 navigate

image-20221201161122613

注意:为了简便,在导航到非 tabBar 页面时,open-type=“navigate” 属性可以省略。

  1. 后退导航
如果要后退到上一页面或多级页面,则需要指定 open-type 属性和 delta 属性,其中:
⚫ open-type 的值必须是 navigateBack,表示要进行后退导航
⚫ delta 的值必须是数字,表示要后退的层级
示例代码如下

image-20221201161213906

注意:为了简便,如果只是后退到上一页面,则可以省略 delta 属性,因为其默认值就是 1。

  1. 导航到 tabBar 页面
    调用 wx.switchTab(Object object) 方法,可以跳转到 tabBar 页面。其中 Object 参数对象的属性列表如下:
    
属性 类型 是否必选 说明
url string 需要跳转的 tabBar 页面的路径,路径后不能带参数
success function 接口调用成功的回调函数
fail function 接口调用失败的回调函数
complete function 接口调用结束的回调函数(调用成功、失败都会执行)
  1. 导航到 tabBar 页面
    示例代码如下

微信小程序_第59张图片

  1. 导航到非 tabBar 页面
    调用 wx.navigateTo(Object object) 方法,可以跳转到非 tabBar 的页面。其中 Object 参数对象的属性列表
    如下:
属性 类型 是否必选 说明
url string 需要跳转到的非 tabBar 页面的路径,路径后可以带参数
success function 接口调用成功的回调函数
fail function 接口调用失败的回调函数
complete function 接口调用结束的回调函数(调用成功、失败都会执行)
  1. 导航到非 tabBar 页面
    示例代码如下:

微信小程序_第60张图片

  1. 后退导航
    调用 wx.navigateBack(Object object) 方法,可以返回上一页面或多级页面。其中 Object 参数对象可选的
    属性列表如下:
属性 类型 默认值 是否必选 说明
delta number 1 返回的页面数,如果 delta 大于现有页面数,则返回到首页
success function 接口调用成功的回调函数
fail function 接口调用失败的回调函数
complete function 接口调用结束的回调函数(调用成功、失败都会执行)

后退导航

微信小程序_第61张图片

  1. 声明式导航传参
    navigator 组件的 url 属性用来指定将要跳转到的页面的路径。同时,路径的后面还可以携带参数:
    ⚫ 参数与路径之间使用 ? 分隔
    ⚫ 参数键与参数值用 = 相连
    ⚫ 不同参数用 & 分隔
    代码示例如下:

image-20221201165115771

  1. 编程式导航传参
    调用 wx.navigateTo(Object object) 方法跳转页面时,也可以携带参数,代码示例如下:

微信小程序_第62张图片

  1. 在 onLoad 中接收导航参数
    通过声明式导航传参或编程式导航传参所携带的参数,可以直接在 onLoad 事件中直接获取到,示例代码如下

微信小程序_第63张图片

  1. 什么是下拉刷新
    下拉刷新是移动端的专有名词,指的是通过手指在屏幕上的下拉滑动操作,从而重新加载页面数据的行为

  2. 启用下拉刷新

启用下拉刷新有两种方式:
全局开启下拉刷新
在 app.json 的 window 节点中,将 enablePullDownRefresh 设置为 true
局部开启下拉刷新
在页面的 .json 配置文件中,将 enablePullDownRefresh 设置为 true

在实际开发中,推荐使用第 2 种方式,为需要的页面单独开启下拉刷新的效果。
3. 配置下拉刷新窗口的样式
在全局或页面的 .json 配置文件中,通过 backgroundColor 和 backgroundTextStyle 来配置下拉刷新窗口
的样式,其中:
⚫ backgroundColor 用来配置下拉刷新窗口的背景颜色,仅支持16 进制的颜色值
⚫ backgroundTextStyle 用来配置下拉刷新 loading 的样式,仅支持 dark 和 light
4. 监听页面的下拉刷新事件
在页面的 .js 文件中,通过 onPullDownRefresh() 函数即可监听当前页面的下拉刷新事件。
例如,在页面的 wxml 中有如下的 UI 结构,点击按钮可以让 count 值自增 +1:

微信小程序_第64张图片

  1. 监听页面的下拉刷新事件
    在触发页面的下拉刷新事件的时候,如果要把 count 的值重置为 0,示例代码如下

微信小程序_第65张图片

  1. 停止下拉刷新的效果
    当处理完下拉刷新后,下拉刷新的 loading 效果会一直显示,不会主动消失,所以需要手动隐藏下拉刷新的
    loading 效果。此时,调用 wx.stopPullDownRefresh() 可以停止当前页面的下拉刷新。示例代码如下:

微信小程序_第66张图片

  1. 什么是上拉触底
    上拉触底是移动端的专有名词,通过手指在屏幕上的上拉滑动操作,从而加载更多数据的行为。

  2. 监听页面的上拉触底事件
    在页面的 .js 文件中,通过 onReachBottom() 函数即可监听当前页面的上拉触底事件。示例代码如下:

微信小程序_第67张图片

  1. 配置上拉触底距离

    3. 上拉触底距离指的是触发上拉触底事件时,滚动条距离页面底部的距离。
       可以在全局或页面的 .json 配置文件中,通过 onReachBottomDistance 属性来配置上拉触底的距离。
       小程序默认的触底距离是 50px,在实际开发中,可以根据自己的需求修改这个默认值
    
    
    

案例微信小程序_第68张图片

2. 案例的实现步骤
① 定义获取随机颜色的方法
② 在页面加载时获取初始数据
③ 渲染 UI 结构并美化页面效果
④ 在上拉触底时调用获取随机颜色的方法
⑤ 添加 loading 提示效果
⑥ 对上拉触底进行节流处理

随机获取颜色

微信小程序_第69张图片

页面加载时获取初始数据

微信小程序_第70张图片

渲染UI结构并美化页面效果

微信小程序_第71张图片

上拉触底获取随机颜色

微信小程序_第72张图片

  1. 步骤5 - 添加 loading 提示效果

微信小程序_第73张图片

3. 步骤6 - 对上拉触底进行节流处理
① 在 data 中定义 isloading 节流阀
⚫ false 表示当前没有进行任何数据请求
⚫ true 表示当前正在进行数据请求
② 在 getColors() 方法中修改 isloading 节流阀的值
⚫ 在刚调用 getColors 时将节流阀设置 true
⚫ 在网络请求的 complete 回调函数中,将节流阀重置为 false
③ 在 onReachBottom 中判断节流阀的值,从而对数据请求进行节流控制
⚫ 如果节流阀的值为 true,则阻止当前请求
⚫ 如果节流阀的值为 false,则发起数据请求

自定义编译模式

微信小程序_第74张图片

生命周期

生命周期(Life Cycle)是指一个对象从创建 -> 运行 -> 销毁的整个阶段,强调的是一个时间段。例如:
⚫ 张三出生,表示这个人生命周期的开始
⚫ 张三离世,表示这个人生命周期的结束
⚫ 中间张三的一生,就是张三的生命周期
我们可以把每个小程序运行的过程,也概括为生命周期:
⚫ 小程序的启动,表示生命周期的开始
⚫ 小程序的关闭,表示生命周期的结束
⚫ 中间小程序运行的过程,就是小程序的生命周期
2. 生命周期的分类
在小程序中,生命周期分为两类,分别是:
① 应用生命周期
⚫ 特指小程序从启动 -> 运行 -> 销毁的过程
② 页面生命周期
⚫ 特指小程序中,每个页面的加载 -> 渲染 -> 销毁的过程
其中,页面的生命周期范围较小,应用程序的生命周期范围较大,如图所示:

image-20221201165917539

3. 什么是生命周期函数
生命周期函数:是由小程序框架提供的内置函数,会伴随着生命周期,自动按次序执行。
生命周期函数的作用:允许程序员在特定的时间点,执行某些特定的操作。例如,页面刚加载的时候,可以在
onLoad 生命周期函数中初始化页面的数据。
注意:生命周期强调的是时间段,生命周期函数强调的是时间点。
4. 生命周期函数的分类
小程序中的生命周期函数分为两类,分别是:
① 应用的生命周期函数
⚫ 特指小程序从启动 -> 运行 -> 销毁期间依次调用的那些函数
② 页面的生命周期函数
⚫ 特指小程序中,每个页面从加载 -> 渲染 -> 销毁期间依次调用的那些函数
  1. 应用的生命周期函数
    小程序的应用生命周期函数需要在 app.js 中进行声明,示例代码如下:

微信小程序_第75张图片

  1. 页面的生命周期函数
    小程序的页面生命周期函数需要在页面的 .js 文件中进行声明,示例代码如下:

微信小程序_第76张图片

wxs脚本

  1. 什么是 wxs
    WXS(WeiXin Script)是小程序独有的一套脚本语言,结合 WXML,可以构建出页面的结构。

  2. wxs 的应用场景
    wxml 中无法调用在页面的 .js 中定义的函数,但是,wxml 中可以调用 wxs 中定义的函数。因此,小程序中
    wxs 的典型应用场景就是“过滤器”

  3. wxs 和 JavaScript 的关系*
    虽然 wxs 的语法类似于 JavaScript,但是 wxs 和 JavaScript 是完全不同的两种语言:
    ① wxs 有自己的数据类型
    ⚫ number 数值类型、string 字符串类型、boolean 布尔类型、object 对象类型、
    ⚫ function 函数类型、array 数组类型、 date 日期类型、 regexp 正则
    ② wxs 不支持类似于 ES6 及以上的语法形式
    ⚫ 不支持:let、const、解构赋值、展开运算符、箭头函数、对象属性简写、etc…
    ⚫ 支持:var 定义变量、普通 function 函数等类似于 ES5 的语法
    ③ wxs 遵循 CommonJS 规范
    ⚫ module 对象
    ⚫ require() 函数
    ⚫ module.exports 对象

  4. 内嵌 wxs 脚本
    wxs 代码可以编写在 wxml 文件中的 标签内,就像 Javascript 代码可以编写在 html 文件中的

微信小程序_第77张图片

  1. 定义外联的 wxs 脚本
    wxs 代码还可以编写在以 .wxs 为后缀名的文件内,就像 javascript 代码可以编写在以 .js 为后缀名的文件中
    一样。示例代码如下:

微信小程序_第78张图片

  1. 使用外联的 wxs 脚本
    在 wxml 中引入外联的 wxs 脚本时,必须为 标签添加 module 和 src 属性,其中:
    ⚫ module 用来指定模块的名称
    ⚫ src 用来指定要引入的脚本的路径,且必须是相对路径
    示例代码如下

微信小程序_第79张图片

  1. 与 JavaScript 不同
    为了降低 wxs(WeiXin Script)的学习成本, wxs 语言在设计时借大量鉴了 JavaScript 的语法。但是本质上,
    wxs 和 JavaScript 是完全不同的两种语言

  2. 不能作为组件的事件回调
    wxs 典型的应用场景就是“过滤器”,经常配合 Mustache 语法进行使用,例如

image-20221201170228879

但是,在 wxs 中定义的函数不能作为组件的事件回调函数。例如,下面的用法是错误的:

image-20221201170238036

  1. 隔离性
    隔离性指的是 wxs 的运行环境和其他 JavaScript 代码是隔离的。体现在如下两方面:
    ① wxs 不能调用 js 中定义的函数
    ② wxs 不能调用小程序提供的 API

  2. 性能好
    ⚫ 在 iOS 设备上,小程序内的 WXS 会比 JavaScript 代码快 2 ~ 20 倍
    ⚫ 在 android 设备上,二者的运行效率无差异

列表页面

微信小程序_第80张图片

  1. 页面导航传参

  2. 上拉触底加载下一页面的数据

  3. 下拉刷新列表数据

  4. 列表页面的 API 接口
    以分页的形式,加载指定分类下商铺列表的数据:
    ① 接口地址
    ⚫ https://www.escook.cn/categories/:cate_id/shops
    ⚫ URL 地址中的 :cate_id 是动态参数,表示分类的 Id
    ② 请求方式
    ⚫ GET 请求
    ③ 请求参数
    ⚫ _page 表示请求第几页的数据
    ⚫ _limit 表示每页请求几条数据

  5. 判断是否还有下一页数据

    如果下面的公式成立,则证明没有下一页数据了:
    页码值 * 每页显示多少条数据 >= 总数据条数
    page * pageSize >= total
    案例1:总共有 77 条数据,如果每页显示 10 条数据,则总共分为 8 页,其中第 8 页只有 7 条数据
    page(7)* pageSize(10) >= total(77)
    page(8)* pageSize(10) >= total(77)
    案例2:总共有 80 条数据,如果每页显示 10 条数据,则总共分为 8 页,其中第 8 页面有 10 条数据
    page(7)* pageSize(10) >= total(80)
    page(8)* pageSize(10) >= total(80)
    

总结

① 能够知道如何实现页面之间的导航跳转
⚫ 声明式导航、编程式导航
② 能够知道如何实现下拉刷新效果
⚫ enablePullDownRefresh、onPullDownRefresh
③ 能够知道如何实现上拉加载更多效果
⚫ onReachBottomDistance、onReachBottom
④ 能够知道小程序中常用的生命周期函数
⚫ 应用生命周期函数:onLaunch, onShow, onHide
⚫ 页面生命周期函数:onLoad, onShow, onReady, onHide, onUnload

基础加强

  1. 创建组件
    ① 在项目的根目录中,鼠标右键,创建 components -> test 文件夹
    ② 在新建的 components -> test 文件夹上,鼠标右键,点击“新建 Component”
    ③ 键入组件的名称之后回车,会自动生成组件对应的 4 个文件,后缀名分别为 .js,.json, .wxml 和 .wxss
    注意:为了保证目录结构的清晰,建议把不同的组件,存放到单独目录中,例如:

微信小程序_第81张图片

  1. 引用组件
    组件的引用方式分为“局部引用”和“全局引用”,顾名思义:
    ⚫ 局部引用:组件只能在当前被引用的页面内使用
    ⚫ 全局引用:组件可以在每个小程序页面中使用

  2. 局部引用组件
    在页面的 .json 配置文件中引用组件的方式,叫做“局部引用”。示例代码如下:

微信小程序_第82张图片

  1. 全局引用组件
    在 app.json 全局配置文件中引用组件的方式,叫做“全局引用”。示例代码如下:

微信小程序_第83张图片

  1. 全局引用 VS 局部引用
    根据组件的使用频率和范围,来选择合适的引用方式:
    ⚫ 如果某组件在多个页面中经常被用到,建议进行“全局引用”
    ⚫ 如果某组件只在特定的页面中被用到,建议进行“局部引用”

  2. 组件和页面的区别
    从表面来看,组件和页面都是由 .js、.json、.wxml 和 .wxss 这四个文件组成的。但是,组件和页面的 .js 与
    .json 文件有明显的不同:
    ⚫ 组件的 .json 文件中需要声明 “component”: true 属性
    ⚫ 组件的 .js 文件中调用的是 Component() 函数
    ⚫ 组件的事件处理函数需要定义到 methods 节点中

  3. 组件样式隔离
    默认情况下,自定义组件的样式只对当前组件生效,不会影响到组件之外的
    UI 结构,如图所示:
    ⚫ 组件 A 的样式不会影响组件 C 的样式
    ⚫ 组件 A 的样式不会影响小程序页面的样式
    ⚫ 小程序页面的样式不会影响组件 A 和 C 的样式
    好处:
    ① 防止外界的样式影响组件内部的样式
    ② 防止组件的样式破坏外界的样式

微信小程序_第84张图片

  1. 组件样式隔离的注意点
    ⚫ app.wxss 中的全局样式对组件无效
    ⚫ 只有 class 选择器会有样式隔离效果,id 选择器、属性选择器、标签选择器不受样式隔离的影响
    建议:在组件和引用组件的页面中建议使用 class 选择器,不要使用 id、属性、标签选择器!

  2. 修改组件的样式隔离选项
    默认情况下,自定义组件的样式隔离特性能够防止组件内外样式互相干扰的问题。但有时,我们希望在外界能
    够控制组件内部的样式,此时,可以通过 styleIsolation 修改组件的样式隔离选项,用法如下:

微信小程序_第85张图片

  1. styleIsolation 的可选值
可选值 默认值 描述
isolated 表示启用样式隔离,在自定义组件内外,使用 class 指定的样式将不会相互影响
apply-shared 表示页面 wxss 样式将影响到自定义组件,但自定义组件 wxss 中指定的样式不会影响页面
shared 表示页面 wxss 样式将影响到自定义组件,自定义组件 wxss 中指定的样式也会影响页面和其他设置了 apply-shared 或 shared 的自定义组件
  1. data 数据
    在小程序组件中,用于组件模板渲染的私有数据,需要定义到 data 节点中,示例如下:

微信小程序_第86张图片

  1. methods 方法
    在小程序组件中,事件处理函数和自定义方法需要定义到 methods 节点中,示例代码如下:

微信小程序_第87张图片

  1. properties 属性
    在小程序组件中,properties 是组件的对外属性,用来接收外界传递到组件中的数据,示例代码如下:

微信小程序_第88张图片

  1. data 和 properties 的区别
    在小程序的组件中,properties 属性和 data 数据的用法相同,它们都是可读可写的,只不过:
    ⚫ data 更倾向于存储组件的私有数据
    ⚫ properties 更倾向于存储外界传递到组件中的数据

微信小程序_第89张图片

  1. 使用 setData 修改 properties 的值
    由于 data 数据和 properties 属性在本质上没有任何区别,因此 properties 属性的值也可以用于页面渲染,
    或使用 setData 为 properties 中的属性重新赋值,示例代码如下:

微信小程序_第90张图片

  1. 什么是数据监听器
    数据监听器用于监听和响应任何属性和数据字段的变化,从而执行特定的操作。它的作用类似于 vue 中的
    watch 侦听器。在小程序组件中,数据监听器的基本语法格式如下:

微信小程序_第91张图片

  1. 数据监听器的基本用法
    组件的 UI 结构如下:

微信小程序_第92张图片

  1. 数据监听器的基本用法
    组件的 .js 文件代码如下:

微信小程序_第93张图片

  1. 监听对象属性的变化
    数据监听器支持监听对象中单个或多个属性的变化,示例语法如下

微信小程序_第94张图片

image-20221201171751945

  1. 渲染 UI 结构

微信小程序_第95张图片

  1. 定义 button 的事件处理函数

image-20221201171818716

  1. 监听对象中指定属性的变化

微信小程序_第96张图片

  1. 监听对象中所有属性的变化
    如果某个对象中需要被监听的属性太多,为了方便,可以使用通配符 ** 来监听对象中所有属性的变化,示例
    代码如下:

微信小程序_第97张图片

  1. 什么是纯数据字段
    概念:纯数据字段指的是那些不用于界面渲染的 data 字段。
    应用场景:例如有些情况下,某些 data 中的字段既不会展示在界面上,也不会传递给其他组件,仅仅在当前
    组件内部使用。带有这种特性的 data 字段适合被设置为纯数据字段。
    好处:纯数据字段有助于提升页面更新的性能。

  2. 使用规则
    在 Component 构造器的 options 节点中,指定 pureDataPattern 为一个正则表达式,字段名符合这个正则
    表达式的字段将成为纯数据字段,示例代码如下:

image-20221201171856899

  1. 使用纯数据字段改造数据监听器案例

微信小程序_第98张图片

  1. 组件全部的生命周期函数
    小程序组件可用的全部生命周期如下表所示:
生命周期函数 参数 描述说明
created 在组件实例刚刚被创建时执行
attached 在组件实例进入页面节点树时执行
ready 在组件在视图层布局完成后执行
moved 在组件实例被移动到节点树另一个位置时执行
detached 在组件实例被从页面节点树移除时执行
error Object Error 每当组件方法抛出错误时执行
  1. 组件主要的生命周期函数
    在小程序组件中,最重要的生命周期函数有 3 个,分别是 created、attached、detached。它们各自的特点
    如下:
    ① 组件实例刚被创建好的时候,created 生命周期函数会被触发
    ⚫ 此时还不能调用 setData
    ⚫ 通常在这个生命周期函数中,只应该用于给组件的 this 添加一些自定义的属性字段
    ② 在组件完全初始化完毕、进入页面节点树后, attached 生命周期函数会被触发
    ⚫ 此时, this.data 已被初始化完毕
    ⚫ 这个生命周期很有用,绝大多数初始化的工作可以在这个时机进行(例如发请求获取初始数据)
    ③ 在组件离开页面节点树后, detached 生命周期函数会被触发
    ⚫ 退出一个页面时,会触发页面内每个自定义组件的 detached 生命周期函数
    ⚫ 此时适合做一些清理性质的工作

  2. lifetimes 节点
    在小程序组件中,生命周期函数可以直接定义在 Component 构造器的第一级参数中,可以在 lifetimes 字段
    内进行声明(这是推荐的方式,其优先级最高)。示例代码如下:

微信小程序_第99张图片

  1. 什么是组件所在页面的生命周期
    有时,自定义组件的行为依赖于页面状态的变化,此时就需要用到组件所在页面的生命周期。
    例如:每当触发页面的 show 生命周期函数的时候,我们希望能够重新生成一个随机的 RGB 颜色值。
    在自定义组件中,组件所在页面的生命周期函数有如下 3 个,分别是:
生命周期函数 参数 描述
show 组件所在的页面被展示时执行
hide 组件所在的页面被隐藏时执行
resize Object Size 组件所在的页面尺寸变化时执行
  1. pageLifetimes 节点
    组件所在页面的生命周期函数,需要定义在 pageLifetimes 节点中,示例代码如下:

微信小程序_第100张图片

  1. 生成随机的 RGB 颜色值

微信小程序_第101张图片

微信小程序_第102张图片

  1. 什么是插槽
    在自定义组件的 wxml 结构中,可以提供一个 节点(插槽),用于承载组件使用者提供的 wxml 结构。

微信小程序_第103张图片

  1. 单个插槽
    在小程序中,默认每个自定义组件中只允许使用一个 进行占位,这种个数上的限制叫做单个插槽。

微信小程序_第104张图片

  1. 启用多个插槽
    在小程序的自定义组件中,需要使用多 插槽时,可以在组件的 .js 文件中,通过如下方式进行启用。
    示例代码如下:

微信小程序_第105张图片

  1. 定义多个插槽
    可以在组件的 .wxml 中使用多个 标签,以不同的 name 来区分不同的插槽。示例代码如下:

微信小程序_第106张图片

  1. 使用多个插槽
    在使用带有多个插槽的自定义组件时,需要用 slot 属性来将节点插入到不同的 中。示例代码如下:

微信小程序_第107张图片

  1. 父子组件之间通信的 3 种方式
    ① 属性绑定
    ⚫ 用于父组件向子组件的指定属性设置数据,仅能设置 JSON 兼容的数据
    ② 事件绑定
    ⚫ 用于子组件向父组件传递数据,可以传递任意数据
    ③ 获取组件实例
    ⚫ 父组件还可以通过 this.selectComponent() 获取子组件实例对象
    ⚫ 这样就可以直接访问子组件的任意数据和方法

  2. 属性绑定
    属性绑定用于实现父向子传值,而且只能传递普通类型的数据,无法将方法传递给子组件。父组件的示例代码
    如下:

微信小程序_第108张图片

  1. 属性绑定
    子组件在 properties 节点中声明对应的属性并使用。示例代码如下:

微信小程序_第109张图片

  1. 事件绑定
    事件绑定用于实现子向父传值,可以传递任何类型的数据。使用步骤如下:
    ① 在父组件的 js 中,定义一个函数,这个函数即将通过自定义事件的形式,传递给子组件
    ② 在父组件的 wxml 中,通过自定义事件的形式,将步骤 1 中定义的函数引用,传递给子组件
    ③ 在子组件的 js 中,通过调用 this.triggerEvent(‘自定义事件名称’, { /* 参数对象 */ }) ,将数据发送到父组件
    ④ 在父组件的 js 中,通过 e.detail 获取到子组件传递过来的数据

事件绑定

步骤1:在父组件的 js 中,定义一个函数,这个函数即将通过自定义事件的形式,传递给子组件。

微信小程序_第110张图片

步骤2:在父组件的 wxml 中,通过自定义事件的形式,将步骤 1 中定义的函数引用,传递给子组件。

微信小程序_第111张图片

步骤3:在子组件的 js 中,通过调用 this.triggerEvent(‘自定义事件名称’, { /* 参数对象 */ }) ,将数据发送到父组件。

微信小程序_第112张图片

步骤4:在父组件的 js 中,通过 e.detail 获取到子组件传递过来的数据。

微信小程序_第113张图片

可在父组件里调用 this.selectComponent(“id或class选择器”) ,获取子组件的实例对象,从而直接访问子组
件的任意数据和方法。调用时需要传入一个选择器,例如 this.selectComponent(“.my-component”)。

微信小程序_第114张图片

  1. 什么是 behaviors
    behaviors 是小程序中,用于实现组件间代码共享的特性,类似于 Vue.js 中的 “mixins”

微信小程序_第115张图片

  1. behaviors 的工作方式
    每个 behavior 可以包含一组属性、数据、生命周期函数和方法。组件引用它时,它的属性、数据和方法会被
    合并到组件中。
    每个组件可以引用多个 behavior,behavior 也可以引用其它 behavior。

  2. 创建 behavior
    调用 Behavior(Object object) 方法即可创建一个共享的 behavior 实例对象,供所有的组件使用:

微信小程序_第116张图片

  1. 导入并使用 behavior
    在组件中,使用 require() 方法导入需要的 behavior,挂载后即可访问 behavior 中的数据或方法,示例代码
    如下:

微信小程序_第117张图片

  1. behavior 中所有可用的节点
可用的节点 类型 是否必填 描述
properties Object Map 同组件的属性
data Object 同组件的数据
methods Object 同自定义组件的方法
behaviors String Array 引入其它的 behavior
created Function 生命周期函数
attached Function 生命周期函数
ready Function 生命周期函数
moved Function 生命周期函数
detached Function 生命周期函数
  1. 同名字段的覆盖和组合规则*
    组件和它引用的 behavior 中可以包含同名的字段,此时可以参考如下 3 种同名时的处理规则:
    ① 同名的数据字段 (data)
    ② 同名的属性 (properties) 或方法 (methods)
    ③ 同名的生命周期函数
    关于详细的覆盖和组合规则,大家可以参考微信小程序官方文档给出的说明:
    https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/behaviors.html

① 能够创建并引用组件
⚫ 全局引用、局部引用、usingComponents
② 能够知道如何修改组件的样式隔离选项
⚫ options -> styleIsolation( isolated, apply-shared, shared)
③ 能够知道如何定义和使用数据监听器
⚫ observers
④ 能够知道如何定义和使用纯数据字段
⚫ options -> pureDataPattern
⑤ 能够知道实现组件父子通信有哪3种方式
⚫ 属性绑定、事件绑定、this.selectComponent(’ id或class选择器’)
⑥ 能够知道如何定义和使用behaviors
⚫ 调用 Behavior() 构造器方法

使用npm包

小程序对 npm 的支持与限制
目前,小程序中已经支持使用 npm 安装第三方包,从而来提高小程序的开发效率。但是,在小程序中使用
npm 包有如下 3 个限制:
① 不支持依赖于 Node.js 内置库的包
② 不支持依赖于浏览器内置对象的包
③ 不支持依赖于 C++ 插件的包
总结:虽然 npm 上的包有千千万,但是能供小程序使用的包却“为数不多”

  1. 什么是 Vant Weapp
    Vant Weapp 是有赞前端团队开源的一套小程序 UI 组件库,助力开发者快速搭建小程序应用。它所使用的是
    MIT 开源许可协议,对商业使用比较友好。
    官方文档地址 https://youzan.github.io/vant-weapp
    扫描下方的小程序二维码,体验组件库示例:

  2. 安装 Vant 组件库
    在小程序项目中,安装 Vant 组件库主要分为如下 3 步:
    ① 通过 npm 安装(建议指定版本为@1.3.3)
    ② 构建 npm 包
    ③ 修改 app.json
    详细的操作步骤,大家可以参考 Vant 官方提供的快速上手教程:
    https://youzan.github.io/vant-weapp/#/quickstart#an-zhuang

  3. 使用 Vant 组件
    安装完 Vant 组件库之后,可以在 app.json 的 usingComponents 节点中引入需要的组件,即可在 wxml 中
    直接使用组件。示例代码如下:

微信小程序_第118张图片

  1. 定制全局主题样式
    Vant Weapp 使用 CSS 变量来实现定制主题。 关于 CSS 变量的基本用法,请参考 MDN 文档:
    https://developer.mozilla.org/zh-CN/docs/Web/CSS/Using_CSS_custom_properties

在 app.wxss 中,写入 CSS 变量,即可对全局生效:

所有可用的颜色变量,请参考 Vant 官方提供的配置文件:
https://github.com/youzan/vant-weapp/blob/dev/packages/common/style/var.less

微信小程序_第119张图片

  1. 基于回调函数的异步 API 的缺点
    默认情况下,小程序官方提供的异步 API 都是基于回调函数实现的,例如,网络请求的 API 需要按照如下的方
    式调用:

微信小程序_第120张图片

缺点:容易造成回调地狱的问题,代码的可读性、维护性差!

  1. 什么是 API Promise 化
    API Promise化,指的是通过额外的配置,将官方提供的、基于回调函数的异步 API,升级改造为基于
    Promise 的异步 API,从而提高代码的可读性、维护性,避免回调地狱的问题

  2. 实现 API Promise 化
    在小程序中,实现 API Promise 化主要依赖于 miniprogram-api-promise 这个第三方的 npm 包。它的安装
    和使用步骤如下:

微信小程序_第121张图片

  1. 调用 Promise 化之后的异步 API

微信小程序_第122张图片

  1. 什么是全局数据共享
    全局数据共享(又叫做:状态管理)是为了解决组件之间数据共享的问题。
    开发中常用的全局数据共享方案有:Vuex、Redux、MobX 等

微信小程序_第123张图片

  1. 小程序中的全局数据共享方案
    在小程序中,可使用 mobx-miniprogram 配合 mobx-miniprogram-bindings 实现全局数据共享。其中:
    ⚫ mobx-miniprogram 用来创建 Store 实例对象
    ⚫ mobx-miniprogram-bindings 用来把 Store 中的共享数据或方法,绑定到组件或页面中使用

微信小程序_第124张图片

  1. 安装 MobX 相关的包
    在项目中运行如下的命令,安装 MobX 相关的包:

image-20221201181558867

注意:MobX 相关的包安装完毕之后,记得删除 miniprogram_npm 目录后,重新构建 npm。

  1. 创建 MobX 的 Store 实例

微信小程序_第125张图片

  1. 将 Store 中的成员绑定到页面中

微信小程序_第126张图片

  1. 在页面上使用 Store 中的成员

微信小程序_第127张图片

  1. 将 Store 中的成员绑定到组件中

微信小程序_第128张图片

  1. 在组件中使用 Store 中的成员

微信小程序_第129张图片

分包

  1. 什么是分包

分包指的是把一个完整的小程序项目,按照需求划分为不同的子包,在构建时打包成不同的分包,用户在使用
时按需进行加载。

  1. 分包的好处
    对小程序进行分包的好处主要有以下两点:
    ⚫ 可以优化小程序首次启动的下载时间
    ⚫ 在多团队共同开发时可以更好的解耦协作

  2. 分包前项目的构成
    分包前,小程序项目中所有的页面和资源都被打包到了一起,导致整个项目体积过大,影响小程序首次启动的
    下载时间。

微信小程序_第130张图片

  1. 分包后项目的构成
    分包后,小程序项目由 1 个主包 + 多个分包组成:
    ⚫ 主包:一般只包含项目的启动页面或 TabBar 页面、以及所有分包都需要用到的一些公共资源
    ⚫ 分包:只包含和当前分包有关的页面和私有资源

微信小程序_第131张图片

  1. 分包的加载规则
    ① 在小程序启动时,默认会下载主包并启动主包内页面
    ⚫ tabBar 页面需要放到主包中
    ② 当用户进入分包内某个页面时,客户端会把对应分包下载下来,下载完成后再进行展示
    ⚫ 非 tabBar 页面可以按照功能的不同,划分为不同的分包之后,进行按需下载

  2. 分包的体积限制
    目前,小程序分包的大小有以下两个限制:
    ⚫ 整个小程序所有分包大小不超过 16M(主包 + 所有分包)
    ⚫ 单个分包/主包大小不能超过 2M

配置

微信小程序_第132张图片

  1. 打包原则
    ① 小程序会按 subpackages 的配置进行分包,subpackages 之外的目录将被打包到主包中
    ② 主包也可以有自己的 pages(即最外层的 pages 字段)
    ③ tabBar 页面必须在主包内
    ④ 分包之间不能互相嵌套

  2. 引用原则
    ① 主包无法引用分包内的私有资源
    ② 分包之间不能相互引用私有资源
    ③ 分包可以引用主包内的公共资源

微信小程序_第133张图片

  1. 什么是独立分包
    独立分包本质上也是分包,只不过它比较特殊,可以独立于主包和其他分包而单独运行。

  2. 独立分包和普通分包的区别
    最主要的区别:是否依赖于主包才能运行
    ⚫ 普通分包必须依赖于主包才能运行
    ⚫ 独立分包可以在不下载主包的情况下,独立运行

  3. 独立分包的应用场景
    开发者可以按需,将某些具有一定功能独立性的页面配置到独立分包中。原因如下:
    ⚫ 当小程序从普通的分包页面启动时,需要首先下载主包
    ⚫ 而独立分包不依赖主包即可运行,可以很大程度上提升分包页面的启动速度
    注意:一个小程序中可以有多个独立分包。

  4. 独立分包配置

微信小程序_第134张图片

  1. 引用原则

独立分包和普通分包以及主包之间,是相互隔绝的,不能相互引用彼此的资源!例如:
① 主包无法引用独立分包内的私有资源
② 独立分包之间,不能相互引用私有资源
③ 独立分包和普通分包之间,不能相互引用私有资源
④ 特别注意:独立分包中不能引用主包内的公共资源

  1. 什么是分包预下载
    分包预下载指的是:在进入小程序的某个页面时,由框架自动预下载可能需要的分包,从而提升进入后续分包
    页面时的启动速度

  2. 配置分包的预下载
    预下载分包的行为,会在进入指定的页面时触发。在 app.json 中,使用 preloadRule 节点定义分包的预下载
    规则,示例代码如下

微信小程序_第135张图片

  1. 分包预下载的限制
    同一个分包中的页面享有共同的预下载大小限额 2M,例如:

微信小程序_第136张图片

案例自定义tabbar

  1. 实现步骤
    自定义 tabBar 分为 3 大步骤,分别是:
    ① 配置信息
    ② 添加 tabBar 代码文件
    ③ 编写 tabBar 代码
    详细步骤,可以参考小程序官方给出的文档:
    https://developers.weixin.qq.com/miniprogram/dev/framework/ability/custom-tabbar.html

总结

① 能够知道如何安装和配置 vant-weapp 组件库
⚫ 参考 Vant 的官方文档
② 能够知道如何使用 MobX 实现全局数据共享
⚫ 安装包、创建 Store、参考官方文档进行使用
③ 能够知道如何对小程序的 API 进行 Promise 化
⚫ 安装包、在 app.js 中进行配置
④ 能够知道如何实现自定义 tabBar 的效果
⚫ Vant 组件库 + 自定义组件 + 全局数据共享

项目实战

1. 起步

#1.1 uni-app 简介

uni-app 是一个使用 Vue.js 开发所有前端应用的框架。开发者编写一套代码,可发布到 iOS、Android、H5、以及各种小程序(微信/支付宝/百度/头条/QQ/钉钉/淘宝)、快应用等多个平台。

微信小程序_第137张图片

详细的 uni-app 官方文档,请翻阅 https://uniapp.dcloud.net.cn/

#1.2 开发工具

uni-app 官方推荐使用 HBuilderX 来开发 uni-app 类型的项目。主要好处:

  • 模板丰富
  • 完善的智能提示
  • 一键运行

当然,你依然可以根据自己的喜好,选择使用 VS Code、Sublime、记事本… 等自己喜欢的编辑器!

#1.2.1 下载 HBuilderX

  1. 访问 HBuilderX 的官网首页 https://www.dcloud.io/hbuilderx.html
  2. 点击首页的 DOWNLOAD 按钮
  3. 选择下载 正式版 -> App 开发版

#1.2.2 安装 HBuilderX

  1. 将下载的 zip包 进行解压缩
  2. 将解压之后的文件夹,存放到纯英文的目录中(且不能包含括号等特殊字符)
  3. 双击 HBuilderX.exe 即可启动 HBuilderX

#1.2.3 安装 scss/sass 编译

为了方便编写样式(例如:),建议安装 scss/sass 编译 插件。插件下载地址:

https://ext.dcloud.net.cn/plugin?name=compile-node-sass

进入插件下载页面之后,点击右上角的 使用 HBuilderX 导入插件 按钮进行自动安装,截图如下:

微信小程序_第138张图片

#1.2.4 快捷键方案切换

操作步骤:工具 -> 预设快捷键方案切换 -> VS Code

微信小程序_第139张图片

#1.2.5 修改编辑器的基本设置

操作步骤:工具 -> 设置 -> 打开 Settings.json 按需进行配置

微信小程序_第140张图片

源码视图下可用的参考配置:

{
  "editor.colorScheme": "Default",
  "editor.fontSize": 12,
  "editor.fontFamily": "Consolas",
  "editor.fontFmyCHS": "微软雅黑 Light",
  "editor.insertSpaces": true,
  "editor.lineHeight": "1.5",
  "editor.minimap.enabled": false,
  "editor.mouseWheelZoom": true,
  "editor.onlyHighlightWord": false,
  "editor.tabSize": 2,
  "editor.wordWrap": true,
  "explorer.iconTheme": "vs-seti",
  "editor.codeassist.px2rem.enabel": false,
  "editor.codeassist.px2upx.enabel": false
}

Tips:可以使用 Ctrl + 鼠标滚轮 缩放编辑器

#1.3 新建 uni-app 项目

  1. 文件 -> 新建 -> 项目

    微信小程序_第141张图片

  2. 填写项目基本信息

    微信小程序_第142张图片

  3. 项目创建成功

    微信小程序_第143张图片

#1.4 目录结构

一个 uni-app 项目,默认包含如下目录及文件:

┌─components            uni-app组件目录
│  └─comp-a.vue         可复用的a组件
├─pages                 业务页面文件存放的目录
│  ├─index
│  │  └─index.vue       index页面
│  └─list
│     └─list.vue        list页面
├─static                存放应用引用静态资源(如图片、视频等)的目录,注意:静态资源只能存放于此
├─main.js               Vue初始化入口文件
├─App.vue               应用配置,用来配置小程序的全局样式、生命周期函数等
├─manifest.json         配置应用名称、appid、logo、版本等打包信息
└─pages.json            配置页面路径、页面窗口样式、tabBar、navigationBar 等页面类信息

#1.5 把项目运行到微信开发者工具

  1. 填写自己的微信小程序的 AppID:

    微信小程序_第144张图片

  2. 在 HBuilderX 中,配置“微信开发者工具”的安装路径

    微信小程序_第145张图片

  3. 在微信开发者工具中,通过 设置 -> 安全设置 面板,开启“微信开发者工具”的服务端口

    微信小程序_第146张图片

  4. 在 HBuilderX 中,点击菜单栏中的 运行 -> 运行到小程序模拟器 -> 微信开发者工具,将当前 uni-app 项目编译之后,自动运行到微信开发者工具中,从而方便查看项目效果与调试:

    微信小程序_第147张图片

  5. 初次运行成功之后的项目效果:

    微信小程序_第148张图片

#1.6 使用 Git 管理项目

#1.6.1 本地管理

  1. 在项目根目录中新建 .gitignore 忽略文件,并配置如下:
# 忽略 node_modules 目录
/node_modules
/unpackage/dist

注意:由于我们忽略了 unpackage 目录中仅有的 dist 目录,因此默认情况下, unpackage 目录不会被 Git 追踪

此时,为了让 Git 能够正常追踪 unpackage 目录,按照惯例,我们可以在 unpackage 目录下创建一个叫做 .gitkeep 的文件进行占位

  1. 打开终端,切换到项目根目录中,运行如下的命令,初始化本地 Git 仓库:
git init
  1. 将所有文件都加入到暂存区:
git add .
  1. 本地提交更新:
git commit -m "init project"

#1.6.2 把项目托管到码云

  1. 注册并激活码云账号( 注册页面地址:https://gitee.com/signup )
  2. 生成并配置 SSH 公钥
  3. 创建空白的码云仓库
  4. 把本地项目上传到码云对应的空白仓库中

2. tabBar

#2.0 创建 tabBar 分支

运行如下的命令,基于 master 分支在本地创建 tabBar 子分支,用来开发和 tabBar 相关的功能:

git checkout -b tabbar

#2.1 创建 tabBar 页面

pages 目录中,创建首页(home)、分类(cate)、购物车(cart)、我的(my) 这 4 个 tabBar 页面。在 HBuilderX 中,可以通过如下的两个步骤,快速新建页面:

  1. pages 目录上鼠标右键,选择新建页面

  2. 在弹出的窗口中,填写页面的名称勾选 scss 模板之后,点击创建按钮。截图如下:

    微信小程序_第149张图片

#2.2 配置 tabBar 效果

  1. 资料 目录下的 static 文件夹 拷贝一份,替换掉项目根目录中的 static 文件夹

  2. 修改项目根目录中的 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"
          }
        ]
      }
    }
    

#2.3 删除默认的 index 首页

  1. 在 HBuilderX 中,把 pages 目录下的 index首页文件夹 删除掉
  2. 同时,把 page.json 中记录的 index 首页 路径删除掉
  3. 为了防止小程序运行失败,在微信开发者工具中,手动删除 pages 目录下的 index 首页文件夹
  4. 同时,把 components 目录下的 uni-link 组件文件夹 删除掉

#2.4 修改导航条的样式效果

  1. 打开 pages.json 这个全局的配置文件

  2. 修改 globalStyle 节点如下:

    {
      "globalStyle": {
        "navigationBarTextStyle": "white",
        "navigationBarTitleText": "黑马优购",
        "navigationBarBackgroundColor": "#C00000",
        "backgroundColor": "#FFFFFF"
      }
    }
    

#2.5 分支的提交与合并

  1. 将本地的 tabbar 分支进行本地的 commit 提交:
git add .
git commit -m "完成了 tabBar 的开发"
  1. 将本地的 tabbar 分支推送到远程仓库进行保存:
git push -u origin tabbar
  1. 将本地的 tabbar 分支合并到本地的 master 分支:
git checkout master
git merge tabbar
  1. 删除本地的 tabbar 分支:
git branch -d tabbar

3. 首页

#3.0 创建 home 分支

运行如下的命令,基于 master 分支在本地创建 home 子分支,用来开发和 home 首页相关的功能:

git checkout -b home

#3.1 配置网络请求

由于平台的限制,小程序项目中不支持 axios,而且原生的 wx.request() API 功能较为简单,不支持拦截器等全局定制的功能。因此,建议在 uni-app 项目中使用 @escook/request-miniprogram 第三方包发起网络数据请求。

请参考 @escook/request-miniprogram 的官方文档进行安装、配置、使用

官方文档:https://www.npmjs.com/package/@escook/request-miniprogram

最终,在项目的 main.js 入口文件中,通过如下的方式进行配置:

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()
}

#3.2 轮播图区域

#3.2.1 请求轮播图的数据

实现步骤:

  1. 在 data 中定义轮播图的数组
  2. 在 onLoad 生命周期函数中调用获取轮播图数据的方法
  3. 在 methods 中定义获取轮播图数据的方法

示例代码:

export default {
  data() {
    return {
      // 1. 轮播图的数据列表,默认为空数组
      swiperList: [],
    }
  },
  onLoad() {
    // 2. 在小程序页面刚加载的时候,调用获取轮播图数据的方法
    this.getSwiperList()
  },
  methods: {
    // 3. 获取轮播图数据的方法
    async getSwiperList() {
      // 3.1 发起请求
      const { data: res } = await uni.$http.get('/api/public/v1/home/swiperdata')
      // 3.2 请求失败
      if (res.meta.status !== 200) {
        return uni.showToast({
          title: '数据请求失败!',
          duration: 1500,
          icon: 'none',
        })
      }
      // 3.3 请求成功,为 data 中的数据赋值
      this.swiperList = res.message
    },
  },
}

#3.2.2 渲染轮播图的 UI 结构

  1. 渲染 UI 结构:

    <template>
      <view>
        
        <swiper :indicator-dots="true" :autoplay="true" :interval="3000" :duration="1000" :circular="true">
          
          <swiper-item v-for="(item, i) in swiperList" :key="i">
            <view class="swiper-item">
              
              <image :src="item.image_src">image>
            view>
          swiper-item>
        swiper>
      view>
    template>
    
  2. 美化 UI 结构:

    
    

#3.2.3 配置小程序分包

分包可以减少小程序首次启动时的加载时间

为此,我们在项目中,把 tabBar 相关的 4 个页面放到主包中,其它页面(例如:商品详情页、商品列表页)放到分包中。在 uni-app 项目中,配置分包的步骤如下:

  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. subpkg 目录上鼠标右键,点击 新建页面 选项,并填写页面的相关信息:

    微信小程序_第150张图片

#3.2.4 点击轮播图跳转到商品详情页面

节点内的 view 组件,改造为 navigator 导航组件,并动态绑定 url 属性 的值。

  1. 改造之前的 UI 结构:

    <swiper-item v-for="(item, i) in swiperList" :key="i">
      <view class="swiper-item">
        
        <image :src="item.image_src">image>
      view>
    swiper-item>
    
  2. 改造之后的 UI 结构:

    <swiper-item v-for="(item, i) in swiperList" :key="i">
        <navigator class="swiper-item" :url="'/subpkg/goods_detail/goods_detail?goods_id=' + item.goods_id">
          
          <image :src="item.image_src">image>
        navigator>
    swiper-item>
    

#3.2.5 封装 uni.$showMsg() 方法

当数据请求失败之后,经常需要调用 uni.showToast({ /* 配置对象 */ }) 方法来提示用户。此时,可以在全局封装一个 uni.$showMsg() 方法,来简化 uni.showToast() 方法的调用。具体的改造步骤如下:

  1. main.js 中,为 uni 对象挂载自定义的 $showMsg() 方法:

    // 封装的展示消息提示的方法
    uni.$showMsg = function (title = '数据加载失败!', duration = 1500) {
      uni.showToast({
        title,
        duration,
        icon: 'none',
      })
    }
    
  2. 今后,在需要提示消息的时候,直接调用 uni.$showMsg() 方法即可:

    async getSwiperList() {
       const { data: res } = await uni.$http.get('/api/public/v1/home/swiperdata')
       if (res.meta.status !== 200) return uni.$showMsg()
       this.swiperList = res.message
    }
    

#3.3 分类导航区域

#3.3.1 获取分类导航的数据

实现思路:

  1. 定义 data 数据
  2. 在 onLoad 中调用获取数据的方法
  3. 在 methods 中定义获取数据的方法

示例代码如下:

export default {
  data() {
    return {
      // 1. 分类导航的数据列表
      navList: [],
    }
  },
  onLoad() {
    // 2. 在 onLoad 中调用获取数据的方法
    this.getNavList()
  },
  methods: {
    // 3. 在 methods 中定义获取数据的方法
    async getNavList() {
      const { data: res } = await uni.$http.get('/api/public/v1/home/catitems')
      if (res.meta.status !== 200) return uni.$showMsg()
      this.navList = res.message
    },
  },
}

#3.3.2 渲染分类导航的 UI 结构

  1. 定义如下的 UI 结构:

    
    <view class="nav-list">
       <view class="nav-item" v-for="(item, i) in navList" :key="i">
         <image :src="item.image_src" class="nav-img">image>
       view>
    view>
    
  2. 通过如下的样式美化页面结构:

    .nav-list {
      display: flex;
      justify-content: space-around;
      margin: 15px 0;
    
      .nav-img {
        width: 128rpx;
        height: 140rpx;
      }
    }
    

#3.3.2 点击第一项,切换到分类页面

  1. nav-item 绑定点击事件处理函数:

    <!-- 分类导航区域 -->
    <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>
    
  2. 定义 navClickHandler 事件处理函数:

    // nav-item 项被点击时候的事件处理函数
    navClickHandler(item) {
      // 判断点击的是哪个 nav
      if (item.name === '分类') {
        uni.switchTab({
          url: '/pages/cate/cate'
        })
      }
    }
    

#3.4 楼层区域

#3.4.1 获取楼层数据

实现思路:

  1. 定义 data 数据
  2. 在 onLoad 中调用获取数据的方法
  3. 在 methods 中定义获取数据的方法

示例代码如下:

export default {
  data() {
    return {
      // 1. 楼层的数据列表
      floorList: [],
    }
  },
  onLoad() {
    // 2. 在 onLoad 中调用获取楼层数据的方法
    this.getFloorList()
  },
  methods: {
    // 3. 定义获取楼层列表数据的方法
    async getFloorList() {
      const { data: res } = await uni.$http.get('/api/public/v1/home/floordata')
      if (res.meta.status !== 200) return uni.$showMsg()
      this.floorList = res.message
    },
  },
}

#3.4.2 渲染楼层的标题

  1. 定义如下的 UI 结构:

    
    <view class="floor-list">
      
      <view class="floor-item" v-for="(item, i) in floorList" :key="i">
        
        <image :src="item.floor_title.image_src" class="floor-title">image>
      view>
    view>
    
  2. 美化楼层标题的样式:

    .floor-title {
      height: 60rpx;
      width: 100%;
      display: flex;
    }
    

#3.4.3 渲染楼层里的图片

  1. 定义楼层图片区域的 UI 结构:

    
    <view class="floor-img-box">
      
      <view class="left-img-box">
        <image :src="item.product_list[0].image_src" :style="{width: item.product_list[0].image_width + 'rpx'}" mode="widthFix">image>
      view>
      
      <view class="right-img-box">
        <view class="right-img-item" v-for="(item2, i2) in item.product_list" :key="i2" v-if="i2 !== 0">
          <image :src="item2.image_src" mode="widthFix" :style="{width: item2.image_width + 'rpx'}">image>
        view>
      view>
    view>
    
  2. 美化楼层图片区域的样式:

    .right-img-box {
      display: flex;
      flex-wrap: wrap;
      justify-content: space-around;
    }
    
    .floor-img-box {
      display: flex;
      padding-left: 10rpx;
    }
    

#3.4.4 点击楼层图片跳转到商品列表页

  1. subpkg 分包中,新建 goods_list 页面

    微信小程序_第151张图片

  2. 楼层数据请求成功之后,通过双层 forEach 循环,处理 URL 地址:

    // 获取楼层列表数据
    async getFloorList() {
      const { data: res } = await uni.$http.get('/api/public/v1/home/floordata')
      if (res.meta.status !== 200) return uni.$showMsg()
    
      // 通过双层 forEach 循环,处理 URL 地址
      res.message.forEach(floor => {
        floor.product_list.forEach(prod => {
          prod.url = '/subpkg/goods_list/goods_list?' + prod.navigator_url.split('?')[1]
        })
      })
    
      this.floorList = res.message
    }
    
  3. 把图片外层的 view 组件,改造为 navigator 组件,并动态绑定 url 属性 的值:

    
    <view class="floor-img-box">
      
      <navigator class="left-img-box" :url="item.product_list[0].url">
        <image :src="item.product_list[0].image_src" :style="{width: item.product_list[0].image_width + 'rpx'}" mode="widthFix">image>
      navigator>
      
      <view class="right-img-box">
        <navigator class="right-img-item" v-for="(item2, i2) in item.product_list" :key="i2" v-if="i2 !== 0" :url="item2.url">
          <image :src="item2.image_src" mode="widthFix" :style="{width: item2.image_width + 'rpx'}">image>
        navigator>
      view>
    view>
    

#3.5 分支的合并与提交

  1. 将本地的 home 分支进行本地的 commit 提交:

    git add .
    git commit -m "完成了 home 首页的开发"
    
  2. 将本地的 home 分支推送到远程仓库进行保存:

    git push -u origin home
    
  3. 将本地的 home 分支合并到本地的 master 分支:

    git checkout master
    git merge home
    
  4. 删除本地的 home 分支:

    git branch -d home
    

4. 分类

#4.0 创建 cate 分支

运行如下的命令,基于 master 分支在本地创建 cate 子分支,用来开发分类页面相关的功能:

git checkout -b cate

#4.1 渲染分类页面的基本结构

  1. 定义页面结构如下:

    <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">xxxview>
            <view class="left-scroll-view-item">xxxview>
            <view class="left-scroll-view-item">xxxview>
            <view class="left-scroll-view-item">xxxview>
            <view class="left-scroll-view-item">xxxview>
            <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">zzzview>
            <view class="left-scroll-view-item">zzzview>
            <view class="left-scroll-view-item">zzzview>
            <view class="left-scroll-view-item">zzzview>
            <view class="left-scroll-view-item">多复制一些节点,演示纵向滚动效果view>
          scroll-view>
        view>
      view>
    template>
    
  2. 动态计算窗口的剩余高度:

    <script>
      export default {
        data() {
          return {
            // 窗口的可用高度 = 屏幕高度 - navigationBar高度 - tabBar 高度
            wh: 0
          };
        },
        onLoad() {
          // 获取当前系统的信息
          const sysInfo = uni.getSystemInfoSync()
          // 为 wh 窗口可用高度动态赋值
          this.wh = sysInfo.windowHeight
        }
      }
    </script>
    
  3. 美化页面结构:

    .scroll-view-container {
      display: flex;
    
      .left-scroll-view {
        width: 120px;
    
        .left-scroll-view-item {
          line-height: 60px;
          background-color: #f7f7f7;
          text-align: center;
          font-size: 12px;
    
          // 激活项的样式
          &.active {
            background-color: #ffffff;
            position: relative;
    
            // 渲染激活项左侧的红色指示边线
            &::before {
              content: ' ';
              display: block;
              width: 3px;
              height: 30px;
              background-color: #c00000;
              position: absolute;
              left: 0;
              top: 50%;
              transform: translateY(-50%);
            }
          }
        }
      }
    }
    

#4.2 获取分类数据

  1. 在 data 中定义分类数据节点:

    data() {
      return {
        // 分类数据列表
        cateList: []
      }
    }
    
  2. 调用获取分类列表数据的方法:

    onLoad() {
      // 调用获取分类列表数据的方法
      this.getCateList()
    }
    
  3. 定义获取分类列表数据的方法:

    methods: {
      async getCateList() {
        // 发起请求
        const { data: res } = await uni.$http.get('/api/public/v1/categories')
        // 判断是否获取失败
        if (res.meta.status !== 200) return uni.$showMsg()
        // 转存数据
        this.cateList = res.message
      }
    }
    

#4.3 动态渲染左侧的一级分类列表

  1. 循环渲染列表结构:

    
    <scroll-view class="left-scroll-view" scroll-y :style="{height: wh + 'px'}">
      <block v-for="(item, i) in cateList" :key="i">
        <view class="left-scroll-view-item">{{item.cat_name}}view>
      block>
    scroll-view>
    
  2. 在 data 中定义默认选中项的索引:

    data() {
      return {
        // 当前选中项的索引,默认让第一项被选中
        active: 0
      }
    }
    
  3. 循环渲染结构时,为选中项动态添加 .active 类名:

    <block v-for="(item, i) in cateList" :key="i">
      <view :class="['left-scroll-view-item', i === active ? 'active' : '']">{{item.cat_name}}view>
    block>
    
  4. 为一级分类的 Item 项绑定点击事件处理函数 activeChanged

    <block v-for="(item, i) in cateList" :key="i">
      <view :class="['left-scroll-view-item', i === active ? 'active' : '']" @click="activeChanged(i)">{{item.cat_name}}view>
    block>
    
  5. 定义 activeChanged 事件处理函数,动态修改选中项的索引:

    methods: {
      // 选中项改变的事件处理函数
      activeChanged(i) {
        this.active = i
      }
    }
    

#4.4 动态渲染右侧的二级分类列表

  1. data 中定义二级分类列表的数据节点:

    data() {
      return {
        // 二级分类列表
        cateLevel2: []
      }
    }
    
  2. 修改 getCateList 方法,在请求到数据之后,为二级分类列表数据赋值:

    async getCateList() {
      const { data: res } = await uni.$http.get('/api/public/v1/categories')
      if (res.meta.status !== 200) return uni.$showMsg()
      this.cateList = res.message
      // 为二级分类赋值
      this.cateLevel2 = res.message[0].children
    }
    
  3. 修改 activeChanged 方法,在一级分类选中项改变之后,为二级分类列表数据重新赋值:

    activeChanged(i) {
      this.active = i
      // 为二级分类列表重新赋值
      this.cateLevel2 = this.cateList[i].children
    }
    
  4. 循环渲染右侧二级分类列表的 UI 结构:

    
    <scroll-view class="right-scroll-view" scroll-y :style="{height: wh + 'px'}">
      <view class="cate-lv2" v-for="(item2, i2) in cateLevel2" :key="i2">
        <view class="cate-lv2-title">/ {{item2.cat_name}} /view>
      view>
    scroll-view>
    
  5. 美化二级分类的标题样式:

    .cate-lv2-title {
      font-size: 12px;
      font-weight: bold;
      text-align: center;
      padding: 15px 0;
    }
    

#4.5 动态渲染右侧的三级分类列表

  1. 在二级分类的 组件中,循环渲染三级分类的列表结构:

    
    <scroll-view class="right-scroll-view" scroll-y :style="{height: wh + 'px'}">
      <view class="cate-lv2" v-for="(item2, i2) in cateLevel2" :key="i2">
        <view class="cate-lv2-title">/ {{item2.cat_name}} /view>
        
        <view class="cate-lv3-list">
          
          <view class="cate-lv3-item" v-for="(item3, i3) in item2.children" :key="i3">
            
            <image :src="item3.cat_icon">image>
            
            <text>{{item3.cat_name}}text>
          view>
        view>
      view>
    scroll-view>
    
  2. 美化三级分类的样式:

    .cate-lv3-list {
      display: flex;
      flex-wrap: wrap;
    
      .cate-lv3-item {
        width: 33.33%;
        margin-bottom: 10px;
        display: flex;
        flex-direction: column;
        align-items: center;
    
        image {
          width: 60px;
          height: 60px;
        }
    
        text {
          font-size: 12px;
        }
      }
    }
    

#4.6 切换一级分类后重置滚动条的位置

  1. 在 data 中定义 滚动条距离顶部的距离

    data() {
      return {
        // 滚动条距离顶部的距离
        scrollTop: 0
      }
    }
    
  2. 动态为右侧的 组件绑定 scroll-top 属性的值:

    
    <scroll-view class="right-scroll-view" scroll-y :style="{height: wh + 'px'}" :scroll-top="scrollTop">scroll-view>
    
  3. 切换一级分类时,动态设置 scrollTop 的值:

    // 选中项改变的事件处理函数
    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
    }
    

#4.7 点击三级分类跳转到商品列表页面

  1. 为三级分类的 Item 项绑定点击事件处理函数如下:

    <view class="cate-lv3-item" v-for="(item3, i3) in item2.children" :key="i3" @click="gotoGoodsList(item3)">
      <image :src="item3.cat_icon">image>
      <text>{{item3.cat_name}}text>
    view>
    
  2. 定义事件处理函数如下:

    // 点击三级分类项跳转到商品列表页面
    gotoGoodsList(item3) {
      uni.navigateTo({
        url: '/subpkg/goods_list/goods_list?cid=' + item3.cat_id
      })
    }
    

#4.8 分支的合并与提交

  1. cate 分支进行本地提交:

    git add .
    git commit -m "完成了分类页面的开发"
    
  2. 将本地的 cate 分支推送到码云:

    git push -u origin cate
    
  3. 将本地 cate 分支中的代码合并到 master 分支:

    git checkout master
    git merge cate
    git push
    
  4. 删除本地的 cate 分支:

    git branch -d cate
    

5. 搜索

#5.0 创建 search 分支

运行如下的命令,基于 master 分支在本地创建 search 子分支,用来开发搜索相关的功能:

git checkout -b search

#5.1 自定义搜索组件

#5.1.1 自定义 my-search 组件

  1. 在项目根目录的 components 目录上,鼠标右键,选择 新建组件,填写组件信息后,最后点击 创建 按钮:

    微信小程序_第152张图片

  2. 在分类页面的 UI 结构中,直接以标签的形式使用 my-search 自定义组件:

    
    <my-search>my-search>
    
  3. 定义 my-search 组件的 UI 结构如下:

    <template>
      <view class="my-search-container">
        
        <view class="my-search-box">
          <uni-icons type="search" size="17">uni-icons>
          <text class="placeholder">搜索text>
        view>
      view>
    template>
    

    注意:在当前组件中,我们使用 view 组件模拟 input 输入框的效果;并不会在页面上渲染真正的 input 输入框

  4. 美化自定义 search 组件的样式:

    .my-search-container {
      background-color: #c00000;
      height: 50px;
      padding: 0 10px;
      display: flex;
      align-items: center;
    }
    
    .my-search-box {
      height: 36px;
      background-color: #ffffff;
      border-radius: 15px;
      width: 100%;
      display: flex;
      align-items: center;
      justify-content: center;
    
      .placeholder {
        font-size: 15px;
        margin-left: 5px;
      }
    }
    
  5. 由于自定义的 my-search 组件高度为 50px,因此,需要重新计算分类页面窗口的可用高度:

    onLoad() {
      const sysInfo = uni.getSystemInfoSync()
      // 可用高度 = 屏幕高度 - navigationBar高度 - tabBar高度 - 自定义的search组件高度
      this.wh = sysInfo.windowHeight - 50
    }
    

#5.1.2 通过自定义属性增强组件的通用性

为了增强组件的通用性,我们允许使用者自定义搜索组件的 背景颜色圆角尺寸

  1. 通过 props 定义 bgcolorradius 两个属性,并指定值类型和属性默认值:

    props: {
      // 背景颜色
      bgcolor: {
        type: String,
        default: '#C00000'
      },
      // 圆角尺寸
      radius: {
        type: Number,
        // 单位是 px
        default: 18
      }
    }
    
  2. 通过属性绑定的形式,为 .my-search-container 盒子和 .my-search-box 盒子动态绑定 style 属性:

    <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>
    
  3. 移除对应 scss 样式中的 背景颜色圆角尺寸

    .my-search-container {
      // 移除背景颜色,改由 props 属性控制
      // background-color: #C00000;
      height: 50px;
      padding: 0 10px;
      display: flex;
      align-items: center;
    }
    
    .my-search-box {
      height: 36px;
      background-color: #ffffff;
      // 移除圆角尺寸,改由 props 属性控制
      // border-radius: 15px;
      width: 100%;
      display: flex;
      align-items: center;
      justify-content: center;
    
      .placeholder {
        font-size: 15px;
        margin-left: 5px;
      }
    }
    

#5.1.3 为自定义组件封装 click 事件

  1. my-search 自定义组件内部,给类名为 .my-search-boxview 绑定 click 事件处理函数:

    <view class="my-search-box" :style="{'border-radius': radius + 'px'}" @click="searchBoxHandler">
      <uni-icons type="search" size="17">uni-icons>
      <text class="placeholder">搜索text>
    view>
    
  2. my-search 自定义组件的 methods 节点中,声明事件处理函数如下:

    methods: {
      // 点击了模拟的 input 输入框
      searchBoxHandler() {
        // 触发外界通过 @click 绑定的 click 事件处理函数
        this.$emit('click')
      }
    }
    
  3. 在分类页面中使用 my-search 自定义组件时,即可通过 @click 为其绑定点击事件处理函数:

    
    <my-search @click="gotoSearch">my-search>
    

    同时在分类页面中,定义 gotoSearch 事件处理函数如下:

    methods: {
       // 跳转到分包中的搜索页面
       gotoSearch() {
         uni.navigateTo({
           url: '/subpkg/search/search'
         })
       }
    }
    

#5.1.4 实现首页搜索组件的吸顶效果

  1. 在 home 首页定义如下的 UI 结构:

    
    <view class="search-box">
      <my-search @click="gotoSearch">my-search>
    view>
    
  2. 在 home 首页定义如下的事件处理函数:

    gotoSearch() {
      uni.navigateTo({
        url: '/subpkg/search/search'
      })
    }
    
  3. 通过如下的样式实现吸顶的效果:

    .search-box {
      // 设置定位效果为“吸顶”
      position: sticky;
      // 吸顶的“位置”
      top: 0;
      // 提高层级,防止被轮播图覆盖
      z-index: 999;
    }
    

#5.2 搜索建议

#5.2.1 渲染搜索页面的基本结构

  1. 定义如下的 UI 结构:

    <view class="search-box">
      
      <uni-search-bar @input="input" :radius="100" cancelButton="none">uni-search-bar>
    view>
    
  2. 修改 components -> uni-search-bar -> uni-search-bar.vue 组件,将默认的白色搜索背景改为 #C00000 的红色背景:

    .uni-searchbar {
      /* #ifndef APP-NVUE */
      display: flex;
      /* #endif */
      flex-direction: row;
      position: relative;
      padding: 16rpx;
      /* 将默认的 #FFFFFF 改为 #C00000 */
      background-color: #c00000;
    }
    
  3. 实现搜索框的吸顶效果:

    .search-box {
      position: sticky;
      top: 0;
      z-index: 999;
    }
    
  4. 定义如下的 input 事件处理函数:

    methods: {
      input(e) {
        // e.value 是最新的搜索内容
        console.log(e.value)
      }
    }
    

#5.2.2 实现搜索框自动获取焦点

  1. 修改 components -> uni-search-bar -> uni-search-bar.vue 组件,把 data 数据中的 showshowSync 的值,从默认的 false 改为 true 即可:

    data() {
      return {
        show: true,
        showSync: true,
        searchVal: ""
      }
    }
    
  2. 使用手机扫码预览,即可在真机上查看效果。

#5.2.3 实现搜索框的防抖处理

  1. 在 data 中定义防抖的延时器 timerId 如下:

    data() {
      return {
        // 延时器的 timerId
        timer: null,
        // 搜索关键词
        kw: ''
      }
    }
    
  2. 修改 input 事件处理函数如下:

    input(e) {
      // 清除 timer 对应的延时器
      clearTimeout(this.timer)
      // 重新启动一个延时器,并把 timerId 赋值给 this.timer
      this.timer = setTimeout(() => {
        // 如果 500 毫秒内,没有触发新的输入事件,则为搜索关键词赋值
        this.kw = e.value
        console.log(this.kw)
      }, 500)
    }
    

#5.2.4 根据关键词查询搜索建议列表

  1. 在 data 中定义如下的数据节点,用来存放搜索建议的列表数据:

    data() {
      return {
        // 搜索结果列表
        searchResults: []
      }
    }
    
  2. 在防抖的 setTimeout 中,调用 getSearchList 方法获取搜索建议列表:

    this.timer = setTimeout(() => {
      this.kw = e.value
      // 根据关键词,查询搜索建议列表
      this.getSearchList()
    }, 500)
    
  3. methods 中定义 getSearchList 方法如下:

    // 根据搜索关键词,搜索商品建议列表
    async getSearchList() {
      // 判断关键词是否为空
      if (this.kw === '') {
        this.searchResults = []
        return
      }
      // 发起请求,获取搜索建议列表
      const { data: res } = await uni.$http.get('/api/public/v1/goods/qsearch', { query: this.kw })
      if (res.meta.status !== 200) return uni.$showMsg()
      this.searchResults = res.message
    }
    

#5.2.5 渲染搜索建议列表

  1. 定义如下的 UI 结构:

    
    <view class="sugg-list">
      <view class="sugg-item" v-for="(item, i) in searchResults" :key="i" @click="gotoDetail(item.goods_id)">
        <view class="goods-name">{{item.goods_name}}view>
        <uni-icons type="arrowright" size="16">uni-icons>
      view>
    view>
    
  2. 美化搜索建议列表:

    .sugg-list {
      padding: 0 5px;
    
      .sugg-item {
        font-size: 12px;
        padding: 13px 0;
        border-bottom: 1px solid #efefef;
        display: flex;
        align-items: center;
        justify-content: space-between;
    
        .goods-name {
          // 文字不允许换行(单行文本)
          white-space: nowrap;
          // 溢出部分隐藏
          overflow: hidden;
          // 文本溢出后,使用 ... 代替
          text-overflow: ellipsis;
          margin-right: 3px;
        }
      }
    }
    
  3. 点击搜索建议的 Item 项,跳转到商品详情页面:

    gotoDetail(goods_id) {
      uni.navigateTo({
        // 指定详情页面的 URL 地址,并传递 goods_id 参数
        url: '/subpkg/goods_detail/goods_detail?goods_id=' + goods_id
      })
    }
    

5.3 搜索历史

#5.3.1 渲染搜索历史记录的基本结构

  1. 在 data 中定义搜索历史的假数据

    data() {
      return {
        // 搜索关键词的历史记录
        historyList: ['a', 'app', 'apple']
      }
    }
    
  2. 渲染搜索历史区域的 UI 结构:

    
    <view class="history-box">
      
      <view class="history-title">
        <text>搜索历史text>
        <uni-icons type="trash" size="17">uni-icons>
      view>
      
      <view class="history-list">
        <uni-tag :text="item" v-for="(item, i) in historyList" :key="i">uni-tag>
      view>
    view>
    
  3. 美化搜索历史区域的样式:

    .history-box {
      padding: 0 5px;
    
      .history-title {
        display: flex;
        justify-content: space-between;
        align-items: center;
        height: 40px;
        font-size: 13px;
        border-bottom: 1px solid #efefef;
      }
    
      .history-list {
        display: flex;
        flex-wrap: wrap;
    
        .uni-tag {
          margin-top: 5px;
          margin-right: 5px;
        }
      }
    }
    

#5.3.2 实现搜索建议和搜索历史的按需展示

  1. 当搜索结果列表的长度不为 0的时候(searchResults.length !== 0),需要展示搜索建议区域,隐藏搜索历史区域

  2. 当搜索结果列表的长度等于 0的时候(searchResults.length === 0),需要隐藏搜索建议区域,展示搜索历史区域

  3. 使用 v-ifv-else 控制这两个区域的显示和隐藏,示例代码如下:

    
    <view class="sugg-list" v-if="searchResults.length !== 0">
      
    view>
    
    
    <view class="history-box" v-else>
      
    view>
    

#5.3.3 将搜索关键词存入 historyList

  1. 直接将搜索关键词 pushhistoryList 数组中即可

    methods: {
      // 根据搜索关键词,搜索商品建议列表
      async getSearchList() {
        // 省略其它不必要的代码...
    
        // 1. 查询到搜索建议之后,调用 saveSearchHistory() 方法保存搜索关键词
        this.saveSearchHistory()
      },
      // 2. 保存搜索关键词的方法
      saveSearchHistory() {
        // 2.1 直接把搜索关键词 push 到 historyList 数组中
        this.historyList.push(this.kw)
      }
    }
    
  2. 上述实现思路存在的问题:

    • 关键词前后顺序的问题(可以调用数组的 reverse() 方法对数组进行反转)
    • 关键词重复的问题(可以使用 Set 对象进行去重操作

#5.3.4 解决关键字前后顺序的问题

  1. data 中的 historyList 不做任何修改,依然使用 push 进行末尾追加

  2. 定义一个计算属性 historys,将 historyList 数组 reverse 反转之后,就是此计算属性的值:

    computed: {
      historys() {
        // 注意:由于数组是引用类型,所以不要直接基于原数组调用 reverse 方法,以免修改原数组中元素的顺序
        // 而是应该新建一个内存无关的数组,再进行 reverse 反转
        return [...this.historyList].reverse()
      }
    }
    
  3. 页面中渲染搜索关键词的时候,不再使用 data 中的 historyList,而是使用计算属性 historys

    <view class="history-list">
      <uni-tag :text="item" v-for="(item, i) in historys" :key="i">uni-tag>
    view>
    

#5.3.5 解决关键词重复的问题

  1. 修改 saveSearchHistory 方法如下:

    // 保存搜索关键词为历史记录
    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)
    }
    

#5.3.6 将搜索历史记录持久化存储到本地

  1. 修改 saveSearchHistory 方法如下:

    // 保存搜索关键词为历史记录
    saveSearchHistory() {
      const set = new Set(this.historyList)
      set.delete(this.kw)
      set.add(this.kw)
      this.historyList = Array.from(set)
      // 调用 uni.setStorageSync(key, value) 将搜索历史记录持久化存储到本地
      uni.setStorageSync('kw', JSON.stringify(this.historyList))
    }
    
  2. onLoad 生命周期函数中,加载本地存储的搜索历史记录:

    onLoad() {
      this.historyList = JSON.parse(uni.getStorageSync('kw') || '[]')
    }
    

#5.3.7 清空搜索历史记录

  1. 为清空的图标按钮绑定 click 事件:

    <uni-icons type="trash" size="17" @click="cleanHistory">uni-icons>
    
  2. methods 中定义 cleanHistory 处理函数:

    // 清空搜索历史记录
    cleanHistory() {
      // 清空 data 中保存的搜索历史
      this.historyList = []
      // 清空本地存储中记录的搜索历史
      uni.setStorageSync('kw', '[]')
    }
    

#5.3.8 点击搜索历史跳转到商品列表页面

  1. 为搜索历史的 Item 项绑定 click 事件处理函数:

    <uni-tag :text="item" v-for="(item, i) in historys" :key="i" @click="gotoGoodsList(item)">uni-tag>
    
  2. methods 中定义 gotoGoodsList 处理函数:

    // 点击跳转到商品列表页面
    gotoGoodsList(kw) {
      uni.navigateTo({
        url: '/subpkg/goods_list/goods_list?query=' + kw
      })
    }
    

#5.4 分支的合并与提交

  1. search 分支进行本地提交:

    git add .
    git commit -m "完成了搜索功能的开发"
    
  2. 将本地的 search 分支推送到码云:

    git push -u origin search
    
  3. 将本地 search 分支中的代码合并到 master 分支:

    git checkout master
    git merge search
    git push
    
  4. 删除本地的 search 分支:

    git branch -d search
    

6. 商品列表

#6.0 创建 goodslist 分支

运行如下的命令,基于 master 分支在本地创建 goodslist 子分支,用来开发商品列表相关的功能:

git checkout -b search

#6.1 定义请求参数对象

  1. 为了方便发起请求获取商品列表的数据,我们要根据接口的要求,事先定义一个请求参数对象

    data() {
      return {
        // 请求参数对象
        queryObj: {
          // 查询关键词
          query: '',
          // 商品分类Id
          cid: '',
          // 页码值
          pagenum: 1,
          // 每页显示多少条数据
          pagesize: 10
        }
      }
    }
    
  2. 将页面跳转时携带的参数,转存到 queryObj 对象中:

    onLoad(options) {
      // 将页面参数转存到 this.queryObj 对象中
      this.queryObj.query = options.query || ''
      this.queryObj.cid = options.cid || ''
    }
    
  3. 为了方便开发商品分类页面,建议大家通过微信开发者工具,新建商品列表页面的编译模式

    微信小程序_第153张图片

#6.2 获取商品列表数据

  1. 在 data 中新增如下的数据节点:

    data() {
      return {
        // 商品列表的数据
        goodsList: [],
        // 总数量,用来实现分页
        total: 0
      }
    }
    
  2. onLoad 生命周期函数中,调用 getGoodsList 方法获取商品列表数据:

    onLoad(options) {
      // 调用获取商品列表数据的方法
      this.getGoodsList()
    }
    
  3. methods 节点中,声明 getGoodsList 方法如下:

    methods: {
      // 获取商品列表数据的方法
      async getGoodsList() {
        // 发起请求
        const { data: res } = await uni.$http.get('/api/public/v1/goods/search', this.queryObj)
        if (res.meta.status !== 200) return uni.$showMsg()
        // 为数据赋值
        this.goodsList = res.message.goods
        this.total = res.message.total
      }
    }
    

#6.3 渲染商品列表结构

  1. 在页面中,通过 v-for 指令,循环渲染出商品的 UI 结构:

    <template>
      <view>
        <view class="goods-list">
          <block v-for="(goods, i) in goodsList" :key="i">
            <view class="goods-item">
              
              <view class="goods-item-left">
                <image :src="goods.goods_small_logo || defaultPic" class="goods-pic">image>
              view>
              
              <view class="goods-item-right">
                
                <view class="goods-name">{{goods.goods_name}}view>
                <view class="goods-info-box">
                  
                  <view class="goods-price">¥{{goods.goods_price}}view>
                view>
              view>
            view>
          block>
        view>
      view>
    template>
    
  2. 为了防止某些商品的图片不存在,需要在 data 中定义一个默认的图片:

    data() {
      return {
        // 默认的空图片
        defaultPic: 'https://img3.doubanio.com/f/movie/8dd0c794499fe925ae2ae89ee30cd225750457b4/pics/movie/celebrity-default-medium.png'
      }
    }
    

    并在页面渲染时按需使用:

    <image :src="goods.goods_small_logo || defaultPic" class="goods-pic">image>
    
  3. 美化商品列表的 UI 结构:

    .goods-item {
      display: flex;
      padding: 10px 5px;
      border-bottom: 1px solid #f0f0f0;
    
      .goods-item-left {
        margin-right: 5px;
    
        .goods-pic {
          width: 100px;
          height: 100px;
          display: block;
        }
      }
    
      .goods-item-right {
        display: flex;
        flex-direction: column;
        justify-content: space-between;
    
        .goods-name {
          font-size: 13px;
        }
    
        .goods-price {
          font-size: 16px;
          color: #c00000;
        }
      }
    }
    

#6.4 把商品 item 项封装为自定义组件

  1. components 目录上鼠标右键,选择 新建组件

    微信小程序_第154张图片

  2. goods_list 页面中,关于商品 item 项相关的 UI 结构、样式、data 数据,封装到 my-goods 组件中:

    <template>
      <view class="goods-item">
        
        <view class="goods-item-left">
          <image :src="goods.goods_small_logo || defaultPic" class="goods-pic">image>
        view>
        
        <view class="goods-item-right">
          
          <view class="goods-name">{{goods.goods_name}}view>
          <view class="goods-info-box">
            
            <view class="goods-price">¥{{goods.goods_price}}view>
          view>
        view>
      view>
    template>
    
    <script>
      export default {
        // 定义 props 属性,用来接收外界传递到当前组件的数据
        props: {
          // 商品的信息对象
          goods: {
            type: Object,
            defaul: {},
          },
        },
        data() {
          return {
            // 默认的空图片
            defaultPic: 'https://img3.doubanio.com/f/movie/8dd0c794499fe925ae2ae89ee30cd225750457b4/pics/movie/celebrity-default-medium.png',
          }
        },
      }
    script>
    
    <style lang="scss">
      .goods-item {
        display: flex;
        padding: 10px 5px;
        border-bottom: 1px solid #f0f0f0;
    
        .goods-item-left {
          margin-right: 5px;
    
          .goods-pic {
            width: 100px;
            height: 100px;
            display: block;
          }
        }
    
        .goods-item-right {
          display: flex;
          flex-direction: column;
          justify-content: space-between;
    
          .goods-name {
            font-size: 13px;
          }
    
          .goods-price {
            font-size: 16px;
            color: #c00000;
          }
        }
      }
    style>
    
  3. goods_list 组件中,循环渲染 my-goods 组件即可:

    <view class="goods-list">
      <block v-for="(item, i) in goodsList" :key="i">
        
        <my-goods :goods="item">my-goods>
      block>
    view>
    

#6.5 使用过滤器处理价格

  1. my-goods 组件中,和 data 节点平级,声明 filters 过滤器节点如下:

    filters: {
      // 把数字处理为带两位小数点的数字
      tofixed(num) {
        return Number(num).toFixed(2)
      }
    }
    
  2. 在渲染商品价格的时候,通过管道符 | 调用过滤器:

    
    <view class="goods-price">¥{{goods.goods_price | tofixed}}view>
    

#6.6 上拉加载更多

#6.6.1 初步实现上拉加载更多

  1. 打开项目根目录中的 pages.json 配置文件,为 subPackages 分包中的 goods_list 页面配置上拉触底的距离:

     "subPackages": [
       {
         "root": "subpkg",
         "pages": [
           {
             "path": "goods_detail/goods_detail",
             "style": {}
           },
           {
             "path": "goods_list/goods_list",
             "style": {
               "onReachBottomDistance": 150
             }
           },
           {
             "path": "search/search",
             "style": {}
           }
         ]
       }
     ]
    
  2. goods_list 页面中,和 methods 节点平级,声明 onReachBottom 事件处理函数,用来监听页面的上拉触底行为:

    // 触底的事件
    onReachBottom() {
      // 让页码值自增 +1
      this.queryObj.pagenum += 1
      // 重新获取列表数据
      this.getGoodsList()
    }
    
  3. 改造 methods 中的 getGoodsList 函数,当列表数据请求成功之后,进行新旧数据的拼接处理:

    // 获取商品列表数据的方法
    async getGoodsList() {
      // 发起请求
      const { data: res } = await uni.$http.get('/api/public/v1/goods/search', this.queryObj)
      if (res.meta.status !== 200) return uni.$showMsg()
    
      // 为数据赋值:通过展开运算符的形式,进行新旧数据的拼接
      this.goodsList = [...this.goodsList, ...res.message.goods]
      this.total = res.message.total
    }
    

#6.6.2 通过节流阀防止发起额外的请求

  1. 在 data 中定义 isloading 节流阀如下:

    data() {
      return {
        // 是否正在请求数据
        isloading: false
      }
    }
    
  2. 修改 getGoodsList 方法,在请求数据前后,分别打开和关闭节流阀:

    // 获取商品列表数据的方法
    async getGoodsList() {
      // ** 打开节流阀
      this.isloading = true
      // 发起请求
      const { data: res } = await uni.$http.get('/api/public/v1/goods/search', this.queryObj)
      // ** 关闭节流阀
      this.isloading = false
    
      // 省略其它代码...
    }
    
  3. onReachBottom 触底事件处理函数中,根据节流阀的状态,来决定是否发起请求:

    // 触底的事件
    onReachBottom() {
      // 判断是否正在请求其它数据,如果是,则不发起额外的请求
      if (this.isloading) return
    
      this.queryObj.pagenum += 1
      this.getGoodsList()
    }
    

#6.6.3 判断数据是否加载完毕

  1. 如果下面的公式成立,则证明没有下一页数据了:

    当前的页码值 * 每页显示多少条数据 >= 总数条数
    pagenum * pagesize >= total
    
  2. 修改 onReachBottom 事件处理函数如下:

    // 触底的事件
    onReachBottom() {
      // 判断是否还有下一页数据
      if (this.queryObj.pagenum * this.queryObj.pagesize >= this.total) return uni.$showMsg('数据加载完毕!')
    
      // 判断是否正在请求其它数据,如果是,则不发起额外的请求
      if (this.isloading) return
    
      this.queryObj.pagenum += 1
      this.getGoodsList()
    }
    

#6.7 下拉刷新

  1. pages.json 配置文件中,为当前的 goods_list 页面单独开启下拉刷新效果:

    "subPackages": [{
      "root": "subpkg",
      "pages": [{
        "path": "goods_detail/goods_detail",
        "style": {}
      }, {
        "path": "goods_list/goods_list",
        "style": {
          "onReachBottomDistance": 150,
          "enablePullDownRefresh": true,
          "backgroundColor": "#F8F8F8"
        }
      }, {
        "path": "search/search",
        "style": {}
      }]
    }]
    
  2. 监听页面的 onPullDownRefresh 事件处理函数:

    // 下拉刷新的事件
    onPullDownRefresh() {
      // 1. 重置关键数据
      this.queryObj.pagenum = 1
      this.total = 0
      this.isloading = false
      this.goodsList = []
    
      // 2. 重新发起请求
      this.getGoodsList(() => uni.stopPullDownRefresh())
    }
    
  3. 修改 getGoodsList 函数,接收 cb 回调函数并按需进行调用:

    // 获取商品列表数据的方法
    async getGoodsList(cb) {
      this.isloading = true
      const { data: res } = await uni.$http.get('/api/public/v1/goods/search', this.queryObj)
      this.isloading = false
      // 只要数据请求完毕,就立即按需调用 cb 回调函数
      cb && cb()
    
      if (res.meta.status !== 200) return uni.$showMsg()
      this.goodsList = [...this.goodsList, ...res.message.goods]
      this.total = res.message.total
    }
    

#6.8 点击商品 item 项跳转到详情页面

  1. 将循环时的 block 组件修改为 view 组件,并绑定 click 点击事件处理函数:

    <view class="goods-list">
      <view v-for="(item, i) in goodsList" :key="i" @click="gotoDetail(item)">
        
        <my-goods :goods="item">my-goods>
      view>
    view>
    
  2. methods 节点中,定义 gotoDetail 事件处理函数:

    // 点击跳转到商品详情页面
    gotoDetail(item) {
      uni.navigateTo({
        url: '/subpkg/goods_detail/goods_detail?goods_id=' + item.goods_id
      })
    }
    

#6.9 分支的合并与提交

  1. goodslist 分支进行本地提交:

    git add .
    git commit -m "完成了商品列表页面的开发"
    
  2. 将本地的 goodslist 分支推送到码云:

    git push -u origin goodslist
    
  3. 将本地 goodslist 分支中的代码合并到 master 分支:

    git checkout master
    git merge goodslist
    git push
    
  4. 删除本地的 goodslist 分支:

    git branch -d goodslist
    

7. 商品详情

#7.0 创建 goodsdetail 分支

运行如下的命令,基于 master 分支在本地创建 goodsdetail 子分支,用来开发商品详情页相关的功能:

git checkout -b goodsdetail

#7.1 添加商品详情页的编译模式

  1. 在微信开发者工具中,点击工具栏上的编译模式下拉菜单,选择 添加编译模式 选项:

    微信小程序_第155张图片

  2. 勾选 启动页面 的路径,并填写了 启动参数 之后,点击 确定 按钮,添加详情页面的编译模式:

    微信小程序_第156张图片

#7.2 获取商品详情数据

  1. data 中定义商品详情的数据节点:

    data() {
      return {
        // 商品详情对象
        goods_info: {}
      }
    }
    
  2. onLoad 中获取商品的 Id,并调用请求商品详情的方法:

    onLoad(options) {
      // 获取商品 Id
      const goods_id = options.goods_id
      // 调用请求商品详情数据的方法
      this.getGoodsDetail(goods_id)
    }
    
  3. methods 中声明 getGoodsDetail 方法:

    methods: {
      // 定义请求商品详情数据的方法
      async getGoodsDetail(goods_id) {
        const { data: res } = await uni.$http.get('/api/public/v1/goods/detail', { goods_id })
        if (res.meta.status !== 200) return uni.$showMsg()
        // 为 data 中的数据赋值
        this.goods_info = res.message
      }
    }
    

#7.3 渲染商品详情页的 UI 结构

#7.3.1 渲染轮播图区域

  1. 使用 v-for 指令,循环渲染如下的轮播图 UI 结构:

    
    <swiper :indicator-dots="true" :autoplay="true" :interval="3000" :duration="1000" :circular="true">
      <swiper-item v-for="(item, i) in goods_info.pics" :key="i">
        <image :src="item.pics_big">image>
      swiper-item>
    swiper>
    
  2. 美化轮播图的样式:

    swiper {
      height: 750rpx;
    
      image {
        width: 100%;
        height: 100%;
      }
    }
    

#7.3.2 实现轮播图预览效果

  1. 为轮播图中的 image 图片绑定 click 事件处理函数:

    <swiper-item v-for="(item, i) in goods_info.pics" :key="i">
      
      <image :src="item.pics_big" @click="preview(i)">image>
    swiper-item>
    
  2. methods 中定义 preview 事件处理函数:

    // 实现轮播图的预览效果
    preview(i) {
      // 调用 uni.previewImage() 方法预览图片
      uni.previewImage({
        // 预览时,默认显示图片的索引
        current: i,
        // 所有图片 url 地址的数组
        urls: this.goods_info.pics.map(x => x.pics_big)
      })
    }
    

#7.3.3 渲染商品信息区域

  1. 定义商品信息区域的 UI 结构如下:

    
    <view class="goods-info-box">
      
      <view class="price">¥{{goods_info.goods_price}}view>
      
      <view class="goods-info-body">
        
        <view class="goods-name">{{goods_info.goods_name}}view>
        
        <view class="favi">
          <uni-icons type="star" size="18" color="gray">uni-icons>
          <text>收藏text>
        view>
      view>
      
      <view class="yf">快递:免运费view>
    view>
    
  2. 美化商品信息区域的样式:

    // 商品信息区域的样式
    .goods-info-box {
      padding: 10px;
      padding-right: 0;
    
      .price {
        color: #c00000;
        font-size: 18px;
        margin: 10px 0;
      }
    
      .goods-info-body {
        display: flex;
        justify-content: space-between;
    
        .goods-name {
          font-size: 13px;
          padding-right: 10px;
        }
        // 收藏区域
        .favi {
          width: 120px;
          font-size: 12px;
          display: flex;
          flex-direction: column;
          justify-content: center;
          align-items: center;
          border-left: 1px solid #efefef;
          color: gray;
        }
      }
    
      // 运费
      .yf {
        margin: 10px 0;
        font-size: 12px;
        color: gray;
      }
    }
    

#7.3.4 渲染商品详情信息

  1. 在页面结构中,使用 rich-text 组件,将带有 HTML 标签的内容,渲染为小程序的页面结构:

    
    <rich-text :nodes="goods_info.goods_introduce">rich-text>
    
  2. 修改 getGoodsDetail 方法,从而解决图片底部 空白间隙 的问题:

    // 定义请求商品详情数据的方法
    async getGoodsDetail(goods_id) {
      const { data: res } = await uni.$http.get('/api/public/v1/goods/detail', { goods_id })
      if (res.meta.status !== 200) return uni.$showMsg()
    
      // 使用字符串的 replace() 方法,为 img 标签添加行内的 style 样式,从而解决图片底部空白间隙的问题
      res.message.goods_introduce = res.message.goods_introduce.replace(//g, ')
      this.goods_info = res.message
    }
    
  3. 解决 .webp 格式图片在 ios 设备上无法正常显示的问题:

    // 定义请求商品详情数据的方法
    async getGoodsDetail(goods_id) {
      const { data: res } = await uni.$http.get('/api/public/v1/goods/detail', { goods_id })
      if (res.meta.status !== 200) return uni.$showMsg()
    
      // 使用字符串的 replace() 方法,将 webp 的后缀名替换为 jpg 的后缀名
      res.message.goods_introduce = res.message.goods_introduce.replace(//g, ').replace(/webp/g, 'jpg')
      this.goods_info = res.message
    }
    

#7.3.5 解决商品价格闪烁的问题

  1. 导致问题的原因:在商品详情数据请求回来之前,data 中 goods_info 的值为 {},因此初次渲染页面时,会导致 商品价格、商品名称 等闪烁的问题。

  2. 解决方案:判断 goods_info.goods_name 属性的值是否存在,从而使用 v-if 指令控制页面的显示与隐藏:

    <template>
      <view v-if="goods_info.goods_name">
       
      view>
    template>
    

#7.4 渲染详情页底部的商品导航区域

#7.4.1 渲染商品导航区域的 UI 结构

基于 uni-ui 提供的 GoodsNav 组件来实现商品导航区域的效果

  1. 在 data 中,通过 optionsbuttonGroup 两个数组,来声明商品导航组件的按钮配置对象:

    data() {
      return {
        // 商品详情对象
        goods_info: {},
        // 左侧按钮组的配置对象
        options: [{
          icon: 'shop',
          text: '店铺'
        }, {
          icon: 'cart',
          text: '购物车',
          info: 2
        }],
        // 右侧按钮组的配置对象
        buttonGroup: [{
            text: '加入购物车',
            backgroundColor: '#ff0000',
            color: '#fff'
          },
          {
            text: '立即购买',
            backgroundColor: '#ffa200',
            color: '#fff'
          }
        ]
      }
    }
    
  2. 在页面中使用 uni-goods-nav 商品导航组件:

    
    <view class="goods_nav">
      
      
      
      
      
      <uni-goods-nav :fill="true" :options="options" :buttonGroup="buttonGroup" @click="onClick" @buttonClick="buttonClick" />
    view>
    
  3. 美化商品导航组件,使之固定在页面最底部:

    .goods-detail-container {
      // 给页面外层的容器,添加 50px 的内padding,
      // 防止页面内容被底部的商品导航组件遮盖
      padding-bottom: 50px;
    }
    
    .goods_nav {
      // 为商品导航组件添加固定定位
      position: fixed;
      bottom: 0;
      left: 0;
      width: 100%;
    }
    

#7.4.2 点击跳转到购物车页面

  1. 点击商品导航组件左侧的按钮,会触发 uni-goods-nav@click 事件处理函数,事件对象 e 中会包含当前点击的按钮相关的信息:

    // 左侧按钮的点击事件处理函数
    onClick(e) {
      console.log(e)
    }
    

    打印的按钮信息对象如下:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jfUpFg6q-1669892095577)微信小程序_第157张图片

  2. 根据 e.content.text 的值,来决定进一步的操作:

    // 左侧按钮的点击事件处理函数
    onClick(e) {
      if (e.content.text === '购物车') {
        // 切换到购物车页面
        uni.switchTab({
          url: '/pages/cart/cart'
        })
      }
    }
    

#7.5 分支的合并与提交

  1. goodsdetail 分支进行本地提交:

    git add .
    git commit -m "完成了商品详情页面的开发"
    
  2. 将本地的 goodsdetail 分支推送到码云:

    git push -u origin goodsdetail
    
  3. 将本地 goodsdetail 分支中的代码合并到 master 分支:

    git checkout master
    git merge goodsdetail
    git push
    
  4. 删除本地的 goodsdetail 分支:

    git branch -d goodsdetail
    

8. 加入购物车

#8.0 创建 cart 分支

运行如下的命令,基于 master 分支在本地创建 cart 子分支,用来开发购物车相关的功能:

git checkout -b cart

#8.1 配置 vuex

  1. 在项目根目录中创建 store 文件夹,专门用来存放 vuex 相关的模块

  2. store 目录上鼠标右键,选择 新建 -> js文件,新建 store.js 文件:

  3. store.js 中按照如下 4 个步骤初始化 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: {},
    })
    
    // 4. 向外共享 Store 的实例对象
    export default store
    
  4. main.js 中导入 store 实例对象并挂载到 Vue 的实例上:

    // 1. 导入 store 的实例对象
    import store from './store/store.js'
    
    // 省略其它代码...
    
    const app = new Vue({
      ...App,
      // 2. 将 store 挂载到 Vue 实例上
      store,
    })
    app.$mount()
    

#8.2 创建购物车的 store 模块

  1. store 目录上鼠标右键,选择 新建 -> js文件,创建购物车的 store 模块,命名为 cart.js
    微信小程序_第158张图片

  2. cart.js 中,初始化如下的 vuex 模块:

    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: {},
    }
    
  3. store/store.js 模块中,导入并挂载购物车的 vuex 模块,示例代码如下:

    import Vue from 'vue'
    import Vuex from 'vuex'
    // 1. 导入购物车的 vuex 模块
    import moduleCart from './cart.js'
    
    Vue.use(Vuex)
    
    const store = new Vuex.Store({
      // TODO:挂载 store 模块
      modules: {
        // 2. 挂载购物车的 vuex 模块,模块内成员的访问路径被调整为 m_cart,例如:
        //    购物车模块中 cart 数组的访问路径是 m_cart/cart
        m_cart: moduleCart,
      },
    })
    
    export default store
    

#8.3 在商品详情页中使用 Store 中的数据

  1. goods_detail.vue 页面中,修改 标签中的代码如下:

    // 从 vuex 中按需导出 mapState 辅助方法
    import { mapState } from 'vuex'
    
    export default {
      computed: {
        // 调用 mapState 方法,把 m_cart 模块中的 cart 数组映射到当前页面中,作为计算属性来使用
        // ...mapState('模块的名称', ['要映射的数据名称1', '要映射的数据名称2'])
        ...mapState('m_cart', ['cart']),
      },
      // 省略其它代码...
    }
    

    注意:今后无论映射 mutations 方法,还是 getters 属性,还是 state 中的数据,都需要指定模块的名称,才能进行映射。

  2. 在页面渲染时,可以直接使用映射过来的数据,例如:

    
    <view class="yf">快递:免运费 -- {{cart.length}}view>
    

#8.4 实现加入购物车的功能

  1. 在 store 目录下的 cart.js 模块中,封装一个将商品信息加入购物车的 mutations 方法,命名为 addToCart。示例代码如下:

    export default {
      // 为当前模块开启命名空间
      namespaced: true,
    
      // 模块的 state 数据
      state: () => ({
        // 购物车的数组,用来存储购物车中每个商品的信息对象
        // 每个商品的信息对象,都包含如下 6 个属性:
        // { goods_id, goods_name, goods_price, goods_count, goods_small_logo, goods_state }
        cart: [],
      }),
    
      // 模块的 mutations 方法
      mutations: {
        addToCart(state, goods) {
          // 根据提交的商品的Id,查询购物车中是否存在这件商品
          // 如果不存在,则 findResult 为 undefined;否则,为查找到的商品信息对象
          const findResult = state.cart.find((x) => x.goods_id === goods.goods_id)
    
          if (!findResult) {
            // 如果购物车中没有这件商品,则直接 push
            state.cart.push(goods)
          } else {
            // 如果购物车中有这件商品,则只更新数量即可
            findResult.goods_count++
          }
        },
      },
    
      // 模块的 getters 属性
      getters: {},
    }
    
  2. 在商品详情页面中,通过 mapMutations 这个辅助方法,把 vuex 中 m_cart 模块下的 addToCart 方法映射到当前页面:

    // 按需导入 mapMutations 这个辅助方法
    import { mapMutations } from 'vuex'
    
    export default {
      methods: {
        // 把 m_cart 模块中的 addToCart 方法映射到当前页面使用
        ...mapMutations('m_cart', ['addToCart']),
      },
    }
    
  3. 为商品导航组件 uni-goods-nav 绑定 @buttonClick="buttonClick" 事件处理函数:

    // 右侧按钮的点击事件处理函数
    buttonClick(e) {
       // 1. 判断是否点击了 加入购物车 按钮
       if (e.content.text === '加入购物车') {
    
          // 2. 组织一个商品的信息对象
          const goods = {
             goods_id: this.goods_info.goods_id,       // 商品的Id
             goods_name: this.goods_info.goods_name,   // 商品的名称
             goods_price: this.goods_info.goods_price, // 商品的价格
             goods_count: 1,                           // 商品的数量
             goods_small_logo: this.goods_info.goods_small_logo, // 商品的图片
             goods_state: true                         // 商品的勾选状态
          }
    
          // 3. 通过 this 调用映射过来的 addToCart 方法,把商品信息对象存储到购物车中
          this.addToCart(goods)
    
       }
    }
    

#8.5 动态统计购物车中商品的总数量

  1. cart.js 模块中,在 getters 节点下定义一个 total 方法,用来统计购物车中商品的总数量:

    // 模块的 getters 属性
    getters: {
       // 统计购物车中商品的总数量
       total(state) {
          let c = 0
          // 循环统计商品的数量,累加到变量 c 中
          state.cart.forEach(goods => c += goods.goods_count)
          return c
       }
    }
    
  2. 在商品详情页面的 script 标签中,按需导入 mapGetters 方法并进行使用:

    // 按需导入 mapGetters 这个辅助方法
    import { mapGetters } from 'vuex'
    
    export default {
      computed: {
        // 把 m_cart 模块中名称为 total 的 getter 映射到当前页面中使用
        ...mapGetters('m_cart', ['total']),
      },
    }
    
  3. 通过 watch 侦听器,监听计算属性 total 值的变化,从而动态为购物车按钮的徽标赋值

    export default {
      watch: {
        // 1. 监听 total 值的变化,通过第一个形参得到变化后的新值
        total(newVal) {
          // 2. 通过数组的 find() 方法,找到购物车按钮的配置对象
          const findResult = this.options.find((x) => x.text === '购物车')
    
          if (findResult) {
            // 3. 动态为购物车按钮的 info 属性赋值
            findResult.info = newVal
          }
        },
      },
    }
    

#8.6 持久化存储购物车中的商品

  1. cart.js 模块中,声明一个叫做 saveToStorage 的 mutations 方法,此方法负责将购物车中的数据持久化存储到本地:

    // 将购物车中的数据持久化存储到本地
    saveToStorage(state) {
       uni.setStorageSync('cart', JSON.stringify(state.cart))
    }
    
  2. 修改 mutations 节点中的 addToCart 方法,在处理完商品信息后,调用步骤 1 中定义的 saveToStorage 方法:

    addToCart(state, goods) {
       // 根据提交的商品的Id,查询购物车中是否存在这件商品
       // 如果不存在,则 findResult 为 undefined;否则,为查找到的商品信息对象
       const findResult = state.cart.find(x => x.goods_id === goods.goods_id)
    
       if (!findResult) {
          // 如果购物车中没有这件商品,则直接 push
          state.cart.push(goods)
       } else {
          // 如果购物车中有这件商品,则只更新数量即可
          findResult.goods_count++
       }
    
       // 通过 commit 方法,调用 m_cart 命名空间下的 saveToStorage 方法
       this.commit('m_cart/saveToStorage')
    }
    
  3. 修改 cart.js 模块中的 state 函数,读取本地存储的购物车数据,对 cart 数组进行初始化:

    // 模块的 state 数据
    state: () => ({
       // 购物车的数组,用来存储购物车中每个商品的信息对象
       // 每个商品的信息对象,都包含如下 6 个属性:
       // { goods_id, goods_name, goods_price, goods_count, goods_small_logo, goods_state }
       cart: JSON.parse(uni.getStorageSync('cart') || '[]')
    }),
    

#8.7 优化商品详情页的 total 侦听器

  1. 使用普通函数的形式定义的 watch 侦听器,在页面首次加载后不会被调用。因此导致了商品详情页在首次加载完毕之后,不会将商品的总数量显示到商品导航区域:

    watch: {
       // 页面首次加载完毕后,不会调用这个侦听器
       total(newVal) {
          const findResult = this.options.find(x => x.text === '购物车')
          if (findResult) {
             findResult.info = newVal
          }
       }
    }
    
  2. 为了防止这个上述问题,可以使用对象的形式来定义 watch 侦听器(详细文档请参考 Vue 官方的 watch 侦听器教程),示例代码如下:

    watch: {
       // 定义 total 侦听器,指向一个配置对象
       total: {
          // handler 属性用来定义侦听器的 function 处理函数
          handler(newVal) {
             const findResult = this.options.find(x => x.text === '购物车')
             if (findResult) {
                findResult.info = newVal
             }
          },
          // immediate 属性用来声明此侦听器,是否在页面初次加载完毕后立即调用
          immediate: true
       }
    }
    

#8.8 动态为 tabBar 页面设置数字徽标

需求描述:从商品详情页面导航到购物车页面之后,需要为 tabBar 中的购物车动态设置数字徽标。

  1. 把 Store 中的 total 映射到 cart.vue 中使用:

    // 按需导入 mapGetters 这个辅助方法
    import { mapGetters } from 'vuex'
    
    export default {
      data() {
        return {}
      },
      computed: {
        // 将 m_cart 模块中的 total 映射为当前页面的计算属性
        ...mapGetters('m_cart', ['total']),
      },
    }
    
  2. 在页面刚显示出来的时候,立即调用 setBadge 方法,为 tabBar 设置数字徽标:

    onShow() {
       // 在页面刚展示的时候,设置数字徽标
       this.setBadge()
    }
    
  3. methods 节点中,声明 setBadge 方法如下,通过 uni.setTabBarBadge() 为 tabBar 设置数字徽标:

    methods: {
       setBadge() {
          // 调用 uni.setTabBarBadge() 方法,为购物车设置右上角的徽标
          uni.setTabBarBadge({
             index: 2, // 索引
             text: this.total + '' // 注意:text 的值必须是字符串,不能是数字
          })
       }
    }
    

#8.9 将设置 tabBar 徽标的代码抽离为 mixins

注意:除了要在 cart.vue 页面中设置购物车的数字徽标,还需要在其它 3 个 tabBar 页面中,为购物车设置数字徽标。

此时可以使用 Vue 提供的 mixins 特性,提高代码的可维护性。

  1. 在项目根目录中新建 mixins 文件夹,并在 mixins 文件夹之下新建 tabbar-badge.js 文件,用来把设置 tabBar 徽标的代码封装为一个 mixin 文件:

    import { mapGetters } from 'vuex'
    
    // 导出一个 mixin 对象
    export default {
      computed: {
        ...mapGetters('m_cart', ['total']),
      },
      onShow() {
        // 在页面刚展示的时候,设置数字徽标
        this.setBadge()
      },
      methods: {
        setBadge() {
          // 调用 uni.setTabBarBadge() 方法,为购物车设置右上角的徽标
          uni.setTabBarBadge({
            index: 2,
            text: this.total + '', // 注意:text 的值必须是字符串,不能是数字
          })
        },
      },
    }
    
  2. 修改 home.vuecate.vuecart.vuemy.vue 这 4 个 tabBar 页面的源代码,分别导入 @/mixins/tabbar-badge.js 模块并进行使用:

    // 导入自己封装的 mixin 模块
    import badgeMix from '@/mixins/tabbar-badge.js'
    
    export default {
      // 将 badgeMix 混入到当前的页面中进行使用
      mixins: [badgeMix],
      // 省略其它代码...
    }
    

9. 购物车页面

#9.0 创建购物车页面的编译模式

  1. 打开微信开发者工具,点击工具栏上的“编译模式”下拉菜单,选择“添加编译模式”:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fDWZLeUE-1669892095579)

  2. 勾选“启动页面的路径”之后,点击“确定”按钮,新增购物车页面的编译模式:

    https://typora-3.oss-cn-hangzhou.aliyuncs.com/typora202212011904791.png微信小程序_第159张图片

#9.1 商品列表区域

#9.1.1 渲染购物车商品列表的标题区域

  1. 定义如下的 UI 结构:

    
    <view class="cart-title">
      
      <uni-icons type="shop" size="18">uni-icons>
      
      <text class="cart-title-text">购物车text>
    view>
    
  2. 美化样式:

    .cart-title {
      height: 40px;
      display: flex;
      align-items: center;
      font-size: 14px;
      padding-left: 5px;
      border-bottom: 1px solid #efefef;
      .cart-title-text {
        margin-left: 10px;
      }
    }
    

#9.1.2 渲染商品列表区域的基本结构

  1. 通过 mapState 辅助函数,将 Store 中的 cart 数组映射到当前页面中使用:

    import badgeMix from '@/mixins/tabbar-badge.js'
    // 按需导入 mapState 这个辅助函数
    import { mapState } from 'vuex'
    
    export default {
      mixins: [badgeMix],
      computed: {
        // 将 m_cart 模块中的 cart 数组映射到当前页面中使用
        ...mapState('m_cart', ['cart']),
      },
      data() {
        return {}
      },
    }
    
  2. 在 UI 结构中,通过 v-for 指令循环渲染自定义的 my-goods 组件:

    
    <block v-for="(goods, i) in cart" :key="i">
      <my-goods :goods="goods">my-goods>
    block>
    

#9.1.3 为 my-goods 组件封装 radio 勾选状态

  1. 打开 my-goods.vue 组件的源代码,为商品的左侧图片区域添加 radio 组件:

    
    <view class="goods-item-left">
      <radio checked color="#C00000">radio>
      <image :src="goods.goods_small_logo || defaultPic" class="goods-pic">image>
    view>
    
  2. 给类名为 goods-item-leftview 组件添加样式,实现 radio 组件和 image 组件的左右布局:

    .goods-item-left {
      margin-right: 5px;
      display: flex;
      justify-content: space-between;
      align-items: center;
    
      .goods-pic {
        width: 100px;
        height: 100px;
        display: block;
      }
    }
    
  3. 封装名称为 showRadioprops 属性,来控制当前组件中是否显示 radio 组件:

    export default {
      // 定义 props 属性,用来接收外界传递到当前组件的数据
      props: {
        // 商品的信息对象
        goods: {
          type: Object,
          default: {},
        },
        // 是否展示图片左侧的 radio
        showRadio: {
          type: Boolean,
          // 如果外界没有指定 show-radio 属性的值,则默认不展示 radio 组件
          default: false,
        },
      },
    }
    
  4. 使用 v-if 指令控制 radio 组件的按需展示:

    
    <view class="goods-item-left">
      
      <radio checked color="#C00000" v-if="showRadio">radio>
      <image :src="goods.goods_small_logo || defaultPic" class="goods-pic">image>
    view>
    
  5. cart.vue 页面中的商品列表区域,指定 :show-radio="true" 属性,从而显示 radio 组件:

    
    <block v-for="(goods, i) in cart" :key="i">
      <my-goods :goods="goods" :show-radio="true">my-goods>
    block>
    
  6. 修改 my-goods.vue 组件,动态为 radio 绑定选中状态:

    
    <view class="goods-item-left">
      
      <radio :checked="goods.goods_state" color="#C00000" v-if="showRadio">radio>
      <image :src="goods.goods_small_logo || defaultPic" class="goods-pic">image>
    view>
    

#9.1.4 为 my-goods 组件封装 radio-change 事件

  1. 当用户点击 radio 组件,希望修改当前商品的勾选状态,此时用户可以为 my-goods 组件绑定 @radio-change 事件,从而获取当前商品的 goods_idgoods_state

    
    <block v-for="(goods, i) in cart" :key="i">
      
      <my-goods :goods="goods" :show-radio="true" @radio-change="radioChangeHandler">my-goods>
    block>
    

    定义 radioChangeHandler 事件处理函数如下:

    methods: {
      // 商品的勾选状态发生了变化
      radioChangeHandler(e) {
        console.log(e) // 输出得到的数据 -> {goods_id: 395, goods_state: false}
      }
    }
    
  2. my-goods.vue 组件中,为 radio 组件绑定 @click 事件处理函数如下:

    
    <view class="goods-item-left">
      <radio :checked="goods.goods_state" color="#C00000" v-if="showRadio" @click="radioClickHandler">radio>
      <image :src="goods.goods_small_logo || defaultPic" class="goods-pic">image>
    view>
    
  3. my-goods.vue 组件的 methods 节点中,定义 radioClickHandler 事件处理函数:

    methods: {
      // radio 组件的点击事件处理函数
      radioClickHandler() {
        // 通过 this.$emit() 触发外界通过 @ 绑定的 radio-change 事件,
        // 同时把商品的 Id 和 勾选状态 作为参数传递给 radio-change 事件处理函数
        this.$emit('radio-change', {
          // 商品的 Id
          goods_id: this.goods.goods_id,
          // 商品最新的勾选状态
          goods_state: !this.goods.goods_state
        })
      }
    }
    

#9.1.5 修改购物车中商品的勾选状态

  1. store/cart.js 模块中,声明如下的 mutations 方法,用来修改对应商品的勾选状态:

    // 更新购物车中商品的勾选状态
    updateGoodsState(state, goods) {
      // 根据 goods_id 查询购物车中对应商品的信息对象
      const findResult = state.cart.find(x => x.goods_id === goods.goods_id)
    
      // 有对应的商品信息对象
      if (findResult) {
        // 更新对应商品的勾选状态
        findResult.goods_state = goods.goods_state
        // 持久化存储到本地
        this.commit('m_cart/saveToStorage')
      }
    }
    
  2. cart.vue 页面中,导入 mapMutations 这个辅助函数,从而将需要的 mutations 方法映射到当前页面中使用:

    import badgeMix from '@/mixins/tabbar-badge.js'
    import { mapState, mapMutations } from 'vuex'
    
    export default {
      mixins: [badgeMix],
      computed: {
        ...mapState('m_cart', ['cart']),
      },
      data() {
        return {}
      },
      methods: {
        ...mapMutations('m_cart', ['updateGoodsState']),
        // 商品的勾选状态发生了变化
        radioChangeHandler(e) {
          this.updateGoodsState(e)
        },
      },
    }
    

#9.1.6 为 my-goods 组件封装 NumberBox

注意:NumberBox 组件是 uni-ui 提供的

  1. 修改 my-goods.vue 组件的源代码,在类名为 goods-info-box 的 view 组件内部渲染 NumberBox 组件的基本结构:

    <view class="goods-info-box">
      
      <view class="goods-price">¥{{goods.goods_price | tofixed}}view>
      
      <uni-number-box :min="1">uni-number-box>
    view>
    
  2. 美化页面的结构:

    .goods-item-right {
      display: flex;
      flex: 1;
      flex-direction: column;
      justify-content: space-between;
    
      .goods-name {
        font-size: 13px;
      }
    
      .goods-info-box {
        display: flex;
        align-items: center;
        justify-content: space-between;
      }
    
      .goods-price {
        font-size: 16px;
        color: #c00000;
      }
    }
    
  3. my-goods.vue 组件中,动态为 NumberBox 组件绑定商品的数量值:

    <view class="goods-info-box">
      
      <view class="goods-price">¥{{goods.goods_price | tofixed}}view>
      
      <uni-number-box :min="1" :value="goods.goods_count">uni-number-box>
    view>
    
  4. my-goods.vue 组件中,封装名称为 showNumprops 属性,来控制当前组件中是否显示 NumberBox 组件:

    export default {
      // 定义 props 属性,用来接收外界传递到当前组件的数据
      props: {
        // 商品的信息对象
        goods: {
          type: Object,
          defaul: {},
        },
        // 是否展示图片左侧的 radio
        showRadio: {
          type: Boolean,
          // 如果外界没有指定 show-radio 属性的值,则默认不展示 radio 组件
          default: false,
        },
        // 是否展示价格右侧的 NumberBox 组件
        showNum: {
          type: Boolean,
          default: false,
        },
      },
    }
    
  5. my-goods.vue 组件中,使用 v-if 指令控制 NumberBox 组件的按需展示:

    <view class="goods-info-box">
      
      <view class="goods-price">¥{{goods.goods_price | tofixed}}view>
      
      <uni-number-box :min="1" :value="goods.goods_count" @change="numChangeHandler" v-if="showNum">uni-number-box>
    view>
    
  6. cart.vue 页面中的商品列表区域,指定 :show-num="true" 属性,从而显示 NumberBox 组件:

    
    <block v-for="(goods, i) in cart" :key="i">
      <my-goods :goods="goods" :show-radio="true" :show-num="true" @radio-change="radioChangeHandler">my-goods>
    block>
    

#9.1.7 为 my-goods 组件封装 num-change 事件

  1. 当用户修改了 NumberBox 的值以后,希望将最新的商品数量更新到购物车中,此时用户可以为 my-goods 组件绑定 @num-change 事件,从而获取当前商品的 goods_idgoods_count:

    
    <block v-for="(goods, i) in cart" :key="i">
      <my-goods :goods="goods" :show-radio="true" :show-num="true" @radio-change="radioChangeHandler" @num-change="numberChangeHandler">my-goods>
    block>
    

    定义 numberChangeHandler 事件处理函数如下:

    // 商品的数量发生了变化
    numberChangeHandler(e) {
      console.log(e)
    }
    
  2. my-goods.vue 组件中,为 uni-number-box 组件绑定 @change 事件处理函数如下:

    <view class="goods-info-box">
      
      <view class="goods-price">¥{{goods.goods_price | tofixed}}view>
      
      <uni-number-box :min="1" :value="goods.goods_count" @change="numChangeHandler">uni-number-box>
    view>
    
  3. my-goods.vue 组件的 methods 节点中,定义 numChangeHandler 事件处理函数:

    methods: {
      // NumberBox 组件的 change 事件处理函数
      numChangeHandler(val) {
        // 通过 this.$emit() 触发外界通过 @ 绑定的 num-change 事件
        this.$emit('num-change', {
          // 商品的 Id
          goods_id: this.goods.goods_id,
          // 商品的最新数量
          goods_count: +val
        })
      }
    }
    

#9.1.8 解决 NumberBox 数据不合法的问题

问题说明:当用户在 NumberBox 中输入字母等非法字符之后,会导致 NumberBox 数据紊乱的问题

  1. 打开项目根目录中 components/uni-number-box/uni-number-box.vue 组件,修改 methods 节点中的 _onBlur 函数如下:

    _onBlur(event) {
      // 官方的代码没有进行数值转换,用户输入的 value 值可能是非法字符:
      // let value = event.detail.value;
    
      // 将用户输入的内容转化为整数
      let value = parseInt(event.detail.value);
    
      if (!value) {
        // 如果转化之后的结果为 NaN,则给定默认值为 1
        this.inputValue = 1;
        return;
      }
    
      // 省略其它代码...
    }
    
  2. 修改完毕之后,用户输入小数被转化为整数,用户输入非法字符被替换为默认值 1

#9.1.9 完善 NumberBox 的 inputValue 侦听器

问题说明:在用户每次输入内容之后,都会触发 inputValue 侦听器,从而调用 this.$emit(“change”, newVal) 方法。这种做法可能会把不合法的内容传递出去!

  1. 打开项目根目录中 components/uni-number-box/uni-number-box.vue 组件,修改 watch 节点中的 inputValue 侦听器如下:

    inputValue(newVal, oldVal) {
      // 官方提供的 if 判断条件,在用户每次输入内容时,都会调用 this.$emit("change", newVal)
      // if (+newVal !== +oldVal) {
    
      // 新旧内容不同 && 新值内容合法 && 新值中不包含小数点
      if (+newVal !== +oldVal && Number(newVal) && String(newVal).indexOf('.') === -1) {
        this.$emit("change", newVal);
      }
    }
    
  2. 修改完毕之后,NumberBox 组件只会把合法的、且不包含小数点的新值传递出去

#9.1.10 修改购物车中商品的数量

  1. store/cart.js 模块中,声明如下的 mutations 方法,用来修改对应商品的数量:

    // 更新购物车中商品的数量
    updateGoodsCount(state, goods) {
      // 根据 goods_id 查询购物车中对应商品的信息对象
      const findResult = state.cart.find(x => x.goods_id === goods.goods_id)
    
      if(findResult) {
        // 更新对应商品的数量
        findResult.goods_count = goods.goods_count
        // 持久化存储到本地
        this.commit('m_cart/saveToStorage')
      }
    }
    
  2. cart.vue 页面中,通过 mapMutations 这个辅助函数,将需要的 mutations 方法映射到当前页面中使用:

    import badgeMix from '@/mixins/tabbar-badge.js'
    import { mapState, mapMutations } from 'vuex'
    
    export default {
      mixins: [badgeMix],
      computed: {
        ...mapState('m_cart', ['cart']),
      },
      data() {
        return {}
      },
      methods: {
        ...mapMutations('m_cart', ['updateGoodsState', 'updateGoodsCount']),
        // 商品的勾选状态发生了变化
        radioChangeHandler(e) {
          this.updateGoodsState(e)
        },
        // 商品的数量发生了变化
        numberChangeHandler(e) {
          this.updateGoodsCount(e)
        },
      },
    }
    

#9.1.11 渲染滑动删除的 UI 效果

滑动删除需要用到 uni-ui 的 uni-swipe-action 组件和 uni-swipe-action-item。详细的官方文档请参考SwipeAction 滑动操作。

  1. 改造 cart.vue 页面的 UI 结构,将商品列表区域的结构修改如下(可以使用 uSwipeAction 代码块快速生成基本的 UI 结构):

    
    
    <uni-swipe-action>
      <block v-for="(goods, i) in cart" :key="i">
        
        <uni-swipe-action-item :options="options" @click="swipeActionClickHandler(goods)">
          <my-goods :goods="goods" :show-radio="true" :show-num="true" @radio-change="radioChangeHandler" @num-change="numberChangeHandler">my-goods>
        uni-swipe-action-item>
      block>
    uni-swipe-action>
    
  2. 在 data 节点中声明 options 数组,用来定义操作按钮的配置信息:

    data() {
      return {
        options: [{
          text: '删除', // 显示的文本内容
          style: {
            backgroundColor: '#C00000' // 按钮的背景颜色
          }
        }]
      }
    }
    
  3. methods 中声明 uni-swipe-action-item 组件的 @click 事件处理函数:

    // 点击了滑动操作按钮
    swipeActionClickHandler(goods) {
      console.log(goods)
    }
    
  4. 美化 my-goods.vue 组件的样式:

    .goods-item {
      // 让 goods-item 项占满整个屏幕的宽度
      width: 750rpx;
      // 设置盒模型为 border-box
      box-sizing: border-box;
      display: flex;
      padding: 10px 5px;
      border-bottom: 1px solid #f0f0f0;
    }
    

#9.1.12 实现滑动删除的功能

  1. store/cart.js 模块的 mutations 节点中声明如下的方法,从而根据商品的 Id 从购物车中移除对应的商品:

    // 根据 Id 从购物车中删除对应的商品信息
    removeGoodsById(state, goods_id) {
      // 调用数组的 filter 方法进行过滤
      state.cart = state.cart.filter(x => x.goods_id !== goods_id)
      // 持久化存储到本地
      this.commit('m_cart/saveToStorage')
    }
    
  2. cart.vue 页面中,使用 mapMutations 辅助函数,把需要的方法映射到当前页面中使用:

    methods: {
      ...mapMutations('m_cart', ['updateGoodsState', 'updateGoodsCount', 'removeGoodsById']),
      // 商品的勾选状态发生了变化
      radioChangeHandler(e) {
        this.updateGoodsState(e)
      },
      // 商品的数量发生了变化
      numberChangeHandler(e) {
        this.updateGoodsCount(e)
      },
      // 点击了滑动操作按钮
      swipeActionClickHandler(goods) {
        this.removeGoodsById(goods.goods_id)
      }
    }
    

#9.2 收货地址区域

#9.2.1 创建收货地址组件

  1. components 目录上鼠标右键,选择 新建组件,并填写组件相关的信息:

    微信小程序_第160张图片

  2. 渲染收货地址组件的基本结构:

    <view>
    
      
      <view class="address-choose-box">
        <button type="primary" size="mini" class="btnChooseAddress">请选择收货地址+button>
      view>
    
      
      <view class="address-info-box">
        <view class="row1">
          <view class="row1-left">
            <view class="username">收货人:<text>escooktext>view>
          view>
          <view class="row1-right">
            <view class="phone">电话:<text>138XXXX5555text>view>
            <uni-icons type="arrowright" size="16">uni-icons>
          view>
        view>
        <view class="row2">
          <view class="row2-left">收货地址:view>
          <view class="row2-right">河北省邯郸市肥乡区xxx 河北省邯郸市肥乡区xxx 河北省邯郸市肥乡区xxx 河北省邯郸市肥乡区xxx view>
        view>
      view>
    
      
      <image src="/static/[email protected]" class="address-border">image>
    view>
    
  3. 美化收货地址组件的样式:

    // 底部边框线的样式
    .address-border {
      display: block;
      width: 100%;
      height: 5px;
    }
    
    // 选择收货地址的盒子
    .address-choose-box {
      height: 90px;
      display: flex;
      align-items: center;
      justify-content: center;
    }
    
    // 渲染收货信息的盒子
    .address-info-box {
      font-size: 12px;
      height: 90px;
      display: flex;
      flex-direction: column;
      justify-content: center;
      padding: 0 5px;
    
      // 第一行
      .row1 {
        display: flex;
        justify-content: space-between;
    
        .row1-right {
          display: flex;
          align-items: center;
    
          .phone {
            margin-right: 5px;
          }
        }
      }
    
      // 第二行
      .row2 {
        display: flex;
        align-items: center;
        margin-top: 10px;
    
        .row2-left {
          white-space: nowrap;
        }
      }
    }
    

#9.2.2 实现收货地址区域的按需展示

  1. 在 data 中定义收货地址的信息对象:

    export default {
      data() {
        return {
          // 收货地址
          address: {},
        }
      },
    }
    
  2. 使用 v-ifv-else 实现按需展示:

    
    <view class="address-choose-box" v-if="JSON.stringify(address) === '{}'">
      <button type="primary" size="mini" class="btnChooseAddress">请选择收货地址+button>
    view>
    
    
    <view class="address-info-box" v-else>
      
    view>
    

#9.2.3 实现选择收货地址的功能

  1. 请选择收货地址+button 按钮绑定点击事件处理函数:

    
    <view class="address-choose-box" v-if="JSON.stringify(address) === '{}'">
      <button type="primary" size="mini" class="btnChooseAddress" @click="chooseAddress">请选择收货地址+button>
    view>
    
  2. 定义 chooseAddress 事件处理函数,调用小程序提供的 chooseAddress() API 实现选择收货地址的功能:

    methods: {
      // 选择收货地址
      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
        }
      }
    }
    
  3. 定义收货详细地址的计算属性:

    computed: {
      // 收货详细地址的计算属性
      addstr() {
        if (!this.address.provinceName) return ''
    
        // 拼接 省,市,区,详细地址 的字符串并返回给用户
        return this.address.provinceName + this.address.cityName + this.address.countyName + this.address.detailInfo
      }
    }
    
  4. 渲染收货地址区域的数据:

    
    <view class="address-info-box" v-else>
      <view class="row1">
        <view class="row1-left">
          <view class="username">收货人:<text>{{address.userName}}text>view>
        view>
        <view class="row1-right">
          <view class="phone">电话:<text>{{address.telNumber}}text>view>
          <uni-icons type="arrowright" size="16">uni-icons>
        view>
      view>
      <view class="row2">
        <view class="row2-left">收货地址:view>
        <view class="row2-right">{{addstr}}view>
      view>
    view>
    

#9.2.4 将 address 信息存储到 vuex 中

  1. store 目录中,创建用户相关的 vuex 模块,命名为 user.js

    export default {
      // 开启命名空间
      namespaced: true,
    
      // state 数据
      state: () => ({
        // 收货地址
        address: {},
      }),
    
      // 方法
      mutations: {
        // 更新收货地址
        updateAddress(state, address) {
          state.address = address
        },
      },
    
      // 数据包装器
      getters: {},
    }
    
  2. store/store.js 模块中,导入并挂载 user.js 模块:

    // 1. 导入 Vue 和 Vuex
    import Vue from 'vue'
    import Vuex from 'vuex'
    // 导入购物车的 vuex 模块
    import moduleCart from './cart.js'
    // 导入用户的 vuex 模块
    import moduleUser from './user.js'
    
    // 2. 将 Vuex 安装为 Vue 的插件
    Vue.use(Vuex)
    
    // 3. 创建 Store 的实例对象
    const store = new Vuex.Store({
      // TODO:挂载 store 模块
      modules: {
        // 挂载购物车的 vuex 模块,模块内成员的访问路径被调整为 m_cart,例如:
        // 购物车模块中 cart 数组的访问路径是 m_cart/cart
        m_cart: moduleCart,
        // 挂载用户的 vuex 模块,访问路径为 m_user
        m_user: moduleUser,
      },
    })
    
    // 4. 向外共享 Store 的实例对象
    export default store
    
  3. 改造 address.vue 组件中的代码,使用 vuex 提供的 address 计算属性 替代 data 中定义的本地 address 对象

    // 1. 按需导入 mapState 和 mapMutations 这两个辅助函数
    import { mapState, mapMutations } from 'vuex'
    
    export default {
      data() {
        return {
          // 2.1 注释掉下面的 address 对象,使用 2.2 中的代码替代之
          // address: {}
        }
      },
      methods: {
        // 3.1 把 m_user 模块中的 updateAddress 函数映射到当前组件
        ...mapMutations('m_user', ['updateAddress']),
        // 选择收货地址
        async chooseAddress() {
          const [err, succ] = await uni.chooseAddress().catch((err) => err)
    
          // 用户成功的选择了收货地址
          if (err === null && succ.errMsg === 'chooseAddress:ok') {
            // 3.2 把下面这行代码注释掉,使用 3.3 中的代码替代之
            // this.address = succ
    
            // 3.3 调用 Store 中提供的 updateAddress 方法,将 address 保存到 Store 里面
            this.updateAddress(succ)
          }
        },
      },
      computed: {
        // 2.2 把 m_user 模块中的 address 对象映射当前组件中使用,代替 data 中 address 对象
        ...mapState('m_user', ['address']),
        // 收货详细地址的计算属性
        addstr() {
          if (!this.address.provinceName) return ''
    
          // 拼接 省,市,区,详细地址 的字符串并返回给用户
          return this.address.provinceName + this.address.cityName + this.address.countyName + this.address.detailInfo
        },
      },
    }
    

#9.2.5 将 Store 中的 address 持久化存储到本地

  1. 修改 store/user.js 模块中的代码如下:

    export default {
      // 开启命名空间
      namespaced: true,
    
      // state 数据
      state: () => ({
        // 3. 读取本地的收货地址数据,初始化 address 对象
        address: JSON.parse(uni.getStorageSync('address') || '{}'),
      }),
    
      // 方法
      mutations: {
        // 更新收货地址
        updateAddress(state, address) {
          state.address = address
    
          // 2. 通过 this.commit() 方法,调用 m_user 模块下的 saveAddressToStorage 方法将 address 对象持久化存储到本地
          this.commit('m_user/saveAddressToStorage')
        },
        // 1. 定义将 address 持久化存储到本地 mutations 方法
        saveAddressToStorage(state) {
          uni.setStorageSync('address', JSON.stringify(state.address))
        },
      },
    
      // 数据包装器
      getters: {},
    }
    

#9.2.6 将 addstr 抽离为 getters

目的:为了提高代码的复用性,可以把收货的详细地址抽离为 getters,方便在多个页面和组件之间实现复用。

  1. 剪切 my-address.vue 组件中的 addstr 计算属性的代码,粘贴到 user.js 模块中,作为一个 getters 节点:

    // 数据包装器
    getters: {
      // 收货详细地址的计算属性
      addstr(state) {
        if (!state.address.provinceName) return ''
    
        // 拼接 省,市,区,详细地址 的字符串并返回给用户
        return state.address.provinceName + state.address.cityName + state.address.countyName + state.address.detailInfo
      }
    }
    
  2. 改造 my-address.vue 组件中的代码,通过 mapGetters 辅助函数,将 m_user 模块中的 addstr 映射到当前组件中使用:

    // 按需导入 mapGetters 辅助函数
    import { mapState, mapMutations, mapGetters } from 'vuex'
    
    export default {
      // 省略其它代码
      computed: {
        ...mapState('m_user', ['address']),
        // 将 m_user 模块中的 addstr 映射到当前组件中使用
        ...mapGetters('m_user', ['addstr']),
      },
    }
    

#9.2.7 重新选择收货地址

  1. 为 class 类名为 address-info-box 的盒子绑定 click 事件处理函数如下:

    
    <view class="address-info-box" v-else @click="chooseAddress">
      
    view>
    

#9.2.8 解决收货地址授权失败的问题

如果在选择收货地址的时候,用户点击了取消授权,则需要进行特殊的处理,否则用户将无法再次选择收货地址

  1. 改造 chooseAddress 方法如下:

    // 选择收货地址
    async chooseAddress() {
      // 1. 调用小程序提供的 chooseAddress() 方法,即可使用选择收货地址的功能
      //    返回值是一个数组:第1项为错误对象;第2项为成功之后的收货地址对象
      const [err, succ] = await uni.chooseAddress().catch(err => err)
    
      // 2. 用户成功的选择了收货地址
      if (succ && succ.errMsg === 'chooseAddress:ok') {
        // 更新 vuex 中的收货地址
        this.updateAddress(succ)
      }
    
      // 3. 用户没有授权
      if (err && err.errMsg === 'chooseAddress:fail auth deny') {
        this.reAuth() // 调用 this.reAuth() 方法,向用户重新发起授权申请
      }
    }
    
  2. methods 节点中声明 reAuth 方法如下:

    // 调用此方法,重新发起收货地址的授权
    async reAuth() {
      // 3.1 提示用户对地址进行授权
      const [err2, confirmResult] = await uni.showModal({
        content: '检测到您没打开地址权限,是否去设置打开?',
        confirmText: "确认",
        cancelText: "取消",
      })
    
      // 3.2 如果弹框异常,则直接退出
      if (err2) return
    
      // 3.3 如果用户点击了 “取消” 按钮,则提示用户 “您取消了地址授权!”
      if (confirmResult.cancel) return uni.$showMsg('您取消了地址授权!')
    
      // 3.4 如果用户点击了 “确认” 按钮,则调用 uni.openSetting() 方法进入授权页面,让用户重新进行授权
      if (confirmResult.confirm) return uni.openSetting({
        // 3.4.1 授权结束,需要对授权的结果做进一步判断
        success: (settingResult) => {
          // 3.4.2 地址授权的值等于 true,提示用户 “授权成功”
          if (settingResult.authSetting['scope.address']) return uni.$showMsg('授权成功!请选择地址')
          // 3.4.3 地址授权的值等于 false,提示用户 “您取消了地址授权”
          if (!settingResult.authSetting['scope.address']) return uni.$showMsg('您取消了地址授权!')
        }
      })
    }
    

#9.2.9 解决 iPhone 真机上无法重新授权的问题

问题说明:在 iPhone 设备上,当用户取消授权之后,再次点击选择收货地址按钮的时候,无法弹出授权的提示框!

  1. 导致问题的原因 - 用户取消授权后,再次点击 “选择收货地址” 按钮的时候:

    • 模拟器安卓真机上,错误消息 err.errMsg 的值为 chooseAddress:fail auth deny
    • iPhone 真机上,错误消息 err.errMsg 的值为 chooseAddress:fail authorize no response
  2. 解决问题的方案 - 修改 chooseAddress 方法中的代码,进一步完善用户没有授权时的 if 判断条件即可:

    async chooseAddress() {
      // 1. 调用小程序提供的 chooseAddress() 方法,即可使用选择收货地址的功能
      //    返回值是一个数组:第1项为错误对象;第2项为成功之后的收货地址对象
      const [err, succ] = await uni.chooseAddress().catch(err => err)
    
      // 2. 用户成功的选择了收货地址
      if (succ && succ.errMsg === 'chooseAddress:ok') {
        this.updateAddress(succ)
      }
    
      // 3. 用户没有授权
      if (err && (err.errMsg === 'chooseAddress:fail auth deny' || err.errMsg === 'chooseAddress:fail authorize no response')) {
        this.reAuth()
      }
    }
    

9.3 结算区域

#9.3.1 把结算区域封装为组件

  1. components 目录中,新建 my-settle 结算组件:

    微信小程序_第161张图片

  2. 初始化 my-settle 组件的基本结构和样式:

    
    
    
    
    
    
  3. cart.vue 页面中使用自定义的 my-settle 组件,并美化页面样式,防止页面底部被覆盖:

    
    
    
    

#9.3.2 渲染结算区域的结构和样式

  1. 定义如下的 UI 结构:

    
    <view class="my-settle-container">
      
      <label class="radio">
        <radio color="#C00000" :checked="true" /><text>全选text>
      label>
    
      
      <view class="amount-box">
        合计:<text class="amount">¥1234.00text>
      view>
    
      
      <view class="btn-settle">结算(0)view>
    view>
    
  2. 美化样式:

    .my-settle-container {
      position: fixed;
      bottom: 0;
      left: 0;
      width: 100%;
      height: 50px;
      // 将背景色从 cyan 改为 white
      background-color: white;
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding-left: 5px;
      font-size: 14px;
    
      .radio {
        display: flex;
        align-items: center;
      }
    
      .amount {
        color: #c00000;
      }
    
      .btn-settle {
        height: 50px;
        min-width: 100px;
        background-color: #c00000;
        color: white;
        line-height: 50px;
        text-align: center;
        padding: 0 10px;
      }
    }
    

#9.3.3 动态渲染已勾选商品的总数量

  1. store/cart.js 模块中,定义一个名称为 checkedCount 的 getters,用来统计已勾选商品的总数量:

    // 勾选的商品的总数量
    checkedCount(state) {
      // 先使用 filter 方法,从购物车中过滤器已勾选的商品
      // 再使用 reduce 方法,将已勾选的商品总数量进行累加
      // reduce() 的返回值就是已勾选的商品的总数量
      return state.cart.filter(x => x.goods_state).reduce((total, item) => total += item.goods_count, 0)
    }
    
  2. my-settle 组件中,通过 mapGetters 辅助函数,将需要的 getters 映射到当前组件中使用:

    import { mapGetters } from 'vuex'
    
    export default {
      computed: {
        ...mapGetters('m_cart', ['checkedCount']),
      },
      data() {
        return {}
      },
    }
    
  3. checkedCount 的值渲染到页面中:

    
    <view class="btn-settle">结算({{checkedCount}})view>
    

#9.3.4 动态渲染全选按钮的选中状态

  1. 使用 mapGetters 辅助函数,将商品的总数量映射到当前组件中使用,并定义一个叫做 isFullCheck 的计算属性:

    import { mapGetters } from 'vuex'
    
    export default {
      computed: {
        // 1. 将 total 映射到当前组件中
        ...mapGetters('m_cart', ['checkedCount', 'total']),
        // 2. 是否全选
        isFullCheck() {
          return this.total === this.checkedCount
        },
      },
      data() {
        return {}
      },
    }
    
  2. 为 radio 组件动态绑定 checked 属性的值:

    
    <label class="radio">
      <radio color="#C00000" :checked="isFullCheck" /><text>全选text>
    label>
    

#9.3.5 实现商品的全选/反选功能

  1. store/cart.js 模块中,定义一个叫做 updateAllGoodsState 的 mutations 方法,用来修改所有商品的勾选状态:

    // 更新所有商品的勾选状态
    updateAllGoodsState(state, newState) {
      // 循环更新购物车中每件商品的勾选状态
      state.cart.forEach(x => x.goods_state = newState)
      // 持久化存储到本地
      this.commit('m_cart/saveToStorage')
    }
    
  2. my-settle 组件中,通过 mapMutations 辅助函数,将需要的 mutations 方法映射到当前组件中使用:

    // 1. 按需导入 mapMutations 辅助函数
    import { mapGetters, mapMutations } from 'vuex'
    
    export default {
      // 省略其它代码
      methods: {
        // 2. 使用 mapMutations 辅助函数,把 m_cart 模块提供的 updateAllGoodsState 方法映射到当前组件中使用
        ...mapMutations('m_cart', ['updateAllGoodsState']),
      },
    }
    
  3. 为 UI 中的 label 组件绑定 click 事件处理函数:

    
    <label class="radio" @click="changeAllState">
      <radio color="#C00000" :checked="isFullCheck" /><text>全选text>
    label>
    
  4. my-settle 组件的 methods 节点中,声明 changeAllState 事件处理函数:

    methods: {
      ...mapMutations('m_cart', ['updateAllGoodsState']),
      // label 的点击事件处理函数
      changeAllState() {
        // 修改购物车中所有商品的选中状态
        // !this.isFullCheck 表示:当前全选按钮的状态取反之后,就是最新的勾选状态
        this.updateAllGoodsState(!this.isFullCheck)
      }
    }
    

#9.3.6 动态渲染已勾选商品的总价格

  1. store/cart.js 模块中,定义一个叫做 checkedGoodsAmount 的 getters,用来统计已勾选商品的总价格:

    // 已勾选的商品的总价
    checkedGoodsAmount(state) {
      // 先使用 filter 方法,从购物车中过滤器已勾选的商品
      // 再使用 reduce 方法,将已勾选的商品数量 * 单价之后,进行累加
      // reduce() 的返回值就是已勾选的商品的总价
      // 最后调用 toFixed(2) 方法,保留两位小数
      return state.cart.filter(x => x.goods_state)
                       .reduce((total, item) => total += item.goods_count * item.goods_price, 0)
                       .toFixed(2)
    }
    
  2. my-settle 组件中,使用 mapGetters 辅助函数,把需要的 checkedGoodsAmount 映射到当前组件中使用:

    ...mapGetters('m_cart', ['total', 'checkedCount', 'checkedGoodsAmount'])
    
  3. 在组件的 UI 结构中,渲染已勾选的商品的总价:

    
    <view class="amount-box">
      合计:<text class="amount">¥{{checkedGoodsAmount}}text>
    view>
    

#9.3.7 动态计算购物车徽标的数值

  1. 问题说明:当修改购物车中商品的数量之后,tabBar 上的数字徽标不会自动更新。

  2. 解决方案:改造 mixins/tabbar-badge.js 中的代码,使用 watch 侦听器,监听 total 总数量的变化,从而动态为 tabBar 的徽标赋值:

    import { mapGetters } from 'vuex'
    
    // 导出一个 mixin 对象
    export default {
      computed: {
        ...mapGetters('m_cart', ['total']),
      },
      watch: {
        // 监听 total 值的变化
        total() {
          // 调用 methods 中的 setBadge 方法,重新为 tabBar 的数字徽章赋值
          this.setBadge()
        },
      },
      onShow() {
        // 在页面刚展示的时候,设置数字徽标
        this.setBadge()
      },
      methods: {
        setBadge() {
          // 调用 uni.setTabBarBadge() 方法,为购物车设置右上角的徽标
          uni.setTabBarBadge({
            index: 2,
            text: this.total + '', // 注意:text 的值必须是字符串,不能是数字
          })
        },
      },
    }
    

#9.3.8 渲染购物车为空时的页面结构

  1. 资料 目录中的 [email protected] 图片复制到项目的 /static/ 目录中

  2. 改造 cart.vue 页面的 UI 结构,使用 v-ifv-else 控制购物车区域空白购物车区域的按需展示:

    <template>
      <view class="cart-container" v-if="cart.length !== 0">
    
        
    
        
    
        
    
        
    
      view>
    
      
      <view class="empty-cart" v-else>
        <image src="/static/[email protected]" class="empty-img">image>
        <text class="tip-text">空空如也~text>
      view>
    template>
    
  3. 美化空白购物车区域的样式:

    .empty-cart {
      display: flex;
      flex-direction: column;
      align-items: center;
      padding-top: 150px;
    
      .empty-img {
        width: 90px;
        height: 90px;
      }
    
      .tip-text {
        font-size: 12px;
        color: gray;
        margin-top: 15px;
      }
    }
    

#9.4 分支的合并与提交

  1. cart 分支进行本地提交:

    git add .
    git commit -m "完成了购物车的开发"
    
  2. 将本地的 cart 分支推送到码云:

    git push -u origin cart
    
  3. 将本地 cart 分支中的代码合并到 master 分支:

    git checkout master
    git merge cart
    git push
    
  4. 删除本地的 cart 分支:

    git branch -d cart
    

10. 登录与支付

#10.0 创建 settle 分支

运行如下的命令,基于 master 分支在本地创建 settle 子分支,用来开发登录与支付相关的功能:

git checkout -b settle

#10.1 点击结算按钮进行条件判断

说明:用户点击了结算按钮之后,需要先后判断是否勾选了要结算的商品是否选择了收货地址是否登录

  1. my-settle 组件中,为结算按钮绑定点击事件处理函数:

    
    <view class="btn-settle" @click="settlement">结算({{checkedCount}})view>
    
  2. my-settle 组件的 methods 节点中声明 settlement 事件处理函数如下:

    // 点击了结算按钮
    settlement() {
      // 1. 先判断是否勾选了要结算的商品
      if (!this.checkedCount) return uni.$showMsg('请选择要结算的商品!')
    
      // 2. 再判断用户是否选择了收货地址
      if (!this.addstr) return uni.$showMsg('请选择收货地址!')
    
      // 3. 最后判断用户是否登录了
      if (!this.token) return uni.$showMsg('请先登录!')
    }
    
  3. my-settle 组件中,使用 mapGetters 辅助函数,从 m_user 模块中将 addstr 映射到当前组件中使用:

    export default {
      computed: {
        ...mapGetters('m_cart', ['total', 'checkedCount', 'checkedGoodsAmount']),
        // addstr 是详细的收货地址
        ...mapGetters('m_user', ['addstr']),
        isFullCheck() {
          return this.total === this.checkedCount
        },
      },
    }
    
  4. store/user.js 模块的 state 节点中,声明 token 字符串:

    export default {
      // 开启命名空间
      namespaced: true,
    
      // state 数据
      state: () => ({
        // 收货地址
        address: JSON.parse(uni.getStorageSync('address') || '{}'),
        // 登录成功之后的 token 字符串
        token: '',
      }),
    
      // 省略其它代码
    }
    
  5. my-settle 组件中,使用 mapState 辅助函数,从 m_user 模块中将 token 映射到当前组件中使用:

    // 按需从 vuex 中导入 mapState 辅助函数
    import { mapGetters, mapMutations, mapState } from 'vuex'
    
    export default {
      computed: {
        ...mapGetters('m_cart', ['total', 'checkedCount', 'checkedGoodsAmount']),
        ...mapGetters('m_user', ['addstr']),
        // token 是用户登录成功之后的 token 字符串
        ...mapState('m_user', ['token']),
        isFullCheck() {
          return this.total === this.checkedCount
        },
      },
    }
    

#10.2 登录

#10.2.1 定义 my 页面的编译模式

  1. 点击 微信开发者工具 工具栏上的编译模式下拉菜单,选择 添加编译模式
    微信小程序_第162张图片
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QxugvxBK-

  2. 勾选启动页面的路径之后,点击确定按钮:

    微信小程序_第163张图片

#10.2.2 实现登录和用户信息组件的按需展示

  1. components 目录中新建登录组件

    微信小程序_第164张图片

  2. components 目录中新建用户信息组件

    微信小程序_第165张图片

  3. my.vue 页面中,通过 mapState 辅助函数,导入需要的 token 字符串:

    import badgeMix from '@/mixins/tabbar-badge.js'
    // 1. 从 vuex 中按需导入 mapState 辅助函数
    import { mapState } from 'vuex'
    
    export default {
      mixins: [badgeMix],
      computed: {
        // 2. 从 m_user 模块中导入需要的 token 字符串
        ...mapState('m_user', ['token']),
      },
      data() {
        return {}
      },
    }
    
  4. my.vue 页面中,实现登录组件用户信息组件的按需展示:

    <template>
      <view>
    
        
        <my-login v-if="!token">my-login>
    
        
        <my-userinfo v-else>my-userinfo>
    
      view>
    template>
    

#10.2.3 实现登录组件的基本布局

  1. my-login 组件定义如下的 UI 结构:

    <template>
      <view class="login-container">
        
        <uni-icons type="contact-filled" size="100" color="#AFAFAF">uni-icons>
        
        <button type="primary" class="btn-login">一键登录button>
        
        <view class="tips-text">登录后尽享更多权益view>
      view>
    template>
    
  2. 美化登录组件的样式:

    .login-container {
      // 登录盒子的样式
      height: 750rpx;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      background-color: #f8f8f8;
      position: relative;
      overflow: hidden;
    
      // 绘制登录盒子底部的半椭圆造型
      &::after {
        content: ' ';
        display: block;
        position: absolute;
        width: 100%;
        height: 40px;
        left: 0;
        bottom: 0;
        background-color: white;
        border-radius: 100%;
        transform: translateY(50%);
      }
    
      // 登录按钮的样式
      .btn-login {
        width: 90%;
        border-radius: 100px;
        margin: 15px 0;
        background-color: #c00000;
      }
    
      // 按钮下方提示消息的样式
      .tips-text {
        font-size: 12px;
        color: gray;
      }
    }
    

#10.2.4 点击登录按钮获取微信用户的基本信息

需求描述:需要获取微信用户的头像昵称等基本信息。

  1. 为登录的 button 按钮绑定 open-type="getUserInfo" 属性,表示点击按钮时,希望获取用户的基本信息:

    
    
    <button type="primary" class="btn-login" open-type="getUserInfo" @getuserinfo="getUserInfo">一键登录button>
    
  2. methods 节点中声明 getUserInfo 事件处理函数如下:

    methods: {
      // 获取微信用户的基本信息
      getUserInfo(e) {
        // 判断是否获取用户信息成功
        if (e.detail.errMsg === 'getUserInfo:fail auth deny') return uni.$showMsg('您取消了登录授权!')
    
        // 获取用户信息成功, e.detail.userInfo 就是用户的基本信息
        console.log(e.detail.userInfo)
      }
    }
    

#10.2.5 将用户的基本信息存储到 vuex

  1. store/user.js 模块的 state 节点中,声明 userinfo 的信息对象如下:

    // state 数据
    state: () => ({
      // 收货地址
      // address: {}
      address: JSON.parse(uni.getStorageSync('address') || '{}'),
      // 登录成功之后的 token 字符串
      token: '',
      // 用户的基本信息
      userinfo: JSON.parse(uni.getStorageSync('userinfo') || '{}')
    }),
    
  2. store/user.js 模块的 mutations 节点中,声明如下的两个方法:

    // 方法
    mutations: {
      // 省略其它代码...
    
      // 更新用户的基本信息
      updateUserInfo(state, userinfo) {
        state.userinfo = userinfo
        // 通过 this.commit() 方法,调用 m_user 模块下的 saveUserInfoToStorage 方法,将 userinfo 对象持久化存储到本地
        this.commit('m_user/saveUserInfoToStorage')
      },
    
      // 将 userinfo 持久化存储到本地
      saveUserInfoToStorage(state) {
        uni.setStorageSync('userinfo', JSON.stringify(state.userinfo))
      }
    }
    
  3. 使用 mapMutations 辅助函数,将需要的方法映射到 my-login 组件中使用:

    // 1. 按需导入 mapMutations 辅助函数
    import { mapMutations } from 'vuex'
    
    export default {
      data() {
        return {}
      },
      methods: {
        // 2. 调用 mapMutations 辅助方法,把 m_user 模块中的 updateUserInfo 映射到当前组件中使用
        ...mapMutations('m_user', ['updateUserInfo']),
        // 获取微信用户的基本信息
        getUserInfo(e) {
          // 判断是否获取用户信息成功
          if (e.detail.errMsg === 'getUserInfo:fail auth deny') return uni.$showMsg('您取消了登录授权!')
          // 获取用户信息成功, e.detail.userInfo 就是用户的基本信息
          // console.log(e.detail.userInfo)
    
          // 3. 将用户的基本信息存储到 vuex 中
          this.updateUserInfo(e.detail.userInfo)
        },
      },
    }
    

#10.2.6 登录获取 Token 字符串

需求说明:当获取到了微信用户的基本信息之后,还需要进一步调用登录相关的接口,从而换取登录成功之后的 Token 字符串

  1. getUserInfo 方法中,预调用 this.getToken() 方法,同时把获取到的用户信息传递进去:

    // 获取微信用户的基本信息
    getUserInfo(e) {
      // 判断是否获取用户信息成功
      if (e.detail.errMsg === 'getUserInfo:fail auth deny') return uni.$showMsg('您取消了登录授权!')
    
      // 将用户的基本信息存储到 vuex 中
      this.updateUserInfo(e.detail.userInfo)
    
      // 获取登录成功后的 Token 字符串
      this.getToken(e.detail)
    }
    
  2. methods 中定义 getToken 方法,调用登录相关的 API,实现登录的功能:

    // 调用登录接口,换取永久的 token
    async getToken(info) {
      // 调用微信登录接口
      const [err, res] = await uni.login().catch(err => err)
      // 判断是否 uni.login() 调用失败
      if (err || res.errMsg !== 'login:ok') return uni.$showError('登录失败!')
    
      // 准备参数对象
      const query = {
        code: res.code,
        encryptedData: info.encryptedData,
        iv: info.iv,
        rawData: info.rawData,
        signature: info.signature
      }
    
      // 换取 token
      const { data: loginResult } = await uni.$http.post('/api/public/v1/users/wxlogin', query)
      if (loginResult.meta.status !== 200) return uni.$showMsg('登录失败!')
      uni.$showMsg('登录成功')
    }
    

#10.2.7 将 Token 存储到 vuex

  1. store/user.js 模块的 mutations 节点中,声明如下的两个方法:

    mutations: {
      // 省略其它代码...
    
      // 更新 token 字符串
      updateToken(state, token) {
        state.token = token
        // 通过 this.commit() 方法,调用 m_user 模块下的 saveTokenToStorage 方法,将 token 字符串持久化存储到本地
        this.commit('m_user/saveTokenToStorage')
      },
    
      // 将 token 字符串持久化存储到本地
      saveTokenToStorage(state) {
        uni.setStorageSync('token', state.token)
      }
    }
    
  2. 修改 store/user.js 模块的 state 节点如下:

    // state 数据
    state: () => ({
      // 收货地址
      address: JSON.parse(uni.getStorageSync('address') || '{}'),
      // 登录成功之后的 token 字符串
      token: uni.getStorageSync('token') || '',
      // 用户的基本信息
      userinfo: JSON.parse(uni.getStorageSync('userinfo') || '{}')
    }),
    
  3. my-login 组件中,把 vuex 中的 updateToken 方法映射到当前组件中使用:

    methods: {
      // 1. 使用 mapMutations 辅助方法,把 m_user 模块中的 updateToken 方法映射到当前组件中使用
      ...mapMutations('m_user', ['updateUserInfo', 'updateToken'])
    
      // 省略其它代码...
    
      // 调用登录接口,换取永久的 token
      async getToken(info) {
        // 调用微信登录接口
        const [err, res] = await uni.login().catch(err => err)
        // 判断是否 uni.login() 调用失败
        if (err || res.errMsg !== 'login:ok') return uni.$showError('登录失败!')
    
        // 准备参数对象
        const query = {
          code: res.code,
          encryptedData: info.encryptedData,
          iv: info.iv,
          rawData: info.rawData,
          signature: info.signature
        }
    
        // 换取 token
        const { data: loginResult } = await uni.$http.post('/api/public/v1/users/wxlogin', query)
        if (loginResult.meta.status !== 200) return uni.$showMsg('登录失败!')
    
        // 2. 更新 vuex 中的 token
        this.updateToken(loginResult.message.token)
      }
    }
    

#10.3 用户信息

#10.3.1 实现用户头像昵称区域的基本布局

  1. my-userinfo 组件中,定义如下的 UI 结构:

    <template>
      <view class="my-userinfo-container">
    
        
        <view class="top-box">
          <image src="" class="avatar">image>
          <view class="nickname">xxxview>
        view>
    
      view>
    template>
    
  2. 美化当前组件的样式:

    .my-userinfo-container {
      height: 100%;
      // 为整个组件的结构添加浅灰色的背景
      background-color: #f4f4f4;
    
      .top-box {
        height: 400rpx;
        background-color: #c00000;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
    
        .avatar {
          display: block;
          width: 90px;
          height: 90px;
          border-radius: 45px;
          border: 2px solid white;
          box-shadow: 0 1px 5px black;
        }
    
        .nickname {
          color: white;
          font-weight: bold;
          font-size: 16px;
          margin-top: 10px;
        }
      }
    }
    
  3. my.vue 页面中,为最外层包裹性质的 view 容器,添加 class="my-container" 的类名,并美化样式如下:

    page,
    .my-container {
      height: 100%;
    }
    

#10.3.2 渲染用户的头像和昵称

  1. my-userinfo 组件中,通过 mapState 辅助函数,将需要的成员映射到当前组件中使用:

    // 按需导入 mapState 辅助函数
    import { mapState } from 'vuex'
    
    export default {
      computed: {
        // 将 m_user 模块中的 userinfo 映射到当前页面中使用
        ...mapState('m_user', ['userinfo']),
      },
      data() {
        return {}
      },
    }
    
  2. 将用户的头像和昵称渲染到页面中:

    
    <view class="top-box">
      <image :src="userinfo.avatarUrl" class="avatar">image>
      <view class="nickname">{{userinfo.nickName}}view>
    view>
    

#10.3.3 渲染第一个面板区域

  1. my-userinfo 组件中,定义如下的 UI 结构:

    
    <view class="panel-list">
      
      <view class="panel">
        
        <view class="panel-body">
          
          <view class="panel-item">
            <text>8text>
            <text>收藏的店铺text>
          view>
          <view class="panel-item">
            <text>14text>
            <text>收藏的商品text>
          view>
          <view class="panel-item">
            <text>18text>
            <text>关注的商品text>
          view>
          <view class="panel-item">
            <text>84text>
            <text>足迹text>
          view>
        view>
      view>
    
      
    
      
    view>
    
  2. 美化第一个面板的样式:

    .panel-list {
      padding: 0 10px;
      position: relative;
      top: -10px;
    
      .panel {
        background-color: white;
        border-radius: 3px;
        margin-bottom: 8px;
    
        .panel-body {
          display: flex;
          justify-content: space-around;
    
          .panel-item {
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: space-around;
            font-size: 13px;
            padding: 10px 0;
          }
        }
      }
    }
    

#10.3.4 渲染第二个面板区域

  1. 定义第二个面板区域的 UI 结构:

    
    <view class="panel">
      
      <view class="panel-title">我的订单view>
      
      <view class="panel-body">
        
        <view class="panel-item">
          <image src="/static/my-icons/icon1.png" class="icon">image>
          <text>待付款text>
        view>
        <view class="panel-item">
          <image src="/static/my-icons/icon2.png" class="icon">image>
          <text>待收货text>
        view>
        <view class="panel-item">
          <image src="/static/my-icons/icon3.png" class="icon">image>
          <text>退款/退货text>
        view>
        <view class="panel-item">
          <image src="/static/my-icons/icon4.png" class="icon">image>
          <text>全部订单text>
        view>
      view>
    view>
    
  2. 对之前的 SCSS 样式进行改造,从而美化第二个面板的样式:

    .panel-list {
      padding: 0 10px;
      position: relative;
      top: -10px;
    
      .panel {
        background-color: white;
        border-radius: 3px;
        margin-bottom: 8px;
    
        .panel-title {
          line-height: 45px;
          padding-left: 10px;
          font-size: 15px;
          border-bottom: 1px solid #f4f4f4;
        }
    
        .panel-body {
          display: flex;
          justify-content: space-around;
    
          .panel-item {
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: space-around;
            font-size: 13px;
            padding: 10px 0;
    
            .icon {
              width: 35px;
              height: 35px;
            }
          }
        }
      }
    }
    

#10.3.5 渲染第三个面板区域

  1. 定义第三个面板区域的 UI 结构:

    
    <view class="panel">
      <view class="panel-list-item">
        <text>收货地址text>
        <uni-icons type="arrowright" size="15">uni-icons>
      view>
      <view class="panel-list-item">
        <text>联系客服text>
        <uni-icons type="arrowright" size="15">uni-icons>
      view>
      <view class="panel-list-item">
        <text>退出登录text>
        <uni-icons type="arrowright" size="15">uni-icons>
      view>
    view>
    
  2. 美化第三个面板区域的样式:

    .panel-list-item {
      height: 45px;
      display: flex;
      justify-content: space-between;
      align-items: center;
      font-size: 15px;
      padding: 0 10px;
    }
    

#10.3.6 实现退出登录的功能

  1. 为第三个面板区域中的 退出登录 项绑定 click 点击事件处理函数:

    <view class="panel-list-item" @click="logout">
      <text>退出登录text>
      <uni-icons type="arrowright" size="15">uni-icons>
    view>
    
  2. my-userinfo 组件的 methods 节点中定义 logout 事件处理函数:

    // 退出登录
    async logout() {
      // 询问用户是否退出登录
      const [err, succ] = await uni.showModal({
        title: '提示',
        content: '确认退出登录吗?'
      }).catch(err => err)
    
      if (succ && succ.confirm) {
         // 用户确认了退出登录的操作
         // 需要清空 vuex 中的 userinfo、token 和 address
         this.updateUserInfo({})
         this.updateToken('')
         this.updateAddress({})
      }
    }
    
  3. 使用 mapMutations 辅助方法,将需要用到的 mutations 方法映射到当前组件中:

    // 按需导入辅助函数
    import { mapState, mapMutations } from 'vuex'
    
    export default {
      methods: {
        ...mapMutations('m_user', ['updateUserInfo', 'updateToken', 'updateAddress']),
      },
    }
    

#10.4 三秒后自动跳转

#10.4.1 三秒后自动跳转到登录页面

需求描述:在购物车页面,当用户点击 “结算” 按钮时,如果用户没有登录,则 3 秒后自动跳转到登录页面

  1. my-settle 组件的 methods 节点中,声明一个叫做 showTips 的方法,专门用来展示倒计时的提示消息:

    // 展示倒计时的提示消息
    showTips(n) {
      // 调用 uni.showToast() 方法,展示提示消息
      uni.showToast({
        // 不展示任何图标
        icon: 'none',
        // 提示的消息
        title: '请登录后再结算!' + n + ' 秒后自动跳转到登录页',
        // 为页面添加透明遮罩,防止点击穿透
        mask: true,
        // 1.5 秒后自动消失
        duration: 1500
      })
    }
    
  2. data 节点中声明倒计时的秒数:

    data() {
      return {
        // 倒计时的秒数
        seconds: 3
      }
    }
    
  3. 改造 结算 按钮的 click 事件处理函数,如果用户没有登录,则预调用一个叫做 delayNavigate 的方法,进行倒计时的导航跳转:

    // 点击了结算按钮
    settlement() {
      // 1. 先判断是否勾选了要结算的商品
      if (!this.checkedCount) return uni.$showMsg('请选择要结算的商品!')
    
      // 2. 再判断用户是否选择了收货地址
      if (!this.addstr) return uni.$showMsg('请选择收货地址!')
    
      // 3. 最后判断用户是否登录了,如果没有登录,则调用 delayNavigate() 进行倒计时的导航跳转
      // if (!this.token) return uni.$showMsg('请先登录!')
      if (!this.token) return this.delayNavigate()
    },
    
  4. 定义 delayNavigate 方法,初步实现倒计时的提示功能

    // 延迟导航到 my 页面
    delayNavigate() {
      // 1. 展示提示消息,此时 seconds 的值等于 3
      this.showTips(this.seconds)
    
      // 2. 创建定时器,每隔 1 秒执行一次
      setInterval(() => {
        // 2.1 先让秒数自减 1
        this.seconds--
        // 2.2 再根据最新的秒数,进行消息提示
        this.showTips(this.seconds)
      }, 1000)
    },
    

    上述代码的问题:定时器不会自动停止,此时秒数会出现等于 0 或小于 0 的情况!

  5. data 节点中声明定时器的 Id 如下:

    data() {
      return {
        // 倒计时的秒数
        seconds: 3,
        // 定时器的 Id
        timer: null
      }
    }
    
  6. 改造 delayNavigate 方法如下:

    // 延迟导航到 my 页面
    delayNavigate() {
      this.showTips(this.seconds)
    
      // 1. 将定时器的 Id 存储到 timer 中
      this.timer = setInterval(() => {
        this.seconds--
    
        // 2. 判断秒数是否 <= 0
        if (this.seconds <= 0) {
          // 2.1 清除定时器
          clearInterval(this.timer)
    
          // 2.2 跳转到 my 页面
          uni.switchTab({
            url: '/pages/my/my'
          })
    
          // 2.3 终止后续代码的运行(当秒数为 0 时,不再展示 toast 提示消息)
          return
        }
    
        this.showTips(this.seconds)
      }, 1000)
    },
    

    上述代码的问题:seconds 秒数不会被重置,导致第 2 次,3 次,n 次 的倒计时跳转功能无法正常工作

  7. 进一步改造 delayNavigate 方法,在执行此方法时,立即将 seconds 秒数重置为 3 即可:

    // 延迟导航到 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)
    }
    

#10.4.2 登录成功之后再返回之前的页面

核心实现思路:在自动跳转到登录页面成功之后,把返回页面的信息存储到 vuex 中,从而方便登录成功之后,根据返回页面的信息重新跳转回去。

返回页面的信息对象,主要包含 { openType, from } 两个属性,其中 openType 表示以哪种方式导航回之前的页面;from 表示之前页面的 url 地址

  1. store/user.js 模块的 state 节点中,声明一个叫做 redirectInfo 的对象如下:

    // state 数据
    state: () => ({
      // 收货地址
      address: JSON.parse(uni.getStorageSync('address') || '{}'),
      // 登录成功之后的 token 字符串
      token: uni.getStorageSync('token') || '',
      // 用户的基本信息
      userinfo: JSON.parse(uni.getStorageSync('userinfo') || '{}'),
      // 重定向的 object 对象 { openType, from }
      redirectInfo: null
    }),
    
  2. store/user.js 模块的 mutations 节点中,声明一个叫做 updateRedirectInfo 的方法:

    mutations: {
      // 更新重定向的信息对象
      updateRedirectInfo(state, info) {
        state.redirectInfo = info
      }
    }
    
  3. my-settle 组件中,通过 mapMutations 辅助方法,把 m_user 模块中的 updateRedirectInfo 方法映射到当前页面中使用:

    methods: {
      // 把 m_user 模块中的 updateRedirectInfo 方法映射到当前页面中使用
      ...mapMutations('m_user', ['updateRedirectInfo']),
    }
    
  4. 改造 my-settle 组件 methods 节点中的 delayNavigate 方法,当成功跳转到 my 页面 之后,将重定向的信息对象存储到 vuex 中:

    // 延迟导航到 my 页面
    delayNavigate() {
      // 把 data 中的秒数重置成 3 秒
      this.seconds = 3
      this.showTips(this.seconds)
    
      this.timer = setInterval(() => {
        this.seconds--
    
        if (this.seconds <= 0) {
          // 清除定时器
          clearInterval(this.timer)
          // 跳转到 my 页面
          uni.switchTab({
            url: '/pages/my/my',
            // 页面跳转成功之后的回调函数
            success: () => {
              // 调用 vuex 的 updateRedirectInfo 方法,把跳转信息存储到 Store 中
              this.updateRedirectInfo({
                // 跳转的方式
                openType: 'switchTab',
                // 从哪个页面跳转过去的
                from: '/pages/cart/cart'
              })
            }
          })
    
          return
        }
    
        this.showTips(this.seconds)
      }, 1000)
    }
    
  5. my-login 组件中,通过 mapStatemapMutations 辅助方法,将 vuex 中需要的数据和方法,映射到当前页面中使用:

    // 按需导入辅助函数
    import { mapMutations, mapState } from 'vuex'
    
    export default {
      computed: {
        // 调用 mapState 辅助方法,把 m_user 模块中的数据映射到当前用组件中使用
        ...mapState('m_user', ['redirectInfo']),
      },
      methods: {
        // 调用 mapMutations 辅助方法,把 m_user 模块中的方法映射到当前组件中使用
        ...mapMutations('m_user', ['updateUserInfo', 'updateToken', 'updateRedirectInfo']),
      },
    }
    
  6. 改造 my-login 组件中的 getToken 方法,当登录成功之后,预调用 this.navigateBack() 方法返回登录之前的页面:

    // 调用登录接口,换取永久的 token
    async getToken(info) {
      // 省略其它代码...
    
      // 判断 vuex 中的 redirectInfo 是否为 null
      // 如果不为 null,则登录成功之后,需要重新导航到对应的页面
      this.navigateBack()
    }
    
  7. my-login 组件中,声明 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)
          }
        })
      }
    }
    

#10.5 微信支付

#10.5.1 在请求头中添加 Token 身份认证的字段

  1. 原因说明:只有在登录之后才允许调用支付相关的接口,所以必须为有权限的接口添加身份认证的请求头字段

  2. 打开项目根目录下的 main.js,改造 $http.beforeRequest 请求拦截器中的代码如下:

    // 请求开始之前做一些事情
    $http.beforeRequest = function(options) {
      uni.showLoading({
        title: '数据加载中...',
      })
    
      // 判断请求的是否为有权限的 API 接口
      if (options.url.indexOf('/my/') !== -1) {
        // 为请求头添加身份认证字段
        options.header = {
          // 字段的值可以直接从 vuex 中进行获取
          Authorization: store.state.m_user.token,
        }
      }
    }
    

#10.5.2 微信支付的流程

  1. 创建订单
    • 请求创建订单的 API 接口:把(订单金额、收货地址、订单中包含的商品信息)发送到服务器
    • 服务器响应的结果:订单编号
  2. 订单预支付
    • 请求订单预支付的 API 接口:把(订单编号)发送到服务器
    • 服务器响应的结果:订单预支付的参数对象,里面包含了订单支付相关的必要参数
  3. 发起微信支付
    • 调用 uni.requestPayment() 这个 API,发起微信支付;把步骤 2 得到的 “订单预支付对象” 作为参数传递给 uni.requestPayment() 方法
    • 监听 uni.requestPayment() 这个 API 的 successfailcomplete 回调函数

#10.5.3 创建订单

  1. 改造 my-settle 组件中的 settlement 方法,当前三个判断条件通过之后,调用实现微信支付的方法:

    // 点击了结算按钮
    settlement() {
      // 1. 先判断是否勾选了要结算的商品
      if (!this.checkedCount) return uni.$showMsg('请选择要结算的商品!')
    
      // 2. 再判断用户是否选择了收货地址
      if (!this.addstr) return uni.$showMsg('请选择收货地址!')
    
      // 3. 最后判断用户是否登录了
      // if (!this.token) return uni.$showMsg('请先登录!')
      if (!this.token) return this.delayNavigate()
    
      // 4. 实现微信支付功能
      this.payOrder()
    },
    
  2. my-settle 组件中定义 payOrder 方法如下,先实现创建订单的功能:

    // 微信支付
    async payOrder() {
      // 1. 创建订单
      // 1.1 组织订单的信息对象
      const orderInfo = {
        // 开发期间,注释掉真实的订单价格,
        // order_price: this.checkedGoodsAmount,
        // 写死订单总价为 1 分钱
        order_price: 0.01,
        consignee_addr: this.addstr,
        goods: this.cart.filter(x => x.goods_state).map(x => ({ goods_id: x.goods_id, goods_number: x.goods_count, goods_price: x.goods_price }))
      }
      // 1.2 发起请求创建订单
      const { data: res } = await uni.$http.post('/api/public/v1/my/orders/create', orderInfo)
      if (res.meta.status !== 200) return uni.$showMsg('创建订单失败!')
      // 1.3 得到服务器响应的“订单编号”
      const orderNumber = res.message.order_number
    
       // 2. 订单预支付
    
       // 3. 发起微信支付
     }
    

#10.5.4 订单预支付

  1. 改造 my-settle 组件的 payOrder 方法,实现订单预支付功能:

    // 微信支付
    async payOrder() {
      // 1. 创建订单
      // 1.1 组织订单的信息对象
      const orderInfo = {
        // 开发期间,注释掉真实的订单价格,
        // order_price: this.checkedGoodsAmount,
        // 写死订单总价为 1 分钱
        order_price: 0.01,
        consignee_addr: this.addstr,
        goods: this.cart.filter(x => x.goods_state).map(x => ({ goods_id: x.goods_id, goods_number: x.goods_count, goods_price: x.goods_price }))
      }
      // 1.2 发起请求创建订单
      const { data: res } = await uni.$http.post('/api/public/v1/my/orders/create', orderInfo)
      if (res.meta.status !== 200) return uni.$showMsg('创建订单失败!')
      // 1.3 得到服务器响应的“订单编号”
      const orderNumber = res.message.order_number
    
      // 2. 订单预支付
      // 2.1 发起请求获取订单的支付信息
      const { data: res2 } = await uni.$http.post('/api/public/v1/my/orders/req_unifiedorder', { order_number: orderNumber })
      // 2.2 预付订单生成失败
      if (res2.meta.status !== 200) return uni.$showError('预付订单生成失败!')
      // 2.3 得到订单支付相关的必要参数
      const payInfo = res2.message.pay
    
       // 3. 发起微信支付
     }
    

#10.5.5 发起微信支付

  1. 改造 my-settle 组件的 payOrder 方法,实现微信支付的功能:

    // 微信支付
    async payOrder() {
      // 1. 创建订单
      // 1.1 组织订单的信息对象
      const orderInfo = {
        // 开发期间,注释掉真实的订单价格,
        // order_price: this.checkedGoodsAmount,
        // 写死订单总价为 1 分钱
        order_price: 0.01,
        consignee_addr: this.addstr,
        goods: this.cart.filter(x => x.goods_state).map(x => ({ goods_id: x.goods_id, goods_number: x.goods_count, goods_price: x.goods_price }))
      }
      // 1.2 发起请求创建订单
      const { data: res } = await uni.$http.post('/api/public/v1/my/orders/create', orderInfo)
      if (res.meta.status !== 200) return uni.$showMsg('创建订单失败!')
      // 1.3 得到服务器响应的“订单编号”
      const orderNumber = res.message.order_number
    
       // 2. 订单预支付
       // 2.1 发起请求获取订单的支付信息
       const { data: res2 } = await uni.$http.post('/api/public/v1/my/orders/req_unifiedorder', { order_number: orderNumber })
       // 2.2 预付订单生成失败
       if (res2.meta.status !== 200) return uni.$showError('预付订单生成失败!')
       // 2.3 得到订单支付相关的必要参数
       const payInfo = res2.message.pay
    
       // 3. 发起微信支付
       // 3.1 调用 uni.requestPayment() 发起微信支付
       const [err, succ] = await uni.requestPayment(payInfo)
       // 3.2 未完成支付
       if (err) return uni.$showMsg('订单未支付!')
       // 3.3 完成了支付,进一步查询支付的结果
       const { data: res3 } = await uni.$http.post('/api/public/v1/my/orders/chkOrder', { order_number: orderNumber })
       // 3.4 检测到订单未支付
       if (res3.meta.status !== 200) return uni.$showMsg('订单未支付!')
       // 3.5 检测到订单支付完成
       uni.showToast({
         title: '支付完成!',
         icon: 'success'
       })
     }
    

#10.6 分支的合并与提交

  1. settle 分支进行本地提交:

    git add .
    git commit -m "完成了登录和支付功能的开发"
    
  2. 将本地的 settle 分支推送到码云:

    git push -u origin settle
    
  3. 将本地 settle 分支中的代码合并到 master 分支:

    git checkout master
    git merge settle
    git push
    
  4. 删除本地的 settle 分支:

    git branch -d settle
    

11. 发布

#11.1 为什么要发布

  1. 小程序只有发布之后,才能被用户搜索并使用
  2. 开发期间的小程序为了便于调试,含有 sourcemap 相关的文件,并且代码没有被压缩,因此体积较大,不适合直接当作线上版本进行发布
  3. 通过执行 “小程序发布”,能够优化小程序的体积,提高小程序的运行性能

#11.2 发布小程序的流程

  1. 点击 HBuilderX 菜单栏上的 发行 -> 小程序-微信(仅适用于uni-app)

    微信小程序_第166张图片

  2. 在弹出框中填写要发布的小程序的名称AppId之后,点击发行按钮:

    微信小程序_第167张图片

  3. HBuilderX 的控制台中查看小程序发布编译的进度

    微信小程序_第168张图片

  4. 发布编译完成之后,会自动打开一个新的微信开发者工具界面,此时,点击工具栏上的上传按钮:

    微信小程序_第169张图片

  5. 填写版本号项目备注之后,点击上传按钮:

    微信小程序_第170张图片

  6. 上传完成之后,会出现如下的提示消息,直接点击确定按钮即可:

    微信小程序_第171张图片

  7. 通过微信开发者工具上传的代码,默认处于版本管理开发版本列表中,如图所示:

    微信小程序_第172张图片

  8. 开发版本提交审核 -> 再将 审核通过的版本发布上线,即可实现小程序的发布和上线:

    img

#11.3 发布为 Android App 的流程

  1. 点击 HBuilderX 状态栏左侧的未登录按钮,弹出登录的对话框:

微信小程序_第173张图片

  1. 在弹出的登录对话框中,填写账号密码之后,点击登录即可:

    微信小程序_第174张图片

  2. 打开项目根目录中的 manifest.json 配置文件,在基础配置面板中,获取uni-app 应用标识,并填写应用名称

    微信小程序_第175张图片

  3. 切换到 App 图标配置面板,点击浏览按钮,选择合适的图片之后,再点击自动生成所有图标并替换即可:

    微信小程序_第176张图片

  4. 点击菜单栏上的 发行 -> 原生 App-云打包

    img

  5. 勾选打包配置如下:

    微信小程序_第177张图片

  6. 控制台查看打包的进度信息

    微信小程序_第178张图片

  7. 点击链接下载 apk 的安装包,并安装到 Android 手机中查看打包的效果。

注意:由于开发期间没有进行多端适配,所以有些功能在 App 中无法正常运行,例如:选择收货地址微信登录微信支付

你可能感兴趣的:(微信小程序)