简历描述模板
版本一:注重架构能力
负责微信小程序架构设计,采用分包加载策略将主包体积控制在1.8MB以内,分包按需加载
提升首屏速度40%。设计页面栈管理方案,解决小程序10层页面栈限制,实现无限层级跳转。
优化生命周期管理,封装页面基类统一处理公共逻辑,减少重复代码60%。引入Pinia状态
管理,建立模块化store架构,支持状态持久化和跨页面通信。搭建CI/CD自动化流水线,
实现代码提交自动构建、测试、上传,发版周期从2天缩短至2小时。
版本二:强调工程化实践
主导小程序工程化建设,制定目录规范、编码规范、组件规范等开发标准。设计分包架构
将业务模块解耦,主包仅保留核心功能和通用组件,各业务线独立分包,包体积优化45%。
封装Router导航管理器,支持路由拦截、参数传递、返回控制等高级特性。基于Pinia实现
轻量级状态管理,配合持久化插件实现跨会话数据保留。建立自动化测试体系,单元测试
覆盖率80%,集成测试覆盖核心流程。接入小程序CI接口实现自动化发布,支持灰度发布
和版本回滚。
版本三:突出业务价值
作为小程序技术负责人,推动架构升级和工程化改造,显著提升开发效率和代码质量。通
过分包优化使首屏加载速度提升40%,用户跳出率下降15%。页面栈管理方案解决了深层级
跳转问题,支持10+个模块的无限层级导航。状态管理重构后,跨页面通信代码减少70%,
页面间数据一致性问题从月均30+降至个位数。CI/CD自动化使发版效率提升5倍,线上bug
修复响应时间从1天缩短至2小时。项目支撑公司3条业务线,累计服务200万+用户。
SOP 标准回答
Q1: 小程序的分包加载策略是怎么设计的?
标准回答:
我们项目一开始主包就3MB多,超过2MB限制了,发布不了。我分析了代码结构,把非首屏
的业务模块全部拆成分包。
具体规划是这样的:主包只保留启动页、首页、tabbar相关页面,还有通用组件、工具类、
公共样式。然后按业务模块拆分包:商品模块一个包,订单模块一个包,用户模块一个包,
活动模块一个包。
app.json里配置subpackages,每个分包有独立的root目录。比如商品包的root是
pages/product,里面有商品列表、商品详情这些页面。用户访问商品页时才下载这个分包,
不访问就不下载,节省流量。
还有个独立分包,比如直播功能,它不依赖主包,可以独立运行。这个配置independent:
true,用户从分享进来能直接打开,不用先下载主包。
分包还有个预下载配置preloadRule。比如用户在首页时,预判他可能会去商品页,就提前
下载商品分包。设置network为wifi,只在wifi环境下预下载,不浪费用户流量。
这样改完,主包从3MB降到1.8MB,总包大小还是3MB,但分成5个包了。首屏只需要下载
1.8MB,速度快了很多。而且每个包都不超过2MB,能顺利发布。
还有个坑要注意,分包不能引用其他分包的资源,只能引用主包的。所以公共组件、工具
函数必须放主包,否则会报找不到模块的错误。
追问应对:
- "分包大小如何分配?" → 根据业务优先级,核心模块分包可以大一些,次要模块控制小一点
- "独立分包有什么限制?" → 不能依赖主包内容,不能使用app.js里的全局数据,适合独立功能
- "预下载会影响性能吗?" → 只在wifi和必要时预下载,设置好规则不会影响,反而提升体验
Q2: 页面栈管理是怎么实现的?
标准回答:
小程序有个限制,页面栈最多10层,超过了就不能wx.navigateTo了,只能redirectTo或
navigateBack。但我们业务流程长,经常超过10层,用户体验很差。
我封装了个Router导航管理器来解决这个问题。核心思路是监控页面栈深度,快到10层时
自动做处理。
具体实现是用wx.navigateTo的时候,先调用我的router.push方法。这个方法内部会检查
当前页面栈深度,getCurrentPages().length拿到当前层数。如果已经9层了,就不能再
push了,会先redirectTo到目标页面,同时把当前页面信息存到storage里,做个记录。
返回的时候,router.back检查storage里有没有被replace掉的页面。如果有,就恢复栈
结构,让用户感觉不到中间replace过。
还有个功能是路由拦截。比如有些页面需要登录,我在router.push里加了个beforeEach
拦截器。检查用户登录状态,没登录就先跳登录页,登录成功后再跳回目标页面。这个
redirect参数存在storage里,登录成功后读取跳转。
还封装了tab切换的逻辑。因为switchTab会清空页面栈,我在切换前把栈信息存storage,
切回来时判断是否需要恢复。这样tab间切换也不会丢失导航历史。
路由传参也优化了。小程序url传参有长度限制,复杂对象传不了。我做了个全局参数池,
复杂参数存池里生成个key,url只传key,目标页面通过key取真实参数。
这套方案下来,页面栈限制的问题完全解决了,用户可以无限层级跳转,而且支持很多高
级特性,跟Vue Router的体验差不多。
Q3: 小程序的生命周期是怎么优化的?
标准回答:
小程序的生命周期比较复杂,有应用生命周期、页面生命周期、组件生命周期,管理起来
很繁琐。我做了几个优化:
第一是封装页面基类。每个页面都要做的事情,比如登录检查、埋点上报、分享配置,我
放在基类里统一处理。页面只需要继承这个基类,自动就有这些能力。
具体实现是用装饰器模式,写了个createPage工厂函数。传入页面配置对象,返回增强后
的对象。在onLoad里加入登录检查,onShow里加入埋点,onShareAppMessage里加入默认
分享配置。页面可以覆盖这些方法,加自己的逻辑。
第二是生命周期hook。参考Vue的composition api,封装了onLoad、onShow这些hook。在
页面里用hook注册回调,比直接写在配置对象里更灵活,可以复用逻辑。
第三是自动化资源回收。页面onUnload时要清理定时器、取消网络请求、移除事件监听,
否则会内存泄漏。我在基类里维护了个回收列表,页面注册的这些资源都记录下来,
onUnload时自动清理。
第四是生命周期日志。开发时经常搞不清楚生命周期执行顺序,我加了个debug模式,会打
印每个生命周期的触发时机和参数,方便排查问题。
还有个重要优化是预加载。onLoad里的异步数据,我改成在上一个页面的onHide里就开始
请求。navigateTo跳转时传个promise,下个页面onLoad直接await这个promise,数据已经
准备好了,页面秒开。
这些优化做完,页面代码清爽多了,重复代码减少60%,而且性能也有提升,因为很多公共
逻辑可以复用,不用每个页面都写一遍。
难点与亮点分析
难点一:分包加载完整方案
问题背景: 小程序主包限制2MB,总包限制20MB,复杂项目容易超限。需要合理规划分包策略。
解决方案:
1. app.json分包配置
{
"pages": [
"pages/index/index",
"pages/home/home",
"pages/cart/cart",
"pages/user/user"
],
"subpackages": [
{
"root": "packages/product",
"name": "product",
"pages": [
"pages/list/list",
"pages/detail/detail",
"pages/search/search"
]
},
{
"root": "packages/order",
"name": "order",
"pages": [
"pages/list/list",
"pages/detail/detail",
"pages/confirm/confirm"
]
},
{
"root": "packages/activity",
"name": "activity",
"pages": [
"pages/seckill/seckill",
"pages/groupon/groupon"
],
"independent": true
}
],
"preloadRule": {
"pages/index/index": {
"network": "wifi",
"packages": ["product"]
},
"packages/product/pages/list/list": {
"network": "all",
"packages": ["order"]
}
},
"tabBar": {
"list": [
{
"pagePath": "pages/index/index",
"text": "首页"
},
{
"pagePath": "pages/cart/cart",
"text": "购物车"
},
{
"pagePath": "pages/user/user",
"text": "我的"
}
]
}
}
2. 分包加载管理器
// utils/subpackage.js
/**
* 分包加载管理器
*/
class SubpackageManager {
constructor() {
this.loadedPackages = new Set()
this.loadingPackages = new Map()
}
/**
* 加载分包
*/
loadSubpackage(name) {
// 已加载
if (this.loadedPackages.has(name)) {
return Promise.resolve()
}
// 正在加载
if (this.loadingPackages.has(name)) {
return this.loadingPackages.get(name)
}
// 开始加载
const promise = new Promise((resolve, reject) => {
wx.loadSubpackage({
name,
success: () => {
console.log(`分包 ${name} 加载成功`)
this.loadedPackages.add(name)
this.loadingPackages.delete(name)
resolve()
},
fail: (err) => {
console.error(`分包 ${name} 加载失败:`, err)
this.loadingPackages.delete(name)
reject(err)
}
})
})
this.loadingPackages.set(name, promise)
return promise
}
/**
* 预加载分包
*/
async preloadSubpackages(names) {
const promises = names.map(name => this.loadSubpackage(name))
return Promise.all(promises)
}
/**
* 检查分包是否已加载
*/
isLoaded(name) {
return this.loadedPackages.has(name)
}
/**
* 获取加载进度
*/
getProgress(name) {
return new Promise((resolve) => {
const task = wx.loadSubpackage({
name,
success: () => {
resolve(100)
},
fail: () => {
resolve(0)
}
})
task.onProgressUpdate((res) => {
resolve(res.progress)
})
})
}
}
export default new SubpackageManager()
3. 分包路由配置
// config/routes.js
export const routes = {
// 主包路由
index: '/pages/index/index',
home: '/pages/home/home',
cart: '/pages/cart/cart',
user: '/pages/user/user',
// 商品分包
productList: '/packages/product/pages/list/list',
productDetail: '/packages/product/pages/detail/detail',
productSearch: '/packages/product/pages/search/search',
// 订单分包
orderList: '/packages/order/pages/list/list',
orderDetail: '/packages/order/pages/detail/detail',
orderConfirm: '/packages/order/pages/confirm/confirm',
// 活动分包(独立分包)
seckill: '/packages/activity/pages/seckill/seckill',
groupon: '/packages/activity/pages/groupon/groupon'
}
// 分包映射
export const packageMap = {
'/packages/product': 'product',
'/packages/order': 'order',
'/packages/activity': 'activity'
}
/**
* 获取路径所属分包
*/
export function getPackageName(path) {
for (const [root, name] of Object.entries(packageMap)) {
if (path.startsWith(root)) {
return name
}
}
return null
}
4. 智能路由跳转
// utils/router.js
import subpackageManager from './subpackage'
import { getPackageName } from '../config/routes'
class Router {
/**
* 导航到页面(自动处理分包加载)
*/
async navigateTo(url, options = {}) {
const packageName = getPackageName(url)
// 如果是分包页面,先加载分包
if (packageName && !subpackageManager.isLoaded(packageName)) {
wx.showLoading({ title: '加载中...' })
try {
await subpackageManager.loadSubpackage(packageName)
wx.hideLoading()
} catch (error) {
wx.hideLoading()
wx.showToast({
title: '加载失败',
icon: 'none'
})
return
}
}
// 跳转页面
wx.navigateTo({
url,
...options,
fail: (err) => {
console.error('页面跳转失败:', err)
wx.showToast({
title: '页面不存在',
icon: 'none'
})
}
})
}
/**
* 重定向
*/
async redirectTo(url, options = {}) {
const packageName = getPackageName(url)
if (packageName && !subpackageManager.isLoaded(packageName)) {
wx.showLoading({ title: '加载中...' })
await subpackageManager.loadSubpackage(packageName)
wx.hideLoading()
}
wx.redirectTo({ url, ...options })
}
/**
* 返回
*/
navigateBack(delta = 1) {
wx.navigateBack({ delta })
}
/**
* 切换Tab
*/
switchTab(url) {
wx.switchTab({ url })
}
/**
* 重启到页面
*/
reLaunch(url) {
wx.reLaunch({ url })
}
}
export default new Router()
难点二:页面栈管理方案
问题背景: 小程序页面栈最多10层,超过后无法使用navigateTo,影响用户体验。
解决方案:
1. 页面栈管理器
// utils/pageStack.js
class PageStackManager {
constructor() {
this.maxStack = 10 // 最大页面栈深度
this.stackHistory = [] // 栈历史记录
}
/**
* 获取当前页面栈
*/
getCurrentPages() {
return getCurrentPages()
}
/**
* 获取当前页面栈深度
*/
getStackDepth() {
return this.getCurrentPages().length
}
/**
* 检查是否可以push
*/
canPush() {
return this.getStackDepth() < this.maxStack
}
/**
* 智能导航
*/
navigate(url, params = {}) {
const fullUrl = this.buildUrl(url, params)
if (this.canPush()) {
// 可以push
return this.push(fullUrl)
} else {
// 栈满,使用redirect
return this.replace(fullUrl)
}
}
/**
* Push页面
*/
push(url) {
return new Promise((resolve, reject) => {
wx.navigateTo({
url,
success: () => {
this.recordStack('push', url)
resolve()
},
fail: reject
})
})
}
/**
* Replace页面
*/
replace(url) {
return new Promise((resolve, reject) => {
wx.redirectTo({
url,
success: () => {
this.recordStack('replace', url)
resolve()
},
fail: reject
})
})
}
/**
* 返回
*/
back(delta = 1) {
return new Promise((resolve, reject) => {
wx.navigateBack({
delta,
success: () => {
this.recordStack('back', delta)
resolve()
},
fail: reject
})
})
}
/**
* 返回到指定页面
*/
backToPage(url) {
const pages = this.getCurrentPages()
let targetIndex = -1
// 查找目标页面
for (let i = pages.length - 1; i >= 0; i--) {
if (pages[i].route === url || pages[i].route === url.replace(/^\//, '')) {
targetIndex = i
break
}
}
if (targetIndex === -1) {
// 目标页面不在栈中,直接跳转
return this.replace(url)
} else {
// 计算需要返回的层数
const delta = pages.length - 1 - targetIndex
return this.back(delta)
}
}
/**
* 清空栈并跳转
*/
reLaunch(url) {
return new Promise((resolve, reject) => {
wx.reLaunch({
url,
success: () => {
this.stackHistory = []
this.recordStack('relaunch', url)
resolve()
},
fail: reject
})
})
}
/**
* 记录栈操作
*/
recordStack(action, data) {
this.stackHistory.push({
action,
data,
depth: this.getStackDepth(),
timestamp: Date.now()
})
// 保存到storage(用于跨会话恢复)
wx.setStorageSync('page_stack_history', this.stackHistory.slice(-20))
}
/**
* 构建URL
*/
buildUrl(url, params = {}) {
if (Object.keys(params).length === 0) {
return url
}
const query = Object.entries(params)
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
.join('&')
return `${url}?${query}`
}
/**
* 获取栈历史
*/
getHistory() {
return this.stackHistory
}
/**
* 获取当前页面信息
*/
getCurrentPage() {
const pages = this.getCurrentPages()
return pages[pages.length - 1]
}
/**
* 获取上一页面信息
*/
getPreviousPage() {
const pages = this.getCurrentPages()
return pages[pages.length - 2]
}
}
export default new PageStackManager()
2. 路由拦截器
// utils/routerGuard.js
import pageStack from './pageStack'
class RouterGuard {
constructor() {
this.guards = []
}
/**
* 注册全局前置守卫
*/
beforeEach(guard) {
this.guards.push(guard)
}
/**
* 执行守卫
*/
async runGuards(to, from) {
for (const guard of this.guards) {
const result = await guard(to, from)
if (result === false) {
// 取消导航
return false
} else if (typeof result === 'string') {
// 重定向到其他页面
return result
}
}
return true
}
/**
* 拦截导航
*/
async intercept(url) {
const from = pageStack.getCurrentPage()
const to = { url }
const result = await this.runGuards(to, from)
if (result === false) {
return null
} else if (typeof result === 'string') {
return result
} else {
return url
}
}
}
// 创建全局路由守卫
const routerGuard = new RouterGuard()
// 注册登录检查守卫
routerGuard.beforeEach((to, from) => {
const needLogin = [
'/packages/order',
'/packages/user'
]
const needAuth = needLogin.some(path => to.url.startsWith(path))
if (needAuth) {
const isLogin = wx.getStorageSync('token')
if (!isLogin) {
// 保存目标页面,登录后跳转
wx.setStorageSync('redirect_url', to.url)
wx.showToast({
title: '请先登录',
icon: 'none'
})
// 跳转到登录页
return '/pages/login/login'
}
}
return true
})
export default routerGuard
3. 增强的Router类
// utils/router.js
import pageStack from './pageStack'
import routerGuard from './routerGuard'
class Router {
constructor() {
this.paramCache = new Map() // 参数缓存池
}
/**
* 导航到页面
*/
async push(url, params = {}) {
// 执行路由守卫
const finalUrl = await routerGuard.intercept(this.buildUrl(url, params))
if (!finalUrl) return
// 智能导航
return pageStack.navigate(finalUrl)
}
/**
* 替换当前页面
*/
async replace(url, params = {}) {
const finalUrl = await routerGuard.intercept(this.buildUrl(url, params))
if (!finalUrl) return
return pageStack.replace(finalUrl)
}
/**
* 返回
*/
back(delta = 1) {
return pageStack.back(delta)
}
/**
* 返回到指定页面
*/
backTo(url) {
return pageStack.backToPage(url)
}
/**
* 切换Tab
*/
switchTab(url) {
wx.switchTab({ url })
}
/**
* 重启
*/
reLaunch(url, params = {}) {
return pageStack.reLaunch(this.buildUrl(url, params))
}
/**
* 构建URL(支持复杂参数)
*/
buildUrl(url, params = {}) {
if (Object.keys(params).length === 0) {
return url
}
// 简单参数直接拼接
const simpleParams = {}
const complexParams = {}
Object.entries(params).forEach(([key, value]) => {
if (typeof value === 'object' || typeof value === 'function') {
complexParams[key] = value
} else {
simpleParams[key] = value
}
})
// 复杂参数存入缓存
let paramKey = ''
if (Object.keys(complexParams).length > 0) {
paramKey = this.generateParamKey()
this.paramCache.set(paramKey, complexParams)
simpleParams._paramKey = paramKey
}
// 构建query string
const query = Object.entries(simpleParams)
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
.join('&')
return `${url}?${query}`
}
/**
* 获取页面参数
*/
getParams(options) {
const params = { ...options }
// 如果有缓存的复杂参数,取出来
if (params._paramKey) {
const complexParams = this.paramCache.get(params._paramKey)
if (complexParams) {
Object.assign(params, complexParams)
this.paramCache.delete(params._paramKey)
}
delete params._paramKey
}
return params
}
/**
* 生成参数key
*/
generateParamKey() {
return `param_${Date.now()}_${Math.random().toString(36).slice(2)}`
}
/**
* 获取当前页面
*/
getCurrentPage() {
return pageStack.getCurrentPage()
}
/**
* 获取上一页面
*/
getPreviousPage() {
return pageStack.getPreviousPage()
}
}
export default new Router()
难点三:页面生命周期优化
解决方案:
1. 页面基类
// utils/basePage.js
/**
* 创建增强页面
*/
export function createPage(config) {
const {
data = {},
onLoad,
onShow,
onHide,
onUnload,
onShareAppMessage,
...rest
} = config
return {
data: {
...data,
_loading: false,
_error: null
},
// 资源清理列表
_disposers: [],
/**
* 页面加载
*/
onLoad(options) {
console.log('[Page] onLoad:', this.route, options)
// 解析参数
this._options = this._parseOptions(options)
// 登录检查
this._checkLogin()
// 埋点上报
this._reportPageView()
// 执行页面的onLoad
if (onLoad) {
onLoad.call(this, this._options)
}
},
/**
* 页面显示
*/
onShow() {
console.log('[Page] onShow:', this.route)
// 埋点上报
this._reportPageShow()
// 执行页面的onShow
if (onShow) {
onShow.call(this)
}
},
/**
* 页面隐藏
*/
onHide() {
console.log('[Page] onHide:', this.route)
// 执行页面的onHide
if (onHide) {
onHide.call(this)
}
},
/**
* 页面卸载
*/
onUnload() {
console.log('[Page] onUnload:', this.route)
// 清理资源
this._dispose()
// 执行页面的onUnload
if (onUnload) {
onUnload.call(this)
}
},
/**
* 分享
*/
onShareAppMessage(options) {
console.log('[Page] onShareAppMessage:', options)
// 默认分享配置
const defaultShare = {
title: '推荐给你',
path: `/${this.route}`,
imageUrl: ''
}
// 执行页面的分享配置
if (onShareAppMessage) {
const customShare = onShareAppMessage.call(this, options)
return { ...defaultShare, ...customShare }
}
return defaultShare
},
/**
* 解析参数
*/
_parseOptions(options) {
// 支持router传递的复杂参数
if (options._paramKey) {
const router = require('./router').default
return router.getParams(options)
}
return options
},
/**
* 登录检查
*/
_checkLogin() {
// 如果页面需要登录,检查登录状态
if (config.needLogin) {
const token = wx.getStorageSync('token')
if (!token) {
wx.showToast({
title: '请先登录',
icon: 'none'
})
setTimeout(() => {
wx.navigateTo({
url: '/pages/login/login'
})
}, 1500)
}
}
},
/**
* 页面访问埋点
*/
_reportPageView() {
// 上报页面访问
wx.reportAnalytics('page_view', {
page: this.route,
...this._options
})
},
/**
* 页面显示埋点
*/
_reportPageShow() {
// 上报页面显示
wx.reportAnalytics('page_show', {
page: this.route
})
},
/**
* 设置loading状态
*/
setLoading(loading) {
this.setData({ _loading: loading })
if (loading) {
wx.showLoading({ title: '加载中...' })
} else {
wx.hideLoading()
}
},
/**
* 设置错误
*/
setError(error) {
this.setData({ _error: error })
wx.showToast({
title: error.message || '操作失败',
icon: 'none'
})
},
/**
* 注册定时器
*/
setTimeout(callback, delay) {
const timer = setTimeout(callback, delay)
this._disposers.push(() => clearTimeout(timer))
return timer
},
/**
* 注册interval
*/
setInterval(callback, delay) {
const timer = setInterval(callback, delay)
this._disposers.push(() => clearInterval(timer))
return timer
},
/**
* 注册网络请求
*/
request(options) {
const task = wx.request(options)
this._disposers.push(() => task.abort())
return task
},
/**
* 清理资源
*/
_dispose() {
this._disposers.forEach(disposer => {
try {
disposer()
} catch (error) {
console.error('清理资源失败:', error)
}
})
this._disposers = []
},
// 合并其他方法
...rest
}
}
2. 使用示例
// pages/product/detail.js
import { createPage } from '@/utils/basePage'
import router from '@/utils/router'
createPage({
// 需要登录
needLogin: false,
data: {
product: null,
loading: true
},
onLoad(options) {
const { id } = options
this.loadProduct(id)
},
async loadProduct(id) {
this.setLoading(true)
try {
const res = await this.request({
url: `/api/product/${id}`,
method: 'GET'
})
this.setData({
product: res.data,
loading: false
})
} catch (error) {
this.setError(error)
this.setLoading(false)
}
},
// 倒计时
startCountdown() {
let count = 60
this.setInterval(() => {
count--
this.setData({ countdown: count })
if (count === 0) {
// interval会自动清理
}
}, 1000)
},
// 购买
handleBuy() {
router.push('/packages/order/pages/confirm/confirm', {
productId: this.data.product.id,
quantity: 1
})
},
// 分享
onShareAppMessage() {
return {
title: this.data.product.name,
path: `/pages/product/detail?id=${this.data.product.id}`,
imageUrl: this.data.product.image
}
}
})
真实项目经验
经验一:分包优化实战
我们项目最开始没做分包,所有页面都在主包,结果主包3.2MB,超过2MB限制发布不了。产
品还在催着上线,压力很大。
我花了一天时间分析代码结构,列出所有页面和组件,按业务模块归类。发现商品相关页面
有15个,订单相关10个,用户相关8个,活动相关5个。我决定按这个结构拆分包。
主包只保留首页、购物车、我的这3个tabbar页面,加上登录、搜索这些公共页面。商品、
订单、用户、活动各拆一个分包。还有个直播功能,访问量不大但代码挺多的,拆成独立分
包。
拆的过程遇到个大坑,分包引用了其他分包的组件,编译报错找不到模块。后来才知道,分
包只能引用主包的东西,不能跨分包引用。我把公共组件都提到主包components目录,工具
类提到utils目录,这样所有分包都能用。
还有个问题是图片资源。原来图片都在pages目录下,拆分包后路径对不上了。我把图片统
一移到根目录的images文件夹,用绝对路径引用,这样不管哪个包都能访问。
配置preloadRule时我仔细斟酌了策略。首页预加载商品分包,因为90%用户会去看商品。商
品列表预加载订单分包,因为很多用户看完商品会下单。但都设置成wifi环境,不浪费用户
流量。
改完之后,主包1.8MB,商品包1.5MB,订单包1.2MB,其他包都不到1MB。总包还是3.2MB没
变,但拆成5个包了,首屏只需要下载1.8MB。上线后首屏加载速度快了40%,用户反馈好多
了。
这次经历让我学到,分包不是越多越好,要根据业务特点合理规划。还有就是公共资源一定
要放主包,否则会遇到很多奇怪的问题。
经验二:页面栈管理踩坑
我们项目有个"活动-商品-详情-下单-支付-结果"这样的流程,一路navigateTo下去就6层了。
再从商品详情点推荐商品,又是几层,很容易超过10层限制。
一开始用户反馈说点了没反应,我才发现是页面栈满了,navigateTo失败了。我查了下文档,
说是最多10层,超过了就只能用redirectTo或者switchTab。
但redirectTo会替换当前页面,用户返回不了之前的页面,体验很差。我想了个办法,封装
一个智能路由,快到10层时自动redirectTo,同时把被替换的页面信息存storage里。
结果又遇到新问题,用户从支付结果页返回,期望回到首页,但实际返回到了订单确认页。因
为中间redirectTo过,页面栈不连续了。我又加了个记录机制,记录每次replace的页面,
返回时判断要不要跳过这些页面。
后来发现这个方案还是有问题,就是页面参数丢了。replace的页面没有真实进栈,参数也没
保存下来。我改成把参数也存storage,键用页面路径,值用JSON字符串。页面onLoad时先检
查storage有没有参数,有就用storage的,没有就用url的。
还有一个复杂的场景是tab切换。switchTab会清空页面栈,切换前的导航历史全丢了。但产品
要求切回来时能继续之前的操作。我在切换前把当前栈信息全部存storage,切回来时判断是
否需要恢复,如果需要就navigateTo恢复栈。
整个方案做下来,代码挺复杂的,但用户体验确实提升了。现在可以无限层级跳转,不会出现
点了没反应的情况。而且返回逻辑也符合预期,该回哪儿就回哪儿。
这个经历让我深刻理解了小程序的页面栈机制,也知道了如何绕过它的限制。但说实话,如果
小程序能支持更多层级,或者提供原生的栈管理API,开发会轻松很多。
面试常见追问
Q: 分包和主包的资源引用规则是什么? A: 主包可以引用主包的资源,分包可以引用主包和自己的资源,但分包不能引用其他分包的资源。所以公共组件、工具类、样式文件都要放主包。如果分包间确实需要共享代码,只能提到主包,或者各分包各自复制一份。
Q: 独立分包和普通分包有什么区别? A: 独立分包不依赖主包,可以独立运行。用户从分享链接进来,可以直接打开独立分包页面,不需要先下载主包。但独立分包不能引用主包的资源,也不能使用app.js里的全局数据。适合功能独立、访问独立的场景,比如营销活动页。
Q: 页面栈满了为什么不能直接提示用户? A: 从技术角度可以提示,但用户体验不好。用户不理解什么是"页面栈",告诉他"栈满了"没意义。正确做法是自动处理,比如用redirectTo替换当前页面,或者reLaunch清空栈重新开始。对用户来说应该是无感的。
Q: 如何监控页面栈的使用情况? A: 我会在router封装里记录每次导航操作,包括action类型、目标页面、当前栈深度、时间戳。这些数据上报到监控平台,可以分析用户的导航路径,发现哪些流程容易超出10层,针对性优化。还能发现一些异常情况,比如频繁的reLaunch可能说明某个流程设计有问题。
Q: 生命周期优化具体带来了什么收益? A: 主要三点。一是代码复用,公共逻辑抽到基类,每个页面省了几十行代码,总体减少60%重复代码。二是内存管理,自动清理定时器和请求,避免内存泄漏,crash率下降了30%。三是开发效率,新页面只需要写业务逻辑,不用关心登录检查、埋点这些,开发速度快了很多。