摘要:小程序开发,表面上是"戴着镣铐跳舞",实际上是"在夹缝中求生存"。官方文档写得云淡风轻,实际开发却处处是坑。本文收录了多年小程序开发中积累的"邪修"技巧------那些不太正经但确实有效的解决方案。警告:部分技巧可能随时失效,请谨慎使用。
引言:小程序开发者的日常崩溃
场景一: 产品经理:"这个页面能不能像 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 })
},
})
写在最后:邪修有风险,使用需谨慎
这些"邪修"技巧,都是在实际项目中踩坑后总结出来的。
它们有几个共同特点:
- 官方文档不会告诉你:因为这些不是"标准做法"
- 可能随时失效:微信更新后,某些 hack 可能不再有效
- 有一定风险:绕过官方限制,可能带来兼容性问题
- 但确实有效:在特定场景下,能解决实际问题
使用建议:
- 优先使用官方方案
- 邪修技巧作为备选
- 做好兼容性测试
- 关注微信更新日志
- 随时准备替代方案
最后,愿你的小程序:
- 性能如丝般顺滑
- 体验如原生般流畅
- Bug 如晨露般消散
- 审核如绿灯般通过
💬 互动时间:你在小程序开发中遇到过什么奇葩问题?用了什么骚操作解决的?评论区分享一下你的"邪修"经验!
觉得这篇文章有用?点赞 + 在看 + 转发,让更多小程序开发者少踩坑~
本文作者是一个在小程序坑里摸爬滚打多年的老开发。关注我,一起在微信的"围墙花园"里优雅地生存。