小程序"邪修"秘籍:那些官方文档不会告诉你的骚操作

摘要:小程序开发,表面上是"戴着镣铐跳舞",实际上是"在夹缝中求生存"。官方文档写得云淡风轻,实际开发却处处是坑。本文收录了多年小程序开发中积累的"邪修"技巧------那些不太正经但确实有效的解决方案。警告:部分技巧可能随时失效,请谨慎使用。


引言:小程序开发者的日常崩溃

场景一: 产品经理:"这个页面能不能像 H5 一样丝滑滚动?" 你:"小程序有性能限制..." 产品经理:"竞品可以。" 你:(内心崩溃)

场景二: 设计师:"这个动画效果很简单啊,就是一个弹性回弹。" 你:"小程序的动画 API..." 设计师:"Figma 里一秒钟就做出来了。" 你:(开始掉头发)

场景三: 测试:"这个在 iOS 上正常,安卓上怎么崩了?" 你:"我看看..."(打开微信开发者工具,一切正常) 你:"真机调试..."(问题复现) 你:"这..." 测试:"还有,在微信 8.0.32 版本上也有问题。" 你:(想转行)

欢迎来到小程序开发的世界。

今天,我要分享一些"邪修"技巧------那些官方文档不会告诉你,但能救你命的骚操作。


第一章:性能优化の黑魔法

1.1 setData 的"分片"艺术

问题: setData 数据量大时,页面卡顿严重。

官方建议: 减少 setData 的数据量。

邪修技巧: 数据分片 + 路径更新

javascript 复制代码
// ❌ 错误做法:一次性更新大数组
this.setData({
  list: newList, // 假设有 1000 条数据
})

// ✅ 邪修技巧一:路径更新(只更新变化的部分)
// 假设只有第 5 条数据变了
this.setData({
  "list[5].name": "新名字",
  "list[5].status": "updated",
})

// ✅ 邪修技巧二:分片更新(大数据量时)
async function setDataInChunks(data, chunkSize = 20) {
  const keys = Object.keys(data)
  for (let i = 0; i < keys.length; i += chunkSize) {
    const chunk = {}
    keys.slice(i, i + chunkSize).forEach((key) => {
      chunk[key] = data[key]
    })
    await new Promise((resolve) => {
      this.setData(chunk, resolve)
    })
  }
}

// ✅ 邪修技巧三:虚拟列表(终极方案)
// 只渲染可视区域的数据
Page({
  data: {
    visibleList: [], // 当前可见的数据
    startIndex: 0, // 起始索引
    itemHeight: 100, // 每项高度
    containerHeight: 0, // 容器高度
  },

  fullList: [], // 完整数据放在非响应式属性中

  onScroll(e) {
    const { scrollTop } = e.detail
    const startIndex = Math.floor(scrollTop / this.data.itemHeight)
    const visibleCount =
      Math.ceil(this.data.containerHeight / this.data.itemHeight) + 2

    // 只有 startIndex 变化时才更新
    if (startIndex !== this.data.startIndex) {
      this.setData({
        startIndex,
        visibleList: this.fullList.slice(startIndex, startIndex + visibleCount),
      })
    }
  },
})

1.2 图片加载の"障眼法"

问题: 大量图片加载时,页面白屏或卡顿。

邪修技巧: 渐进式加载 + 占位图 + 懒加载

html 复制代码
<!-- WXML -->
<view class="image-wrapper">
  <!-- 占位骨架 -->
  <view
    class="skeleton"
    wx:if="{{!imageLoaded}}"
    style="background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); background-size: 200% 100%; animation: shimmer 1.5s infinite;"
  />

  <!-- 缩略图(先加载小图) -->
  <image
    wx:if="{{!imageLoaded}}"
    class="thumbnail"
    src="{{thumbnailUrl}}"
    mode="aspectFill"
  />

  <!-- 原图(懒加载) -->
  <image
    class="main-image {{imageLoaded ? 'loaded' : ''}}"
    src="{{imageUrl}}"
    mode="aspectFill"
    lazy-load
    bindload="onImageLoad"
    binderror="onImageError"
  />
