前言
先礼貌性吹一下~
微信小程序从 17 年发布到现在已经 6 年时间了,在这 6 年的时间里,微信小程序得到了迅速的发展和普及,成为了移动互联网领域的一股强大力量。
截至2022年底,微信小程序已经拥有了超过 6 亿的日活跃用户,涵盖了从生活服务到游戏娱乐等各行各业的应用场景,成为了很多企业和个人进行移动互联网应用开发的首选平台。微信小程序的生态系统也在不断扩展和完善,不仅提供了丰富的组件和开发工具,还提供了广告、支付、智能客服、云开发等增值服务,为开发者提供了更多的商业变现机会。
不过和其庞大的商业价值相比,微信小程序的技术架构却经常被程序员吐槽,其自研的 微信小程序框架
和 WXML/WXSS/JS
语言,与主流的 Web
开发语言和框架有较大的区别,开发者需要学习新的开发技术和语法;
受制于其运行环境,开发的产品体验也有局限性,比如页面跳转深度不能超过 10 页、代码包大小有限制、获取硬件相关 API
如蓝牙、NFC
等都需要额外的授权操作,随着版本更新,现在连用户头像、昵称、手机号都不能直接拿到了需要通过弹窗手动授权。
反正类似的问题还有很多,一言难尽,但是从业务上来说,小程序产品无疑是成功的,其具有开发门槛低、使用方便、开发周期短等优点,可以满足很多企业和个人的需求;
我上一次独自完整的开发一个小程序还是几年前了,很多细节都已遗忘,这次随着团队开发新的小程序项目的机会,重新记录梳理一个小程序项目中需要掌握的知识。
技术选型
小程序开发如果没有特殊的原因,用原生框架写就好,不建议用第三方框架如 uniapp
、mpvue
等,mpvue 是美团给大家挖的坑,bug 巨多而且官方19年就停止维护了,稍微新一点的写法都不支持了。
mpvue 曾经遇到过的 bug 举例:
- 在 mpvue 中,页面正常跳转后并没有销毁,
beforeDestroy
、destroyed
基本没用,第二次打开同一个页面的时候,data 也不会是初始化的数据,这就很坑了,需要手动初始化 data;
this.$options && Object.assign(this.$data, this.$options.data()) // mpvue bug 手动初始化变量
- 混用原生 和 vue 的生命周期引起未知 bug, 官方说不要用小程序原生的生命周期钩子(狗子)。
- 模版中三元运算符都不支持?
- 一个页面子组件超过7个会导致子组件事件无法触发
- 绑定对象属性值为0时会赋值为空字符串
- mpvue 不支持style 和class 属性中使用函数 ...
总结下就是 原生小程序本身就有坑,在这基础上做的第三方框架问题只会更多,而且随着 vue 和 小程序版本更新,它得一直维护,否则会越来越难用。
uniapp 应该说肯定是比 mpvue 好很多的,毕竟不是个 kpi 项目,不过同样会遇到以上的问题,如果没有跨平台的需求, 直接用原生就好。
原生写法的特点:
- 不需要像 webpack 一样启动运行服务器;
- 也支持 npm 包,不过需要额外在开发者工具中构架一下才能使用,构建后生成了一个
miniprogram_npm
文件夹;
登录/注册
小程序的登录注册流程和 web 不一样,不需要一个传统的账号密码登录/注册页面,官方推荐流程如下图,从前端来说,只需要这样:
- wx.login() 获取临时 code;
- 调用后端接口传 code,后端返回 token,存入 storage 里面;
- 之后每次请求将 token 放入 request headers 里,给后端做身份验证;
openID
是用户在单个小程序内的唯一标识,unionID
是微信开放平台的用户标识,同一用户在不同的微信公众号和小程序中使用同一个微信开放平台账号时,可以通过 UnionID 实现用户信息的关联;后端给的 token 之所以能实现身份验证就是因为和 openID 做了关联。
除了以上基本流程外,还要注意几点,通过 wx.login 拿到的 code 是会过期的,一般过期时间 5 分钟左右,建议在每次请求前先执行 wx.checkSession
看下 code 是否过期,如果过期了可以先把当前接口用数组暂存下来,完整的走一遍登录流程后,再取出暂存的接口执行,这样就实现了静默登录
。
code 和 后端返回的 token(自定义登录态),过期时间是不一样的,token 的过期时间是由开发者自行管理的,通常会比 code 更长一些,所以在请求前 校验 wx.checkSession 的基础上还要在响应里校验 res 返回值是否过期,当后端接口因token 过期返回了特定的错误码时 如 981,就执行登录过期的操作,一般是清空用户信息和 token,重新 onLoad 页面,执行登录流程。
手机号一键登录
这种登录方式需要用户主动点击授权按钮,调起微信的弹窗授权,点击同意后可获得包含加密手机号的数据对象,通过后端接口进行数据解密可拿到用户手机号信息(旧版)。
新版(2.21.2起)略有改动,新增了 code 字段,后端可消费 code 来 调用 getPhoneNumber
接口获取手机号。文档
<button open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber"></button> Page({ getPhoneNumber (e) { console.log(e.detail.code) } })
数据是存 globalData 还是 storage 好
这个问题困扰了我半天,看了几个已有的小程序,发现很多是混用的,这让我有点疑惑,借助 chatGpt 我对两者做了比较如下:
最后我选择使用 storage
来做数据存储,我不想混用,容易带来维护问题,globalData
作为全局变量非持久化缓存,可能小程序放后台几分钟就丢失了,感觉 storage 稳一点,目前运行起来暂没发现有什么问题,我主要存 userData、身份 token、省市区数据等。
需要注意的是,在可能引起 userData 改变的操作后,需要获取最新的数据,更新 srotage 并使用 wx.relaunch
来刷新页面缓存,激活子页面的 onload 事件来重新获取最新的 userData,以避免引起数据同步问题。
如何等待 onLaunch 拿到后端数据后,再执行页面 onLoad
onLaunch
是小程序生命周期函数之一,表示小程序初始化时执行的函数;它只在冷启动时执行一次,热启动则不执行,可以进行一些全局的初始化工作,例如获取用户信息、登录状态的检测、网络请求的初始化等。
理论上我们需要先执行小程序的 onLaunch 当拿到结果后再执行页面的 onLoad 事件,可实际上并非如此,onLaunch 执行后立马就执行了页面的 onLoad,当需要在 onLaunch 执行异步操作时候,就不满足需求了,该怎么解决这个问题呢?
举例一个实际场景,在 onLaunch 时候获取用户信息,然后存到 storage 里供所有页面使用, 我们可以通过回调函数的方法解决数据同步问题。
// 主入口 App({ onLaunch: function () { getUserData().then(res => { if (this.onLaunchReady) { this.onLaunchReady(res) } }) } }) // 业务页面 const App = getApp() Page({ onLoad() { const data = wx.getStorage('userData') && JSON.parse(wx.getStorage('userData')) if(data) { this.setData({ userData: data }) } else { App.onLaunchReady = data => { this.setData({ userData: data }) } } } })
使用 behavior 解决代码复用问题
页面或者自定义组件中可以使用类似 mixins
的工具 behavior
来服用相同的数据字段和方法, 比如每个页面的 onLoad 方法中可能需要获取/更新用户信息,就可以放到 behavior 中。
const { getUserData } = require('./http') // my-behavior.js module.exports = Behavior({ data: { userData: {}, }, methods: { onLoad: async function () { const userData = wx.getStorageSync('userData') && JSON.parse(wx.getStorageSync('userData')) if (userData) { this.setData({ userData, }) } else { const res = await getUserData() this.setData({ userData: res, }) } this.onLoaded() }, }, })
这里可能你有一点疑惑,之前不是把 getUserData()
放到小程序的 onLaunch
函数里了吗,怎么又写在页面内的onload 里了,其实是因为我发现当接口登录过期时,我执行 wx.reLaunch
来刷新页面,本以为会触发小程序的 onLaunch 进行二次登录,实则并没有,这样就引起 bug 了,所以放弃了那个方案,改为每个页面都在onload 里进行判断酌情处理,为了减少代码重复,于是使用 behavior 来复用这块逻辑,当 behavior
里的 onload 处理完后,要调用自定义的 onLoaded 函数来通知页面,可以在 页面内定义 onLoaded
函数来执行当前页面数据获取等操作。
除了使用 behavior 外,还可以对 Page 构造器进行改造,比如自定义 PageFormat 函数对 Page 里的各个属性进行劫持,这样也可以实现代码复用。
重视自定义组件
小程序的自定义组件我认为是非常重要的,对于 C 端来说,很多模块重复使用的频率更高,比如 头部栏
模块,一般展示用户头像、昵称、基本信息、消息通知等,像我这周做的这个小程序,首页、中间页、我的,都需要展示头部栏,无非是样式有些不同,而头像上传、登录/注册、认证等逻辑都是一样的,这种如果不封装成组件,在每个页面内重复编码将带来巨大的代码冗余,不利于维护。
还有像自定义导航栏也是需要封装的,现在很多UI 设计的不使用系统自带的导航栏,微信官方也支持在页面 json 文件里配置 "navigationStyle": "custom"
来隐藏系统导航栏,这种使用频率很高的功能一定要写成组件。
自定义组件的写法很简单,文档 看下就懂,和 Page 构造器相比只是写法上略有不同,有意思的是其事件传递看起来和 vue 的很接近,完全没有使用障碍。
组件内部的生命周期有两类,一类是组件自身的, 一类是组件所在页面的,举例如下,不全,完整的看文档。
Component({ lifetimes: { attached: function() { // 在组件实例进入页面节点树时执行 }, detached: function() { // 在组件实例被从页面节点树移除时执行 }, }, pageLifetimes: { show: function() { // 页面被展示 }, hide: function() { // 页面被隐藏 }, resize: function(size) { // 页面尺寸变化 } } })
骨架屏
小程序现在官方提供了骨架屏方案,只需要在开发者工具中点击按钮,即可生成当前页面内的骨架屏代码,匹配度上比传统的 UI 组件库提供的骨架屏组件好一些,不过也有缺点,比如:
- 代码量较大,生成了多个文件;
- 只能控制页面级别,无法对局部精细控制;
- 难以实现复杂布局;
所以用不用也要看情况,我自己没有用它这个,而是用了 vant-weapp
组件库的骨架屏组件。
如何用 canvas 制作动态活动海报
小程序也支持 canvas 标签,用法细节和 web 有一些区别,这块我之前单独写了一篇文章,感兴趣的可以看看 小程序使用 canvas 制作活动分享海报的一点点细节
总结
我在做的这个小程序目前才开发一周,功能较简单,值得记录的点暂时就这么多,后续有新的积累后会再次更新分享给大家,总的来说小程序开发相比 web 来说是比较简单的,我们在微信给的框架里有限度的发挥其能力,很容易触碰到技术边界,但也可以利用好微信提供的生态接口,在业务上发挥其优势,服务好客户。