<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Vant-Weapp on 方糖猫Blog</title>
    <link>https://ftmao.com/blog/tags/vant-weapp/</link>
    <description>AI 视频、AI Agent 与工程实践笔记</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Wed, 16 Aug 2023 09:40:00 +0800</lastBuildDate><atom:link href="https://ftmao.com/blog/tags/vant-weapp/index.xml" rel="self" type="application/rss+xml" /><item>
      <title>微信小程序开发从入门到精通：环境搭建、WXML/WXSS、组件库、架构、平台能力与发布全指南</title>
      <link>https://ftmao.com/blog/posts/wechat-miniprogram-beginner-to-mastery/</link>
      <pubDate>Wed, 16 Aug 2023 09:40:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/wechat-miniprogram-beginner-to-mastery/</guid>
      <description>&lt;h1 id=&#34;微信小程序开发从入门到精通环境搭建wxmlwxss组件库架构平台能力与发布全指南&#34;&gt;微信小程序开发从入门到精通：环境搭建、WXML/WXSS、组件库、架构、平台能力与发布全指南&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;目标读者：想进入小程序开发的前端同学，或者写过 Vue/React、想搞清楚「小程序和网页开发到底差在哪」的同学。&lt;br&gt;
知识骨架与编排方式参考姊妹篇《&lt;a href=&#34;https://ftmao.com/blog/blog/posts/android-beginner-to-mastery/&#34;&gt;Android 开发从入门到精通&lt;/a&gt;》——同样是「由浅入深、踩坑驱动」。写 UI 的心法还可以看《&lt;a href=&#34;https://ftmao.com/blog/blog/posts/frontend-ui-vue-mastery/&#34;&gt;前端 UI 开发从入门到精通（Vue 技术栈）&lt;/a&gt;》，小程序的组件化思想和 Vue 一脉相承。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;全文共八篇，顺序就是学习的顺序——每一篇都依赖前一篇打下的基础：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;一 环境搭建&amp;lt;br/&amp;gt;注册 AppID 跑起 Hello World&amp;#34;] --&amp;gt; B[&amp;#34;二 语法基础&amp;lt;br/&amp;gt;WXML/WXSS/JS/JSON 四件套&amp;#34;]
    B --&amp;gt; C[&amp;#34;三 UI 组件库&amp;lt;br/&amp;gt;写出像样的页面&amp;#34;]
    C --&amp;gt; D[&amp;#34;四 架构与生命周期&amp;lt;br/&amp;gt;从写页面到写应用&amp;#34;]
    D --&amp;gt; E[&amp;#34;五 网络与存储&amp;lt;br/&amp;gt;数据从哪来 存到哪去&amp;#34;]
    E --&amp;gt; F[&amp;#34;六 平台能力&amp;lt;br/&amp;gt;登录 订阅消息 分享 支付&amp;#34;]
    F --&amp;gt; G[&amp;#34;七 质量与发布&amp;lt;br/&amp;gt;性能 体验版 审核上线&amp;#34;]
    G --&amp;gt; H[&amp;#34;八 AI 时代&amp;lt;br/&amp;gt;让 AI 写 80% 的代码&amp;#34;]
    style H fill:#e8f4e8&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;为什么是这个顺序？前三篇解决「&lt;strong&gt;能不能写出来&lt;/strong&gt;」；第四篇解决「&lt;strong&gt;页面多了以后会不会散架&lt;/strong&gt;」；第五、六篇解决「&lt;strong&gt;能不能和后端、和微信这个平台打交道&lt;/strong&gt;」；第七篇解决「&lt;strong&gt;敢不敢提交审核发给真实用户&lt;/strong&gt;」；第八篇解决「&lt;strong&gt;如何持续快下去&lt;/strong&gt;」。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第一篇-环境搭建比-android-友好但坑藏在账号里&#34;&gt;第一篇 环境搭建：比 Android 友好，但坑藏在「账号」里&lt;/h2&gt;
&lt;p&gt;先说好消息：小程序的环境搭建是整个客户端开发里最友好的——不用装 JDK，不用配 SDK，不用 Gradle 转圈。坏消息是：&lt;strong&gt;门槛从「装软件」转移到了「注册账号」&lt;/strong&gt;，而账号相关的坑往往要在提交审核那天才爆出来。&lt;/p&gt;
&lt;h3 id=&#34;11-你需要准备的三样东西&#34;&gt;1.1 你需要准备的三样东西&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[&amp;#34;小程序账号 (AppID)&amp;lt;br/&amp;gt;mp.weixin.qq.com 注册&amp;#34;] --&amp;gt; D[&amp;#34;微信开发者工具&amp;#34;]
    B[&amp;#34;微信开发者工具&amp;lt;br/&amp;gt;官方 IDE 写代码 + 模拟器 + 调试&amp;#34;] --&amp;gt; E[&amp;#34;能跑起 Hello World&amp;lt;br/&amp;gt;= 入门完成一半&amp;#34;]
    C[&amp;#34;微信扫码登录&amp;lt;br/&amp;gt;一个微信号即可&amp;#34;] --&amp;gt; D
    D --&amp;gt; E&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;动手清单：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;注册小程序账号&lt;/strong&gt;：到 &lt;a href=&#34;https://mp.weixin.qq.com&#34;&gt;mp.weixin.qq.com&lt;/a&gt; 选择「小程序」注册，拿到 &lt;strong&gt;AppID&lt;/strong&gt;（在「开发管理 → 开发设置」里）。注意一个邮箱只能绑定一个微信公众产品账号。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;下载微信开发者工具&lt;/strong&gt;：认准&lt;a href=&#34;https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html&#34;&gt;官方页面&lt;/a&gt;，选稳定版（Stable Build）。它是「编辑器 + 模拟器 + 调试器 + 发布工具」四合一，写小程序不需要再装别的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;新建项目&lt;/strong&gt;：填入 AppID，模板选「不使用云服务」的最小模板，后端服务选「不使用模板」。模拟器里看到 Hello World 即通关。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果只是学习不打算发布，也可以在新建项目时选「测试号」，跳过注册——但测试号很多能力（支付、订阅消息、发布）用不了，建议还是注册一个正式的个人账号。&lt;/p&gt;
&lt;h3 id=&#34;12-踩坑故事一类目选错审核排队两周白排&#34;&gt;1.2 踩坑故事一：类目选错，审核排队两周白排&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：朋友的小程序功能全部做完，信心满满提交审核，第二天被拒：「所选类目与小程序实际内容不符」。改类目需要提供《增值电信业务经营许可证》——他没有。推倒重来，换了个人主体账号重新注册，前面所有配置全部作废。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：注册时选择的&lt;strong&gt;服务类目&lt;/strong&gt;和&lt;strong&gt;主体类型&lt;/strong&gt;（个人 / 企业）在注册后很难改，而且直接决定你的小程序能做什么：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    Q{&amp;#34;小程序要做什么？&amp;#34;}
    Q --&amp;gt;|&amp;#34;内容展示 / 工具&amp;lt;br/&amp;gt;不涉及交易&amp;#34;| A[&amp;#34;个人主体即可&amp;lt;br/&amp;gt;注册免费 门槛低&amp;#34;]
    Q --&amp;gt;|&amp;#34;电商 / 需要微信支付&amp;#34;| B[&amp;#34;必须企业主体&amp;lt;br/&amp;gt;+ 对应经营类目&amp;#34;]
    Q --&amp;gt;|&amp;#34;医疗 / 金融 / 社交&amp;#34;| C[&amp;#34;企业主体 + 行业资质&amp;lt;br/&amp;gt;提前查资质清单再动手&amp;#34;]
    style C fill:#ffe4e1&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;教训&lt;/strong&gt;：&lt;strong&gt;先查类目，再写代码&lt;/strong&gt;。微信官方有《小程序开放的服务类目》文档，写清楚每个类目需要什么资质。个人主体不能碰电商交易、不能做陌生人社交——无数个人开发者做到一半才发现「个人号不支持」，这是小程序生态最贵的一个坑，因为它爆在最后一步。&lt;/p&gt;
&lt;h3 id=&#34;13-踩坑故事二不校验合法域名的虚假安全感&#34;&gt;1.3 踩坑故事二：「不校验合法域名」的虚假安全感&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：开发期接口调得好好的，一到真机预览就白屏，控制台报 &lt;code&gt;request:fail url not in domain list&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：小程序的 &lt;code&gt;wx.request&lt;/code&gt; 只能请求在 mp 后台&lt;strong&gt;预先配置过的 HTTPS 域名&lt;/strong&gt;（request 合法域名）。开发者工具里有个「详情 → 本地设置 → 不校验合法域名」的开关，很多人开发期一直勾着它，勾到忘记它的存在——而真机上这个开关不存在（除非打开调试模式）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解法&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;去 mp 后台「开发管理 → 开发设置 → 服务器域名」配置 request 合法域名，&lt;strong&gt;必须 HTTPS，且域名要 ICP 备案&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;开发期就把「不校验合法域名」关掉，让问题暴露在写代码的时候，而不是发给产品经理体验的时候。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;教训&lt;/strong&gt;：小程序的「域名校验」和 Android 的「明文流量拦截」是同一类东西——平台用白名单逼你上 HTTPS。这类安全约束的特点是「先放行以后再收」基本等于永远不收，第一天就把域名配好。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第二篇-语法基础四个文件就是一个页面&#34;&gt;第二篇 语法基础：四个文件就是一个页面&lt;/h2&gt;
&lt;p&gt;环境通了，接下来是语言。小程序的语法对前端来说几乎没有学习成本——它就是「受限版的 Vue」：模板 + 数据绑定 + 指令 + 组件化。这一篇的重点不是语法本身，而是&lt;strong&gt;它和自由 Web 的差异&lt;/strong&gt;，坑全在差异里。&lt;/p&gt;
&lt;h3 id=&#34;21-一个页面--四个文件&#34;&gt;2.1 一个页面 = 四个文件&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[&amp;#34;index.wxml&amp;lt;br/&amp;gt;模板 ≈ HTML&amp;lt;br/&amp;gt;写结构&amp;#34;] --- P[&amp;#34;一个页面&amp;#34;]
    B[&amp;#34;index.wxss&amp;lt;br/&amp;gt;样式 ≈ CSS&amp;lt;br/&amp;gt;多了 rpx 单位&amp;#34;] --- P
    C[&amp;#34;index.js&amp;lt;br/&amp;gt;逻辑：Page({...})&amp;lt;br/&amp;gt;data + 生命周期 + 事件处理&amp;#34;] --- P
    D[&amp;#34;index.json&amp;lt;br/&amp;gt;配置：导航栏标题&amp;lt;br/&amp;gt;引入哪些组件&amp;#34;] --- P
    P --&amp;gt; E[&amp;#34;在 app.json 的 pages 数组里&amp;lt;br/&amp;gt;注册路径才算存在&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;全局还有一组 &lt;code&gt;app.js / app.json / app.wxss&lt;/code&gt;：App 的生命周期、全局配置（页面注册、tabBar、窗口样式）、全局样式。&lt;/p&gt;
&lt;p&gt;一个最小页面长这样：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;!-- pages/index/index.wxml --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;view&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;class&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;container&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;text&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;class&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;title&amp;#34;&lt;/span&gt;&amp;gt;{{title}}&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;text&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;view&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;wx:for&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;{{todos}}&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;wx:key&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;id&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;class&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;todo&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;text&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;class&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;{{item.done ? &amp;#39;done&amp;#39; : &amp;#39;&amp;#39;}}&amp;#34;&lt;/span&gt;&amp;gt;{{item.text}}&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;text&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;view&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;button&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;bindtap&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;onAdd&amp;#34;&lt;/span&gt;&amp;gt;添加&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;button&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;view&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-js&#34; data-lang=&#34;js&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// pages/index/index.js
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;Page&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;title&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;我的待办&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;todos&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      { &lt;span style=&#34;color:#a6e22e&#34;&gt;id&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;text&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;学会 WXML&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;done&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      { &lt;span style=&#34;color:#a6e22e&#34;&gt;id&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;text&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;学会 setData&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;done&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;false&lt;/span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;onAdd&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;todos&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;todos&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;concat&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#a6e22e&#34;&gt;id&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; Date.&lt;span style=&#34;color:#a6e22e&#34;&gt;now&lt;/span&gt;(),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#a6e22e&#34;&gt;text&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;新任务&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#a6e22e&#34;&gt;done&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;false&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// ⚠️ 改数据必须走 setData，直接 this.data.todos = todos 界面不会刷新
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;setData&lt;/span&gt;({ &lt;span style=&#34;color:#a6e22e&#34;&gt;todos&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;22-setdata小程序开发的第一性原理&#34;&gt;2.2 setData：小程序开发的第一性原理&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;小程序的视图和逻辑跑在两个不同的线程&lt;/strong&gt;：视图层是 WebView，逻辑层是 JSCore。你在 JS 里改 &lt;code&gt;data&lt;/code&gt;，要通过 &lt;code&gt;setData&lt;/code&gt; 把数据&lt;strong&gt;序列化后跨线程发给视图层&lt;/strong&gt;，界面才会更新。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;sequenceDiagram
    participant L as 逻辑层 (JSCore)
    participant B as 原生桥 (Native)
    participant V as 视图层 (WebView)
    L-&amp;gt;&amp;gt;B: this.setData({ todos })
    Note over B: 数据序列化传输&amp;lt;br/&amp;gt;大对象 / 高频调用都在这里变慢
    B-&amp;gt;&amp;gt;V: 更新 data
    V-&amp;gt;&amp;gt;V: 重新渲染 WXML&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;这一个架构事实解释了小程序里 80% 的性能问题和 80% 的新手困惑：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;直接赋值 &lt;code&gt;this.data.x = 1&lt;/code&gt; 数据变了但界面不刷新——因为没有经过那座桥。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;setData&lt;/code&gt; 传几十 KB 的大对象会卡——桥上的运输成本。&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;onPageScroll&lt;/code&gt; 里每次滚动都 &lt;code&gt;setData&lt;/code&gt; 会卡成 PPT——桥上堵车。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;现在只需要记住结论：setData 是有成本的，数据要小、频率要低。&lt;/strong&gt; 第七篇会专门讲怎么优化。&lt;/p&gt;
&lt;h3 id=&#34;23-十分钟速览和-vue-的核心差异&#34;&gt;2.3 十分钟速览：和 Vue 的核心差异&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-js&#34; data-lang=&#34;js&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;Page&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;// 1. 数据：必须先声明在 data 里，模板才能用
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; { &lt;span style=&#34;color:#a6e22e&#34;&gt;count&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;list&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; [] },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;// 2. 改数据：setData，支持路径写法
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;onInc&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;setData&lt;/span&gt;({ &lt;span style=&#34;color:#a6e22e&#34;&gt;count&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;count&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// 深层路径：this.setData({ &amp;#39;list[0].done&amp;#39;: true })
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;// 3. 没有计算属性，官方方案是 WXS 或 setData 时算好
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;//    （新语法支持 computed 需引第三方库，见第四篇）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;// 4. 生命周期：onLoad / onShow / onReady / onHide / onUnload
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;onLoad&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;options&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// options 里是页面跳转带来的参数：/pages/detail?id=1 → options.id === &amp;#39;1&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;onShow&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// 每次页面显示都会来（包括从下一页返回），刷新数据的常见位置
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;WXML 指令对照表（会 Vue 的看一眼就够）：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Vue&lt;/th&gt;
					&lt;th&gt;小程序 WXML&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;v-if&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;wx:if&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;v-else&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;wx:else&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;v-for&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;wx:for&lt;/code&gt;，配合 &lt;code&gt;wx:key&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;@click&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;bindtap&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;:src=&amp;quot;url&amp;quot;&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;src=&amp;quot;{{url}}&amp;quot;&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;v-model&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;没有&lt;/strong&gt;，手写 &lt;code&gt;value=&amp;quot;{{x}}&amp;quot; bindinput=&amp;quot;onInput&amp;quot;&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;感悟&lt;/strong&gt;：小程序没有 &lt;code&gt;v-model&lt;/code&gt; 是刻意的——双向绑定在「两线程架构」下是个性能陷阱，官方逼着你显式处理每一次输入。理解这一点，你就理解了小程序 API 设计的整体气质：&lt;strong&gt;宁可啰嗦，不可模糊&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;24-rpx一个单位解决屏幕适配&#34;&gt;2.4 rpx：一个单位解决屏幕适配&lt;/h3&gt;
&lt;p&gt;小程序做了件 Android 和 Web 都很羡慕的事：定义了 &lt;strong&gt;rpx&lt;/strong&gt;（responsive pixel），屏幕宽度恒为 750rpx。设计稿按 750 宽出图，iPhone 6/7/8 上 1rpx = 0.5px，在更宽的机型上等比放大。样式里写 &lt;code&gt;width: 375rpx&lt;/code&gt; 就是「半屏宽」，不用算任何比例。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事三：rpx 用在不该用的地方&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：海报生成页用了 Canvas 手绘，按设计稿把字号写成 28rpx，导出图片在小屏手机上文字糊成一团。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：Canvas 2D 接口、以及 &lt;code&gt;wx.createSelectorQuery&lt;/code&gt; 拿到的尺寸单位都是&lt;strong&gt;物理 px&lt;/strong&gt;，不是 rpx。rpx 只活在 WXSS 里。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解法&lt;/strong&gt;：涉及 Canvas / JS 计算尺寸的地方，先换算：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-js&#34; data-lang=&#34;js&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; { &lt;span style=&#34;color:#a6e22e&#34;&gt;pixelRatio&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;windowWidth&lt;/span&gt; } &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;wx&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;getWindowInfo&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;rpxToPx&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;rpx&lt;/span&gt;) =&amp;gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;rpx&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;windowWidth&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;750&lt;/span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;教训&lt;/strong&gt;：记住 rpx 的管辖范围只有 WXSS。凡是在 JS 里处理尺寸，第一件事就是确认单位。&lt;/p&gt;
&lt;h3 id=&#34;25-踩坑故事四回调里的-this-指向丢了&#34;&gt;2.5 踩坑故事四：回调里的 this 指向丢了&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：&lt;code&gt;wx.request&lt;/code&gt; 的 success 回调里 &lt;code&gt;this.setData(...)&lt;/code&gt; 报错 &lt;code&gt;Cannot read property &#39;setData&#39; of undefined&lt;/code&gt;。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-js&#34; data-lang=&#34;js&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// ❌ 经典错误：普通函数的 this 不是页面实例
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;wx&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;request&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;url&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;https://api.example.com/todos&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;success&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;function&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;res&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;setData&lt;/span&gt;({ &lt;span style=&#34;color:#a6e22e&#34;&gt;todos&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;res&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt; });  &lt;span style=&#34;color:#75715e&#34;&gt;// this 是 undefined
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// ✅ 解法一：箭头函数（词法 this），今天的推荐写法
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;wx&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;request&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;url&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;https://api.example.com/todos&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;success&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;setData&lt;/span&gt;({ &lt;span style=&#34;color:#a6e22e&#34;&gt;todos&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;res&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;教训&lt;/strong&gt;：这是 JavaScript 的老问题，但它是每个小程序新手的「成人礼」。&lt;strong&gt;统一用箭头函数写回调&lt;/strong&gt;，这个问题就绝迹了。顺手把 &lt;code&gt;var that = this&lt;/code&gt; 这种上古写法从代码里清除出去。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第三篇-ui-组件库别从零画按钮&#34;&gt;第三篇 UI 组件库：别从零画按钮&lt;/h2&gt;
&lt;p&gt;语法有了，马上写界面。小程序自带一套基础组件（view、text、button、image、scroll-view、swiper……），但它们是「零件」不是「成品」——没有现成的日历、下拉刷新、商品卡片。这一篇解决两个问题：选什么组件库，以及怎么用它写出一个像样的页面。&lt;/p&gt;
&lt;h3 id=&#34;31-主流组件库怎么选&#34;&gt;3.1 主流组件库怎么选&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    Q{&amp;#34;做什么样的小程序？&amp;#34;}
    Q --&amp;gt;|&amp;#34;电商 / 通用业务&amp;lt;br/&amp;gt;组件要全&amp;#34;| A[&amp;#34;vant-weapp (有赞)&amp;lt;br/&amp;gt;生态最大 文档最细&amp;#34;]
    Q --&amp;gt;|&amp;#34;要腾讯系设计风格&amp;lt;br/&amp;gt;长期维护&amp;#34;| B[&amp;#34;TDesign (腾讯官方)&amp;lt;br/&amp;gt;设计规范完整&amp;#34;]
    Q --&amp;gt;|&amp;#34;京东系 / 多端&amp;#34;| C[&amp;#34;NutUI (京东)&amp;#34;]
    Q --&amp;gt;|&amp;#34;一套代码同时出&amp;lt;br/&amp;gt;小程序 + H5 + App&amp;#34;| D[&amp;#34;跨端框架&amp;lt;br/&amp;gt;Taro / uni-app&amp;lt;br/&amp;gt;⚠️ 见 3.4 节的权衡&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;vant-weapp&lt;/strong&gt;：有赞出品，事实上的生态老大。组件全（60+），电商场景（商品、优惠券、地址编辑）开箱即用，社区问题几乎都能搜到答案。&lt;strong&gt;不知道选什么就选它&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TDesign&lt;/strong&gt;：腾讯官方，设计体系完整，维护稳定。和 vant-weapp 二选一主要看设计稿更接近哪个风格。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NutUI&lt;/strong&gt;：京东出品，风格轻量，多端版本（Taro）做得好。&lt;/li&gt;
&lt;li&gt;基础组件之外值得一提的：&lt;code&gt;wxml-to-canvas&lt;/code&gt;（海报生成）、&lt;code&gt;echarts-for-weixin&lt;/code&gt;（图表）、&lt;code&gt;computed&lt;/code&gt; / &lt;code&gt;mobx-miniprogram&lt;/code&gt;（下一篇的主角）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;引入方式&lt;/strong&gt;（以 vant-weapp 为例）：小程序从 2019 年起支持 npm，但和 Web 的 npm 用法不一样，多一步「构建」：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;npm i @vant/weapp -S&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;然后在开发者工具里点「&lt;strong&gt;工具 → 构建 npm&lt;/strong&gt;」，再在页面的 json 里声明：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;usingComponents&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;van-button&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;@vant/weapp/button/index&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;van-field&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;@vant/weapp/field/index&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;32-踩坑故事五npm-装了组件死活找不到&#34;&gt;3.2 踩坑故事五：npm 装了，组件死活找不到&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：&lt;code&gt;npm install&lt;/code&gt; 成功，&lt;code&gt;usingComponents&lt;/code&gt; 也写了，模拟器报 &lt;code&gt;Component is not found in path &amp;quot;@vant/weapp/button/index&amp;quot;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：小程序构建工具&lt;strong&gt;不直接读 &lt;code&gt;node_modules&lt;/code&gt;&lt;/strong&gt;，需要「工具 → 构建 npm」把它编译成 &lt;code&gt;miniprogram_npm&lt;/code&gt; 目录。装完包没构建、或者 &lt;code&gt;app.json&lt;/code&gt; 同级目录结构不对（&lt;code&gt;project.config.json&lt;/code&gt; 里 &lt;code&gt;miniprogramRoot&lt;/code&gt; 配错），都会撞上这个报错。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解法&lt;/strong&gt;：构建 npm 后重启编译；团队项目把 &lt;code&gt;miniprogram_npm&lt;/code&gt; 加入 git 忽略，但要在 README 里写明「拉代码后先构建 npm」——新人入职第一天栽在这上面的比例高得离谱。&lt;/p&gt;
&lt;h3 id=&#34;33-demo一个五脏俱全的登录页&#34;&gt;3.3 Demo：一个五脏俱全的登录页&lt;/h3&gt;
&lt;p&gt;把数据绑定、事件、vant-weapp 组件串起来——每个小程序都有的页面：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;!-- pages/login/login.wxml --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;view&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;class&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;page&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;van-field&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;value&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;{{form.phone}}&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;label&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;手机号&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;placeholder&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;请输入手机号&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;error-message&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;{{errors.phone}}&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;bind:change&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;onPhoneChange&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;van-field&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;value&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;{{form.code}}&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;label&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;验证码&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;placeholder&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;请输入验证码&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;bind:change&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;onCodeChange&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;van-button&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;slot&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;button&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;size&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;small&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;type&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;primary&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#a6e22e&#34;&gt;disabled&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;{{countdown &amp;gt; 0}}&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#a6e22e&#34;&gt;bindtap&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;onSendCode&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      {{countdown &amp;gt; 0 ? countdown + &amp;#39;s 后重发&amp;#39; : &amp;#39;发送验证码&amp;#39;}}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;van-button&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;van-field&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;van-button&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;type&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;primary&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;block&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;round&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;loading&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;{{loading}}&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;bindtap&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;onLogin&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    登录
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;van-button&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;view&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-js&#34; data-lang=&#34;js&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// pages/login/login.js
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;Page&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;form&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; { &lt;span style=&#34;color:#a6e22e&#34;&gt;phone&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;code&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;&amp;#39;&lt;/span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;errors&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; { &lt;span style=&#34;color:#a6e22e&#34;&gt;phone&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;&amp;#39;&lt;/span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;countdown&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;loading&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;false&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;onPhoneChange&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;e&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// van-field 的值在 e.detail 里
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;setData&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;form.phone&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;e&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;detail&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;errors.phone&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;onCodeChange&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;e&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;setData&lt;/span&gt;({ &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;form.code&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;e&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;detail&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;onSendCode&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#f92672&#34;&gt;!&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;/^1\d{10}$/&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;test&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;form&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;phone&lt;/span&gt;)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;setData&lt;/span&gt;({ &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;errors.phone&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;手机号格式不对&amp;#39;&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;setData&lt;/span&gt;({ &lt;span style=&#34;color:#a6e22e&#34;&gt;countdown&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;60&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;timer&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;setInterval&lt;/span&gt;(() =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;next&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;countdown&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;setData&lt;/span&gt;({ &lt;span style=&#34;color:#a6e22e&#34;&gt;countdown&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;next&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;next&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;) &lt;span style=&#34;color:#a6e22e&#34;&gt;clearInterval&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;timer&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }, &lt;span style=&#34;color:#ae81ff&#34;&gt;1000&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// api.sendSmsCode(this.data.form.phone)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;onLogin&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; { &lt;span style=&#34;color:#a6e22e&#34;&gt;phone&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;code&lt;/span&gt; } &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;form&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#f92672&#34;&gt;!&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;/^1\d{10}$/&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;test&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;phone&lt;/span&gt;)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;setData&lt;/span&gt;({ &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;errors.phone&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;手机号格式不对&amp;#39;&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;setData&lt;/span&gt;({ &lt;span style=&#34;color:#a6e22e&#34;&gt;loading&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// 第五篇会把它换成真正的请求封装
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;setTimeout&lt;/span&gt;(() =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;setData&lt;/span&gt;({ &lt;span style=&#34;color:#a6e22e&#34;&gt;loading&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;false&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#a6e22e&#34;&gt;wx&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;switchTab&lt;/span&gt;({ &lt;span style=&#34;color:#a6e22e&#34;&gt;url&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;/pages/home/home&amp;#39;&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }, &lt;span style=&#34;color:#ae81ff&#34;&gt;800&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;onUnload&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;clearInterval&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;timer&lt;/span&gt;);  &lt;span style=&#34;color:#75715e&#34;&gt;// ⚠️ 页面销毁要清定时器
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;设计要点&lt;/strong&gt;（这些比语法值钱）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;表单状态装进一个 &lt;code&gt;form&lt;/code&gt; 对象&lt;/strong&gt;，更新用 setData 的路径语法（&lt;code&gt;&#39;form.phone&#39;: value&lt;/code&gt;），只传变化的那一小条数据过线程桥——2.2 节的第一性原理在这里第一次兑现。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;按钮的「正常 / 加载中 / 禁用」形态&lt;/strong&gt;由状态驱动，UI 本质是状态机，这在 Vue、Android、小程序里是同一句话。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;定时器在 &lt;code&gt;onUnload&lt;/code&gt; 里清掉&lt;/strong&gt;——页面销毁了定时器还在跑，轻则无效 setData 报错刷屏，重则内存泄漏。这是第四篇生命周期的预演。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;34-原生还是跨端框架一个绕不开的决策&#34;&gt;3.4 原生还是跨端框架？一个绕不开的决策&lt;/h3&gt;
&lt;p&gt;只要做小程序，早晚有人问你：为什么不用 Taro / uni-app，一套代码出五端？我的建议是&lt;strong&gt;按团队和业务的真实情况选&lt;/strong&gt;，而不是按技术情怀：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    Q{&amp;#34;业务分布在哪？&amp;#34;}
    Q --&amp;gt;|&amp;#34;只发微信小程序&amp;lt;br/&amp;gt;且追求极致体验&amp;#34;| A[&amp;#34;原生开发&amp;lt;br/&amp;gt;能力最全 坑最少&amp;lt;br/&amp;gt;新 API 第一天就能用&amp;#34;]
    Q --&amp;gt;|&amp;#34;微信 + 支付宝/抖音多平台&amp;lt;br/&amp;gt;或同时要 H5&amp;#34;| B[&amp;#34;uni-app / Taro&amp;lt;br/&amp;gt;用平台差异最小的子集&amp;#34;]
    Q --&amp;gt;|&amp;#34;团队全是 React 技术栈&amp;#34;| C[&amp;#34;Taro&amp;#34;]
    Q --&amp;gt;|&amp;#34;团队全是 Vue 技术栈&amp;#34;| D[&amp;#34;uni-app&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;踩坑故事六：跨端框架里用了「全平台都支持」的组件，抖音端直接白屏&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：uni-app 项目用了一个官方组件库的级联选择器，微信端、H5 端正常，发布到抖音小程序直接白屏，排查三天发现是该组件内部用了抖音端不支持的一个 API。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;感悟&lt;/strong&gt;：跨端框架解决的是「语法转换」，解决不了「&lt;strong&gt;平台能力差异&lt;/strong&gt;」。微信小程序的能力更新最快（微信自家的新能力当然先给自己），跨端框架追平永远有时间差。选跨端，就要接受「用所有平台能力的最小公约数」这个约束。&lt;strong&gt;小团队单平台起步，先原生，等真的要多端了再迁移，往往比第一天就上跨端框架更划算。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第四篇-架构与生命周期从写页面到写应用&#34;&gt;第四篇 架构与生命周期：从「写页面」到「写应用」&lt;/h2&gt;
&lt;p&gt;前三篇你能写出一个页面了。但真实小程序是几十个页面、被用户以你想象不到的顺序跳转、随时被微信切到后台。这一篇建立组织代码的地基。&lt;/p&gt;
&lt;h3 id=&#34;41-生命周期apppagecomponent-三层&#34;&gt;4.1 生命周期：App、Page、Component 三层&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;stateDiagram-v2
    [*] --&amp;gt; App_onLaunch: 冷启动(整个小程序只此一次)
    App_onLaunch --&amp;gt; Page_onLoad: 打开第一个页面
    Page_onLoad --&amp;gt; Page_onShow: 页面显示
    Page_onShow --&amp;gt; Page_onReady: 首次渲染完成
    Page_onReady --&amp;gt; Page_onHide: 跳转下一页 / 切后台
    Page_onHide --&amp;gt; Page_onShow: 返回本页 / 回到前台
    Page_onShow --&amp;gt; Page_onUnload: 页面被销毁(redirect/navigateBack)
    Page_onUnload --&amp;gt; [*]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;真正值钱的是这三条规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;onLoad&lt;/code&gt; 一生只来一次，&lt;code&gt;onShow&lt;/code&gt; 每次显示都来&lt;/strong&gt;：&lt;code&gt;onLoad&lt;/code&gt; 只在页面创建时执行一次，拿着跳转参数；&lt;code&gt;onShow&lt;/code&gt; 每次显示都来。&lt;strong&gt;「返回时要不要刷新列表」的答案就是把刷新逻辑放 &lt;code&gt;onShow&lt;/code&gt;，而不是假设 &lt;code&gt;onLoad&lt;/code&gt; 会再来&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;onReady&lt;/code&gt; 之前别碰节点&lt;/strong&gt;：&lt;code&gt;wx.createSelectorQuery()&lt;/code&gt; 查 DOM 要等首次渲染完成，放 &lt;code&gt;onLoad&lt;/code&gt; 里查高度会拿到 0。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;小程序没有可靠的「退出回调」&lt;/strong&gt;：用户按胶囊按钮关掉小程序，&lt;code&gt;onUnload&lt;/code&gt; 之后 App 可能还活着，也可能随时被系统回收。重要数据不能指望「退出时再保存」，和 Android 的 &lt;code&gt;onDestroy&lt;/code&gt; 不保证调用是同一个道理。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事七：编辑页保存返回，列表还是旧数据&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：从列表页进编辑页，改完保存返回，列表纹丝不动。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：返回上一页走的是 &lt;code&gt;navigateBack&lt;/code&gt;，上一页的实例&lt;strong&gt;没有被销毁&lt;/strong&gt;，&lt;code&gt;onLoad&lt;/code&gt; 不会再执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解法&lt;/strong&gt;，两种主流姿势：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-js&#34; data-lang=&#34;js&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 解法一：列表页把刷新放 onShow
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;onShow&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;loadList&lt;/span&gt;();   &lt;span style=&#34;color:#75715e&#34;&gt;// 返回时自然刷新，缺点是首次会重复加载，加 flag 控制
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 解法二：编辑页保存后主动通知上一页（事件总线 / getCurrentPages）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;pages&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;getCurrentPages&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;prevPage&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;pages&lt;/span&gt;[&lt;span style=&#34;color:#a6e22e&#34;&gt;pages&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;length&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;prevPage&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;onNeedRefresh&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;?&lt;/span&gt;.();   &lt;span style=&#34;color:#75715e&#34;&gt;// 直接调上一页的方法
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;wx&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;navigateBack&lt;/span&gt;();&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;42-页面栈小程序导航的宪法&#34;&gt;4.2 页面栈：小程序导航的「宪法」&lt;/h3&gt;
&lt;p&gt;小程序的页面不是平铺的，是&lt;strong&gt;一个栈&lt;/strong&gt;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph stack[&amp;#34;页面栈（最多 10 层）&amp;#34;]
        A[&amp;#34;首页&amp;lt;br/&amp;gt;栈底&amp;#34;] --&amp;gt; B[&amp;#34;列表页&amp;#34;] --&amp;gt; C[&amp;#34;详情页&amp;#34;] --&amp;gt; D[&amp;#34;编辑页&amp;lt;br/&amp;gt;栈顶&amp;#34;]
    end
    E[&amp;#34;navigateTo&amp;lt;br/&amp;gt;压栈 保持当前页&amp;#34;] -.-&amp;gt;|&amp;#34;+1 层&amp;#34;| stack
    F[&amp;#34;navigateBack&amp;lt;br/&amp;gt;出栈返回&amp;#34;] -.-&amp;gt;|&amp;#34;-1 层&amp;#34;| stack
    G[&amp;#34;redirectTo&amp;lt;br/&amp;gt;替换栈顶&amp;#34;] -.-&amp;gt;|&amp;#34;层数不变&amp;#34;| stack
    H[&amp;#34;reLaunch&amp;lt;br/&amp;gt;清空重开&amp;#34;] -.-&amp;gt;|&amp;#34;栈重置&amp;#34;| stack
    I[&amp;#34;switchTab&amp;lt;br/&amp;gt;跳 tabBar 页&amp;#34;] -.-&amp;gt;|&amp;#34;非 tabBar 页全部出栈&amp;#34;| stack&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;踩坑故事八：跳转到第十一层，按钮点了没反应&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：一个「猜你喜欢 → 详情 → 相关推荐 → 详情 → …」的商品浏览链路，用户连点十次之后，再点卡片毫无反应，既不跳转也不报错（老基础库静默失败）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：&lt;code&gt;navigateTo&lt;/code&gt; 的页面栈上限是 &lt;strong&gt;10 层&lt;/strong&gt;，到了第 11 次压栈直接失败。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解法&lt;/strong&gt;：自循环的详情链路用 &lt;code&gt;redirectTo&lt;/code&gt;（替换栈顶而不是压栈），或者监测栈深度，满了就 &lt;code&gt;reLaunch&lt;/code&gt; 回首页。这也是「返回键要按十下才能退出」这种体验事故的根源——&lt;strong&gt;栈越深，用户的返回路径越长&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事九：navigateTo 跳 tabBar 页，永远不生效&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：&lt;code&gt;wx.navigateTo({ url: &#39;/pages/home/home&#39; })&lt;/code&gt; 调用了，纹丝不动。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：tabBar 页面不在普通页面栈体系里，必须用 &lt;code&gt;wx.switchTab&lt;/code&gt;；反过来，非 tabBar 页不能用 &lt;code&gt;switchTab&lt;/code&gt;。两类页面的 URL 在 &lt;code&gt;app.json&lt;/code&gt; 里就决定了（&lt;code&gt;tabBar.list&lt;/code&gt; 里的那几个），API 选错是唯一可能的原因。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;教训&lt;/strong&gt;：这条错不报错、静默失败，是新人调试时间最长的坑之一。记住口诀：&lt;strong&gt;tabBar 用 switchTab，栈满用 redirectTo，重启用 reLaunch，其他 navigateTo&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;43-组件化与通信从页面到-component&#34;&gt;4.3 组件化与通信：从页面到 Component&lt;/h3&gt;
&lt;p&gt;页面大了要拆组件。自定义组件也是四文件结构，js 里用 &lt;code&gt;Component({...})&lt;/code&gt; 代替 &lt;code&gt;Page({...})&lt;/code&gt;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-js&#34; data-lang=&#34;js&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// components/price-card/index.js
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;Component&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;options&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;styleIsolation&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;isolated&amp;#39;&lt;/span&gt;,   &lt;span style=&#34;color:#75715e&#34;&gt;// ⚠️ 见踩坑故事十
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;properties&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;price&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; { &lt;span style=&#34;color:#a6e22e&#34;&gt;type&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; Number, &lt;span style=&#34;color:#a6e22e&#34;&gt;value&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt; },      &lt;span style=&#34;color:#75715e&#34;&gt;// 父传子
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; { &lt;span style=&#34;color:#75715e&#34;&gt;/* 组件内部状态 */&lt;/span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;methods&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;onBuy&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;triggerEvent&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;buy&amp;#39;&lt;/span&gt;, { &lt;span style=&#34;color:#a6e22e&#34;&gt;price&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;price&lt;/span&gt; });  &lt;span style=&#34;color:#75715e&#34;&gt;// 子传父：派事件
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;!-- 父页面使用 --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;price-card&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;price&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;{{item.price}}&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;bind:buy&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;onBuy&amp;#34;&lt;/span&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;通信方式的全景：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[&amp;#34;父 → 子&amp;lt;br/&amp;gt;properties&amp;#34;] --- S[&amp;#34;组件通信&amp;#34;]
    B[&amp;#34;子 → 父&amp;lt;br/&amp;gt;triggerEvent 派事件&amp;#34;] --- S
    C[&amp;#34;任意组件 ↔ 全局&amp;lt;br/&amp;gt;mobx-miniprogram 绑定&amp;#34;] --- S
    D[&amp;#34;页面间&amp;lt;br/&amp;gt;EventChannel(navigateTo 时建立)&amp;lt;br/&amp;gt;或事件总线&amp;#34;] --- S&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;踩坑故事十：改组件库样式改不动，自己页面的样式却互相污染&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：给 &lt;code&gt;van-button&lt;/code&gt; 写了个覆盖样式死活不生效；反过来，A 页面给 &lt;code&gt;.title&lt;/code&gt; 写的红色，B 页面的 &lt;code&gt;.title&lt;/code&gt; 也变红了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：小程序的样式隔离规则和直觉相反：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;页面之间&lt;/strong&gt;：样式是全局的！A 页面 WXSS 里的类名会污染 B 页面（同名类）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;组件内部&lt;/strong&gt;：默认有隔离（&lt;code&gt;isolated&lt;/code&gt;），外面的样式进不去，里面的样式出不来——所以改不动组件库。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;解法&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;页面样式一律加页面级命名空间：&lt;code&gt;.page-order .title { ... }&lt;/code&gt;，或者干脆多用组件（组件天然隔离）。&lt;/li&gt;
&lt;li&gt;组件需要接收外部样式时，在 &lt;code&gt;options&lt;/code&gt; 里调 &lt;code&gt;styleIsolation: &#39;apply-shared&#39;&lt;/code&gt;（外能进内不出）或 &lt;code&gt;shared&lt;/code&gt;（双向），改组件库主题时经常要用到。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;44-全局状态mobx-miniprogram-就够了&#34;&gt;4.4 全局状态：mobx-miniprogram 就够了&lt;/h3&gt;
&lt;p&gt;页面多了之后，「用户信息、购物车数量」这类全局状态在几十个页面间传来传去会迅速失控。小程序生态没有 Vuex/Pinia 那样的官方答案，事实标准是 &lt;strong&gt;mobx-miniprogram + mobx-miniprogram-bindings&lt;/strong&gt;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-js&#34; data-lang=&#34;js&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// stores/user.js
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;import&lt;/span&gt; { &lt;span style=&#34;color:#a6e22e&#34;&gt;observable&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;action&lt;/span&gt; } &lt;span style=&#34;color:#a6e22e&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;mobx-miniprogram&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;userStore&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;observable&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;userInfo&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;null&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;get&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;isLoggedIn&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!!&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;userInfo&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;setUserInfo&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;action&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;function&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;info&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;userInfo&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;info&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-js&#34; data-lang=&#34;js&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 页面绑定：store 变了，页面 data 自动同步
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;import&lt;/span&gt; { &lt;span style=&#34;color:#a6e22e&#34;&gt;createStoreBindings&lt;/span&gt; } &lt;span style=&#34;color:#a6e22e&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;mobx-miniprogram-bindings&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;import&lt;/span&gt; { &lt;span style=&#34;color:#a6e22e&#34;&gt;userStore&lt;/span&gt; } &lt;span style=&#34;color:#a6e22e&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;../../stores/user&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;Page&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;onLoad&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;storeBindings&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;createStoreBindings&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;, {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#a6e22e&#34;&gt;store&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;userStore&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#a6e22e&#34;&gt;fields&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;userInfo&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;isLoggedIn&amp;#39;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#a6e22e&#34;&gt;actions&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;setUserInfo&amp;#39;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;onUnload&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;storeBindings&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;destroyStoreBindings&lt;/span&gt;();  &lt;span style=&#34;color:#75715e&#34;&gt;// ⚠️ 记得销毁
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;思考&lt;/strong&gt;：这套写法的心智模型和 Vue 的 Pinia + computed 完全一致——「store 是唯一数据源，页面只是绑定」。同时别忘了给 &lt;code&gt;Component&lt;/code&gt; 配上官方扩展库 &lt;code&gt;computed&lt;/code&gt; 和 &lt;code&gt;watch&lt;/code&gt;（&lt;code&gt;mobx-miniprogram&lt;/code&gt; 同一作者出品），小程序原生组件没有计算属性，手写派生状态是代码腐烂的起点。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第五篇-网络与存储数据从哪来存到哪去&#34;&gt;第五篇 网络与存储：数据从哪来，存到哪去&lt;/h2&gt;
&lt;p&gt;架构定了，现在填数据的肉。这一篇学完，你就能写出「登录 + 联网 CRUD + 本地缓存」的完整小程序了。&lt;/p&gt;
&lt;h3 id=&#34;51-wxrequest先把回调包成-promise&#34;&gt;5.1 wx.request：先把回调包成 Promise&lt;/h3&gt;
&lt;p&gt;小程序的联网 API 是回调风格的 &lt;code&gt;wx.request&lt;/code&gt;，直接用会很快陷入回调地狱（2.5 节那种嵌套套嵌套）。&lt;strong&gt;项目第一天就该做的封装&lt;/strong&gt;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-js&#34; data-lang=&#34;js&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// utils/request.js
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;BASE_URL&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;https://api.example.com&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;function&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;request&lt;/span&gt;({ &lt;span style=&#34;color:#a6e22e&#34;&gt;url&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;method&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;GET&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;needAuth&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt; }) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; Promise((&lt;span style=&#34;color:#a6e22e&#34;&gt;resolve&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;reject&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;wx&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;request&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#a6e22e&#34;&gt;url&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;BASE_URL&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;url&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#a6e22e&#34;&gt;method&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#a6e22e&#34;&gt;timeout&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;10000&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#a6e22e&#34;&gt;header&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;// 统一带 token：小程序没有 Cookie 自动管理，登录态自己维护
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a6e22e&#34;&gt;Authorization&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;needAuth&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;?&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;`Bearer &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;${&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;wx&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;getStorageSync&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;token&amp;#39;&lt;/span&gt;)&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;`&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#a6e22e&#34;&gt;success&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;res&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;res&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;statusCode&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;200&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;res&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;statusCode&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;300&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#a6e22e&#34;&gt;resolve&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;res&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        } &lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;res&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;statusCode&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;===&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;401&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#75715e&#34;&gt;// token 过期：清登录态，跳登录页
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#a6e22e&#34;&gt;wx&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;removeStorageSync&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;token&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#a6e22e&#34;&gt;wx&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;reLaunch&lt;/span&gt;({ &lt;span style=&#34;color:#a6e22e&#34;&gt;url&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;/pages/login/login&amp;#39;&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#a6e22e&#34;&gt;reject&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; Error(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;登录已过期&amp;#39;&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        } &lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#a6e22e&#34;&gt;reject&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; Error(&lt;span style=&#34;color:#a6e22e&#34;&gt;res&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;?&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;message&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;||&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;`请求失败 &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;${&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;res&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;statusCode&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;`&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#a6e22e&#34;&gt;fail&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;// 断网、超时、域名不在白名单都走这里
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a6e22e&#34;&gt;reject&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; Error(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;网络异常，请稍后重试&amp;#39;&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 使用：async/await，和前端写 axios 一个手感
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;async&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;onLoad&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;todos&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;request&lt;/span&gt;({ &lt;span style=&#34;color:#a6e22e&#34;&gt;url&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;/todos&amp;#39;&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;setData&lt;/span&gt;({ &lt;span style=&#34;color:#a6e22e&#34;&gt;todos&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  } &lt;span style=&#34;color:#66d9ef&#34;&gt;catch&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;e&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;wx&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;showToast&lt;/span&gt;({ &lt;span style=&#34;color:#a6e22e&#34;&gt;title&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;e&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;message&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;icon&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;none&amp;#39;&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;设计要点&lt;/strong&gt;：拦截逻辑（带 token、401 处理、错误归一化）写在这一层，业务代码就只剩一行 &lt;code&gt;await&lt;/code&gt;——这和 Android 的 OkHttp 拦截器、前端的 axios 拦截器是同一种分层思想。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事十一：401 了，但页面还停在原地&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：token 过期后接口返回 401，封装的 request 里做了 &lt;code&gt;reLaunch&lt;/code&gt; 跳登录页，但用户反馈「偶尔跳不过去」。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：&lt;code&gt;wx.reLaunch&lt;/code&gt; 跳转登录页时，如果当前页面栈里有 tabBar 页，&lt;code&gt;reLaunch&lt;/code&gt; 的目标 URL 带了 query 参数在部分基础库上被丢弃；更隐蔽的是——&lt;strong&gt;401 时多个并发请求会触发多次 reLaunch&lt;/strong&gt;，第二次 reLaunch 打断了第一次。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解法&lt;/strong&gt;：401 处理加防抖锁：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-js&#34; data-lang=&#34;js&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;isRedirectingToLogin&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;false&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;function&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;handleUnauthorized&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;isRedirectingToLogin&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;isRedirectingToLogin&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;wx&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;removeStorageSync&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;token&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;wx&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;reLaunch&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;url&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;/pages/login/login&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;complete&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; () =&amp;gt; { &lt;span style=&#34;color:#a6e22e&#34;&gt;isRedirectingToLogin&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;false&lt;/span&gt;; },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;移动端特有的提醒&lt;/strong&gt;（小程序同理）：微信用户会进电梯、切 Wi-Fi 和流量，弱网和断线是&lt;strong&gt;常态而不是异常&lt;/strong&gt;。&lt;code&gt;timeout&lt;/code&gt; 必须配、失败要给重试入口、&lt;code&gt;wx.showToast&lt;/code&gt; 报错别弹全屏错误页——静默重试一次，体验天差地别。&lt;/p&gt;
&lt;h3 id=&#34;52-本地存储wxsetstorage-的分寸&#34;&gt;5.2 本地存储：wx.setStorage 的分寸&lt;/h3&gt;
&lt;p&gt;小程序的存储比 Android 简单得多，没有 Room 没有 DataStore，就一个 &lt;code&gt;wx.setStorage / wx.getStorage&lt;/code&gt;（异步）和 &lt;code&gt;wx.setStorageSync / wx.getStorageSync&lt;/code&gt;（同步）：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    Q{&amp;#34;要存什么？&amp;#34;}
    Q --&amp;gt;|&amp;#34;token / 用户设置&amp;lt;br/&amp;gt;小键值对&amp;#34;| A[&amp;#34;wx.setStorageSync&amp;lt;br/&amp;gt;同步 简单&amp;#34;]
    Q --&amp;gt;|&amp;#34;列表缓存 / 草稿&amp;lt;br/&amp;gt;几 KB 以上的数据&amp;#34;| B[&amp;#34;wx.setStorage (异步版)&amp;lt;br/&amp;gt;⚠️ 同步版大数据会卡 UI&amp;#34;]
    Q --&amp;gt;|&amp;#34;要跟随用户换设备&amp;lt;br/&amp;gt;或免后端&amp;#34;| C[&amp;#34;微信云开发&amp;lt;br/&amp;gt;云数据库 + 云函数&amp;#34;]
    Q --&amp;gt;|&amp;#34;敏感数据(密码之类)&amp;#34;| D[&amp;#34;❌ 别存本地&amp;lt;br/&amp;gt;存 token 不存密码&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;两个数字要记住：单个 key 上限 1MB，总上限 10MB。超出直接写入失败。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事十二：storage 同步读写把首页卡死了&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：首页加载越来越慢，Profiling 发现 &lt;code&gt;getStorageSync&lt;/code&gt; 一个调用占了 200ms。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：运营活动的弹窗配置、AB 实验分组、用户信息……十几个模块都往 storage 里塞，有个模块把整份商品列表（几百 KB）也存了进去，首页启动时同步读，主线程原地罚站。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解法&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;大对象读写一律用异步版（&lt;code&gt;wx.getStorage&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;启动时只读「渲染首屏必需」的字段，其余懒加载。&lt;/li&gt;
&lt;li&gt;token 这种小且高频的用同步版没问题——&lt;strong&gt;按数据大小和频率选 API，不是按顺手程度&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;53-云开发没有后端时的另一条路&#34;&gt;5.3 云开发：没有后端时的另一条路&lt;/h3&gt;
&lt;p&gt;如果你是个人开发者、没有自己的服务器，&lt;strong&gt;微信云开发&lt;/strong&gt;（CloudBase）值得单独了解：云数据库（类 MongoDB）、云函数（Node.js）、云存储、云调用（直接调微信 API 免鉴权），在开发者工具里一站式完成，不用买服务器、不用配域名、不用过 ICP 备案——对，&lt;strong&gt;云开发的请求不受 1.3 节域名白名单限制&lt;/strong&gt;，这对个人开发者是巨大优势：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-js&#34; data-lang=&#34;js&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 免鉴权、免域名配置，一行调用
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;db&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;wx&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;cloud&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;database&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;res&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;db&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;collection&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;todos&amp;#39;&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;where&lt;/span&gt;({ &lt;span style=&#34;color:#a6e22e&#34;&gt;done&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;false&lt;/span&gt; }).&lt;span style=&#34;color:#a6e22e&#34;&gt;get&lt;/span&gt;();&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;代价是：数据模型简单、复杂业务（多表事务、复杂查询）不如自建后端灵活，迁移到自有后端的成本不低。&lt;strong&gt;工具类、内容类、Demo 项目用云开发；交易核心、复杂业务自建后端。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第六篇-平台能力登录订阅消息分享支付与分包&#34;&gt;第六篇 平台能力：登录、订阅消息、分享、支付与分包&lt;/h2&gt;
&lt;p&gt;到这里，你的小程序已经「自成一体」了。这一篇让它和微信这个平台打交道。平台能力的坑有一个共同特征：&lt;strong&gt;文档看着简单，但 API 的生命周期变迁极快，照着两年前的教程写必踩坑&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;61-登录code-换-openid-的三段握手&#34;&gt;6.1 登录：code 换 openid 的三段握手&lt;/h3&gt;
&lt;p&gt;小程序登录是平台能力里设计最优雅的一个，也是面试必考题：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;sequenceDiagram
    participant MP as 小程序前端
    participant WX as 微信服务器
    participant S as 你的后端
    MP-&amp;gt;&amp;gt;WX: wx.login() 拿 code(5分钟有效)
    MP-&amp;gt;&amp;gt;S: 把 code 发给自己的后端
    S-&amp;gt;&amp;gt;WX: code + appid + secret 换&amp;lt;br/&amp;gt;openid + session_key
    WX--&amp;gt;&amp;gt;S: openid(用户唯一标识) + session_key
    S--&amp;gt;&amp;gt;MP: 签发自家 token(如 JWT)
    Note over MP: 之后所有请求带 token&amp;lt;br/&amp;gt;session_key 永远不出后端&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;三条铁律&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;session_key&lt;/code&gt; 绝不能下发到前端&lt;/strong&gt;——它能解密用户敏感数据，泄漏等于把用户数据大门敞开。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;code 只能用一次且 5 分钟过期&lt;/strong&gt;，换取动作必须在后端做（secret 也不能进前端代码）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;前端维护的是自家 token&lt;/strong&gt;，不是微信的 code。5.1 节 request 封装里带的 &lt;code&gt;Authorization&lt;/code&gt; 就是它。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事十三：跟着老教程写 getUserProfile，提交审核被拒&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：照着 2021 年的教程用 &lt;code&gt;wx.getUserProfile&lt;/code&gt; 拿用户头像昵称，真机一调返回 &lt;code&gt;getUserProfile:fail can only be invoked by user TAP gesture&lt;/code&gt; 或直接被拒审。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：微信在 2022 年&lt;strong&gt;回收了 &lt;code&gt;getUserProfile&lt;/code&gt;&lt;/strong&gt; 能力。现在获取头像昵称的官方姿势是两个表单组件：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;!-- 头像：button open-type=&amp;#34;chooseAvatar&amp;#34; --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;button&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;open-type&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;chooseAvatar&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;bind:chooseavatar&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;onChooseAvatar&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;image&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;src&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;{{avatarUrl || &amp;#39;/images/default-avatar.png&amp;#39;}}&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;button&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;!-- 昵称：input type=&amp;#34;nickname&amp;#34;，拉起微信输入法半屏 --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;input&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;type&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;nickname&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;placeholder&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;请输入昵称&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;bind:blur&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;onNicknameBlur&amp;#34;&lt;/span&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;教训&lt;/strong&gt;：这是小程序开发最重要的生存技能——&lt;strong&gt;微信 API 的废弃和变更极快，任何「照着教程写」的代码都要去官方文档核对一遍最新版本&lt;/strong&gt;。关注「微信小程序开发者文档」的更新日志，比关注任何公众号都有用。&lt;/p&gt;
&lt;h3 id=&#34;62-订阅消息推送的正确打开方式&#34;&gt;6.2 订阅消息：推送的正确打开方式&lt;/h3&gt;
&lt;p&gt;小程序没有 Android 那种「随便推」的通知系统，触达用户的正道是&lt;strong&gt;订阅消息&lt;/strong&gt;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;用户在小程序里&amp;lt;br/&amp;gt;主动点击按钮&amp;#34;] --&amp;gt; B[&amp;#34;wx.requestSubscribeMessage&amp;lt;br/&amp;gt;弹出订阅授权框&amp;#34;]
    B --&amp;gt; C{&amp;#34;用户怎么选？&amp;#34;}
    C --&amp;gt;|&amp;#34;允许&amp;#34;| D[&amp;#34;后端在服务通知里&amp;lt;br/&amp;gt;下发一次消息&amp;#34;]
    C --&amp;gt;|&amp;#34;拒绝&amp;#34;| E[&amp;#34;⚠️ 同一个模板&amp;lt;br/&amp;gt;短期内不能再弹&amp;#34;]
    C --&amp;gt;|&amp;#34;勾选『总是保持以上选择』&amp;#34;| F[&amp;#34;以后静默按上次选择执行&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;关键认知&lt;/strong&gt;：订阅消息默认是&lt;strong&gt;一次性的&lt;/strong&gt;——用户点一次允许，你只能推一次。长期订阅仅对政务、医疗等特定类目开放。所以正确的产品设计是「&lt;strong&gt;在最关键的动作上请求订阅&lt;/strong&gt;」（下单、预约、打卡），而不是见缝插针地弹。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事十四：订阅消息发出去了，用户没收到&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;按出现概率排查：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;模板 ID 不对&lt;/strong&gt;：模板在 mp 后台「订阅消息」里申请，&lt;code&gt;tmplIds&lt;/code&gt; 写错或用了测试模板，发送直接报错。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键词格式不匹配&lt;/strong&gt;：模板里的 &lt;code&gt;{{thing1.DATA}}&lt;/code&gt;（字符串）、&lt;code&gt;{{amount2.DATA}}&lt;/code&gt;（金额）每种类型有严格格式限制，金额必须是数字、字符数超限都会发送失败——&lt;strong&gt;失败原因要去查发送接口的返回码，客户端无感知&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户没订阅 / 订阅次数用完了&lt;/strong&gt;：一次订阅只够推一次，推完额度清零。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跳转页面未发布&lt;/strong&gt;：消息配置的 &lt;code&gt;page&lt;/code&gt; 路径必须是&lt;strong&gt;已发布版本里存在的页面&lt;/strong&gt;，配了还在开发中的页面，用户点消息会跳到错误页。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;63-分享与裂变小程序的增长引擎&#34;&gt;6.3 分享与裂变：小程序的增长引擎&lt;/h3&gt;
&lt;p&gt;小程序生于社交，分享能力是它的原生优势：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-js&#34; data-lang=&#34;js&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 页面级分享：定义了才有「转发」入口的内容
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;Page&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;onShareAppMessage&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#a6e22e&#34;&gt;title&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;这个记账小程序太好用了&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#a6e22e&#34;&gt;path&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;/pages/home/home?from=share&amp;amp;uid=&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;uid&lt;/span&gt;,  &lt;span style=&#34;color:#75715e&#34;&gt;// ⚠️ 路径带参做裂变归因
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#a6e22e&#34;&gt;imageUrl&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;/images/share-cover.png&amp;#39;&lt;/span&gt;,  &lt;span style=&#34;color:#75715e&#34;&gt;// 5:4 比例，自定义封面
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    };
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;应知应会的分寸：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;分享无法拿到「是否分享成功」的回调了&lt;/strong&gt;（早年有，已回收）——「分享成功就送优惠券」这种强制裂变做不了，这也是微信的刻意设计。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;朋友圈&lt;/strong&gt;：小程序不能直接分享到朋友圈（除了特定的「分享到朋友圈」Beta 能力且仅 Android），正道是生成海报图让用户自己发——3.1 节的 &lt;code&gt;wxml-to-canvas&lt;/code&gt; 就是干这个的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;裂变归因全靠 path 参数&lt;/strong&gt;：谁分享的、从哪进来的，都编码在 &lt;code&gt;onShareAppMessage&lt;/code&gt; 的 path 里，落地页在 &lt;code&gt;onLoad(options)&lt;/code&gt; 里解析。这套机制就是小程序增长黑客的全部基础。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;64-支付五步流程前后端各管一半&#34;&gt;6.4 支付：五步流程，前后端各管一半&lt;/h3&gt;
&lt;p&gt;微信支付是平台能力里链条最长的，画出来再写代码：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;sequenceDiagram
    participant MP as 小程序
    participant S as 你的后端
    participant WX as 微信支付
    MP-&amp;gt;&amp;gt;S: 1. 创建订单(商品 金额)
    S-&amp;gt;&amp;gt;WX: 2. 统一下单(用商户号密钥签名)
    WX--&amp;gt;&amp;gt;S: prepay_id
    S--&amp;gt;&amp;gt;MP: 3. 返回支付参数(二次签名)
    MP-&amp;gt;&amp;gt;WX: 4. wx.requestPayment 拉起收银台
    WX--&amp;gt;&amp;gt;MP: 用户输密码完成
    WX--&amp;gt;&amp;gt;S: 5. ⚠️ 支付结果异步回调(这才是到账依据)&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;踩坑故事十五：以前端回调为准发货，被刷单薅了羊毛&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：&lt;code&gt;wx.requestPayment&lt;/code&gt; 的 &lt;code&gt;success&lt;/code&gt; 回调里直接通知后端「支付成功，发货」，上线一个月被恶意用户用篡改的客户端刷出大量未付款订单。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：前端的一切回调都不可信。唯一可信的到账凭证是&lt;strong&gt;微信支付服务器推送给你后端的异步回调（pay 通知）&lt;/strong&gt;，验签通过才算数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;教训&lt;/strong&gt;：这条规则在 Android 内购、Stripe 支付里一模一样——&lt;strong&gt;客户端回调只负责 UI 表现，服务端回调才负责业务状态&lt;/strong&gt;。凡是涉及钱的状态变更，永远以服务端到服务端的通知为准。&lt;/p&gt;
&lt;p&gt;另外记住 1.2 节的连锁反应：虚拟商品（课程、会员）在 &lt;strong&gt;iOS 端不能走微信支付&lt;/strong&gt;（苹果规则），要么引导安卓/H5 支付，要么走小程序虚拟支付通道并接受抽成——类目和主体在注册那天就决定了你有没有资格碰这块。&lt;/p&gt;
&lt;h3 id=&#34;65-分包加载2mb-限制的解法&#34;&gt;6.5 分包加载：2MB 限制的解法&lt;/h3&gt;
&lt;p&gt;小程序主包上限 &lt;strong&gt;2MB&lt;/strong&gt;（总包 20MB），所有页面塞一个包很快就会撞线。答案是分包：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// app.json
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;pages&amp;#34;&lt;/span&gt;: [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;pages/home/home&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;pages/login/login&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;subPackages&amp;#34;&lt;/span&gt;: [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;root&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;packageOrder&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;pages&amp;#34;&lt;/span&gt;: [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;pages/list/list&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;pages/detail/detail&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;root&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;packageUser&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;pages&amp;#34;&lt;/span&gt;: [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;pages/profile/profile&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;pages/setting/setting&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;preloadRule&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;pages/home/home&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;network&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;wifi&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;packages&amp;#34;&lt;/span&gt;: [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;packageUser&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[&amp;#34;主包 ≤ 2MB&amp;lt;br/&amp;gt;首页 + tabBar 页 + 公共代码&amp;#34;] --&amp;gt;|&amp;#34;用户点进订单模块&amp;#34;| B[&amp;#34;订单分包&amp;lt;br/&amp;gt;按需下载&amp;#34;]
    A --&amp;gt;|&amp;#34;Wi-Fi 下预加载&amp;#34;| C[&amp;#34;个人中心分包&amp;#34;]
    style A fill:#e8f4e8&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;设计要点&lt;/strong&gt;：首页和 tabBar 页必须在主包（启动就加载）；低频功能（设置、关于、客服）果断拆分包；&lt;code&gt;preloadRule&lt;/code&gt; 让「首页加载完、Wi-Fi 下静默下载个人中心包」，用户点过去秒开。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第七篇-质量与发布从能跑到敢上线&#34;&gt;第七篇 质量与发布：从「能跑」到「敢上线」&lt;/h2&gt;
&lt;p&gt;功能做完只是上半场。小程序的发布比 Android 多了一道「平台审核」，这是它最独特的一关。&lt;/p&gt;
&lt;h3 id=&#34;71-性能三板斧setdata分包图片&#34;&gt;7.1 性能三板斧：setData、分包、图片&lt;/h3&gt;
&lt;p&gt;小程序性能问题的 90% 集中在这三件事上：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一板斧：setData 瘦身&lt;/strong&gt;（2.2 节第一性原理的兑现）&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[&amp;#34;页面卡顿&amp;#34;] --&amp;gt; B{&amp;#34;setData 传的什么？&amp;#34;}
    B --&amp;gt;|&amp;#34;整个大列表&amp;#34;| C[&amp;#34;改成路径更新&amp;lt;br/&amp;gt;&amp;#39;list[3].done&amp;#39;: true&amp;#34;]
    B --&amp;gt;|&amp;#34;高频调用&amp;lt;br/&amp;gt;onPageScroll / 倒计时&amp;#34;| D[&amp;#34;合并 + 节流&amp;lt;br/&amp;gt;攒一批一次发&amp;#34;]
    B --&amp;gt;|&amp;#34;传了视图用不到的字段&amp;#34;| E[&amp;#34;data 里只放渲染需要的数据&amp;lt;br/&amp;gt;其余存 this 普通属性&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-js&#34; data-lang=&#34;js&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// ❌ 每秒 setData 整个对象
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;setData&lt;/span&gt;({ &lt;span style=&#34;color:#a6e22e&#34;&gt;form&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;form&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// ✅ 只传变化的字段
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;setData&lt;/span&gt;({ &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;form.phone&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;value&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// ✅ 不参与渲染的数据根本别进 data
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;Page&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; { &lt;span style=&#34;color:#a6e22e&#34;&gt;list&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; [] },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;onLoad&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;_rawResponse&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;null&lt;/span&gt;;  &lt;span style=&#34;color:#75715e&#34;&gt;// 内部缓存，挂 this 上不挂 data
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;第二板斧：分包&lt;/strong&gt;——6.5 节已经讲过，启动速度的第一优化项。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三板斧：图片&lt;/strong&gt;——列表图一律压缩到展示尺寸的 2 倍以内、用 &lt;code&gt;image&lt;/code&gt; 组件的 &lt;code&gt;lazy-load&lt;/code&gt;、CDN 域名配进 downloadFile 白名单。首屏图太多时，骨架屏比 loading 转圈的感知快一倍。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事十六：倒计时把列表页卡成 PPT&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：团购列表每个卡片都有「距结束 xx:xx」倒计时，页面滑动掉帧严重。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：20 个卡片各自开了 &lt;code&gt;setInterval&lt;/code&gt;，每秒 20 次 &lt;code&gt;setData&lt;/code&gt; 跨线程传输——桥堵了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解法&lt;/strong&gt;：&lt;strong&gt;一个全局定时器&lt;/strong&gt;，每秒算一次所有卡片的剩余时间，合并成一次 &lt;code&gt;setData&lt;/code&gt;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-js&#34; data-lang=&#34;js&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 一个 timer 驱动全部卡片
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;timer&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;setInterval&lt;/span&gt;(() =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;updates&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {};
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;list&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;forEach&lt;/span&gt;((&lt;span style=&#34;color:#a6e22e&#34;&gt;item&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;i&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;updates&lt;/span&gt;[&lt;span style=&#34;color:#e6db74&#34;&gt;`list[&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;${&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;i&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;].remainText`&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;formatRemain&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;item&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;endTime&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;setData&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;updates&lt;/span&gt;);   &lt;span style=&#34;color:#75715e&#34;&gt;// 每秒只有这一次过桥
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}, &lt;span style=&#34;color:#ae81ff&#34;&gt;1000&lt;/span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;72-调试三板斧vconsole真机调试与体验版&#34;&gt;7.2 调试三板斧：vConsole、真机调试与体验版&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;开发者工具&lt;/strong&gt;：模拟器 + Wxml 面板（看最终渲染树）+ AppData 面板（实时看每个页面的 data，排查 setData 问题的第一现场）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;真机调试&lt;/strong&gt;：开发版小程序右上角胶囊菜单里打开「开发调试」，vConsole 会浮在页面上——手机上看到 console.log、网络请求、storage 内容。&lt;strong&gt;真机和模拟器的差异比你想的大&lt;/strong&gt;：字体渲染、安全区、性能、个别 API 行为都不一样，发布前必须真机过一遍主流程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;体验版&lt;/strong&gt;：上传代码后在 mp 后台设为体验版，添加体验成员（微信号），扫码即用。它是「发给产品和测试同学」的标准姿势，比 Android 装 APK 优雅得多。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事十七：体验版打不开，群里一片「白屏了」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：上传体验版后，自己手机正常，测试同学扫码一直白屏。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;排查路径&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对方的微信号加进「体验成员」了吗？&lt;/li&gt;
&lt;li&gt;你上传后&lt;strong&gt;设为体验版&lt;/strong&gt;了吗？（上传 ≠ 自动成为体验版，要在版本管理里手动选）&lt;/li&gt;
&lt;li&gt;对方是不是打开了「调试模式」导致走了别的版本？（体验版打开调试后会加载开发版代码，版本就乱了）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;教训&lt;/strong&gt;：小程序有「开发版 / 体验版 / 线上版」三个版本共存，版本切换的开关藏在各处。发体验版给别人之前，用一个不在体验成员名单里的微信号扫一次——&lt;strong&gt;模拟用户的真实路径&lt;/strong&gt;，这一分钟能省一小时的群聊排查。&lt;/p&gt;
&lt;h3 id=&#34;73-审核与发布小程序独有的最后一关&#34;&gt;7.3 审核与发布：小程序独有的最后一关&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;开发者工具&amp;lt;br/&amp;gt;上传代码&amp;#34;] --&amp;gt; B[&amp;#34;mp 后台&amp;lt;br/&amp;gt;提交审核&amp;#34;]
    B --&amp;gt; C{&amp;#34;审核结果&amp;lt;br/&amp;gt;几小时到几天&amp;#34;}
    C --&amp;gt;|&amp;#34;通过&amp;#34;| D[&amp;#34;手动点『发布』&amp;lt;br/&amp;gt;全量上线&amp;#34;]
    C --&amp;gt;|&amp;#34;拒绝&amp;#34;| E[&amp;#34;按驳回原因改&amp;lt;br/&amp;gt;重新排队&amp;#34;]
    D --&amp;gt; F[&amp;#34;灰度发布&amp;lt;br/&amp;gt;按比例放量 可回退&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;提高一次过审率的实战清单：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;类目和内容对齐&lt;/strong&gt;（1.2 节）：电商功能挂企业类目，个人号别碰交易。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;给用户「不登录也能逛」的路径&lt;/strong&gt;：审核员会拒绝「打开就强制授权手机号」的小程序。登录应该发生在「需要身份的那个动作」上（下单、收藏），而不是启动时。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;测试账号&lt;/strong&gt;：审核表单里提供测试账号密码或免密通道，审核员进不去你的核心流程必拒。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;敏感功能自审&lt;/strong&gt;：UGC 内容要有举报/审核机制说明；涉及医疗、金融的回答要有资质上传。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;感悟&lt;/strong&gt;：审核这道关常被骂，但它塑造了小程序生态和 App 生态的根本不同——&lt;strong&gt;小程序是「寄居」在微信里的，平台既是分发渠道也是监管者&lt;/strong&gt;。接受这个设定，把审核标准当作产品设计的一部分（登录时机、隐私协议、类目选择），而不是发布前的拦路虎，你的过审率会高得多。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第八篇-ai-时代怎么少写一半代码把小程序做出来&#34;&gt;第八篇 AI 时代：怎么少写一半代码把小程序做出来&lt;/h2&gt;
&lt;p&gt;前七篇是「内功」，这一篇是「兵器」。我的结论和 Android 篇一致：&lt;strong&gt;AI 没有让小程序开发变简单，它让「懂行的人」快了 3-5 倍，让「不懂行的人」更快地写出能跑但烂尾的代码&lt;/strong&gt;。区别在于你怎么用它。&lt;/p&gt;
&lt;h3 id=&#34;81-ai-在小程序开发各环节的胜任度&#34;&gt;8.1 AI 在小程序开发各环节的胜任度&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph good[&amp;#34;AI 做得极好 放心交出去&amp;#34;]
        A1[&amp;#34;单页面骨架&amp;lt;br/&amp;gt;wxml + wxss + Page({...})&amp;#34;]
        A2[&amp;#34;表单页 / 列表页&amp;lt;br/&amp;gt;配 vant-weapp 组件&amp;#34;]
        A3[&amp;#34;request 封装 / 工具函数&amp;#34;]
        A4[&amp;#34;单元测试与类型定义&amp;#34;]
    end
    subgraph mid[&amp;#34;AI 能做 但你要盯着&amp;#34;]
        B1[&amp;#34;跨页面状态设计&amp;#34;]
        B2[&amp;#34;性能优化(setData/分包)&amp;#34;]
        B3[&amp;#34;组件库主题定制&amp;#34;]
    end
    subgraph bad[&amp;#34;AI 做不好 还得靠人&amp;#34;]
        C1[&amp;#34;类目/资质/审核问题&amp;lt;br/&amp;gt;(政策天天变)&amp;#34;]
        C2[&amp;#34;真机才出现的 bug&amp;lt;br/&amp;gt;安全区 键盘 滚动&amp;#34;]
        C3[&amp;#34;微信 API 的最新变更&amp;lt;br/&amp;gt;(训练数据滞后)&amp;#34;]
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;第三格要特别说：微信 API 的变更速度（6.1 节 getUserProfile 的故事）恰好打在 AI 的软肋上——训练数据里全是旧 API 的示例。AI 给你写 &lt;code&gt;wx.getUserInfo&lt;/code&gt; 时你要能一眼认出「这是 2021 年的写法」。&lt;/p&gt;
&lt;h3 id=&#34;82-实战工作流需求--小程序的-ai-协作五步法&#34;&gt;8.2 实战工作流：需求 → 小程序的 AI 协作五步法&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;第一步：先写「项目宪法」，再让 AI 写代码。&lt;/strong&gt; 在项目里放一个 &lt;code&gt;AGENTS.md&lt;/code&gt;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-markdown&#34; data-lang=&#34;markdown&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;# 项目约定
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; 原生小程序（不用跨端框架），组件库 @vant/weapp，less 写样式
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; 页面四文件结构：wxml/wxss/js/json，页面样式加命名空间防污染
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; 全局状态用 mobx-miniprogram + mobx-miniprogram-bindings，组件配 computed 扩展
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; 网络统一走 utils/request.js 封装，async/await，业务代码禁止裸 wx.request
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; setData 只传变化字段的路径写法；不参与渲染的数据挂 this 普通属性
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; 跳转口诀：tabBar 用 switchTab，自循环链路用 redirectTo，其他 navigateTo
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; 回调一律箭头函数；定时器/绑定在 onUnload 里清理
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- 登录态：wx.login 拿 code 给后端换 token，前端只存 token，禁存 session_key&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;注意这份清单里的每一条，都对应着前面某一篇的某个坑——&lt;strong&gt;项目宪法本质上是踩坑笔记的浓缩&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二步：页面级生成，而不是小程序级生成。&lt;/strong&gt; 不要对 AI 说「给我写一个记账小程序」，要说「按项目约定写一个记账首页：顶部本月支出汇总卡片，下面是账单列表，用 van-cell，下拉刷新，每条显示分类图标、备注、金额」。一次一个页面，AI 的输出质量和你的可控性都最高。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三步：让它先写数据结构和状态，再写 UI。&lt;/strong&gt; 先生成页面的 &lt;code&gt;data&lt;/code&gt; 结构、接口契约、状态流转，review 通过后再写 wxml——这正是第四、五篇的架构顺序。状态设计错了，UI 写得再漂亮也是白写。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第四步：报错直接喂回去，但分清两类问题。&lt;/strong&gt; 模拟器里的 JS 报错、组件找不到这类&lt;strong&gt;代码级问题&lt;/strong&gt;，贴给 AI 循环两三次基本能解；但 1.2 节那种&lt;strong&gt;账号类目问题&lt;/strong&gt;、6.1 节那种 &lt;strong&gt;API 已废弃问题&lt;/strong&gt;、真机上才出现的白屏——AI 帮不了，它没有你的 mp 后台权限，也没有真机。这就是为什么 AI 篇必须排在最后：&lt;strong&gt;它是放大器，放大的是你前七篇的水平&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第五步：人工 review 的五个必查点。&lt;/strong&gt; AI 生成的小程序代码，我每次都只细查五处：① &lt;code&gt;setData&lt;/code&gt; 是不是整个大对象往里塞（性能重灾区）；② 定时器和 store 绑定有没有在 &lt;code&gt;onUnload&lt;/code&gt; 里清理（泄漏来源）；③ 跳转 API 用对没有（tabBar 页 navigateTo 的静默失败 AI 经常犯）；④ 请求有没有走统一封装、token 处理对不对；⑤ 用的微信 API 是不是已经废弃的版本。其他代码粗看即可——&lt;strong&gt;review 的精力要花在 AI 的已知短板上&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;83-一个真实对比demo-小程序的工时变化&#34;&gt;8.3 一个真实对比：Demo 小程序的工时变化&lt;/h3&gt;
&lt;p&gt;第三篇那个登录页 + 一个待办列表页的小程序 Demo，三年前我要写一整天（查 API、调样式、处理状态）。2026 年用 AI 的实际耗时：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;pie title 一个双页面 Demo 的 2 小时去哪了
    &amp;#34;写 AGENTS.md 和提示词&amp;#34; : 20
    &amp;#34;AI 生成 + 我 review 修改&amp;#34; : 55
    &amp;#34;AppID/类目/域名配置(AI帮不上)&amp;#34; : 25
    &amp;#34;真机测试与细节打磨&amp;#34; : 20&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;注意那张饼图里最大的启示：&lt;strong&gt;省下来的时间没有消失，它转移到了「描述需求」和「审查代码」上&lt;/strong&gt;，还有一部分转移到了 AI 永远碰不到的「平台事务」上——注册、类目、域名、审核。AI 时代的核心能力从「打字速度」变成了三样：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;把模糊需求拆成精确技术任务的能力&lt;/strong&gt;——你对第四篇架构的理解越深，给 AI 的指令就越准。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;快速判断代码对错的能力&lt;/strong&gt;——看不懂 setData 成本的人，review 不了 AI 写的小程序代码。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;搞定平台事务的能力&lt;/strong&gt;——类目、资质、审核、API 变更，这些「和微信打交道」的事，一件都外包不出去。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;84-思考ai-是放大器不是替代品&#34;&gt;8.4 思考：AI 是放大器，不是替代品&lt;/h3&gt;
&lt;p&gt;小程序生态和 AI 有一个有趣的共同点：它们都在「消灭记忆型知识的价值」。小程序当年消灭了「会写 Objective-C/Java 才能做移动端」的门槛；AI 今天消灭了「背得出 API 参数才配写代码」的门槛。留下来的价值是同一种东西——&lt;strong&gt;判断型知识&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这个状态该放 &lt;code&gt;data&lt;/code&gt; 还是挂 &lt;code&gt;this&lt;/code&gt; 上？&lt;/li&gt;
&lt;li&gt;这个跳转该用 &lt;code&gt;redirectTo&lt;/code&gt; 还是 &lt;code&gt;navigateTo&lt;/code&gt;？&lt;/li&gt;
&lt;li&gt;这个需求个人主体能不能做，会不会死在审核？&lt;/li&gt;
&lt;li&gt;AI 写的这段代码用的 API，是不是去年就被回收了？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以这篇文章前七篇的内容，在 AI 时代不但没有过时，反而更重要了——它们是你在 AI 写完代码后，能说「对」或者「不对」的全部依据。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;结语学习路线收个尾&#34;&gt;结语：学习路线收个尾&lt;/h2&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;第 1 周&amp;lt;br/&amp;gt;注册 AppID + 跑通模板&amp;lt;br/&amp;gt;+ 四件套语法&amp;#34;] --&amp;gt; B[&amp;#34;第 2-3 周&amp;lt;br/&amp;gt;vant-weapp 组件库&amp;lt;br/&amp;gt;写 5 个静态页面&amp;#34;]
    B --&amp;gt; C[&amp;#34;第 1-2 个月&amp;lt;br/&amp;gt;生命周期 + request 封装&amp;lt;br/&amp;gt;+ 登录流程&amp;lt;br/&amp;gt;写一个联网 CRUD 小程序&amp;#34;]
    C --&amp;gt; D[&amp;#34;第 2-4 个月&amp;lt;br/&amp;gt;订阅消息/分享/支付/分包&amp;lt;br/&amp;gt;体验版 审核 发布上线&amp;#34;]
    D --&amp;gt; E[&amp;#34;持续&amp;lt;br/&amp;gt;把 AI 变成&amp;lt;br/&amp;gt;你的初级工程师&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;三条反直觉的建议作为结尾：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;第一周死磕账号不丢人&lt;/strong&gt;。类目、主体、域名、备案，这些「和微信打交道」的事才是真正的门槛，写代码反而是最简单的部分。这不是你笨，是这个生态的真实成本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第一天就用真机预览&lt;/strong&gt;。模拟器掩盖了安全区、键盘、性能和版本差异问题——你的用户分布在几千种机型的微信里，而小程序的真机预览只要扫个码，这是整个客户端开发里成本最低的真机测试，没有借口不用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;让 AI 写代码，让自己写笔记&lt;/strong&gt;。每踩一个坑记下「现象、根因、下次怎么一眼认出」，这些笔记是 AI 时代你不可被替代的资产——因为 AI 知道所有答案，但它不知道&lt;strong&gt;微信上个月又改了哪个 API，以及你的审核上次为什么被拒&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;祝你的 setData 永远轻盈，审核永远一次过，小程序在 iPhone SE 和折叠屏上都一样流畅。&lt;/p&gt;
</description>
    </item>
    
  </channel>
</rss>