</view>
javascript 复制代码
// JS
Page({
  data: {
    imageLoaded: false,
    thumbnailUrl: "", // 缩略图 URL(可以用 OSS 的图片处理参数生成)
    imageUrl: "",
  },

  onLoad() {
    const originalUrl = "https://example.com/big-image.jpg"
    this.setData({
      // 阿里云 OSS 图片处理:生成 100px 宽的缩略图
      thumbnailUrl: `${originalUrl}?x-oss-process=image/resize,w_100`,
      imageUrl: originalUrl,
    })
  },

  onImageLoad() {
    this.setData({ imageLoaded: true })
  },

  onImageError() {
    // 加载失败时显示默认图
    this.setData({
      imageUrl: "/images/default.png",
      imageLoaded: true,
    })
  },
})
css 复制代码
/* WXSS */
.image-wrapper {
  position: relative;
  width: 100%;
  height: 200px;
  overflow: hidden;
}

.skeleton {
  position: absolute;
  inset: 0;
}

.thumbnail {
  position: absolute;
  inset: 0;
  filter: blur(10px);
  transform: scale(1.1);
}

.main-image {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 0.3s ease;
}

.main-image.loaded {
  opacity: 1;
}

@keyframes shimmer {
  0% {
    background-position: -200% 0;
  }
  100% {
    background-position: 200% 0;
  }
}

1.3 长列表の"回收站"策略

问题: 无限滚动列表,滚动久了内存爆炸。

邪修技巧: DOM 回收 + 骨架占位

javascript 复制代码
// 核心思路:只保留可视区域 ± 缓冲区的真实 DOM,其他用骨架占位
Component({
  data: {
    list: [],
    recycledIndexes: new Set(), // 被回收的索引
  },

  // 配置
  BUFFER_SIZE: 5, // 缓冲区大小
  RECYCLE_THRESHOLD: 20, // 超出多少开始回收

  methods: {
    onScroll(e) {
      const { scrollTop } = e.detail
      const itemHeight = 120
      const viewportHeight = this.viewportHeight || 600

      // 计算可视区域
      const startIndex = Math.floor(scrollTop / itemHeight)
      const endIndex = Math.ceil((scrollTop + viewportHeight) / itemHeight)

      // 计算需要保留的范围(可视区 + 缓冲区)
      const keepStart = Math.max(0, startIndex - this.BUFFER_SIZE)
      const keepEnd = Math.min(
        this.data.list.length,
        endIndex + this.BUFFER_SIZE
      )

      // 回收超出范围的 DOM
      const recycledIndexes = new Set()
      this.data.list.forEach((_, index) => {
        if (
          index < keepStart - this.RECYCLE_THRESHOLD ||
          index > keepEnd + this.RECYCLE_THRESHOLD
        ) {
          recycledIndexes.add(index)
        }
      })

      this.setData({ recycledIndexes: [...recycledIndexes] })
    },
  },
})
html 复制代码
<!-- WXML:根据是否被回收显示不同内容 -->
<scroll-view scroll-y bindscroll="onScroll" style="height: 100vh;">
  <view wx:for="{{list}}" wx:key="id" class="list-item" style="height: 120px;">
    <!-- 被回收的显示骨架 -->
    <block wx:if="{{recycledIndexes.includes(index)}}">
      <view class="skeleton-item" />
    </block>

    <!-- 未被回收的显示真实内容 -->
    <block wx:else>
      <image src="{{item.image}}" lazy-load />
      <view class="content">
        <text>{{item.title}}</text>
        <text>{{item.desc}}</text>
      </view>
    </block>
  </view>
</scroll-view>

第二章:样式の"奇技淫巧"

2.1 安全区域の"万能公式"

问题: iPhone 刘海屏、底部安全区域适配。

邪修技巧: CSS 变量 + env() + 兜底值

css 复制代码
/* 在 app.wxss 中定义全局变量 */
page {
  --safe-area-top: env(safe-area-inset-top, 0px);
  --safe-area-bottom: env(safe-area-inset-bottom, 0px);
  --safe-area-left: env(safe-area-inset-left, 0px);
  --safe-area-right: env(safe-area-inset-right, 0px);

  /* 导航栏高度(状态栏 + 导航栏) */
  --nav-height: calc(var(--safe-area-top) + 44px);

  /* 底部 TabBar 高度 */
  --tabbar-height: calc(var(--safe-area-bottom) + 50px);
}

/* 自定义导航栏 */
.custom-navbar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: var(--nav-height);
  padding-top: var(--safe-area-top);
  background: #fff;
  z-index: 999;
}

