微信小程序劝退指南
序:一场命中注定的相遇
春风十里,不如与你相遇。那天,我怀揣一腔热忱,决定开发某微信小程序。彼时的我,对于无限的可能性心潮澎湃,对于未知的坑洼一无所知。就这样,我踏上了一条荆棘丛生却又不得不走的道路,开始了与微信小程序的相爱相杀。
如果时光可以倒流,我想回到那个天真的自己身边,轻声说一句:“少年,你确定要走这条路吗?“
第一章:TypeScript 的海市蜃楼
作为一个追求类型安全的现代开发者,我自然而然地选择了 TypeScript。微信官方也宣称支持 TypeScript,这让我对未来的开发之旅充满信心。然而,当我真正开始动手时,才发现所谓的”支持”,不过是海市蜃楼中的一座空中楼阁。
官方文档对 TypeScript 的介绍寥寥数语,如同蜻蜓点水,让人摸不着头脑。当我试图为 Page 和 Component 添加泛型时,更是陷入了无尽的迷茫:到底该填什么?官方示例少之又少,网上资料更是稀缺如金。我只能在黑暗中摸索,通过反复试错,才勉强理解了其中的规则。
// 这究竟要填什么?官方文档并未详细说明
Page<DataType, CustomOption>({
data: {
// ...
},
});
微信小程序的 TypeScript 支持,就如同一位言不由衷的恋人,嘴上说着喜欢,行动上却总是敷衍了事。我不禁想问:若不能给予完整的拥抱,何必开启这段感情?
类型声明文件的缺失更是让人头疼。官方提供的.d.ts
文件覆盖面有限,许多 API 和组件的类型定义不够完善,甚至存在错误。当你尝试使用wx.navigateTo
并传入正确的参数时,TypeScript 依然会报错,提示你参数类型不匹配。这时你不得不使用as any
这样的类型断言来绕过检查,这让 TypeScript 的类型安全荡然无存。
// 明明参数正确,却还是报错
wx.navigateTo({
url: "/pages/detail/detail?id=1", // 类型检查可能报错
});
// 不得不使用类型断言绕过检查
wx.navigateTo({
url: "/pages/detail/detail?id=1",
} as any);
网上有不少开发者尝试自行编写更完善的类型声明文件,但这些努力往往是零散的、不完整的。更令人沮丧的是,当小程序框架升级时,这些自定义的类型声明可能立即过时,让你的努力付之东流。
我曾经天真地以为,使用第三方框架如 Taro 或 uni-app 可以缓解这一问题。然而,当你的项目规模扩大,或者需要使用一些特定的原生功能时,你依然会面临类型不匹配的困扰。这些框架也只能在其支持范围内提供类型安全,一旦越界,你又会回到 TypeScript 的荒漠中。
第二章:啰嗦冗长的语法之痛
现代前端框架如 React、Vue 已经为我们带来了优雅简洁的开发体验。然而,当我踏入微信小程序的领域,仿佛一脚踏入了上个时代的泥沼。那些啰嗦冗长的语法,让我想起了早期 web 开发的艰难岁月。
数据绑定需要使用 this.setData()
,而非简单直接的赋值操作;页面间通信需要层层传递,没有优雅的全局状态管理方案;组件传值繁琐,观察者模式实现困难。这种种限制,无不给开发者带来额外的心智负担。
// 简单的数据更新,却要如此繁琐
this.setData({
counter: this.data.counter + 1,
"userInfo.age": this.data.userInfo.age + 1,
"list[0].name": "new name",
});
有时我不禁怀疑,这是否是为了让初学者更容易理解而刻意设计的低门槛语法?还是说,这是为了适应某些特定场景的妥协之作?无论如何,这种设计让习惯了现代框架的开发者备感疲惫。
这种语法上的落后不仅体现在数据绑定上,在事件处理方面同样令人沮丧。在 React 或 Vue 中,你可以简单地传递参数:
// React中的事件处理
<button onClick={() => handleClick(item.id)}>点击</button>
// Vue中的事件处理
<button @click="handleClick(item.id)">点击</button>
而在小程序中,你需要使用 data-*
属性来传递参数,然后在事件处理函数中通过 e.currentTarget.dataset
来获取:
<!-- 小程序中的事件处理 -->
<button bindtap="handleClick" data-id="{{item.id}}">点击</button>
// 事件处理函数
handleClick(e) {
const id = e.currentTarget.dataset.id;
// 处理逻辑
}
这种设计不仅繁琐,还容易出错。当你需要传递多个参数时,情况会变得更加复杂。而且,dataset
属性会将驼峰命名转换为全小写,如 data-userId
会变成 useridx
,这进一步增加了错误的可能性。
状态管理更是小程序的一大痛点。没有类似 Redux 或 Vuex 那样成熟的状态管理方案,开发者只能通过全局变量、页面栈、本地存储等方式来管理状态,这使得状态管理变得混乱不堪。当应用规模增大时,这种问题会更加明显。
网上流传着许多非官方的状态管理解决方案,如 westore
、omix
等,但这些方案往往缺乏完善的生态支持,使用起来也不够顺畅。而且,当小程序框架升级时,这些第三方库可能会出现兼容性问题。
第三章:双花括号的禁锢牢笼
在大多数现代前端框架中,模板语法是如此强大而灵活。然而,微信小程序的双花括号 {{}}
宛如一座精心打造的牢笼,困住了开发者的想象力。
在这个牢笼中,你不能自由地调用函数,不能书写哪怕稍微复杂一点的表达式。需要截取一个字符串?抱歉,string.slice()
不被允许直接在模板中调用。你必须在页面的 methods 中定义一个专门的函数,然后在模板中引用它。
<!-- 这样简单的操作都不被允许 -->
<view>{{text.slice(0, 10)}}</view>
<!-- 而必须在WXS中定义函数 -->
<view>{{sliceText(text, 0, 10)}}</view>
这种限制导致了大量冗余代码的产生,让简单的逻辑展示变得异常复杂。我理解小程序追求轻量和性能的初衷,但牺牲开发体验来换取的这一点性能提升,真的值得吗?
在模板中你不能执行的操作还有很多,比如:
- 三元运算符嵌套:
{{a ? (b ? c : d) : e}}
不被支持。 - 数组方法:
{{arr.filter(item => item > 0)}}
不被支持。 - 模板字符串:
{{`Hello, ${name}`}}
不被支持。 - 对象解构:
{{const {a, b} = obj}}
不被支持。
这迫使开发者必须在 JavaScript 代码中预先处理好所有数据,然后才能在模板中使用,这无疑增加了代码量和复杂度。
列表渲染也是一大痛点。在 React 中,你可以简单地使用 .map()
方法来渲染列表:
{
items.map((item) => <div key={item.id}>{item.name}</div>);
}
而在小程序中,你必须使用 wx:for
指令,并且还需要手动指定索引名和项目名:
<view wx:for="{{items}}" wx:key="id" wx:for-item="item" wx:for-index="index">
{{item.name}}
</view>
当涉及到嵌套循环时,这种语法会变得更加繁琐,特别是需要为每个循环指定不同的 item 和 index 名称。
条件渲染同样不够灵活。在 Vue 中,你可以使用 v-if
、v-else-if
和 v-else
来处理复杂的条件逻辑。而在小程序中,虽然有 wx:if
、wx:elif
和wx:else
,但它们在处理复杂条件时显得力不从心,特别是当条件表达式本身就很复杂时。
网上有开发者尝试通过自定义组件或自定义指令来解决这些问题,但这些方案往往治标不治本,而且可能引入新的问题。官方的解决方案是使用 WXS,但 WXS 本身就有很多限制,这就引出了我们的下一个问题。
第四章:WXS 的荆棘小径
当我发现模板中的限制时,我欣喜地看到了 WXS 的存在。它似乎是为了解决模板中逻辑处理能力有限的问题而生。然而,当我深入了解它时,又一次陷入了失望的泥潭。
WXS 只支持 JavaScript 的一个极小子集,许多常用的功能和 API 都不可用。尤其是在处理日期格式化时,这一问题显得尤为突出。你不能使用 new Date()
,只能使用 getDate()
;而当你尝试在 WXS 中处理从 JavaScript 传来的 Date 对象时,更是会发现它变成了一个空对象 {}
。
// 在WXS中处理日期,简直是一场噩梦
var formatDate = function (dateString) {
// 不能使用new Date()
var date = getDate(dateString);
// 在iOS上还可能无法处理ISO日期中的Z标记
// ...复杂的格式化逻辑
return formattedDate;
};
更令人沮丧的是,当你尝试通过 ISO 日期字符串来解决问题时,又会发现在苹果设备上可能无法正确处理包含”Z”分隔符的 ISO 格式。这种平台差异化的问题,无疑给开发增加了更多的不确定性。
WXS 的限制远不止于此。它不支持 ES6+的很多特性,如箭头函数、解构赋值、类、Promise 等。这意味着你必须使用老旧的 JavaScript 语法,如 var
而非let
/const
,function
而非 =>
等。
更糟糕的是,WXS 和页面的 JavaScript 代码是相互隔离的。这意味着你不能在 WXS 中访问页面中定义的变量或函数,也不能在页面中直接调用 WXS 中定义的函数。这种隔离设计让状态共享变得复杂,增加了代码的耦合度和维护难度。
WXS 还有性能上的限制。虽然官方宣称 WXS 比 WXML 中的逻辑运算快 2~20 倍,但在实际使用中,当处理复杂逻辑或大量数据时,WXS 的性能表现往往不尽如人意。
许多开发者在网上分享了他们使用 WXS 的痛苦经历,从简单的字符串操作到复杂的数据转换,WXS 的种种限制让这些本应简单的任务变得困难重重。有人甚至建议完全避免使用 WXS,而是在 JavaScript 代码中预处理所有数据,这又回到了我们之前提到的问题。
举个具体的例子,假设你需要根据一组日期数据生成日历视图。在普通的 JavaScript 中,这是一个相对简单的任务,你可以使用 Date 对象、数组方法等来处理。但在 WXS 中,你需要自行实现各种辅助函数,如日期计算、数组处理等,代码量急剧增加,可读性和可维护性也大大降低。
网上有些第三方库试图解决 WXS 的限制,提供更多功能,但这些库往往体积较大,与小程序追求轻量的初衷相悖。而且,这些库的兼容性和性能也难以保证。
第五章:云开发的繁文缛节
微信小程序的云开发功能,本应是其一大亮点。然而,当你试图将 TypeScript 引入云函数时,又一次被繁文缛节所困扰。
每一个云函数都需要单独配置 TypeScript 支持,没有一劳永逸的全局配置方案。你需要为每个云函数创建独立的 tsconfig.json,设置独立的编译选项,以及独立的依赖管理。这种重复性的工作,不仅浪费时间,更容易引入错误。
# 为每个云函数配置TypeScript,重复且易错
|- cloudfunctions/
|- function1/
|- package.json
|- tsconfig.json
|- index.ts
|- function2/
|- package.json
|- tsconfig.json
|- index.ts
# 以此类推...
云函数的开发、调试和部署流程也较为繁琐,缺乏现代开发工具链应有的流畅体验。在这个崇尚效率的时代,这种繁文缛节无疑是对开发者宝贵时间的一种浪费。
云开发的问题远不止于 TypeScript 配置。当你的应用规模增大,用户量增加时,你会发现云开发的限制逐渐显现。首先是免费配额有限,一旦超出,费用会迅速增加。而且,部分 API 和功能在免费版中不可用,需要升级到付费版才能使用。
数据库操作也是一大痛点。云开发使用的是 NoSQL 数据库,虽然灵活,但对于习惯了 SQL 的开发者来说,适应起来需要时间。而且,云数据库的查询能力有限,复杂的多条件查询、关联查询、聚合查询等操作实现起来都很繁琐。
// 复杂查询需要多次操作或使用云函数
// 比如获取某个用户的所有订单,并包含商品信息
const db = wx.cloud.database();
const userOrders = await db
.collection("orders")
.where({
userId: app.globalData.userId,
})
.get();
// 然后还需要遍历订单获取商品信息
for (let order of userOrders.data) {
const goodsInfo = await db.collection("goods").doc(order.goodsId).get();
order.goodsInfo = goodsInfo.data;
}
云函数调用和文件操作同样存在性能和稳定性问题。在网络不稳定的情况下,云函数调用可能失败,需要额外的重试机制。文件上传下载也可能因网络问题中断,需要实现断点续传等功能。
更让人头疼的是,云开发的错误信息往往不够明确,排查问题需要耗费大量时间。官方文档虽然不断更新,但对于一些复杂场景的最佳实践介绍不多,开发者需要自行摸索。
网上有不少开发者分享了他们使用云开发的经验和教训。有人因为数据库设计不当导致性能问题,有人因为云函数超时导致业务中断,有人因为存储空间不足导致上传失败。这些问题在开发初期可能不明显,但随着应用规模扩大,逐渐成为不可忽视的障碍。
第六章:仿真与真机的二重奏
所有的开发测试都顺利通过,你信心满满地将小程序提交审核,准备上线。然而,当用户开始使用你的小程序时,各种奇怪的问题开始涌现——这是因为,微信小程序的许多 API 在仿真环境和真机环境中表现并不一致。
例如,wx.getUserProfile
这样的 API,在开发者工具中也许运行良好,但在真机上可能会有完全不同的行为或限制。这种不一致性,让开发者不得不在两种环境中反复测试,增加了开发的复杂度和不确定性。
// 在开发者工具中运行正常,在真机上可能行为不一
wx.getUserProfile({
desc: "用于完善用户资料",
success: (res) => {
// 处理成功逻辑
},
fail: (err) => {
// 处理失败逻辑
},
});
这种差异不仅存在于 API 行为上,有时甚至影响到样式渲染、动画效果等方面。作为开发者,你必须时刻保持警惕,为各种可能的异常情况做好准备。
真机测试的痛苦不仅在于行为差异,还在于调试困难。在开发者工具中,你可以方便地使用断点、日志等功能进行调试。但在真机上,你只能通过添加大量的 console.log
语句来了解代码执行情况,这极大地降低了开发效率。
更令人头疼的是不同设备间的差异。安卓和 iOS 之间的差异是大家都知道的,但即使在同一系统的不同设备上,小程序的表现也可能不一致。一些较老的设备可能不支持某些新特性,或者性能较差导致卡顿。这要求开发者必须考虑兼容性和性能优化,增加了开发难度。
网上充斥着各种因设备差异导致的问题讨论:有人反映在某型号手机上小程序闪退,有人发现在低端设备上动画效果卡顿,有人遇到特定机型下样式错乱等。这些问题往往难以复现和解决,需要开发者付出额外的时间和精力。
例如,在 iOS 设备上,input 组件的键盘弹起和收起会导致页面布局问题;在安卓设备上,scroll-view 组件的滚动表现可能与预期不符。这些平台特定的问题需要开发者编写条件判断代码或使用不同的解决方案,增加了代码的复杂性。
// 针对不同平台的条件处理
const system = wx.getSystemInfoSync().system.toLowerCase();
if (system.includes("ios")) {
// iOS特定处理
} else if (system.includes("android")) {
// 安卓特定处理
} else {
// 其他平台处理
}
这种为平台差异编写的代码不仅增加了维护难度,还可能因微信客户端更新而失效,需要持续关注和调整。
第七章:权限的迷宫
小程序的权限管理是另一个让开发者头痛的问题。与普通网页不同,小程序需要用户授权才能访问许多功能,如位置信息、相机、相册等。这本是为了保护用户隐私的良好设计,但在实际开发中却常常成为障碍。
最典型的例子是获取用户信息。在早期版本中,可以通过 wx.getUserInfo
直接获取用户信息。但后来,出于隐私保护考虑,微信对此 API 进行了限制,要求在按钮上使用 open-type="getUserInfo"
才能调用。再后来,又推出了 wx.getUserProfile
API,但这个 API 每次调用都会弹窗询问用户,无法静默获取用户信息。
// 旧版获取用户信息方式,现已不推荐使用
wx.getUserInfo({
success: (res) => {
// 处理用户信息
},
});
// 新版获取用户信息方式,需要用户每次授权
wx.getUserProfile({
desc: "用于完善会员资料",
success: (res) => {
// 处理用户信息
},
});
这种 API 的变更没有很好的向后兼容性,导致许多开发者需要重写相关代码。而且,新的 API 使用体验较差,用户每次使用相关功能都需要授权,可能导致转化率下降。
位置信息的获取也是一大难点。微信对位置信息的访问有严格限制,需要用户显式授权。而且,用户可以随时撤销授权,这要求开发者处理各种可能的授权状态。更麻烦的是,即使用户已授权,在某些情况下(如小程序长时间未使用)也可能需要重新授权。
// 获取位置信息,需要处理各种授权状态
wx.getSetting({
success: (res) => {
if (res.authSetting["scope.userLocation"]) {
// 已授权,可以直接获取位置
wx.getLocation({
success: (res) => {
// 处理位置信息
},
});
} else if (res.authSetting["scope.userLocation"] === false) {
// 用户拒绝授权,引导用户开启授权
wx.showModal({
title: "提示",
content: "需要获取您的位置信息,请在设置中开启",
success: (res) => {
if (res.confirm) {
wx.openSetting();
}
},
});
} else {
// 首次获取授权
wx.getLocation({
success: (res) => {
// 处理位置信息
},
fail: () => {
// 处理授权失败
},
});
}
},
});
这种复杂的授权逻辑需要开发者写大量的代码来处理各种情况,增加了开发难度和维护成本。而且,不同 API 的授权机制可能不同,需要分别处理。
网上有许多开发者分享了他们在处理权限问题时的经验和教训。有人因为没有正确处理授权状态导致应用崩溃,有人因为授权提示文案不当导致用户拒绝授权,有人因为频繁请求授权导致用户体验下降。这些问题需要开发者投入大量时间和精力来解决。
第八章:样式与组件的囚笼
小程序的样式系统和组件系统也有诸多限制,让习惯了现代前端开发的程序员倍感束缚。
首先是样式限制。小程序不支持直接引入外部 CSS 库,也不支持在 JS 中动态修改样式。所有样式必须在 WXSS 文件中预先定义,这限制了样式的灵活性。而且,小程序中的选择器能力有限,不支持一些复杂的 CSS 选择器,如:nth-child、:not 等。
/* 这样的高级选择器在小程序中不支持 */
.list-item:nth-child(odd) {
background-color: #f0f0f0;
}
.button:not(.disabled) {
color: blue;
}
组件间样式隔离也是一个问题。在小程序中,自定义组件的样式默认是隔离的,父组件的样式不会影响到子组件。虽然可以通过 options.styleIsolation
来控制样式隔离方式,但这仍不如 React 或 Vue 中的样式管理灵活。
// 控制样式隔离方式
Component({
options: {
styleIsolation: "isolated", // 或 'apply-shared' 或 'shared'
},
});
小程序内置的组件虽然基本满足需求,但在某些复杂场景下显得功能不足。例如,表单组件相对简单,缺乏表单验证等高级功能;列表组件无法处理大量数据,需要开发者自行实现虚拟列表;富文本展示和编辑功能有限,需要使用第三方插件。
<!-- 缺乏验证功能的表单 -->
<form bindsubmit="formSubmit">
<input name="username" placeholder="用户名" />
<input name="password" password placeholder="密码" />
<button form-type="submit">提交</button>
</form>
// 需要手动验证表单
formSubmit(e) {
const { username, password } = e.detail.value;
if (!username) {
wx.showToast({
title: '请输入用户名',
icon: 'none'
});
return;
}
if (password.length < 6) {
wx.showToast({
title: '密码不能少于6位',
icon: 'none'
});
return;
}
// 提交表单
}
第三方组件的使用也不够方便。在 npm 支持之前,引入第三方组件需要手动复制文件,维护成本高。即使在有了 npm 支持后,也需要通过构建将 npm 包构建为小程序可用的模块,过程繁琐。而且,并非所有 npm 包都适用于小程序,许多包可能需要修改才能使用。
# 构建npm
# 1. 安装依赖
npm install some-component
# 2. 点击开发者工具中的"工具"菜单,选择"构建npm"
# 3. 在页面或组件中引入
网上有许多开发者分享了他们在使用小程序组件时的困扰。有人抱怨 scroll-view 的滚动表现不佳,有人吐槽 map 组件的功能限制,有人不满 rich-text 的展示效果。这些问题需要开发者寻找替代方案或自行实现所需功能,增加了开发的复杂度和工作量。
框架求生:第三方开发框架的曙光
面对原生小程序开发的种种痛点,许多开发者寻求第三方框架的帮助。这些框架在一定程度上缓解了开发痛苦,但也引入了新的问题。
uni-app 与 Taro:跨平台的救星
uni-app基于 Vue.js,支持编译到多个平台,提供了类似 Vue 的开发体验:
<template>
<view class="container">
<text>{{ message }}</text>
<button @click="changeMessage">点击修改</button>
</view>
</template>
<script>
export default {
data() {
return {
message: "Hello uni-app",
};
},
methods: {
changeMessage() {
this.message = "Changed!"; // 直接修改数据,无需setData
},
},
};
</script>
Taro则采用 React 语法,同样支持多端开发:
import React, { useState } from "react";
import { View, Text, Button } from "@tarojs/components";
function Index() {
const [message, setMessage] = useState("Hello Taro");
return (
<View className="container">
<Text>{message}</Text>
<Button onClick={() => setMessage("Changed!")}>点击修改</Button>
</View>
);
}
export default Index;
这些框架的共同优势在于:
- 熟悉的开发体验(Vue 或 React)
- 跨平台能力,一套代码运行多端
- 规避了原生小程序的许多语法限制
但它们也带来了新的挑战:
- 性能损耗,特别是在复杂应用中
- 平台特性支持滞后,微信小程序更新后需要等待框架适配
- 调试复杂度增加,特别是平台特定的问题
- 与原生能力的兼容性问题
框架选择的权衡
选择框架时需要考虑:
- 团队技术栈:与团队已掌握的技术(Vue/React)匹配
- 项目复杂度:简单项目可能原生反而更直接
- 跨平台需求:是否真的需要支持多端
- 性能要求:对性能要求高的场景可能不适合使用框架
对我而言,在尝试使用第三方框架后仍然遇到了许多问题,尤其是需要使用一些平台特定功能时。框架确实解决了部分开发体验问题,但同时也引入了新的复杂性。
最终,我认识到技术选型没有银弹,应当根据项目具体需求做出选择。对于只需要在微信平台运行的简单应用,原生开发可能更为直接;而对于复杂且需要跨平台的项目,框架则能提供更好的开发效率。
终章:拨云见日
尽管微信小程序开发之路充满荆棘,但它确实为开发者提供了连接海量用户的桥梁。我认为理解其限制与优势,才能在这个平台上创造出更好的作品。
微信小程序的诸多限制确实有其考量—性能、安全、兼容性,但这不代表我们必须全盘接受其带来的开发痛苦。批判与改进并行不悖,期待小程序生态能够不断优化开发体验,同时我们开发者也要不断提升自己的技术能力,在限制中寻找创新。
如果你正准备踏上小程序开发之路,希望我的经验能为你提供一些参考,让你少走弯路。若文中有任何技术上的错误或不妥之处,也欢迎指正。
技术发展日新月异,但追求卓越代码的初心不变。且行且思,砥砺前行。