← 返回文章列表

小程序架构设计

整体设计围绕分包治理、路由拦截器、生命周期统一、解决大型小程序在体积超限、页面栈溢出、代码冗余等常见痛点,最终实现业务快速迭代、线上稳定可控、多人高效协作的企业级开发标准。
架构设计 2025-11-23 11:32:35 评论 0

分包加载完整方案

问题背景: 小程序主包限制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
    }
  }
})
评论: