2024年9月

1、概述

1.1 什么是Subject Alternative Name(证书主体别名)

SAN(Subject Alternative Name) 是 SSL 标准 x509 中定义的一个扩展。
它允许一个证书支持多个不同的域名。通过使用SAN字段,可以在一个证书中指定多个DNS名称(域名)、IP地址或其他类型的标识符,这样证书就可以同时用于多个不同的服务或主机上。
这种灵活性意味着企业不需要为每个域名单独购买和安装证书,从而降低了成本和复杂性。

先来看一看 Google 是怎样使用 SAN 证书的,下面是 Youtube 网站的证书信息:

这里可以看到这张证书的 Common Name 字段是 *.google.com,那么为什么这张证书却能够被 www.youtube.com 这个域名所使用呢。原因就是这是一张带有 SAN 扩展的证书,下面是这张证书的 SAN 扩展信息:


这里可以看到,这张证书的 Subject Alternative Name 段中列了一大串的域名,因此这张证书能够被多个域名所使用。对于 Google 这种域名数量较多的公司来说,使用这种类型的证书能够极大的简化网站证书的管理。

1.2 SAN的由来

在早期的互联网中,每个SSL/
TLS证书通常只包含一个CN字段,用于标识单一的域名或IP地址。随着虚拟主机技术的发展和企业对于简化管理的需求增加,需要一种机制能够允许单个证书有效地代表多个域名或服务。例如,一个企业可能拥有多个子域名,希望用单一的证书来保护它们。

为了解决这个问题,SAN扩展被引入到X.509证书标准中。最初在1999年的RFC 2459中提出,SAN提供了一种方法来指定额外的主题名称,从而使得一个证书能有效地代表多个实体。

1.3 SAN的作用和重要性

  1. 多域名保护:SAN使得一个证书可以保护多个域名和子域名,减少了管理的复杂性和成本。
  2. 灵活性增强:企业和组织可以更灵活地管理和部署证书,根据需要快速调整和扩展保护范围。
  3. 兼容性:随着技术的发展,现代浏览器和客户端软件都已经支持SAN。它们会优先检查SAN字段,如果找到匹配项,通常不会再回退到检查CN。

1.4 如何使用SAN

在申请SSL/TLS证书时,可以指定一个或多个SAN值。这些值通常是你希望证书保护的域名或IP地址。证书颁发机构(CA)在颁发证书时会验证这些信息的正确性,并将它们包含在证书的SAN字段中。

2、如何在OpenSSL证书中添加SAN

在OpenSSL中创建证书时添加SAN,需要在配置文件中添加一个subjectAltName扩展。这通常涉及到以下几个步骤:

  1. 准备配置文件:在配置文件中指定subjectAltName扩展,并列出要包含的域名和IP地址。
  2. 生成密钥:使用OpenSSL生成私钥。
  3. 生成证书签发请求(CSR):使用私钥和配置文件生成CSR,该CSR将包含SAN信息。
  4. 签发证书:使用CA(证书颁发机构)或自签名方式签发证书,证书中将包含SAN信息。

2.1 准备带有SAN扩展的证书请求配置文件

  • 创建带有SAN扩展字段的证书签名请求(CSR)的配置文件, alt_names 的配置段为SAN扩展字段配置。确保在将其保存至文件(如csr.conf)。

[ req ]  
default_bits = 2048  
prompt = no  
default_md = sha256  
req_extensions = v3_ext  
distinguished_name = dn  
  
[ dn ]  
C = CN  
ST = ShangDong
L = SZ  
O = Wise2c  
OU = Wise2c  
CN = zmc  
  
[ req_ext ]  
subjectAltName = @alt_names  
  
[ alt_names ]  
DNS.1 = *.zmcheng.com  
DNS.2 = *.zmcheng.net  
DNS.3 = *.zmc.com  
DNS.4 = *.zmc.net  
  
[ v3_ext ]  
basicConstraints=CA:FALSE  
keyUsage=keyEncipherment,dataEncipherment  
extendedKeyUsage=serverAuth,clientAuth  
subjectAltName=@alt_names

注意 1:SAN扩展字段不仅可以配置域名,还可以配置邮箱、IP地址、URI。

// Subject Alternate Name values. (Note that these values may not be valid
// if invalid values were contained within a parsed certificate. For
// example, an element of DNSNames may not be a valid DNS domain name.)
DNSNames       []string
EmailAddresses []string
IPAddresses    []net.IP // Go 1.1
URIs           []*url.URL // Go 1.10

2.2 生成密钥

  • 生成密钥位数为 2048 的 ca.key

