分类 其它 下的文章

liwen01 2024.09.16

前言

802.11 无线 WiFi 网有三类帧:
数据帧、管理帧、控制帧
。与有线网相比,无线 WiFi 网会复杂很多。大部分应用软件开发对 WiFi 的控制帧和管理帧了解得并不多,因为它们在物理层和数据链路层就已经被处理了,上层应用很少能感知到。

一般是在设备出现 WiFi 连接不上,或者是工作不稳定的情况下,才会去分析 WiFi 驱动工作情况以及抓 WiFi 空口包数据来分析。

有抓过 WiFi 空口包的同学应该清楚,无线 WiFi 网的网络包不管是从包类型还是包信息内容来看,都会比有线网的包复杂很多。

Image

这篇主要介绍有线网与无线 WiFi 网在物理层的差异、WiFi 介质访问控制原理和它的一些关键技术以及无线用户的接入过程。

WiFi 空口包抓包、分析,以及各类 WiFi 帧结构的介绍放到下一篇介绍。这篇是基础原理知识的介绍,也是后面一篇 WiFi 帧分析的基础。

(一) 有线与无线网物理层差异

Image

  • CSMA/CD 用于有线网络,通过检测和处理冲突来维持网络的稳定性。
  • CSMA/CA 用于无线网络,强调冲突的预防,以应对无线信道共享的挑战。

(1) 有线网 CSMA/CD

在有线网络中,设备间通过网线相互连接,它的工作原理是:

载波侦听
:设备在发送数据之前会先监听网络,以检测是否有其他设备在传输数据。如果检测到网络空闲,则开始发送数据。

冲突检测:
在数据发送过程中,设备持续监听网络。如果检测到冲突 (即两个或多个设备同时发送数据导致信号混合) ,发送数据的设备会停止传输,并发送一个“冲突信号”以通知网络上的其他设备。

重传数据:
发生冲突后,设备会等待一段随机的时间后再次尝试发送数据。这个随机等待时间称为“退避算法”,可以有效减少后续冲突的可能性。

这个就是有线网
CSMA/CD (Carrier Sense Multiple Access with Collision Detection,载波侦听多路访问与冲突检测)
的工作原理。

它依赖的是网线共享介质,
设备在数据发送的过程中,还可以检测网线状态

(2) 无线网 CSMA/CA

与有线网不同的是,无线网它是通过电磁波进行数据交互。
无线是半双工工作模式,无线客户端没有同时进行接收和发送的能力,无法检测到冲突。

所以有线网络中的冲突检测方式,在无线中并不适用,并且无线中还存在相邻站点不一定能侦听到对方的情况:

Image

手机位于两个路由器中间,并且距离两个路由器都比较远,这个时候路由器 AP1 检测不到路由器 AP2 的信号,两个路由器之间都不知道对方是否有在给手机 STA2 发送数据。

所以无线网引入了
CSMA/CA
( Carrier Sense Multiple Access with Collision Avoidance,载波侦听多路访问/冲突避免)

(3) CSMA/CA 工作原理:

载波侦听
:设备在发送数据之前也会监听无线信道,检查是否有其他设备在使用。只有信道空闲,设备才会继续执行发送操作。

冲突避免
:为了尽量避免冲突,在发送数据之前,设备可能会先发送一个“
准备发送
”信号 (如RTS,即请求发送) ,并等待接收设备返回“
允许发送
”信号 (如CTS,即清除发送) 。

数据发送
:收到 CTS 信号后,设备才会发送数据。

ACK确认
:数据发送成功后,接收设备会发送一个确认信号 (ACK) 。如果发送设备在规定时间内没有收到 ACK,它会认为数据丢失并重新发送。

由于无线网引入了额外的信号交换 (如 RTS/CTS) 和 ACK 等机制,增加了无线网设备连接、数据交互的复杂度。

(二) CSMA/CA 的关键技术

Image

(1) ACK 确认机制 (Acknowledgment Mechanism)

在数据帧成功发送并被接收设备接收到后,接收设备会发送一个
ACK (Acknowledgment)帧
给发送设备,表示数据成功到达。

如果发送设备在指定时间内未收到 ACK,它会认为数据丢失并重新发送。

Image

ACK 机制
保证了数据传输的可靠性,避免了数据丢失后无反馈导致的传输失败。

(2) RTS/CTS 机制 (Request to Send / Clear to Send)

RTS/CTS 机制
是 CSMA/CA 中用于减少冲突的关键技术。它通过引入“
准备发送
”请求 (RTS) 和“
允许发送
”确认 (CTS) 这两个控制帧,在发送数据之前确保信道空闲,减少隐蔽节点问题。

RTS(Request to Send)
:发送设备首先向接收设备发送一个 RTS 帧,表明自己想要发送数据。

CTS (Clear to Send)
:接收设备在确认信道空闲后,会回复一个 CTS 帧,允许发送设备进行数据传输。

(a) RTS/CTS与隐藏节点:

隐藏节点
指在接收者的通信范围内而在发送者通信范围外的节点。

Image

  • PC1 要发送数据,所以它发送了 RTS 帧, 这时路由器可以收到该 RTS 帧,但是 PC2 与 PC1 距离较远,PC2 收不到 PC1 的 RTS 帧。
  • 路由器 AP 收到PC1 的 RTS 帧后,会同时向 PC1 和 PC2 发送 CTS 帧
  • PC1 收到路由器发的 CTS 帧后,PC1 开始发送数据。
  • PC2 收到路由器发的 CTS 帧后,PC2 保持安静,不能发送数据

(b) RTS/CTS与暴露节点:

暴露节点
指在发送者的通信范围之内而在接收者通信范围之外的节点

Image

AP1 和 AP2 属于同一个信道,当 PC1 向 AP1 发送数据时,PC2 也希望向 AP2 发送数据。

根据 CSMA/CA 协议,PC2 侦听信道,它将听到 PC1 正在发送数据,于是错误地认为它此时不能向 AP2 发送数据,但实际上它的发送不会影响 AP1 的数据接收,这就导致 PC2 所谓暴露节点问题的出现。

Image

(c) 暴露节点解决方案

  • PC1 要发送数据,于是发送 RTS 帧,AP1 和 PC2 都可以接收到该 RTS帧
  • AP1 收到 RTS 帧后,会发送 CTS 帧
  • PC1 收到 CTS 帧后可以开始传输数据
  • PC2 如果也收到了 AP1 的 CTS 帧,PC2 不能与 AP2 发送数据,只能保持安静
  • PC2 如果只收到 PC1 的 RTS 帧,但是没有收到 AP1 发送的 CTS帧,这个时候 PC2 可以发送数据给 AP2,并且也不会影响到 AP1 数据的接收

通过 RTS/CTS 机制,避免了同时传输引发的冲突。

(3) 随机退避算法 (Random Backoff Algorithm)

当信道忙碌时,设备不会立即重新尝试发送数据,而是会等待一个随机的时间段后再尝试。这种随机等待时间由
退避算法
(Backoff Algorithm) 决定,以减少多个设备同时再次尝试发送数据的可能性,从而避免冲突。

具体做法是,在每次检测到信道忙碌后,设备会生成一个随机退避时间。退避时间越长,设备等待的时间越久,从而分散重试时间点,降低冲突概率。

Image

  • 发送数据前随机选取退避时间
  • 退避时间最短的站优先发送数据帧
  • 最短的时间为退避窗口
  • 新的退避时间 = 上次退避时间 - 退避窗口

(4) 帧间间隔 (Interframe Space, IFS)

IFS  (Interframe Space, 帧间间隔)
用于控制设备在发送数据帧之间的等待时间,以确保无线信道的公平性和有效性。根据不同的情况,IEEE 802.11 标准定义了几种不同类型的 IFS:

(a) 短帧间间隔 (Short Interframe Space, SIFS)

应用场景
:用于高优先级的操作,如 ACK 确认帧、CTS 帧、以及从站的响应帧。

特点
:SIFS 是所有 IFS 中最短的,确保重要数据能够迅速传输而不受其他帧的干扰。由于它的间隔短,接收方可以快速发出确认,减少等待时间,提高数据传输效率。

(b) 点协调功能帧间间隔 (Point Coordination Function Interframe Space, PIFS)

应用场景
:用于集中控制模式下,接入点 (AP) 在无竞争的情况下使用,如在 PCF (点协调功能) 模式下的优先级操作。

特点
:PIFS 的等待时间比 DIFS 短,但比 SIFS 长。它主要用于在竞争前启动通信,以便接入点在竞争阶段之前获得信道控制权。

(c) 分布式协调功能帧间间隔 (Distributed Coordination Function Interframe Space, DIFS)

应用场景
:用于普通数据帧的传输,通常在竞争环境中使用。

特点
:DIFS 是正常数据帧在竞争信道时使用的间隔。它的等待时间比 PIFS 长,确保优先级较低的设备在优先级较高的操作完成后再尝试发送数据。

(d) 扩展帧间间隔 (Extended Interframe Space, EIFS)

应用场景
:当一个设备接收到一个有错误的数据帧时,它会等待 EIFS 时间后再试图发送数据。

特点
:EIFS 是所有 IFS 中最长的,旨在避免网络中更多的冲突或干扰发生。当设备认为信道状况不佳时,会使用更长的等待时间以减少进一步的冲突。

(e)应用实例

Image

  • 发送者想要发送数据,先侦听信道状态,如果空闲,它就会发送一个 RTS 帧(请求发送帧)
  • 接收者接收到该 RTS 帧之后,等待一个 SIFS (短帧间间隔)时间后,再发送一个 CTS 帧(可以发送数据了)
  • 发送者收到 CTS 帧之后,间隔一个 SIFS (短帧间间隔)时间后,发送数据帧。
  • 接受者接收到数据后,再间隔一个 SIFS 后应答一个 ACK 帧
  • 最下面一行的 NAV 是其它设备的一个时间记时器,当设备的 NAV 计时器大于零时,设备会认为信道正在被其他设备占用,因此不会尝试进行传输,被进制访问介质。
  • ACK 之后有个 DIFS (分布式协调功能帧间间隔),这是个竞争窗口,容许优先级低的设备再尝试发送数据。

通过不同的 IFS 间隔时间,CSMA/CA 可以控制不同类型的数据帧在信道上的优先级。高优先级的数据帧 (如 ACK 和 CTS) 使用较短的间隔,确保它们能快速获得信道使用权。

通过强制设备在每次传输前等待一定时间,IFS 降低了多个设备同时传输的可能性,减少了冲突的发生。

(5) 载波侦听 (Carrier Sensing)

(a) 物理载波侦听与虚拟载波侦听

载波侦听
在设备发送数据之前,它会先侦听无线信道,检查是否有其他设备在使用该信道。如果信道空闲,设备才会继续后续的发送操作。这一过程被称为
物理载波侦听

虚拟载波侦听
是通过网络分配器 (如无线接入点) 来管理信道的占用情况,利用控制帧 (如 RTS/CTS)来减少冲突。

(b) 网络分配向量(Network Allocation Vector,NAV)

NAV 是无线设备在信道上听到某些控制帧 (如 RTS/CTS 或数据帧) 时设置的一个计时器。这个计时器表示该设备预计信道会被占用的时间,设备会在 NAV 计时器归零之前避免传输数据。

(c) NAV 的工作原理

NAV 设置:
当一个设备在无线信道上接收到某个帧 (如 RTS 或 CTS) 时,它会读取该帧中的持续时间字段 (Duration Field) 。这个字段表示该帧预期的占用时间,包括发送数据帧和接收 ACK 所需的时间。接收设备会根据这个持续时间设置自己的 NAV 计时器。

信道占用判断:
当设备的 NAV 计时器大于零时,设备会认为信道正在被其他设备占用,因此不会尝试进行传输。NAV 计时器归零后,设备会再次检查信道是否空闲,如果空闲,则可以开始自己的传输。

NAV 与物理载波侦听的结合
:NAV 是一种
虚拟载波侦听
机制,它与
物理载波侦听
相结合。

物理载波侦听是通过硬件直接检测信道上的信号强度,来判断信道是否被占用。

NAV 通过读取帧信息来推断信道的占用状态。物理载波侦听与 NAV 结合使用,有助于更准确地判断信道状态,减少冲突。

Image

PC1 给路由器发送数据帧,同一个范围的PC2、PC3、phone 也会收到该数据帧帧。

PC1 发送的数据帧在该帧的 MAC 头部有个 Duration 字段,表示持续时间,它包括一个短帧间间隔(SIFS)时间加上 ACK 帧的时间,假设该时间是50us。

PC2、PC3、phone 在收到 frame 数据帧后,会去读取该帧的 Duration 字段,然后重置自己的
NAV
计数器为 Duration 字段的值 50 us。

(三)无线用户接入过程

我们手机、笔记本电脑等设备的 WiFi 模块一般工作在 STA 模式,路由器的 WiFi 一般工作在 AP 模式。当我们 STA 设备连接到路由器 AP 的时候,有三个过程:
扫描、认证、关联

Image

  • 扫描阶段
    STA进行扫描,定期搜索周围的无线网络,获取到周围的无线网络信息。

  • 认证阶段
    STA 接入 WiFi 网络前需要进行终端身份验证,即链路认证。链路认证通常被认为是终端连接 AP 并访问 WiFi 的起点。

  • 关联阶段
    完成链路认证后,STA 会继续发起链路服务协商。

(1) 扫描阶段 (Scanning)

扫描阶段 (Scanning)
是 WiFi 设备连接无线网络的第一步,设备通过扫描找到可用的无线接入点 (Access Point, AP) ,从而选择合适的 AP 进行连接。WiFi 标准中定义了两种主要的扫描方式:主动扫描 (Active Scanning) 和被动扫描 (Passive Scanning) 。每种方式有其特点和应用场景。

(a) 主动扫描 (Active Scanning)

在主动扫描过程中,设备会主动向周围的无线信道发送探测请求 (Probe Request) ,并等待 AP 发送探测响应 (Probe Response) 。这个过程可以快速发现周围的 WiFi 网络,尤其是在设备不知道有哪些网络可用的情况下。主动扫描的过程分为以下几个步骤:

探测请求 (Probe Request)
设备依次切换到不同的信道 ( WiFi 网络使用多个信道进行通信,不同的标准使用不同的信道范围,比如 2.4GHz 频段有 14 个信道) 。

在每个信道上,设备会发送探测请求帧 (Probe Request) 。这个探测请求可以是针对所有网络的广播,也可以是针对特定 SSID 的单播。

Image

针对某个特定 SSID 的单播,探测包中有携带需要探测的 SSID 信息,只有 SSID 能够匹配上的 AP 才会返回探测响应包。

这种场景一般是设备已经配置过网络,设备端有保存需要连接的 AP ,设备上电就直接扫描该 AP 是否存在。

Image

