
文章的 TOC 太多了,看起来不方便?PC 端可以看看我的 《两个 TamperMonkey 脚本,解决掘金阅读体验的三点不爽》,可以解决一些掘金阅读体验问题。
我写的「入坑」系列文章:
- 《入坑 Mac,看这一篇就够了》
- 《入坑 iTerm + OMZ,看这一篇就够了》
- 《入坑 Firefox Developer Edition 及 Mobile 版,看这一篇就够了》
- 《入坑 WebStorm,看这一篇就够了》
- 《入坑 VSCode,看这一篇就够了》
- 《入坑 Vim,看这一篇就够了》
- 《入坑 Git,看这一篇就够了》
- 《入坑 Docusaurus,看这一篇就够了》
- 《入坑 Nginx,看这一篇就够了》
- 《入坑 UniApp 写小程序,一些感受》← 本文
🎼 前言
好久没发表文章了,有很多想写的,但总是找各种借口推到明天的明天的明天。每天陆陆续续总会收到一些赞和收藏,非常感谢大家对我之前写的文章的认可。
距离上次发文章,也经历了漫长的时间,期间发生了一些让我很郁闷的事情。总之,2024 年 1 月初,我从上海回到了杭州。
回到杭州后,我加入了一家初创小公司,做 AI 的。我的说法是「趟一趟这趟历史的浑水」,老板的说法是「躬身入局」,瞬间觉得自己文化水平有些捉襟见肘了。
加入公司做的第一件事情便是写一个小程序。这篇文章其实在我接受这项任务的时候就已经想好要写了,也早就应该写了,但最终还是一拖再拖,闷馊闷臭了。
TL;DR
你或许会发现这篇文章和我其他的有些差别,是的,图很少,很干。
这不是一篇「如何用 uni-app 写小程序」或者「如何开发小程序」的主题的文章,因此不会特别多地介绍各种入坑知识(但也会涉及),更多的是我如何思考小程序开发中的技术选型、最佳实践以及遇到的各种问题的吐槽等等。
主要内容