openssl genrsa -out ca.key 2048
  • 依据 ca.key 生成 ca.crt (使用 -days 参数来设置证书有效时间):

openssl req -x509 -new -nodes -key ca.key -subj "/CN=zmc" -days 10000 -out ca.crt
  • 生成密钥位数为 2048 的 server.key

openssl genrsa -out server.key 2048

2.3 生成证书签发请求

  • 基于配置文件生成证书签名请求

openssl req -new -key server.key -out server.csr -config csr.conf

2.4 签发证书

  • 使用 ca.key、ca.crt 和 server.csr 生成服务器证书

openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \-CAcreateserial -out server.crt -days 10000 \-extensions v3_ext -extfile csr.conf
  • 查看证书,可以看到创建出带有SAN扩展字段证书

openssl x509  -noout -text -in ./server.crt

3、客户端验证服务端证书步骤

客户端(一般是浏览器)在请求HTTPS网站时,验证服务端证书的过程确实是一个复杂且关键的安全步骤。以下是以浏览器为例,说下浏览器验证服务端证书步骤:

  1. 发起HTTPS请求:
    • 用户通过浏览器输入HTTPS网站的URL,并按下回车键或点击链接,浏览器开始发起HTTPS请求。
  2. 服务端响应并发送证书:
    • 服务器在收到HTTPS请求后,会将其SSL/TLS证书发送给浏览器。这个证书包含了服务器的公钥、证书颁发机构(CA)的信息、证书的有效期、以及可能包括的SAN(主题备用名称)和CN(通用名称)等字段。
  3. 浏览器验证证书颁发机构(CA):
    • 浏览器会检查证书是否由受信任的CA签发。浏览器内置了一个受信任的CA列表(也称为根证书列表),它会使用这些CA的公钥来验证证书链中每一级证书签名的有效性,直到找到信任的根证书。
  4. 检查证书有效期:
    • 浏览器会验证证书的有效期,确保当前时间在证书的有效期内。
  5. 验证证书域名

    • 浏览器会检查证书中的SAN字段(如果存在)和CN字段,确保它们与用户正在访问的域名相匹配。如果域名不匹配,浏览器将认为证书无效,并显示警告信息。
  6. 检查证书吊销状态(可选):
    • 浏览器可能会通过OCSP(在线证书状态协议)或CRL(证书吊销列表)来检查证书是否已被吊销。这一步是可选的,但有助于及时发现并阻止使用已泄露私钥的证书。
  7. 生成会话密钥(TLS握手过程的一部分):
    • 如果证书验证通过,浏览器和服务器将进行TLS握手,以协商一个安全的会话密钥。这个密钥将用于后续通信的加密和解密。
  8. 加密通信:
    • 一旦会话密钥协商完成,浏览器和服务器就可以开始加密通信了。浏览器会使用会话密钥加密发送给服务器的数据,而服务器则使用相同的会话密钥解密这些数据。

注意事项

  • 如果浏览器在验证证书的过程中发现任何问题(如证书不受信任、已过期、域名不匹配等),它将向用户显示警告信息,并可能阻止用户继续访问该网站。
  • SAN字段的优先级通常高于CN字段。如果证书中同时包含了SAN和CN字段,并且它们之间的域名不一致,浏览器将优先使用SAN字段中的域名进行验证。
  • 浏览器内置的受信任CA列表可能会随着时间的推移而更新,以反映新的CA或撤销旧的CA。因此,用户应该保持其浏览器和操作系统的更新,以确保其能够识别最新的受信任CA。

以上步骤详细描述了浏览器在请求HTTPS网站时验证服务端证书的过程。这个过程确保了用户与服务器之间的通信是安全、可信的。

4、示例——Kubectl客户端访问KubeApiserver

4.1 环境

KubeApiserver地址:10.20.32.205:6443

KubeApiserver证书信息:

[root@member-cluster2-master1 pki]# pwd
/etc/kubernetes/pki
[root@member-cluster2-master1 pki]# ls
apiserver.crt  apiserver-kubelet-client.crt  ca.crt  front-proxy-ca.crt  front-proxy-client.crt  sa.key
apiserver.key  apiserver-kubelet-client.key  ca.key  front-proxy-ca.key  front-proxy-client.key  sa.pub
[root@member-cluster2-master1 pki]# openssl x509  -noout -text -in ./apiserver.crt 
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 495742113187184862 (0x6e13ad34cb940de)
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN = kubernetes
        Validity
            Not Before: Sep 11 01:13:49 2024 GMT
            Not After : Aug 18 01:16:09 2124 GMT
        Subject: CN = kube-apiserver
       .......

            X509v3 Subject Alternative Name: 
                DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local, DNS:lb.zmc.local, DNS:localhost, DNS:member-cluster2-master1, DNS:member-cluster2-master1.cluster.local, IP Address:10.234.0.1, IP Address:10.20.32.205, IP Address:127.0.0.1
    Signature Algorithm: sha256WithRSAEncryption
       .......

