返回笔记首页

7.1 小程序架构设计 - 企业级架构实践

主题配置

简历描述模板

版本一:注重架构能力

plain
负责微信小程序架构设计,采用分包加载策略将主包体积控制在1.8MB以内,分包按需加载
提升首屏速度40%。设计页面栈管理方案,解决小程序10层页面栈限制,实现无限层级跳转。
优化生命周期管理,封装页面基类统一处理公共逻辑,减少重复代码60%。引入Pinia状态
管理,建立模块化store架构,支持状态持久化和跨页面通信。搭建CI/CD自动化流水线,
实现代码提交自动构建、测试、上传,发版周期从2天缩短至2小时。

版本二:强调工程化实践

plain
主导小程序工程化建设,制定目录规范、编码规范、组件规范等开发标准。设计分包架构
将业务模块解耦,主包仅保留核心功能和通用组件,各业务线独立分包,包体积优化45%。
封装Router导航管理器,支持路由拦截、参数传递、返回控制等高级特性。基于Pinia实现
轻量级状态管理,配合持久化插件实现跨会话数据保留。建立自动化测试体系,单元测试
覆盖率80%,集成测试覆盖核心流程。接入小程序CI接口实现自动化发布,支持灰度发布
和版本回滚。

版本三:突出业务价值

plain
作为小程序技术负责人,推动架构升级和工程化改造,显著提升开发效率和代码质量。通
过分包优化使首屏加载速度提升40%,用户跳出率下降15%。页面栈管理方案解决了深层级
跳转问题,支持10+个模块的无限层级导航。状态管理重构后,跨页面通信代码减少70%,
页面间数据一致性问题从月均30+降至个位数。CI/CD自动化使发版效率提升5倍,线上bug
修复响应时间从1天缩短至2小时。项目支撑公司3条业务线,累计服务200万+用户。

SOP 标准回答

Q1: 小程序的分包加载策略是怎么设计的?

标准回答

plain
我们项目一开始主包就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: 页面栈管理是怎么实现的?

标准回答

plain
小程序有个限制,页面栈最多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: 小程序的生命周期是怎么优化的?

标准回答

plain
小程序的生命周期比较复杂,有应用生命周期、页面生命周期、组件生命周期,管理起来
很繁琐。我做了几个优化:

第一是封装页面基类。每个页面都要做的事情,比如登录检查、埋点上报、分享配置,我
放在基类里统一处理。页面只需要继承这个基类,自动就有这些能力。

具体实现是用装饰器模式,写了个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分包配置

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. 分包加载管理器

javascript
// 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. 分包路由配置

javascript
// 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. 智能路由跳转

javascript
// 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. 页面栈管理器

javascript
// 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. 路由拦截器

javascript
// 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类

javascript
// 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. 页面基类

javascript
// 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. 使用示例

javascript
// 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
    }
  }
})

真实项目经验

经验一:分包优化实战

plain
我们项目最开始没做分包,所有页面都在主包,结果主包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%,用户反馈好多
了。

这次经历让我学到,分包不是越多越好,要根据业务特点合理规划。还有就是公共资源一定
要放主包,否则会遇到很多奇怪的问题。

经验二:页面栈管理踩坑

plain
我们项目有个"活动-商品-详情-下单-支付-结果"这样的流程,一路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%。三是开发效率,新页面只需要写业务逻辑,不用关心登录检查、埋点这些,开发速度快了很多。