当探测请求是针对所有网络的广播时,探测包中 SSID 信息是为 NULL,接收到该探测包的 AP 都会返回探测响应包。

比较常见的场景是我们要手动去连接 WiFi 时,先会去扫描所有的信道的 WiFi 热点,然后生成一个 WiFi 热点列表。

探测请求的主要内容包括:

  • SSID (可以是具体的SSID,也可以是广播请求)
  • 支持的速率
  • 扩展功能信息 (如支持的安全协议)

探测响应 (Probe Response)

AP在接收到探测请求后,会返回探测响应帧 (Probe Response) 。这个响应帧包含AP的详细信息,例如:

  • SSID (网络名称)
  • BSSID (AP的MAC地址)
  • 信道号
  • 支持的速率
  • 安全协议信息 (如WPA/WPA2)
  • 网络容量和设备数量
  • 其他可能的扩展功能 (如QoS、WMM等)

主动扫描的优缺点:

优点
:能够快速发现隐藏的 WiFi 网络 (隐藏SSID的网络) ,因为设备可以通过探测请求主动询问某个特定SSID的存在。

缺点
:主动发送请求帧会增加设备的能耗,且在某些环境中可能暴露设备的存在和意图,减少隐私性。

(b) 被动扫描 (Passive Scanning)

在被动扫描过程中,设备不会主动发送探测请求,而是通过监听特定信道上的信标帧 (Beacon Frame),从中获取 AP 的信息。信标帧是 AP 定期广播的一种特殊的管理帧,所有设备只需监听信道即可获知周围可用的网络。

Image

信标帧 (Beacon Frame)

AP 会定期 (通常是100ms)在指定信道上广播信标帧。信标帧包含了AP的关键信息,包括:

  • SSID
  • BSSID (AP的MAC地址)
  • 支持的传输速率
  • 信道号
  • 安全信息 (如WPA/WPA2)
  • 网络时间戳 (用于同步设备的时钟)
  • 其他可能的功能 (如WMM、HT Capabilities、VHT Capabilities等)

监听信道

  • 设备依次切换到不同的信道,并在每个信道上监听信标帧。每个信道可能包含多个 AP 的信标帧。
  • 通过这些信标帧,设备可以了解哪些 AP 在附近可用,并获取相关的配置信息。

被动扫描的优缺点:

优点
:更节能,因为设备只需被动监听信标帧,而不需要主动发送请求。它也不会暴露设备的身份和意图,增强了隐私性。

缺点
:相比主动扫描,发现 AP 的速度较慢,因为设备必须等待 AP 广播信标帧。尤其在密集的网络环境中,等待多个 AP 广播信标帧可能会耗费更多时间。

(2) 认证阶段 (Authentication)

认证是设备和 AP 之间相互确认身份的过程。在传统的 WiFi 认证中,有两种方式:

开放系统认证 (Open System Authentication)
:这是最简单的方式,不需要设备和AP之间进行任何密钥交换,所有请求都会通过。

共享密钥认证 (Shared Key Authentication)
:设备和 AP 会通过 WEP (Wired Equivalent Privacy) 密钥进行加密认证。这种方式现已很少使用,因为 WEP 的安全性较差,已被更强的 WPA/WPA2/WPA 3等认证方式取代。

Image

现代 WiFi 网络一般使用 WPA/WPA2/WPA3 等协议进行身份验证,结合了 PSK (Pre-Shared Key) 或企业级的 RADIUS 认证服务器来提升安全性。

WiFi 认证就简单介绍这些,后面会专门再针对 WiFi 认证做详细介绍。

(3) 关联阶段

在完成扫描和认证阶段之后,设备必须通过关联阶段,才能与无线接入点 (Access Point, AP) 建立正式的连接,并开始进行数据通信。在这个阶段,设备与 AP 之间会进行详细的参数交换,确保双方能够兼容并高效地进行后续通信。

Image

关联阶段包括
关联请求 (Association Request)

关联响应 (Association Response)
两部分:

(a) 关联请求 (Association Request)

在认证通过后, WiFi 设备会发送一个关联请求帧 (Association Request),请求与 AP 建立正式的连接。这个关联请求帧携带了设备的详细信息,以便 AP 了解设备的能力,并确定双方能否以最佳方式进行通信。

关联请求帧的主要字段包括:

  • SSID
    :所请求连接的网络名称。
  • 支持的速率集 (Supported Rates Set)
    :设备所支持的传输速率,AP可以根据这些速率来选择合适的通信速率。
  • 扩展速率集 (Extended Supported Rates)
    :用于支持更高的传输速率 (例如802.11n/ac/ax)。
  • 信道信息
    :设备希望使用的信道以及相关的频段信息。
  • 安全信息
    :包括设备支持的加密和认证协议 (如WPA2/WPA3等)。
  • 能力信息 (Capability Information)
    :设备的其他功能,如是否支持QoS (Quality of Service)、电源管理等。
  • HT/VHT Capabilities
    :如果设备支持802.11n (HT, High Throughput) 或802.11ac/ax (VHT, Very High Throughput),它会在关联请求中包含这些信息,以允许AP使用更高效的技术来优化传输性能 (如 MIMO、多信道聚合等)。

(b) 关联响应 (Association Response)

AP 接收到设备的关联请求帧后,会检查设备的请求信息,评估其是否能够接受设备的连接。通常,AP 会基于设备的能力和当前网络的负载情况作出决定。然后,AP 会通过
关联响应帧 (Association Response)
来通知设备是否成功关联。

关联响应帧包括以下重要信息:

  • 状态码 (Status Code)
    :指示关联是否成功。如果状态码为0,表示关联成功;否则,设备需要重新尝试。
  • 关联标识符 (Association ID, AID)
    每个成功关联的设备会被 AP 分配一个唯一的 AID,用于区分不同的设备。在后续通信中,AP 使用 AID 来管理每个设备。
  • 支持的速率信息
    :AP 会在响应中确认双方支持的最高传输速率,这些速率会成为后续通信中的基准。
  • 可能的信道信息
    :如果 AP 支持多信道传输或设备的请求中包含特定信道要求,AP 会在响应中确认所选择的信道。

(c) 关联成功后的状态

一旦设备收到关联成功的响应帧,它就会正式成为 AP 的一部分,并可以开始通过AP 访问网络资源。关联成功后的状态有以下几个关键点:

  • 设备获得AID
    :设备的 AID 可以用于 AP 管理关联设备的无线资源,例如通过AID 来识别哪些设备可以发送或接收数据。
  • 资源分配
    :AP 可能根据网络条件和设备的能力,分配特定的无线资源 (如信道带宽、时隙等)。
  • 准备数据传输
    :关联完成后,设备与 AP 之间的通信变为数据帧传输,设备可以通过 AP 接入互联网或本地网络资源。

(d) 关联失败的情况

关联过程并非总是成功的,可能的失败原因包括:

  • AP 超载
    :如果 AP 已经连接了过多的设备,它可能会拒绝新的关联请求,通常会返回一个“超载”状态码。
  • 信道不匹配
    :设备请求使用的信道可能不在 AP 当前支持的信道范围内。
  • 安全协议不兼容
    :如果设备和 AP 使用不同的安全协议 (例如设备只支持 WPA, 而 AP 要求 WPA2 或 WPA3),关联将会失败。
  • 信号质量差
    :如果设备距离 AP 太远,信号质量过差,AP 可能无法维持稳定的连接,从而拒绝关联请求。

结尾

上面主要介绍了有线网与 WiFi 无线网在物理层的差异、WiFi 介质访问控制原理及其关键技术、无线用户的接入过程。

下一篇将介绍 WiFi 空口包抓包、以及各类 WiFi 包的解析。

上面内容,如有错误,欢迎评论区提示指出,不胜感激。

------------------End------------------
如需获取更多内容
请关注 liwen01 公众号

代码整洁之道

image-20240904225436374

简介:

本书是编程大师“Bob 大叔”40余年编程生涯的心得体会的总结,讲解要成为真正专业的程序员需要具备什么样的态度,需要遵循什么样的原则,需要采取什么样的行动。作者以自己以及身边的同事走过的弯路、犯过的错误为例,意在为后来者引路,助其职业生涯迈上更高台阶。

本书适合所有程序员阅读,也可供所有想成为具备职业素养的职场人士参考。

第十二章 协作

image-20240918072128072

大多数软件都是由团队开发出来的。当团队成员能够十分专业地互相协作时,整个团队是最为高效的。单打独斗与游离于团队之外都是不专业的表现。

12.1 程序员与人

我们并非是因为喜欢和其他人在一起工作才选择做程序员的。我们都认为人际关系难以应付而且毫无规律。编程用的机器则整洁,行为也可预见。
如果可以一个人待在房间里数个小时沉浸在一些真正有趣的问题上,那将会是最开心的时光。

好吧,我这么说可能有点儿以偏概全了,确实也有不少例外。有许多程序员很善于和别人共事合作,享受其中的挑战。但是整个群体的平均状况还是朝我所描述的方向发展的。
我们,程序员们,还是最享受面无表情的沉思,把自己像蚕茧一样裹起来,沉浸于问题思考中。

程序员与雇主:

专业程序员的首要职责是满足雇主的需求。这意味着要和你的经理们、业务分析师们、测试工程师们和其他团队成员很好地协作,深刻理解业务目标。这并不是说你必须要成为业务方面的老学究,而是说你需要理解手上正在编写的代码的业务价值是什么,了解雇你的企业将如何从你的工作中获得回报。

因此,专业程序员会花时间去理解业务。他们会和用户讨论他们正在使用的软件,会和销售人员与市场人员讨论所遭遇的问题,会和经理们沟通,明确团队的短期目标和长期目标。

程序员与程序员:

