简介
小程序是依赖特定小程序平台环境的跨平台应用程序
无需下载安装,即开即用,同时在使用体验上更贴近一般手机APP
小程序平台
小程序平台虽然多,但开发体验都是类似的,使用的框架、语法、和API定义基本相同
以下列出一些常见的小程序平台【小程序文件命名大赏】
微信小程序 wxml wxss wxs
QQ小程序 qml qss qs
支付宝小程序 axml acss sjs
抖音小程序 ttml ttss sjs
京东小程序 jxml jds jxss
百度小程序 swan css js
快手小程序 ksml css
等等...
以及快应用(手机厂商系小程序)
使用场景
小程序应用于各大平台,各种不同的行业和业务,包括ToC、ToB的各种场景,各种类别如下图
分支:车载小程序
发展前景
总体来看,各大平台方都在推进小程序的生态建设,推出各种扶持计划,开放平台的各种能力。
各种知名品牌都在做多个小程序平台的布局,纷纷入驻各类平台。
小程序的使用场景也在拓展,应用于更多细分领域。
目前,小程序对于业务的重要性非常明显,未来几年小程序也应该会平稳发展。
优势
小程序自带跨平台能力,同时开发使用js语言,与一般前端开发体验相似,相较于手机原生应用,开发效率更高
不需要下载安装即可使用,使用门槛更低
一些常见功能,宿主平台都封装好了,提供了js API,提高了开发效率,如媒体、网络、蓝牙、文件、位置等操作
依托于各大流量平台,可以轻松使用相应的平台能力,与平台内其他功能联动,同时在平台内推广也较为容易
劣势
小程序进入入口深,应用留存率差
运行依靠特定小程序平台容器,受特定平台监管约束,开发时可使用的功能较原生少,开发灵活度低
本质私域应用,无法接入现有工作流,所有开发工作需要在平台提供的网站和应用下进行
可能因平台规则调整,或者一些付费功能的推出而被迫适配,带来额外的人力消耗和开销
开发体验不好
具体表现为,小程序的兼容性不强,提供的API有些在不同平台(安卓、ios)上表现不同
API陈旧不更新,如许多微信API仍不支持Promise调用,需要使用onSuccess回调函数
小bug较多,如果遇上bug问题,对于开发者来说没有办法,只能等平台官方修复,往往需要很长时间
类似的技术
在国内已经凉凉的 PWA
开发入门
与小程序开发相关内容以微信小程序为例,各平台都差不多
快速起步
跟随微信的开始文档,申请并注册小程序账号,并下载小程序开发工具
在小程序管理平台填写小程序的相关信息
并在设置-账号信息处获得 AppID,AppID是小程序的唯一标识,与认证的主体和邮箱绑定
AppID是公开的,泄露没有安全风险
之后打开小程序开发工具,填写相应的AppID,并选择模版
这里官方提供了 ts + less/sass 的模版,直接选择 ts + less 的模版
此时初始化的目录结构如下,miniprogram
为小程序的src
目录
bash
/pages 目录下为小程序的页面文件,初始有index和logs两个页面
/utils 工具函数
app.json 全局配置文件
app.less 全局样式文件
app.ts 注册小程序,小程序入口
代码构成
从上图目录结构可见,index 页面下有 .json .less .ts .wxml 4种类型文件,即
.json
后缀的JSON
配置文件.wxss
后缀的WXSS
样式文件 .less.js
后缀的JS
脚本逻辑文件 .ts.wxml
后缀的WXML
模板文件
全局配置 app.json
app.json 为小程序的全局配置,可以配置小程序页面、窗口样式、底部tabBar、分包结构、使用到的权限等
json
{
// 小程序默认启动首页
"entryPagePath": "pages/index/index",
// 小程序页面数组
"pages": ["pages/index/index", "pages/home/index", "pages/taskAdd/index"],
// 全局窗口默认样式
"window": {
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#fff",
"navigationBarTitleText": "Todo",
"navigationBarTextStyle": "black"
},
// 底部 tab 栏
"tabBar": {
"color": "#9ca3af",
"selectedColor": "#374151",
"backgroundColor": "#FFF",
"borderStyle": "white",
"list": [
{
"pagePath": "pages/index/index",
"text": "主页",
"iconPath": "assets/tabBar/list-outline.png",
"selectedIconPath": "assets/tabBar/list.png"
},
{
"pagePath": "pages/home/index",
"text": "个人",
"iconPath": "assets/tabBar/home-outline.png",
"selectedIconPath": "assets/tabBar/home.png"
}
]
},
// 注册全局组件
"usingComponents": {
"van-button": "@vant/weapp/button/index",
"van-cell": "@vant/weapp/cell/index",
"van-cell-group": "@vant/weapp/cell-group/index"
},
"sitemapLocation": "sitemap.json",
"lazyCodeLoading": "requiredComponents"
}
pages 数组中的每一项对应一个页面
如pages/index/index
中的index对应该页面的4种文件的名称,必须严格同名
默认情况下,pages中的第一个页面为小程序的启动页面
手机顶部为状态栏(statusBar)
之后是顶部导航栏(navigationBar),背景(background),页面(page),以及底部tab栏
全局配置中的windows
就是指顶部导航栏的样式设置
页面配置 page.json
针对单个页面的配置,如配置该页面的导航栏样式,标题文本,是否开启下拉刷新,触底距离等
详见 页面配置
json
{
// 导航栏标题
"navigationBarTitleText": "消息详情",
// 开启下拉刷新
"enablePullDownRefresh": true,
// 注册页面使用的组件
"usingComponents": {
"van-cell": "@vant/weapp/cell/index",
"van-cell-group": "@vant/weapp/cell-group/index",
}
工具配置 project.config.json
针对于小程序开发者工具的配置文件,包含项目基本信息,编译设置,编译模式等
详见 项目配置文件
useCompilerPlugins 可配置编译插件,以支持 typescript、less、sass
packNpmManually packNpmRelationList 手动构建并在小程序中使用 npm
wxml
小程序的wxml文件对应于html文件,但语法不同
- 标签只能写微信提供的基础组件和注册的自定义组件,如 view组件 对应 div
- wxml提供了类似vue的语法,在下文会再说明
html
<!-- log.wxml -->
<scroll-view scroll-y type="list" style="height: 100vh; padding-top: env(safe-area-inset-top); box-sizing: border-box;">
<!-- 条件渲染 -->
<view class="log-list" wx:if="showLog">
<!-- 列表渲染 数据绑定 -->
<block wx:for="{{logs}}" wx:key="timeStamp" wx:for-item="log">
<text class="log-item">{{index + 1}}. {{log.date}}</text>
</block>
</view>
</scroll-view>
可以下载 vscode 插件 WXML - Language Service 获得 .wxml 文件的语法高亮和自动补全
wxss
对应于 css 文件,不同点如下
- 只支持部分选择器,如常见的元素、类、id选择器
- 新增了尺寸单位
rpx
,用于做屏幕大小适配
rpx会根据屏幕宽度进行自适应,规定所有屏幕宽为750rpx。
如在 iPhone6 上,屏幕宽度为375px,共有750个物理像素,则750rpx = 375px = 750物理像素,1rpx = 0.5px = 1物理像素。
- 在 app.wxss 书写的为全局样式,page.wxss 书写的为单页面样式,各个页面的样式完全隔离
- 页面顶层为
page
,相当于html
中的body
,可用于修改页面背景颜色等
宿主环境
微信客户端给小程序所提供的环境为宿主环境
微信小程序运行在多种平台上:ios/iPadOS、Android、Windows PC、Mac、微信开发者工具
不同平台的运行环境不同, 逻辑层代码运行和视图层渲染的环境不同,详见小程序运行时|运行环境
如 ios 的逻辑层运行在 JavaScriptCore 渲染层运行在 WKWebView
Android 的逻辑层运行在 V8,渲染层运行在 XWeb
ios、Android、微信开发者工具的运行环境差异大,在开发完毕后必须在不同系统的真机上进行测试
通信模型
小程序的渲染层和逻辑层分别由2个线程管理,为双线程模型
渲染层的界面使用了WebView 进行渲染,逻辑层采用JsCore线程运行JS脚本。
一个小程序存在多个页面,所以渲染层存在多个WebView线程,形成了小程序的页面栈
这两个线程的通信会经由微信客户端做中转,逻辑层发送网络请求也经由Native转发
需要注意的是,一般 Web 开发中,每个页面中 JS 线程与渲染线程是互斥的。
而在小程序中,JS 线程与渲染线程(视图层)是独立的,视图的渲染更新,并不会阻塞 JS 的执行,同时 JS 的逻辑执行,也不会阻塞视图的渲染更新。 JS 驱动视图的更新是异步的,并且 JS 无法直接访问视图的 DOM。
简单理解:小程序运行是双线程的,js线程与渲染线程独立,视图更新是异步的
视图更新
小程序的语法虽然类似vue,使用插值语法,但没有监听变量的更改,需要使用 setData 函数,并以对象形式传入需要更新的 data,主动触发视图更新,详见合理使用setData
html
<view>{{ msg }}</view>
ts
Page({
data: {
msg: 'Hello World'
},
onLoad: function () {
this.setData({ msg: 'Goodbye' })
}
})
setData 所做的事如下
- 逻辑层虚拟 DOM 树的遍历和更新,触发组件生命周期和 observer 等;
- 将 data 从逻辑层传输到渲染层;
- 视图层虚拟 DOM 树的更新、真实 DOM 元素的更新并触发页面渲染更新。
第二步传输在独立的运行环境间进行,需要进行数据的序列化、反序列化,因此数据传输过程是异步的、非实时的,传输量越大,耗时越久
所以在使用 setData 时注意只传入改变的数据,控制使用的频率
逻辑层
小程序开发框架的逻辑层使用 JavaScript
引擎
开发者写的所有代码最终将会打包成一份 JavaScript
文件,并在小程序启动的时候运行,直到小程序销毁
需要注意的是所有页面的脚本逻辑都跑在同一个JsCore 线程
页面使用setTimeout或者setInterval的定时器,然后跳转到其他页面时,这些定时器并没有被清除,需要开发者自己在页面离开的时候进行清理
当使用 音视频功能 创建 context 时需要特别注意在结束后清除 context,防止内存泄漏
注册小程序 App
每个小程序都需要在 app.js
中调用 App
方法注册小程序实例,绑定生命周期回调函数,注册全局属性和函数。
此时注册的为小程序全局唯一实例,全部页面共享。
如下方的例子,上方以 on 开头的为 app 的生命周期函数,下方的 globalData 是自定义的属性
App所有生命周期函数见文档 小程序 App
js
// app.js
App({
onLaunch (options) {
// 监听小程序初始化 小程序初始化完成时触发,全局只触发一次
},
onShow (options) {
// 监听小程序启动或切前台 小程序启动,或从后台进入前台显示时触发。
},
onHide () {
// 监听小程序切后台 小程序从前台进入后台时触发。
},
onError (msg) {
// 错误监听函数 小程序发生脚本错误或 API 调用报错时触发
},
globalData: 'I am global data'
})
})
在页面中,可以使用 getApp
函数来获取 App 全局实例,以获取注册在 App 上的属性和函数,如全局数据
js
// xxx.js
const appInstance = getApp()
console.log(appInstance.globalData) // I am global data
注册页面 Page
页面由 wxml、js、wxss、json文件组成,其中wxml和js文件是必选的,wxss和json文件是可选的。
页面路径需要在小程序全局配置文件 app.json 中的 pages 字段声明,否则这个页面不会被注册到宿主环境中
使用 Page
函数在对应的 js 文件中注册页面,指定页面的初始数据、生命周期回调函数、事件处理函数等
使用 this.data
访问页面数据,使用this.setData
更新页面数据,使用 this.setData
时只设置需要修改的属性,其他属性会自动合并。
注意:
- 不要把data中的任意一项的value设为undefined,最好使用null,否则可能导致bug,这是因为
this.setData
会自动合并属性,传入 undefined 和不传该属性的效果是一样的 - 仅支持设置可json化的数据,函数、类是无法设置的
js
//index.js
Page({
data: {
text: "This is page data."
},
onLoad: function(options) {
// 页面创建时执行
},
onShow: function() {
// 页面出现在前台时执行
},
onReady: function() {
// 页面首次渲染完毕时执行
},
onHide: function() {
// 页面从前台变为后台时执行
},
onUnload: function() {
// 页面销毁时执行
},
onPullDownRefresh: function() {
// 触发下拉刷新时执行
},
onReachBottom: function() {
// 页面触底时执行
},
onShareAppMessage: function () {
// 页面被用户分享时执行
},
onPageScroll: function() {
// 页面滚动时执行
},
onResize: function() {
// 页面尺寸变化时执行
},
onTabItemTap(item) {
// tab 点击时执行
console.log(item.index)
console.log(item.pagePath)
console.log(item.text)
},
// 事件响应函数
viewTap: function() {
this.setData({
text: 'Set some data for updating view.'
}, function() {
// this is setData callback
})
},
// 自由数据
customData: {
hi: 'MINA'
}
})
其他 Page 的生命周期函数见 小程序 Page
App 生命周期
初次进入小程序的时候,微信客户端初始化好宿主环境,同时从网络下载或者从本地缓存中拿到小程序的代码包,把它注入到宿主环境。
初始化完毕后,微信客户端就会给App实例派发onLaunch事件,App构造器参数所定义的onLaunch方法会被调用。
进入小程序之后,用户可以点击右上角的关闭,或者在手机上切换应用,此时小程序并没有被直接销毁,我们把这种情况称为"小程序进入后台状态",App构造器参数所定义的onHide方法会被调用。 当再次回到微信或者再次打开小程序时,微信客户端会把"后台"的小程序唤醒,我们把这种情况称为"小程序进入前台状态",App构造器参数所定义的onShow方法会被调用。
场景值
打开小程序的途径是非常多的,包括在微信客户端内唤起,或者是其他App唤起微信小程序,或者是小程序唤起其他小程序。
为了区分不同打开小程序的途径,方便区分处理,小程序引入了【场景值】,在 onLaunch 参数中传入
对于小程序,可以在 App
的 onLaunch
和 onShow
的函数参数,或在页面中调用函数 wx.getLaunchOptionsSync 中获取场景值。
此处列出常见的几个场景值,所有场景值见 场景值列表
场景值 | 说明 |
---|---|
1001 | 发现页小程序「最近使用」列表 |
1005 | 微信首页顶部搜索框的搜索结果页 |
1006 | 发现栏小程序主入口搜索框的搜索结果页 |
1007 | 单人聊天会话中的小程序消息卡片 |
1008 | 群聊会话中的小程序消息卡片 |
1010 | 收藏夹 |
1011 | 扫描二维码 |
1012 | 长按图片识别二维码 |
1013 | 扫描手机相册中选取的二维码 |
1014 | 小程序订阅消息(与1107相同) |
Page 生命周期
页面初次加载时调用 onLoad 方法,可获得页面 query 参数,onLoad在页面没被销毁之前只会触发1次
页面显示后,调用 onShow 方法,从别的页面返回到当前页面时,当前页的onShow方法也会被调用
在页面初次渲染完成时,调用 onReady 方法,onReady在页面没被销毁前只会触发1次
onReady触发时,表示页面已经准备妥当,在逻辑层就可以和视图层进行交互了。
页面不可见时,调用 onHide 方法,常见于切换到其他页面。
当页面被销毁回收时,调用 onUnload 方法,如使用 wx.redirectTo、wx.navigateBack
此外,page 还有针对于用户行为的函数
- onPullDownRefresh 下拉刷新
- onReachBottom 上拉触底
- onPageScroll 页面滚动
- onShareAppMessage 用户转发 可定义转发标题和路径,只有定义了此事件处理函数,右上角菜单才会显示"转发"按钮
在页面配置 page.json 中可以调整下拉 loading 的样式,控制是否开启下拉刷新,以及页面上拉触底事件触发时距页面底部距离
页面路由
小程序含有多个页面,各个页面的进入顺序形成了页面栈,页面栈最多十层
当小程序含有底部tab栏时,tab栏每个对应的页面都会被缓存,页面栈底部为 tabbar对应的页面,之后才是新打开的页面
可以使用getCurrentPages()
函数获取当前的页面栈,每个页面对象中含有页面路径、参数等信息。
路由方式 | 页面栈表现 |
---|---|
初始化 | 新页面入栈 |
打开新页面 wx.navigateTo | 新页面入栈 |
页面重定向 wx.redirectTo | 当前页面出栈,新页面入栈 |
页面返回 wx.navigateBack | 页面不断出栈,直到目标返回页 |
Tab 切换 wx.switchTab | 页面全部出栈,只留下新的 Tab 页面 |
重加载 wx.reLaunch | 页面全部出栈,只留下新的页面 |
其中 wx.navigateTo 的功能最强大,可以在当前页面和要打开的页面间建立监听(eventChannel),实现页面间通信
当前页面通过eventChannel向被打开页面传送数据,而被打开的页面可以触发事件,更新当前页面的数据
js
wx.navigateTo({
url: 'test?id=1',
events: {
// 为指定事件添加一个监听器,获取被打开页面传送到当前页面的数据
someEvent: function(data) {
console.log(data)
}
...
},
success: function(res) {
// 通过eventChannel向被打开页面传送数据
res.eventChannel.emit('acceptDataFromOpenerPage', { data: 'test' })
}
})
js
// test.js
Page({
onLoad: function(option){
console.log(option.query)
const eventChannel = this.getOpenerEventChannel()
eventChannel.emit('someEvent', {data: 'test'});
// 监听acceptDataFromOpenerPage事件,获取上一页面通过eventChannel传送到当前页面的数据
eventChannel.on('acceptDataFromOpenerPage', function(data) {
console.log(data)
})
}
})
视图层
wxml
基本类似 vue 语法,在模版中被 {{ }} 括起的内容会被作为 js 表达式处理,但是只能进行简单的运算,不能写
每个微信自带的组件都含有 id、class、style、data-* 等属性
数据绑定
html
<view id="item-{{id}}">{{ message }} </view>
js
Page({
data: {
message: 'Hello',
id: 0,
},
})
条件渲染
wx:if hidden 语法
wx:if 类似 v-if ,hidden 类似 v-show
html
<view wx:if="{{view == 'WEBVIEW'}}"> WEBVIEW </view>
<view wx:elif="{{view == 'APP'}}"> APP </view>
<view wx:else="{{view == 'MINA'}}"> MINA </view>
<view hidden="{{true}}">hidden</view>
或者使用 block wx:if,此处 block 不是组件,只是包装了内部元素,类似 react fragment 或 vue template
html
<block wx:if="{{true}}">
<view> view1 </view>
<view> view2 </view>
</block>
wx:if 会重建和销毁组件,而 hidden 相当于 display:none
频繁切换时更推荐使用 hidden
列表渲染
默认渲染的数组,下标为 index,当前项为 item
html
<view wx:for="{{array}}">
{{index}}: {{item.message}}
</view>
或者使用 wx:for-item 指定元素项名,使用 wx:for-index 指定下标名
html
<view wx:for="{{array}}" wx:for-index="idx" wx:for-item="itemName">
{{idx}}: {{itemName.message}}
</view>
使用 block wx:for 也可
html
<block wx:for="{{[1, 2, 3]}}">
<view> {{index}}: </view>
<view> {{item}} </view>
</block>
当列表可能改变时,最好携带 wx:key
直接使用字符串表示数组项的某个属性,或者使用*this表示key为数组项本身
html
<switch wx:for="{{objectArray}}" wx:key="unique" style="display: block;"> {{item.id}} </switch>
<button bindtap="switch"> Switch </button>
<button bindtap="addToFront"> Add to the front </button>
<switch wx:for="{{numberArray}}" wx:key="*this" style="display: block;"> {{item}} </switch>
<button bindtap="addNumberToFront"> Add to the front </button>
事件
小程序中的事件类似 dom 事件
每个小程序的组件都有对应的事件
常见的事件有
- tap 手指触摸后马上离开,即点击(click)
- longpress 手指触摸后,超过350ms再离开,即长按
- input 输入框输入
- focus 输入框聚焦
- blur 输入框失焦
在事件前添加 bind 或 catch 或 capture-bind
bind 事件会向上冒泡 catch 事件不会向上冒泡,如catchtap 不会冒泡,而 bindtap 会冒泡
使用 capture-bind 绑定捕获阶段
事件绑定的写法如下,在 bindtap 中的值必须是 字符串,绑定 Page 中的事件处理函数
回顾:因为小程序采用双线程模型,视图层中事件触发时告知逻辑层,执行相应函数,所以事件绑定直接为函数名
视图层其实不知道页面实时的data、method等数据
html
<view bind:tap="bindTapView">tap me!</view>
类似这样的写法,写箭头函数是错误的
html
<view bind:tap="(e)=>bindTapView(e)">tap me!</view>
如果想让事件函数携带数据,可以在 view 上使用 data-* 属性,并使用数据绑定,如
html
<view bind:tap="bindTapView" data-number="{{1}}"
data-array="{{[1,2,3]}}" data-param="{{param}}">tap me!</view>
js
Page({
data: {
param: {
a: 1,
b: 2,
},
},
bindTapView(e: any) {
console.log(e)
},
})
然后在事件处理函数的事件对象e中获得
自定义组件
自定义组件的内容较多,此处只介绍常用的
创建组件
创建组件,像页面一样需要 wxml wxss js json 4种文件
首先在 json 文件中声明此为自定义组件
json
{
"component": true
}
然后书写 wxml、wxss 文件 见 组件模板和样式
组件模版:
组件模版中中可以使用 slot,类似 vue 中的 slot,也可以使用具名插槽来使用多个 slot
默认情况下一个组件只能有一个 slot,可在组件中声明使用多 slot
js
Component({
options: {
multipleSlots: true // 在组件定义时的选项中启用多slot支持
}
...
html
<!-- index.wxml 单slot-->
<view class="row" bind:tap="tapRow">
<view class="row-left">{{title}} {{subTitle}}</view>
<slot></slot>
<view class="row-right"></view>
</view>
html
<!-- index.wxml 多slot-->
<view class="row" bind:tap="tapRow">
<view class="row-left">{{title}} {{subTitle}}</view>
<slot name="content"></slot>
<slot name="right"></slot>
</view>
引用单slot的组件时
html
<custom-row title="custom title" subTitle="{{subTitle}}">
<view>custom slot</view>
</custom-row>
在引用多slot的组件时,声明每个slot的name
html
<!-- 引用组件的页面模板 -->
<view>
<custom-row title="custom title" subTitle="{{subTitle}}">
<view slot="content">content</view>
<view slot="right">right</view>
</custom-row>
</view>
组件样式:
组件的样式只能使用类选择器
默认情况下,自定义组件的样式只受到自定义组件 wxss 的影响,甚至不受全局样式影响,与外部完全隔离,除非使用 externalClasses 属性声明外部样式类
声明组件:
使用 Component 函数声明组件,详见 Component 构造器
注意:要声明每个传入的props,组件内使用this.data.propName
访问prop
ts
// index.ts
Component({
// 外部传入 props
properties: {
title: { // 有默认值的prop
type: String,
value: '默认值',
},
subTitle: { // 无默认值的prop
type: String,
},
},
// 组件内部数据
data: {
rightText: 'right',
},
// 自定义方法
methods: {
tapRow() {
console.log('tap row')
},
},
lifetimes: {
// 生命周期函数
attached() {},
moved() {},
detached() {},
},
pageLifetimes: {
// 组件所在页面的生命周期函数
show() {},
hide() {},
resize() {},
},
})
使用组件
在要引入的页面的json中注册自定义组件,key为组件的标签名,value为组件的位置
如果是在自定义组件内引入其他组件,方法也相同
注意:组件标签名只能包含小写字母、中划线、下划线,建议都使用肉串式命名
json
{
"usingComponents": {
"custom-row": "../../components/custom-row/index"
}
}
在页面的wxml中使用,并传参
html
<!-- 以数据绑定方式传入组件prop -->
<custom-row title="custom title" subTitle="{{subTitle}}">
<view>custom slot</view>
</custom-row>
组件通信
- 父传子 props 数据绑定
父传子 props 已说明
在 Component 的 properties 属性中声明 props 的类型和默认值,然后在 wxml 中传入即可
- 子传父 events 事件
events 事件
在 Component 中使用 this.triggerEvent 函数触发自定义事件
ts
// index.ts
Component({
...
// 自定义方法
methods: {
tapRow(e: any) {
console.log('tap row')
// 新增
this.triggerEvent('customTap', e)
},
},
...
})
然后在wxml中绑定事件处理函数
html
<custom-row ... bind:customTap="bindCustomTap">
<view>custom slot</view>
</custom-row>
ts
bindCustomTap(e: any) {
console.log('customTap', e)
},
获得的输出如下,在 triggerEvent 中传入的值可以在 event 参数的 detail 中获得
或者直接获取组件实例进行操作,类似 react 的 forwardRef 或 vue 的 ref
ts
// 自定义组件 my-component 内部
Component({
behaviors: ['wx://component-export'],
export() {
return { myField: 'myValue' }
}
})
html
<!-- 使用自定义组件时 -->
<my-component id="the-id" />
ts
// 父组件调用
const child = this.selectComponent('#the-id') // 等于 { myField: 'myValue' }
数据监听
wx 为组件提供了 observers 数据监听属性,用法类似 vue2 的watch属性
- 可用于监听 prop 变化
- 每次 setData 函数涉及该字段时触发,即使值不发生改变时也会触发
ts
Component({
...
observers: {
// 监听多个字段
'numberA, numberB': function(numberA, numberB) {
// 在 numberA 或者 numberB 被设置时,执行这个函数
this.setData({
sum: numberA + numberB
})
},
// 监听对象内字段
'some.subfield': function(subfield) {
// 使用 setData 设置 this.data.some.subfield 时触发
// (除此以外,使用 setData 设置 this.data.some 也会触发)
subfield === this.data.some.subfield
},
// 监听数组项
'arr[12]': function(arr12) {
// 使用 setData 设置 this.data.arr[12] 时触发
// (除此以外,使用 setData 设置 this.data.arr 也会触发)
arr12 === this.data.arr[12]
},
}
})
生命周期
组件的生命周期如下
- created 组件实例创建完毕,无法使用 setData
- attached 组件初始化完毕,进入页面节点,可使用 setData
- ready 组件渲染完毕
- detached 组件被从页面节点移除
另外还有
- moved 组件实例被移动到节点树另一个位置
- error 组件实例被从页面节点树移除时执行
组件中还能使用页面的生命周期函数,包含显示(show)、隐藏(hide)、尺寸变化(resize)
ts
// index.ts
Component({
...
lifetimes: {
// 生命周期函数
attached() {},
detached() {},
},
pageLifetimes: {
// 组件所在页面的生命周期函数
show() {},
hide() {},
resize() {},
},
})
组件库
weui 小程序官方组件库,可以拓展库直接引入不占代码体积
TDeisgn 腾讯官方组件库
vant 17年开源,长期维护
开发流程
小程序管理后台中的角色大体有三种
- 管理员 拥有所有权限,可以添加项目成员和体验成员
- 项目成员 参与小程序开发、运营的成员,可添加体验成员
- 体验成员 能使用体验版小程序
而项目成员又有三种角色权限,这个权限区分不明确,一般会给直接每个项目成员所有这三种角色权限
- 运营者 使用 开发管理,包括提交审核、发布等,以及其他运营类功能
- 开发者 可使用小程序开发工具,以及使用开发版小程序,修改管理后台中的开发设置
- 数据分析者 查看统计数据
各角色的权限详见 协同工作
小程序有4种版本
- 开发版 使用开发者工具进行预览、真机调试时为开发版,开发者将本地代码上传到 开发版本时对应的也是开发版
- 体验版 从上传的开发版中选择一个作为体验版
- 审核中版本 从开发版中选择一个提交审核,同时只能有一个版本处于审核状态
- 正式版 线上的小程序版本
一般的开发流程为
- 使用微信开发者工具进行开发,开发完毕后将代码包上传开发版本,并填写版本号等信息
- 从开发版本中选择一个版本为体验版,供测试走查
- 体验版没有问题后,提交审核
- 审核通过后,该版本处于待上线的状态,可以选择 全量发布和灰度发布,灰度发布可以仅允许项目成员使用,方便做上线后的测试工作
其他
awesome-weapp 一些微信小程序开发资源的汇总,可以找一些实用的库和可运行的demo
小程序开发框架
不在本文讨论范围,只简单说明
上面的文章已经说的比较清楚了,如果想使用框架开发小程序,基本只有 Taro 和 uni-app 可以选择
而选择使用小程序原生开发还是使用小程序开发框架进行开发主要看需求
小程序开发框架
优势:
- 可以使用完整的 react / vue 框架语法,不需要深入学习小程序原生语法,从web开发转到小程序开发的门槛更低,开发效率更高。
- 一定程度支持跨端,同时编译为多个平台的小程序
- 一般提供框架和一整套解决方案,如脚手架、组件库,相对更省心
劣势:
- 需要额外学习开发框架相关知识
- 编译后包体积较大,开发到一定规模不得不分包
- 无法使用各平台原生开发的库,否则失去跨端能力
- 性能有所损耗
- 在踩小程序原生坑的基础上还要踩开发框架的坑,后续开发依赖框架更新和修复
小程序原生开发
优势:
- 性能最优,编译打包体积小,依靠各小程序平台,相对最稳定
劣势:
- 各小程序平台开发细节有所不同,不能跨端
- 小程序的原生语法开发舒适度不如react、vue等主流前端框架
常见场景应用
兼容
小程序运行在不同的平台,不同的操作系统版本,不同的微信客户端版本上
为了实现兼容,微信提供了接口 wx.getSystemInfoSync
结果中比较重要的是 客户端平台,操作系统版本,微信版本
得到的结果如下
json
{
"brand": "Xiaomi", // 设备品牌
"model": "M2011K2C", // 设备型号
"screenWidth": 412, // 屏幕宽度
"screenHeight": 899, // 屏幕高度
"pixelRatio": 3.5, // 设备像素比
"windowWidth": 412, // 可使用窗口宽度
"windowHeight": 810, // 可使用窗口高度
"statusBarHeight": 40, // 状态栏的高度
"language": "zh_CN", // 微信语言
"version": "8.0.40", // 微信版本号
"system": "Android 13", // 操作系统及其版本
"platform": "android", // 客户端平台
"SDKVersion": "3.0.0", // 客户端基础库版本
"safeArea": { // 在竖屏正方向下的安全区域
"width": 412,
"right": 412,
"top": 40,
"left": 0,
"bottom": 899,
"height": 859
},
"fontSizeSetting": 16, // 用户字体大小
"deviceOrientation": "portrait", // 设备方向
...
}
同时也可使用 wx.canIUse 来判断对应的方法或属性在当前手机上是否可用
如果对某个版本有强依赖关系,也可以设置小程序最低可用版本和基础库最低可用版本
授权
小程序的部分接口需要用户授权后才能使用,这类需要授权的接口被微信分为多个 scope
获得一个 scope 的授权后,可以使用 scope 内的 api
常见的 scope 如
- scope.userLocation 精确地理位置
- scope.camera 摄像头
- scope.writePhotosAlbum 添加到相册
使用这些需要授权的 api 分以下几步
- 调用 wx.getSetting 获得当前用户的授权状态
- 如果用户已授权,直接调用 api 即可
- 如果用户未授权,使用 wx.authorize 请求该 scope 的授权,授权成功则调用 api
如果用户之前拒绝授权过,那么使用 wx.authorize 不会出现授权请求弹窗
此时只能引导用户打开小程序设置页 wx.openSettings,自行打开授权,否则无法使用该功能
交互反馈
触摸反馈
小程序没有pc端 hover,这种鼠标悬停的交互,相应的,小程序组件如 view、button都提供了 hover-class 属性
用于改变当组件被按下时的样式,提供触摸反馈
html
<view hover-class="task-line-hover"></view>
常见交互
常见的交互有
- loading 加载中
- toast 轻量的反馈
- modal 反馈
- actionsheet 动作面板,提供选项
这些交互微信都提供了 api 支持
需搭配 wx.hideLoading
可以设置 icon 为 none,以显示两行的文字
滚动行为
微信提供了统一的下拉刷新交互
当用户进行下拉行为时,触发 page 的 onPullDownRefresh 函数
当用户滑动屏幕到底部时,触发 page 的 onReachBottom 函数,可实现上滑加载功能
页面本身支持滚动,如果页面的某个区域需要滚动,需要支持下拉刷新、上滑加载的行为
可使用微信组件scroll-view
网络通信
小程序提供的网络通信 api 包括 网络请求、下载、上传、websocket
要注意的是,小程序只支持 https 请求,并且需要预先在管理后台中设置服务器域名才可使用
wx.request 返回 RequestTask
wx.downloadFile 返回 DownloadTask
wx.uploadFile 返回 UploadTask
wx.connectSocket 返回 SocketTask
每种请求接口调用都会返回相应的Task对象,借助Task对象可以 abort 请求,也可用于监听下载和上传的进度
只要请求发送,并返回响应就视为成功,需要开发者自己根据 响应 判断请求是否正常
请求触发 fail 的情况一般是 用户没有网络,或者请求超时,DNS解析错误
ts
wx.request({
url: 'https://test.com/postdata',
method: 'POST',
header: { 'content-type': 'application/json' },
data: {
a: {
b: [1, 2, 3],
c: { d: 'test' },
},
},
success(res) {
if (res.statusCode === 200) {
console.log(res) // 服务器回包信息
}
},
fail(err) {
console.log(err) // 错误信息
},
})
此外,网络请求的这些api都不支持promise风格调用,可以自己封装 promise 版本的 wx.request 函数
ts
type WechatRequestOption = WechatMiniprogram.RequestOption<
string | WechatMiniprogram.IAnyObject | ArrayBuffer
>
export function request(option: WechatRequestOption) {
return new Promise((resolve, reject) => {
wx.request({
...option,
success(res) {
resolve(res)
},
fail(err) {
reject(err)
},
})
})
}
也可以在此基础上根据业务场景自行封装 RequestUtil 类,实现请求的前置和后置拦截器
微信登陆
小程序一般需要获取微信登录态,并且标识当前用户
以下是一些概念介绍
- AppId 当前小程序的唯一标识,公开信息,无安全风险
- AppSecret 当前小程序的密钥,在后端调用 微信服务器 接口时需要使用,不能泄漏
- openid 用户在当前小程序的唯一标识
- unionid 微信开放平台账号下的唯一标识,如果一个账号绑定多个小程序,那么用户在这些小程序下的 unionid 是相同的
- session_key 本次登录的会话密钥
-
前端:调用 wx.login 获取 code (用户登录凭证,有效期5分钟)
-
前端:调用 后端接口,携带 code 信息
-
后端:接收到 code 信息,调用微信接口 code2Session 获得当前登录用户的 openid、unionid
-
后端:微信接口返回 openid、unionid信息,后端携带其他业务字段,一并返回给前端
-
前端:得到 openid、unionid及其他业务字段,存储在全局状态中
为什么小程序能做到免登录,因为 小程序 openid 与 单个微信账号绑定,而微信账号又与 手机号 绑定
所以根据 openid 就能判断当前登录的用户,可以绑定 openid 和 业务用户信息
比如 张三用户对应某个 openid,绑定后,下次张三通过这个微信账号使用小程序就不需要再登录了
数据缓存
小程序提供了 Storage 本地存储,与 localStorage 使用基本一致
Storage 大小限制为 10MB,通过 getStorageInfo 可以查看当前 Stroage 占用大小和限制的大小
设备能力
微信提供了一些设备能力,如 蓝牙、NFC、WIFI、日历、联系人、短信等等
详见 API - 设备
比较常用的是 网络状态、微信扫码
网络状态
监听网络状态变化,在无网络时,或者弱网情况下跳转到提示界面
在用户使用流量时,进行下载等行为时提示用户
微信扫码
可以选择从相机或者相册
可以扫描 一维码,二维码,返回解码后的内容
媒体能力
小程序提供了 地图、图片、音视频、实时音视频、录音、相机等媒体能力
其中比较常用的是 图片、视频、音频
图片
如果要显示图片,使用组件 image
图片方面,小程序提供了以下api
- 保存图片到相册 wx.saveImageToPhotosAlbum
要注意的是,该接口只支持本地文件,所以网络文件应该先下载到本地再保存到相册
- 预览图片或视频 wx.previewMedia 通过此接口预览的图片和视频,可以显示长按菜单,可以保存、收藏、转发给朋友
- 获取图片宽高、格式 wx.getImageInfo
- 编辑图片 wx.editImage 仅支持本地图片,使用后可能遇到 内容安全审核 合规问题
调用该接口会调起微信本身的编辑图片页面
-
压缩图片 wx.compressImage
-
拍摄或从相册选择图片和视频 wx.chooseMedia
视频
如果要显示视频,使用组件 video
如果需要自定义视频控件,并且使用编程方式控制视频播放、暂停等操作,则需要使用 VideoContext
在 video 组件上写明 id,并使用 wx.createVideoContext 获得 VideoContext
视频方面,小程序提供了以下 api
- 保存视频到相册 wx.saveVideoToPhotosAlbum 仅支持本地视频
- 获取视频信息,包括大小、时长等 wx.getVideoInfo 仅支持本地文件
- 压缩视频 wx.compressVideo
- 拍摄或从相册选择图片和视频 wx.chooseMedia
音频
小程序的音频组件 audio 已经停止维护了,不建议使用
若想播放音频,直接使用 wx.createInnerAudioContext 创建 InnerAudioContext
并在 innerAudioContext 实例上设置音频地址 src,使用 play、pause、stop、seek 等方法控制音频播放
文档
有时需要在小程序中显示和预览文档,如word、ppt、excel、pdf文件
可以先使用 wx.downloadFile 下载文档到本地
再使用 wx.openDocument 打开新页面,为微信的预览文档页面
小程序码
微信中可扫描的小程序码图片需要后端调用微信服务器接口获得
调用接口时可以传入page和scene,返回二进制图片
page为打开的页面路径
scene的值会作为query参数传递给页面,方便业务做处理
消息推送
消息推送大体上分为 一次性订阅消息 和 长期订阅消息
- 一次性订阅消息:用户订阅一次对应的模版消息后,服务端可不限时的发送一条对应的服务消息
- 长期订阅消息:用户订阅一次后,服务端可长期下发服务消息,仅面向线下公共服务开放
一般使用的都是一次性订阅消息,需要用户订阅一次后,服务端才能发送一次消息,如果一次性订阅多个消息模版,那么每个模版对应发送一次消息的资格。
- 选用消息模版
首先需要在小程序后台的 功能 - 订阅消息 处配置我的模版,选用公共模版库中的某个模版,并配置模版的关键字,场景使用说明等信息,等待审核通过后就能使用了,根据 模版ID,调用接口即可
- 用户订阅消息
调用 wx.requestSubscribeMessage 接口,传入此次消息的模版
该接口会呼起如下的授权动作面板
下面的各个提醒和通知的标题就对应了调用接口时的 tmplIds 参数,即小程序后台选择的模版ID
- 服务端发送信息
在模版的详细内容处留了填入对应参数的属性名,如活动名称对应 thing1.DATA
服务端调用接口 wx.requestSubscribeMessage 说明其中的几个参数
- page 点击消息后的跳转页面,可以带query参数
- template_id 使用的模版id
- touser 用户的openid
- data 模版内容
- miniprogram_state 跳转的小程序类型,如体验版、线上版等
这样该条消息就对应了 小程序内的一个URL,方便业务处理