适合读者
- 想入坑写小程序,技术栈为 Vue 的同学
- 正在或即将用 UniApp 开发小程序的同学
- 单纯想了解了解小程序开发的同学
你将了解
- UniApp 开发小程序的入坑姿势(二选一)
- UniApp 和微信小程序的一些坑
- UniApp 的一些天坑和闭坑秘籍
- 一些算是最佳实践吧
术语
本文正文中用到的术语解释:
| 日期 | 版本说明 |
|---|---|
| MP | 小程序(真机或模拟器) |
| H5 | 运行成 H5 模式 |
编辑历史
| 日期 | 版本说明 |
|---|---|
| 2025/06/30 | V1 |
🕹️ 技术选型
写这个小程序之前,我的背景是这样的:
- 知道小程序这个东西,但从来没有写过或了解过其技术栈
- 从来没有正式用过 Vue 写过应用,只通过 Weex 用过一点点
那为什么我上呢?原因有两个:没其他人了,这样的挑战让我兴奋大大大于担忧。
选择开发框架 uni-app
你可能知道小程序框架除了 UniApp 之外,比较有名的还有 Taro 啥的(我..不知道)。那为什么选 UniApp 呢?
其实就是被同事「忽悠」的。他是这么跟我说的:
- 所有的组件它都给你弄好了,
button什么的,样式你都不需要操作,它就长那样... - 只要
div改成view,span改用text就好了 - 很简单,超级简单...
- 它甚至有自己的开发工具,叫
HBuilderX(简称HX),你只需要... - ...
以上说了,小程序对我来说是一个全新的领域,我并没有急着进入业务开发,而是花了两个礼拜熟悉相关技术栈、并最终决定项目架构。说白了,就是看官文的「快速上手」本地瞎折腾。
不选 HX 做开发工具
官文的「快速上手」有两篇,HX 或 CLI,这是一个问题。
看过我文章的同学,应该能够了解到我的几个特点:
- 自称「软件研究员」,喜欢玩软件,尤其是开发工具,特别喜欢研究配置
- WebStorm 铁粉,自以为得心应手
- VSCode 略懂,自以为很多重度用户还不及我对其了解
- 快捷键高手
- 钟爱命令行
- 代码洁癖
以上都会对我最后的选择有影响。
为了做出正确的选择,同时也是熟悉的一个过程,HX 和 CLI 两种方式,我都试了。
首先,作为「软件研究员」,我是非常喜欢玩各种软件的。但 HX 粗糙复古的图标先有让人眼前一「暗」的感觉,打开界面瞬间,更令我失去了热情。配色简直一言难尽,官方吹嘘说是「护眼」。
从界面上看,感觉它可能基于 Monaco,但又刻意不想做得跟 VSCode 很像。
为了了解技术栈,我跟文档操作了好几拨,同时也写了一些 Demo 代码,然感觉用着并不爽利,尤其是快捷键,虽然可以自定义。
更要命的是,它擅自修改了系统文件关联。
最令我不能接受的是它的 uni_modules 模式,一旦引入意味着两个后果:
- 你会有一堆不是你写的代码必须要提交,意味着你的代码提交量反应不了你的真实工作,并且需要专门扩展 Lint 等工具的忽略列表
- 你会被 HX 绑架,因为只有 HX 可以添加或更新
uni_modules
最最不能接受的是,HX 把很多细节都隐藏了,依赖了哪些东西,版本是什么,两眼一抹黑。想引个 npm 包?不行。想配 Husky?不行。想写个 npm script?不行。全因为没有 package.json。
在对比了 HX 和 CLI 项目后,发现 HX 的项目其实就是 CLI 项目的 src 目录,这就意味着 CLI 的项目实际可以用 HX 开发,而 HX 的项目只能 HX 开发。包容性很重要。
总结一下 HX 的弊端:
- 要一个有经验的已经偏爱了某个 IDE 或者编辑器的开发者,单单为了一种开发框架换一个新开发工具,除非此开发工具真的有亮的不行的亮点,但 HX 在 IDE 或编辑器这一层面来讲几乎没亮点
- HX 项目不具包容性,基本上就是只能强迫人用 HX,以后想升级、迁移都是问题
另外让我很反感的是官方宣传 HX 的措辞,在他们 快速上手 - 通过 vue-cli 命令行 中,我引用几段(因为他们可能会改掉)。
为了提升易用性,降低门槛
很多开发者对 node 不熟悉、对命令行有心理抵触。不要想当然认为所有开发者都会 node,HBuilder 有几百万开发者,其中掌握 node 的开发者连一半都占不到。
「很多」?「心理抵触」?
我向来认为 IDE 除了便利开发之外,最重要的一个作用就是,它是开发者最好的「老师」之一。而命令行是开发者必会的技能之一,作为老师,应当「循循善诱」。
而且,使用命令行,没人说要「掌握」Node 呀,本人不才,命令行还行,但 Node 知识略懂。
如果你习惯其他 IDE,开发 uni-app 低效也无所谓,那也可以用其他 IDE。
很低情商的一句话..就像在说「老子天下第一,不用是你活该」,很茶的感觉。
为什么选 CLI 方式
不选 HX,就只能选 CLI 方式。好处:
- 拥有的是一个完整的可自运行的项目
- 能用命令行提效
- 能用 npm
- 能用 Husky 搭配各种 Lint
- 能用习惯的 IDE 或编辑器,也可以用 HX
CLI 需承担的「坏处」
需配置 easycom
首先是 easycom。
只要组件安装在项目的
components目录下或uni_modules目录下,并符合components/组件名称/组件名称.(vue|uvue)目录结构...
说实话,我对这种隐式依赖很反感,但它也的确提供了一些便利。CLI 开发模式要做的是在 src/pages.json 下添加以下配置:
bash
"easycom": {
"autoscan": true,
"custom": {
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
}
}
但这只是告知了编译器,而并没有告知你的开发工具,你会看到 IDE 在抱怨:
Pasted image 20250716123751.png
要让 IDE 也知道这个东西存在,并且能够点过去,还需要装插件:
- WebStorm 用 Uniapp Tool
- VSCode 用「我没找到」
WebStorm 使用插件后的效果(这里我故意写错了一个属性,IDE 能检测到并给出警告):
Pasted image 20250716123825.png
不能使用 uni_modules
如上一章节总的摘录所说,uni_modules 也是 easycom 的一种,所以其实也不是不能用。
当然你也可以用 HX 打开项目后,用 HX 的能力进行安装和升级。
然,我非常不建议这么搞。
以我的这个项目的经验来讲,我的确有从 uni_modules 的项目中汲取经验,但并没有使用任何一个。这种硬拷代码的三方依赖,实在叫人难以下咽。
TypeScript
无 TS 不 Coding,必须 TS,所以我用的是他们提供的 TS 模板。
Vue3
我并没有怎么接触 Vue,所以只想用最新的版本。UniApp 支持 Vue2 和 Vue3,但作为 Vue 生手加升级狂魔,我必须全面使用 Vue3 语法。模板以及 uni_modules 的代码,目前(截止 2025/06)大部分好像都还是 Vue2。
TSX
我想用。但小程序端还不支持,所以放弃。
🔋 开发折腾
折腾的成果
由于没法在 UniApp 项目下搞 Storybook,因此我专门搞了一个测试页面 pages/_xx,以便随时可以调戏 UniApp 和通用组件。
MP 封装
在 UniApp 中,你有一个全局的 uni 对象,它下边挂了很多方法,然而,我不建议你在业务代码中直接裸用 uni.xx,而是做一层封装。以下是几点考量:
- 我们可以一眼就知道在项目中用到了多少相关的接口
uni不够简洁,封装后使用更简洁uni类型定义不精确或者不正确,封装后对 TS 更友好uni存在多态,可能导致这些异化逻辑散落到业务层,封装以保护代码一致性uni存在平台差异问题,必须封装后方能安全使用uni不面向业务,通过封装可以使其具备业务特性,使代码更简洁- 万一哪天换框架了,我不需要到处改业务代码
问题:不够简洁
比如 uni.login,多数情况我们只需要返回的 code,然而原始返回长这样:
arduino
{
errMsg: 'login:ok', // ← 微信
success: true, // ← 支付宝
code: '0e3ejr000AXOwR1g43400j7gwL2ejr05'
}
问题:类型定义不精确或者不正确
还是 uni.login 的例子,官方的数据返回类型如下:
typescript
interface LoginRes {
errMsg: string;
code: string; // ← 唯一有用且需要关心的部分
// 💥 以下都没有
authResult: string;
anonymousCode?: string;
authCode?: string;
authErrorScope?: any;
authSucessScope?: string [];
appleInfo?: AppleLoginAppleInfo;
}
问题:方法多态
比如对 storage 操作,uni 提供了同步(getStorageSync)和异步(getStorage)两种,这会导致业务代码的不一致性,通过封装可以限制成一种。
另外,多数 Promise 接口都提供了 success、fail 回调,而传了回调就不再是 Promise 了,这让我这种 Promise 狂魔很不爽。
问题:平台差异性
uni 的接口并未处理平台间的兼容差异问题。
- uni.getDeviceInfo,它的定义是「同步获取」,但在支付宝小程序(该接口不支持支付宝)下却返回了
Promise - uni.getUserInfo,H5 下直接报方法不存在(微信已宣布清退此接口)
- 调用成功,微信的返回可能是
{ errMsg: 'xx:ok', ...others },支付宝可能是{ success: true, ...others } - 调用失败,微信可能到
Promise.reject,而支付宝可能是Promise.resolve
调用失败不一致性的例子:uni.getStorage:key 不存在的情况,微信会 reject({ errMsg: 'getStorage:fail data not found' }),支付宝则 resolve({ data: null })。
但 storage 的同步接口还是对齐的,这也是为什么我们封装用的是同步接口的原因之一。
问题:需要面向业务
比如 storage 对 key 有保留前缀,我们可以统一设置前缀避免不必要的问题。
如何封装
我们需要:
- 封装错误
MpError,以保证所有的错误都是Error对象,而不是uni给出的普通Object - 工具方法
wrapUni,强制所有能Promise化的 API 都必须返回Promise,标准化了错误对象MpError,且对不存在的方法进行了捕获并抛出封装后的MpError错误 - 所有的方法名以
mp打头,拼上真正的 API 名称(有的时候可以稍作改动) - 按需封装,可以在写业务的时候,用到哪个封装哪个
- 封装的原则是:若方法有静态和 Promise,舍其一;明确真正的作用,简化输出
比如以下是对 uni.login 的封装:
typescript
/**
* 💥H5 → Promise reject 「login:fail method 'uni.login' not supported」
*/
export default function mpLoginCode(): Promise<string> {
return wrapUni<string>(() => uni.login().then(result => result.code));
}
我们通过将其名称改为 mpLoginCode 明确了其真正的作用,返回改成 Promise<string> 简化了其结果。
以下是我一边写业务一边在 src/pkg/mp 下封装的接口概览:
Pasted image 20250716125756.png
注意循环依赖
由于我在封装 mp 的过程中对相关的错误进行了日志上报,这非常容易导致循环依赖。我在后面写 Taro 项目的时候吸取了教训,在 sls、stroage、fetcher 等模块中不再反向依赖 mp,而是直接将封装写在了相关模块的内部。
国际化
首先,请走出你的认知误区,国际化的目的并不一定是为了服务海外用户。为项目添加国际化的能力有如下好处:
- 保证文案的一致性
- 保证数据展示的一致性(数字、日期等的格式化展示也属于国际化)
- 为海外业务提前布局
所以,这也是我写小程序一开始就着手考虑的基础建设之一。
关于国际化的文案有两个
秉承 UniApp 的尿性,国际化也有很多坑:
-
$t在小程序下无法使用插值- 传
values类型报错...,但如果不传 - H5 下会把文案中的插值干掉
- MP 下传与不传都会保留文案中的原始值
- 传
-
由于真机中没有 Intl,模板不能用
$n,也不能export const { global: { d } } = middlewareI18n -
由于真机中没有 Intl,模板不能用
$d,也不能export const { global: { d } } = middlewareI18n
以下是我封装了 pkg/middleware-i18n:
Pasted image 20250716132700.png
在 main.ts 中安装此中间件:
javascript
import {
createSSRApp
} from 'vue';
+import middlewareI18n from '@/pkg/middleware-i18n';
import App from './App.vue';
export function createApp() { // eslint-disable-line @typescript-eslint/explicit-function-return-type
const app = createSSRApp(App);
+ app.use(middlewareI18n);
return {
app
};
}
Pinia
要不要一个全局状态管理工具呢?
以我写 React 的经验,我可以不依赖于任何全局状态管理就能够写好一整个应用。但 Vue,我就不够有信心了,更不要说有小程序这个天坑的加持。
最后发现写页面其实可以不需要全局状态管理,但页面间共享的信息一定是有的,因此全局状态还是有必要的。
接下来的问题就是,选择哪个作为状态管理工具呢。
我最终在 Vuex 和 Pina 之间选择了 Pinia,没什么特别的理由。主要是好像 Vuex 自己也推荐用户选择 Pinia 了,更重要的是我实验性地用 Pinia 成功了。
Pinia 的使用方式比较简单,但非常不直观。我们需要:
- 定义一个
middleware - 在
main的createApp中app.use(middlewarePinia),并必须在return中带上Pinia这个对象 - 使用 Pinia 的
defineStore定义需要的 Store - 在
App.vue中初始化定义好的 Store(有需要的话)
然后便可在应用各处快乐的使用定义好的 Store 进行全局状态的读写操作了。在我的应用了我定义了三个 Store:
defineStore('login', ...)用于维护用户登录信息defineStore('config', ...)用于维护应用配置,比如三方链接、功能开关等defineStore('system-info', ...)用于维护部分uni.getSystemInfoSync得到的信息,因为部分系统信息会变,比如theme
接口请求
接口返回一般会把真正的数据做一层包裹,类似这样:
csharp
interface ApiResponse<T = null> {
code: string;
message: string | null;
data: T;
}
前端一般来说只关心两层:
code为「成功」时的datacode为「失败」时的message
我见过过分勤劳的人在每次调用的接口后加上一段条件判断的,但我是懒人,所以我需要一个全局请求的封装,在成功时直接得到数据,在失败时候抛出错误。
同时,我见过太多后端的接口问题,从命名到类型的诡异,再有就是各种不一致。所以,我封装了接口请求的基础逻辑 fetcher,然后在 fetcher 的基础上以对象为核心分批封装了所有的数据接口,把 API 的 method、URL、以及恶臭到可能极致的参数和结果统统埋进了数据层。这样,暴露给前端业务层的请求,就是类型明确,干净卫生的纯 Promise 方法。
这是我一贯坚持的编程方式,且我不愿意把这个交给 AI 来做,因为在梳理数据接口,把后端接口转化为前端 API 方法的过程,其实就是理解业务的过程,且这种跟 UI 无关的联调模式,能在联调初期发现后端设计上的问题。
后来,我在写 Taro 小程序的时候,实在受不了到处复制代码,又为了能够尽可能复用 PC 端的数据封装逻辑,于是把写 PC 端时期封装的 @kcuf/fetcher 进行了提纯,写了 @kcuf/fetcher-core,使其可以脱离环境使用,使各端的接口请求方式可以保持在同一基准线。
SSE
写过 AI 流式交互的同学,应该都知道 SSE。
小程序请求接口有个 enableChunked 类似这样(我已经不记得为什么要用 wx.request 而不是 uni 了):
php
// #ifndef H5
const requestTask = wx.request({
url: apiUrl,
enableChunked: true,
header: headers,
success: resolvePromise,
fail: (err): void => reject(createFetcherErrorNetwork(request, err.errMsg))
});
requestTask.onHeadersReceived(() => onOpen?.());
requestTask.onChunkReceived((e: IChunkReceivedArg) => {
processArrayBuffer(e.data);
});
// #endif
但我需要兼容 H5,而浏览器自带的 EventSource 并不支持 header,虽然有 EventSourcePolyfill,但我试了有循环启动的 BUG(不明就里),且 Polyfill 写得比较臃肿。于是,我自己写了一个 @kcuf/fetch-sse。
解析 SSE Chunk 需要 TextDecoder,小程序下没有这个全局对象,可以参考 FastestSmallestTextEncoderDecoder。
条件编译
UniApp 提供了 条件编译 机制,上次听说「条件编译」已经不知道是什么时候了,大概是上古时期了吧。
条件编译的好处是,特定环境不会有永远用不到的代码片段。
但我其实不太喜欢条件编译,尤其在带 return 语句的时候,会看起来有语法错误。
日志
虽然微信平台会默认添加 PV、UV,甚至新用户等日志,但更细节的日志还是应该我们自己弄。我采用了阿里云 SLS Web Tracking 的方式,对以下场景进行了全局统一的日志支持:
- 小程序及页面生命周期
- API 请求成功与失败
另外就是关心一下关键的用户点击等操作。
.env 文件,有无必要
你可以在 .env 中写接口的 Base URL,或者别的跟环境有关的配置信息。但这意味着你需要承担一个后果:不同环境的构建结果无法统一。
或者这并不是什么大问题。但我更倾向于写一个 env 包(这也是我后面写 Taro 时用的方案),把跟环境有关的都封装在这个包里面,也不会造成 import.meta.env.VITE_XX 漫天飞舞的惨烈现象。
🪁 上线折腾
你以为开发已经很折磨人了?你想少了,上线还得再折磨你一遍。
包大小限制
由于本人没有经验,所有的页面都放在了 src/pages 下,然后也不知道什么叫「主包」「分包」,在微信开发者工具上进行上传的时候就被告知「你的包超过了 2MB,不能上传」。
我看了相差不了多少,就勾选了项目本地设置中的「上传时压缩脚本」,将将及格,但开发工具死活不给你记住那个选项,每次还给你提醒。等业务多了,代码超不少了,就要考虑分包了。
可恶地是,无论 UniApp 还是 Taro,亦或是微信自己,都没有把分包作为很重要的文档,给开发者看到。如果你正在着急上线,然后也不直到什么叫分包(就像当初我一样)那么就会很烦躁。
资质材料
如果你的小程序是个全新的东西,那么你在上线前,起码需要准备一周左右,因为微信平台(其他我不知道)会要求你上传一些资质材料,具体流程我没有记,总之挺烦的。
上线前,还可能要求上传截图,他们有 AI 会对截图里的内容和你小程序的元信息进行匹配,若有问题,还会被打回(这个过程在每次发布正式版都要来一次)。
🤯 问题记录小全
我专门为开发过程中遇到的问题及相应的解决方案做了记录,或许你也会遇到。
SSR
想在 H5 使用 SSR?最好想清楚了。因为很多的 API 并没有做相应的保护,比如 uni.getLocale 就不行(官方没有做降级,也没有在文档中标注)。
这就是为什么
start命令用的是uni而不是uni --ssr的原因。
国际化 - manifest.json
不可用,在微信小程序开发工具中报错「project.config.json: libVersion field needs to be string, string」。
国际化 legacy 问题
不 legacy: false |
legacy: false |
|
|---|---|---|
H5 $t |
✅ | ✅ |
H5 useI18n |
💥1 | ✅ |
小程序 $t |
✅ | 💥2 😱3 |
小程序 useI18n |
✅ | ✅ |
- 💥1 - 报错(白屏)「Uncaught (in promise) SyntaxError: Not available in legacy mode」
- 💥2 - 报错(部分组件不展示)「TypeError: _ctx.$t is not a function」
- 😱3 - 警告(子组件不展示)「Property "$t" was accessed during render but is not defined on instance」
其中 2 和 3 容易导致「问题代码上线」,风险更高,故此:
- 不用
legacy: false - 不用
useI8n - 仅用
$t
没有 @keydown.meta.enter
如题
TS 问题
| 组件 | 来源 | 定义 | 实际 |
|---|---|---|---|
chooseImage |
@dcloudio/types |
chooseImage(...): void |
chooseImage(...): Promise |
uni-search-bar |
@uni-helper/uni-ui-types |
`onInput({ value: string | number; }): void` |
uni-list-item |
@uni-helper/uni-ui-types |
`link: false | Xx` |
@dcloudio/types 提供了全局的 namespace UniApp,在 TS 文件下使用完全没有问题,但不能在 Vue 下使用(即使 script 为 ts),会 Eslint 报错。
H5 VS MP
| - | H5 | MP | 说明 |
|---|---|---|---|
$t 国际化插值参数 |
✅ | ❌ | MP 不会进行插值替换,且 IDE 报错 $t 不接受两个参数 |
slot name 带中划线 |
✅ | ❌ | 尽可能用单个词,若必须用词组,则需下划线或骆驼命名 |
slot v-if |
✅ | 🪲 | MP 无论如何都会触发 slot 的 onMounted,不论是否最终判定为需要渲染 github.com/dcloudio/un...,这种时候,不能依赖 onMounted 进行数据获取等初始化操作,可以用 watch,需设置 immediate: true |
v-if |
✅ | 🪲 | MP 下必须是 boolean,不能是 funcion 对象 |
v-show |
✅ | 🪲 | MP 下 v-show 不能定义在组件定义(会一直展示着),只能用在 |
类似大写的 Input 的组件名 |
✅ | 🪲 | MP 下的渲染节点会变成 input 从而导致奇奇怪怪的问题(比如样式) |
onLoad |
同步 | 异步 | |
uni.showModal 传入 undefined 的 confirmText 或 cancelText |
🪲 | ❌ | H5 会显示英文,MP 抛错 |
uni.chooseImage 取消 |
❌ | ✅ | H5 下取消,会导致 Promise 一直挂起 |
uni.request(//xxx) |
✅ | ❌ | MP invalid URL |
uni.createInnerAudioContext() 复用一个 context |
✅ | ❌ | MP 播放第二次的时候,报错「TypeError: Cannot read property 'then' of undefined」,解决方案:用完即焚 |
| 封装组件传 class | ✅ | ❌ | MP 下会多一层,class 在这多出来的一层上,导致样式可能失效,uniapp.dcloud.net.cn/tutorial/vu... |
<uni-row> / <uni-col> 传 class |
✅ | ❌ | 用 CSS developer.mozilla.org/en-US/docs/... |
<uni-swipe-action> |
✅ | 🪲 | MP 下右侧多一个 view 包裹,导致 flex 无效,必须自己多裹一层 flex |
<text> 裹组件 |
✅ | 🪲 | MP 下内裹组件的 <text> 高度为 0,但只有 text 支持 selectable... |
| Url 中的参数 | 🪲 | ✅ | H5 下,script 中从 props 得到的是正确(解析后)的参数,但在 template 中却还是解析前的 |
Audio onCanplay |
✅ | 🪲 | MP 在结束后,会重新触发一次... |
Audio duration |
✅ | 🪲 | MP 第一次 onCanplay 拿不到时长,只有在调用 .stop() 后自动触发的第二次 onCanplay 可以... |
在 TS/JS 中 import 样式文件 |
✅ | 🪲 | MP 下会被 tree-shake 掉 |
Map 组件需要申请腾讯 key,lbs.qq.com/dev/console... Map 中定位需要到小程序开发工具测试,web 端模拟器拿不到地理坐标
🌰 onLoad
javascript
import {
onLoad
} from '@dcloudio/uni-app';
console.info(111);
onLoad(() => console.info(222));
console.info(333);
MP 输出:
111
333
222
H5 输出:
111
222
333
开发工具 VS 真机
| - | 微信开发工具 | 真机 | 说明 |
|---|---|---|---|
Intl |
✅ | ❌ | 真机不支持 vue-i18n 的 d 和 $d(警告) |
TextDecoder |
✅ | ❌ | |
RecorderManager duration |
❌ | ✅ | 开发工具下 duration 无效,需加 hack |
三方库
| 三方库 | H5 | 微信开发工具 | 真机 | 说明 |
|---|---|---|---|---|
marked @12.0.1 |
✅ | ✅ | ❌ | 报错 /^((?![*_])[\s\p{P}\p{S}])/: Invalid property name in character class |
micromark @4.0.0 |
✅ | ❌ | 🚫 | 构建报错 [vite]: Rollup failed to resolve import "micromark-util-symbol"(或其他包) |
markdown-it @14.0.0 |
✅ | ❌ | 🚫 | 构建报错 [vite]: Rollup failed to resolve import "linkify-it" |
其它代码设计问题
关于页面 padding
有些页面需要 padding,有些不需要,看起来可以用下面这样的代码解决。但!
xml
<style lang="scss" scoped>
page {
padding: 20px;
}
</style>
H5 没有问题,MP 不行;如果要想 MP 可以,则必须去掉 scoped,于是 H5 下所有的页面受到影响...
uni-list-item
link 点击出错没有任何反应,事件也是 click 而不是 error,这导致问题很难被发现:
javascript
pageApi(api) {
let callback = {
url: this.to,
success: res => {
this.$emit('click', {
data: res
});
},
fail: err => {
this.$emit('click', { // <-- ...
data: err
});
}
};
// ...
}
🙋 FAQ
❓为什么不用 ncu 更新 uni-app 的包?
他们的包都是死版本,试过改成 ^x.y.z 会导致 uni_modules 和 components 自动注入失效等一系列奇怪的问题,而且安装的包会多版本重复。
官方的更新的方式,使用 npx @dcloudio/uvm@lates,参考 更新依赖到指定版本。
另外,UniApp 的整体依赖貌似很不健康,我曾经仅仅只是升级了 @dcloudio/* 的包,然后,跑不起来了就... 一个多年的 陈年老 BUG 无人管。
📌️ 链接
🪭 写在最后
如果你的技术栈只有 Vue,那么用 UniApp 写小程序无疑是不二之选,如果你的技术栈是 React,那末,你可以试试 Taro(我试了,可用)。
如果你顾及到框架的可持续发展,你会发现 Taro 的更新频率不如 UniApp,我曾经也以为 Taro 是死掉的状态,因为最末一个版本已经很久不动了,直到有一天(2026/07 的某一天),它居然升了个小版本,然后我选择的 UI 库 @taroify 也升了几次小版本。装死?Very confusing.. 🤔
若你选择 Taro,那么你就要容忍它的工具链陈旧(Vite 4,最新 8,而 Webpack 虽然是 5,但必须写死版本)和仅支持到 React 18 的问题(意味着你无法随心所欲地升级基础库)。
遗憾地是,无论你选择 UniApp 还是 Taro,你都不得不容忍它们 API 的设计问题(有的可能需要回溯到微信本身的 API 设计问题)、微信小程序开发工具的慢、Buggy、与真机不一致,还有就是 H5 和小程序兼容性问题等等。
总之,小程序开发是我见过,开发体验最差的工种 😘。