Skip to content
返回

小程序

目录

点击展开

小程序

小程序是很容易入门和掌握的技术栈,如果你技术栈偏窄,可以考虑补充一下小程序的知识。

小程序双线程架构

参考答案

::: details

1. 架构组成

(1)逻辑层(Service)

(2)渲染层(View)

(3)系统层(Native)

wxyl

:::

直接修改 this.data 为何不会触发视图更新?

参考答案

::: details

小程序中直接修改 this.data 不会触发视图更新的原因如下:

1. 数据更新机制的设计

小程序采用 显式更新 策略,只有通过 this.setData() 方法修改数据时,才会触发以下流程:

直接修改 this.data 仅改变逻辑层的数据,但 未触发上述流程,因此渲染层无法感知数据变化。

2. 双线程架构的限制

小程序的逻辑层(Service)与渲染层(View)运行在独立线程中:

两者通过 异步通信(JSBridge)传递数据。

直接修改 this.data 不会触发系统层的数据传递,导致渲染层无法同步更新。

3. 性能优化考量

若每次数据修改都自动触发更新:

通过 this.setData()批量合并更新机制,可优化性能:

// 合并多次更新,仅触发一次通信和渲染
this.setData({ a: 1 })
this.setData({ b: 2 })
// 等效于
this.setData({ a: 1, b: 2 })

4. 数据一致性与安全性

:::

setData 底层做了哪些性能优化处理?

参考答案

::: details

1. 核心优化机制

(1) 数据通信优化

(2) 更新调度优化

(3) 渲染层优化

(4) 通信协议优化

:::

this.setData({ list: largeDataArray }) 有问题吗?

参考答案

::: details

在小程序开发中,使用 this.setData({ list: largeDataArray }) 传递一个大型数据数组(尤其是包含成千上万条数据时)确实存在明显的性能问题

1. 核心问题分析

(1) 数据传输瓶颈

(2) 渲染性能问题

(3) 频繁 GC(垃圾回收)

2. 优化方案

(1) 分页加载(懒加载)

(2) 虚拟列表(按需渲染)

(3) 纯数据字段(Pure Data)

(4) 数据压缩

(5) WebWorker 计算

// 在 Worker 中处理数据
const worker = wx.createWorker('workers/data-handler.js')
worker.postMessage({ action: 'filter', data: largeDataArray })
worker.onMessage((res) => {
  this.setData({ list: res.filteredData })
})

(6) 原生组件替代

:::

小程序登录流程

参考答案

::: details

wxlogin

说明

之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。

注意事项

特殊字段

【注意】同一个用户在不同的小程序中拥有不同的openid。因此,在开发小程序时,不能使用openid来进行用户的唯一性判断。

【注意】用户的 unionid 只有在用户将多个应用绑定到同一个微信开放平台账号下时才会生成。因此,如果用户没有绑定多个应用,那么小程序将无法获取用户的 unionid。

【注意】每个 code 只能使用一次,且有效期为 5 分钟。因此,在使用 code 进行登录时,需要及时将其转换成用户的 openid 和 session_key 等信息,以免出现 code 过期的情况。

:::

如何在不发版的情况下实现小程序的 AB 测试?

参考答案

::: details

在小程序中实现无需发版的 AB 测试,可通过 动态配置 + 数据驱动 的方案完成。

  1. 云端配置管理
  1. 客户端分组逻辑
// 工具函数:一致性哈希分流
function getABGroup(experimentId, userId) {
  const hash = crypto
    .createHash('md5')
    .update(experimentId + userId)
    .digest('hex')
  const value = parseInt(hash.slice(0, 8), 16) % 100
  return value < 50 ? 'group_a' : 'group_b' // 按比例分配
}

// 小程序启动时获取配置
wx.cloud.callFunction({
  name: 'getABConfig',
  success: (res) => {
    const userId = getApp().globalData.userId
    const group = getABGroup(res.data.experiment_id, userId)
    this.setData({ abParams: res.data.groups.find((g) => g.name === group).params })
  },
})
  1. 界面动态渲染
<!-- WXML 根据配置渲染 -->
<button style="background-color: {{abParams.button_color}};" bindtap="handleClick">立即购买</button>
  1. 数据埋点上报
// 点击事件处理
handleClick() {
  wx.reportAnalytics('button_click', {
    experiment_id: '2023_button_color',
    group: this.data.abGroup,
    button_color: this.data.abParams.button_color
  });
}
  1. 数据分析阶段

关键技术细节

  1. 流量分配算法
  1. 动态更新策略
  1. 灰度发布控制