/* 页面内容(避开导航栏) */
.page-content {
  padding-top: var(--nav-height);
  padding-bottom: var(--tabbar-height);
  min-height: 100vh;
  box-sizing: border-box;
}

/* 底部固定按钮 */
.bottom-button {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  padding: 12px 16px;
  padding-bottom: calc(12px + var(--safe-area-bottom));
  background: #fff;
}

2.2 1px 边框の"像素级"方案

问题: 在高清屏上,1px 边框看起来太粗。

邪修技巧: 伪元素 + transform 缩放

css 复制代码
/* 通用 1px 边框 mixin(用 class 实现) */

/* 底部 1px 边框 */
.border-bottom-1px {
  position: relative;
}

.border-bottom-1px::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 1px;
  background: #e5e5e5;
  transform: scaleY(0.5);
  transform-origin: 0 100%;
}

/* 四周 1px 边框 */
.border-1px {
  position: relative;
}

.border-1px::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 200%;
  height: 200%;
  border: 1px solid #e5e5e5;
  border-radius: inherit;
  transform: scale(0.5);
  transform-origin: 0 0;
  pointer-events: none;
  box-sizing: border-box;
}

/* 带圆角的 1px 边框 */
.border-1px-radius {
  position: relative;
  border-radius: 8px;
}

.border-1px-radius::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 200%;
  height: 200%;
  border: 1px solid #e5e5e5;
  border-radius: 16px; /* 圆角也要 *2 */
  transform: scale(0.5);
  transform-origin: 0 0;
  pointer-events: none;
  box-sizing: border-box;
}

2.3 文字截断の"终极方案"

