微信小程序开发教程

尚硅谷微信小程序开发教程,2024最新版微信小程序项目实战!

一、小程序基础

1. 初始小程序

微信小程序是一种运行在微信内部的 轻量级 应用程序。

使用小程序时 不需要下载,用户 扫一扫搜一下 即可打开应用,它也体现了 “用完即走” 的理念,用户不用关心太多应用的问题,它实现了应用“触手可及”的梦想,应用无处不在,随时可用,但又 无需安装卸载

使用 App 过程:王者荣耀 → 应用商店 → 下载 + 安装 → 5 v 5 → 卸载…

使用小程序过程:羊了个羊 → 扫、搜 → 直接使用 → 关掉…

小程序的四大特性:无需安装用完即走无需卸载触手可及

2. 注册微信小程序

小程序开发与网页开发不一样,在开始微信小程序开发之前,需要访问 微信公众平台,注册一个微信小程序账号。

有了小程序的账号以后,我们才可以开发和管理小程序,后续需要通过该账号进行 开发信息的设置开发成员的添加,也可以用该账号查看小程序的运营数据。

在申请账号之前,我们需要先 准备一个邮箱,该邮箱要求:

  1. 未被微信公众平台注册
  2. 未被微信开发平台注册
  3. 未被个人微信号绑定过,如果被绑定了需要解绑 或 使用其他邮箱

注:因流程较多,详细步骤可参考笔记https://developers.weixin.qq.com/miniprogram/dev/framework/quickstart/getstart.html#%E7%94%B3%E8%AF%B7%E8%B4%A6%E5%8F%B7

3. 完善小程序账号信息

在完成小程序账号的注册后,需要打开微信公众平台对小程序账号进行一些设置,这是因为小程序在 上线阶段 - 提交审核 的时候,小程序账号信息是必填项,因此注册小程序以后,需要补充小程序的基本信息,如名称、图标、类目等。同时需要进行小程序备案和微信认证

4. 项目成员和体验成员

小程序提供了两种不同成员角色:项目成员体验成员

项目成员:表示参与小程序开发、运营的成员,包括运营者、开发者及数据分析者,项目成员可登陆微信公众后台,管理员可以在成员管理中添加、删除项目成员,并设置项目成员的角色。

体验成员:表示参与小程序内测体验的成员,可使用体验版小程序,但不属于项目成员。管理员及项目成员均可添加、删除体验成员。

5. 小程序开发者ID

微信小程序账号只要开发者满足开发资质都可以进行注册,并且会获得对应的 开发者 ID;一个完整的开发者 ID 由 小程序ID(AppID)小程序密钥(AppSecret) 组成。

小程序 ID 是小程序在整个微信账号体系内的唯一身份凭证,后续在很多地方都会用到,例如:新建小程序项目、真机调试、发布小程序等操作时,必须有小程序 ID。

小程序密钥 是开发者对小程序拥有所有权的凭证,在进行 微信登录、微信支付,或进行发送消息等高级开发时回使用到。

6. 微信开发者工具下载

微信开发者工具官网:https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html

为了帮助开发者简单和高效的开发和调式微信小程序,微信官方提供了 微信开发者工具,利用开发者工具可以很方便的进行小程序开发、代码查看、以及编辑、预览和发布等。

微信开发者工具包含三个版本:

  1. 稳定版:稳定性高,开发中一般推荐大家使用稳定版本
  2. 预发布版:稳定性尚可,一般包含新的、大的特性,通过了内部测试
  3. 开发版:稳定性差,主要用于尽快修复缺陷和敏捷上线小的特性

注意事项:微信开发者工具必须联网使用

7. 创建小程序项目

  1. 打开微信开发者工具,左侧选择小程序,点击 + 号即可新建项目
  2. 在弹出的新页面,填写项目信息

8. 文件和目录结构介绍

一个完整的小程序项目分为两个部分:主体文件页面文件

主体文件 又称 全局文件,能够作用于整个小程序,影响到小程序的每个页面,主体文件必须放到项目的根目录下

主体文件由三部分组成:

  1. app.js:小程序入口文件
  2. app.json:小程序的全局配置文件
  3. app.wxss:小程序的全局样式

注意事项:主体文件的名字必须是app,app.js 和 app.json 文件是必须的

页面文件 是每个页面所需的文件,小程序页面文件都存放在pages目录下,一个页面一个文件夹


每个页面通常由四个文件组成,每个文件只对当前页面有效:

  1. .js:页面逻辑
  2. .wxml:页面结构
  3. .wxss:页面样式
  4. .json:小页面配置

注意事项:.js 文件和 .wxml文件是必须的

9. 如何新建页面 以及 调试基础库

1)新建页面
  1. 方式一
    在项目目录中找到 pages 文件夹,点击右键选中新建文件夹,输入文件名,再次选中新建的文件名,右键选择新建Page,输入page名称(名字与文件夹名一致即可,不需要后缀名),微信开发者工具会自动生成相应的.js.wxml.wxss.json 等文件。
  2. 方式二
    app.json 中新增对应的路由并保存,对应的文件及文件夹会自动生成。
2) 调试基础库

小程序调试基础库是指 微信开发者工具中可以选择的微信基础库版本。

微信基础库是指小程序的运行环境,给小程序提供了运行所需的各种API工具,以及基础框架和运行逻辑等。

小程序开发者可以在微信开发者工具中选择所需的微信基础库版本,作为运行和调试小程序时的运行环境。

每个小程序有自己所允许使用的基础库最低版本要求,开发者需要选择要兼容的基础库版本,从而确保小程序的功能正常运行。

10.如何调试小程序

在进行项目开发的时候,不可避免的需要进行调试,那么如何调试小程序呢?


注意事项:微信开发者工具缓存非常重要;如果发现代码和预期不一样,先点击编译;编译后还是没有达到预期的效果,就需要清除缓存,甚至重启项目才可以。

二、配置文件

1. 配置文件介绍

JSON 是一种轻量级的数据格式,常用于前后端的数据交互,但是在小程序中,JSON扮演的配置项的角色,用于配置项目或者页面属性和行为,每个页面或组件也都有一个对应的 json 文件。

小程序中常见的配置文件有以下几种:

  1. app.json:小程序全局配置文件,用于配置小程序的一些全局属性和页面路由。
  2. 页面.json:小程序页面配置文件,也称为局部配置文件,用于配置当前页面的窗口样式、页面标题等。
  3. project.config.json:小程序项目的配置文件,用于保存项目的一些配置信息和开发者的个人设置。
  4. sitemap.json:配置小程序及其页面是否允许被微信索引(即被微信搜索),提高小程序在搜索引擎搜索到的概率。
  5. project.private.config.json:可选,用于保存开发者自定义的配置信息,例如小程序的第三方 API 密钥、版本号等敏感信息,这些信息不应该被公开或共享,因此需要保存在本地进行保护。同时,由于这个文件不会被上传到服务器或共享给其他开发者,因此不同开发者之间也可以使用不同的 private.config.json 文件,并且可以独立配置;由于 project.private.config.json 包含敏感信息,一般不会被提交到代码仓库中,而是由开发人员在本地进行管理。因此,在拉取代码时,project.private.config.json 文件可能会出现冲突,因为不同开发人员的私有配置信息可能不同。另外,由于project.private.config.json 文件在本地管理,可能会被误删除或修改,也会导致冲突的出现。开发人员可以在项目中加入一些约定,比如将project.private.config.json 文件放在一个独立的目录下,并在.gitignore文件中将该目录排除在代码仓库之外。这样可以更好地管理私有配置信息,避免冲突的出现。

原文链接:https://blog.csdn.net/snowball_li/article/details/134553156

2. 全局配置 - pages配置

pages 字段:用来指定小程序由哪些页面组成,用于让小程序知道由哪些页面组成以及页面定义在哪个目录,每一项都对应一个页面的路由信息。


在配置 pages字段时,有以下注意事项:

  1. 页面路由不需要些后缀名,框架会自动去寻找对应位置的四个文件进行处理
  2. 小程序中新增/减少页面,都需要对pages数组进行修改
  3. 未指定 enterPagePath 时,数组的第一项代表小程序的初始页面(首页)

3. 全局配置 - window 配置

window 字段:用于设置小程序的状态栏、导航条、标题、窗口背景色。


官方文档:window 配置

4. 全局配置 - tabbar 配置

tabbar 字段:定义小程序顶部、底部 tab 栏,用以实现页面之间的快速切换,可以通过 tabBar配置项指定 tab 栏的表现,以及 tab 切换时显示的对应页面。

注意事项:tab按数组的顺序排序,list 配置最少2个、最多5个tab

官方文档:tabbar 配置

5. 页面配置

小程序的页面配置,也称为局部配置,每一个小程序页面也可以使用自己的 .json 文件来对本页面的窗口进行配置

需要注意的是:页面配置文件的属性和全局配置文件中的 window 属性几乎一致的,只不过这里不需要额外指定 window 字段,因此如果出现相同的配置项,页面中配置项 会覆盖全局配置文件中相同的配置项

官方文档:页面配置

6. 项目配置文件和配置sass

在创建项目的时候,每个项目的根目录生成两个 config.json 文件,用于保存开发者在工具上做的个性化配置,例如和编译有关的配置。

当重新安装微信开发者工具或者换电脑工作时,只要载入同一个项目的代码包,开发者工具就会自动恢复到当时开发项目时的个性化配置。


项目根目录中 project.config.jsonproject.private.config.json 文件都可以对项目进行配置

project.config.json项目配置文件,常用来进行配置公共的配置

project.private.config.json项目私有配置,常用来配置个人的配置

注意事项:

  1. project.private.config.json 写到 .gitignore 避免版本管理的冲突
  2. **与最终编译结果有关的设置 必须设置到 **project.config.json

官方文档:项目配置项文件

7. sitemap.json 文件

sitemap.json 文件:配置小程序及其页面是否允许被微信索引,提高小程序在微信内部被用户搜索到的概率

微信现已开放小程序内搜索,开发者可以通过 sitemap.json 配置来设置小程序页面是否允许微信索引。当开发者允许微信索引时,微信会通过爬虫的形式,为小程序的页面内容建立索引。当用户的搜索词条触发该索引时,小程序的页面将可能展示在搜索结果中。

注意事项:

  1. 注:没有 sitemap.json 则默认所有页面都能被索引
  2. { "action": "allow", "page": "*" } 是优先级最低的默认规则,未显示指明 “disallow” 的都默认被索引

三、样式

1. 小程序的样式和组件介绍

在开发 Web 网站的时候:页面的结构由 HTML 进行编写,例如:经常会用到 div、p、span、img、a 等标签

页面的样式由 CSS 进行编写,例如:经常会采用 .class、 #id、element 等选择器

但是在小程序中不能使用 HTML 标签,也没有 DOM 和 BOM,CSS 也仅仅支持部分选择器。

小程序提供了 WXML 进行页面结构编写,同时提供了 WXSS 进行页面的样式编写。

WXML 提供了 view、text、image、navigator 等标签来构建页面结构,只不过在小程序中将标签称为 组件

WXSS 对 CSS 扩充和修改,新增了尺寸单位 rpx、提供了全局的样式和局部样式,另外需要注意的是 WXSS 仅支持部分 CSS 选择器

官方文档:选择器

2. 尺寸单位 rpx

随着智能手机的发展,手机设备的宽度也逐渐多元化,这就需要开发者在开发的时候,需要适配不同屏幕宽度的手机。为了解决屏幕适配的问题,微信小程序推出了 rpx 单位。

rpx:是小程序新增的自适应单位,它可以根据不同设备的屏幕宽度进行自适应缩放

小程序规定任何型号手机:屏幕宽度都为 750rpx

开发建议:

  1. 开发微信小程序时设计师可以用 iPhone6 作为视觉稿的标准,iPhone6 的设计稿一般是 750px
  2. 如果 iPhone6 作为视觉稿的标准量取多少 px,直接写多少 rpx 即可,开发起来更方便,也能够适配屏幕的宽度

设计稿宽度是 750px,而 iPhone6 的手机设备宽度是 375rpx,设计稿想完整展示到手机中,就需要缩小一倍,

在 iPhone6 下,px 和 rpx 的换算关系是:1rpx = 0.5px,750rpx = 375px,刚好能够填充满整个屏幕的宽度。

<!-- 
	需求:绘制一个盒子,让盒子的宽度占据屏幕的一半
	view是小程序提供的组件,是容器组件,类似于 div,也是一个块级元素,占据一行
	如果想实现需求,不能使用 px,px是固定的单位,不能实现自适应,需要使用小程序提供的 rpx
	微信小程序规定,不管是什么型号的手机,屏幕单位都是 750rpx
	rpx 是可以实现自适应的
-->
<view class="box">尚硅谷</view>
.box {
	width: 375rpx;
	height: 600rpx;
	background-color: lightgreen;
}

3. 全局样式和局部样式

在进行网页开发时,我们经常创建 global.css、base.css 或者 reset.css 作为全局样式文件进行重置样式或者样式统一,然后在每个页面或者组件中写当前页面或组件的局部样式,小程序中也存在全局样式和局部样式。

全局样式:指在 app.wxss 中定义的样式规则,作用于每一个页面,例如:设置字号、背景色、宽高等全局样式,

局部样式:指在 page.wxss 中定义的样式规则,只作用于相应的页面,并会覆盖 app.wxss 中相同的选择器。

四、组件

1. 组件案例演示

小程序常用的组件:

  1. view 组件
  2. swiper 和 swiper-item 组件
  3. image 组件
  4. text 组件
  5. navigator 组件
  6. scroll-view 组件
  7. 字体图标

使用小程序常用的组件实现项目首页的效果图

1.1 轮播图区域绘制

在进行网页开发的时候,实现轮播图的时候,我们通常先使用 HTML、CSS 实现轮播图的结构样式,然后使用 JS 控制轮播图的效果,或者直接使用插件实现轮播图的功能,而在小程序中实现小程序功能则相对简单很多。

在小程序中,提供了 swiperswiper-item 组件实现轮播图:

swiper:滑块视图容器,其中只能放置swiper-item 组件

swiper-item:只可放置在 swiper 组件中,宽高自动设置为100%,代表 swiper 中的每一项

官方文档: swiper 和 swiper-item

<!-- 轮播区域 -->
<view class="swiper">
  <swiper
    autoplay
    circular
    indicator-dots
    interval="2000"   
    indicator-color="#fff"
    indicator-active-color="#f3514f"
  >
    <swiper-item>
      1
    </swiper-item>
    <swiper-item>
      2
    </swiper-item>
    <swiper-item>
      3
    </swiper-item>
  </swiper>
</view>
// 轮播图区域样式
.swiper {
	swiper {
		height: 360rpx;
		background-color: skyblue;
		swiper-item {
			/*
			 	& 在 Sass 中代表的是父选择器,引用的意思
				相当于:swiper-item:first-child {}
			*/
			&:first-child {
				background-color: lightsalmon;
			}
			&:last-child {
				background-color: lightseagreen;
			}
		}
	}
}
1.2 轮播图图片添加

在小程序中,如果需要渲染图片,需要使用image 组件,常用的属性有4个:

  1. src 属性:图片资源地址
  2. mode:图片裁剪、缩放的模式
  3. show-menu-by-longpress:长按图片显示菜单
  4. lazy-load:图片懒加载

注意事项:

  1. image 默认具有宽度和高度,宽是 320rpx 高度是 240rpX
  2. image 组件不给 src 属性设置图片地址,也占据宽和高

官方文档:image 组件

<!-- 轮播区域 -->
<view class="swiper">
  <swiper
    autoplay
    circular
    indicator-dots
    interval="2000"   
    indicator-color="#fff"
    indicator-active-color="#f3514f"
  >
    <!-- 
      src:图片的资源地址
      mode:图片的裁剪和缩放模式
      show-menu-by-longpress:长按展示菜单,菜单中有转发给好友、收藏、保存等功能
      lazy-load:图片懒加载功能,在滑动到一定的距离(上下三屏)以后展示图片
    -->
    <swiper-item>
      <image src="../../assets/banner/banner-1.png" mode="aspectFit" show-menu-by-longpress lazy-load />
    </swiper-item>
    <swiper-item>
      <image src="../../assets/banner/banner-2.png" mode="aspectFit" show-menu-by-longpress lazy-load />
    </swiper-item>
    <swiper-item>
      <image src="../../assets/banner/banner-3.png" mode="aspectFit" show-menu-by-longpress lazy-load />
    </swiper-item>
  </swiper>
</view>
// 轮播图区域样式
.swiper {
	swiper {
		height: 360rpx;
		swiper-item {
			image{
				width: 100%;
				height: 100%;
			}
		}
	}
}
1.3 绘制公司信息区域

在小程序中,如果需要渲染文本,需要使用 text 组件,常用的属性有 2 个:

  1. user-select:文本是否可选,用于长按选择文本
  2. space:显示连续空格

注意事项:

  1. 除了文本节点以外的其他节点都无法长按选中
  2. text 组件内只支持 text 嵌套

官方文档:text 组件

<!-- 公司信息 -->
<view class="info">
  <!-- 
    text 不可嵌套其他组件
    user-select:长按以后选中文本 
    boolean 文本是否可选,该属性会使文本节点显示为 inline-block	

    space:显示连续空格 
    可选值:ensp	中文字符空格一半大小 
           emsp	中文字符空格大小 
           nbsp	根据字体设置的空格大小
 -->
 <text space="ensp">同城配送</text>
 <text space="ensp">行业龙头</text>
 <text space="ensp">半小时送达</text>
 <text space="ensp">100% 好评</text>
</view>
page {
	height: 100vh;
	background-color: #efefef !important;
	padding: 16rpx;
	box-sizing: border-box;
	display: flex;
	flex-direction: column;
	> view {
		&:nth-child(n+2) {
			margin-top: 16rpx;
		}
	}
}

// 轮播图区域样式
.swiper {
	border-radius: 10rpx;
	overflow: hidden;
	swiper {
		height: 360rpx;
		swiper-item {
			image{
				width: 100%;
				height: 100%;
			}
		}
	}
}

.info {
	display: flex;
	justify-content: space-between;
	background-color: #fff;
	padding: 16rpx;
	border-radius: 10rpx;
	font-size: 24rpx;
}
1.4 商品导航区域
  1. veiw:视图容器
  2. image:图片组件
  3. text:文本组件
