跳到主要内容
约 12 分钟6 张卡

小程序

小程序是运行在宿主应用中的应用形态。开发者使用宿主提供的页面、组件和 API,用户通过宿主打开服务。它的开发语言可以接近 Web,但运行环境、资源加载、页面生命周期和能力边界都由宿主管理。

本文以微信小程序的 Page、WXML 与 WebView 渲染模式为主要例子。准备相关项目面试时,重点是解释逻辑与显示怎样通信、分包减少了哪段等待,以及页面离开后异步任务怎样处理;不能把浏览器经验直接当成小程序的运行规则。

1. 面试核心​

先记住这几条,再读正文
  • 小程序逻辑层不是浏览器页面,不能把 window、document 和任意 DOM 操作当成可用能力。视图使用宿主组件,网络和设备能力使用宿主接口。卡 · 宿主与运行环境
  • 在 WebView 渲染模式下,逻辑层与视图层有明确边界。数据更新和事件都需要经过框架协调,调用频率、数据量和渲染范围一起影响开销。卡 · 数据更新
  • 页面隐藏不等于卸载。onShow 可以多次触发,onLoad 针对一次页面实例只触发一次;请求、计时器和订阅需要按可见性与存活状态管理。卡 · 页面生命周期与异步任务
  • 分包减少入口必须加载的代码,预下载则把后续页面的下载提前。普通分包仍依赖主包;独立分包入口必须能在主包尚未加载时运行。卡 · 分包与预下载
  • 优化要区分启动、更新、资源与接口耗时。分包不会自动加快业务接口,减少 setData 也不能代替长列表和图片资源治理。卡 · 数据更新
  • 宿主 API 的存在不等于一定能成功调用。网络配置、能力支持、用户授权和业务权限需要分别检查,开发工具表现也不能替代真机结论。

2. 宿主与运行环境​

宿主是提供小程序运行环境的应用,例如微信。它负责加载小程序代码、创建页面、暴露平台能力,并实施相应限制。业务服务端仍然是独立的一层,不会因为前端运行在微信里就自动获得身份校验和数据权限控制。

维度普通 Web 页面微信小程序原生应用
运行与分发浏览器加载 URL 及其资源宿主加载已发布的小程序代码包操作系统安装、运行应用包
界面描述HTML、CSS、DOM 等WXML、WXSS 与宿主组件等平台 UI 工具或对应渲染方案
常见页面逻辑接口浏览器 APIPage、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. 参考资料​

来源:Coding 101 原文 · 免费阅读,引用请保留出处。内容使用说明