4.2 Kubectl正常访问KubeApiserver(证书SAN扩展字段值包含访问地址)

  • 通过KubeApiserver ip地址访问

  • 通过域名主机映射访问,域名在SAN里面

4.3 Kubectl异常访问KubeApiserver(证书SAN扩展字段值不包含访问地址)

  • 通过域名主机映射访问,域名不在SAN里面,客户端验证证书失败。

5、其他——KubeApiserver证书SAN扩展字段是否要把整个集群IP都列上

第一次接触Kubernetes并安装集群时,网上翻了很多博客,在生成KubeApiserver证书环节,都写着要把整个待安装集群的ip地址都写到KubeApiserver证书请求文件的SAN扩展字段里面,也没讲什么原因。

至于在生成KubeApiserver证书环节是否要把整个集群的ip地址都写到KubeApiserver证书请求文件的SAN扩展字段里面,还是要看集群里面的组件如何访问KubeApiserver,如果:

  • 调度器(kube-scheduler)、控制器(kube-controller-manage)都是直接和当前节点的kube-apiserver组件直接交互;
  • kubelet、kubectl、kube-proxy通过VIP代理到kube-apiserver组件。

对于以上这种情况,SAN扩展字段可以只写VIP(负责均衡器IP等)、MasterIPs即可,WorkerIP没必要写到SAN扩展字段里面。

6、结论

Subject Alternative Name(主体别名)是一个证书扩展字段,用于指定证书所适用的主机名列表。当客户端连接到服务器时,会检查服务器返回的证书中的主体别名是否包含与请求的主机名匹配的条目。

SAN的引入极大地增强了数字证书的功能和应用范围,使得管理和保护多域名环境变得更加高效和简便。作为现代网络安全的一个关键组成部分,理解并正确使用SAN对于任何需要部署SSL/TLS保护的个人或组织都至关重要。随着网络环境的不断演变和新需求的不断出现,SAN将继续发挥其在保护网络通信安全中的重要作用。无论是IT专业人员还是普通用户,都应该了解SAN的基本概念和实践,以确保在数字世界中安全地通信和交互。

我的博客地址:
如何避免旧请求的数据覆盖掉最新请求 - 蚊子的前端博客

在检索的场景中,经常会对同一个接口发起不同的检索条件的请求,若前一个请求响应较慢时,可能会覆盖掉我们后发起请求的结果。

如我们先发起一个搜索请求,参数是 A;这个请求还没结束,我们发起了参数是 B 的搜索请求;可能因网络原因或者后端服务处理等原因,后发起的参数 B 的请求先响应了,然后我们把数据展示到页面中;过一会儿之前发起参数是 A 的搜索请求也返回结果了。但实际上,参数 B 的响应结果,才是我们需要展示的最新的数据。

那么如何避免这种现象呢?

1. 请求锁定

可以对发起请求的按钮、输入框等,或者是在全局中,添加 loading,只有得到上一个请求的响应结果后,才取消 loading,才允许用户发起下一次的请求。

const App = () => {
  const [loading, setLoading] = useState(false);

  const request = async (data) => {
    if (loading) {
      // 若请求还没结束,则无法发起新的请求
      return;
    }
    setLoading(true);
    const result = await axios("/api", { data, method: "post" });
    setLoading(false);
  };

  return (
    <div className="app">
      <Form disabled={loading} onFinish={request}>
        <Form.Item>
          <Input />
        </Form.Item>
        <Button htmlType="submit" loading={loading}>
          搜索
        </Button>
      </Form>
    </div>
  );
};

直接用源头进行控制,根本不存在多个请求并行的情况,也就无所谓谁覆盖谁了。

2. 防抖

一般应用在纯输入框的搜索功能中,在用户停止输入一段时间后,才发起搜索,可以加大两次检索请求之间的时间间隔.

const App = () => {
  const request = async () => {};

  return (
    <div className="app">
      {/* 停止输入700ms后触发请求 */}
      <input onInput={debounce(request, 700)} />
    </div>
  );
};


防抖
措施并不能完全杜绝数据被覆盖。假如上一次的请求确实很慢,那也会出现覆盖后续请求的现象。

3. 取消上次的请求

当需要发起新的请求,上次的请求还没结束时,可以取消上次请求。