<!-- 商品导航 -->
<view class="good-nav">
  <view class="nav-item">
    <image src="../../assets/category/cate-1.png" mode=""/>
    <text>鲜花玫瑰</text>
  </view>
  <view class="nav-item">
    <image src="../../assets/category/cate-2.png" mode=""/>
    <text>鲜花玫瑰</text>
  </view>
  <view class="nav-item">
    <image src="../../assets/category/cate-3.png" mode=""/>
    <text>鲜花玫瑰</text>
  </view>
  <view class="nav-item">
    <image src="../../assets/category/cate-4.png" mode=""/>
    <text>鲜花玫瑰</text>
  </view>
  <view class="nav-item">
    <image src="../../assets/category/cate-5.png" mode=""/>
    <text>鲜花玫瑰</text>
  </view>
</view>
page {
	height: 100vh;
	background-color: #efefef !important;
	padding: 16rpx;
	box-sizing: border-box;
	display: flex;
	flex-direction: column;
	> view {
		&:nth-child(n+2) {
			margin-top: 16rpx;
		}
	}
}

// 轮播图区域样式
.swiper {
	border-radius: 10rpx;
	overflow: hidden;
	swiper {
		height: 360rpx;
		swiper-item {
			image{
				width: 100%;
				height: 100%;
			}
		}
	}
}

// 公司信息
.info {
	display: flex;
	justify-content: space-between;
	background-color: #fff;
	padding: 20rpx 16rpx;
	border-radius: 10rpx;
	font-size: 24rpx;
}

// 商品导航区域
.good-nav {
	display: flex;
	justify-content: space-between;
	background-color: #fff;
	padding: 20rpx 16rpx;
	border-radius: 10rpx;
	.nav-item {
		display: flex;
		flex-direction: column;
		align-items: center;

		image {
			width: 80rpx;
			height: 80rpx;
		}
		text {
			font-size: 24rpx;
			margin-top: 12rpx;
		}
	}
}
1.5 跳转到商品列表

在小程序中,如果需要进行跳转,需要使用 navigation 组件,常用的属性有 2 个:

  1. url:当前小程序内的跳转链接
  2. open-type:跳转方式
  • navigate:保留当前页面,跳转到应用内的某个页面但是不能跳到 tabbar 页面
  • redirect:关闭当前页面,跳转到应用内的某个页面。但不能跳转到 tabbar 页面
  • switchTab:跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面
  • reLaunch:关闭所有页面,打开到应用内的某个页面
  • navigateBack:关闭当前页面,返回上一页面或多级页面

注意事项:

  1. 路径后可以带参数。参数与路径之间使用 ? 分隔,参数键与参数值用 = 相连,不同参数用 & 分隔
    例如:/list?id=10&name=hua,在 onLoad(options) 生命周期函数 中获取传递的参数
  2. open-type=“switchTab" 时不支持传参

官方文档:navigator

<!-- view 小程序提供的容器组件,可以直接当成 div 使用即可 -->
<!-- 轮播区域 -->
<view class="swiper">
  <swiper
    autoplay
    circular
    indicator-dots
    interval="2000"   
    indicator-color="#fff"
    indicator-active-color="#f3514f"
  >
    <!-- 
      src:图片的资源地址
      mode:图片的裁剪和缩放模式
      show-menu-by-longpress:长按展示菜单,菜单中有转发给好友、收藏、保存等功能
      lazy-load:图片懒加载功能,在滑动到一定的距离(上下三屏)以后展示图片
    -->
    <swiper-item>
      <image src="../../assets/banner/banner-1.png" mode="aspectFit" show-menu-by-longpress lazy-load />
    </swiper-item>
    <swiper-item>
      <image src="../../assets/banner/banner-2.png" mode="aspectFit" show-menu-by-longpress lazy-load />
    </swiper-item>
    <swiper-item>
      <image src="../../assets/banner/banner-3.png" mode="aspectFit" show-menu-by-longpress lazy-load />
    </swiper-item>
  </swiper>
</view>

<!-- 公司信息 -->
<view class="info">
  <!-- 
    text 不可嵌套其他组件
    user-select:长按以后选中文本 
    boolean 文本是否可选,该属性会使文本节点显示为 inline-block	

    space:显示连续空格 
    可选值:ensp	中文字符空格一半大小 
           emsp	中文字符空格大小 
           nbsp	根据字体设置的空格大小
 -->
 <text space="ensp">同城配送</text>
 <text space="ensp">行业龙头</text>
 <text space="ensp">半小时送达</text>
 <text space="ensp">100% 好评</text>
</view>

<!-- 商品导航 -->
<view class="good-nav">
  <view class="nav-item">
    <!-- 
      在进行页面跳转时,需要在路径的前面添加 / 斜线,否则跳转不成功
      open-type	string类型	默认值 navigate	 跳转方式
      参数列表:
        navigate 对应 wx.navigateTo 或 wx.navigateToMiniProgram 的功能
                只能跳转到非 tabbar 页面,不能跳转到 tabbar 页面,保留上级页面

        redirect 对应 wx.redirectTo 的功能
                只能跳转到非 tabbar 页面,不能跳转到 tabbar 页面,关闭上级页面

        switchTab	对应 wx.switchTab 的功能	
                  只能跳转到 tabbar 页面,不能跳转到非 tabbbar 页面

        reLaunch	对应 wx.reLaunch 的功能
                  关闭所有页面,打开到应用内的某个页面

        navigateBack	对应 wx.navigateBack 或 wx.navigateBackMiniProgram (基础库 2.24.4 版本支持)的功能
                      关闭当前页面,返回上一页面或多级页面 默认返回上一页 当返回多级页面时,需要添加 delta 属性,表示返回的层级 默认为 1

        exit	退出小程序,target="miniProgram"时生效
    -->
    <navigator url="/pages/list/list?id=10&num=hua">
      <image src="../../assets/category/cate-1.png" mode=""/>
      <text>鲜花玫瑰</text>
    </navigator>
  </view>
  <view class="nav-item">
    <navigator url="/pages/list/list?id=10&num=hua">
      <image src="../../assets/category/cate-2.png" mode=""/>
      <text>鲜花玫瑰</text>
    </navigator>
  </view>
  <view class="nav-item">
    <navigator url="/pages/list/list?id=10&num=hua">
      <image src="../../assets/category/cate-3.png" mode=""/>
      <text>鲜花玫瑰</text>
    </navigator>
  </view>
  <view class="nav-item">
    <navigator url="/pages/list/list?id=10&num=hua">
      <image src="../../assets/category/cate-4.png" mode=""/>
      <text>鲜花玫瑰</text>
    </navigator>
  </view>
  <view class="nav-item">
    <navigator url="/pages/list/list?id=10&num=hua">
      <image src="../../assets/category/cate-5.png" mode=""/>
      <text>鲜花玫瑰</text>
    </navigator>
  </view>
</view>

<!-- 推荐商品 -->
<view class="good-hot"></view>
page {
	height: 100vh;
	background-color: #efefef !important;
	padding: 16rpx;
	box-sizing: border-box;
	display: flex;
	flex-direction: column;
	> view {
		&:nth-child(n+2) {
			margin-top: 16rpx;
		}
	}
}

// 轮播图区域样式
.swiper {
	border-radius: 10rpx;
	overflow: hidden;
	swiper {
		height: 360rpx;
		swiper-item {
			image{
				width: 100%;
				height: 100%;
			}
		}
	}
}

// 公司信息
.info {
	display: flex;
	justify-content: space-between;
	background-color: #fff;
	padding: 20rpx 16rpx;
	border-radius: 10rpx;
	font-size: 24rpx;
}

// 商品导航区域
.good-nav {
	display: flex;
	justify-content: space-between;
	background-color: #fff;
	padding: 20rpx 16rpx;
	border-radius: 10rpx;
	.nav-item {
		navigator {
			display: flex;
			flex-direction: column;
			align-items: center;

			image {
				width: 80rpx;
				height: 80rpx;
			}
			text {
				font-size: 24rpx;
				margin-top: 12rpx;
			}
		}
	}
}
1.6 推荐商品区域 - 滚动效果

在微信想小程序中如果想实现内容滚动,需要使用 scroll-view 组件

scroll-view:可滚动视图区域,适用于需要滚动展示内容的场景,用于在小程序中实现类似于网页中的滚动条效果,用户可以通过手指滑动或者点击滚动条来滚动内容。

先来学习两个属性:

  1. scroll-x:允许横向滚动
  2. scroll-y:允许纵向滚动
<!-- 推荐商品 -->
<view class="good-hot">
  <scroll-view class="scroll-x" scroll-x="true">
    <view>1</view>
    <view>2</view>
    <view>3</view>
  </scroll-view>
</view>
// 推荐商品
.good-hot {
	.scroll-x {
		width: 100%;
		white-space: nowrap;
		background-color: skyblue;
		view {
			display: inline-block;
			width: 300rpx;
			height: 80rpx;
			&:last-child {
				background-color: slateblue;
			}
			&:first-child {
				background-color: springgreen;
			}
		}
	}
}

横向滚动时,需要把宽度固定,纵向滚动时,需要把纵向滚动固定

1.7 推荐商品区域 - 实现结构样式
<!-- view 小程序提供的容器组件,可以直接当成 div 使用即可 -->
<!-- 轮播区域 -->
<view class="swiper">
  <swiper
    autoplay
    circular
    indicator-dots
    interval="2000"   
    indicator-color="#fff"
    indicator-active-color="#f3514f"
  >
    <!-- 
      src:图片的资源地址
      mode:图片的裁剪和缩放模式
      show-menu-by-longpress:长按展示菜单,菜单中有转发给好友、收藏、保存等功能
      lazy-load:图片懒加载功能,在滑动到一定的距离(上下三屏)以后展示图片
    -->
    <swiper-item>
      <image src="../../assets/banner/banner-1.png" mode="aspectFit" show-menu-by-longpress lazy-load />
    </swiper-item>
    <swiper-item>
      <image src="../../assets/banner/banner-2.png" mode="aspectFit" show-menu-by-longpress lazy-load />
    </swiper-item>
    <swiper-item>
      <image src="../../assets/banner/banner-3.png" mode="aspectFit" show-menu-by-longpress lazy-load />
    </swiper-item>
  </swiper>
</view>

<!-- 公司信息 -->
<view class="info">
  <!-- 
    text 不可嵌套其他组件
    user-select:长按以后选中文本 
    boolean 文本是否可选,该属性会使文本节点显示为 inline-block	

    space:显示连续空格 
    可选值:ensp	中文字符空格一半大小 
           emsp	中文字符空格大小 
           nbsp	根据字体设置的空格大小
 -->
 <text space="ensp">同城配送</text>
 <text space="ensp">行业龙头</text>
 <text space="ensp">半小时送达</text>
 <text space="ensp">100% 好评</text>
</view>

<!-- 商品导航 -->
<view class="good-nav">
  <view class="nav-item">
    <!-- 
      在进行页面跳转时,需要在路径的前面添加 / 斜线,否则跳转不成功
      open-type	string类型	默认值 navigate	 跳转方式
      参数列表:
        navigate 对应 wx.navigateTo 或 wx.navigateToMiniProgram 的功能
                只能跳转到非 tabbar 页面,不能跳转到 tabbar 页面,保留上级页面

        redirect 对应 wx.redirectTo 的功能
                只能跳转到非 tabbar 页面,不能跳转到 tabbar 页面,关闭上级页面

        switchTab	对应 wx.switchTab 的功能	
                  只能跳转到 tabbar 页面,不能跳转到非 tabbbar 页面

        reLaunch	对应 wx.reLaunch 的功能
                  关闭所有页面,打开到应用内的某个页面

        navigateBack	对应 wx.navigateBack 或 wx.navigateBackMiniProgram (基础库 2.24.4 版本支持)的功能
                      关闭当前页面,返回上一页面或多级页面 默认返回上一页 当返回多级页面时,需要添加 delta 属性,表示返回的层级 默认为 1

        exit	退出小程序,target="miniProgram"时生效
    -->
    <navigator url="/pages/list/list?id=10&num=hua">
      <image src="../../assets/category/cate-1.png" mode=""/>
      <text>鲜花玫瑰</text>
    </navigator>
  </view>
  <view class="nav-item">
    <navigator url="/pages/list/list?id=10&num=hua">
      <image src="../../assets/category/cate-2.png" mode=""/>
      <text>鲜花玫瑰</text>
    </navigator>
  </view>
  <view class="nav-item">
    <navigator url="/pages/list/list?id=10&num=hua">
      <image src="../../assets/category/cate-3.png" mode=""/>
      <text>鲜花玫瑰</text>
    </navigator>
  </view>
  <view class="nav-item">
    <navigator url="/pages/list/list?id=10&num=hua">
      <image src="../../assets/category/cate-4.png" mode=""/>
      <text>鲜花玫瑰</text>
    </navigator>
  </view>
  <view class="nav-item">
    <navigator url="/pages/list/list?id=10&num=hua">
      <image src="../../assets/category/cate-5.png" mode=""/>
      <text>鲜花玫瑰</text>
    </navigator>
  </view>
</view>

<!-- 推荐商品 -->
<view class="good-hot">
  <scroll-view class="scroll-x" scroll-x>
    <view>
      <view class="good-item">
        <image src="../../assets/floor/1.png" mode=""/>
        <text>鲜花玫瑰</text>
        <text>66</text>
      </view>
    </view>
    <view>
      <view class="good-item">
        <image src="../../assets/floor/2.png" mode=""/>
        <text>鲜花玫瑰</text>
        <text>99</text>
      </view>
    </view>
    <view>
      <view class="good-item">
        <image src="../../assets/floor/3.png" mode=""/>
        <text>鲜花玫瑰</text>
        <text>100</text>
      </view>
    </view>
    <view>
      <view class="good-item">
        <image src="../../assets/floor/4.png" mode=""/>
        <text>鲜花玫瑰</text>
        <text>105</text>
      </view>
    </view>
    <view>
      <view class="good-item">
        <image src="../../assets/floor/5.png" mode=""/>
        <text>鲜花玫瑰</text>
        <text>88</text>
      </view>
    </view>
  </scroll-view>
</view>
page {
	height: 100vh;
	background-color: #efefef !important;
	padding: 16rpx;
	box-sizing: border-box;
	display: flex;
	flex-direction: column;
	> view {
		&:nth-child(n+2) {
			margin-top: 16rpx;
		}
	}
}

// 轮播图区域样式
.swiper {
	border-radius: 10rpx;
	overflow: hidden;
	swiper {
		height: 360rpx;
		swiper-item {
			image{
				width: 100%;
				height: 100%;
			}
		}
	}
}

// 公司信息
.info {
	display: flex;
	justify-content: space-between;
	background-color: #fff;
	padding: 20rpx 16rpx;
	border-radius: 10rpx;
	font-size: 24rpx;
}

// 商品导航区域
.good-nav {
	display: flex;
	justify-content: space-between;
	background-color: #fff;
	padding: 20rpx 16rpx;
	border-radius: 10rpx;
	.nav-item {
		navigator {
			display: flex;
			flex-direction: column;
			align-items: center;

			image {
				width: 80rpx;
				height: 80rpx;
			}
			text {
				font-size: 24rpx;
				margin-top: 12rpx;
			}
		}
	}
}

// 推荐商品
.good-hot {
	background-color: #fff;
	border-radius: 10rpx;
	padding: 16rpx;
	font-size: 24rpx;

	.scroll-x {
		width: 100%;
		white-space: nowrap;
		view {
			display: inline-block;
			width: 320rpx;
			height: 440rpx;
			margin-right: 16rpx;

			.good-item {
				display: flex;
				flex-direction: column;
				justify-content: space-between;

				image {
					width: 100%;
					height: 320rpx;
				}

				text {
					&:nth-of-type(1) {
						font-weight: bold;
					}
				}
			}
			&:last-child {
				margin-right: 0;
			}
		}
	}
}
1.8 字体图标的使用

在项目中使用到的小图标,一般由公司设计师进行设计,设计好以后上传到 阿里巴巴矢量图标库,然后方便程序员来进行使用。

小程序中的字体图标使用方式与 Web 开发中的使用方式一样的。

阿里巴巴矢量图标库 中搜索或上传自己需要的图标,添加到项目,生成对应的样式文件,在微信小程序项目目录创建,字体图标 iconfont 文件夹,然后新建 iconfont.scss 样式文件,导入到 app.scss文件中,即可使用。


样式文件推荐使用 Base64 格式的字体文件,在项目设置中设置,如下图:


iconfont.scss 文件中