css 复制代码
/* 单行截断 */
.text-ellipsis {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* 多行截断(兼容性最好的方案) */
.text-ellipsis-2 {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  text-overflow: ellipsis;
  word-break: break-all;
}

/* 多行截断 + 展开收起(需要 JS 配合) */
.text-expandable {
  position: relative;
  max-height: calc(1.5em * 3); /* 3 行 */
  overflow: hidden;
  transition: max-height 0.3s ease;
}

.text-expandable.expanded {
  max-height: none;
}

.text-expandable::after {
  content: "...展开";
  position: absolute;
  right: 0;
  bottom: 0;
  padding-left: 20px;
  background: linear-gradient(to right, transparent, #fff 50%);
  color: #1890ff;
}

.text-expandable.expanded::after {
  content: none;
}

第三章:交互の"黑科技"

3.1 下拉刷新の"自定义"方案

问题: 原生下拉刷新样式太丑,无法自定义。

邪修技巧: 禁用原生 + 自己实现

json 复制代码
// page.json
{
  "enablePullDownRefresh": false,
  "disableScroll": false
}
html 复制代码
<!-- WXML -->
<view class="pull-refresh-container">
  <!-- 下拉提示区域 -->
  <view
    class="pull-refresh-header"
    style="transform: translateY({{pullDistance - 80}}px);"
  >
    <view
      class="refresh-icon {{refreshing ? 'rotating' : ''}}"
      style="transform: rotate({{pullDistance * 2}}deg);"
    >
      ↻
    </view>
    <text>{{refreshText}}</text>
  </view>

  <!-- 内容区域 -->
  <scroll-view
    scroll-y
    class="content-scroll"
    style="transform: translateY({{pullDistance}}px);"
    bindtouchstart="onTouchStart"
    bindtouchmove="onTouchMove"
    bindtouchend="onTouchEnd"
    bindscroll="onScroll"
  >
    <slot />
  </scroll-view>
</view>
javascript 复制代码
// JS
Component({
  data: {
    pullDistance: 0,
    refreshing: false,
    refreshText: "下拉刷新",
    startY: 0,
    scrollTop: 0,
  },

  THRESHOLD: 80, // 触发刷新的阈值

  methods: {
    onTouchStart(e) {
      if (this.data.refreshing) return
      this.setData({ startY: e.touches[0].clientY })
    },

    onTouchMove(e) {
      if (this.data.refreshing) return
      if (this.data.scrollTop > 0) return // 不在顶部时不触发

      const currentY = e.touches[0].clientY
      const distance = currentY - this.data.startY

      if (distance > 0) {
        // 阻尼效果:拉得越远,阻力越大
        const pullDistance = Math.min(distance * 0.5, 120)
        const refreshText =
          pullDistance >= this.THRESHOLD ? "释放刷新" : "下拉刷新"

        this.setData({ pullDistance, refreshText })
      }
    },

    onTouchEnd() {
      if (this.data.refreshing) return

      if (this.data.pullDistance >= this.THRESHOLD) {
        // 触发刷新
        this.setData({
          pullDistance: this.THRESHOLD,
          refreshing: true,
          refreshText: "刷新中...",
        })

        this.triggerEvent("refresh")
      } else {
        // 回弹
        this.setData({ pullDistance: 0 })
      }
    },

    onScroll(e) {
      this.setData({ scrollTop: e.detail.scrollTop })
    },

    // 外部调用:刷新完成
    stopRefresh() {
      this.setData({
        pullDistance: 0,
        refreshing: false,
        refreshText: "下拉刷新",
      })
    },
  },
})

3.2 手势密码の"纯 Canvas"实现

javascript 复制代码
// 手势密码组件
Component({
  data: {
    points: [], // 9 个点的坐标
    selectedPoints: [], // 已选中的点
    touchPoint: null, // 当前触摸点
  },

  lifetimes: {
    attached() {
      this.initCanvas()
    },
  },

  methods: {
    initCanvas() {
      const query = this.createSelectorQuery()
      query
        .select("#gesture-canvas")
        .fields({ node: true, size: true })
        .exec((res) => {
          const canvas = res[0].node
          const ctx = canvas.getContext("2d")

          // 设置 canvas 尺寸
          const dpr = wx.getSystemInfoSync().pixelRatio
          canvas.width = res[0].width * dpr
          canvas.height = res[0].height * dpr
          ctx.scale(dpr, dpr)

          this.canvas = canvas
          this.ctx = ctx
          this.canvasWidth = res[0].width
          this.canvasHeight = res[0].height

          // 初始化 9 个点
          this.initPoints()
          this.draw()
        })
    },

    initPoints() {
      const padding = 50
      const width = this.canvasWidth - padding * 2
      const gap = width / 2
      const points = []

      for (let row = 0; row < 3; row++) {
        for (let col = 0; col < 3; col++) {
          points.push({
            x: padding + col * gap,
            y: padding + row * gap,
            index: row * 3 + col,
          })
        }
      }

      this.setData({ points })
    },

    draw() {
      const { ctx, canvasWidth, canvasHeight } = this
      const { points, selectedPoints, touchPoint } = this.data

      // 清空画布
      ctx.clearRect(0, 0, canvasWidth, canvasHeight)

      // 画连线
      if (selectedPoints.length > 0) {
        ctx.beginPath()
        ctx.strokeStyle = "#1890ff"
        ctx.lineWidth = 3
        ctx.lineCap = "round"
        ctx.lineJoin = "round"

        selectedPoints.forEach((pointIndex, i) => {
          const point = points[pointIndex]
          if (i === 0) {
            ctx.moveTo(point.x, point.y)
          } else {
            ctx.lineTo(point.x, point.y)
          }
        })

        // 连接到当前触摸点
        if (touchPoint) {
          ctx.lineTo(touchPoint.x, touchPoint.y)
        }

        ctx.stroke()
      }

      // 画点
      points.forEach((point, index) => {
        const isSelected = selectedPoints.includes(index)

        // 外圈
        ctx.beginPath()
        ctx.arc(point.x, point.y, 25, 0, Math.PI * 2)
        ctx.strokeStyle = isSelected ? "#1890ff" : "#ddd"
        ctx.lineWidth = 2
        ctx.stroke()

        // 内圈
        ctx.beginPath()
        ctx.arc(point.x, point.y, isSelected ? 10 : 5, 0, Math.PI * 2)
        ctx.fillStyle = isSelected ? "#1890ff" : "#ddd"
        ctx.fill()
      })
    },

    onTouchStart(e) {
      this.setData({ selectedPoints: [], touchPoint: null })
      this.handleTouch(e)
    },

    onTouchMove(e) {
      this.handleTouch(e)
    },

    onTouchEnd() {
      const { selectedPoints } = this.data

      if (selectedPoints.length >= 4) {
        // 触发事件,返回密码
        this.triggerEvent("complete", {
          password: selectedPoints.join(""),
        })
      } else if (selectedPoints.length > 0) {
        // 密码太短
        this.triggerEvent("error", {
          message: "至少连接 4 个点",
        })
      }

      this.setData({ touchPoint: null })
      this.draw()
    },

    handleTouch(e) {
      const touch = e.touches[0]
      const { points, selectedPoints } = this.data

      // 获取相对于 canvas 的坐标
      const query = this.createSelectorQuery()
      query
        .select("#gesture-canvas")
        .boundingClientRect((rect) => {
          const x = touch.clientX - rect.left
          const y = touch.clientY - rect.top

          // 检查是否触碰到某个点
          points.forEach((point, index) => {
            const distance = Math.sqrt(
              Math.pow(x - point.x, 2) + Math.pow(y - point.y, 2)
            )

            if (distance < 30 && !selectedPoints.includes(index)) {
              selectedPoints.push(index)
              this.setData({ selectedPoints })
            }
          })

          this.setData({ touchPoint: { x, y } })
          this.draw()
        })
        .exec()
    },
  },
})

第四章:数据の"骚操作"

4.1 本地存储の"加密"方案

问题: wx.setStorageSync 存储的数据是明文,容易被篡改。

邪修技巧: 简单加密 + 签名校验

javascript 复制代码
// utils/secureStorage.js
const SECRET_KEY = "your-secret-key-here" // 实际项目中应该更复杂

// 简单的加密函数(生产环境建议用更强的加密)
function encrypt(data) {
  const str = JSON.stringify(data)
  // Base64 编码 + 简单混淆
  const base64 = wx.arrayBufferToBase64(new TextEncoder().encode(str))
  // 添加签名
  const signature = generateSignature(base64)
  return `${base64}.${signature}`
}

function decrypt(encryptedData) {
  try {
    const [base64, signature] = encryptedData.split(".")

    // 验证签名
    if (generateSignature(base64) !== signature) {
      console.warn("数据签名验证失败,可能被篡改")
      return null
    }

    // 解码
    const buffer = wx.base64ToArrayBuffer(base64)
    const str = new TextDecoder().decode(buffer)
    return JSON.parse(str)
  } catch (e) {
    console.error("解密失败", e)
    return null
  }
}

// 生成签名(简单实现,生产环境用 HMAC)
function generateSignature(data) {
  let hash = 0
  const str = data + SECRET_KEY
  for (let i = 0; i < str.length; i++) {
    const char = str.charCodeAt(i)
    hash = (hash << 5) - hash + char
    hash = hash & hash
  }
  return Math.abs(hash).toString(16)
}

// 封装的安全存储 API
export const secureStorage = {
  set(key, value, options = {}) {
    const { encrypt: shouldEncrypt = true, expire = 0 } = options

    const data = {
      value,
      timestamp: Date.now(),
      expire: expire > 0 ? Date.now() + expire : 0,
    }

    const storageValue = shouldEncrypt ? encrypt(data) : JSON.stringify(data)
    wx.setStorageSync(key, storageValue)
  },

  get(key, options = {}) {
    const { decrypt: shouldDecrypt = true, defaultValue = null } = options

    try {
      const storageValue = wx.getStorageSync(key)
      if (!storageValue) return defaultValue

      const data = shouldDecrypt
        ? decrypt(storageValue)
        : JSON.parse(storageValue)

      if (!data) return defaultValue

      // 检查是否过期
      if (data.expire > 0 && Date.now() > data.expire) {
        wx.removeStorageSync(key)
        return defaultValue
      }

      return data.value
    } catch (e) {
      return defaultValue
    }
  },

  remove(key) {
    wx.removeStorageSync(key)
  },
}

// 使用示例
secureStorage.set("userToken", "abc123", { expire: 7 * 24 * 60 * 60 * 1000 }) // 7天过期
const token = secureStorage.get("userToken")

4.2 请求の"智能重试"

javascript 复制代码
// utils/request.js
const MAX_RETRY = 3
const RETRY_DELAY = 1000

// 判断是否应该重试
function shouldRetry(error, retryCount) {
  if (retryCount >= MAX_RETRY) return false

  // 网络错误重试
  if (error.errMsg?.includes("request:fail")) return true

  // 超时重试
  if (error.errMsg?.includes("timeout")) return true

  // 5xx 错误重试
  if (error.statusCode >= 500) return true

  return false
}

// 延迟函数
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))