程序员与程序员之间通常很难密切合作,这就会带来一些不小的问题。

  1. 代码个体所有

    不正常的团队最糟糕的症状是,每个程序员在自己的代码周边筑起一道高墙,拒绝让其他程序员接触到这些代码。我曾在许多地方看到过,不少程序员甚至不愿让其他程序员看见他们的代码。这是引发灾难的“最有效秘诀”。

  2. 协作性的代码共有权

    我赞同这种做法:团队中每位成员都能签出任何模块的代码,做出任何他们认为合适的修改。我期望拥有代码的是整个团队,而非个人。专业开发人员是不会阻止别人修改代码的。他们不会在代码上构造所有权的藩篱,而是尽可能多地互相合作。他们通过合作来达到学习的目的。

  3. 结对

    “专业人士会结对工作。”为什么?因为至少对有些问题而言,结对是最有效的解决方法。

    专业人士结对工作,还因为这是分享知识的最好途径。

    专业人士之所以结对,是因为结对是复查代码最好的方式。(
    最有效率且最有效果的代码复查方法,就是以互相协作的方式完成代码编写。

12.2 一起协作

专业人士会共同工作。当戴着耳机坐在角落里时,你是无法参与合作的。因此,我期望大家能够围坐在一张桌子前,彼此面对面。你要能够感受到其他人的恐惧担忧,要能够听到其他人工作不顺时的牢骚,要有口头上和肢体语言上的下意识的沟通交流。整个团队要像一个统一的整体,彼此连通。

也许你认为自己一个人工作时会做得更好。也许确实如此,但这并不意味着你一个人工作时,整个团队会做得更好。况且,事实上,一个人单独工作时,不太可能会工作得更好。有些时候,单独工作是正确的。当你只是需要长时间努力思考一个问题时,可以一个人单独工作。当任务琐碎且无足轻重、和另外一个人一起工作显得浪费时,可以一个人工作。但是一般说来,和其他人紧密协作、在大部分时间段中结对工作,是最好的做法。

12.3 结论

也许我们不是因为通过编程可以和人互相协作才选择从事这项工作的。但真不走运,编程就意味着与人协作。我们需要和业务人员一起工作,我们之间也需要互相合作。

常见的多智能体框架有几类,有智能体相互沟通配合一起完成任务的例如ChatDev,CAMEL等协作模式, 还有就是一个智能体负责一类任务,通过选择最合适的智能体来完成任务的路由模式,当然还有一些多智能体共享记忆层的复杂交互模式,这一章我们针对智能体路由,也就是选择最合适的智能体来完成任务这个角度看看有哪些方案。

上一章我们讨论的何时使用RAG的决策问题,把范围放大,把RAG作为一个智能体,基座LLM作为另一个智能体,其实RAG决策问题也是多智能体路由问题的一个缩影。那实际应用场景中还有哪些类型的智能体路由呢?

  • 不同角色的智能体,例如看到最搞笑的是不同流派的算命机器人
  • 不同工具挂载的智能体,例如接入不同知识库,拥有不同领域工具
  • 不同思考方式的智能体,例如COT思考,有Step-back思考,有outline思考
  • 不同工作流的智能体,例如例如不使用RAG,使用单步RAG,多步RAG的智能体路由
  • 把以上融合,也就是不同角色,工具,思考方式,工作流的综合智能体路由

而这里我们看两种外挂策略,也就是可以直接在当前已有多智能体外层进行路由的方案。

基于能力和领域的智能体路由

MARS其实是一篇大模型出现前的文章,但是却可以作为多Agent路由的基础文章之一,它主要针对当
不同领域(能力)的智能体选择
。思路非常清晰。论文先定义了多智能体选择问题,该问题的组成元素包括

  • query: 用户提问
  • agent skill:对于智能体能力的描述,也可以是sample queries
  • agent response:智能体对用户提问的回答

那自然就有两种智能体选择的方案,
一个是直接基于query进行选择(Query-Pairing),一个是基于智能体response进行选择(Response-pairing)
,当前的多智能体决策也就是这两个大方向,前者更快但精度有限,后者更慢但效果更好。下面说下方案中的细节,因为实际操作时你会发现两个方案都有难点。

image

Question pairing

基于query进行判断的问题在于
如何描述agent能干啥
,论文指出智能体的能力边界不好界定,更难描述。

论文给出的一个方案是使用
query sample
,虽然不知道模型的全局能力,但是基于用户历史的使用情况,可以知道模型能回答哪些query,例如"locate me some good places in Kentucky that serve sushi"这个问题,"Alexa", "Google"可以回答这个问题。那就可以基于历史收集的query样本训练一个
多标签分类模型,预测每个query哪些智能体可以回答
。其实这种方案也是使用了response,只不过使用的是历史agent回答。

除了query分类,论文还用了相似度。论文收集了agent在公开网站上的能力描述,例如"Our productivity bot helps you stay productive and organized. From sleep timers and alarms to reminders, calendar management, and email ....".然后使用agent描述和query的文本相似度排序作为agent能否回答该问题的判断。这里论文尝试了bm25,USE,还有微调Roberta等方式进行向量编码。之前我们也考虑过类似KNN的方案,但这种方案有个问题在于文本相似可以衡量领域差异,例如数学Agent,金融Agent,但是无法区分任务复杂程度,所以不适用于领域之外的其他agent路由场景。

Response Pairing

使用在线模型回答来进行路由的核心难点其实就是如何判断response质量,论文指出的是前文多通过response和query的相似度来判断,这是不够的,还要判断准确性,因此论文采用了cross-encoder训练了query-response ranking模型。不过在大模型出来后的这两年,对于response回答质量有了更全面的评价标准,例如OpenAI的3H(Helful, Harmless,Honesty),DeepMind更关注的2H(helpful, harmless),也有了更多的Reward和Judement模型的训练方案,感兴趣的同学可以去看
好对齐RLHF-OpenAI·DeepMind·Anthropic对比分析

这里就不细说论文的方案了,直接来看下效果吧。论文在22年当时的四大Agent(Aleax,Google,houndify,Adasa)上评估,基于Response排序的方案最好,不过使用Query Sample分类的方案效果也不差。

image

基于问题复杂程度的智能体路由

  • Adaptive-RAG: Learning to Adapt Retrieval-Augmented Large Language Models through Question Complexity

前面的MARS更多是从领域层面对智能体进行划分,例如bank agent,weather agent,transport agent,但是RAG问题上,领域差异更多只影响数据库路由,也就是使用哪些召回,查什么数据。还有一个更重要的差异,来自问题的复杂度。类似的方案有SELF-RAG,不过它是把路由融合在了模型推理的过程中,整体复杂度太高,可用性就有些低了。所以我们看下Adaptive-RAG的外挂路由的方案。

Adaptive-RAG提出了通过分类器,对query复杂程度进行分类,并基于分类结果分别选择LLM直接回答,简单单步RAG,或者复杂多步RAG(论文选择了Interleaving-COT),如下图
image

那如何判断一个query的复杂程度呢,这里其实和前面MARS提出的query pairing中的query多标签分类模型的思路是相似的。也是使用同一个query,3种模式的回答结果的优劣作为标签来训练分类模型,当然也可以是listwise排序模型。论文使用的是有标准答案的QA数据集,因此多模型回答的结果判断起来会比较简单,这里3种回答方式也有优先级,那就是更简单的链路能回答正确的话,默认标签是最简单的方案。这里的query分类器,论文训练了T5-Large,样本只有400条query,以及每个问题对应在3种链路上的回答结果。

而在现实场景中RAG样本的反馈收集要复杂的多,需要先基于标注样本训练Reward模型,得到对回答质量的评分,再使用Reward模型对多个链路的回答进行打分从而得到分类标签。

如果你的RAG链路选择更多,优先级排序更加复杂的话,不妨使用多标签模型,得到多个候选agent,再基于多个agent之间的优先级选择复杂程度最低,或者在该任务上优先级最高的Agent进行回答。

效果论文分别在single-step和multi-hopQA数据集上进行验证,Adaptive都能在保证更优效果的同时,使用更少的时间和步骤完成任务(Oracle是当分类器完全正确时的效果比较天花板)

image

基于用户偏好的智能体路由

  • Zooter:Routing to the Expert: Efficient Reward-guided Ensemble of Large
    Language Models

第三篇论文是从用户回答偏好出发,选择最合适的agent,其实也是最优的基座模型。基座模型Ensemble和Routing也算是智能体路由中的一个独立的方向,包括的大模型小模型路由以求用更少的成本更快的速度来平衡效果,也有多个同等能能力的模型路由来互相取长补短。个人认为基座模型的路由比不同领域的Agent,或者rag要复杂一些,因为基座模型间的差异在文本表征上更加分散,抽象难以进行归类和划分。这差异可能来自预训练的数据分布差异,指令数据集的风格差异,或者rlhf的标注规则差异等等~

正是因为难以区分,所以基座模型路由要是想使用query-pairing达到可以和response-pairing相近的效果和泛化性,需要更多,更丰富的训练数据。Zooter给出的就是蒸馏方案,也就是训练reward模型对多模型的回答进行评分,然后把模型评分作为标签来训练query路由模型。如下

image

蒸馏部分,论文借鉴了蒸馏损失函数,为了从reward模型中保留更多的信息,这里没有把多模型的reward打分最后转化成top-answer的多分类问题,而是把reward打分进行了归一化,直接使用KL-divergence让模型去拟合多个模型回答之间的相对优劣。同时考虑到reward-model本身的噪声问题,论文在蒸馏时也使用了label-smoothing的方案来降低噪声,提高模型回答置信度。其实也可以使用多模型reward打分的熵值来进行样本筛选。

奖励函数,论文使用QwenRM作为reward模型,混合多数据集构建了47,986条query样本,对mdeberta-v3-base进行了蒸馏训练。

效果上,论文对比了6个单基座模型,使用蒸馏后的模型进行query路由(ours),以及使用不同Reward模型对response进行路由,还有SOTA GPT3.5和GPT4

  • 不同Reward模型的效果差异较大,在当前评估的4个任务集上,Qwen和Ultra的效果要显著更好
  • 论文蒸馏的方式训练的Zooter模型在query路由的效果上可以基本比肩使用RM进行response路由,使用1/6的推理成本就能做到相似的效果有相似的推理效果

image

更多智能体路由相关方案

更多RAG路由,智能体路由,基座模型路由Ensemble的论文,大家感兴趣的可以自己去看

  • 智能体路由
    • One Agent To Rule Them All: Towards Multi-agent Conversational AI
    • A Multi-Agent Conversational Recommender System
  • 基座模型路由&Ensemble
    • Large Language Model Routing with Benchmark Datasets
    • LLM-BL E N D E R: Ensembling Large Language Models with Pairwise Ranking and Generative Fusion
    • RouteLLM: Learning to Route LLMs with Preference Data
    • More Agents Is All You Need
    • Routing to the Expert: Efficient Reward-guided Ensemble of Large Language Models
  • 动态RAG(When to Search & Search Plan)
    • SELF-RAG: LEARNING TO RETRIEVE, GENERATE, AND CRITIQUE THROUGH SELF-REFLECTION ⭐
    • Self-Knowledge Guided Retrieval Augmentation for Large Language Models
    • Self-DC: When to retrieve and When to generate Self Divide-and-Conquer for Compositional Unknown Questions
    • Small Models, Big Insights: Leveraging Slim Proxy Models To Decide When and What to Retrieve for LLMs
    • Adaptive-RAG: Learning to Adapt Retrieval-Augmented Large Language Models through Question Complexity
    • REAPER: Reasoning based Retrieval Planning for Complex RAG Systems
    • When to Retrieve: Teaching LLMs to Utilize Information Retrieval Effectively
    • PlanRAG: A Plan-then-Retrieval Augmented Generation for Generative Large Language Models as Decision Makers

想看更全的大模型相关论文梳理·微调及预训练数据和框架·AIGC应用,移步Github >>
DecryPrompt

小李移动开发成长记 —— 大话小程序


传统网站
前端开发的同学初次接触
小程序
,会有许多
困惑
:为什么没有div,view 是什么、怎么没有 ajax,wx.request 为什么是回调方式、预览怎么要用小程序开发者工具、APPID有什么用、安装npm包怎么还要构建、tabBar 是什么、语法怎么和vue很像但是有的部分又不同、@import 用法怎么和 css 中不同...

本篇通过微信小程序(发布较早,影响力较大)来介绍小程序,帮助你
快速认识小程序
,并解决以上困惑。主要介绍:

  1. 小程序和
    网站
    的差异
  2. 小程序和
    vue
    语法差异
  3. 小程序通信模型和
    运行机制
  4. 三方小程序
    开发流程
  5. 如何新建项目,全局和局部配置是什么
  6. 小程序
    基本语法
    :wxsl、wxss、js(wxs)
  7. 小程序
    API的Promise化
  8. 应用
    生命周期
    和页面生命周期
  9. 小程序组件和自定义组件
  10. 小程序
    路由和状态
    管理
  11. 分包是什么,有什么作用

背景

小李虽然会一些react、vue、js,但是移动端开发做的比较少,几乎
不会小程序
开发。

下一阶段的任务是
移动端开发
,涉及H5、小程序、公司内部自建的移动端的框架、调试工具、TS等。

如何快速上手这部分工作,经过三分钟的思考,决定出大致要加强的方向:

  • 小程序
    :有许多小程序的开发
  • js 基础
    :代码要写的优雅,比如许多 if 不是一个好习惯;不要看到别人的 Promise 就感觉生疏,别人的解构写法是否有问题也能看出来
  • react
    :项目技术栈用到 react
  • 加强TS
    :许多代码有TS,否则看不懂,修改代码也不能通过编译
  • 移动端开发流程
    :熟悉公司移动开发流程,比如模拟器、云真机、实体机、公司的抓包工具
  • 移动端玩法
    :比如调用 jsAPI(getLocation)需要同时开启“系统地理位置、系统授权宿主(支付宝/小程序)地理位置权限、宿主给小程序地理位置权限”,否则可能会弹出”地理位置权限申请“的弹框;悬浮球的玩法;半屏展示;全屏展示页面(即webview 扩展到状态栏)

认识小程序

小程序 VS 网站

小程序类似网站
,只是网站在浏览器中打开,而小程序通过小程序平台(微信、支付宝)打开。

两者
相似点

  • 跨平台:微信小程序在微信中打开,支付宝小程序在支付宝中打开;网站在浏览器中打开
  • 无需下载和安装
  • 实时更新:发布后,用户就会自动获取最新版本

两者
不同点

  • 运行环境:小程序的宿主是小程序平台,例如微信、支付宝;网站运行在浏览器中;
  • 功能和权限:小程序因为嵌在超级应用内(微信、支付宝),可以调用一些原生功能,比如支付、地理位置、摄像头,部分功能需要用户授权;网站受限于浏览器提供的API,总体上授权会更有限;
  • 体验和性能:小程序在体验和性能上更接近原生应用,响应更快、UI更流畅
  • 生态与流量:借助超级应用的平台生态,容易获取用户。比如微信小程序可以通过微信群、朋友圈等社交渠道快速传播;网站通过搜索引擎优化(SEO)、广告,相对小程序,推广难度可能大一些
  • 开发语言和工具:使用特定的框架和工具集,例如微信小程序使用WXML、WXSS、JS;网站使用标准的HTML、CSS、JS以及各种前端框架和库(React、Vue)
  • 开发模式:网站是浏览器+代码编辑器;微信小程序:申请小程序开发账号、安装小程序开发者工具、创建和配置小程序项目
  • 代码托管:网站本地开发完提交到代码托管平台(github),之后会从托管平台部署到实际运行的服务器上;小程序代码在本地编写和调试后,直接上传到对应的小程序平台,这里涉及使用官方提供的开发者工具上传代码,平台审核,审核后发布;小程序平台负责代码的托管和版本控制。

微信小程序 VS Vue

有人说小程序比 Vue 简单多了。我们来对比两者异同,会发现
小程序在语法上有许多和vue相似

相同点

  • 组件开发:微信小程序使用 wxml定义组件结构、wxss 定义样式、js 定义逻辑、json 用于配置;Vue 使用 .vue 进行单文件组件开发
  • 数据绑定:微信小程序通过
    {{}}
    进行数据绑定,类似 vue.js 模板语法
  • 事件处理:微信小程序使用 bingdtap 或 catchtap 等事件绑定;vue 使用 v-on(或@) 进行事件绑定
  • 条件渲染和列表渲染:小程序使用 wx:if 和 wx:for 指令;vue 使用 v-if 和 v-for 指令

不同点

  • 运行环境:微信小程序运行在微信的容器环境中,只能在微信中使用,依赖于微信的API和平台;vue 运行在浏览器和node.js中。
  • 文件结构:微信小程序,每个组件由4个文件组成(wxml, wxss, js, json);vue 在一个 .vue 文件中
  • 样式处理:微信小程序 使用wxss 进行定义,类似CSS;vue 使用标准的CSS
  • 框架特征:微信小程序:提供了一些特定微信环境的API,例如访问微信支付;Vue专注于UI层,提供了丰富的生态系统
  • 生态系统和扩展:微信小程序,由微信官方提供丰富的API,社区贡献组件库和开发工具;Vue 有强大的生态系统,包括大量的第三方插件、组件库和开发工具

宿主环境

手机
微信
是微信小程序的宿主环境,
支付宝
是支付宝小程序的宿主环境

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

通过宿主环境,小程序提供的能力包含:
通信模型

运行机制

组件

API

通信模型

小程序通信主体包含:
渲染层

逻辑层

  • wxml(类似 html) 和 wxss(类似 css) 工作在渲染层
  • js脚本工作在逻辑层

小程序中的通信模型分两部分:

  • 渲染层和逻辑层通信:由微信客户端进行转发
  • 逻辑层和第三方服务器之间的通信:由微信客户端进行转发
运行机制

小程序
启动过程

  1. 小程序代码下载到本地
    :用户首次打开或更新小程序时,微信客户端会从远程服务器下载小程序的代码包。这个过程会根据网络状况和代码包大小有所不同,微信平台会对代码包进行一定的优化和压缩处理以加快下载速度。
  2. 解析 app.json 全局配置文件
    :下载完成后,微信客户端会首先解析app.json文件,这个文件包含了小程序的全局配置信息,如页面路径、窗口表现、网络超时时间、底部tab等。这些配置决定了小程序的基本框架和表现形式。
  3. 执行 app.js
    ,小程序入口文件:接下来,微信客户端会执行app.js文件,这是小程序的逻辑层入口。在app.js中,会调用App函数来创建小程序实例,并可以在这个函数中定义全局的数据和方法,进行一些初始化操作,如注册全局的生命周期回调函数(如onLaunch, onShow, onHide等)。
  4. 渲染小程序首页
    :根据app.json中配置的首页路径,微信客户端会加载首页的.wxml(结构)、.wxss(样式)和.js(逻辑)文件,开始渲染小程序的首页。逻辑层会通过Page函数创建页面实例,并执行页面的生命周期函数,如onLoad,进行数据初始化、网络请求等操作。随后,渲染层依据逻辑层提供的数据渲染页面内容。
  5. 小程序启动完成
    :当首页页面渲染完成并呈现给用户时,标志着小程序的启动过程结束,此时用户可以开始与小程序进行交互。同时,小程序的不同页面之间可以通过页面路由进行跳转,逻辑层与渲染层继续根据用户的操作进行数据更新和界面重绘。

Tip
:上面是冷启动,对于已经打开过的小程序,再次进入可能就会有热启动的情况。比如代码下载可能就会被跳过

页面渲染过程

  1. 加载解析页面的 .json 配置文件
    :当需要渲染某个页面时,微信小程序框架首先会加载该页面对应的.json配置文件。这个文件定义了页面的窗口样式、导航栏样式等页面级的配置信息,这些配置会影响页面的外观和行为。
  2. 加载页面的 .wxml 和 .wxss 样式
    :紧接着,框架会加载页面的结构文件.wxml和样式文件.wxss。.wxml文件定义了页面的结构和布局,类似于HTML;而.wxss文件则是用来控制页面元素样式的,类似于CSS。这两个文件共同决定了页面的外观。
  3. 执行页面的 .js 文件
    ,调用 Page() 创建页面实例:之后,框架会执行页面的逻辑文件.js。在这个文件中,通过调用Page函数来创建页面实例,并可以在其中定义页面的初始数据、生命周期函数(如onLoad、onShow、onHide等)、事件处理函数等。页面的初始化数据和逻辑处理都在这个阶段完成。
  4. 页面渲染完成
    :当页面的结构、样式和数据都准备就绪后,微信小程序的渲染引擎会根据.wxml和.wxss以及页面实例中的数据来渲染页面。这个过程包括解析WXML模板,应用WXSS样式,绑定数据到模板,最终生成用户可见的界面。页面渲染完成后,用户就可以看到并开始与这个页面进行交互了。
API

小程序官方把API分三类:

  • 事件监听API
    :以 on 开头,监听某些事件。例如 wx.onWindowResize(callback)(小程序中没有windown) 监听窗口尺寸变化
  • 同步API
    :以Sync结尾的都是同步API,通过函数直接获取,如果执行出错会抛出异常。例如wx.setStorageSync('key', 'value') 向本地缓存写入数据
  • 异步API:类似$.ajax,需要通过 success、fail、cpmplete 接收调用的结果,例如 wx.request 发起网络数据请求,通过 success 接收调用的结果

小程序研发流程

小程序开发流程
不同于传统网站

传统网站开发:vscode编写代码,浏览器预览效果,git提交到代码。

小程序开发步骤大致如下(以微信小程序
三方
开发为例):

  1. 申请小程序账号,获得 AppId(你要创建的小程序唯一标识)
  2. 通过小程序开发者工具创建项目
  3. 通过小程序开发者工具编译预览效果
  4. 通过小程序开发者工具把代码上传到微信平台
  5. 选择一个开发版本作为体验版
  6. 体验完成申请发布
  7. 发布到微信平台

Tip
:一方开发通常指的是由小程序的所有者的开发,也是官方开发; 三方开发,是指由第三方开发者为小程序提供功能或服务;

小程序账号和APPID

注册小程序账号,主要是为了获得APPID。

APPID
是小程序唯一标识,用于在微信上识别和区分不同的小程序,在注册过程中,需要填写一些基本信息,如小程序名称、小程序介绍、小程序类别等。完成这些,微信会为你生成一个APPID。APPID将用于开发、发布和运营小程序各种操作,包含开发工具的配置等

大致流程如下:

  • 点击注册:进入微信官网(
    https://mp.weixin.qq.com/cgi-bin/wx
    ),点击“注册”
  • 注册小程序:包含账号信息(邮箱、密码...)、邮箱激活、信息登记(注册国家、主体类型-个人:身份证姓名、身份证、手机、短信)

注册后就可以登录到小程序后台管理界面,在“开发”导航中就能找到APPID。

小程序开发工具

小程序你不用特定工具怎么预览效果?
浏览器又不认识小程序

微信开发者工具提供如下功能:

  • 快速创建小程序项目
  • 代码查看和编辑
  • 小程序进行调试、预览
  • 小程序发布

找到稳定版下载安装成功,在桌面会看到一个二维码,用自己的微信扫一扫,登录后就能打开“微信开发者工具”。

创建项目:可以指定项目所在目录、后端服务是否选择云开发、语言有javascript或 TypeScript。

小程序工具主界面
分五个部分:

  • 菜单栏:如帮助(里面有“开发者文档”)、设置、项目、工具(有
    构建 npm
    、插件)
  • 工具栏:模拟器、编辑器、调试器、
    编译

    真机调试
  • 模拟器:模拟微信(底部有:页面路径、
    页面参数
  • 代码编辑区
  • 调试区:
    console控制台
    、Network、Storage
自定义编译模式

通过小程序工具,
普通编译
会从小程序首页开始,而平时我们修改某页面逻辑,保存后想立刻看到效果,而不是又从首页切换好几次才到该页面。这是,我们可以使用“自定义编译条件”。

点击“普通编译”下的“添加编译模式”,选择要启动的页面,还可以传参,新建即可。下次就选择这个页面编译即可。

一个页面可以创建多个编译页面,比如有参的、无参的...

协同工作

小程序通常不是一个人完成的。

微信小程序成员管理(
三方
)体现在管理员对小程序项目成员及体验成员的管理

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

开发者的权限有:

  • 开发者权限
  • 体验者权限
  • 登录权限:登录小程序后台,无需管理员确认
  • 开发设置:设置小程序服务器域名、消息推送及扫描普通二维码打开小程序

Tip
:之所以有这些角色,因为小程序的开发流程不同于网站开发,小程序的代码由小程序平台管理。

小程序版本

小程序发布流程大致如下:上传代码到开发版本,多次迭代开发版本,根据开发版本生成体验版本,验证通过后提交审核,审核通过后发布。

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

Tip
:微信小程序和支付宝小程序都提供了多版本开发和管理功能。体验版只能同时根据其中一个开发版本生成。

推广和运营数据

发布后就需要推广

推广可以基于微信码和小程序码。

小程序码的优势:

  • 样式上更具有辨识度
  • -更加清晰树立小程序品牌形象

小程序可以通过后台查看
运营数据
,也可以使用“小程序数据助手”(微信搜索)查看已发布小程序相关数据:访问分析、实时同级、用户画像...

小程序也可以使用第三方埋点工具,例如:友盟友、神策数据...

小程序对 npm 包支持和限制

微信小程序支持 NPM 包,但
小程序能用的 Npm 包却不多

下面是一些限制和注意事项:

  • API 限制:不支持依赖 node.js 内置库、浏览器内置对象、C++插件的 npm 包
  • 包大小限制:微信小程序的包大小有限制,单个包不能超过 2 MB,总体积不能超过 20 MB。因此,在使用 NPM 包时需要注意其体积,避免超出限制。
  • 构建工具:NPM 包的使用需要通过微信开发者工具进行构建和处理,确保在开发者工具中启用了 "构建 NPM" 功能。

新建项目和配置

项目基本结构

创建一个微信小程序项目,
目录结构
如下:

- pages: 存放所有小程序的页面
- utils:存放工具性质的模块
- app.js:小程序入口文件
- app.json:小程序全局配置文件。包含小程序所有页面路径、窗口外观、界面表现(所有页面的背景色、文字颜色、小程序组件所使用的样式版本)、底部tab
- project.config.json:项目配置文件。记录我们对小程序开发工具做的个性化配置,如项目名、小程序账号ID、编译相关配置(ES6转ES5、上传代码时自动压缩混淆、上传代码时样式自动补全)
- sitemap.json:配置小程序及其页面是否允许被微信索引。微信现已开放了小程序内搜索,类似网页的SEO。

小程序官方建议所有小程序页面都放在
pages
目录中,以单独文件夹存放:

- pages
  - index
    - index.js 页面脚本
    - index.wxml 页面结构
    - index.wxss 页面样式
    - index.json 当前页面的配置,如窗口的外观
  - pageb
    - pageb.js
    - pageb.wxml
    - pageb.wxss
    - pageb.json

Tip
:小程序中有4种
json配置文件
(具体作用后面会介绍)

  • 项目根目录中的 app.json
  • 项目根目录中的 project.config.json
  • 项目根目录中的 sitemap.json
  • 每个页面文件夹中的 json

新建小程序页面

在 app.json->pages 中新增页面存放路径,ctrl+s保存,工具会
自动
创建对应页面文件。

{
  pages: [
    "pages/index/index",
    "pages/pageb/pageb",
  + "pages/pageb/pagec"
  ]
}

修改项目首页

只需
调整
app.json->pages 数组中页面路径的
顺序
,小程序会把排在第一位的页面,当做项目首页渲染。

全局配置

小程序根目录下的
app.json
是小程序全局配置文件。

常用配置:

  • pages 记录当前小程序所有页面的存放路径
  • window 全局设置小程序窗口外观
  • tabBar 设置小程序底部的 tabBar 效果
  • style 是否启用新版的组件样式

示例:

{
  "pages": [
    "pages/index/index",
    "pages/logs/logs"
  ],
  "window": {
    "navigationBarTitleText": "小程序示例",
    "navigationBarBackgroundColor": "#ffffff",
    "navigationBarTextStyle": "black",
    "backgroundColor": "#eeeeee",
    "backgroundTextStyle": "light",
    "enablePullDownRefresh": true,
    "onReachBottomDistance": 50
  },
  "tabBar": {
    "color": "#7A7E83",
    "selectedColor": "#3cc51f",
    "borderStyle": "black",
    "backgroundColor": "#ffffff",
    "list": [
      {
        "pagePath": "pages/index/index",
        "text": "首页",
        "iconPath": "images/icon_home.png",
        "selectedIconPath": "images/icon_home_active.png"
      },
      {
        "pagePath": "pages/logs/logs",
        "text": "日志",
        "iconPath": "images/icon_logs.png",
        "selectedIconPath": "images/icon_logs_active.png"
      }
    ]
  },
  "style": "v2"
}
窗口

小程序窗口组成部分(从上到下):

  • navigationBar
    导航栏区域
    :包含时间、电量、微信标题
  • background
    背景区域,默认不可见,下拉才显示
  • 页面主体区域
    ,用了显示 wxml 中布局

windown节点常用配置项:

  • navigationBarTitleText
    导航栏标题
    文字 字符串
  • navigationBarBackgroundColor 导航栏背景颜色 默认#000000,类型 HexColor
  • navigationBarTextStyle 导航栏颜色(标题、电池等颜色) 仅支持 black/white,默认 white
  • backgroundColor 窗口背景色 默认#ffffff,类型 H3xColor
  • backgroundTextStyle 下拉loading 的样式,仅支持 dark/light,默认 dark
  • enablePullDownRefresh 是否全局开启下拉刷新。默认 false。开启后会作用于小程序每个页面。
  • onReachBottomDistance 页面上拉触底时间触发时距离底部距离,单位 px,默认 50,若无特殊需求,不建议修改。

Tip

下拉刷新
,通常做法是在页面中单独开启,而非在这里全局开启。下拉刷新开启后,若要实现刷新,还得在 onPullDownRefresh 方法中处理下来刷新逻辑,这个方法会在用户触发下拉刷新操作时被调用。


:模拟器不能百分之百还原真机。例如下拉刷新,在模拟器中,下拉后,过了3秒,下拉自动合上;而在真机中,不会自动合上

tabBar

小程序中 tabBar 是
导航组件
。特性有:

  • 位置: 通常位于小程序界面的底部。
  • 图标和文字: 每个 tab 都可以包含图标和文字。
  • 选中状态: 可以配置选中和未选中状态下的图标和文字颜色。
  • 页面映射: 每个 tab 对应一个页面路径,点击 tab 会切换到相应的页面。

以下是一个典型的 app.json 中
tabBar 配置示例
:

{
  "tabBar": {
    "color": "#999999",
    "selectedColor": "#1c1c1b",
    "backgroundColor": "#ffffff",
    "borderStyle": "black",
    "list": [
      {
        "pagePath": "pages/home/index",
        "text": "首页",
        "iconPath": "/images/icon_home.png",
        "selectedIconPath": "/images/icon_home_active.png"
      },
      {
        "pagePath": "pages/search/index",
        "text": "搜索",
        "iconPath": "/images/icon_search.png",
        "selectedIconPath": "/images/icon_search_active.png"
      },
      {
        "pagePath": "pages/profile/index",
        "text": "我的",
        "iconPath": "/images/icon_profile.png",
        "selectedIconPath": "/images/icon_profile_active.png"
      }
    ]
  }
}


:tabBar 只能配置最少2个,最多5个。当渲染顶部tabBar 时,
不显示 icon
,只显示文本。说tabBar 中的页面要放在 pages 前面,否则显示不出。

tabBar 有6个组成部分:

  • color,tab上文字的颜色
  • selectedColor,tab文字选中时的颜色
  • backgroundColor,tabBar 背景色
  • borderStyle,tabBar 边框颜色
  • iconPath,未选中时图片路径
  • selectedIconPath,选中时图片路径

tabBar 节点配置项:

  • position,默认 bottom,可配置 top
  • borderStyle,默认 black,仅支持 black/white
  • color,hexColor 类型
  • selectedColor,hexColor 类型
  • backgroundColor,hexColor 类型
  • list,Array,必填,
    最少2个,最多5个

每个 tab 项配置选项:

  • pagePath,必填,页面路径,页面必须在 pages 中预先定义
  • text,必填,tab上显示的文字
  • iconPath,未选中时图片路径;position 为top时不显示 icon
  • selectedIconPath,选中时图片路径;position 为top时不显示 icon

页面配置

在小程序中,全局配置和页面配置可以
定义页面的外观和行为
。当全局配置和页面配置冲突时,确实遵循就近原则,最终效果通常以页面配置为准。这意味着页面特定的配置会覆盖全局配置。这样可以确保页面的定制化效果。

页面配置中常用配置项:

  • navigationBarTitleText 导航栏标题
  • navigationBarBackgroundColor 导航栏背景颜色
  • navigationBarTextStyle 导航栏文字颜色
  • backgroundColor 页面背景颜色
  • backgroundTextStyle 下拉loading 的样式
  • enablePullDownRefresh 是否全局开启下拉刷新
  • onReachBottomDistance 页面上拉触底时间触发时距离底部距离
  • disableScroll 禁止页面滚动
  • usingComponents 页面使用的自定义组件列表

小程序基本语法

wxml

微信小程序的 wxml 类似网页中的 html。支付宝小程序中是 axml。

wxml 和 html 区别

  • 标签名称不同(比如用 view 代替 div):
    • HTML: div、span、img、a
    • wxml: view、text、image、navigator
  • 属性节点不同
<a href="http://www.baidu.com">百度</a>
<navigator url="http://www.baidu.com">百度</navigator>
  • 提供了类似 vue 的模板语法:数据绑定、列表渲染、条件渲染
数据绑定

在 data 中定义数据,在 wxml 中使用。例如:

Page({
  data: {
    name: '张三',
    age: 18,
    url: 'http://....png',
    randomNum: Math.random() * 10,
  }
})

用Mustache语法(
{{}}
)将变量包起来即可:

<view>{{ name }}</view>
<view>{{ randomNum > 5 ? '大于5': '小于或等于5' }}</view>

动态绑定属性不同于 vue 的 v-bind,小程序的动态绑定属性是直接在标签上写(
写法不同而已,死记即可
),例如:

<image src="{{ url }}"></image>

Tip
: 数据在小程序开发工具控制台的 AppData tab中可以看到。

条件渲染

小程序和vue中条件渲染对比:

  • 语法差异:微信小程序使用 wx:if、hidden、block wx:if,vue中使用 v-if,v-show。
  • wx:if 和 v-if 类似,是真正的条件渲染
  • hidden 和 v-hsow 类似,都是通过 css 控制显隐,元素始终存在
  • block 类似 template,一次控制多个组件的展示与隐藏,且都不会渲染成实际的 dom 元素

用法:在 wxml 中使用 wx:if、wx:elif、wx:else 标签,在 data 中定义变量,在 wx:if 中使用变量。

<view>
  <view wx:if="{{ age > 18 }}">
    你成年了
  </view>
  <view wx:elif="{{ age < 18 }}">  你未成年
  </view>
  <view wx:else>
    你很少年
  </view>
</view>
列表渲染

小程序和vue中列表渲染对比:

  • 语法差异:微信小程序使用 wx:for、wx:key,vue中使用 v-for和:key
  • 都强调为列表渲染的每一项制定一个唯一的 key
  • vue 在列表渲染中提供了更丰富的功能
  • wx:for 和 v-for 类似,都是遍历数组,渲染成列表

用法:在 wxml 中使用 wx:for 标签,在 data 中定义数组,在 wx:for 中使用数组。

默认当前循环项索引是 index,当前循环项是 item:

<view class="container">
  <block wx:for="{{items}}" wx:key="index">
    <view>
      <text>{{index}}: {{item}}</text>
    </view>
  </block>
</view>

Page({
  data: {
    items: ['Item 1', 'Item 2', 'Item 3']
  }
});

wx:for-item 和 wx:for-index 用于自定义变量名,使得代码更加清晰和可读。

<view class="container">
  <block wx:for="{{items}}" wx:for-item="user" wx:for-index="idx" wx:key="id">
    <view>
      <text>Index: {{idx}}</text>
      <text>ID: {{user.id}}</text>
      <text>Name: {{user.name}}</text>
    </view>
  </block>
</view>

Page({
  data: {
    items: [
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' },
      { id: 3, name: 'Charlie' }
    ]
  }
});


: 小程序的 key,直接是循环项中的属性,且不需要
{{}}
。如果是vue,得通过循环项找到

<template v-for="item in items" :key="item.id">

wxss

小程序的样式,类似网页的 css。

wxss 对比 css

wxss 具备 css 大部分特定,wxss 还对 css 进行了扩充和修改,以适应小程序的开发

wxss 和 css 区别:

  • 新增
    rpx
    (responsive pixel,响应式像素)尺寸单位
    • css中实现响应式布局,需要手动进行像素转换(常用 rem)。比如设计师提供的是 750 稿子,我们可能会将 1rem 等于75,设计稿的75就是1rem
    • wxss 在底层支持新的尺寸单位 rpx,在不同屏幕上会自动进行换算,同样是 750 的稿子,75个大小直接写成 75rpx 即可。
  • 提供全局样式和局部样式
    • 微信小程序:项目根目录中 app.wxss 会作用于素有小程序页面;局部页面的 .wxss 样式仅对当前页面生效
    • web 中CSS是全局作用,除非使用CSS模块化工具
  • 文件类型:微信小程序是 .wxss
  • 媒体查询:微信小程序不支持传统CSS媒体查询,如
    @media
  • css动画和过度:微信小程序支持部分 css 动画和过度,但有一些限制
  • wxss 仅支持部分常见的 css属性和选择器:.class和#id、element、并集选择器和后代选择器、::after和::before等伪类选择器
  • flex布局:微信小程序中的 flex 大部分与css一致,但具体表现有细微差异
  • 引入样式:微信小程序通过
    @import
    引入其他 .wxss,不支持 @import url() 形式引入外部 css
rpx

rpx 原理非常简单,把所有设备的屏幕从宽度上
等分 750 份

  • 在375的设备,1rpx 等于 0.5px
  • 在1500的设备,1rpx 等于 2px

Tip
:rem和 rpx 在实现响应式布局中,主要关注的是宽度的自适应。高度需要自行处理,比如等比扩展,或者限制最高高度。

iphone 屏幕宽度是 375px(逻辑像素),共有 750个像素点(物理像素),1个逻辑像素等于2个物理像素,等分750rpx。则:

  • 750rpx = 375px = 750 物理像素
  • 1rpx = 0.5px = 1 物理像素

开发举例:根据设计稿来,有的要求是1:1,有的是1:2,宽100px
200px的盒子,转成rpx 就是 200rpx
400rpx。

@import

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

@import "demo.wxss";
.box{

}

使用时是 class 而非 className。

Tip
:微信小程序支持使用 less,不过需要进行一些配置。

全局样式和局部样式

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

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


:当局部样式和全局样式冲突,和 css 中一样:哪个权重高用哪个,如果权重相同,则使用就近原则(采取局部样式)

Tip
:把鼠标放到小程序工具中选择器上,会有选中提示,例如:Selector Specificity:(0, 1, 0)

js

Tip
:小程序中的 js 分3大类

  • app.js:小程序入口文件,通过调用 App() 函数启动整个小程序
  • 页面.js:页面的入口文件,通过调用 Page() 函数创建并运行页面
  • 普通.js:普通功能模块文件,用来封装公共的函数或属性,供页面使用
wxs

wxs在微信小程序中的作用类似 Vue.js中的
过滤器
(vue3 已废除过滤器)

小程序中的 wxs 和 javascript 是两种语言,区别有:

  • wxs 在视图层中运行,js运行在逻辑层
  • wxs 隔离性。不能调用 js 中定义的函数,不能调用小程序的API
  • wxs 设计初衷为了提高数据处理性能,特别是与界面渲染密切相关场景,减少和逻辑层的通信

Tip
: 在 ios 中,小程序内的 wxs 比 js 块 2~20倍;在安卓上无差异。

wxs的语法基于JavaScript,这意味着如果你熟悉JavaScript,学习wxs会相对容易:

  • wxs 有自己的数据类型:number、string、boolean、array、object...
  • wxs 不支持类似es6+语法,不支持let、const、解构赋值、箭头函数;支持 var、function、es5语法
  • wxs 遵循 commonjs规范:module对象、require()函数、module.exports对象
  • wxs 可以编写在
    <wxs>
    标签中,就像js写在
    <script>
    中,wxs 必须提供 module 属性,用来指定当前 wxs 模块名称

外联wxs用法(src必须是相对路径):

<!-- index.wxml -->
<wxs module="utils" src="../../utils/utils.wxs"/>
<view>
  <text>{{utils.formatDate(new Date())}}</text>
</view>
// utils.wxs
module.exports = {
  formatDate: function(date) {
    var year = date.getFullYear();
    var month = date.getMonth() + 1;
    var day = date.getDate();
    return [year, month, day].map(this.formatNumber).join('-');
  },

  formatNumber: function(n) {
    n = n.toString();
    return n[1] ? n : '0' + n;
  }
};

数据请求

小程序中网络数据,处于安全考虑,小程序官方对数据接口做了如下限制:

  • 只能请求 https 类型接口
  • 必须将接口的域名添加到信任列表中

假如希望在自己的微信小程序中,希望请求https://www.xxx.com/域名下的接口。配置步骤:登录微信小程序后台->开发->开发设置->服务器域名->修改request合法域名。注意:

  • 域名只支持 https
  • 域名不能是 ip 地址或 localhost
  • 域名必须经过 ICP 备案
  • 服务器域名一个月内最多可申请5次修改

Tip
: 如果后端仅提供http协议接口,为不耽误开发进度,可以在微信开发者工具中,临时开启「开发环境不校验请求域名、TLS版本及HTTPS证书」,跳过 request 合法域名校验,仅限在开发和调试阶段使用。

get和post请求

在微信小程序中,您可以使用
wx.request
方法来发起 HTTP GET 和 POST 请求。这个方法提供了一种简单的方式来与服务器进行数据交互:

请看示例:

wx.request({
  url: 'https://api.example.com/data',
  method: 'GET', 
  data: {
    key1: 'value1',
    key2: 'value2'
  }, 
  header: {
    'content-type': 'application/json' 
  },
  success: function(res) {
  },
  fail: function(err) {
  }
});
wx.request({
  url: 'https://api.example.com/submit', 
  method: 'POST',
  data: {
    key1: 'value1',
    key2: 'value2'
  }, 
  header: {
    'content-type': 'application/json' 
  },
  success: function(res) {
  },
  fail: function(err) {
  }
});
小程序和跨域

小程序没有常规的跨域问题,但本质上还是涉及一些
。但是对于前端开发,则无需处理跨域。

跨域(Cross-Origin Resource Sharing,简称 CORS)是指一个域名下的文档或脚本尝试请求另一个域名下的资源时,由于浏览器的同源策略(Same-origin policy)限制而导致的请求被阻拦的行为。这里的“同源”指的是协议、域名和端口号完全相同。同源策略是一种安全措施,旨在防止恶意网站通过脚本读取另一个网站的敏感数据。

跨域的本质是指浏览器出于安全考虑,实施的一种同源策略(Same-origin policy)

小程序的主体不是浏览器,而是小程序平台,所以没有常规的跨域问题。

因为小程序需要配置受信任域名,其实也在一定程度上有了安全保障,小程序的服务端也会涉及到CORS的配置

小程序和Ajax

Ajax核心依赖浏览器中的 XMLHttpRequest 对象,而小程序的宿主环境是微信客户端,所以
小程序不叫”发起ajax请求“,而叫”发起网络请求“

微信小程序没有直接使用 ajax 这个术语,但提供了类似异步HTTP请求能力,主要通过 wx.request 接口来完成 Get 或 Post 请求,这一过程和Ajax非常类似,都是异步获取数据并更新界面而不阻塞页面。所以小程序中不说”ajax“,但实际上具备异步获取数据的能力

wx.request 和 ajax 功能相似,运营环境和实现机制不同。

请看示例:

wx.request({
  url: 'https://api.example.com/data', 
  method: 'GET', 
  data: {
    key1: 'value1',
    key2: 'value2'
  }, // 请求参数
  header: {
    'content-type': 'application/json' 
  },
  success: function(res) {
  },
  fail: function(err) {
  }
});

wx.request 可以与 async/await 和 Promise.all 配合使用:

  • 封装一个使用 wx.request 的 Promise 函数
const request = (options) => {
  return new Promise((resolve, reject) => {
    wx.request({
      ...options,
      success: res => resolve(res),
      fail: err => reject(err)
    });
  });
};
  • 然后在 async/await 中使用它:
Page({
  async onLoad() {
    try {
      const response = await request({
        url: 'https://example.com/api/data',
        method: 'GET'
      });
      console.log('Data:', response.data);
    } catch (err) {
      console.error('Error:', err);
    }
  }
});
小程序API的 Promise 化

在开发微信小程序时,许多原生 API 是基于
回调函数
的,这在现代 JavaScript 编程中可能不太方便。为了更好地处理异步操作,我们可以将这些回调函数形式的 API 转换为
Promise
形式

一种是手动封装

一种是用库(例如miniprogram-api-promise)。例如:
安装,构建后,在你的项目中配置并使用:

// app.js
import wxp from 'miniprogram-api-promise';

App({
  onLaunch() {
    // 把所有 wx 函数 promise 化
    wxp.init();
  }
});

在页面或组件中使用:

// pages/index/index.js
Page({
  data: {},

  async onLoad() {
    try {
      const response = await wx.p.request({
        url: 'https://api.example.com/data',
        method: 'GET',
      });
      console.log(response.data);
    } catch (error) {
      console.error(error);
    }
  }
});

生命周期

小程序有两类生命周期:

  1. 应用生命周期
    :小程序从启动->运行->销毁
  2. 页面生命周期
    :小程序中每个页面的加载->渲染->销毁

页面的生命周期范围小,应用程序的生命周期范围大:
小程序启动-> 页面A的生命周期 -> 页面B的生命周期 ->页面C的生命周期 -> 小程序结束

应用生命周期

应用生命周期函数需要写在 app.js中:

App({
  onLaunch: function(opts) {},
  onShow: function(opts){},
  hoHide: function(){},
})
  • onLaunch: 小程序启动后立即执行,全局只触发一次。适合做一些初始化设置,如登录、全局变量初始化等。
  • onShow: 小程序启动或者从后台进入前台显示时触发。可以在这里执行数据请求、恢复界面状态等操作。
  • onHide: 小程序从前台进入后台时触发。可以在此清理临时数据、暂停计时器等,以节省资源。
  • onError: 捕获小程序的异常错误,包括脚本错误、API调用错误等,对于监控小程序运行状态非常有用。
  • onUnhandledRejection (可选): 捕获未处理的Promise拒绝错误,这是较新的API,用于增强错误处理能力。

Tip
:微信开发者工具有一个选项“切后台”,就可以模拟切到后台

页面生命周期

每个小程序页面也有其独立的生命周期,主要用于控制页面的加载、渲染、显示、隐藏和卸载等过程。主要生命周期包括:

  • onLoad: 页面加载时触发(
    一个页面只调用一次
    )。适合初始化页面数据、获取页面参数等。
  • onShow: 页面显示/切入前台时触发。可以在这里设置页面数据、响应上个页面传递的参数等。
  • onReady: 页面初次渲染完成时触发(
    一个页面只调用一次
    )。此时可以进行一些DOM操作(虽然一般推荐使用setData来改变界面)。
  • onHide: 页面隐藏/切后台时触发。可以在这里保存页面状态、停止定时器等。
  • onUnload: 页面卸载时触发。适合做一些清理工作,如取消网络请求、移除事件监听等。
  • onPullDownRefresh: 页面下拉刷新时触发,需要在页面配置中开启enablePullDownRefresh。
  • onReachBottom: 页面上拉触底时触发,用于分页加载更多数据。
  • onPageScroll: 页面滚动时触发,可以用来监控页面滚动位置。
  • onShareAppMessage: 用户点击页面内分享按钮时触发,用于自定义分享内容

Tip
:后台进入前台,先执行全局的 onShow,再执行页面的 onShow。

下拉刷新

启用下拉刷新有全局开启下拉和局部开启下拉,实际开发,推荐使用局部开启下来,也就是为需要的页面单独开启下拉。

这里说一下实现:

  • 开启局部页面下拉
{
  "enablePullDownRefresh": true
}
  • 在页面中实现下拉刷新逻辑,注意调用 stopPullDownRefresh,否则真机中下拉效果一直显示,不会主动消失。
Page({
  onPullDownRefresh: function() {
    // 这里写你的数据重新加载或更新逻辑
    console.log('正在刷新...');

    // 模拟异步数据加载过程,实际情况中可能是发起网络请求获取新数据
    setTimeout(() => {
      // 数据加载完毕,停止下拉刷新的动画
      wx.stopPullDownRefresh();
      console.log('刷新完成');
    }, 1000); // 延迟时间仅作为示例,实际应根据你的数据加载时间调整
  },

  // 页面的其他生命周期函数和方法...
})
上拉触底

前面提到配置中默认是距离底部50px时触发,没有特别要求不用改

现在要实现上拉触底逻辑,只需要在 onReachBottom 中编码即可:

Page({
  data: {
    itemList: [], // 初始数据列表
    page: 1,     // 当前页数,用于分页加载
    hasMore: true // 是否还有更多数据
  },

  onReachBottom: function() {
    // 当用户滑动到底部时触发此函数
    if (this.data.hasMore) {
      this.loadMoreData();
    } else {
      wx.showToast({
        title: '没有更多数据了',
        icon: 'none'
      });
    }
  },

behaviors

小程序 behaviors 和 vue 中
mixins
类似。相似点有:

  • 都可以定义组件的属性(properties 或 props)和数据(data)、方法
  • 都可以定义生命周期函数(微信小程序中的 lifetimes,Vue 中的生命周期钩子函数如 created 等)

mixins 有一些问题:

  • 命名冲突:当多个 mixins 和组件本身定义了相同名称的属性或方法时,会导致命名冲突。Vue 会采用一种优先级机制来决定使用哪个,但这可能导致意料之外的行为。
  • 来源不明:当查看一个组件时,不清楚哪些属性和方法是来自于 mixins,这会使得代码理解和维护变得困难。在大型项目中,特别是多个 mixins 叠加使用时,这个问题尤其明显。
  • 耦合性:mixins 将共享逻辑放在一起,但这些逻辑可能高度依赖于组件本身的数据结构和其他逻辑,这导致了高度的耦合性,使得 mixins 难以重用和测试。

Vue 3 的组合 API(Composition API)在很多情况下可以替代 mixins,并且解决了某些 mixins 的不足之处,比如命名冲突和代码组织不清晰等问题

小程序 hehaviors 和 vue 中 mixins 区别:

  • 属性和数据的合并策略:Vue 提供了比较详细的合并策略(如数组合并和对象覆盖),而微信小程序的behaviors 主要是覆盖属性
  • 多重继承:微信小程序的 behaviors 支持多重继承,即一个组件可以使用多个 behaviors。Vue 的 mixins 也支持多重混入,但是在冲突解决上,Vue 的策略更为复杂和灵活

事件

事件绑定

事件是渲染层到逻辑层的通讯
:事件将用户在渲染层产生的动作,反馈到逻辑层进行处理。

小程序中常用事件:

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

在微信小程序中,推荐使用tap,而非传统html中的 click,因为小程序为了优化移动端触摸体验,特别设计了 tap 事件来处理用户点击。相对click,有几个优势:

  • 移动设备优化:click在移动设备存在 300 毫秒的延迟,这是为了区分单击和双击操作。而tap没有这种延迟
  • 更好的触摸体验:tap专为触摸屏设计,更符合用户在移动设备上的操作习惯。

请看示例:

Page({
  data: {
    message: '按钮尚未被点击'
  },
  // 方法不像vue需要写在 methods 中,和 data 同级即可。
  handleTap: function (e) {
    this.setData({
      message: '按钮被点击了!'
    });
    wx.showToast({
      title: '你点击了按钮',
      icon: 'none'
    });
  }
});
<view class="container">
  <button bindtap="handleTap">点击我</button>
  <view class="message">{{message}}</view>
</view>

除了tap事件,小程序还提供了一些常见的触摸事件:longpress(长按)、touchstart(触摸开始)、touchemove(触摸移动)、touchend(触摸结束)、touchcancel(触摸取消)等

Tip:小程序中其他事件有

事件类型 事件 说明
触摸事件 touchstart 手指触摸动作开始
touchmove 手指触摸后移动
touchend 手指触摸动作结束
touchcancel 手指触摸动作被打断,如来电提醒
tap 手指触摸后马上离开
longpress 手指触摸后,超过350ms再离开
longtap 手指触摸后,超过350ms再离开(别名)
表单事件 submit 表单提交
reset 表单重置
input 输入框输入时触发
focus 输入框获得焦点时触发
blur 输入框失去焦点时触发
媒体事件 play 开始播放
pause 暂停播放
ended 播放结束
timeupdate 播放进度更新
error 播放错误
waiting 正在加载中
图片事件 load 图片加载完成时触发
error 图片加载错误时触发
滚动事件 scroll 滚动时触发
scrolltoupper 滚动到顶部/左边时触发
scrolltolower 滚动到底部/右边时触发
开放能力事件 contact 用户点击客服按钮时触发
getuserinfo 获取用户信息事件
getphonenumber 获取用户手机号事件
事件对象

当事件回调触发时,会有一个事件对象 event,其详细属性有:

  • type,string,事件类型。如tap,其type 就是tap
  • target,Object,触发时间的组件的一些属性值集合(
    常用
  • detail,Object,事件对象中其他属性(额外信息)(
    常用
  • currentTarget,Object,当前触发事件的组件的一些属性值集合
  • touches,Array,触摸事件,当前停留在屏幕中的触摸点信息的数组(几个手指)
  • changedTouches,Array,触摸事件,当前变化的触摸点信息的数组
  • timeStamp,Integer,页面打开到触发事件所经历的毫秒数

Tip
: target 和 currentTarget 的区别类似 web 中target 和 currentTarget。target 是触发改事件的源,CurrentTarget 则是当前事件绑定的组件。比如点击 view 中的 button,e.target 是按钮,而 e.currentTarget 是 view。

<view bind:tap="callback">
  <button>btn</button>
</view>
事件传参

小程序事件传参不同于 vue

在Vue中可以这么写:
<button @click="handleClick(123)">Button 1</button>

但小程序会将 bindtap 属性值统一当做事件名处理,相当于调用 handleClick(123) 的事件处理函数。

微信小程序:通过
data-*
属性传递参数,使用 event.currentTarget(或target).dataset 获取参数。请看示例:

<view class="container">
  <button data-id="1" data-name="button1" bindtap="handleTap">Button 1</button>
  <button data-id="2" data-name="button2" bindtap="handleTap">Button 2</button>
</view>

Page({
  handleTap: function(event) {
    const { id, name } = event.currentTarget.dataset;  // 获取多个参数
    console.log('Button clicked:', id, name);
  }
});

数据同步

在微信小程序中,
this.setData
是用于更新页面数据的主要方法。当数据改变时,视图会自动更新。this.setData 可以用来修改 Page 对象中的数据,并将数据的变化反映到界面上。

<!-- example.wxml -->
<view class="container">
  <text>计数值: {{count}}</text>
  <button bindtap="incrementCount">增加</button>
  <button bindtap="decrementCount">减少</button>
  <input placeholder="输入内容" bindinput="handleInput"/>
  <text>输入内容: {{inputValue}}</text>
</view>

// example.js
Page({
  data: {
    count: 0,
    inputValue: ''
  },

  // 增加计数
  incrementCount: function () {
    this.setData({
      count: this.data.count + 1
    });
  },

  // 减少计数
  decrementCount: function () {
    this.setData({
      count: this.data.count - 1
    });
  },

  // 处理输入事件
  handleInput: function (e) {
    this.setData({
      inputValue: e.detail.value
    });
  }
});

Tip

  • 在Vue中通常直接修改数据,对于某些情况可能需要用上this.$set,但是到了 vue3 中,由于改用 proxy 响应式系统,可以自动检测和监听响应式属性的新增和删除,更加方便。
  • 小程序的 setData 和 react 中的 useState 非常相似。合并状态都是合并,而非替换。请看示例:
Page({
  data: {
    count: 0,
    inputValue: ''
  },
  incrementCount: function () {
    this.setData({
      count: this.data.count + 1
    });
  },
  handleInput: function (e) {
    this.setData({
      inputValue: e.detail.value
    });
  }
});
文本框和数据的同步

对于文本框和数据的同步,小程序和vue实现原理类似。

vue中可以通过 v-model 实现双向绑定,但是
v-model 的本质
是 value 的属性以及 @input 事件

<input type="text" v-model="message" placeholder="Enter text"/>
或
<input type="text" :value="message" @input="updateMessage" placeholder="Enter text"/>

new Vue({
  el: '#app',
  data: {
    message: ''
  },
  methods: {
    updateMessage(event) {
      this.message = event.target.value;
    }
  }
});

用微信小程序是这样:

<input type="text" value="{{inputValue}}" bindinput="handleInput" placeholder="Enter text"/>


Page({
  data: {
    inputValue: '',
    errorMsg: ''
  },
  handleInput: function(event) {
    const value = event.detail.value;
    let errorMsg = '';
    if (value.length < 3) {
      errorMsg = 'Input must be at least 3 characters long';
    }
    this.setData({
      inputValue: value,
      errorMsg: errorMsg
    });
  }
});

小程序组件

小程序中的组件也由宿主环境提供,开发者可以基于组件搭建出漂亮的页面。小程序的组件分类有:

  • 视图容器
  • 基础内容
  • 表单组件
  • 导航组件
  • 媒体组件
  • 地图组件
  • canvas 画布组件
  • 开放能力
  • 无障碍访问

Tip
: 微信小程序 vs 支付宝小程序常用组件对比。感觉几乎相同

功能/类别 微信小程序组件 支付宝小程序组件 备注
视图容器 view view 基本视图容器
scroll-view scroll-view 可滚动视图容器
swiper swiper 滑块视图容器
movable-view movable-view 可移动的视图容器
cover-view cover-view 覆盖在原生组件上的视图容器
list 列表视图容器
基础内容 text text 文本标签
icon icon 图标组件
rich-text rich-text 富文本组件
progress progress 进度条
表单组件 form form 表单,用于收集数据
input input 单行输入框
textarea textarea 多行输入框
checkbox checkbox 复选框
radio radio 单选按钮
switch switch 开关选择器
slider slider 滑动选择器
picker picker 选择器
picker-view picker-view 嵌入页面的滚动选择器
label label 标签,用于表单控件的说明
导航组件 navigator navigator 页面导航
媒体组件 image image 图片组件
video video 视频组件
audio audio 音频组件
camera camera 相机组件
live-player live-player 实时音视频播放组件
live-pusher live-pusher 实时音视频推流组件
地图组件 map map 地图组件
画布组件 canvas canvas 画布组件,用于绘制图形
开放能力 open-data contact-button 微信开放数据组件和支付宝客服按钮
web-view web-view 嵌入网页内容
ad ad 广告组件
official-account lifestyle 微信公众号组件和支付宝生活号组件
login button 登录按钮(不同场景使用)
pay-button button 支付按钮(不同场景使用)
无障碍访问 aria-role aria-role 无障碍角色
aria-label aria-label 无障碍标签
常用视图容器组件
  • view,普通视图区域,类似html中的div,是一个块级元素,常用于实现页面布局
  • scroll-view, 可滚动的视图区域
  • swiper和swiper-item,轮播图容器组件和轮播图 item 组件


:为什么不用 div ,而要创建 view?

:微信小程序选择使用 view 等自定义组件而不是原生 HTML 标签,如 div,主要出于以下几个原因:

  1. 框架设计:为适配小程序的架构和特性。
  2. 性能优化:提升移动端的渲染性能和用户体验。
  3. 一致性和兼容性:确保在不同平台和设备上的一致表现。
  4. 更好地支持小程序特性:与小程序的生命周期、事件系统和样式管理等深度集成。
  5. 方便管理和维护:提供完善的组件体系和 API,简化开发和维护工作。
  6. 安全性:避免直接操作 DOM 带来的安全问题。
  7. 适合移动开发:更好地适配移动端的开发和用户体验需求。
    通过使用 view 组件,微信小程序能够更好地控制和优化应用的表现,提供更高效和一致的开发和用户体验。


:有了 view,为什么还得单独为了滚动创建 scroll-view?

:尽管 view 组件已经提供了基本的容器功能,但 scroll-view 组件作为专门的滚动容器,具有以下显著优势:

  1. 专为滚动设计:提供了丰富的功能和配置选项,便于控制滚动行为。
  2. 平滑滚动与性能优化:经过优化,提供更好的滚动体验。
  3. 额外功能支持:支持弹性滚动、滚动条隐藏等移动端常见功能。
  4. 可组合性和复用性:使得代码更模块化、易读和可维护。
  5. 事件监听与处理:丰富的事件机制,便于处理滚动相关逻辑。
  6. 动态控制滚动位置:通过属性控制滚动位置,支持动画效果。
  7. 避免样式冲突:确保滚动区域的独立性和稳定性。
    总之,微信小程序引入 scroll-view 组件,是为了提供更强大、更优化的滚动功能,提升用户体验和开发效率。


:就不能将 scroll-view 合并到 view?

:尽管将 scroll-view 的功能合并到 view 组件中在理论上是可行的,但在实践中会引入许多复杂性和技术挑战。微信小程序选择将 scroll-view 与 view 分开实现,是为了:

  1. 保持组件的职责单一,简化开发和维护。
  2. 优化性能,提供更高效的滚动体验。
  3. 提供丰富的功能和事件支持,增强灵活性。
  4. 避免样式和布局冲突,确保向下兼容。
    分开实现虽然增加了学习和使用的成本,但从长期来看,能够更好地满足开发者和用户的需求,同时保持代码的简洁和高效。因此,微信小程序将 scroll-view 独立出来是一个经过深思熟虑的设计选择。
基础内容

text
: 长按选中(selectable属性)只能使用 text,放在 view 中的不可以。

rich-tex
t:通过其nodes 属性节点,可以把HTML字符串渲染成对应的UI结构

其他常用组件
  • button
    :按钮组件,功能比html中的 button 按钮丰富(主色调、大按钮小按钮、警告按钮...),通过 open-type属性可以调用微信提供的各种功能(客服、转发、获取用户信息)
  • image
    :图片组件,image组件默认宽度约300px,高度约240px。mode 属性可以用来指定图片的裁剪和缩放,aspectFill类似 cover,aspectFit类似contain。其中差异需要自己品味
  • navigator
    :导航组件,类似html中的a标签,用于页面跳转

自定义组件

小程序开发者工具也提供了创建组件的便捷方式,右键“新建 Component”

局部组件和全局组件

组件从引用方式分为:

  1. 局部引用
    :组件只能在当前被引用的页面中使用
  2. 全局引用
    :组件每个小程序页面都可以使用
  • 局部引用示例:
components/
  my-component/
    my-component.wxml
    my-component.wxss
    my-component.js
    my-component.json
{
  "usingComponents": {
    "my-component": "/components/my-component/my-component"
  }
}
<view>
  <my-component text="Welcome to My Component"></my-component>
</view>
// my-component.json
{
  "component": true
}
  • 全局引用示例:
components/
  my-global-component/
    my-global-component.wxml
    my-global-component.wxss
    my-global-component.js
    my-global-component.json

在 app.json 中进行全局引用配置

{
  "pages": [
    "pages/index/index",
    "pages/logs/logs"
  ],
  "window": {
    "backgroundTextStyle": "light",
    "navigationBarBackgroundColor": "#fff",
    "navigationBarTitleText": "WeChat",
    "navigationBarTextStyle": "black"
  },
  "usingComponents": {
    "my-global-component": "/components/my-global-component/my-global-component"
  }
}

页面和组件的区别

在小程序中,页面和组件在开发解构和使用方式上有许多相似之处,但他们用途和特性有所不同

特性 页面(Page) 组件(Component)
功能 用户交互的独立视图 可复用的功能模块或UI元素
组成文件 .wxml
,
.wxss
,
.js
,
.json
.wxml
,
.wxss
,
.js
(调用Component()函数、事件需要定义到 methods中),
.json
(需要
"component": true
生命周期 onLoad
,
onShow
,
onReady
,
onHide
,
onUnload
created
,
attached
,
ready
,
moved
,
detached
路由和导航 支持路由和导航 API 不支持路由和导航
组合和嵌套 不能嵌套在其他页面中 可以嵌套在页面或其他组件中
复用性 通常独立使用 高,可在多个页面中引用

组件样式隔离

在微信小程序中,组件的样式是
默认隔离
的。这意味着组件的样式不会影响到外部页面或其他组件,而外部样式也不会影响到组件内部。这种样式隔离机制有助于提高组件的独立性和可复用性

如果希望外界影响到组件,也可以通过设置 `"styleIsolation" 来修改。微信小程序的样式隔离有三种模式:

  • isolated(默认):完全隔离,组件的样式不会影响到外部,外部的样式也不会影响到组件内部。
  • apply-shared:组件样式不影响外部,但外部的全局样式可以影响组件内部。
  • shared:组件样式和外部样式互相影响。

Tip
:说只有class选择器会有样式隔离效果,id选择器、属性选择器、标签选择器不会受样式隔离影响

数据、方法和属性

组件中的数据、方法和属性,请看示例:

Component({
  properties: {
    max: {
      type: Number,
      value: 10
    }
  },
  data: {
    name: 'pjl'
  },
  methods: {
    handleFn() {
      // true
      console.log(this.data === this.properties);
      // 使用 setData 修改 properties 值
      this.setData({max: this.properties.max + 1})
    }
  }
})

Tip
:说小程序中properties 属性和 data 数据用法相同,都是
可读可写

数据监听

微信小程序中的
observers
和 Vue.js 中的 watch 功能相似,都是用于监听数据变化并做出响应。然而,Vue.js 的 watch 提供了更多选项和更大的灵活性,适用于更复杂的监听需求。微信小程序的 observers 则较为简单和直接。

语法:

Compoment({
  observers: {
    '字段A, 字段B': function(字段A的新值, 字段B的新值) {
    }
  }
})
  • 监听多个数据
observers: {
    'countA, countB': function(newCountA, newCountB) {
      console.log(`CountA has changed to: ${newCountA}, CountB has changed to: ${newCountB}`);
      this.setData({
        sum: newCountA + newCountB
      });
    }
)
  • 监听对象中的多个属性
observers: {
    'obj.v1, obj.v2': function(newFirstElement, newSecondElement) {
      console.log(`First element has changed to: ${newFirstElement}, Second element has changed to: ${newSecondElement}`);
    }
}
  • 监听对象中所有属性
observers: {
  'obj.**': function(newObj) {
    
  }

纯数据字段

微信小程序有
纯数据字段
,其主要作用:

  • 减少数据传输:使用 setData 方法时,所有的非纯数据字段都会被序列化并发送到视图层。如果某些数据仅在逻辑层使用,并且不需要渲染到视图中,可以将这些数据标记为纯数据字段,以避免不必要的传输,从而提高性能
  • 状态管理:纯数据字段可以用于存储组件内部的一些临时状态或计算结果,这些状态或结果不需要被渲染到视图中。例如,缓存一些计算结果或者维护一些内部状态。
  • 代码的可维护性:标记纯数据字段可以帮助开发者更清楚地区分哪些数据是需要渲染的,哪些数据仅用于逻辑处理。这有助于提高代码的可读性和可维护性。

在 options 中使用
pureDataPattern
。请看示例:

Component({
  // 组件的属性列表
  properties: {
    initialValue: {
      type: Number,
      value: 0
    }
  },

  // 组件的初始数据
  data: {
    displayResult: 0,
    __internalCache: 0 // 纯数据字段,不会被传递到视图层
  },

  // 定义纯数据对象的匹配模式
  options: {
    pureDataPattern: /^__/
  },

组件生命周期

  • created(常用): 组件实例被创建时触发,此时组件的属性值、数据等尚未初始化,不能进行数据绑定操作(即不能使用 setData 方法)。
  • attached(常用):组件实例进入页面节点树时触发,可以访问属性值和数据,适合在这个阶段进行数据绑定和初始化工作。通常用于初始化数据、监听某些事件等。
  • ready:组件布局完成,即视图层的渲染已经完成,此时可以对组件的 DOM 结构进行操作。
  • moved:组件实例被移动到节点树另一个位置
  • detached(常用):组件实例被从页面节点树中移除时触发。
    适合在这个阶段进行清理工作,例如取消事件监听、清除定时器等,防止内存泄漏。
  • error:每当组件方法抛出错误时执行

小程序组件,生命周期可以直接定义在 Component 构造器一级参数中,也可以写在 lifetimes 字段内(推荐方式,优先级更高)

Component({
  // 低优先级
  error(err) {
     
  },
  lifetimes: {
    error(err) {
     
    }
  }
});

组件在页面的生命周期

有时,自定义组件行为依赖于页面状态的变化,这时就得用到组件所在页面的生命周期。比如每当触发页面的 show 声明周期时,希望重新生成一个数。

组件所在页面的生命周期有3个:

  • show:组件所在页面展示时触发
  • hide:组件所在页面隐藏时触发
  • resize:组件所在页面尺寸变化时触发

例如:

Component({
  pageLifetimes: {
    show() {
      console.log('Component in page show');
      // 页面显示时执行的逻辑
    }
  }
});

插槽

和 vue 中类似,没有作用域插槽。

有单个插槽和多个插槽

组件通信

微信小程序中组件通信,和vue中类似,父传子用属性,子传父用事件。

Tip
:微信小程序还有父组件通过 this.selectComponent() 获取组件实例(应该要少用)

子组件向父组件传递数据示例:

// 子组件
Component({
  methods: {
    incrementCount() {
      // 触发自定义事件,传递数据
      this.triggerEvent('countChange', { count: this.data.count + 1 });
    }
  }
});

<view class="my-component">
  <button bindtap="incrementCount">Increment</button>
</view>
// 父组件
<view class="container">
  <my-component bind:countChange="handleCountChange"></my-component>
</view>

Page({
  handleCountChange(e) {
    // e.detail获取子组件传递的数据
    const newCount = e.detail.count;
    
  }
});

微信小程序安装 vant weapp

vant weapp 是一套小程序UI组件库。

小程序使用npm 包的和传统网站有一些不同。比如:

  • 安装和引用:传统网站npm 包会安装在 node_modules 目录中;小程序开发者工具会将 node_modules 中的包处理后放在 miniprogram_npm 目录,引用npm包时,需要使用 miniprogram_npm 路径,例如:"miniprogram_npm/@vant/weapp/button/index"。
  • 构建:小程序需要在微信开发者工具中额外执行 “构建 NPM” 操作,将 NPM 包从 node_modules 构建到 miniprogram_npm 目录
  • 包体积大小限制:传统网站没有严格限制包体积大小

微信小程序安装 vant weapp,大概步骤(详细看vant官网):

  • 通过 npm 安装
  • 构建 npm 包
  • 修改 app.json

Tip
:小程序比较特殊,每安装一个包都得构建才能使用。建议先删除 miniprogram_npm 这个包在构建,否则容易构建失败等问题

路由导航

导航就是页面中相互跳转,浏览器中有
<a>

location.href

vue 的单页面中有
编程式导航

命令行导航
,在微信小程序中也有编程式导航和命令行导航

小程序和单页应用

先说一下传统的单页应用:

  • 单个HTML页面
  • 前端路由
  • 无刷新体验
  • 前后端分离

微信小程序在某种程度上与单页应用有相似的用户体验和部分技术实现,但在
严格技术定义来看,它并不是单页应用

微信小程序采用多页面框架,每个页面独立存在。切换页面的时候就可以和原生一致,可以做到滑动的效果。

小程序和单页的相似:

  • 无刷新体验
  • 客户端路由:通过客户端的API进行页面导航
  • 前后端分离

小程序和单页的差异:

  • 页面独立:每个小程序的页面都是独立的,有自己的文件和生命周期。传统的apa则是在一个HTML文件内动态渲染和更新内容
  • 页面加载:小程序页面切换时会加载相应的文件

vue路由 vs 小程序路由

vue 中编程式导航和命令式导航,就像这样:

this.$router.push({ path: '/some/path', query: { key: 'value' } });

<router-link :to="{ name: 'routeName', params: { userId: 123 } }">Go to User</router-link>

微信小程序中的命令式导航主要通过页面的 WXML 文件中的
<navigator>
组件实现,类似于 HTML 的
<a>
标签。

<navigator url="/pages/somePath/somePath">Go to Some Path</navigator>
<navigator url="/pages/tabPage/tabPage" open-type="switchTab">Go to Tab Page</navigator>

微信小程序中的编程式导航通过 wx.navigateTo、wx.redirectTo、wx.switchTab 和 wx.reLaunch 等方法实现。这些方法允许开发者在 JavaScript 代码中进行页面跳转

// 保留当前页面,跳转到应用内的某个页面
wx.navigateTo({
  url: '/pages/somePath/somePath'
});

// 关闭当前页面,跳转到应用内的某个页面
wx.redirectTo({
  url: '/pages/somePath/somePath'
});

// 跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面
wx.switchTab({
  url: '/pages/tabPage/tabPage'
});

// 关闭所有页面,打开到应用内的某个页面
wx.reLaunch({
  url: '/pages/somePath/somePath'
});
对比分析

编程式导航:

  • Vue Router:通过 this.$router.push 等方法进行导航,支持多种导航方式(path、name、params、query)。
  • 微信小程序:通过 wx.navigateTo、wx.redirectTo 等方法进行导航,功能丰富,但需要指定具体的 URL。

命令式导航:

  • Vue Router:通过
    <router-link>
    组件进行导航,语义化强,结构清晰,易读。
  • 微信小程序:通过
    <navigator>
    组件进行导航,功能类似
    <router-link>
    ,但没有 Vue 的路由命名和参数传递功能,需要通过 URL 进行导航。

参数传递:

  • Vue Router:支持通过 params 和 query 传递参数,非常灵活。
  • 微信小程序:参数需要拼接在 URL 中,不够直观,参数传递相对复杂。

适用场景:

  • Vue Router:适用于复杂的单页应用(SPA),需要强大的路由管理功能和灵活的参数传递。
  • 微信小程序:适用于小程序开发,注重简单和快速导航,符合小程序的设计哲学。

通过对比,可以看出 Vue Router 在单页应用中的复杂导航管理方面更
强大
,而微信小程序的导航设计则更加
简洁
和快速,符合小程序快速开发的需求

  1. 编程式导航:Vue Router 和微信小程序都提供了强大的编程式导航功能,前者通过 this.$router.push 等方法,后者通过 wx.navigateTo 等方法。Vue Router 更加灵活,参数传递更方便;微信小程序的编程式导航功能比较简单,需指定具体 URL。
  2. 命令式导航:Vue Router 使用
    <router-link>
    ,微信小程序使用
    <navigator>
    。两者功能类似,都是用于声明式地定义导航结构,但 Vue Router 提供了更强大的路由命名和参数传递功能。

声明式导航

  • 导航到
    tabBar页面
    : url 必须以 / 开头;open-type表示跳转方式,必须为 switchTab。请看示例:
<navigator url="/pages/page1/page1" open-type="switchTab">导航到page1</navigator>
  • 导航到非
    tabBar页面
    :url 必须以 / 开头;open-type表示跳转方式,必须为 navigate(可省略)。请看示例:
<navigator url="/pages/page1/page1" open-type="navigate">导航到page1</navigator>
  • 后退导航,比如后退上一页或多级页面:open-type必须是 navigateBack,表示后退导航;delta 必须是数字(默认是1,可省略),表示后退层级。请看示例:
<navigator open-type="navigateBack" delta="1">返回上一页</navigator>

编程式导航

  • 导航到 tabBar 页面:调用 wx.switchTab(Object obj)。obj 中属性有:url(必填,路径后不能带参数)、success、fail、complete
wx.switchTab({
  url: '/pages/tabBar/home/home',
  success: function(res) {
    // 成功回调
  },
  fail: function(err) {
    // 失败回调
  }
});
  • 导航到非 tabBar 页面:调用 wx.navigateTo(Object obj)。obj 中属性有:url(必填,路径后能带参数)、success、fail、complete
wx.navigateTo({
  url: '/pages/page1/page1',
  success: function(res) {
    // 成功回调
  },
  fail: function(err) {
    // 失败回调
  }
});
  • 后退导航:调用 wx.navigateBack(Object obj)。obj 中属性有:delta(默认是1,可省略)、success、fail、complete
wx.navigateBack({
  delta: 1,
  success: function(res) {
    // 成功回调
  },
  fail

导航传参

  • 声明式导航传参:直接写在后面
<navigator url="/pages/page1/page1?name=pjl&age=18">导航到page1</navigator>
  • 编程式导航传参
wx.navigateTo({
      url: '/pages/detail/detail?itemId=123&itemName=ExampleItem'
    });

新页面接收参数:

Page({
  onLoad: function(options) {
    // options 对象包含了传递的参数
    console.log(options.itemId); // 输出: 123
    console.log(options.itemName); // 输出: ExampleItem
  }
});

状态管理

全局数据共享有:vuex、mobx、Redux等

小程序中可以使用
mobx
管理小程序状态。大概步骤:

  • 安装 MobX 和 MobX 的微信小程序支持库 mobx-miniprogram 和 mobx-miniprogram-bindings
  • 构建 npm 包
  • 创建 Mobx store:在项目根目录下创建一个 store 文件夹,然后在里面创建 index.js 文件,定义你的 MobX store
// store/index.js
import { observable, action } from 'mobx-miniprogram';

export const store = observable({
  // 定义状态
  count: 0,

  // 定义计算属性
  get doubleCount() {
    return this.count * 2;
  },

  // 定义动作
  increment: action(function() {
    this.count += 1;
  }),
  decrement: action(function() {
    this.count -= 1;
  })
});
  • 将 Store 注入小程序

  • 使用 MobX 绑定页面:在页面中使用 mobx-miniprogram-bindings 库来绑定 MobX store

// pages/index/index.js
import { createStoreBindings } from 'mobx-miniprogram-bindings';
import { store } from '../../store';

Page({
  // 初始化 Store Bindings
  onLoad() {
    this.storeBindings = createStoreBindings(this, {
      store,
      fields: ['count', 'doubleCount'],
      actions: ['increment', 'decrement']
    });
  },

  // 销毁 Store Bindings
  onUnload() {
    this.storeBindings.destroyStoreBindings();
  }
});

通常建议每个页面都有自己的 Store

全局store和页面store混合使用也是一种很好的实践。

分包

小程序中的
分包
(subpackage)是指将小程序的代码分割成多个子包(subpackage),每个子包可以独立开发、测试、发布,最终合并成一个完整的小程序

分包的优点

  • 通过将小程序的资源按需加载,可以减少首次加载时的资源量,提高启动速度。
  • 多团队共同开发,解耦协作

分包类型

分包中三种包:

  • 主包(Main Package):小程序的核心包,包括小程序的入口文件(如 app.js、app.json 和 app.wxss)以及小程序根目录下的资源。主包在小程序启动时加载。
  • 分包(Subpackage):除了主包之外的其他包,按需加载。可以包含页面、组件及其他资源。
  • 独立分包(Independent Subpackage):一种特殊的分包形式,独立分包可以独立于主包运行,适用于需要快速启动的小程序模块。

分包加载规则

分包后,小程序项目:1个主包+多个分包

  • 主包:通常只包含项目启动页面或Tabbar页面、以及所有分包需要的公共资源
  • 分包:只包含当前分包的页面和私有资源(图片、js、wxss、wxs...)

小程序启动时,默认下载主包并启动主包内页面,当用户进入分包某页面时,客户端会把对应分包下载下来后再展示

分包配置

假设我们有一个主包和两个分包 subpackageA 和 subpackageB。

项目目录结构如下:

├── app.js
├── app.json
├── app.wxss
├── pages
│   ├── index
│   └── logs
├── subpackageA
│   ├── pages
│   │   ├── pageA1
│   │   │   ├── pageA1.js
│   │   │   ├── pageA1.json
│   │   │   ├── pageA1.wxml
│   │   │   └── pageA1.wxss
│   │   ├── pageA2
│   │       ├── pageA2.js
│   │       ├── pageA2.json
│   │       ├── pageA2.wxml
│   │       └── pageA2.wxss
├── subpackageB
│   ├── pages
│   │   ├── pageB1
│   │   │   ├── pageB1.js
│   │   │   ├── pageB1.json
│   │   │   ├── pageB1.wxml
│   │   │   └── pageB1.wxss
│   │   ├── pageB2
│   │       ├── pageB2.js
│   │       ├── pageB2.json
│   │       ├── pageB2.wxml
│   │       └── pageB2.wxss

在 app.json 中配置分包信息配置(subPackages)如下:

// app.json
{
  "pages": [
    "pages/index/index",
    "pages/logs/logs"
  ],
  "subPackages": [
    {
      "root": "subpackageA",
      "pages": [
        "pages/pageA1/pageA1",
        "pages/pageA2/pageA2"
      ]
    },
    {
      "root": "subpackageB",
      "pages": [
        "pages/pageB1/pageB1",
        "pages/pageB2/pageB2"
      ]
    }
  ]
}

Tip:分包的体积是有一定限制的,分包体积可以在“小程序开发者工具”中查看。

打包原则

  • 小程序会安装 subpackages 的配置进行分包,subpackages之外的目录会被打包到主包中
  • tabBar 页面必须在主包内
  • 分包直接不能相互嵌套

分包引用规则

  • 分包可以引用主包内公共资源
  • 主包不能引用分包内私有资源
  • 分包之间不能相互引用私有资源

独立分包

独立分包是微信小程序提供的一种特殊分包形式,允许某些分包独立于主包运行。这对于需要快速启动的模块尤其有用,例如登录模块、功能独立的插件模块等。使用独立分包可以显著提高小程序的启动速度和用户体验。

独立分包的特点:

  • 独立运行:独立分包无需加载主包即可启动,具有独立的入口文件(如 app.js、app.json、app.wxss)。
  • 快速启动:由于独立分包不依赖主包,可以显著提高这些模块的启动速度,适用于需要快速启动的场景。
  • 资源隔离:独立分包的资源相对主包和其他分包是隔离的,适用于功能比较独立的模块。

将分包配置成独立分包,只需要一个配置:independent。请看示例

// app.json
{
  "pages": [
    "pages/index/index",
    "pages/logs/logs"
  ],
  "subPackages": [
    {
      "root": "subpackageA",
      "pages": [
        "pages/pageA1/pageA1",
        "pages/pageA2/pageA2"
      ],
      "independent": true
    }
  ]
}

分包预下载

分包预下载:是指进入小程序某页面时,框架自动下载可能需要的包。

例如进入 tabBar pageB 页面时下载 packageA。

通过 preloadRule 配置。就像这样:

{
  "pages": [
    "pages/pageA/index",
    "pages/pageB/index"
  ],
  "tabBar": {
    "list": [
      {
        "pagePath": "pages/pageB/index",
        "text": "PageB"
      }
    ]
  },
  "subPackages": [
    {
      "root": "packageA/",
      "pages": [
        "pageC/index"
      ]
    }
  ],
  "preloadRule": {
    "pages/pageB/index": {
      // wifi、2g、3g、4g。wifi 不包含 2g。
      "network": "all",
      "packages": ["packageA"]
    }
  }
}

1、在react项目中安装react-pdf依赖包

建议安装8.0.2版本的react-pdf,如果安装更高版本的可能出现一些浏览器的兼容性问题;

npm install react-pdf@8.0.2 -S

1、PC端的使用

1.1、封装一个组件:PdfViewModal.tsx

import React, { useState } from 'react'import { Modal, Spin, Alert } from'antd'import { Document, Page, pdfjs } from'react-pdf'import'react-pdf/dist/esm/Page/AnnotationLayer.css'import'react-pdf/dist/esm/Page/TextLayer.css';//配置 PDF.js 的 worker 文件
pdfjs.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.js', import.meta.url).toString()

interface PDFPreviewModalProps {
fileName: string
| nullfileUrl: string| null //传入的 PDF 文件地址 onCancel: () => void //关闭弹框的回调 }

const PDFPreviewModal: React.FC
<PDFPreviewModalProps> = ({ fileName, fileUrl, onCancel }) =>{
const [numPages, setNumPages]
= useState<number | null>(null)
const [pdfWidth, setPdfWidth]
= useState<number>(600) //默认宽度为 600px const [loading, setLoading] = useState<boolean>(true) //控制加载状态 const [error, setError] = useState<boolean>(false) //控制加载错误状态 //当 PDF 加载成功时,设置页面数量 const onDocumentLoadSuccess = ({ numPages }: { numPages: number }) =>{
setNumPages(numPages)
setLoading(
false) //加载成功后,隐藏 loading }//加载失败时,设置错误状态 const onDocumentLoadError = () =>{
setLoading(
false)
setError(
true) //出错时显示错误提示 }//获取 PDF 页面加载后的宽度 const onPageLoadSuccess = ({ width }: { width: number }) =>{
setPdfWidth(width)
}
return(<Modal
title
={`【${fileName}】预览`}
open
onCancel
={onCancel}
footer
={null}
width
={pdfWidth + 100}
style
={{ top: 20}}>{error?(<Alert message="加载 PDF 文件失败" type="error" showIcon /> ) : (<>{loading&&(<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '80vh' }}> <Spin size="large" /> </div> )}
{fileUrl
&&(<> <div style={{ height: '88vh', overflowY: 'auto', padding: '24px' }}> <Document//file={new URL('/public/temp/DXF文件要求.pdf',import.meta.url).toString()} file={fileUrl}
onLoadSuccess
={onDocumentLoadSuccess}
onLoadError
={onDocumentLoadError}>{Array.from(new Array(numPages), (el, index) =>(<Page key={`page_${index + 1}`} pageNumber={index + 1} onLoadSuccess={onPageLoadSuccess} /> ))}</Document> </div> </> )}</> )}</Modal> )
}

export
default PDFPreviewModal

1.2、业务代码中引入该组件

import React, { useState, useEffect, useCallback } from 'react'import { Form } from'antd'import { List } from'antd'import PDFPreviewModal from'@/components/PdfViewModal.tsx'

const PdfTest = (props: any) =>{const [previewFile, setPreviewFile]= useState<any>()

const onTestPdf = () => {
  setPreviewFile({
    fileName: 'abc.pdf',
    fileUrl: 'http://****/abc.pdf'
  })
}
return(<div className="mrUp mrLink">
   <div onClick={onTestPdf}>测试预览PDF</div>
{!!previewFile?.publicFileUrl &&(<PDFPreviewModal
fileName
={previewFile?.fileName}
fileUrl
={previewFile?.publicFileUrl}
onCancel
={() => setPreviewFile('')}/> )}</div> )
}

export
default PdfTest

2、H5移动端的使用

移动端加入放大、缩小、上一页、下一页的功能;

2.1、封装一个组件:PDFViwer.tsx

import React, { useState } from 'react';
import { Button, Modal, Space, Toast, Divider } from
'antd-mobile'import { UpOutline, DownOutline, AddCircleOutline, MinusCircleOutline } from'antd-mobile-icons'import { Document, Page, pdfjs } from'react-pdf';
import
'react-pdf/dist/esm/Page/AnnotationLayer.css'; //样式导入 import 'react-pdf/dist/esm/Page/TextLayer.css' //配置 PDF.js 的 worker 文件 pdfjs.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.js', import.meta.url).toString()

interface PDFPreviewModalProps {
fileUrl: string
| null; //传入的 PDF 文件地址 }

const styleBtnDv
={
display:
'flex',
justifyContent:
'center',
height:
'1rem',
alignItems:
'center',
gap:
'0.4rem',
margin:
'0.3rem 1rem',
padding:
'0 0.6rem',
background:
'#444',
borderRadius:
'0.5rem'}

const styleBtn
={
flex:
1,
display:
'flex',
justifyContent:
'center',
height:
'0.6rem',
alignItems:
'center',
}
//PDF预览功能 const PDFViwer: React.FC<PDFPreviewModalProps> = ({ fileUrl }) =>{
const [pageNumber, setPageNumber]
= useState(1);
const [numPages, setNumPages]
= useState(1);
const [scale, setScale]
= useState(0.65);//当 PDF 加载成功时,设置页面数量 const onDocumentLoadSuccess = ({ numPages }: { numPages: number }) =>{
setNumPages(numPages);
};
//上一页 functionlastPage() {if (pageNumber == 1) {
Toast.show({
content:
'已是第一页'})return;
}
const page
= pageNumber - 1;
setPageNumber(page);
}
//下一页 functionnextPage() {if (pageNumber ==numPages) {
Toast.show(
"已是最后一页");return;
}
const page
= pageNumber + 1;
setPageNumber(page);
}
//缩小 functionpageZoomOut() {if (scale <= 0.3) {
Toast.show(
"已缩放至最小");return;
}
const newScale
= scale - 0.1;
setScale(newScale);
}
//放大 functionpageZoomIn() {if (scale >= 5) {
Toast.show(
"已放大至最大");return;
}
const newScale
= scale + 0.1;
setScale(newScale);
}
return(<div>{/*预览 PDF 文件*/}
{fileUrl
?(<div style={{ height: 'calc(100vh - 4.5rem)', overflowY: 'auto', padding: '24px' }}> <Document//写死的pdf文件地址,用于本地测试使用,打包提交前需要注释掉 //file={new URL("/public/temp/AI销售助手-宽带&套餐&战新.pdf", import.meta.url).toString()} //真实传入的pdf地址 file={fileUrl}
onLoadSuccess
={onDocumentLoadSuccess}> <Page pageNumber={pageNumber} scale={scale} /> </Document> </div> ) : (<p>没有选择文件</p> )}<div style={styleBtnDv}> <div style={styleBtn} onClick={lastPage}><UpOutline color='#fff' fontSize={'0.6rem'} /></div> <div style={{ color: '#fff', fontSize: '0.35rem', ...styleBtn }}>{pageNumber}/{numPages}</div> <div style={styleBtn} onClick={nextPage}><DownOutline color='#fff' fontSize={'0.6rem'} /></div> <div style={styleBtn} onClick={pageZoomIn}><AddCircleOutline color='#fff' fontSize={'0.6rem'} /></div> <div style={styleBtn} onClick={pageZoomOut}><MinusCircleOutline color='#fff' fontSize={'0.6rem'} /></div> </div> </div> );
};

export
default PDFViwer;

2.2、业务代码中引入该组件

import React, { useMemo, useRef, useState } from 'react'import { ErrorBlock, Swiper, SwiperRef, Popup, } from'antd-mobile'import PDFViwer from'@/components/PDFViwer';

const ellipsis1
={"white-space": "nowrap","overflow": "hidden","text-overflow": "ellipsis",
}
const IntroduceDocList = (props: any) =>{
const { loading, introduceDocList }
=props//const introduceDocList = [ //{publicFileUrl: '/public/temp/DXF文件要求.pdf', fileName:'DXF文件要求.pdf'}, //{publicFileUrl: '/public/temp/AI销售助手-宽带&套餐&战新.pdf', fileName:'AI销售助手-宽带&套餐&战新.pdf'}, //]

const [introduceDocList, setIntroduceDocList] = useState({
  {publicFileUrl: 'http://****/abc.pdf', fileName:'abc.pdf'},
{publicFileUrl: 'http://****/def.pdf', fileName:'def.pdf'},
});
const [pdf, setPdf] = useState({ id: 1});
const [showPdfViwer, setShowPdfViwer]
= useState(false)

const onOpenPdfViewer
= (item) =>{
console.log(item);
setPdf(item);
setShowPdfViwer(
true);
}
return( <div>{
introduceDocList
?.map(item =>(<div data-url={item?.publicFileUrl} style={{ marginBottom: '0.3rem', fontSize: '0.4rem' }}> <span style={{color:'#0B75FF'}} onClick={() => onOpenPdfViewer(item)}>{item.fileName}</span> </div> ))
}
<Popup
position
='right'visible={showPdfViwer}
showCloseButton
bodyStyle
={{ width: '100%'}}
destroyOnClose
={true}
onClose
={() =>{
setShowPdfViwer(
false)
setPdf({ id:
1})
}}
> <div style={{ padding: '0.3rem 1rem', fontSize: '0.35rem', fontWeight: 600, textAlign:'center', ...ellipsis1 }}>{pdf?.fileName}</div> <div style={{ height: '100%' }} data-url={pdf?.publicFileUrl}> <PDFViwer fileUrl={pdf?.publicFileUrl} /> </div> </Popup> </div> )
}
export
default IntroduceDocList

效果图:

注意:挡在本地开发时,如果预览的pdf文件地址是线上地址,则会报跨域的问题,需要服务端解决跨域问题。