@font-face {
  font-family: "iconfont"; /* Project id 4569815 */
  src: 
       url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAYwAAsAAAAAC7gAAAXjAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHFQGYACDMgqKPIh4ATYCJAMUCwwABCAFhGcHUxslClGULlKC7Mdh7DwJwwiNSoHG4pHKh7t33+Ph+WO85776XZbpUfPRaLymgQ2jaOQTKh5VW/RQnTaFPCnlLBTsBoV8foHpyXwBHtS5FK41Ey/D5cMvtwriFxfsRpM3eSvD9VO7xCv///1KJ9L+ANYF8PumqizD/gDVBvDYWnSBDPieFadVwc2lmOo1XiPQbVss5dPU7GKgf40vFwNrqszJIKmOazTJB5qCQ2kQhU5z63pqEQ9AJd1UvgiA+8Hvx0cQGJokNTOvcvZCCgaOPlN6dlql+z8iPwfehIDdEyoy1jfmiuvNgWtUIt16YOiuCqg6PBzVKZFJN3J7Jnp2+v9/Es3RTN3WPzyKLAmiMQ29xD0sFt4oOYrE6AabDKMINgnG01A9i6F6p3RjBIjHAKRTYFrmp9y0ydq4AFeYURpwYFlDoYDX7mhgbTZJlhcUyJwVGZ0K1ZaCz+Jb3a6LpL826BUyUij30HxTvwcZqmFVtKQtM1FVWZM/rXrOVYl/v+Ix3+SJ7F/WUwzBsHNKQIGgz5U1hqGcxlkW/8H7t0lm0Gz7Ek1Taryixq1Vv+QdVD4lKDucJRk6rw8rsOOFi7uHQQZrEAJ0QI8YJtiaKgDI29uRhR1uLyu3Jb5n7eQWADRK+IE2JMzrpZJlUVg0zx4gpoktixoG0vU+sg/GmMCHMJXl1W2/TBQJqkftbkcCCxkHNG8vTlscJatE24clazI93VHMQLy+xNipha41KAZVlYvGGlVAlKAsRLaHHddENkfOzjwa6mBJ76R4Wr7LsofXvOsXXNMZDnoAANpn2dUv8hHvKVpUwJVf+p8EdOQ7v9cYpBlnBQLL4HFyjx/oHbp1hI9TJsLX4NAGwm8GY+ZErzCaB2eeJdmpkjdPmJ4oV+UO+ZQyuN+oZPzcdo05I6DJLcOB4JNBZdp0gk7tt/RBdUapzHNkAInfa7I3nj/QXOpOKqk/NP2mU5ugTQeWeWcOVOZrGcKrJlu09hvNa20x+WAI87VKNinWepCt4d/jLYH+ox8MuSR4Z5rvskqrBzmrsi8lHxQDiUEDio9hqmieNwKMx5rv8sp0TQB/QzBtowkmvbYgmFFvUm8V6PGseHqCjoAysjb6pDa1JnWhc1xV8+nBNe0GS6xYeMHhPTwAWadE1cocnywvryyfnF221SszxrR2F1kzvUhZEGZlHlZZk9Gekd42l97enj7Xlq5tNZfR1pZxol/PSM9+ch3Pc/GekwKfFzG+tp9xLt5nrSh2Zvqry5kV3jNR0zHLmXTwf56qdatynZhwrXKrvWqjjzGmZ9e6Xa3YXL3vfDsTQ0ZHkzHMDxt9jDFN/0N6JBbwKtcfE2vE6f4L/tMNeJrVX7KyBDSuBqsr9ObVvdZ5PYUaobFUI1n98mSNZd5mBd0Pj12wo0ye2aRoLDFcopHiZOT0WHPYcFgzxtrsofkGs0dmdHFXdhd2BIOjmG7RQBdx2Rx6BMXli2gnQ+cNvM3ZW0otKN++JPFvZBplY9sUbjGGDKKQYGvQDOD/Xe5XiQvEDzlPJX5vVEC+AZC/Kl/8nnbzi/4G6wyiv2rq/MfWvegapUjTvdgpwSu+HuP5jrRFI2QsVYOOooO0uquFEt9I3+cSGfZ4GveIKX2hM6PkIekwA1mnWWbhrkLVYxeaTnvQbU3K2T1GcFRE6cKivh+EQQpI+j2CbNAZZuHegWrcc2gG/YVu50Lvcj0WA02S4nw5jkGkBdIoKyZsszxYtBAnG4R8aVm2FJfKaKRhgK9/vpyGi3HpGEfIGslAuZyAhJQVwVTubrhQyEJOyjI4Kvel5HIu0s+PqPoRX5QVgSRSOD45HAZCtIBoKJYY4dHLs3+vEI7UQIhP2pFZ6pOSMXJaHArgy78DM80g7jR0I40yjUiB5OQjQEcnpZIIlCogdEK+wIK46hwDh6oBX9SAHCeSn0VHdDX4Lm8SvWDlf3akeb0aKXKUqNF0XOKzHC0mVS//EDVUyKICQzkrJlEKd2McTssSOwAA') format('woff2'),
       url('//at.alicdn.com/t/c/font_4569815_u50pb2x14v.woff?t=1717164520149') format('woff'),
       url('//at.alicdn.com/t/c/font_4569815_u50pb2x14v.ttf?t=1717164520149') format('truetype');
}