// 带重试的请求
async function requestWithRetry(options, retryCount = 0) {
  try {
    const response = await new Promise((resolve, reject) => {
      wx.request({
        ...options,
        success: (res) => {
          if (res.statusCode >= 200 && res.statusCode < 300) {
            resolve(res)
          } else {
            reject({ ...res, errMsg: `HTTP ${res.statusCode}` })
          }
        },
        fail: reject,
      })
    })

    return response
  } catch (error) {
    if (shouldRetry(error, retryCount)) {
      console.log(
        `请求失败,${RETRY_DELAY}ms 后重试 (${retryCount + 1}/${MAX_RETRY})`
      )

      // 指数退避
      await delay(RETRY_DELAY * Math.pow(2, retryCount))

      return requestWithRetry(options, retryCount + 1)
    }

    throw error
  }
}

// 请求队列(防止并发过多)
class RequestQueue {
  constructor(maxConcurrent = 5) {
    this.maxConcurrent = maxConcurrent
    this.currentCount = 0
    this.queue = []
  }

  async add(requestFn) {
    if (this.currentCount >= this.maxConcurrent) {
      // 等待队列
      await new Promise((resolve) => this.queue.push(resolve))
    }

    this.currentCount++

    try {
      return await requestFn()
    } finally {
      this.currentCount--

      // 释放队列中的下一个
      if (this.queue.length > 0) {
        const next = this.queue.shift()
        next()
      }
    }
  }
}

