小程序
小程序是运行在宿主应用中的应用形态。开发者使用宿主提供的页面、组件和 API,用户通过宿主打开服务。它的开发语言可以接近 Web,但运行环境、资源加载、页面生命周期和能力边界都由宿主管理。
本文以微信小程序的 Page、WXML 与 WebView 渲染模式为主要例子。准备相关项目面试时,重点是解释逻辑与显示怎样通信、分包减少了哪段等待,以及页面离开后异步任务怎样处理;不能把浏览器经验直接当成小程序的运行规则。
1. 面试核心
先记住这几条,再读正文- 小程序逻辑层不是浏览器页面,不能把
window、document和任意 DOM 操作当成可用能力。视图使用宿主组件,网络和设备能力使用宿主接口。卡 · 宿主与运行环境 - 在 WebView 渲染模式下,逻辑层与视图层有明确边界。数据更新和事件都需要经过框架协调,调用频率、数据量和渲染范围一起影响开销。卡 · 数据更新
- 页面隐藏不等于卸载。
onShow可以多次触发,onLoad针对一次页面实例只触发一次;请求、计时器和订阅需要按可见性与存活状态管理。卡 · 页面生命周期与异步任务 - 分包减少入口必须加载的代码,预下载则把后续页面的下载提前。普通分包仍依赖主包;独立分包入口必须能在主包尚未加载时运行。卡 · 分包与预下载
- 优化要区分启动、更新、资源与接口耗时。分包不会自动加快业务接口,减少
setData也不能代替长列表和图片资源治理。卡 · 数据更新 - 宿主 API 的存在不等于一定能成功调用。网络配置、能力支持、用户授权和业务权限需要分别检查,开发工具表现也不能替代真机结论。
2. 宿主与运行环境
宿主是提供小程序运行环境的应用,例如微信。它负责加载小程序代码、创建页面、暴露平台能力,并实施相应限制。业务服务端仍然是独立的一层,不会因为前端运行在微信里就自动获得身份校验和数据权限控制。
| 维度 | 普通 Web 页面 | 微信小程序 | 原生应用 |
|---|---|---|---|
| 运行与分发 | 浏览器加载 URL 及其资源 | 宿主加载已发布的小程序代码包 | 操作系统安装、运行应用包 |
| 界面描述 | HTML、CSS、DOM 等 | WXML、WXSS 与宿主组件等 | 平台 UI 工具或对应渲染方案 |
| 常见页面逻辑接口 | 浏览器 API | Page、Component、wx.* 等 | 平台 SDK |
| 导航和生命周期 | 浏览器导航及应用路由 | 宿主页面栈和页面生命周期 | 平台管理的界面生命周期 |
| 设备与平台能力 | 受浏览器能力和授权限制 | 受宿主接口、授权与平台配置限制 | 受系统 API、权限及平台规则限制 |
这里的 H5 指通常所说的移动 Web 页面。它也可能运行在某个应用的 WebView 中,并通过该应用专门提供的桥接接口调用能力;不能因此推断所有 H5 都有相同的宿主 API。
微信小程序逻辑层没有供业务代码任意访问页面的 document。从网页复制依赖 DOM 的库,不能只改入口文件就期待它正常运行。纯 JavaScript 的计算逻辑较容易复用,视图、导航、网络适配和设备能力则要检查实际依赖。
uni-app、Taro 等跨端工具可以转换部分代码、统一部分接口,但输出仍受目标平台约束。能编译到多个平台,不意味着组件表现、生命周期、包体或权限流程完全一致。项目介绍中应说清使用了哪种工具、编译目标是什么、哪些地方做了平台适配。
3. 逻辑层与视图层
逻辑层执行业务 JavaScript,维护状态并处理事件;视图层根据模板、样式和组件显示界面。在常见 WebView 模式下,逻辑与视图处于不同运行环境,通过框架传递更新和事件。
一次点击的路径是:用户操作视图中的按钮,框架把对应事件交给逻辑层处理,处理函数修改需要显示的数据,再由框架把变化落实到视图。分离并不代表两侧互不影响:消息积压、逻辑计算过重或视图更新过大,都会让用户感觉响应迟缓。
“逻辑层一个线程、视图层一个线程”只是简化描述,不能据此断言整个小程序始终只有两个线程。多个页面、宿主原生组件和渲染引擎会带来更多实现细节。微信还提供 Skyline 渲染引擎,其内部结构与 WebView 模式不同;本文不把 WebView 的跨环境通信成本推广成所有渲染模式的固定实现。
3.1 数据更新
setData 是向框架提交数据变化的接口,不只是给普通对象赋值。 它会更新对应的 this.data,并让框架应用视图更新。业务侧读到的新值,与这组变化已经完整应用到渲染引擎,是不同的完成点;需要等待后者时使用相应回调,而不是猜测一个固定延时。
下面是同一个页面的 WXML 和 JavaScript。点击一次按钮,将第一项数量增加一:
<view>数量:{{items[0].quantity}}</view>
<button bindtap="increase">增加</button>
Page({
data: {
items: [{ id: 'book', quantity: 1 }],
},
increase() {
const next = this.data.items[0].quantity + 1;
this.setData({ 'items[0].quantity': next }, () => {
console.log('本次更新已应用到渲染引擎');
});
console.log(this.data.items[0].quantity);
},
});
第一次点击后,逻辑侧读取的数量为 2,框架也会把显示更新为 2。路径 'items[0].quantity' 表明只提交这一处变化,不需要为修改一个数字而传回整个列表。回调只说明这次更新的完成点,不表示业务请求成功,也不表示期间没有发生后续更新。
如果只写 this.data.items[0].quantity++,普通对象中的值可能改变,但没有通过 setData 提交相应的视图更新,会造成逻辑与显示不一致。反过来,每次都调用 this.setData(this.data),又把大量未变化字段送进更新流程。
不参与显示的请求序号、任务句柄等,不应为了“方便访问”都放入 data。频繁事件中的连续变化可以按交互要求合并,相关字段也可以一次提交;但不能为了减少次数而延迟用户必须立即看到的反馈。数据量之外,还应控制更新涉及的节点范围,例如把独立高频变化拆到较小的组件中。
4. 页面生命周期与异步任务
页面生命周期描述页面实例的状态变化,不等于业务请求的状态变化。 网络响应可能在页面隐藏或卸载后才到达,需要应用自己判断是否仍适合更新界面。
| 页面回调 | 含义 | 可以据此安排的工作 |
|---|---|---|
onLoad | 页面实例加载,一次实例调用一次 | 读取路由参数,建立本次页面状态 |
onShow | 页面显示,包括从隐藏状态回来 | 按新鲜度刷新数据,恢复可见时才需要的工作 |
onReady | 页面初次渲染完成 | 执行依赖初始界面准备完成的操作 |
onHide | 页面隐藏 | 暂停不必在后台继续的计时或显示更新 |
onUnload | 页面实例卸载 | 解除订阅、清理任务,并使旧回调失效 |
例如从列表 A 用 wx.navigateTo 打开详情 B,A 隐藏,B 创建;从 B 返回时,B 卸载,原来的 A 再次显示。A 的 onShow 会再次执行,不能把它当成只运行一次的构造过程。页面替换则要区分 wx.redirectTo:它会卸载当前页面,再打开新页面。
如果在每次 onShow 中注册监听,却从不解除,来回打开详情后,同一个事件可能触发多次处理。如果只在 onUnload 停止高频刷新,隐藏在页面栈中的 A 仍可能持续做无用更新。因此“隐藏时暂停”和“卸载时释放”需要分别考虑。
请求还存在两个独立条件:页面是否有效,以及响应是否属于当前请求。搜索词从 A 改到 B 后,即使页面一直存在,A 的晚到结果也不应覆盖 B。可以记录递增请求序号,只有当前序号允许应用结果;卸载时使序号失效并清理任务。取消请求可以减少浪费,但不能把取消视为服务端从未执行,也不能只靠取消代替旧结果检查。
在 wx.request 的 success 中仍要检查 HTTP 状态和业务结果。服务器返回错误状态码也可能进入 success;它描述收到响应,不等于业务操作成功。
5. 分包与预下载
分包按照配置把代码和资源划分到不同加载单元,减少某个入口必须一次加载的内容。主包包含必要入口与公共内容;普通分包按需加载,但运行仍依赖主包。它们属于代码加载策略,不是把业务接口拆成多个服务。
下面的 app.json 把首页放在主包,把订单页放入普通分包,并在进入首页后、满足 Wi-Fi 条件时预下载订单分包:
{
"pages": ["pages/home/index"],
"subPackages": [
{
"root": "packages/orders",
"name": "orders",
"pages": ["pages/list/index"]
}
],
"preloadRule": {
"pages/home/index": {
"network": "wifi",
"packages": ["orders"]
}
}
}
订单页的完整路径为 packages/orders/pages/list/index;preloadRule 的键是触发预下载的页面路径,packages 可以引用分包的名称或根路径。network: "wifi" 限制预下载时机,不表示用户只能在 Wi-Fi 下打开订单页。没有预下载或预下载尚未完成时,进入该页仍要等待所需代码可用。
预下载是用提前消耗流量和资源,换取后续访问少等待。 它不是预先执行订单页面的 onLoad,更不是自动请求订单列表。若大多数用户不会进入订单页,提前下载可能只有成本;若首屏请求已经占满网络,预下载还可能与重要资源竞争。需要结合访问路径和测量结果选择,而不是预下载所有分包。
5.1 包之间的依赖
拆包必须同时整理依赖,否则体积划分不会带来预期收益。 普通分包可以引用主包的公共内容,但主包不能把尚未加载的分包当成同步可用的模块。普通分包之间也不能默认自由同步引用;分包异步化属于需要另外设计的加载方式,不能只改目录就获得。
将所有公共依赖都搬到主包,会让主包越来越大,抵消启动收益。可以先区分:每个入口都需要的基础能力、只属于某个业务的组件,以及体积大但低频的资源,再决定归属。tabBar 页面应放在主包;分包根目录也不能互相嵌套。
独立分包可以在主包尚未下载时作为入口运行,因此不能假定 App 已注册、getApp() 一定返回对象,或主包样式与公共模块已经存在。它适合能独立启动的业务入口,但也要求整理初始化和依赖。它不是普通分包加一个标记后就自然获得相同运行结果。
包体限制与平台配置有关,工程中应以开发工具和发布检查为准。面试解释分包时,先讲加载和依赖关系,比只背某个大小上限更有价值。
6. 性能与能力边界
先定位等待或卡顿发生在哪一段,再选择优化手段。 一个页面“慢”,可能是首次加载代码慢、接口返回慢,也可能是大量视图更新阻塞了交互。
| 现象 | 优先检查 | 对应措施 |
|---|---|---|
| 冷启动等待较久 | 入口包体、初始化计算、必要请求 | 缩小主包,延后非必要初始化,减少首屏依赖 |
| 进入低频页等待 | 目标分包是否已下载、业务接口耗时 | 按访问概率预下载,分别处理接口和代码等待 |
| 滚动或输入卡顿 | 更新频率、传输数据量、渲染节点范围 | 合并合理更新,只提交变化字段,控制长列表和组件范围 |
| 图片出现较慢 | 图片尺寸、格式、下载和解码成本 | 使用合适尺寸,按可见区域加载,保留占位避免跳动 |
| 返回页面后重复请求 | onShow 重复执行、监听累积 | 明确新鲜度策略,统一注册与解除时机 |
在本文的 WebView 模式中,可以给 image 组件设置 lazy-load="{{true}}",由宿主在图片接近可视区域时再开始加载。这个触发点允许提前加载,不是必须等图片已经完全出现在屏幕上;它也不减少图片本身的字节数,仍需控制尺寸并预留显示空间。Skyline 对图片默认采用懒加载,不能把这一属性的开关行为照搬过去。
需要自行控制加载时,应使用宿主支持的观察能力,不能默认把浏览器的 IntersectionObserver 原样复制进逻辑层。定位性能问题时记录代码包下载、数据请求和界面更新各自的耗时,并在目标设备和网络上复查;只看开发工具中一次流畅滚动不足以说明性能达标。
6.1 网络与权限
网络能力、用户授权和业务权限是不同的检查。 wx.request 使用宿主网络接口,不能用“给服务端加 CORS 响应头”代替小程序的请求域名和 HTTPS 配置。开发时跳过域名或证书检查,也不能证明正式环境能够访问。
涉及摄像头、位置等能力时,还要确认当前平台是否支持、相关声明是否满足,以及用户是否授权。被拒绝后应保留可用的替代路径并说明功能受限的原因;不要把所有失败都归为网络错误,也不能靠重复调用让授权必然成功。
最后,平台授权不等于业务鉴权。用户允许获取位置,不代表他可以读取另一个用户的订单。服务端仍要检查登录身份、资源归属和操作权限,密钥也不能放进可分发的小程序代码包。
7 · 面试题卡片
本页题目与参考回答
7.1 小程序端与 Web 端有什么区别?
考察重点:宿主环境、视图与 API 边界、页面和资源管理。
参考回答
小程序运行在宿主提供的环境中,视图、页面管理和平台能力都要遵循宿主接口;普通 Web 页面主要依赖浏览器平台。 以微信为例,逻辑层没有可任意操作页面的 window、document,使用 WXML、WXSS 和组件描述界面,用 setData 等接口提交更新。
小程序还涉及宿主页面生命周期、代码包和分包、请求域名配置与用户授权。纯计算逻辑可以复用,依赖 DOM、路由或设备能力的代码需要适配。不能仅凭形态就断言小程序必然比 H5 快,仍要比较具体实现和运行环境。
7.2 小程序的分包和分包预加载怎样工作?
考察重点:入口下载、普通与独立分包、预下载的触发与代价。
参考回答
分包将代码按加载需要拆开,预下载则在用户进入后续页面前,提前下载可能需要的包。 普通分包依赖主包;独立分包可以在主包尚未加载时启动,因此必须处理好初始化、模块和样式依赖。
可以在 app.json 中用 subPackages 声明包,用 preloadRule 按页面和网络条件配置预下载。预下载不等于提前执行目标页面或预取业务数据,也不保证进入页面时已经完成。它会消耗流量并可能竞争资源,应按访问概率与测量结果配置。
7.3 小程序可以从哪些方面做性能优化?
考察重点:按启动、通信、渲染和资源定位问题,而不是罗列无条件有效的措施。
参考回答
先区分启动慢、接口慢和交互卡顿,再优化对应链路。 启动时减少入口包体和非必要初始化;后续页结合访问路径分包和预下载;更新时减少无效 setData、只提交变化数据,并控制组件和长列表的节点范围。
图片使用适合展示尺寸的资源;在 WebView 模式下,可以给 image 设置 lazy-load="{{true}}",让宿主在图片接近可视区域时加载。页面隐藏后减少无意义更新,回到页面时避免重复监听和无条件重复请求。最终用目标设备、网络和实际操作验证,不能把开发工具表现或代码体积变化直接当成用户体验结论。
7.4 为什么直接修改 this.data 可能没有更新页面?
考察重点:普通赋值与框架更新提交、逻辑数据与渲染完成的区别。
相关内容:数据更新。
参考回答
普通赋值只改了对象,没有通过 setData 提交对应的视图更新。 应把变化以字段或数据路径传给框架,例如 this.setData({ 'items[0].quantity': next })。
调用后读取到新 this.data,不等于这组变化已完整应用到渲染引擎。需要后者时用相应回调;也不要为省事每次传整个 this.data,或在高频事件中不断提交无关变化。
7.5 页面隐藏、卸载和请求返回应怎样配合?
考察重点:页面实例存活、可见性、旧请求结果和资源清理。
相关内容:页面生命周期与异步任务。
参考回答
隐藏页面仍可能保留在页面栈中,卸载才结束这次页面实例;两者都不能保证请求恰好已经结束。 隐藏时暂停不必要的高频工作,卸载时解除订阅、清理任务并让旧回调失效。
对多个并发或连续请求,还要用请求序号等方式保证只有当前结果可以更新界面。onShow 可能反复执行,注册监听要防止累积,是否刷新数据则取决于新鲜度要求。
7.6 介绍小程序项目经历时,需要讲清哪些技术选择?
题型:综合题
考察重点:具体业务与平台约束的对应关系,个人实现和验证证据。
参考回答
围绕自己实际负责的功能,说明约束、实现和结果。 例如为什么选择小程序或跨端工具,首页和低频业务怎样拆包,页面返回时怎样刷新,权限拒绝和请求失败时给用户什么反馈。
如果做过优化,应说明观察到了哪段耗时、采取什么措施,以及在什么设备和网络下验证。没有做过的并发规模、真机测试或指标提升,不能由“使用了某个框架”推导出来。
8. 参考资料
- 微信开放文档:小程序框架
- 微信开放文档:逻辑层
- 微信开放文档:小程序的运行环境
- 微信开放文档:数据更新方法
- 微信开放文档:Page
- 微信开放文档:页面路由
- 微信开放文档:使用分包
- 微信开放文档:分包预下载
- 微信开放文档:独立分包
- 微信开放文档:合理使用 setData
- 微信开放文档:image 图片组件
- 微信开放文档:网络
- 微信开放文档:授权
- 微信开放文档:Skyline 简介
来源:Coding 101 原文 · 免费阅读,引用请保留出处。内容使用说明