.iconfont {
  font-family: "iconfont" !important;
  font-size: 16px;
  font-style: normal;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.icon-haoping:before {
  content: "\e6c6";
}

.icon-icon:before {
  content: "\e66d";
}

.icon-clock:before {
  content: "\e627";
}

.icon-tongchengpeisong:before {
  content: "\e601";
}

app.scss 文件中

// 导入样式文件以后,必须以分号结尾,否则会出现异常
@import "./iconfont/iconfont.scss";

index.scss 文件中

// 公司信息
.info {
	display: flex;
	justify-content: space-between;
	background-color: #fff;
	padding: 20rpx 16rpx;
	border-radius: 10rpx;
	font-size: 24rpx;
	.iconfont {
		font-size: 24rpx;
	}
}

index.wxml 文件中

<!-- 公司信息 -->
<view class="info">
  <!-- 
    text 不可嵌套其他组件
    user-select:长按以后选中文本 
    boolean 文本是否可选,该属性会使文本节点显示为 inline-block	

    space:显示连续空格 
    可选值:ensp	中文字符空格一半大小 
           emsp	中文字符空格大小 
           nbsp	根据字体设置的空格大小
 -->
  <text space="ensp"><text class="iconfont icon-tongchengpeisong"></text>同城配送</text>
  <text space="ensp"><text class="iconfont icon-icon"></text>行业龙头</text>
  <text space="ensp"><text class="iconfont icon-clock"></text>半小时送达</text>
  <text space="ensp"><text class="iconfont icon-haoping"></text>100% 好评</text>
</view>

注意事项:

使用字体图标可能会报错:[渲染层网络层错误] Failed to load font.………该错误可忽略

但在控制台出现错误,会影响开发调试,解决方案是:将字体图标转换成 base64 的格式

2. 背景图片的使用

当编写小程序的样式文件时,我们可以使用 background-image 属性来设置元素的背景图像

注意事项:

小程序的 background-image 不支持本地路径! 需要使用网络图片,或者 base64 ,或者使用 <image /> 组件

五、事件系统

1. 事件绑定和事件对象

小程序中绑定事件与网页开发中绑定事件几乎一致,只不过在小程序中不能通过 on 的方式绑定事件,也没有 click 等事件,小程序中绑定事件使用 bind 方法,click 事件也需要使用 tap 事件来进行代替,绑定事件的方式有两种:

第一种方式:bind:事件名,bind 后面需要跟上冒号,冒号后面跟上事件名,例如:<view bind:tap="fnName"></view>

第二种方式:bind事件名,bind 后面直接跟上事件名,例如:<view bindtap="fnName"></view>

事件处理函数需要写到 .js 文件中,在 .js 文件中需要调用小程序提供的 Page 方法来注册小程序的页面,我们可以直接在 Page 方法中创建事件处理函数。

<button type="primary" size="mini" bind:tap="handleTap">绑定事件</button>
<button type="warn" size="mini" bindtap="handleTap">绑定事件</button>

<!-- 微信小程序input输入框默认不带边框 -->
<input type="text" bindinput="getInputValue"/>

微信小程序input输入框默认不带边框

Page({
  handleTap(event) {
    console.log(event)
  },
  getInputValue(event) {
    console.log(event.detail.value)
  }
})

2. 事件分类以及阻止事件冒泡

事件分为 冒泡事件非冒泡事件

冒泡事件:当一个组件的事件被触发后,该事件会向父节点传递

非冒泡事件:当一个组件的事件被触发后,该事件不会向父节点传递

使用 bind 绑定的事件,会触发事件冒泡,如果想阻止事件冒泡,可以使用 catch 来绑定事件。

<view class="catch" bindtap="parentHander">
	<button catchtap="btnHander">按钮</button>
</view>
Page({
  parentHander () {
    console.log("父组件绑定的事件")
  },
  btnHander () {
    console.log("子组件绑定的事件")
  }
})

以上代码可以组织事件冒泡。

3. 事件传参 - data-*自定义数据

事件传参:在触发事件时,将一些数据作为参数传递给事件处理函数的过程,就是事件传参

在微信小程序中,我们经常会在组件上添加一些自定义数据,然后在事件外理函数中获取这些自定义数据,从而完成业务逻辑的开发

在组件上 通过 data-* 的方式 定义需要传递的数据,其中 是自定义的属性,例如:<view data-id=“100"bindtap="handler”/>

然后通过事件对象进行获取自定义数据

<button catchtap="btnHander" data-id="1" data-name="tom">按钮</button>

<view class="catch" bindtap="parentHander" data-parent-id="1" data-parent-name="tom">
	<button data-id="1" data-name="tom">按钮</button>
</view>
Page({
  // 按钮触发的事件处理函数
  btnHander (event) {
    /**
     * currentTarget 事件绑定者 也就是指:哪个组件绑定了当前事件处理函数
     * target 事件触发者,也就死指:哪个组件触发了当前事件处理函数
     * currentTarget 和 target 都是指按钮,因为是按钮绑定的事件处理函数,同时点击按钮触发事件处理函数,这时候通过谁来获取数据都可以。
     */
    console.log(event.currentTarget.dataset) // {id: "1", name: "tom"}
    console.log(event.target.dataset) // {id: "1", name: "tom"}
  },
  
  // view 触发的事件处理函数
  parentHander (event) {
    /**
     * 点击蓝色区域(不点击按钮)
     * currentTarget 事件绑定者 view
     * target 事件触发者,view
     * currentTarget 和 target 都是指 view,通过谁来获取数据都可以。
     * 
     * 点击按钮(不点击蓝色区域)
     * currentTarget 事件绑定者 view
     * target 事件触发者,按钮
     * 如果想获取view 身上的数据,就必须使用 currentTarget;如果想获取事件本身的数据,就需要使用 target。
     * 
     * 在传递参数的时候,自定义属性是多个单词:
     * 如果单词与单词之间使用中划线 - 进行连接,在事件对象中会被转换为小驼峰写法;
     * 如果使用的是小驼峰写法,在事件对象中会被转为全部小写的
     */
    // 点击蓝色区域时(不能获取按钮的传参)
    console.log(event.currentTarget.dataset) // {parentId: "2", parentName: "tom"}
    console.log(event.target.dataset) // {parentId: "2", parentName: "tom"}
    
    // 点击按钮时(通过 currentTarget 获取父节点传参)
    console.log(event.currentTarget.dataset) // {parentId: "2", parentName: "tom"}
    console.log(event.target.dataset.id)  // {id: "1", name: "tom"}
  }
})

点击父节点不能获取子节点的参数,点击子节点可以通过 currentTarget 获取父节点的参数。

注意事项:

  1. event.target 是指事件触发者,event.currentTarget 是指事件绑定者
  2. 使用 data- 方法传递参数的时候,多个单词由连字符-连接,连字符写法会转换成驼峰写法
  3. 使用 data- 方法传递参数的时候,而大写字符会自动转成小写字符

4. 事件传参 - mark 自定义数据

小程序进行事件传参的时候,除了使用 data-* 属性传递参数外,还可以 使用 mark 标记传递参数

mark 是一种自定义属性,可以在组件上添加,用于来识别具体触发事件的 target 节点。同时 mark 还可以用于承载一些自定义数据

在组件上使用 mark:自定义属性 的方式将数据传递给事件处理函数,例如:<view mark:id=“100" bindtap="handler”/>,然后通过事件对象进行获取自定义数据。

mark 和 data-* 很相似,主要区别在于:*

mark 包含从触发事件的节点到根节点上所有的 mark:属性值

currentTarget.dataset 或者 target.dataset 只包含事件绑定者 或者 事件触发者那一个节点的 data-* 值

<!-- 如果需要使用 mark 进行事件传参,需要使用 mark:自定义属性的方式进行参数传递 -->
<button bindtap="markBtnHander" mark:id="1" mark:name="tom">按钮</button>

<view class="catch" bindtap="markParentHander" mark:parentId="2" mark:parentName="char">
	<button mark:id="1" mark:name="tom">按钮</button>
</view>
Page({
  // 使用 mark:自定义属性 传参
  markBtnHander(event) {
    console.log(event.mark) // {id: "1", name: "tom"}
  },

  markParentHander(event) {
    /**
     * 点击蓝色区域(不点击按钮)
     * 通过事件对象获取的是 view 身上绑定的数据
     * 
     * 点击按钮(不点击蓝色区域)
     * 通过事件对象获取的是 触发事件的节点 以及 父节点身上所有的 mark 数据
     */
    console.log(event.mark) // {id: "1", name: "tom", parentId: "2", parentName: "char"}
  }
})

六、wxml 语法

1. 声明和绑定数据

小程序页面中使用的数据均需要在 Page() 方法的 data 对象中进行声明定义
在将数据声明好以后,在 WXML 使用 Mustache 语法(双大括号{{ }}) 将变量包起来,从而将数据绑定
在 {{ }} 内部可以做一些简单的运算,支持如下几种方式:

  1. 算数运算
  2. 三元运算
  3. 逻辑判断
  4. 其他…

注意事项:在 {{ }} 语法中,只能写表达式,不能写语句,也不能调用 JavaScript 相关的方法

<!-- 如果需要展示数据,在 wxml 中需要使用双大括号写法将变量进行包裹 -->
<!-- 展示内容 -->
<view>{{ school }}</view>
<view>{{ obj.name }}</view>

<!-- 绑定属性值,如果需要动态绑定一个变量,属性值也需要使用双大括号进行包裹 -->
<view id="{{ id }}"></view>
<!-- 如果属性值是布尔值,也需要使用双大括号进行包裹 -->
<checkbox checked="{{ isChecked }}"/>

<!-- 算术运算 -->
<view>{{ id + 1 }}</view>
<view>{{ id - 1 }}</view>
<!-- 三元运算 -->
<view>{{ id === 1 ? "等于" : "不等于" }}</view>

<!-- 逻辑判断 -->
<view>{{ id === 1 }}</view>

<!-- 在双大括号写法内部,只能写表达式,不能写语句,也不能调用 JavaScript 的方法 -->
<!-- 以下写法报错 -->
<!-- <view>{{ if(id === 1) {} }}</view> -->
<!-- <view>{{ for(const i =0; i <= 10; i++) {} }}</view> -->
<!-- 以下代码没有报错,但也不会生效 -->
<!-- <view>{{ obj.name.toUpperCase }}</view> -->
Page({

  /**
   * 在小程序页面中所需要使用的数据均来自于 data
   */
  data: {
    id: 1,
    isChecked: false,
    school: "尚硅谷",
    obj: {
      name: "tom"
    }
  }
})

2. setData() 修改数据

小程序中修改数据不推荐通过赋值的方式进行修改,通过赋值的方式修改数据无法改变页面的数据
而是要通过调用 setData() 方法进行修改,setData() 方法接收对象作为参数,key 是需要修改的数据,value 是最新的值。
setData() 方法有两个作用:

  1. 更新数据
  2. 驱动视图更新
<view>{{ num }}</view>
<button bindtap="handlerUpdata">更新 num</button>
Page({

  /**
   * 在小程序页面中所需要使用的数据均来自于 data
   */
  data: {
    num: 1
  },
  handlerUpdata(event) {
    /**
     * 通过赋值的方式直接修改数据:
     * 能够修改数据,但是不能更新页面上的数据,例如:this.data.num += 1
     * 
     * 通过 this.setData({ key: value }) 方式修改数据:1. 更新数据,2.驱动视图(页面)更新
     * key:是需要更新的数据
     * value:是最新的值
     */
    this.setData({ num: this.data.num + 1 })
  }
})

3. setData() - 修改对象类型数据

  1. 新增 单个/多个属性
  2. 修改 单个/多个属性
  3. 删除 单个/多个属性
<view>{{ userInfo.name }}</view>
<view>{{ userInfo.age }}</view>
<button bindtap="handlerUpdataUserInfo">更新 userInfo</button>
Page({

  /**
   * 在小程序页面中所需要使用的数据均来自于 data
   */
  data: {
    userInfo: {
      name: "Tom",
      age: 10,
      test: "aaa"
    }
  },
  // 更新userInfo
  handlerUpdataUserInfo() {
    /**
     * 新增单个 / 多个属性
     * 如果给对象新增属性,可以将 key 写成数据路径的方式 a.b.c
     */
    this.setData({
      "userInfo.name": "Tom",
      "userInfo.age": 10
    })
    /**
     * 修改单个 / 多个属性
     * 如果需要修改对象属性,可以将 key 写成数据路径的方式 a.b.c
     */
    this.setData({
      "userInfo.name": "jerry",
      "userInfo.age": 18
    })
    /**
     * 优化方案
     * 目前进行新增和修改都是使用数据路径,如果新增和修改的数据量比较小,还可以;
     * 如果修改的数据很多,每次都写数据路径,就太麻烦了,可以使用 ES6 提供的展开运算符 和 Object.assign()
     */
    // 通过展开运算符,能够将对象中的属性复制给另外一个对象,后面的属性会覆盖前面的属性
    const userInfo = {
      ...this.data.userInfo,
      name: "Tom",
      age: 20
    }
    this.setData({userInfo})

    // Object.assign() 将多个对象合并为一个对象
    const userInfo = Object.assign(this.data.userInfo, { name: 'jerry', age: 8 })
    this.setData({ userInfo })

    // 删除单个属性
    delete this.data.userInfo.age;
    this.setData({ userInfo: this.data.userInfo })

    // 删除多个属性 rest 剩余参数
    const {age, test, ...rest} = this.data.userInfo
    this.setData({ userInfo: rest })
  }
})

4. setData() - 修改数组类型数据

  1. 新增数组元素
  2. 修改数组元素
  3. 删除数组元素
<!-- setData() 修改数组类型数据 -->
<view wx:for="{{ list }}" wx:key="index">{{ item }}</view>
<button bindtap="handlerUpdataList">修改数组类型数</button>
Page({

  /**
   * 在小程序页面中所需要使用的数据均来自于 data
   */
  data: {
    list: [1, 2, 3]
  },
  // 修改数组类型数据
  handlerUpdataList() {
    // 新增数组元素
    // 如果直接使用 push 方法,可以更新 data, 但是不能更新 页面中的数组,需要调用 setData() 更新页面
    this.data.list.push(4);
    this.setData({
      list: this.data.list
    })

    const newList = this.data.list.concat(4);
    this.setData({
      list: newList
    })

    const newList = [...this.data.list, 4];
    this.setData({
      list: newList
    })

    // 修改数组元素,使用 数据路径的方式修改 如:{ "list[1].name": 6 }
    this.setData({
      "list[1]": 6
    })

    // 删除数组元素
    this.data.list.splice(1, 1)
    this.setData({
      list: this.data.list
    })

    const newList = this.data.list.filter(item => item != 2)
    this.setData({
      list: newList
    })
  }
})

5. 简易双向数据绑定

在 WXML中,普通属性的绑定是单向的,例如:<input value="{{value}}"/>

如果希望用户输入数据的同时改变 data 中的数据,可以借助简易双向绑定机制。在对应属性之前添加 mode: 前缀即可:

例如:<input model:value="{{value}}" />

注意事项:

简易双向绑定的属性值如下限制:

  1. 只能是一个单一字段的绑定,例如:错误用法:<input model:value="值为{{value}}"/>
  2. 尚不能写 data 路径,也就是不支持数组和对象,例如:错误用法:<input model:value="{{a.b}}"/>
<!-- 单向绑定:数据能够影响页面,但是页面更新不会影响到数据 -->
<input type="text" value="{{ value }}" />
<!-- 双向绑定:数据能够影响页面,页面更新也能够影响数据,实现简易的双向绑定,需要在对应的属性之前添加 model: -->
<input type="text" model:value="{{ value }}" />
<!-- 如果需要获取复选框的选中效果,需要给 checked 添加 model: -->
<checkbox model:checked="{{ isChecked }}"/>是否同意该协议
<!-- 注意事项1:属性值只能是一个单一字段的绑定 -->
<input type="text" model:value="值为{{ value }}" /> <!-- 错误写法 -->
<!-- 注意事项2:属性值不能写成数据路径,也不支持对象和数组 -->
<input type="text" model:value="{{ obj.name }}"/> <!-- 错误写法 -->
Page({
  /**
   * 在小程序页面中所需要使用的数据均来自于 data
   */
  data: {
    isChecked: false,
    obj: {
      name: "tom"
    },
    list: [1, 2, 3],
    value: 123
  }
})

6. 列表渲染 - 基本使用

列表渲染 就是指通过循环遍历一个数组或对象,将其中的每个元素渲染到页面上

在组件上使用 wx:for 属性绑定一个数组或对象,既可使用每一项数据重复渲染当前组件

每一项的变量名默认为 item,下标变量名默认为 index

在使用 wx:for 进行遍历的时候,建议加上 wx:key 属性wx:key 的值以两种形式提供:

  1. 字符串:代表需要遍历的 arrayitem 的某个属性,该属性的值需要是列表中唯一的字符串或数字,且不能动态改变
  2. 保留关键字 *this 代表在 for 循环中的 item 本身,当 item 本身是一个唯一的字符串或者数字时可以使用

注意事项:

  1. 如果不加 wx:key,会报一个 warning,如果明确知道该列表是静态,即以后数据不会改变,或者不必关注其顺序,可以选择忽略。
  2. 在给 wx:key 添加属性值的时候,不需要使用双大括号语法,直接使用遍历的 arrayitem 的某个属性
<!-- 如果需要进行列表渲染,需要使用 wx:for 属性,属性需要使用双大括号进行包裹,每一项的变量名默认是 item,每一项下标的变量名默认是 index -->
<!-- 
	wx:key 提升性能
	wx:key 属性值有两种添加形式:
	1. 字符串,需要是遍历的数组中 item 的某个属性,要求该属性是列表中唯一的字符串或者数字,不能进行动态改变
	2. 保留关键字 *this, *this 代表的是 item 本身,item 本身是唯一的字符串或者数字
	wx:key 的属性值不需要使用大括号进行包裹,直接写遍历的数组中 item 的某个属性
-->
<!-- 如果渲染的是数组,item:数组的每一项,index:下标 -->
<view wx:for="{{ numList }}" wx:key="*this">{{ item }} - {{ index }}</view>
<!-- 如果渲染的是对象,item:对象的值,index:对象的属性 -->
<view wx:for="{{ obj }}" wx:key="index">{{ item }} - {{ index }}</view>
<view wx:for="{{ fruitList }}" wx:key="index">{{ item.id }} - {{ item.name }}</view>
<view wx:for="{{ fruitList }}" wx:key="id">{{ item.id }} - {{ item.name }}</view>
Page({

  /**
   * 在小程序页面中所需要使用的数据均来自于 data
   */
  data: {
    obj: {
      name: "tom",
      age: 12
    },
    numList: [1, 2, 3],
    fruitList: [
      { id: 1, name: "🍎" },
      { id: 2, name: "🍋" },
      { id: 3, name: "🍅" }
    ]
  }
})

7. 列表渲染 - 进阶用法

  1. 如果需要对默认的变量名和下标进行修改,可以使用 wx:for-itemwx:for-index
    • 使用 wx:for-item 可以指定数组当前元素的变量名
    • 使用 wx:for-index 可以指定数组当前下标的变量名
  2. wx:for 用在 <block /> 标签上,以渲染一个包含多个节点的结构块
    • <block /> 并不是一个组件,它仅仅是一个包装元素,不会在页面中做任何渲染,只接受控制属性
    • <block /> 标签在 wxml 中可以用于组织代码结构,支持列表渲染、条件渲染等
<!-- 
	如果需要修改默认的变量名,需要使用 wx:for-item 属性;如果需要修改默认的下标变量名,需要使用 wx:for-index 属性。
	两个属性需要和 wx:for 写到同一组件上
	在重命名、修改以后,需要使用最新的变量名
 -->
<!-- 数组 -->
<view wx:for="{{ fruitList }}" wx:key="id" wx:for-item="fruitItem" wx:for-index="i">
{{ fruitItem.name }}
</view>
<!-- 对象 -->
<view wx:for="{{ obj }}" wx:key="key" wx:for-item="value" wx:for-index="key">
{{ value }} - {{ key }}
</view>

<view wx:for="{{ fruitList }}" wx:key="id" wx:for-item="fruitItem" wx:for-index="i">
	<view>名字:{{ fruitItem.name }}</view>
	<view>价格:{{ fruitItem.price }}</view>
</view>

<!-- 
	block 不是一个组件,只是渲染元素,
	也就是只是包装元素,可以组织代码,支持列表渲染,
	block 不会在页面中做任何渲染,只接受控制属性
-->
<block wx:for="{{ fruitList }}" wx:key="id" wx:for-item="fruitItem" wx:for-index="i">
	<view>名字:{{ fruitItem.name }}</view>
	<view>价格:{{ fruitItem.price }}</view>
</block>
Page({

  /**
   * 在小程序页面中所需要使用的数据均来自于 data
   */
  data: {
    fruitList: [
      { id: 1, name: "🍎", price: 66 },
      { id: 2, name: "🍋", price: 77 },
      { id: 3, name: "🍅", price: 77 }
    ]
  }
})

8. 条件渲染

条件渲染主要用来控制页面结构的展示和隐藏,在微信小程序中实现条件渲染有两种方式:

  1. 使用 wx:ifwx:elifwx:else 属性组
  2. 使用 hidden 属性

wx:ifhidden 二者的区别:

  1. wx:if :当条件为 true 时将结构展示出来,否则结构不会进行展示,通过 移除/新增节点 的方式来实现
  2. hidden:当条件为 true 时会将结构隐藏,否则结构会展示出来,通过 display 样式属性 来实现的
<view class="line">条件渲染</view>
<!-- 
	wx:if 属性组,包含 wx:if  wx:elif  wx: else
	只有对应的条件成立,属性所在的组件才会进行展示 
	wx:elif  wx: else 不能单独使用,在使用的时候,必须结合 wx:if 使用
	使用了 wx:if 属性组的组件不能被打断,组件必须连贯才可以,即中间不能参杂没有使用 wx:if 属性组的组件
 -->
<view wx:if="{{ num === 1 }}">num 等于 {{ num }}</view>
<view wx:elif="{{ num === 2 }}">num 等于 {{ num }}</view>
<view wx:else>num 大于2,目前 num 等于{{ num }}</view>

<!-- 使用了 wx:if 属性组的组件不能被打断,组件必须连贯才可以,即中间不能参杂没有使用 wx:if 属性组的组件 -->
<!-- <view wx:if="{{ num === 1 }}">num 等于 {{ num }}</view>
<view></view>
<view wx:elif="{{ num === 2 }}">num 等于 {{ num }}</view>
<view wx:else>num 大于2,目前 num 等于{{ num }}</view> -->

<!-- 
	hidden 属性
	hidden 属性 属性值,如果是 true,就会隐藏结构,如果是 false,才会展示结构
 -->
<view hidden="{{ !isFlag }}">如果 isFlag 是 true,展示结构,否则隐藏结构</view>

<!-- 
	wx:if 和 hidden 的区别:
	wx:if 控制结构的展示和隐藏,是通过新增和移除结构来实现的
	hidden 属性控制结构的 展示和隐藏,是通过 css 的 display 属性来实现
 -->

<button type="warn" bind:tap="handlerUpdata">更新num</button>
Page({
  /**
   * 在小程序页面中所需要使用的数据均来自于 data
   */
  data: {
    num: 1,
    isFlag: true
  },
  // 更新 num
  handlerUpdata(event) {
    /**
     * 通过赋值的方式直接修改数据:
     * 能够修改数据,但是不能更新页面上的数据,例如:this.data.num += 1
     * 
     * 通过 this.setData({ key: value }) 方式修改数据:1. 更新数据,2.驱动视图(页面)更新
     * key:是需要更新的数据
     * value:是最新的值
     */
    this.setData({ num: this.data.num + 1 })
  }
})

七、生命周期

1. 小程序运行机制


小程序启动可以分为两种情况,一种是冷启动,一种是热启动

冷启动:如果用户首次打开,或小程序销毁后被用户再次打开,此时小程序需要重新加载启动

热启动:如果用户已经打开过某小程序,然后在一定时间内再次打开该小程序,此时小程序并未被销毁,只是从后台状态进入前台状态

前台 和 后台状态

小程序启动后,界面被展示给用户,此时小程序处于**「前台」**状态。

当用户「关闭」小程序时,小程序并没有真正被关闭,而是进入了**「后台」**状态,当用户再次进入微信并打开小程序,小程序又会重新进入「前台」状态

挂起:小程序进入「后台」状态一段时间后(5 秒),微信停止小程序 JavaScript 线程执行,小程序进入**「挂起」**状态当开发者使用了后台播放音乐、后台地理位置等能力时,小程序可以在后台持续运行,不会进入到挂起状态

销毁:如果用户很久没有使用小程序,或者系统资源紧张,小程序会被销毁,即完全终止运行。

当小程序进入后台并被「挂起」后,如果很长时间(目前是 30 分钟)都未再次进入前台,小程序会被销毁当小程序占用系统资源过高,可能会被系统销毁或被微信客户端主动回收。

2. 小程序更新机制

在访问小程序时,微信会将小程序代码包缓存到本地。

开发者在发布了新的小程序版本以后,微信客户端会检查本地缓存小程序有没有新版本,并进行小程序代码包的更新。

小程序的更新机制有两种:启动时同步更新启动时异步更新

启动时同步更新:微信运行时,会定期检查最近使用的小程序是否有更新。如果有更新,下次小程序启动时会同步进行更新,更新到最新版本后再打开小程序。如果 用户长时间未使用小程序时,会强制同步检查版本更新

启动时异步更新:在启动前没有发现更新,小程序每次 冷启动 时都会异步检查是否有更新版本。如果发现有新版本,将会异步下载新版本的代码包,将新版本的小程序在下一次冷启动进行使用,当访问使用的依然是本地的旧版本代码

在启动时异步更新的情况下,如果开发者希望立刻进行版本更新,可以使用 wx.getUpdateMannager API 进行处理。在有新版本时提示用户重启小程序更新新版本。

App({
	/**
	 * onLaunch 是小程序的钩子函数,这个钩子函数在冷启动时肯定会执行到
	 * 当小程序冷启动时,会自动微信后台请求新版本信息,如果有新版本,会立即进行下载
	 */
	onLaunch() {
		// 使用 wx.getUpdateManager() 方法监听下载的状态
		const updateMannager = wx.getUpdateManager()
		// 当下载完成新版本以后,会触发 onUpdateReady 回调函数
		updateMannager.onUpdateReady(function() {
			// 在回调函数中给用户提示
			wx.showModal({
				title: '更新提示',
				content: '新版本已经准备好,是否重启应用?',
				complete: (res) => {
					if (res.confirm) {
						// 强制当前小程序使用新版本,并且会重启当前小程序
						updateMannager.applyUpdate()
					}
				}
			})
		})
	}
})

模拟更新设置:

3. 小程序生命周期介绍

应用生命周期是指应用程序进程 从创建到消亡的整个过程

小程序的生命周期指的是 小程序从启动到销毁的整个过程

小程序完整的生命周期由 应用生命周期页面生命周期组件生命周期 三部分来组成。

小程序生命周期伴随着一些函数,这些函数由小程序框架本身提供,被称为 生命周期函数,生命周期函数会按照顺序依次触发调用,帮助程序员在特定的时机执行特定的操作,辅助程序员完成一些比较复杂的逻辑。

4. 应用生命周期

应用生命周期通常是指一个小程序从 启动 → 运行 → 销毁的整个过程

应用生命周期伴随着一些函数,我们称为 应用生命周期函数,应用生命周期函数需要 app.js 文件的 App() 方法中进行定义,App() 方法必须在 app.js 中进行调用,主要用来注册小程序。

应用生命周期函数由 onLaunchonShowonHide 三个函数组成

从小程序生命周期的角度来看,我们一般讲的 「启动」专指冷启动,热启动一般被称为后台切前台。

App({

	/**
	 * 当小程序初始化完成时,会触发 onLaunch(全局只触发一次)
	 */
	onLaunch: function () {
		/**
		 * 当进行冷启动时,才会触发 onLaunch 钩子函数
		 * 如果时热启动,不会触发 onLaunch 钩子函数,会触发 onShow 钩子函数
		 * 因此,onLaunch(全局只触发一次)
		 */
		console.log("onLaunch 小程序初始化完成时")
	},

	/**
	 * 当小程序启动,或从后台进入前台显示,会触发 onShow
	 */
	onShow: function (options) {
		console.log("onShow 当小程序启动,或从后台进入前台显示")
	},

	/**
	 * 当小程序从前台进入后台,会触发 onHide
	 */
	onHide: function () {
		console.log("onHide 当小程序从前台进入后台")
	},

	/**
	 * 当小程序发生脚本错误,或者 api 调用失败时,会触发 onError 并带上错误信息
	 */
	onError: function (msg) {
		
	}
})

5. 页面生命周期

页面生命周期通常是指一个小程序从 加载 → 运行 → 销毁的整个过程

页面生命周期函数需要在 Page() 方法进行定义

<!-- redirect:销毁当前页面,跳转到下一个页面 -->
<navigator url="/pages/list/list" open-type="redirect">跳转到列表页面-redirect</navigator>
<!-- navigate:保留当前页面,跳转到下一个页面 -->
<navigator url="/pages/list/list" open-type="navigate">跳转到列表页面-navigate</navigator>
Page({

	/**
	 * 页面的初始数据
	 */
	data: {
		
	},

	/**
	 * 生命周期函数--监听页面加载--一个页面只会调用只会调用一次
	 */
	onLoad: function (options) {
		console.log("onLoad 页面创建的时候");
	},

	/**
	 * 生命周期函数--监听页面初次渲染完成--一个页面只会调用只会调用一次
	 */
	onReady: function () {
		console.log("onReady 页面初次渲染完成,代表页面已经准备妥当,可以和视图层进行交互");
	},

	/**
	 * 生命周期函数--监听页面显示--如果从后台进入前台
	 */
	onShow: function () {
		console.log("onShow 页面在前台展示的时候");
	},

	/**
	 * 生命周期函数--监听页面隐藏--在当前小程序进入后台是,也会触发执行
	 */
	onHide: function () {
		console.log("onHide 页面隐藏");
	},

	/**
	 * 生命周期函数--监听页面卸载
	 */
	onUnload: function () {
		console.log("onHide 页面卸载、销毁");
	},

	/**
	 * 页面相关事件处理函数--监听用户下拉动作
	 */
	onPullDownRefresh: function () {
		
	},

	/**
	 * 页面上拉触底事件的处理函数
	 */
	onReachBottom: function () {
		
	},

	/**
	 * 用户点击右上角分享
	 */
	onShareAppMessage: function () {
		
	}
})

onLoad、onReady 两个钩子函数,一个页面只会触发一次

6. 生命周期两个细节

  1. tabBar 页面之间相互切换,页面不会被销毁
  2. 点击左上角,返回上一个页面,会销毁当前页面

八、小程序 API

1. 小程序 API介绍

小程序开发框架提供丰富的微信原生 API,可以方便的调起微信提供的能力,例如:获取用户信息、微信登录、微信支付等,小程序提供
的 API 几乎都挂载在 wx 对象下,例如:wx.request()wx.setStorage() 等,wx 对象实际上就是小程序的宿主环境微信所提供的全局对象

异步 API 支持 callback & Promise 两种调用方式:

  1. 当接口参数 Object 对象中不包含 success/fail/complete 时将默认返回 Promise
  2. 部分接口如 request, uploadFile 本身就有返回值,因此不支持 Promise 风格的调用方式,它们的 promisify 需要开发者自行封装。

2. 网络请求

发起网络请求获取服务器的数据,需要使用 wx,request() 接口 API

wx,request() 请求的域名必须在微信公众平台进行配置,如果使用 wx,request() 请求未配置的域名,在控制台会有相应的报错。

跳过域名的校验的开发:

  1. 在微信开发者工具中,点击详情按钮,切换到本地详情,将不校验合法域名、web-view(业务域名)、TLS版本以及HTTPS证书 勾选上
  2. 在真机上,需要点击胶囊区域的分析按钮,在弹框中选择 开发调试,重启小程序后即可

注意事项:

这两种方式只适用于开发者工具、小程序的开发版和小程序的体验版,项目上线前必须在小程序管理平台进行合法域名的配置。

<button type="warn" bind:tap="getData">获取数据</button>
Page({

	/**
	 * 页面的初始数据
	 */
	data: {
		list: []
	},
	// 获取数据
	getData() {
		// 如果需要发起网络情趣,需要使用 wx.request API
		wx.request({
			// 接口地址
			url: 'https://gmall-prod.atguigu.cn/mall-api/index/findBanner',
			// 请求方式
			method: 'GET',
			// 请求参数
			data: {},
			// 请求头
			header: {},
			// API 调用成功以后,执行的回调
			success: (res) => {
				console.log(res);
				if(res.data.code === 200) {
					this.setData({list: res.data.data})
				}
			},
			// API 调用失败以后,执行的回调
			fail: (err) => {
				console.log(err);
			},
			// API 不管调用成功还是失败,执行的回调
			complete: () => {
				console.log("complete");
			}
		})
	}
})

3. 界面交互 - loading 提示框

小程序提供了一些用于界面交互的 API,例如:loading 提示框、消息提示框、模态对话框等 API

loading 提示框常配合网络请求来使用,用于增加用户体验,对应的API有两个:

  1. wx.showLoading() 显示 loading 提示框
  2. wx.hideLoading() 关闭 loading 提示框
Page({

	/**
	 * 页面的初始数据
	 */
	data: {
		list: []
	},
	// 获取数据
	getData() {
		// 显示 loading 提示框
		wx.showLoading({
			/**
			 * title 用来显示提示的内容
			 * 提示的内容不会自动换行,如果提示的内容比较多,因为在同一行展示,多出来的内容会被隐藏
			 */
			title: "数据加载中....",
			// 是否显示透明蒙层,防止触摸穿透
			mask: true
		});
		// 如果需要发起网络情趣,需要使用 wx.request API
		wx.request({
			// 接口地址
			url: 'https://gmall-prod.atguigu.cn/mall-api/index/findBanner',
			// 请求方式
			method: 'GET',
			// 请求参数
			data: {},
			// 请求头
			header: {},
			// API 调用成功以后,执行的回调
			success: (res) => {
				console.log(res);
				if(res.data.code === 200) {
					this.setData({list: res.data.data})
				}
			},
			// API 调用失败以后,执行的回调
			fail: (err) => {
				console.log(err);
			},
			// API 不管调用成功还是失败,执行的回调
			complete: () => {
				console.log("complete");
				// 关掉 loading 提示框,通常放在 complete 回调函数里面
				wx.hideLoading()
			}
		})
	}
})

注意:wx.showLoading()wx.hideLoading() 必须结合,配对使用。

4. 界面交互 - 模态对话框 - 消息提示框

wx.showModal():模态对话框,常用于询问用户是否执行一些操作

例如:询问用户是否退出登录、是否删除该商品 等

wx.showToast():消息提示框,根据用户的某些操作来告知操作的结果

例如:退出成功给用户提示,提示删除成功等

<button type="warn" bind:tap="delHandler">删除商品</button>
Page({
 	async delHandler() {
		// showModal 显示模态对话框 是一个异步函数,返回 一个 Promise 对象
		const { confirm } = await wx.showModal({
			// 提示标题
			title: '提示',
			// 提示内容
			content: '是否删除该商品?'
		})
		if (confirm) {
			// showToast 消息提示框
			wx.showToast({
				title: '删除成功',
				icon: 'none',
				duration: 2000,
			})
		} else {
			wx.showToast({
				title: '取消删除',
				icon: 'error',
				duration: 2000,
			})
		}
	}
})

5. 本地存储

小程序本地存储是指在小程序中使用 API 将数据存储在用户的设备上,以便小程序运行时和下次启动时快速地读取这些数据

注意事项: 对象类型的数据,可以直接进行存储获取,无需使用 JSON.stringfy()JSON.parse() 转换

同步 API 示例:

<button size="mini" plain type="warn" bindtap="setStorage">存储</button>
<button size="mini" plain type="primary" bindtap="getStorage">获取</button>
<button size="mini" plain type="warn" bindtap="removeStorage">删除</button>
<button size="mini" plain type="primary" bindtap="clearStorage">清空</button>
Page({
  // 将数据存储到本地
  setStorage() {
    // 第一个参数:本地存储中指定的 key,第二个参数:需要存储的数据
    wx.setStorageSync('num', 1)
    // 在小程序中,如果对象类型的数据,可以直接进行存储获取,无需使用 JSON.stringfy()、JSON.parse() 转换
    wx.setStorageSync('obj', {name: 'Tom', age: 10})
  },
  // 获取本地存储的数据
  getStorage() {
    // 从本地存储的数据中获取指定 key 的数据、内容
    const num = wx.getStorageSync('num')
    const obj = wx.getStorageSync('obj')
    console.log(num);
    console.log(obj);
  },
  // 删除本地存储的数据
  removeStorage() {
    // 从本地移除指定 key 的数据、内容
    wx.removeStorageSync('num')
  },
  // 清空本地存储的全部数据
  clearStorage() {
    wx.clearStorageSync()
  }
})

异步 API 示例:

<button size="mini" plain type="warn" bindtap="setStorage">存储</button>
<button size="mini" plain type="primary" bindtap="getStorage">获取</button>
<button size="mini" plain type="warn" bindtap="removeStorage">删除</button>
<button size="mini" plain type="primary" bindtap="clearStorage">清空</button>
Page({
  // 将数据存储到本地
  setStorage() {
    /*----------------------------- 同步 API -------------------------------------------*/ 
    // 第一个参数:本地存储中指定的 key,第二个参数:需要存储的数据
    // wx.setStorageSync('num', 1)
    // 在小程序中,如果对象类型的数据,可以直接进行存储获取,无需使用 JSON.stringfy()、JSON.parse() 转换
    // wx.setStorageSync('obj', {name: 'Tom', age: 10})

    /*----------------------------- 异步 API -------------------------------------------*/ 
    wx.setStorage({
      key: 'num',
      data: 1
    })
    wx.setStorage({
      key: 'obj',
      data: {name: 'Jerry', age: 18}
    })
  },
  // 获取本地存储的数据
  async getStorage() {
    /*----------------------------- 同步 API -------------------------------------------*/ 
    // 从本地存储的数据中获取指定 key 的数据、内容
    // const num = wx.getStorageSync('num')
    // const obj = wx.getStorageSync('obj')
    
    /*----------------------------- 异步 API -------------------------------------------*/ 
    const num = await wx.getStorage({ key: 'num' })
    const obj = await wx.getStorage({ key: 'obj' })
    console.log(num);
    console.log(obj);
  },
  // 删除本地存储的数据
  removeStorage() {
    /*----------------------------- 同步 API -------------------------------------------*/ 
    // 从本地移除指定 key 的数据、内容
    // wx.removeStorageSync('num')

    /*----------------------------- 异步 API -------------------------------------------*/ 
    wx.removeStorage({ key: 'num' })
  },
  // 清空本地存储的全部数据
  clearStorage() {
    /*----------------------------- 同步 API -------------------------------------------*/ 
    // wx.clearStorageSync()
    
    /*----------------------------- 异步 API -------------------------------------------*/ 
    wx.clearStorage()
  }
})

6. 路由与通信

在小程序中的页面跳转,有两种方式:

  1. 声明式导航:navigator 组件
  2. 编程式导航:使用小程序提供的 API

路径后可以带参数,参数与路劲之间使用 ? 分隔,参数键与参数值用 = 相连,不同参数用 & 分隔,例如:path?key=value&key2=value2 参数需要在跳转到的页面的 onLoad 钩子函数 中通过形参进行接收

<button plain size="mini" type="warn" bindtap="navigateTo">navigateTo</button>
<button plain size="mini" type="primary" bindtap="redirectTo">redirectTo</button>
<button plain size="mini" type="warn" bindtap="switchTo">switchTo</button>
<button plain size="mini" type="primary" bindtap="relaunchTo">relaunchTo</button>
Page({
  navigateTo() {
    // 保留当前页面,跳转到应用中其他页面,不能跳转到 tabbar 页面
    wx.navigateTo({
      url: '/pages/list/list?id=1&name=Tom',
      // url: '/pages/cate/cate',
    })
  },
  redirectTo() {
    // 销毁当前页面,跳转到应用中其他页面,不能跳转到 tabbar 页面
    wx.redirectTo({
      url: '/pages/list/list?id=1&name=Tom',
      // url: '/pages/cate/cate',
    })
  },
  switchTo() {
    // 跳转到 tabBar 页面,不能跳转到非 tabBar 页面,路劲后面不能传递参数
    wx.switchTab({
      // url: '/pages/list/list',
      url: '/pages/cate/cate',
    })
  },
  relaunchTo() {
    // 关闭所用的页面,然后跳转到应用中某一个页面
    wx.reLaunch({
      url: '/pages/list/list?id=1&name=Tom'
    })
  }
})
<button plain size="mini" type="warn" bindtap="navigateBack">navigateBack</button>
Page({
  navigateBack() {
    // 关闭当前页面,返回上一级或者返回多级页面
    wx.navigateBack({delta: 1})
  },
  onLoad(options) {
    console.log(options);
  }
})

7. 页面处理函数 - 上拉加载

上拉加载是小程序中常见的一种加载方式,当用户滑动页面到底部时,回家再更多的内同,以便用户继续浏览

小程序中实现上拉加载的方式:

  1. app.json 或者 page.json 中配置距离页面底部距离:onReachBottomDistance;默认 50px
  2. 在 页面.js 中定义 onReachBottom 事件监听用户上拉加载
<view wx:for="{{ numList }}" wx:key="*this">{{ item }}</view>
{
  "usingComponents": {},
  "onReachBottomDistance": 100
}
view {
	height: 400rpx;
	display: flex;
	align-items: center;
	justify-content: center;
}

view:nth-child(odd) {
	background-color: lightcyan;
}
view:nth-child(even) {
	background-color: coral;
}
Page({
  /**
   * 页面的初始数据
   */
  data: {
    numList: [1, 2, 3]
  },
  /**
   * 监听用户上拉加载
   */
  onReachBottom() {
    /**
     * 产品需求
     * 当用户上拉,需要数字进行累加
     * 分析:
     * 当用户上拉加载时,需要对数字进行累加,每次加 3 个数字,目前时 [1, 2, 3], [1, 2, 3, 4, 5, 6]
     * 怎么追加?
     * 获取当前数组的最后一项 n, n + 1, n + 2, n + 3
     */
    wx.showLoading({
      title: '数组加载中...'
    })
    setTimeout(() => {
      // 获取数组的最后一项
      const lastNum = this.data.numList[this.data.numList.length - 1];
      // 定义需要追加的元素
      const newArr = [lastNum + 1, lastNum + 2, lastNum + 3];
      this.setData({numList: [...this.data.numList, ...newArr]})
      wx.hideLoading()
    }, 3000)
  }
})

8.页面处理函数 - 下拉刷新

下拉刷新是小程序中常见的一种刷新方式,当用户下拉页面时,页面会自动刷新,以便用户获取最新的内容。
小程序中实现上拉加载更多的方式:

  1. 在 app.json 或者 page.json 中开启允许下拉,同时可以配置 窗口、loading 样式等
  2. 在 页面.js 中定义 onPullDownRefresh 事件监听用户下拉刷新
<view wx:for="{{ numList }}" wx:key="*this">{{ item }}</view>
{
  "usingComponents": {},
  "onReachBottomDistance": 100,
  "enablePullDownRefresh": true,
  "backgroundColor": "#efefef",
  "backgroundTextStyle": "light"
}
view {
	height: 400rpx;
	display: flex;
	align-items: center;
	justify-content: center;
}

view:nth-child(odd) {
	background-color: lightcyan;
}
view:nth-child(even) {
	background-color: coral;
}
Page({
  /**
   * 页面的初始数据
   */
  data: {
    numList: [1, 2, 3]
  },
  /**
   * 监听用户下拉刷新
   */
  onPullDownRefresh() {
    /**
     * 产品需求:
     * 当用户上拉加载更多以后,如果用户进行了下拉刷新,需要将数据进行重置
     */
    this.setData({
      numList: [1, 2, 3]
    })
    // 在下拉刷新以后,loading 效果有可能不会回弹回去
    if (this.data.numList.length === 3) {
      wx.stopPullDownRefresh()
    }
  },
  /**
   * 监听用户上拉加载
   */
  onReachBottom() {
    /**
     * 产品需求
     * 当用户上拉,需要数字进行累加
     * 分析:
     * 当用户上拉加载时,需要对数字进行累加,每次加 3 个数字,目前时 [1, 2, 3], [1, 2, 3, 4, 5, 6]
     * 怎么追加?
     * 获取当前数组的最后一项 n, n + 1, n + 2, n + 3
     */
    wx.showLoading({
      title: '数组加载中...'
    })
    setTimeout(() => {
      // 获取数组的最后一项
      const lastNum = this.data.numList[this.data.numList.length - 1];
      // 定义需要追加的元素
      const newArr = [lastNum + 1, lastNum + 2, lastNum + 3];
      this.setData({numList: [...this.data.numList, ...newArr]})
      wx.hideLoading()
    }, 3000)
  }
})

9. 增强 scroll-view

使用 scroll-view 实现上拉加载更多和下拉刷新功能

<scroll-view
	scroll-y
	class="scroll-y"
	lower-threshold="100"
	bindscrolltolower="getMore"
	enable-back-to-top
	refresher-enabled
	refresher-default-style="white"
	refresher-background="#f7f7f8"
	bindrefresherrefresh="refreshHandler"
	refresher-triggered="{{ isTriggered }}"
>
	<view wx:for="{{ numList }}" wx:key="*this">{{ item }}</view>
</scroll-view>
.scroll-y {
	height: 100vh;
	background-color: #efefef;
}
view {
	height: 400rpx;
	display: flex;
	align-items: center;
	justify-content: center;
}

view:nth-child(odd) {
	background-color: lightcyan;
}
view:nth-child(even) {
	background-color: coral;
}
Page({

  /**
   * 页面的初始数据
   */
  data: {
    numList: [1, 2, 3],
    isTriggered: false
  },
  // scroll-view 上拉加载更多事件的逻辑
  getMore() {
    /**
     * 产品需求
     * 当用户上拉,需要数字进行累加
     * 分析:
     * 当用户上拉加载时,需要对数字进行累加,每次加 3 个数字,目前时 [1, 2, 3], [1, 2, 3, 4, 5, 6]
     * 怎么追加?
     * 获取当前数组的最后一项 n, n + 1, n + 2, n + 3
     */
    wx.showLoading({
      title: '数组加载中...'
    })
    setTimeout(() => {
      // 获取数组的最后一项
      const lastNum = this.data.numList[this.data.numList.length - 1];
      // 定义需要追加的元素
      const newArr = [lastNum + 1, lastNum + 2, lastNum + 3];
      this.setData({numList: [...this.data.numList, ...newArr]})
      wx.hideLoading()
    }, 3000)
  },

  refreshHandler() {
    wx.showToast({
      title: '下拉刷新...',
    })
    this.setData({
      numList: [1, 2, 3],
      isTriggered: false
    })
  }
})

九、自定义组件

1. 创建和注册组件

小程序目前已经支持组件化开发,可以将页面中的功能模块抽取成自定义组件,以便在不同的页面中重复使用;
也可以将复杂的页面拆分成多个低耦合的模块,有助于代码维护。
开发中常见的组件有两种:

  1. 公共组件:将页面内的功能模块抽取成自定义组件,以便在不同的页面中重复使用
  2. 页面组件:将复杂的页面拆分成多个低耦合的模块,有助于代码维护

如果是公共组件,建议放在项目根目录的 components 文件夹中
如果是页面组件,建议放在对应页面的目录下

建议:一个组件一个文件夹


开发中常见的组件主要分为 公共组件页面组件 两种,因此注册组件的方式也分为两种:

  1. 全局注册:在 app.json 文件中配置 usingComponents 进行注册,注册后可以在任意页面使用
  2. 局部注册:在页面的 json 文件中配置 usingComponents 进行注册,注册后只能在当前页面使用

在 usingComponents 中进行组件注册时,需要提供 自定义组件的组件名 和 自定义组件文件路径
在将组件注册好以后,直接将 自定义组件的组件名 当成 组件标签名 使用即可

2. 组件的数据以及方法

组件数据和方法需要在 组件.js 的 Component 方法中进行定义, Component 创建自定义组件

  1. data:定义组件的内部数据
  2. methods:在组件中事件处理程序需要写到 methods 中才可以

3. 组件的属性 - properties

Properties 是指组件的对外属性,主要用来接收组件使用者传递给组件内部的数据,和 data 一同用于组件的模板渲染

注意事项:
设置属性类型需要使用 type 属性,属性类型是必填项,value 属性为默认值
属性类型可以为 String、Number、Boolean、Object、Array ,也可以为 null 表示不限制类型

4. 组件 wxml 的 slot - 插槽

在使用基础组件时,可以在组件中间写子节点,从而将子节点的内容展示到页面中,自定义组件也可以接收子节点
只不过在组件模板中需要定义 <slot /> 节点,用于承载组件中的子节点

默认情况下,一个组件的 wxml 中只能有一个 slot(默认插槽)。需要使用多 slot 时,可以在组件 js 中声明启用。
同时需要给 slot 添加 name 属性来区分不同的 slot(具名插槽),然后给子节点内容添加 slot 属性,属性值是对应 slot 的 name 名称,从而将内容插入到对应的 slot 中。

"usingComponents": {
  "custom-checkbox": "./components/custom-checkbox/custom-checkbox",
  "custom-slot": "./components/custom-slot/custom-slot"
}
<custom-checkbox lable="我已阅读并同意 用户协议 和 隐私协议" position="right">
	我已阅读并同意 用户协议 和 隐私协议
</custom-checkbox>

<custom-checkbox lable="匿名提交" position="left">
	匿名提交
</custom-checkbox>

<custom-slot>
	<text slot="slot-top">我需要显示到顶部</text>
	<!-- 默认情况下,自定义组件的子节点内容不会进行展示 -->
	<!-- 如果想内容进行展示,需要在组件模板中定义 slot 节点 -->
	我是子节点内容
	<text slot="slot-bottom">我需要显示到底部</text>
</custom-slot>
<view class="custom-checkbox-container">
	<view class="custom-checkbox-box {{ position === 'right' ? 'right' : 'left' }}">
		<checkbox class="custom-checkbox" checked="{{ isChecked }}" bindtap="updateChecked" />
		<view>
		<!-- 
			lable 和 子节点内容都进行了展示
			要么展示 lable 属性,要么展示 子节点内容,
			如果用户传递了 lable 属性,要么展示 lable
			如果用户没有传递了 lable 属性,要么展示 子节点内容
		 -->
			<text wx:if="{{ lable !== '' }}">{{ lable }}</text>
			<slot wx:else />
		</view>
	</view>
</view>
.custom-checkbox-container {
	display: inline-block;
}

.custom-checkbox-box {
	display: flex;
	align-items: center;
}

.custom-checkbox-box.left {
	flex-direction: row-reverse;
}

.custom-checkbox-box.right {
	flex-direction: row;
}

.custom-checkbox {
	margin-left: 10rpx;
}
Component({

  /**
   * 组件的属性列表:组件的对外属性,主要用来接收组件使用者传递给组件内部的属性以及数据
   */
  properties: {
    // 如果需要接收传递的属性,有两种方式:全写、简写
    // lable: String, // 简写
    lable: {
      // type 组件使用者传递过来的数据类型 包含:String、Number、Boolean、Object、Array,也可以设置为:null 表示不显示类型
      type: String, 
      value: ''
    },
    position: {
      type: String,
      value: 'right'
    }
  },

  /**
   * 组件的初始数据:用来定义当前组件内部所需要使用的数据
   */
  data: {
    isChecked: false
  },

  /**
   * 组件的方法列表:在组件中,所有的事件处理程序都需要写到 methods 方法中
   */
  methods: {
    // 更新复选框的状态
    updateChecked() {
      this.setData({
        isChecked: !this.data.isChecked,
        /**
         * 在 JS 中可以访问和获取 properties 中的数据,但一般情况下,不建议修改,因为会造成数据流的混乱
         */
        lable: '在组件内也可以修改 properties 中的数据'
      })
    }
  }
})
<view>
	<!-- 具名插槽 -->
	<slot name="slot-top" />
	<!-- slot 就是用来接收、承载子节点内容,slot 只是一个占位符,子节点内容会将 slot 进行替换 -->
	<!-- 默认插槽 -->
	<view>
		<slot />
	</view>
	<!-- 具名插槽 -->
	<slot name="slot-bottom" />
</view>
Component({
  options: {
    // 启用多 slot 支持
    multipleSlots: true
  }
})

5. 组件样式以及注意事项

自定义组件拥有自己的 wxss 样式,组件 wxss 文件的样式,默认只对当前组件生效
编写组件样式时,需要注意以下几点:

  1. 组件和引用组件的页面不能使用 id 选择器(#a)、属性选择器([a]) 和 标签名选择器,请改用 class 选择器
// 标签名选择器
text {
  color: lightgreen;
}

// id 选择器
#content {
  color: lightgreen;
}

// 属性选择器
[id=content] {
  color: lightgreen;
}

使用 id 选择器 和 属性选择器 直接不生效,标签名选择器 官方不允许使用,样式会生效。推荐使用 class 选择器

  1. 子元素选择器(.a > .b)只能用于 view 组件与其子节点之间,用于其他组件可能导致非预期的情况,即子选择器,只能用于 view 和 子组件,用于其他组件可能会出现样式失效问题
  2. 继承样式,如 font、color ,会从组件外继承到组件内,即继承样式,例如:color\font 都会从组件外继承
  3. 除继承样式外,全局中的样式、组件所在页面的的样式对自定义组件无效(除非更改组件样式隔离选项)
  4. 官方不推荐做法,不建议 在 app.wxss页面.wxss 中使用 标签名(view)选择器(或一些其他特殊选择器)设置样式;如果是在 全局样式文件 中设置样式,会影响项目中全部的相同组件;如果是在页面样式文件中设置样式,会影响当前页面所有的相同组件。
  5. 组件和引用组件的页面中使用后代选择器(.a.b)在一些极端情况下会有非预期的表现,如果出现,请避免使用;解决方案:需要具体到使用的位置

6. 组件样式隔离

默认情况下,自定义组件的样式只受自身 wxss 的影响,但是有时我们需要组件使用者的样式能够影响到组件,这时候就需要指定特殊的样式隔离选项 stylelsolation,选择它支持以下取值:

  1. isolated:表示启用样式隔离,在自定义组件内外,使用 class 指定的样式将不会相互影响(一般情况下的默认值);
  2. apply-shared:表示页面 wxss 样式将影响到自定义组件,但自定义组件 wxss 中指定的样式不会影响页面;
  3. apply-sharedshared:表示页面 wxss 样式将影响到自定义组件,自定义组件 wxss 中指定的样式也会影响页面和其他设置了 apply-shared 或 shared 的自定义组件。
options: {
    /**
     * styleIsolation:配置组件样式隔离
     * 
     * styleIsolation: "isolated",开启样式隔离,默认值;
     * 在默认情况下,自定义组件和组件使用者如果存在相同的类名,类名不会相互影响;
     * 
     * styleIsolation: "apply-shared",表示组件使用者、页面的 wxss 样式能够影响到自定义组件,
     * 但是自定义组件的样式不会影响组件使用者、页面的 wxss 样式;
     * 
     * styleIsolation: "shared",表示组件使用者、页面的 wxss 样式能够影响到自定义组件,
     * 自定义组件的样式会影响组件使用者、页面的 wxss 样式和其他使用了 apply-shared 和 shared 属性的自定义组件
     */
    styleIsolation: "shared"
  },

7. 件样式隔离 - 小程序修改 checkbox 样式

<custom-checkbox lable="我已阅读并同意 用户协议 和 隐私协议" position="right">
  我已阅读并同意 用户协议 和 隐私协议
</custom-checkbox>

<view></view>

<view class="custom">
  <custom-checkbox lable="匿名提交" position="left">
    匿名提交
  </custom-checkbox>
</view>
/* 2.组件使用者也能修改默认样式 */
/* 可以通过在加外层的方式进行提升样式的权重 */
/* 复选框没有选中时默认的样式 */
.custom .custom-checkbox .wx-checkbox-input {
  border: 1px solid skyblue;
}
/* 复选框选中时默认的样式 */
.custom .custom-checkbox .wx-checkbox-input-checked {
  background-color: skyblue !important;
}
Component({
  options: {
    /**
     * styleIsolation:配置组件样式隔离
     * 
     * styleIsolation: "isolated",开启样式隔离,默认值;
     * 在默认情况下,自定义组件和组件使用者如果存在相同的类名,类名不会相互影响;
     * 
     * styleIsolation: "apply-shared",表示组件使用者、页面的 wxss 样式能够影响到自定义组件,
     * 但是自定义组件的样式不会影响组件使用者、页面的 wxss 样式;
     * 
     * styleIsolation: "shared",表示组件使用者、页面的 wxss 样式能够影响到自定义组件,
     * 自定义组件的样式会影响组件使用者、页面的 wxss 样式和其他使用了 apply-shared 和 shared 属性的自定义组件
     */
    styleIsolation: "shared"
  },
  /**
   * 组件的属性列表:组件的对外属性,主要用来接收组件使用者传递给组件内部的属性以及数据
   */
  properties: {
    // 如果需要接收传递的属性,有两种方式:全写、简写
    // lable: String, // 简写
    lable: {
      // type 组件使用者传递过来的数据类型 包含:String、Number、Boolean、Object、Array,也可以设置为:null 表示不显示类型
      type: String, 
      value: ''
    },
    position: {
      type: String,
      value: 'right'
    }
  },

  /**
   * 组件的初始数据:用来定义当前组件内部所需要使用的数据
   */
  data: {
    isChecked: false
  },

  /**
   * 组件的方法列表:在组件中,所有的事件处理程序都需要写到 methods 方法中
   */
  methods: {
    // 更新复选框的状态
    updateChecked() {
      this.setData({
        isChecked: !this.data.isChecked,
        /**
         * 在 JS 中可以访问和获取 properties 中的数据,但一般情况下,不建议修改,因为会造成数据流的混乱
         */
        // lable: '在组件内也可以修改 properties 中的数据'
      })
    }
  }
})
<view class="custom-checkbox-container">
	<view class="custom-checkbox-box {{ position === 'right' ? 'right' : 'left' }}">
		<checkbox class="custom-checkbox" checked="{{ isChecked }}" bindtap="updateChecked" />
		<view class="content">
		<!-- 
			lable 和 子节点内容都进行了展示
			要么展示 lable 属性,要么展示 子节点内容,
			如果用户传递了 lable 属性,要么展示 lable
			如果用户没有传递了 lable 属性,要么展示 子节点内容
		 -->
			<text wx:if="{{ lable !== '' }}">{{ lable }}</text>
			<slot wx:else />
		</view>
	</view>
</view>
.custom-checkbox-container {
	display: inline-block;
}

.custom-checkbox-box {
	display: flex;
	align-items: center;
}

.custom-checkbox-box.left {
	flex-direction: row-reverse;
}

.custom-checkbox-box.right {
	flex-direction: row;
}

.custom-checkbox {
	margin-left: 10rpx;
}

.content {
	font-size: 24rpx;
}

/* 
	复选框组件是公共组件,以后需要在多个页面或者多个项目中使用,所以需要先给复选框组件准备、设置一些默认样式
	如果在其他页面或者项目中使用的时候,发现样式不符合产品需求,可以进行修改、对默认样式进行修改
 */
 /* 
 1. 需要给复选框设置默认样式,需要先找到小程序给复选框提供的类名,
 通过小程序给提供的类名才可以修改,需要到小程序开发者文档,找到复选框文档,审查元素,进行查找

 在自定义组件中,不能直接修改复选框样式,如果需要进行修改,需要设置 styleIsolation: "shared" 
 shared:修改其他页面的样式、组件使用者的样式、以及其他使用了 share 以及 apply-share 的组件
 如果只想影响当前组件,可以添加命名空间
 */
 /* 复选框没有选中时默认的样式 */
.custom-checkbox .wx-checkbox-input {
	width: 24rpx !important;
	height: 24rpx !important;
	border-radius: 50% !important;
	border: 1px solid #fda007;
}
 /* 复选框选中时默认的样式 */
.custom-checkbox .wx-checkbox-input-checked {
	background-color: #fda007 !important;

}

/* 复选框选中时 √ 样式 */
.custom-checkbox .wx-checkbox-input.wx-checkbox-input-checked::before {
	font-size: 22rpx;
	color: #fff;
}

8.数据监听器

数据监听器主要用于监听和响应任何属性(properties)和 数据(data)的变化,当数据发生变化时就会触发对应回调函数,从而方便开发者进行业务逻辑的处理
在组件中如果需要进行数据监听 需要使用 observers 字段

<custom-slot label="标题"></custom-slot>
<view>
	<view>{{ num }}</view>
	<view>{{ count }}</view>
	<view>{{ obj.name }}</view>
	<view>{{ arr[1] }}</view>
	<view>{{ label }}</view>
	<button type="warn" plain bindtap="updateData">更新数据</button>
</view>
Component({

  /**
   * 组件的属性列表
   */
  properties: {
    label: {
      type: String,
      value: "测试"
    }
  },

  /**
   * 组件的初始数据
   */
  data: {
    num: 10,
    count: 100,
    obj: { name: 'Tom', age: 22 },
    arr: [1, 2, 3]
  },

  // 用来监听数据以及属性是否发生了变化
  observers: {
    /**
     * 监听单个属性
     * key: 需要监听的数据
     * value:就是一个回调函数,形参:最新的数据
     */
    num: function(newNum) {
      // 对 data 中的数据进行监听,如果数据没有发生改变,监听器不会执行
      console.log(newNum);
    },
    // count: function(newCount) {
    //   console.log(newCount);
    // }
    /**
     * 同时监听多个数据
     */
    // "num,count": function(newNum, newCount) {
    //   console.log(newNum);
    //   console.log(newCount);
    // }

    // 支持监听属性以及内部数据的变化
    // 'obj.name': function(newName) {
    //   console.log(newName);
    // },
    // 'arr[1]': function(newItem) {
    //   console.log(newItem);
    // },

    // 使用通配符监听对象中所有属性的变化
    // 'obj.**': function(newObj) {
    //   console.log(newObj);
    // }

    label: function(newlabel) {
      // 组件使用者传递了参数,在监听器中就能获取传递的数据,即监听器立即就执行了
      console.log(newlabel);
    },
  },

  /**
   * 组件的方法列表
   */
  methods: {
    // 更新数据
    updateData() {
      this.setData({
        num: this.data.num + 1,
        // count: this.data.count - 1
        // 'obj.name': 'jerry',
        // 'arr[1]': 666
        label: "最新的标题"
      })
    }
  }
})

observers:对 data 中的数据进行监听,如果数据没有发生改变,监听器不会执行;组件使用者(父级)传递了参数,在监听器中就能获取传递的数据,即监听器立即就执行了。

9.组件通信 - 父往子传值

父组件如果需要向子组件传递数据,只需要两个步骤:

  1. 在父组件 WXML 中使用 数据绑定 的方式向子组件传递动态数据
  2. 子组件内部使用 properties 接收父组件传递的数据即可
<custom-checkbox lable="我已阅读并同意 用户协议 和 隐私协议" position="right" checked="{{ isChecked }}">
	我已阅读并同意 用户协议 和 隐私协议
</custom-checkbox>

<view></view>

<view class="custom">
	<custom-checkbox lable="匿名提交" position="left">
		匿名提交
	</custom-checkbox>
</view>
Page({
  data: {
    isChecked: true
  }
})
/* 2.组件使用者也能修改默认样式 */
/* 可以通过在加外层的方式进行提升样式的权重 */
/* 复选框没有选中时默认的样式 */
.custom .custom-checkbox .wx-checkbox-input {
	border: 1px solid skyblue;
}
/* 复选框选中时默认的样式 */
.custom .custom-checkbox .wx-checkbox-input-checked {
	background-color: skyblue !important;
}
<view class="custom-checkbox-container">
	<view class="custom-checkbox-box {{ position === 'right' ? 'right' : 'left' }}">
		<checkbox class="custom-checkbox" checked="{{ isChecked }}" bindtap="updateChecked" />
		<view class="content">
		<!-- 
			lable 和 子节点内容都进行了展示
			要么展示 lable 属性,要么展示 子节点内容,
			如果用户传递了 lable 属性,要么展示 lable
			如果用户没有传递了 lable 属性,要么展示 子节点内容
		 -->
			<text wx:if="{{ lable !== '' }}">{{ lable }}</text>
			<slot wx:else />
		</view>
	</view>
</view>
.custom-checkbox-container {
	display: inline-block;
}

.custom-checkbox-box {
	display: flex;
	align-items: center;
}

.custom-checkbox-box.left {
	flex-direction: row-reverse;
}

.custom-checkbox-box.right {
	flex-direction: row;
}

.custom-checkbox {
	margin-left: 10rpx;
}

.content {
	font-size: 24rpx;
}

/* 
	复选框组件是公共组件,以后需要在多个页面或者多个项目中使用,所以需要先给复选框组件准备、设置一些默认样式
	如果在其他页面或者项目中使用的时候,发现样式不符合产品需求,可以进行修改、对默认样式进行修改
 */
 /* 
 1. 需要给复选框设置默认样式,需要先找到小程序给复选框提供的类名,
 通过小程序给提供的类名才可以修改,需要到小程序开发者文档,找到复选框文档,审查元素,进行查找

 在自定义组件中,不能直接修改复选框样式,如果需要进行修改,需要设置 styleIsolation: "shared" 
 shared:修改其他页面的样式、组件使用者的样式、以及其他使用了 share 以及 apply-share 的组件
 如果只想影响当前组件,可以添加命名空间
 */
 /* 复选框没有选中时默认的样式 */
.custom-checkbox .wx-checkbox-input {
	width: 24rpx !important;
	height: 24rpx !important;
	border-radius: 50% !important;
	border: 1px solid #fda007;
}
 /* 复选框选中时默认的样式 */
.custom-checkbox .wx-checkbox-input-checked {
	background-color: #fda007 !important;

}

/* 复选框选中时 √ 样式 */
.custom-checkbox .wx-checkbox-input.wx-checkbox-input-checked::before {
	font-size: 22rpx;
	color: #fff;
}

Component({
  options: {
    /**
     * styleIsolation:配置组件样式隔离
     * 
     * styleIsolation: "isolated",开启样式隔离,默认值;
     * 在默认情况下,自定义组件和组件使用者如果存在相同的类名,类名不会相互影响;
     * 
     * styleIsolation: "apply-shared",表示组件使用者、页面的 wxss 样式能够影响到自定义组件,
     * 但是自定义组件的样式不会影响组件使用者、页面的 wxss 样式;
     * 
     * styleIsolation: "shared",表示组件使用者、页面的 wxss 样式能够影响到自定义组件,
     * 自定义组件的样式会影响组件使用者、页面的 wxss 样式和其他使用了 apply-shared 和 shared 属性的自定义组件
     */
    styleIsolation: "shared"
  },
  /**
   * 组件的属性列表:组件的对外属性,主要用来接收组件使用者传递给组件内部的属性以及数据
   */
  properties: {
    // 如果需要接收传递的属性,有两种方式:全写、简写
    // lable: String, // 简写
    lable: {
      // type 组件使用者传递过来的数据类型 包含:String、Number、Boolean、Object、Array,也可以设置为:null 表示不显示类型
      type: String, 
      value: ''
    },
    position: {
      type: String,
      value: 'right'
    },
    /**
     * 复选框组件是公共组件
     * 需要在多个页面、在多个项目中进行使用,在使用的时候,有的地方默认是选中的效果,有的地方是没有选中的效果
     * 怎么处理?
     * 首先让复选框默认还是没有被选中的效果
     * 如果希望复选框默认被选中,传递属性(checked=true)到复选框组件
     */
    checked: {
      type: Boolean,
      value: false
    }
  },
  observers: {
    // 如果需要将 properties 中的数据赋值给 data,可以使用 observers 进行处理
    checked: function(newChecked) {
      this.setData({
        isChecked: newChecked
      })
    }
  },

  /**
   * 组件的初始数据:用来定义当前组件内部所需要使用的数据
   */
  data: {
    isChecked: false
  },

  /**
   * 组件的方法列表:在组件中,所有的事件处理程序都需要写到 methods 方法中
   */
  methods: {
    // 更新复选框的状态
    updateChecked() {
      this.setData({
        isChecked: !this.data.isChecked,
        /**
         * 在 JS 中可以访问和获取 properties 中的数据,但一般情况下,不建议修改,因为会造成数据流的混乱
         */
        // lable: '在组件内也可以修改 properties 中的数据'
      })
    }
  }
})

10.组件通信 - 子往父传值

子组件如果需要向父组件传递数据,可以通过小程序提供的事件系统实现,可以传递任意数据。

  1. 自定义组件内部使用 triggerEvent 方法发射一个自定义的事件,同时可以携带数据
  2. 自定义组件标签上通过 bind 方法监听 发射的事件,同时绑定事件处理函数,在事件函数函数中通过事件对象获取传递的数据
<view class="custom-checkbox-container">
  <view class="custom-checkbox-box {{ position === 'right' ? 'right' : 'left' }}">
    <checkbox class="custom-checkbox" checked="{{ isChecked }}" bindtap="updateChecked" />
      <view class="content">
        <!-- 
        lable 和 子节点内容都进行了展示
        要么展示 lable 属性,要么展示 子节点内容,
        如果用户传递了 lable 属性,要么展示 lable
        如果用户没有传递了 lable 属性,要么展示 子节点内容
        -->
        <text wx:if="{{ lable !== '' }}">{{ lable }}</text>
        <slot wx:else />
      </view>
  </view>
</view>
Component({
  options: {
    /**
     * styleIsolation:配置组件样式隔离
     * 
     * styleIsolation: "isolated",开启样式隔离,默认值;
     * 在默认情况下,自定义组件和组件使用者如果存在相同的类名,类名不会相互影响;
     * 
     * styleIsolation: "apply-shared",表示组件使用者、页面的 wxss 样式能够影响到自定义组件,
     * 但是自定义组件的样式不会影响组件使用者、页面的 wxss 样式;
     * 
     * styleIsolation: "shared",表示组件使用者、页面的 wxss 样式能够影响到自定义组件,
     * 自定义组件的样式会影响组件使用者、页面的 wxss 样式和其他使用了 apply-shared 和 shared 属性的自定义组件
     */
    styleIsolation: "shared"
  },
  /**
   * 组件的属性列表:组件的对外属性,主要用来接收组件使用者传递给组件内部的属性以及数据
   */
  properties: {
    // 如果需要接收传递的属性,有两种方式:全写、简写
    // lable: String, // 简写
    lable: {
      // type 组件使用者传递过来的数据类型 包含:String、Number、Boolean、Object、Array,也可以设置为:null 表示不显示类型
      type: String, 
      value: ''
    },
    position: {
      type: String,
      value: 'right'
    },
    /**
     * 复选框组件是公共组件
     * 需要在多个页面、在多个项目中进行使用,在使用的时候,有的地方默认是选中的效果,有的地方是没有选中的效果
     * 怎么处理?
     * 首先让复选框默认还是没有被选中的效果
     * 如果希望复选框默认被选中,传递属性(checked=true)到复选框组件
     */
    checked: {
      type: Boolean,
      value: false
    }
  },
  observers: {
    // 如果需要将 properties 中的数据赋值给 data,可以使用 observers 进行处理
    checked: function(newChecked) {
      this.setData({
        isChecked: newChecked
      })
    }
  },

  /**
   * 组件的初始数据:用来定义当前组件内部所需要使用的数据
   */
  data: {
    isChecked: false
  },

  /**
   * 组件的方法列表:在组件中,所有的事件处理程序都需要写到 methods 方法中
   */
  methods: {
    // 更新复选框的状态
    updateChecked() {
      this.setData({
        isChecked: !this.data.isChecked,
        /**
         * 在 JS 中可以访问和获取 properties 中的数据,但一般情况下,不建议修改,因为会造成数据流的混乱
         */
        // lable: '在组件内也可以修改 properties 中的数据'
      })
      /**
       * 目前复选框组件的状态是存储在复选框组件内部的、存储在自定义组件内部的,
       * 在实际开发中,组件使用者、父组件有时候也需要获取到复选框内部的状态,
       * 需要使用自定义组件内部发射一个自定义事件,如果组件使用者、父组件需要使用数据,绑定自定义事件进行获取即可。
       * 
       * 将数组传递给父组件
       * 如果需要将数据传递给父组件,需要使用 triggerEvent 发射自定义事件,第二个参数是携带得参数
       */
      this.triggerEvent("changeChecked", this.data.isChecked)
    }
  }
})
<!-- 需要在自定义组件标签上通过 bind 方法绑定自定义事件,同时绑定事件处理函数 -->
<custom-checkbox
  lable="我已阅读并同意 用户协议 和 隐私协议"
  position="right"
  checked="{{ isChecked }}"
  bind:changeChecked="getData"
  >
  我已阅读并同意 用户协议 和 隐私协议
</custom-checkbox>

<view></view>

<view class="custom">
  <custom-checkbox lable="匿名提交" position="left">
    匿名提交
  </custom-checkbox>
</view>
Page({
  data: {
    isChecked: true
  },
  getData(event) {
    // 可以通过 事件对象.detail 获取子组件传递给父组件的数据
    console.log(event.detail);
    if (event.detail) {
      console.log("提交");
    } else {
      console.log("请同意协议!");
    }
  }
})

11.组件通信 - 获取组件实例

父组件可以 通过 this.selectComponent 方法,获取子组件实例对象,这样就可以直接访问子组件的任意数据和方法
this.selectComponent 方法在调用时需要传入一个匹配选择器 selector

<custom-checkbox
	class="child"
	id="child"
	lable="我已阅读并同意 用户协议 和 隐私协议"
	position="right"
	checked="{{ isChecked }}"
	bind:changeChecked="getData"
>
	我已阅读并同意 用户协议 和 隐私协议
</custom-checkbox>

<button type="primary" plain bindtap="getChild">获取子组件实例对象</button>
Page({
  data: {
    isChecked: true
  },
  getData(event) {
    // 可以通过 事件对象.detail 获取子组件传递给父组件的数据
    console.log(event.detail);
    if (event.detail) {
      console.log("提交");
    } else {
      console.log("请同意协议!");
    }
  },
  // 获取子组件的实例对象
  getChild() {
    /**
     * this.selectComponent 方法获取子组件实例对象(参数 可传 id 选择器 和 类选择器)
     * 获取到实例对象以后,就能获取子组件所有的数据,也能获取子组件的方法
     */
    const res = this.selectComponent('#child')
    console.log(res.data);
  }
})

12.组件生命周期

组件的生命周期:指的是组件自身的一些钩子函数,这些函数在特定的时间节点时被自动触发
组件的生命周期函数需要在 **lifetimes **字段内进行声明
组件的生命周期函数有 5 个:createdattached、ready、moved、detached
image.png

<custom06 wx:if="{{ num === 1 }}" />
<button type="warn" plain bindtap="handler">销毁自定义组件</button>
Page({
  data: {
    num: 1
  },
  handler() {
    this.setData({
      num: this.data.num + 1
    })
  }
})
<text>{{ name }}</text>
Component({
  data: {
    name: 'Tom'
  },
  // 组件生命周期函数
  lifetimes: {
    /**
     * 组件实例被创建好以后执行
     */
    created() {
      console.log('组件 created');
      // 在 created 钩子函数中不能调用 setData,可以给组件添加一些自定义属性,可以通过 this 的方式进行添加
      // this.setData({
      //   name: 'Jerry'
      // })
      this.test = '测试'
    },
    /**
     * 组件被初始化完毕、模板解析完成,已经把组件挂在到页面上
     */
    attached() {
      console.log('组件 attached');
      // 一般页面的交互会在 attached 钩子函数中进行执行
      this.setData({
        name: 'Jerry'
      }),
      console.log(this.test);
    },
    /**
     * 组件被销毁时
     */
    detached() {
      console.log('组件 detached');
    }
  }
})

注意事项:created 钩子函数中不能调用 setData

13.组件所在页面的生命周期

组件还有一些特殊的生命周期,这类生命周期和组件没有很强的关联
主要用于组件内部监听父组件的展示和隐藏状态,从而方便组件内部执行一些业务逻辑的处理
组件所在页面的生命周期有 4 个:showhide、resize、routeDone,需要在 pageLifeTimes 字段内进行声明
image.png

// 组件所在页面的生命周期
pageLifetimes: {
  // 监听组件所在的页面展示(后台切前台)状态
  show() {
    console.log('组件 show');
  },
  // 监听组件所在的页面隐藏(前台切后台,点击 tabbar)状态
  hide() {
    console.log('组件 hide');
  },
  resize() {},
  routeDone() {}
}

14.小程序生命周期总结

一个小程序完整的生命周期由 应用生命周期页面生命周期组件生命周期 三部分来组成

  1. 小程序冷启动,钩子函数执行的顺序

image.png

  1. 保留当前页面,进入下一个页面,钩子函数执行的顺序
  2. 销毁当前页面,进入下一个页面,钩子函数执行的顺序

image.png

  1. 小程序热启动,钩子函数执行的顺序

image.png

15.拓展 - 使用 Component 构造页面

Component 方法用于创建自定义组件
小程序的页面也可以视为自定义组件,因此页面也可以使用 Component 方法进行创建,从而实现复杂的页面逻辑开发

<navigator url="/pages/detail/detail?id=10&title=测试">跳转到详情页面</navigator>
<view>{{ name }}</view>
<button type="warn" plain bind:tap="updateName">更新名字</button>
{
  "usingComponents": {}
}
Component({
  /**
   * 小程序页面也可以使用 Component 方法进行构造
   * 
   * 为什么需要使用 Component 方法进行构造页面
   * Component 方法功能比 Page 方法强大很多,如果使用 Component 方法构造页面,可以实现更加复杂的页面逻辑开发,例如:属性监听等方法
   * 
   * 注意事项:
   * 1. 要求 .json 文件中必须要有 usingComponents 字段
   * 2. 里面的配置项需要和 Component 中的配置项保持一致
   * 3. 页面中 Page 方法有一些钩子函数、事件监听方法,这些钩子函数和事件监听方法必须放到 methods 对象中才可以
   * 4. 组件的属性 properties 也可以接收页面的参数,在 onLoad 钩子函数中,可以通过 this.data 进行获取
   */
  data: {
    name: 'Tom'
  },

  properties: {
    id: String,
    title: String
  },

  methods: {
    onLoad(options) {
      console.log('Component onload');
      // 以下三种方式都可以获取上级页面传递过来的参数
      // console.log(options);
      // console.log(this.data.id, this.data.title);
      // console.log(this.properties.id, this.properties.title);
    },
    // 更新name
    updateName() {
      this.setData({
        name: 'Jerry'
      })
    }
  }

})

为什么需要使用 Component 方法进行构造页面?
Component 方法功能比 Page 方法强大很多,如果使用 Component 方法构造页面,可以实现更加复杂的页面逻辑开发,例如:属性监听等方法
注意事项:

  1. 要求对应 .json 文件中包含 usingComponents 字段;
  2. 页面使用 Component 构造器,需要定义语普通组件一样的字段与实例方法;
  3. 页面 Page 中的一些生命周期方法(如 onLoad() 等以“on”开头的方法),在 Component 中要写在 methods 属性中才能生效;
  4. 组件的属性 properties 也可以用于接收页面的参数,在 onLoad() 中可以通过 this.data 拿到对应的页面参数;

16.拓展 - 组件的复用机制 behaviors

小程序的 behaviors 方法是一种代码复用的方式,可以将一些通用的逻辑和方法提取出来,然后再多个组件中复用,从而减少代码冗余,提高代码的可维护性。
如果需要 behavior 复用代码,需要使用 Behavior() 方法,每个 behavior 可以包含一组属性,数据,生命周期函数和方法
组件引用它时,它的属性、数据和方法会被合并到组件中,生命周期函数也会再对应世纪被调用
组件和它引用的 behavior 中可以包含同名的字段,对这些字段的处理方法如下:

  1. 如果有同名的属性或方法,采用“就近原则”,组件会覆盖 behavior 中的同名属性或方法
  2. 如果有同名的数据字段且都是对象类型,会进行对象合并,其余情况会 采用“就近原则"进行数据覆盖
  3. 生命周期函数和 observers 不会相互覆盖,会是在对应触发时机被逐个调用,也就是都会被执行
<view>{{label}}</view>
<view>{{name}}</view>
<view>{{obj.name}} - {{obj.age}}</view>

<button type="primary" plain bind:tap="updateName">更新数据</button>
const behavior = Behavior({
	/**
   * 组件的属性列表
   */
  properties: {
    label: {
      type: String,
      value: "我已同意该协议"
    }
  },

  /**
   * 组件的初始数据
   */
  data: {
    name: "Tome",
    obj: {
      name: "Tyke"
    }
  },

  /**
   * 组件的方法列表
   */
  methods: {
    updateName() {
      this.setData({
        name: "Jerry"
      })
      console.log("我是组建的函数~~~");
    }
  },
  lifetimes: {
    attached() {
      console.log("我是组建的生命周期函数~~~");
    }
  }
})

export default behavior
import behavior from './behavior'
Component({
  behaviors: [behavior],
  /**
   * 在以后的开发中,使用 behaviors 进行代码复用的时候,组件 和 behaviors 可能存在相同的字段
   */
  // 如果存在相同的 properties,采用就近原则,使用组件内部的数据
  properties: {
    label: {
      type: String,
      value: "匿名提交"
    }
  },
  // 如果存在相同的 data,如果是对象类型,属性会进行合并,如果不是对象类型的数据,就近原则,展示以组件内部为准
  data: {
    name: "组件中的 name",
    obj: {
      age: 100
    }
  },
  // 如果存在相同的方法,就近原则,展示以组件内部为准
  methods: {
    updateName() {
      console.log("我是组件内部的方法!!!");
    }
  },
  // 如果存在相同的生命周期函数,生命周期函数都会被触发,先执行 behavior 里面的,后执行 组件内里面的
  lifetimes: {
    attached() {
      console.log("我是组件内部调用的生命周期!!!");
    }
  }
})

17.拓展 - 外部样式类

默认情况下,组件和组件使用者之间如果存在相同的类名不会相互影响,组件使用者如果想修改组件的样式,就需要解除样式隔离,但是解除样式隔离以后,在极端情况下,会产生样式冲突、CSS 嵌套太深等问题,从而给我们的开发带来了一定的麻烦。
外部样式类:在使用组件时,组件使用者可以给组件传入 CSS 类名,通过传入的类名修改组件的样式。
如果需要使用外部样式类修改组件的样式,在 Component 中需要用 externalClasses 定义若干个外部样式类。
外部样式类的使用步骤:

  1. 在 Component 中用 extemmalClasses 定义段定义若干个外部样式类
  2. 自定义组件标签通过 属性绑定 的方式提供一个样式类,属性是 externalClasses 定义的元素,属性值是传递的类名
  3. 将接受到的样式类用于自定义组件内部

注意事项:在同一个节点上使用普通样式类和外部样式类时,两个类的优先级是未定义的,因此需要添加 !imporant 以保证外部样式类的优先级

<!-- 属性是在 externalClasses 里面定义的属性,属性值必须是一个类名  -->
<custom09 extend-class="my-class" />
.my-class {
  color: coral !important;
}
<!-- 
  在同一个节点上,如果存在 外部样式类 和 普通的样式类
  两个类的优先级是未定义的,建议:在使用外部样式类时,样式通过 !important 添加权重
 -->
<view class="extend-class box">通过外部样式类修改组件的样式</view>
.box {
  color: cornflowerblue;
}
Component({
  // 组件接受的外部样式类
  externalClasses: [
    'extend-class'
  ]
})

18.完善复选框案例以及总结自定义组件

  1. 组件基本使用:数据、属性、方法、插槽
  2. 组件样式使用:组件样式、注意事项、样式隔离、外部样式类
  3. 组件通信传值:父往子传值、子往父传值、获取组件实例
  4. 组件生命周期:组件的生命周期、组件所在页面的生命周期、总结了小程序全部的生命周期
  5. 组件数据监听器:observers
  6. 组件拓展:使用 Component构造页面、组件复用机制 behaviors 等

如果 styleIsolation 属性值是 shared 时,externalClasses 选项会失效

十、使用 npm 包

1.使用 npm 包

目前小程序已经支持使用 npm 安装第三方包,因为 node modules 目录中的包不会参与小程序项目的编译、上传和打包,因此在小程序项目中要使用的 npm 包,必须走一遍 构建 npm 的过程。
在构建成功以后,默认 会在小程序项目根目录,
也就是
node_modules 同级目录下生成 miniprogram_npm 目录,

里面存放着构建完成以后的 npm 包,也就是小程序运行过程中真正使用的包。
image.png
注意事项:
小程序运行在微信内部,因为运行环境的特殊性,这就导致 并不是所有的包都能够在小程序使用
我们在小程序中提到的包指专为小程序定制的包,简称小程序 npm 包,在使用之前需要先确定该包是否支持小程序
开发者如果需要发布小程序包,需要参考官方规范:npm 支持 | 微信开放文档

2.自定义构建 npm

在实际的开发中,随着项目的功能越来越多、项目越来越复杂,文件目录也变的很繁琐,为了方便进行项目的开发,开发人员通常会对目录结构进行调整优化,例如:将小程序源码放到 miniprogram 目录下
这时候需要开发者在 project.config.json 中 指定 node_moudles 的位置目标 miniprogram_npm 的位置
具体配置如下:

  1. 配置 project.config.json 的 miniprogramRoot 指定小程序源码的目录
  2. 配置 project.config.json 的 setting.packNpmManually 为 true,开启自定义 node_modules 和 miniprogram_npm 位置的构建 npm 方式
  3. 配置 project.config.json 的 seting.packNpmRelationList 项,指定 packagesonPath 和 miniprogramNpmDistDir 的位置

3.Vant Weapp 组件库的使用

Vant Weapp 是有赞前端团队开源的小程序 UI 组件库,基于微信小程序的自定义组件开发,可以用来快速搭建小程序项目。
演示:image 图片组件

{
 ...
  "usingComponents": {
    "van-image": "@vant/weapp/image/index",
    "van-loading": "@vant/weapp/loading/index"
  }
}
<!-- 在使用 van-image 图片组件时,如果需要渲染本地图片是,不能使用 ../,需要相对于小程序源码的目录来查找图片才可以 -->
<van-image
 round width="100"
 height="100" src="/assets/Jerry.png"
 bind:click="imageHandler"
 use-loading-slot
 use-error-slot
 custom-class="custom-class"
>
	<van-loading slot="loading" type="spinner" size="20" vertical />
	<text slot="error">加载失败</text>
</van-image>
Page({
	imageHandler() {
		console.log("点击图片");
	}
})

.custom-class {
  border: 10rpx solid lightgreen !important;
}

在使用 Vant 提供的组件时,只需要两个步骤:

  1. 将组件在 app.json 中进行全部注册 或者 index.json 中进行局部注册
  2. 在引入组件后,可以在 wxml 中直接使用组件

注意事项:将 app.json 中的 “style”: “v2” 去除,小程序的新版基础组件强行加上了许多样式,难以覆盖,不关闭将造成部分组件样式混乱。

4.Vant Weapp 组件样式覆盖

Vant Weapp 基于微信小程序的机制,为开发者提供了 3 种修改组件样式的方法:

  1. 解除样式隔离:在页面中使用 Vant Weapp 组件时,可直接在页面的样式文件中覆盖样式
  2. 使用外部样式类:需要注意的是普通样式类和外部样式类的优先级是未定义的,使用时需要添加 !important 保证外部样式类的优先级
  3. 使用 CSS 变量:在页面或全局对多个组件的样式做批量修改以进行主题样式的定制

十一、分包加载

1. 小程序分包加载

小程序的代码通常是由许多页面、组件以及资源等组成,随着小程序功能的增加,代码量也会逐渐增加,体积过大就会导致用户打开速度变慢,影响用户的使用体验。
分包加载是一种小程序优化技术。将小程序不同功能的代码,分别打包成不同的子包,在构建时打包成不同的分包,用户在使用时按需进行加载,在构建小程序分包项目时,构建会输出一个或多个分包。每个使用分包小程序必定含有一个主包
主包:包含默认启动页面/TabBar 页面 以及 所有分包都需用到公共资源的包
分包:根据开发者的配置进行划分出来的子包
分包后加载顺序:在小程序启动时,默认会下载主包并启动主包内页面,当用户进入分包内某个页面时,微信客户端会把对应分包下载下来,下载完成后再进行展示。
image.png
目前小程序分包大小有以下限制:

  1. 整个小程序所有分包大小不超过 20MB(大小可能调整)
  2. 单个分包/主包大小不能超过 2MB

2. 配置分包加载以及打包、引用原则

小程序如果需要进行分包加载,需要在 app.json 中,通过 subPackages 或者 subpackages 定义分包结构
每个分包结构含三个常用字段:

  1. root:分包的根目录,该目录下的所有文件都会被打包成一个独立的包
  2. name:分包的别名,用于在代码中引用该分包
  3. pages:指定当前分包中包含哪些页面

例:配置 商品模块 分包,分包包含:商品列表、商品详情两个页面

"subPackages": [
    {
      "root": "modules/goodModule",
      "name": "goodModule",
      "pages": ["pages/list/list", "pages/detail/detail"]
    }
  ],

在跳转到分包页面时,需要加上分包路径:

<navigator url="/modules/goodModule/pages/list/list" open-type="navigate">跳转到列表页面</navigator>

打包原则:

  1. tabBar 页面必须在主包内
  2. 最外层的 pages 字段,属于主包的包含的页面
  3. 按 subpackages 配置路径进行打包,配置路径外的目录将被打包到主包中
  4. 分包之间不能相互嵌套,subpackage 的根目录不能是另外一个 subpackage 内的子目录

引用原则:

  1. 主包不可以引用分包的资源,但分包可以使用主包的公共资源
  2. 分包与分包之间资源无法相互引用,分包异步化时不受此条限制

3. 独立分包

独立分包:是指能够独立于主包和其他分包运行的包
从独立分包中页面进入小程序时,不需要下载主包,当用户进入普通分包或主包内页面时,主包才会被下载
开发者可以将功能相对独立的页面配置到独立分包中,因为独立分包不依赖主包就可以运行,可以很大程度上提升分包页面的启动速度。
给 subPackages 定义的分包结构添加 independent 字段,即可声明对应分包为独立分包。

  "subPackages": [
    {
      "root": "modules/goodModule",
      "name": "goodModule",
      "pages": ["pages/list/list", "pages/detail/detail"]
    },
    {
      "root": "modules/markModule",
      "name": "markModule",
      "independent": true,
      "pages": ["pages/mark/mark"]
    }
  ],

注意事项:

  1. 独立分包中不能依赖主包和其他分包中的资源
  2. 主包中的 app.wxss 对独立分包无效
  3. App 只能在主包内定义,独立分包中不能定义 App,会造成无法预期的行为

4. 分包预下载

分包预下载是指访问小程序某个页面时,预先下载其他分包中的代码和资源,当用户需要访问分包中的页面时,已经预先下载的代码和资源,因此可以直接使用,从而提高用户的使用体验。
小程序的分包预下载需要在 app.json 中通过 preloadRule 字段设置预下载规则
preloadRule 是一个对象,对象的 key 表示访问哪个路径时进行预加载,value 是进入此页面的预下载配置,具有两个配置项:

  1. packages:进入页面后预下载分包的 root 或 name,APP 表示主包。
  2. network:在指定网络下预下载,可选值为:all(不限网络)、wifi(仅wifi下预下载)
 "preloadRule": {
    "pages/index/index": {
      "network": "all",
      "packages": ["goodModule", "modules/markModule"]
    },
    "modules/markModule/pages/mark/mark": {
      "network": "all",
      "packages": ["__APP__"]
    }
  },

十二 、开放能力

1. 获取微信头像

当小程序需要让用户完善个人资料时,我们可以通过微信提供的头像、昵称填写能力快速完善
image.png
想使用微信提供的头像填写能力,需要两步:

  1. button 组件 open-type 的值设置为 chooseAvatar
  2. 通过 bindchooseavatar 事件回调获取到头像信息的临时路径。
<view>
	<button class="btn" open-type="chooseAvatar" bindchooseavatar="chooseAvatar">
	<image class="avatar" src="{{ avatar }}" mode=""/>
	</button>
</view>
.btn {
  background-color: transparent;
}
.btn::after {
  border: none;
}

.avatar {
  width: 200rpx;
  height: 200rpx;
  border-radius: 50%;
}
Page({

	/**
	 * 页面的初始数据
	 */
	data: {
		avatar: "../../assets/Jerry.png"
	},

	chooseAvatar(event) {
		// 目前获取的微信头像是临时路径,临时路径是有失效时间的,在实际开发中,需要将临时路径上传到公司的服务器
		const { avatarUrl } = event.detail;
		this.setData({
			avatar: avatarUrl
		})
	}
})

2. 获取微信昵称

当小程序需要让用户完善个人资料时,我们可以通过微信提供的头像、昵称填写能力快速完善
image.png
想使用微信提供的昵称填写能力,需要三步:

  1. 通过 form 组件中包裹住 input 以及 form-type 为 submit 的 button 组件
  2. 需要将 input 组件 type 的值设置为 nickname,当用户输入框输入时,键盘上方会展示微信昵称
  3. 给 form 绑定 submit 事件,在事件处理函数中通过事件对象获取用户昵称
<!-- 需要使用 form 组件包裹住 input 以及 button 组件 -->
<form bindsubmit="onSubmit">
	<!-- input 输入框组件的 type 属性设置为 nickname,用户点击输入框,键盘上方才会显示昵称 -->
	<!-- 如果添加了 name 属性,from 组件就会自动收集 name 属性的表单元素的值-->
	<input type="nickname" name="nickname" placeholder="请输入昵称" />
	<!-- 如果将 from-type="submit",就将按钮变为提交按钮,在点击提交按钮的时候,会触发 表单的 bindsubmit 提交事件 -->
	<button type="primary" plain form-type="submit">点击获取昵称</button>
</form>
input {
  border: 1px solid #179c16;
  margin: 20rpx;
  height: 60rpx;
  border-radius: 20rpx;
  padding-left: 20rpx;
}
Page({
	// 获取微信昵称
	onSubmit(event) {
		// console.log(event);
		const { nickname } = event.detail.value;
		console.log(nickname);
	}
})

3. 转发功能

转发功能,主要帮助用户更流畅地与好友分享内容和服务
想实现转发功能,有两种方式:

  1. 页面is 文件 必须声明 onShareAppMessage 事件监听函数,并自定义转发内容只有定义了此事件处理函数,右上角菜单才会显示“转发”按钮
  2. 通过 给 button 组件设置属性 open-type=“share”,可以在用户点击按钮后触发 Page.onShareAppMessage 事件监听函数

image.png

<!-- 页面设置转发按钮 -->
<button plain type="primary" open-type="share">转发</button>
Page({
	/**
	 * 监听页面按钮的转发以及右上角的转发按钮
	 */
	onShareAppMessage: function (obj) {
		console.log(obj);
		return {
			title: "这是一个神奇的页面~~~",
			page: "/miniprogram/pages/index/index",
			imageUrl: "../../assets/Jerry.png"
		}
	}
})

通过页面转发,形参 obj 的 target 有值 {from: "button", target: {…}},通过右上角 “转发”按钮转发,形参 obj 的 target 没有值,{from: "menu", target: undefined}

4. 分享到朋友圈

小程序页面默认不能被分享到朋友圈,开发者需主动设置“分享到朋友圈”才可以,实现分享到朋友圈需满足两个条件:

  1. 页面 必须 设置允许“发送给朋友”,页面 js 文件声明 onShareAppMessage 事件监听函数
  2. 页面 必须 需设置允许“分享到朋友圈”,页面 js 文件声明 onShareTimeline 事件监听函数

image.png

Page({

	/**
	 * 监听页面按钮的转发以及右上角的转发按钮
	 */
	onShareAppMessage: function (obj) {
		console.log(obj);
		return {
			title: "这是一个神奇的页面~~~",
			page: "/miniprogram/pages/index/index",
			imageUrl: "../../assets/Jerry.png"
		}
	},
	/**
	 * 监听右上角 分享到朋友圈 按钮
	 */
	onShareTimeline: function() {
		return {
			title: "帮我砍一刀~~",
			query: "id=1",
			imageUrl: "../../assets/Jerry.png"
		}
	}
})

5. 手机号验证组件

手机验证组件,用于帮助开发者向用户发起手机号申请,必须经过用户同意后,才能获得由平台验证后的手机号,进而为用户提供相应服务
手机号验证组件分为两种:手机号快速验证组件 以及 手机号实时验证组件

  1. 手机号快速验证组件:平台会对号码进行验证,但不保证是实时验证
<button open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber" />
  1. 手机号实时验证组件:在每次请求时,平台均会对用户选择的手机号进行实时验证
<button open-type="getRealtimePhoneNumber" bindgetrealtimephonenumber="getrealtimephonenumber" />

注意事项:

  1. 目前该接口针对非个人开发者,且完成了认证的小程序开放(不包含海外主体)
  2. 两种验证组件需要付费使用,每个小程序账号将有 1000 次体验额度

image.png

<button
	type="primary"
	plain
	open-type="getPhoneNumber"
	bindgetphonenumber="getPhoneNumber"
>快速验证组件</button>
<button
	type="warn"
	plain
	open-type="getRealtimePhoneNumber"
	bindgetrealtimephonenumber="getRealtimePhoneNumber"
>实时验证组件</button>
Page({
  // 手机号快速验证
	getPhoneNumber(event) {
		/**
		 * 通过事件对象,可以看到,在 event.detail 中可以获取到 code,code 动态令牌,可以使用 code 换取用户的手机号,
		 * 需要将 code 发送给后端,后端在接收到 code 以后,也需要调用 API,换取用户的真正手机号,在换取成功以后,会将手机号返回给前端
		 */
		console.log(event);
	},
	// 手机号实时验证
	getRealtimePhoneNumber(event) {
		console.log(event);
	}
})

6. 客户功能

小程序为开发者提供了客服能力,同时为客服人员提供移动端、网页端客服工作台便于及时处理消息使用方式:

  1. 需要将 button 组件 open-type 的值设置为 contact,当用户点击后就会进入客服会话
  2. 在微信公众后台,绑定后的客服账号,可以登陆 网页端客服 或 移动端小程序 客服接收、发送客服消息

image.png

<button type="warn" plain open-type="contact">联系客服</button>

十三、补充

1. 框架接口 - getApp()

在小程序中,可以通过 getApp() 方法获取到小程序全局唯一的 App 实例,因此在 App() 方法中添加全局共享的数据、方法,从而实现页面、组件的数据传值。

App({

	// 全局共享的数据
	globalData: {
		token: ''
	},

	// 全局共享的方法
	setToken(token) {
		// 如果想获取 token,可以使用 this 的方式进行获取
		this.globalData.token = token
    // 在 App() 方法中,如果想获取 App() 实例,可以通过 this 的方式进行获取,不能通过 getApp() 方法获取
	}
})


<button type="primary" plain bind:tap="login">登录</button>
// getApp() 方法用来获取全局唯一的 App() 实例
const appInstance = getApp();
Page({

  // 登录
  login() {
    // 不要通过 app 实例 调用钩子函数
    console.log(appInstance);
    appInstance.setToken("adadafasdfsfafdafasfdafasfdafaf")
  }
})
const appInstance = getApp();
Page({
  onLoad() {
    console.log(appInstance.globalData.token); // adadafasdfsfafdafasfdafasfdafaf
  }
})

注意事项:

  1. 不要在 App() 方法中使用 getApp(),使用 this 就可以拿到 app 实例
  2. 通过 getpp() 获取实例之后,不要私自调用生命周期函数

2. eventChannel - 页面间通信

如果一个页面通过 wx.navigateTo 打开一个新页面,这两个页面间将建立-条数据通道

  1. 在 wx.navigateTo 的 success 回调中通过 EventChannel 对象发射事件
  2. 被打开的页面可以通过 this.getopenerEventChannel() 方法来获得一个 Eventchannel 对象,进行监听、发射事件
  3. wx.navigateTo 方法中可以定义 events 配置项接收被打开页面发射的事件

image.png

<button type="warn" plain bind:tap="handler">跳转到列表页面</button>
Page({
	handler() {
		wx.navigateTo({
			url: '/pages/list/list',
			events: {
				/**
				 * key:被打开页面通过 eventChannel 发射的事件
				 * value:回调函数,为事件添加一个监听器,获取被打开页面传递给当前页面的数据
				 */
				currentEvent: (res) => {
					console.log(res);
				}
			},
			success(res) {
				console.log(res);
				// 通过 success 回调函数的形参,可以获取 eventChannel 对象,eventChannel 对象给提供了 emit 方法,可以发射事件,同时携带参数
				res.eventChannel.emit("myEvent", { name: 'Tom' })
			}
		})
	}
})
Page({
  navigateBack() {
    // 关闭当前页面,返回上一级或者返回多级页面
    wx.navigateBack({delta: 1})
  },
  onLoad(options) {
    // 通过 this.getOpenerEventChannel() 可以获取 EventChannel 对象
    const EventChannel = this.getOpenerEventChannel();
    // 通过 EventChannel 提供的 on 方法监听页面发射的自定义事件
    EventChannel.on("myEvent", (res) => {
      console.log(res);
    })

    // 通过 EventChannel 提供的 emit 方法,也可以向上一级页面传递数据,需要使用 emit 定义自定义事件,携带需要传递的数据
    EventChannel.emit("currentEvent", {age: 10})
  }
})

3. 小程序组件通信 - 事件总线

随着项目功能的增加,业务逻辑也会变的很复杂,一个页面可能是由多个组件进行构成,并且这些组件之间需要进行数据的传递。这时候如果使用之前学习的组件传值方式进行数据的传递,就会比较麻烦
image.png
事件总线是对发布-订阅模式的一种实现,是一种集中式事件处理机制,允许不同的组件之间进行彼此通信,常用于两个非父子关系组件和兄弟组件之间通讯。我们可以借助第三方的 发布订阅 JS 包,来实现事件总线的功能,PubsubJs

<view>
  <text>父组件,子组件 A 和 子组件 B 是兄弟关系</text>
  <custom06 />
  <custom09 />
</view>
<view class="son1">
  <text>子组件 A</text>
  <button bind:tap="setData">传递数据给兄弟</button>
</view>
import PubSub from 'pubsub-js'
Component({
  data: {
    name: 'Tom'
  },
  methods: {
    setData() {
      /**
       * publish 发布、发射自定义事件
       * 第一个参数:自定义事件的名称
       * 第二个参数:需要传递的数据
       */
      PubSub.publish("myEvent", { name: this.data.name, age: 10 })
    }
  }
})
<view class="son2">
  <text>子组件 B</text>
  <view>{{ name }}</view>
</view>
import PubSub from 'pubsub-js'
Component({
  /**
   * 组件的初始数据
   */
  data: {
    name: ''
  },
  lifetimes: {
    attached() {
      /**
       * subscribe 订阅、监听自定义事件
       * 第一个参数:需要订阅、监听的自定义事件名称
       * 第二个参数:回调函数,回调函数有两个参数:msg:自定义事件的名称  data:传递过来的参数
       */
      PubSub.subscribe("myEvent", (msg, data) => {
        console.log(msg, data);
        this.setData({
          name: data.name
        })
      })
    }
  }
})

4. 自定义导航栏

小程序默认的导航栏与 APP 一样都位于顶部固定位置。但是默认导航栏可能会影响小程序整体风格,且无法满足特定的设计需求,这时候,就需要进行自定义导航栏。
在 app.json 或者 page.json 中,配置 navigationStyle 属性为 custom,即可 自定义导航栏,在设置以后,就会移除默认的导航栏,只保留右上角胶囊按钮。

{
  "usingComponents": {},
  "navigationStyle": "custom"
}
<swiper
	indicator-dots
	autoplay
	interval="2000"
	class="custom-swiper"
>
	<swiper-item>
		<image src="../../assets/banner/banner-1.png" mode=""/>
	</swiper-item>
	<swiper-item>
		<image src="../../assets/banner/banner-2.png" mode=""/>
	</swiper-item>
	<swiper-item>
		<image src="../../assets/banner/banner-3.png" mode=""/>
	</swiper-item>
</swiper>
.custom-swiper {
	height: 440rpx;
}

.custom-swiper image {
	width: 100%;
	height: 100%;
}

5. 小程序上线发布

假设我们目前已经完成了小程序的开发,并且通过了本地测试,这时候小程序就需要上线发布。
image.png
开发版本:点击开发者工具上传后的版本,开发版本只保留每人最新的一份上传的代码,是供开发者和团队测试和调试的版本
体验版本:小程序开发者可以将开发版本转换为体验版本,由测试人员以及产品经理进行测试与体验,确认没问题可提交审核
审核版本:小程序开发者可以将开发版本转换为审核版本,由微信的审核团队进行审核,审核周期为 1~7 天,审核通过可提交发布
线上版本:通过微信小程序平台审核,并由开发者提交发布的正式版本,线上版本是用户可以正常使用的小程序版本

相关推荐

  1. 程序开发教程

    2024-06-14 00:38:03       24 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-06-14 00:38:03       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-06-14 00:38:03       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-06-14 00:38:03       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-06-14 00:38:03       20 阅读

热门阅读

  1. 【设计模式之享元模式 -- C++】

    2024-06-14 00:38:03       6 阅读
  2. 文件已经删除但磁盘空间未释放

    2024-06-14 00:38:03       7 阅读
  3. TikTok限流封号要如何处理

    2024-06-14 00:38:03       7 阅读
  4. 关于自学编程的9点忠告

    2024-06-14 00:38:03       6 阅读
  5. vue中v-bind控制class和style

    2024-06-14 00:38:03       11 阅读
  6. 使用Python多线程批量压缩图片文件

    2024-06-14 00:38:03       7 阅读
  7. PTA:7-186 水仙花数

    2024-06-14 00:38:03       8 阅读
  8. 6-11 函数题:某范围中的最小值

    2024-06-14 00:38:03       8 阅读
  9. SIM卡 移动、联通、电信对比

    2024-06-14 00:38:03       11 阅读
  10. 【ZZULIOJ】1104: 求因子和(函数专题)

    2024-06-14 00:38:03       7 阅读