# 云配置示例:分阶段放量
rollout:
  - stage: 1
    percentage: 10%
    start_time: 2025-01-01
  - stage: 2
    percentage: 100%
    start_time: 2025-01-03

:::

小程序的增量更新

参考答案

::: details

小程序的增量更新机制主要依赖于小程序平台的设计

:::

小程序性能优化

参考答案

::: details

微信 IDE 的小程序评分功能位于调试器 -> Audits 面板中

小程序性能优化的具体维度:

  1. 避免过大的 WXML 节点数目
  2. 避免执行脚本的耗时过长的情况
  3. 避免首屏时间太长的情况
  4. 避免渲染界面的耗时过长的情况
  5. 对网络请求做必要的缓存以避免多余的请求
  6. 所有请求的耗时不应太久
  7. 避免 setData 的调用过于频繁
  8. 避免 setData 的数据过大
  9. 避免短时间内发起太多的图片请求
  10. 避免短时间内发起太多的请求

:::

小程序 WXSS 与 CSS 的区别?

参考答案

::: details

:::

小程序里拿不到 dom 相关的 api ?

参考答案

::: details

微信小程序使用类似 Web 的 WXML 和 WXSS 语言来描述页面结构和样式,但并不提供直接操作 DOM 的 API。这主要有两个原因:

:::

分包加载

参考答案

::: details

小程序分包加载是一种优化技术,用于解决主包体积过大导致的首次加载性能问题。通过将非核心功能模块拆分为独立分包,实现按需加载和动态加载。

一、分包加载核心概念

  1. 包类型
类型说明特点
主包包含启动页面、核心公共组件和基础库用户首次打开小程序时必须下载
普通分包依赖主包的功能模块,按需加载可访问主包资源
独立分包不依赖主包的完整功能模块,可独立运行无法访问主包资源
  1. 体积限制
包类型最大体积总包体积限制
主包2MB20MB (所有分包总和)
单个普通分包2MB
单个独立分包2MB

二、分包配置实现

  1. 目录结构
├── app.js               # 主包入口
├── app.json             # 分包配置
├── subpackages          # 分包目录
   ├── user-center      # 普通分包
   ├── pages
   └── components
   └── shop             # 独立分包
       ├── app.js       # 独立分包入口
       └── pages
└── common               # 公共代码(主包)
  1. app.json 配置
{
  "pages": ["pages/index/index"], // 主包页面
  "subpackages": [
    {
      "root": "subpackages/user-center",
      "name": "user",
      "pages": ["profile", "settings"],
      "independent": false // 普通分包
    },
    {
      "root": "subpackages/shop",
      "name": "shop",
      "pages": ["home", "detail"],
      "independent": true // 独立分包
    }
  ],
  "preloadRule": {
    "pages/index/index": {
      "network": "all",
      "packages": ["user"] // 预加载普通分包
    }
  }
}

三、分包加载策略

  1. 按需加载
  1. 预加载优化
// app.json 预加载配置
"preloadRule": {
  "pages/index/index": {
    "packages": ["user"],    // 预加载分包名
    "network": "wifi"        // 仅WiFi下预加载
  }
}

策略建议

  1. 懒加载配合
// 点击事件触发加载
onTapShop() {
  require('../../subpackages/shop/shop.js'); // 动态导入
  wx.navigateTo({ url: '/subpackages/shop/pages/home' });
}

:::

冷启动与热启动的区别

参考答案

::: details

核心区别对比

对比维度冷启动热启动
触发条件首次打开或销毁后重新打开后台存活状态下重新唤醒
资源加载重新下载代码包、初始化页面直接从内存恢复页面
启动速度较慢(需完整加载)较快(无需重新初始化)
生命周期流程执行 App.onLaunchPage.onLoad仅触发 App.onShowPage.onShow
内存占用重新分配内存复用原有内存
数据状态全局数据需重新初始化保留之前的运行状态
存活时间无限制默认后台存活 5分钟
典型场景用户首次打开或主动杀死进程后重启切换回微信聊天后重新进入

冷启动优化方案

  1. 代码包瘦身

    • 主包控制在 2MB 以内
    • 使用分包加载(单个分包 ≤2MB)
    // app.json 分包配置
    {
      "subpackages": [{
        "root": "subpackage",
        "pages": ["pageA", "pageB"]
      }]
    }
  2. 预加载策略

    // 提前加载非首屏必要资源
    wx.loadSubpackage({
      name: 'subpackage',
      success: () => console.log('分包预加载完成'),
    })
  3. 缓存关键数据

    // 冷启动时读取缓存
    App({
      onLaunch() {
        const cache = wx.getStorageSync('userInfo')
        if (cache) this.globalData.userInfo = cache
      },
    })

热启动优化方案

  1. 状态保持

    // 页面隐藏时保存状态
    Page({
      onHide() {
        wx.setStorageSync('pageState', this.data)
      },
    })
  2. 内存管理

    • 避免在全局对象中存储过大数据
    • 及时清理无用定时器/事件监听
    // 页面卸载时清理资源
    Page({
      onUnload() {
        clearInterval(this.timer)
        this.eventListener.close()
      },
    })
  3. 后台保活策略

    // 播放背景音频延长存活时间
    wx.playBackgroundAudio({
      dataUrl: 'silent.mp3', // 无声音频
    })

异常场景处理

场景冷启动表现热启动表现
代码更新强制下载新包下次冷启动生效
网络中断可能导致白屏已加载内容仍可操作
内存不足正常启动可能被系统回收转为冷启动
全局数据变更重新初始化保持最后一次修改值

调试技巧

  1. 强制冷启动

    // 开发阶段模拟冷启动
    wx.reLaunch({ url: '/pages/index' })
  2. 内存状态检查

    // 查看当前内存占用
    console.log(wx.getPerformance())
    // 输出: { memory: 1024, ... }
  3. 生命周期追踪

    // 监听所有生命周期事件
    const originalOnShow = Page.prototype.onShow
    Page.prototype.onShow = function () {
      console.log('Page.onShow triggered')
      originalOnShow.call(this)
    }

通过理解冷/热启动的差异,开发者可针对性优化小程序性能,建议将 冷启动耗时控制在 1.5 秒内热启动耗时控制在 300 毫秒内,以达到最佳用户体验。

:::

组件通信方案

参考答案

::: details

一、父子组件通信

  1. 父 → 子:Properties 传递
// 父组件
;<child-comp prop-data="{{parentData}}" />

// 子组件 properties 定义
Component({
  properties: {
    propData: { type: Object, value: {} },
  },
})
  1. 子 → 父:自定义事件
// 子组件触发事件
this.triggerEvent('customEvent', { value: data })

// 父组件监听
<child-comp bind:customEvent="handleEvent" />
Page({
  handleEvent(e) {
    console.log(e.detail.value)
  }
})

二、逆向父组件访问

  1. 获取父组件实例
// 父组件设置 id
;<child-comp id="childRef" />

// 父组件通过 selectComponent 获取
Page({
  getChild() {
    const child = this.selectComponent('#childRef')
    child.childMethod() // 调用子组件方法
  },
})

三、兄弟组件通信

  1. 共同父组件中转
graph LR
A[父组件] --> B[子组件A]
A --> C[子组件B]
B -- 事件 --> A
A -- 更新数据 --> C
  1. 全局事件总线
// app.js 中创建事件中心
App({
  eventBus: {
    listeners: {},
    on(event, fn) {
      /* 监听 */
    },
    emit(event, data) {
      /* 触发 */
    },
  },
})

// 组件 A 发送
const app = getApp()
app.eventBus.emit('update', data)

// 组件 B 接收
Component({
  attached() {
    app.eventBus.on('update', this.handleUpdate)
  },
})

四、跨层级通信

  1. 全局状态管理
// app.js 定义共享数据
App({
  globalData: {
    userInfo: null,
  },
})

// 任意组件读取/写入
const app = getApp()
app.globalData.userInfo = { name: 'John' }

// 监听变化(需手动实现)
let observer = null
Component({
  attached() {
    observer = setInterval(() => {
      console.log(app.globalData.userInfo)
    }, 500)
  },
  detached() {
    clearInterval(observer)
  },
})
  1. 页面间通信
// PageA 跳转传参
wx.navigateTo({
  url: '/pages/pageB?id=123',
})

// PageB 获取参数
Page({
  onLoad(options) {
    console.log(options.id) // 123
  },
})

// 返回传参(需配合 getCurrentPages)
const pages = getCurrentPages()
const prevPage = pages[pages.length - 2]
prevPage.setData({ feedback: 'success' })

五、高级通信模式

  1. 组件关系 (relations)
// parent.json
{
  "component": true,
  "usingComponents": {
    "child-comp": "../child/child"
  },
  "relations": {
    "../child/child": {
      "type": "child",
      "linked(target) { /* 子组件插入时 */ }"
    }
  }
}

// parent.js
methods: {
  broadcastToChildren(data) {
    this.children.forEach(child => {
      child.receiveData(data)
    })
  }
}

// child.js
methods: {
  receiveData(data) {
    this.setData({ received: data })
  }
}

六、其它第三方库

  1. 对于超大型项目,建议结合 VuexMobX 等状态管理库(需使用 uni-app/Taro 等框架)。

方案对比

方案适用场景优点缺点
Properties父子简单数据传递官方推荐,类型校验单向数据流
自定义事件子向父传递操作解耦合多层传递较复杂
selectComponent父直接调用子方法快速直接破坏封装性
全局事件总线任意组件间通信灵活度高需手动管理监听/卸载
全局状态跨页面共享数据集中管理非响应式,需手动监听
页面路由传参页面间简单数据传递官方支持数据类型受限
relations存在逻辑关联的组件官方关系管理配置较复杂

最佳实践建议

  1. 优先选择官方方案:对于父子通信,务必使用 properties + triggerEvent

  2. 复杂场景组合使用:全局状态管理 + 事件总线应对跨层级通信

  3. 性能优化关键点

    • 避免在 globalData 中存储过大数据
    • 使用 debounce 控制高频事件触发
    // 防抖处理示例
    let timer
    function emitDebounced(event, data) {
      clearTimeout(timer)
      timer = setTimeout(() => {
        app.eventBus.emit(event, data)
      }, 300)
    }
  4. 内存泄漏防范

    Component({
      detached() {
        // 必须移除全局监听
        app.eventBus.off('update', this.handleUpdate)
      },
    })

    :::

wx:if 与 hidden 的区别

参考答案

::: details

小程序中 wx:ifhidden 的区别主要体现在 渲染机制性能影响使用场景 上。

1. 核心区别

特性wx:ifhidden
渲染机制条件为 true 时渲染组件,否则 不渲染始终渲染组件,通过 display: none 隐藏
DOM 结构条件不满足时 移除组件节点组件节点 始终存在,仅样式隐藏
生命周期切换时触发 attached/detached 生命周期无生命周期触发,仅样式变化
状态保留条件切换后 状态重置(如输入框内容清空)隐藏时 保留状态(如输入框内容不变)
性能开销适合 低频切换(减少初始渲染节点)适合 高频切换(避免重复创建/销毁节点)
使用语法支持 wx:if/wx:elif/wx:else 链式条件判断仅接受布尔值(hidden="{{condition}}"

2. 使用场景对比

(1) 推荐使用 wx:if 的场景

(2) 推荐使用 hidden 的场景

3. 性能优化指南

场景选择方案理由
首屏隐藏的大组件wx:if减少初始渲染节点,提升加载速度
频繁切换的组件hidden避免重复创建/销毁,减少性能开销
需要保留状态的表单hidden隐藏时保留输入内容
多条件分支渲染wx:if语法支持更灵活

4. 底层原理

Taro/Uni-app 跨端原理对比

参考答案

::: details

框架技术栈微信小程序H5App支付宝/百度小程序
TaroReact/Vue
uni-appVue
WePYVue
mpvueVue

1. Taro

京东凹凸实验室

优缺点

2. uni-app

DCloud

优缺点

:::

Taro 的实现原理

参考答案

::: details

一、JSX 转换:Taro 通过 自定义 Babel/TypeScript 编译器 将 JSX 转换为通用虚拟 DOM。针对不同前端框架(React/Vue),在编译时生成对应框架的运行时代码,例如:

// 输入
;<View>Hello</View>

// React 输出
import { createElement } from 'react'
createElement('view', {}, 'Hello')

// Vue 输出
import { h } from 'vue'
h('view', {}, 'Hello')

二、多端适配:Taro 的核心架构分为 编译时 和 运行时

  1. 编译时:通过 AST 解析将代码按目标平台转换,生成平台专属模板(如 .wxml / .swan)
  2. 运行时:

三、跨端样式处理:Taro 样式处理包含以下关键机制:

  1. 条件编译:通过 CSS 注释实现多平台样式隔离
/* #ifdef weapp */
.title {
  color: red;
}
/* #endif */
  1. 单位转换:将 px 按比例转为目标平台单位(如小程序 rpx)
  2. 作用域隔离:通过 CSS Modules 自动生成唯一类名
  3. JavaScript 样式:支持 styled-components 等 CSS-in-JS 方案

四、构建系统:Taro 的构建系统特点:

  1. 插件化架构:通过 @tarojs/plugin- 前缀插件扩展功能
  2. 多编译引擎:

五、运行时性能优化:

  1. 数据通信优化:
  1. 渲染优化:
  1. 包体积优化:

:::


Share this post on:

上一篇文章
前端工程化
下一篇文章
React 原理