const App = () => {
  const cancelSouceRef = useRef(null);

  const request = async () => {
    if (cancelSouceRef.current) {
      // 若存在上次的请求还没结束,则手动取消
      cancelSouceRef.current.cancel("手动取消上次的请求");
    }
    const source = axios.CancelToken.source();
    cancelSouceRef.current = source;

    try {
      const response = await axios.get("/api/data", {
        cancelToken: source.token,
      });
      setData(response.data);
    } catch (error) {
      if (axios.isCancel(error)) {
        console.log("请求被取消", error.message);
      } else {
        console.log("请求出错", error.message);
      }
    } finally {
      cancelSouceRef.current = null;
    }
  };

  return (
    <div className="app">
      <button onClick={request}>请求</button>
    </div>
  );
};

如果服务端已接收到了请求,不论前端是否取消了请求,服务端都会完整查询并返回,只是浏览器不再处理而已。

4. 时序控制

我们在每次请求时,都给该请求一个唯一标识,并在该组件的全局中保存最新的标识,若响应时的标识表示最新的标识,则不处理该响应的结果。

标识只要在当前组件中唯一即可,如自增数字、时间戳、随机数等,都可以。

const App = () => {
  const requestIdRef = useRef(0); // 保存最新的请求id

  const request = async () => {
    requestIdRef.current++; // 每次自增

    const curRequestId = requestIdRef.current;
    try {
      const response = await axios.get("/api/data", {
        cancelToken: source.token,
      });
      if (curRequestId < requestIdRef.current) {
        // 当前请求不是最新的,不做任何处理
        return;
      }
      setData(response.data);
    } catch (error) {
      console.error(error);
    }
  };

  return (
    <div className="app">
      <button onClick={request}>请求</button>
    </div>
  );
};

这是一种比较简单有效,同时能让用户任意搜索的方案。

5. 总结

当然,在实际中,肯定也是多方案的组合。比如纯输入框触发搜索的场景中,一般是
防抖+时序控制
的两种方案的组合,既能减少触发请求的次数,又能避免数据的相互覆盖。

有的同学可能想到「控制请求的并发数量」,用
队列

递归
等方式,每次将发起的请求都放到队列的后面,然后按照队列的顺序发起请求。如我们之前曾经在文章
JavaScript 中的 Promise 异步并发控制
探讨过这种场景。

这种方式倒也能解决问题,不过有种「杀鸡用牛刀」的感觉。因为在现在的场景中,对同一个接口发起多次请求时,其实我们更关心的是最新请求的结果,之前请求的结果直接可以扔掉了。

欢迎关注我的公众号:前端小茶馆。

前端小

在日新月异的 IT 行业中,每隔数年乃至数月,便会涌现出革新性的技术或前沿框架,引领行业潮流。

比如前端开发,我刚开始工作时,大部分都是静态页面+JavaScript,页面上只有一些简单的交互。

后来出现了
Ajax
技术和
JQuery
库,现在想起当年第一次使用
JQuery
时,真的觉得这就是前端库的终点。

结果没过几年,就兴起了
MVC/MVVM
框架,随之而来的就是
AngularJS

EmberJS
,这时,突然就觉得
JQuery
没有那么香了。

AngularJS

EmberJS
还没闹明白,
React
又横空出世,紧接着就是
Vue
。。。

再看后端开发,早期我主要使用
.NET
,或者用一些
Java
,不过那时候后端语言和框架不是那么被重视,

更多的精力都是放在数据库上,尤其是 Oracle,项目上还有
DBA
的角色,专门负责处理数据库的问题,

那时的应用基本都是单机的。

后来,随着应用规模的扩大,性能问题逐渐显现,开始引入缓存技术(Memcached,Redis),

同时,异步编程和多线程技术也开始被广泛应用。

这时候,各种 Web 框架也如雨后春笋般不断崛起,知名的比如 Spring,Django,Rails 等等。

再后来,互联网应用飞速发展,单机应用开始显得笨重且难以维护和扩展。

云服务,微服务,
Docker
成为主流,持续集成和持续部署(
CI/CD
)流程也被广泛采用,

数据安全和隐私保护的重要性也日益增加,后端开发在安全性和认证方面的要求也越来越高。

其他 IT 领域也类似,随着硬件的发展和互联网累计的数据量到达一定规模之后,神经网络,深度学习和强化学习让机器学习领域飞速发展,AI 真正开始走向普通人的生活中。

总之,我们一直处于技术迭代的循环中。

1. 选择通才还是专家?

像 IT 这样迭代如此之快的行业绝无仅有,那么,在技术的世界里,我们应该专注于一个领域还是尝试很多领域?