const requestQueue = new RequestQueue(5)

// 最终封装的请求函数
export async function request(options) {
  return requestQueue.add(() =>
    requestWithRetry({
      timeout: 10000,
      ...options,
      header: {
        "Content-Type": "application/json",
        ...options.header,
      },
    })
  )
}

4.3 全局状态の"响应式"方案

问题: 小程序没有 Vuex/Redux,跨页面状态管理很麻烦。

邪修技巧: 简易响应式 Store

javascript 复制代码
// store/index.js
class Store {
  constructor(initialState = {}) {
    this.state = initialState
    this.listeners = new Map()
    this.computedCache = new Map()
  }

  // 获取状态
  getState(path) {
    if (!path) return this.state
    return path.split(".").reduce((obj, key) => obj?.[key], this.state)
  }

  // 设置状态
  setState(path, value) {
    const keys = path.split(".")
    const lastKey = keys.pop()
    const target = keys.reduce((obj, key) => {
      if (!obj[key]) obj[key] = {}
      return obj[key]
    }, this.state)

    const oldValue = target[lastKey]
    target[lastKey] = value

    // 通知监听者
    this.notify(path, value, oldValue)
  }

  // 批量更新
  batchUpdate(updates) {
    Object.entries(updates).forEach(([path, value]) => {
      this.setState(path, value)
    })
  }

  // 订阅变化
  subscribe(path, callback, immediate = false) {
    if (!this.listeners.has(path)) {
      this.listeners.set(path, new Set())
    }
    this.listeners.get(path).add(callback)

    // 立即执行一次
    if (immediate) {
      callback(this.getState(path), undefined)
    }

    // 返回取消订阅函数
    return () => {
      this.listeners.get(path)?.delete(callback)
    }
  }

  // 通知监听者
  notify(path, newValue, oldValue) {
    // 精确匹配
    this.listeners.get(path)?.forEach((cb) => cb(newValue, oldValue))

    // 父路径也要通知(如 'user' 变化时,'user.name' 的监听者也要通知)
    const parts = path.split(".")
    for (let i = parts.length - 1; i > 0; i--) {
      const parentPath = parts.slice(0, i).join(".")
      this.listeners.get(parentPath)?.forEach((cb) => {
        cb(this.getState(parentPath), undefined)
      })
    }

    // 清除相关的计算缓存
    this.computedCache.clear()
  }

  // 计算属性
  computed(name, getter) {
    if (this.computedCache.has(name)) {
      return this.computedCache.get(name)
    }

    const value = getter(this.state)
    this.computedCache.set(name, value)
    return value
  }
}

// 创建全局 store
export const store = new Store({
  user: null,
  cart: [],
  settings: {
    theme: "light",
    language: "zh-CN",
  },
})

// 页面 Mixin:自动绑定 store 到页面 data
export function connectStore(mapState = {}) {
  return {
    data: {},

    onLoad() {
      this._storeUnsubscribes = []

      // 订阅 store 变化
      Object.entries(mapState).forEach(([dataKey, storePath]) => {
        // 初始化数据
        this.setData({ [dataKey]: store.getState(storePath) })

        // 订阅变化
        const unsubscribe = store.subscribe(storePath, (value) => {
          this.setData({ [dataKey]: value })
        })

        this._storeUnsubscribes.push(unsubscribe)
      })
    },

    onUnload() {
      // 取消订阅
      this._storeUnsubscribes?.forEach((unsub) => unsub())
    },
  }
}

// 使用示例
// pages/cart/cart.js
import { store, connectStore } from "../../store/index"

Page({
  ...connectStore({
    cartItems: "cart",
    user: "user",
  }),

  addToCart(item) {
    const cart = store.getState("cart")
    store.setState("cart", [...cart, item])
  },

  clearCart() {
    store.setState("cart", [])
  },
})

第五章:调试の"神器"

5.1 自制调试面板

javascript 复制代码
// components/debug-panel/debug-panel.js
Component({
  data: {
    visible: false,
    logs: [],
    systemInfo: {},
    networkType: "",
    performance: {},
  },

  lifetimes: {
    attached() {
      // 劫持 console
      this.hijackConsole()

      // 获取系统信息
      this.getSystemInfo()

      // 监听网络变化
      this.watchNetwork()

      // 性能监控
      this.watchPerformance()
    },
  },

  methods: {
    toggle() {
      this.setData({ visible: !this.data.visible })
    },

    hijackConsole() {
      const originalLog = console.log
      const originalError = console.error
      const originalWarn = console.warn

      const addLog = (type, args) => {
        const log = {
          type,
          content: args
            .map((arg) =>
              typeof arg === "object"
                ? JSON.stringify(arg, null, 2)
                : String(arg)
            )
            .join(" "),
          time: new Date().toLocaleTimeString(),
        }

        this.setData({
          logs: [...this.data.logs.slice(-99), log], // 最多保留 100 条
        })
      }

      console.log = (...args) => {
        addLog("log", args)
        originalLog.apply(console, args)
      }

      console.error = (...args) => {
        addLog("error", args)
        originalError.apply(console, args)
      }

      console.warn = (...args) => {
        addLog("warn", args)
        originalWarn.apply(console, args)
      }
    },

    getSystemInfo() {
      const systemInfo = wx.getSystemInfoSync()
      this.setData({ systemInfo })
    },

    watchNetwork() {
      wx.getNetworkType({
        success: (res) => {
          this.setData({ networkType: res.networkType })
        },
      })

      wx.onNetworkStatusChange((res) => {
        this.setData({ networkType: res.networkType })
      })
    },

    watchPerformance() {
      // 获取性能数据
      const performance = wx.getPerformance()
      const observer = performance.createObserver((entryList) => {
        const entries = entryList.getEntries()
        entries.forEach((entry) => {
          console.log(`[Performance] ${entry.name}: ${entry.duration}ms`)
        })
      })

      observer.observe({ entryTypes: ["render", "script", "navigation"] })
    },

    clearLogs() {
      this.setData({ logs: [] })
    },

    copyLogs() {
      const text = this.data.logs
        .map((log) => `[${log.time}] [${log.type}] ${log.content}`)
        .join("\n")

      wx.setClipboardData({
        data: text,
        success: () => {
          wx.showToast({ title: "已复制到剪贴板" })
        },
      })
    },
  },
})
html 复制代码
<!-- debug-panel.wxml -->
<view class="debug-trigger" bindtap="toggle">🐛</view>

<view class="debug-panel {{visible ? 'visible' : ''}}">
  <view class="debug-header">
    <text>调试面板</text>
    <text bindtap="toggle">✕</text>
  </view>

  <view class="debug-tabs">
    <text class="tab active">日志</text>
    <text class="tab">系统</text>
    <text class="tab">网络</text>
  </view>

  <scroll-view class="debug-content" scroll-y>
    <view wx:for="{{logs}}" wx:key="index" class="log-item log-{{item.type}}">
      <text class="log-time">{{item.time}}</text>
      <text class="log-content">{{item.content}}</text>
    </view>
  </scroll-view>

  <view class="debug-footer">
    <button size="mini" bindtap="clearLogs">清空</button>
    <button size="mini" bindtap="copyLogs">复制</button>
  </view>
</view>

5.2 性能监控埋点

javascript 复制代码
// utils/performance.js
class PerformanceMonitor {
  constructor() {
    this.marks = new Map()
    this.measures = []
  }