也就是说,我们应该成为
通才
(拥有广泛的知识,无论有用与否)还是成为
专家
(致力于一个主题或一个特定的分支)呢?

回答这个问题之前,首先了解一下通才和专家具体有什么不同。

1.1. 通才

通才
指的是那些勇于尝试、对广泛领域保持探索精神的人。

在技术的广阔天地里,他们不仅精通某一专业领域,更在多个领域内拥有深厚的知识积累。

这类人往往不会局限于单一的职业路径,而是倾向于在其职业生涯中跨越不同的领域,不断寻求新的挑战与成长。

这样的人能够改变世界。

著名的通才包括像史蒂夫·乔布斯和埃隆·马斯克这样的天才,他们擅长创新新事物。

通才不仅仅是在他们感兴趣的每一件事上都表现出色,他们还能够将解决一个问题时获得的知识,其应用于不同但相关的问题上。

当他们学会了某个领域的基本知识后,就会把这个知识用在之后接触的每个新领域里。这就是首席技术官的工作范围能横跨多种技术和不同领域的原因。

他们会用自己多年积累的见识和经验去应对每一个新挑战,同时还会不断学些新出现的技术。

通才专注于解决问题,而不是某个具体的技术。

1.2. 专家

相反,专家则展现出高度的专注力。

他们致力于深耕细作,在某一特定领域内稳步前行,犹如手持明灯的引路人。

专家们热衷于全面掌握某一领域的所有知识,他们孜孜不倦地研究、实验与学习,以追求更高的专业造诣和更深的理解。

绝大多数技术进展归功于那些在各自领域深耕细作的专家。

他们专注于机器学习(ML)、网络技术(Web)、移动技术、基础设施、中间件以及其他各类技术领域的开发与优化,凭借多年累积的专业知识与经验,推动了这些领域的持续发展与创新。

专家需系统掌握核心基础知识,并通过在特定领域内长期深耕,方能触及并精通该领域的高级课题。

成就斐然的背后,是无可替代的辛勤努力与积累,无捷径可循,比如各种编程语言,框架的发明者,机器学习领域的各位先驱等等。

各学科的重大发现与进步,很大程度上仰赖于这些领域专家的贡献与推进。

软件专家长期以来运用一套稳定的技术栈,在自身领域内积累了深厚的知识,并对外部进展保持洞察。

他们通过不断努力,成为了行业内的权威人士,同时积极寻求和实践创新的方法论。

不过,软件专家并非要求个体局限于单一技术领域,他们也可以自由探索其他技术领域。

2. 个人建议

我自己的感觉是
通才

专家
属于两个极端,作为一个普通人(包括我自己),我的选择是介于两者之间。

如果对某个技术领域非常感兴趣,那么可以花时间去掌握该领域的各个方面,

但不要限制自己,不要排斥接触其他相关领域。

比如,在
github
,我们可以看到很多前端高手,前端的知识和经验已经非常丰富了,也会去学习
Rust
,然后用 Rust 来开发提高前端开发效率的工具。

还有很多机器学习领域的高手,他们也会学习前端的技术,为自己的大模型制作交互界面,让更多的人能够使用大模型。

选择学习什么技术的时候,有两个很重要的因素值得我们好好参考,就是
兴趣

经验

兴趣
虽然有助于我们缩小选择的技术领域,但最好是尝试不同的技术领域之后,再决定自己的真正的兴趣。

比如,不要因为第一个工作接触的是前端,发现前端也挺有意思,就把自己的兴趣定在前端。

决定兴趣之前,多接触几种不同的领域,尝试在不同的领域做一些小工具玩玩,不同担心学了没用或者浪费时间,

想想通才,任何在其他领域中学到的技能都不会浪费。

其次,
经验
是另一个帮助我们做决定的重要因素。

要成为专家,需要花上好多年的时间去积累经验。如果你已经在某个领域里干了好些年,往专家方向发展通常是个不错的选择。

但是,如果你刚起步或者工作中本来就横跨了几个领域,那么,选择成为通才也许更有意义。

最后,有一个忠告,千万不要仅仅因为困难就放弃成为专家,转而成为通才。

因为通才一点也不比专家简单,通才不是“万事通”,更像是多个领域的专家。

总之,千万不要东搞搞西弄弄,一碰到难题就怂,然后又跑去折腾别的,并且自我安慰“我就是兴趣广泛,更适合做个通才!”。

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文件地址是线上地址,则会报跨域的问题,需要服务端解决跨域问题。

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


传统网站
前端开发的同学初次接触
小程序
,会有许多
困惑
:为什么没有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"]
    }
  }
}