  // 标记开始
  mark(name) {
    this.marks.set(name, Date.now())
  }

  // 测量耗时
  measure(name, startMark, endMark) {
    const start = this.marks.get(startMark)
    const end = endMark ? this.marks.get(endMark) : Date.now()

    if (!start) {
      console.warn(`Mark "${startMark}" not found`)
      return
    }

    const duration = end - start
    const measure = { name, duration, timestamp: Date.now() }

    this.measures.push(measure)

    // 超过阈值告警
    if (duration > 1000) {
      console.warn(`[Performance] ${name} 耗时 ${duration}ms,超过 1s 阈值`)
    }

    return duration
  }

  // 自动测量函数执行时间
  async measureAsync(name, fn) {
    const startMark = `${name}_start`
    this.mark(startMark)

    try {
      const result = await fn()
      this.measure(name, startMark)
      return result
    } catch (error) {
      this.measure(`${name}_error`, startMark)
      throw error
    }
  }

  // 获取报告
  getReport() {
    return {
      measures: this.measures,
      summary: {
        total: this.measures.length,
        avgDuration:
          this.measures.reduce((sum, m) => sum + m.duration, 0) /
          this.measures.length,
        maxDuration: Math.max(...this.measures.map((m) => m.duration)),
        slowCount: this.measures.filter((m) => m.duration > 1000).length,
      },
    }
  }

  // 上报数据
  report() {
    const report = this.getReport()

    // 上报到服务器
    wx.request({
      url: "https://your-api.com/performance",
      method: "POST",
      data: report,
    })

    // 清空数据
    this.measures = []
  }
}

export const perfMonitor = new PerformanceMonitor()

// 使用示例
// 页面加载性能监控
Page({
  onLoad() {
    perfMonitor.mark("pageLoad_start")
  },

  onReady() {
    perfMonitor.measure("pageLoad", "pageLoad_start")
  },

  async fetchData() {
    const data = await perfMonitor.measureAsync("fetchData", async () => {
      const res = await request({ url: "/api/data" })
      return res.data
    })

    this.setData({ data })
  },
})

写在最后:邪修有风险,使用需谨慎

这些"邪修"技巧,都是在实际项目中踩坑后总结出来的。

它们有几个共同特点:

  1. 官方文档不会告诉你:因为这些不是"标准做法"
  2. 可能随时失效:微信更新后,某些 hack 可能不再有效
  3. 有一定风险:绕过官方限制,可能带来兼容性问题
  4. 但确实有效:在特定场景下,能解决实际问题

使用建议:

  • 优先使用官方方案
  • 邪修技巧作为备选
  • 做好兼容性测试
  • 关注微信更新日志
  • 随时准备替代方案

最后,愿你的小程序:

  • 性能如丝般顺滑
  • 体验如原生般流畅
  • Bug 如晨露般消散
  • 审核如绿灯般通过

💬 互动时间:你在小程序开发中遇到过什么奇葩问题?用了什么骚操作解决的?评论区分享一下你的"邪修"经验!

觉得这篇文章有用?点赞 + 在看 + 转发,让更多小程序开发者少踩坑~


本文作者是一个在小程序坑里摸爬滚打多年的老开发。关注我,一起在微信的"围墙花园"里优雅地生存。

相关推荐
Charlo1 天前
手把手配置 Ralph -- 火爆 X 的全自动 AI 编程工具
前端·后端·github
我真的叫奥运1 天前
scss mixin svg 颜色控制 以及与 png 方案对比讨论
前端·svg
雲墨款哥1 天前
从一行好奇的代码说起:React的 useEffect 到底是不是生命周期?
前端·react.js·设计模式
weixin_584121431 天前
HTML+layui表单校验范围值,根据条件显示隐藏某输入框
前端·html·layui
加油乐1 天前
react基础概念合集
前端·react.js
小白探索世界欧耶!~1 天前
用iframe实现单个系统页面在多个系统中复用
开发语言·前端·javascript·vue.js·经验分享·笔记·iframe
bl4ckpe4ch1 天前
用可复现实验直观理解 CORS 与 CSRF 的区别与联系
前端·web安全·网络安全·csrf·cors
阿珊和她的猫1 天前
Webpack中import的原理剖析
前端·webpack·node.js
AI前端老薛1 天前
webpack中loader和plugin的区别
前端·webpack