微信小程序基础
特点
- 体验好
- 一次开发多端共享
- 离线缓存(10M)
- 接口更多(相比订阅号与服务号)
小程序与APP的区别
- 下载安装
- 内存占用
- 手机适配
- 产品发布
- 功能区别
- 应用场景
小程序与H5的区别
- 规范不一样
- 运行环境不一样
- 开发方式不一样
- 获取权限不一样
小程序与普通网页开发的区别
- 开发语言
- 多线程
- DOM BOM操作
- 运行环境
- 开发
小程序
注册小程序
mp.weixin.q.com
申请域名和证书
云开发
serviceless
微信开发者工具
技巧
vh单位,整个视口分为100份,设置为100沾满整个视口
rpx单位,宽高自适应750rpx宽度
行内元素text-align:center,实现左右居中
每个页面都运行在单独的webview中渲染,每个页面都有一个__webviewId
视图层webview和逻辑层jscore,运行在独立的进程中,数据绑定和事件机制进行通讯
条件渲染wx:if hidden
wx:for-item wx:for-index=""指定变量
小程序构成
app.json文件是当前小程序的全局配置,包括了小程序的所有页面路径,界面表现,网络超时时间、底部tab等
- page字段---用于描述当前小程序所有页面路径,这是为了让微信客户端知道当前是你的小程序页面定义在那个目录
- window字段---定义了小程序所有页面的顶部背景颜色、文字颜色定义等
工具配置project.config.json,工具配置文件,界面颜色、编译配置等,云配置文件跟随项目
页面配置page.json,和小程序页面相关的位置,让开发阿哲可以独立定义每个页面都有不一样的色调来区分不同的顶部颜色,是否允许下拉刷新等等
JSON文件中无法使用注释,添加注释会引发报错
代码会充斥着非常多的界面交互逻辑和程序各种状态变量,所以引入mvvm开发模式,提倡把渲染和逻辑分离,简单来说就是不再让JS直接操作DOM,JS只需要管理状态即可,然后通过一种模板语法来描述状态和界面结构的关系
wxss
- 新增了尺寸单位,在写css样式时,开发者需要考虑到手机设备的屏幕会有不同的宽度和设备像素比,采用一些技巧来换算一些像素单位。wxss在底层支持了新的尺寸单位rpx,开发者可以免去换算的烦恼,只要交给小程序底层换算即可,由于换算采用的浮点数运算,所以运算结果和预期结果有一点点偏差
- 提供了全局样式和局部样式,和前面的app.js,page.json的概念相同,你可以写一个app.wxss作为全局样式,会作用于当前小程序的所有页面,局部页面样式page.wxss仅对当前页面生效
渲染层和逻辑层
WXXM模板和WXSS样式工作在渲染层,JS脚本工作在逻辑层
渲染层和逻辑层分别由2个线程管理:渲染层的界面使用了webview进项渲染;逻辑层采用了JsCore线程运行JS脚本。一个小程序存在多个界面,所以渲染层存在多个WebView线程,这两个线程的通信会经由微信客户端Native做中转,逻辑层发送网络请求也经由Native转发
程序与页面
微信客户端在打开小程序之前,会把整个小程序的代码包下载到本地。
紧接着通过app.json的page字段就可以知道你当前小程序的所有页面路径
Page是一个页面构造器,这个构造器就生成了一个页面,在生成页面的时候,小程序框架会把data数据可index.wxml一起渲染出最终的结构
- 直接修改 Page实例的this.data 而不调用 this.setData 是无法改变页面的状态的,还会造成数据不一致。
- 由于setData是需要两个线程的一些通信消耗,为了提高性能,每次设置的数据不应超过1024kB。
- 不要把data中的任意一项的value设为undefined,否则可能会有引起一些不可预料的bug。
API
小程序提供的API按照功能主要分为几大类:网络、媒体、文件、数据缓存、位置、设备、界面、界面接地啊你信息还有一些特殊的开放接口
API约定
- wx.on*开头的API是监听某个时间发生的API接口,接受一个Callback函数作为参数,当该事件触发时,会调用Callback函数
- 如未特殊约定,多数API接口为异步接口,都接受一个Object作为参数
- API的Object参数一般由success,fail,complete三个回调来接受接口调用的结果
- wx.get*开头的API是获取宿主环境数据的接口
- wx.set*开头的API是写入数据到宿主环境的接口
API调用大多是异步的,其次,有部分API会拉起微信的原生界面,此时会触发Page的obHide方法,当用户从原生界面返回小程序时会触发Page的onShow方法
事件
渲染层传递给逻辑层的事件
- 点击界面上的某个按钮
- 长按某个区域
- 反馈应该通知给开发者的逻辑层,需要对应的处理状态呈现给用户
- video播放的过程中,播放进度条会一直变化
<!-- page.wxml -->
<view id="tapTest" data-hi="weChat" bindtap="tapName">Click Me!</view>
<!-- page.js -->
Page({
data:{},
tapName:function(event){
console.log(event);
}
})
事件通过bintap这个属性绑定到组件上,同时在当前页面的Page构造器中定义了对应事件处理函数tabName,当用户点击改view区域时,达到触发条件生成事件tap,该事件处理函数tapName会被执行,同时还会收到一个事件对象event
事件类型和事件对象
- touchstart
- touchmove
- touchcancel
- touchend
- tap
- longpress
- logtap
- transitionend
- animationstart
- animationiteration
- animationend
如上列举出的事件之外没其他组件自定义事件如无特殊声明都是非冒泡事件如<form/>submit事件,<input/>的的input事件,<scroll-view/>的scroll事件
事件对象属性列表
- type
- timeStamp
- target
- currentTarget
- detail
- touches
- changedTouches
target和currentTarget的区别,currentTarget为当前事件所绑定的组件,而target则是触发该事件的源头组件。
事件绑定与冒泡捕获
事件绑定的写法和组件属性一致
- key以bind或者catch开头,然后跟上事件类型如bindtap,catchtouchstart,1.5.0起,bind和catch后可以紧跟一个冒号,其含义不变如:bind:tap,catch:touchstart同时bind和catch前还可以加上capture-来表示捕获阶段。
- value是一个字符串,需要对应页面Page 构造器中定义同名的函数,否则触发事件时会在控制台报错。
- bind和capture-bind的含义分别表示事件的冒泡阶段和捕获阶段
captrue-bind不会阻止事件向上冒泡,capture-cache会阻止事件冒泡
兼容
使用wx.getSystemInfo或者wx.getSystemInfoSync来获取手机品牌、操作系统、微信版本以及小程序基础库版本号等,通过这个信息可以针对不同平台做差异化的服务
wx.getSystemInfoSync()
{
brand: "iPhone", // 手机品牌
model: "iPhone 6", // 手机型号
platform: "ios", // 客户端平台
system: "iOS 9.3.4", // 操作系统版本
version: "6.5.23", // 微信版本号
SDKVersion: "1.7.0", // 小程序基础库版本
language: "zh_CN", // 微信设置的语言
pixelRatio: 2, // 设备像素比
screenWidth: 667, // 屏幕宽度
screenHeight: 375, // 屏幕高度
windowWidth: 667, // 可使用窗口宽度
windowHeight: 375, // 可使用窗口高度
fontSizeSetting: 16 // 用户字体大小设置
}
if (wx.openBluetoothAdapter) {
wx.openBluetoothAdapter()
} else {
// 如果希望用户在最新版本的客户端上体验您的小程序,可以这样子提示
wx.showModal({
title: '提示',
content: '当前微信版本过低,无法使用该功能,请升级到最新微信版本后重试。'
})
}
小程序还提供了wx.canIUse这个API,用于判断接口或者组件在当前宿主环境是否可用,其参数格式为: ${API}.${method}.${param}.${options}或者${component}.${attribute}.${option}
各个段的含义如下:
- ${API} 代表 API 名字
- ${method} 代表调用方式,有效值为return, success, object, callback
- ${param} 代表参数或者返回值
- ${options} 代表参数的可选值
- ${component} 代表组件名字
- ${attribute} 代表组件属性
- ${option} 代表组件属性的可选值
使用示例
// 判断接口及其参数在宿主环境是否可用
wx.canIUse('openBluetoothAdapter')
wx.canIUse('getSystemInfoSync.return.screenWidth')
wx.canIUse('getSystemInfo.success.screenWidth')
wx.canIUse('showToast.object.image')
wx.canIUse('onCompassChange.callback.direction')
wx.canIUse('request.object.method.GET')
// 判断组件及其属性在宿主环境是否可用
wx.canIUse('contact-button')
wx.canIUse('text.selectable')
wx.canIUse('button.open-type.contact')
可以选择合适的判断方法来做小程序的向前兼容,以保证我们的小程序在旧版本的微信客户端也能工作正常。在不得已的情况下(小程序强依赖某个新的API或者组件时),还可以通过在小程序管理后台设置“基础库最低版本设置”来达到不向前兼容的目的。例如你选择设置你的小程序只支持1.5.0版本以上的宿主环境,那么当运行着1.4.0版本宿主环境的微信用户打开你的小程序的时候,微信客户端会显示当前小程序不可用,并且提示用户应该去升级微信客户端。
flex布局
设置容器的属性
display:flex;
flex-direction:row(默认值) | row-reverse | column |column-reverse
flex-wrap:nowrap(默认值) | wrap | wrap-reverse
justify-content:flex-start(默认值) | flex-end | center |space-between | space-around | space-evenly
align-items:stretch(默认值) | center | flex-end | baseline | flex-start
align-content:stretch(默认值) | flex-start | center |flex-end | space-between | space-around | space-evenly
设置项目的属性有
order:0(默认值) | <integer>
flex-shrink:1(默认值) | <number>
flex-grow:0(默认值) | <number>
flex-basis:auto(默认值) | <length>
flex:none | auto | @flex-grow @flex-shrink @flex-basis
align-self:auto(默认值) | flex-start | flex-end |center | baseline| stretch
默认的情况下,水平方向的是主轴(main axis),垂直方向的是交叉轴(cross axis)
交互反馈
view、button出煤反馈,通过hover-class属性改变触摸时的样式
button组件的loading属性,true或者false控制
Toast默认时间是1.5s,希望告诉用户这次操作成功并且不打断用户接下来的操作,Toast不适合用来做错误提示
错误提示需要明确告诉用户原因因此不适合这种一闪而过的Toast弹出式提示,一般需要用户明确知晓操作结果状态的话,会使用模态对话框来提示,同时附带下一步操作的指引
宿主环境提供统一的下拉刷新交互,配置开启当亲页面的下拉刷新,用户往下拉动界面触发下拉刷新操作时,Page构造器的onPullDownRefresh回调会被触发,此时开发者重新拉去新数据进行渲染
多数购物小程序会在首页展示一个商品列表,用户滚动到底部的时候,会加载下一页的商品列表渲染到列表的下方,我们把这个交互操作叫为上拉触底,宿主环境提供了上拉的配置和操作触发的回调
//page.json
{"enblePullDownRefresh":true}
{"onReachBottomDistance": 100 }
如果我们不想使用整体的页面进行滚动,而是页面中某一小块区域需要可滚动,此时就要用到宿主环境所提供的scroll-view 可滚动视图组件,可以通过组件的scroll-x和scroll-y属性决定滚动区域是否可以横向或者纵向滚动,scroll-view组件也提供了丰富的滚动回调触发事件
HTTPS网络通信
wx.request对象参数
- url
- data
- header
- method
- dataType
- success
- fail
- complete
小程序宿主环境要求request发起的网络请求必须是https协议请求,wx.request请求的域名需要在小程序管理平台进行配置,如果小程序正式版使用wx.request请求未配置的域名在控制台会报相应的错误
为了方便开发者进行开发调试,开发者工具、小程序的开发板和小程序的体验版在某些情况下允许wx.request请求任意域名
需要考虑旧版本的小程序,更新接口以后需要做向下兼容
传递参数到服务器的两种方式,数据在url上和数据在data对象中,url的最大长度限制是1024字节,在GET请求的情况下两种方式没有事吗区别,使用get的请求方式需要将参数的值需要做一个urlEncode,所以向服务器发送的数据超过1024字节时,就要采用HTTPPOST的形式,此时传递的数据就必须使用data参数
有时候需要传递一些比较复杂的数据结构到后台的时候,用JSON格式会更加适合,此时我们可以在wx.request的header参数设置content-type头部为application/json小程序发起的请求体内佛那个就是data参数对应的json字符串
收到的回包
success回调被调用,返回的对象属性列表
- data
- statusCode
- header
只要成功收到服务器返回,无论HTTP状态码是多少都会进入success回调,因此开发者自己通过 对回报的返回码进行判断后在执行后续的业务逻辑
success回调的参数data字段类型是根据header['content-type']决定的,默认header['content-type']是'application/json',在触发success回调前,小程序宿主环境会对data字段的值做JSON解析,如果解析成功,那么data字段的值会被设置成解析后的Object对象,其他情况data字段都是String类型,其值为HTTP回包包体。
一般使用技巧
由于网络或服务器问题导致一段时间内么有收到网络回包,我们把等待的最长时间称为请求超时时间,小程序默认超时时间是60秒,在小程序的app.json可以指定超时时间
"networkTimeout":{ "request":3000 }
排查异常的方法
- 检查网络
- 见擦汗小程序是否为开发板或者体验版,因为开发板和体验版的小程序不会校验域名
- 检查对应的HTTPS证书是否有效,有时TLSDE版本必须支持1.2及以上版本,可以在开发者工具的console面板输入showRequestIinfo()查看相关信息
- 域名不要使用IP地址或者Localhost,并且不能带端口,同时域名需要经过ICP备案
- 检查app.json配置的超时时间是否太短,超时太短会导致还没收到回包就触发fail回调
- 检查发出的请求是否302到其他域名接口,这种302的情况会被视为请求别的域名接口导致无法发起请求
微信登陆
获取微信登陆凭证Code
- wx.login()获取到微信登陆凭证Code
- wx.request()把code带到自己的服务器
- 通过code和其他信息换取用户ID
- 绑定微信用户ID和自己业务用户ID
- 生成自己业务登陆凭证SessionId
- 返回业务登陆凭证SessionID
- 下次请求wx.request带上SessionId
使用wx.login拿到的是用户的ID编号,具有时效性的凭证,有效时间仅为五分钟,然后把这个临时身份证返回给小程序方,这个零食的身份证称为微信登陆凭证code,如果5分钟内小程序的后台不拿这个临时身份证来微信后台服务器获取微信用户id的话,那么这个临时身份证就作废需要重新调用wx.login
微信服务器换取微信用户身份id
服务器为了确保拿code过来换取身份信息的人刚好是对应的小程序开发者,到服务器请求要同时带上AppId和AppSecret,这两个信息在小程序管理平台开发设置界面可以看到,AppId是公开信息,泄露Appid不会带来安全风险的,但是AppSecret是开发者的隐私数据不应该泄露,如果泄露需要到小程序管理平台进行重置AppSecret,code在成功换取一次后也会立即失效
https://api.weixin.qq.com/sns/jscode2session?appid=<AppId>&secret=<AppSecret>&js_code=<code>&grant_type=authorization_code
接口返回数据
- openid 微信用的唯一标识
- session_key 回话秘钥
- unionid用户在微信开放平台的唯一标识符,本字段在满足一定条件下才返回
openid就是前文一直提到的微信用户id,可以用这个id来区分不同的微信用户。session_key则是微信服务器给开发者服务器颁发的身份凭证,开发者可以用session_key请求微信服务器其他接口来获取一些其他信息,由此可以看到,session_key不应该泄露或者下发到小程序前端。
我们每次都通过小程序前端wx.login()生成微信登录凭证code去微信服务器请求信息,步骤太多造成整体耗时比较严重,因此对于一个比较可信的服务端,给开发者服务器颁发一个时效性更长的会话密钥就显得很有必要了。session_key也存在过期时间
本地缓存
本地数据缓存是小程序存储在当前设备上硬盘上的数据,本地缓存有非常多的用途,我们可以利用本地数据缓存来存储用户在小程序上产生的操作,在用户关闭小程序重新打开时可以恢复之前的状态。可以缓存一些服务器端非实时的数据提高小程序获取数据的速度,在特定的场景下可以提高页面的渲染速度,减少用户等待的时间。
wx.getStoragte/wx.getStorageSync
//异步读取
wx.getStoratge({
key:'key1',
success:function(res){},
fail:function(res){ console.log(读取失败);},
})
//同步读取
try{
const value = wx.getStoratgeSync('key2')
}catch(e){
console.log(读取失败);
}
//异步写入数据
wx.setStorage({
key:'key1',
data:'',
success:function(){},
fail:function(){}
})
//同步写入数据
wx.setStorage('key2', 'data2')
缓存限制和隔离
小程序宿主环境会管理不同小程序的数据缓存,不同的小程序的本地缓存空间是分开的,每个小程序的缓存空间上限为10MB,如果当前缓存已经达到10M在通过wx.setStorage写入缓存会触发fail回调
小程序的本地缓存不仅仅通过小程序这个维度来隔离空间,考虑到同一个设备可以登录不同微信用户,宿主环境还对不同用户的缓存进行隔离,避免用户间的数据隐私泄露
本地缓存是存放在当前设备被IDE,用户切换设备后无法读取到当前设备数据,因此用户的信息不建议只存在本地缓存,应该把数据放在服务器端进行持久化存储
利用本地缓存提前渲染界面
在onload发送请求之前,先检查是否有缓存列表,如果有的话直接渲染界面,然后等到wx.request的success回调之后在覆盖本地缓存重新渲染新的列表
这种做法可以用用户体验到小程序加载非常快,但是也存在缺陷,如果小程序对渲染的数据实时性要求非常高的话用户会看到一些旧的数据所以对于这种应用场景不适用这种优化手段
缓存用户登录态sessionId
通常用户在没有主动退出登陆前,用户的登陆态会保持一段时间,就无须用户频繁地输入账号密码,如果我们把SessionID记录在javascript中某个内存变量,当用户关闭小程序再进SessionId丢失,此时我们需要利用本地缓存能力来持久化存储SessionId
// 把 SessionId 和过期时间放在内存中的全局对象和本地缓存里边
app.globalData.sessionId =data.sessionId
wx.setStorageSync('SESSIONID',data.sessionId)
// 假设登录态保持1天
var expiredTime = +new Date() +1*24*60*60*1000
app.globalData.expiredTime =expiredTime
wx.setStorageSync('EXPIREDTIME',expiredTime)
设备能力
利用微信扫码能力可以减少用户的输入,也可以做一些餐桌点餐的功能、共享单车二维码开启单车、小商品计价等
wx.scanCode()
获取网络状态
wx.getNetworkType({
success: function(){
// networkType字段的有效值:
// wifi/2g/3g/4g/unknown(Android下不常见的网络类型)/none(无网络)
}
})
数据通信
- 不要过于频繁调用setData,应考虑将多次setData合并成一次setData调用
- 数据通信的性能与数据量正相关,因此如果有一些数据字段不在界面中展示且数据结构比较复杂或包含长字符串,则不应该使用setData来设置这些数据
- 与界面渲染无关的数据最好不要设置在data中吗,可以考虑设置在page对象的其他字段下
Page({
onShow:function(){
//不要频繁调用setData
this.setData({a:1})
this.setData({b:2})
//绝大多数可优化为
this.setData({a:1,b:2})
//不要设置不在界面渲染时使用的数据,并将界面无关的数据放在data外
this.setData({
myData: {
a: '这个字段是在wxml中使用了',
b:'这个字段很长并且在wxml中未使用到!...'
}
})
//可以优化为’
this.setData({
'myData.a':'这个字符串在wxml中用到'
})
this._myData = {
b:'这个字段很长并且在wxml中未使用到!...'
}
}
})
视图层会接手用户事件,如点击事件、触摸事件,视图层会将信息反馈给逻辑层,如果一个事件没有绑定事件回调函数,则这个时间不会被反馈给逻辑层
视图将时间反馈给逻辑层时,同样需要一个通信过程,通信的方向是从视图层到逻辑层,因为这个通信过程是异步的,会产生一定的延迟,延迟时间同样和传输的数据量有关系,数据量小于64kb时在30ms内,降低延迟的方式有如下两种
- 去掉不必要的时间绑定(WXML中的bind和catch)从而减少通信的数据量和次数
- 事件绑定时需要传输target和currentTarget的dataset,因而不要在节点的data前缀属性中放置过大的数据
视图层渲染
视图层再接受到初始数据和更新数据setData时需要对视图进行渲染,在一个页面的生命周期里会收到一份初始数据和多份更新数据,收到初始数据时需要执行初始渲染,每次收到更新数据时需要执行重渲染
初始渲染
初始渲染发生在页面刚刚创建时。初始渲染时,将初始数据套用在对应的WXML片段上生成节点树。节点树也就是在开发者工具WXML面板中看到的页面树结构,它包含页面内所有组件节点的名称、属性值和事件回调函数等信息。最后根据节点树包含的各个节点,在界面上依次创建出各个组件。
在这整个流程中,时间开销大体上与节点树中节点的总量成正比例关系。因而减少WXML中节点的数量可以有效降低初始渲染和重渲染的时间开销,提升渲染性能。
<view data-my-data="{{myData}}"> <!-- 这个 view 和下一行的 view 可以合并 -->
<view class="my-class" data-my-data="{{myData}}" bindtap="onTap">
<text> <!-- 这个 text 通常是没必要的 -->
{{myText}}
</text>
</view>
</view>
<!-- 可以简化为 -->
<view class="my-class" data-my-data="{{myData}}" bindtap="onTap">
{{myText}}
</view>
重渲染
初始渲染完成后,视图层可以多次应用setData的数据,每次setData都会执行重新渲染来更新界面
初始渲染中得到的data和当前界面树会保留下来用于重渲染,每次重渲染时,将data和setData数据套用在WXML片段上,得到一个新节点树,将新节点树域当前节点树进行比较,这样可以得到哪些节点的哪些属性需要更新,哪些节点需要添加或删除,最后将setData数据合并到data中,并用新的节点树替换旧的节点树用于下次渲染
在进行当前节点树和新的节点树的比较时,会着重比较setData数据影响到的节点属性,因此去掉不必要设置的数据,减少setData的数据量也有主意提升比较的过程
原生组件通信
一些原生组件支持context来更新组件,不同于setData,使用context来更新组件不会涉及到重渲染过程,数据通信过程也不同,在setData的数据通信流程中,数据从逻辑层出发经过native层转发,传入视图层,在进行一系列渲染步骤之后传入组件,而使用context时,数据从逻辑层传入到native层后,直接传入组件中,这样可以显著降低传输延迟
进行优化的基本方法。主要的优化策略可以归纳为三点:精简代码,降低WXML结构和JS代码的复杂性;合理使用setData调用,减少setData次数和数据量;必要时使用分包优化。
异常
webView层有如下两种党阀可以捕获JS异常
- try,catch
- window.onerror方案。也可以通过window.addEventListener("error", function(evt){}),这个方法能捕捉到语法错误跟运行时错误,同时还能知道出错的信息,以及出错的文件,行号,列号。
自定义组件
在模板中引用的自定义组件机器对应的节点名需要在json中显式定义,否则会被当做一个无意义的节点,除此之外,节点名也可以被声明为抽象节点。
组件样式
- 组件和引用组建的页面不能使用id选择器、属性选择器和标签名选择器,推荐使用class选择器
- 组件和引用组件的页面中使用后代选择器,在一些极端情况下会有非预期的表现,避免使用
- 子元素选择器只能用于view组件与其子节点之间用于其他组件可能导致非预期的情况
- 除继承样式外,app.wxss中的样式,组件所在页面的样式对自定义组件无效(除非更改组件样式隔离选项)
组件样式隔离
默认情况下自定义组建的样式只受自定义组件wxss的影响
- app.wxss或页面的wxss中使用了标签名选择器(或一些其他特殊选择器)来指定样式,这些会影响到页面和全部组件,通常情况下不推荐这种做法
- 指定特殊的样式隔离选项styleIsolation
Component({
options: {
styleIsolation: 'isolated'
}
})
styleIsolation 选项从基础库版本 2.6.5 开始支持。它支持以下取值:
isolated 表示启用样式隔离,在自定义组件内外,使用 class 指定的样式将不会相互影响(一般情况下的默认值); apply-shared 表示页面 wxss 样式将影响到自定义组件,但自定义组件 wxss 中指定的样式不会影响页面; shared 表示页面 wxss 样式将影响到自定义组件,自定义组件 wxss 中指定的样式也会影响页面和其他设置了 apply-shared 或 shared 的自定义组件。(这个选项在插件中不可用。) 使用后两者时,请务必注意组件间样式的相互影响。
如果这个 Component 构造器用于构造页面 ,则默认值为 shared ,且还有以下几个额外的样式隔离选项可用:
page-isolated 表示在这个页面禁用 app.wxss ,同时,页面的 wxss 不会影响到其他自定义组件; page-apply-shared 表示在这个页面禁用 app.wxss ,同时,页面 wxss 样式不会影响到其他自定义组件,但设为 shared 的自定义组件会影响到页面; page-shared 表示在这个页面禁用 app.wxss ,同时,页面 wxss 样式会影响到其他设为 apply-shared 或 shared 的自定义组件,也会受到设为 shared 的自定义组件的影响。 从小程序基础库版本 2.10.1 开始,也可以在页面或自定义组件的 json 文件中配置 styleIsolation (这样就不需在 js 文件的 options 中再配置)
此外,小程序基础库版本 2.2.3 以上支持 addGlobalClass 选项,即在 Component 的 options 中设置 addGlobalClass: true 。 这个选项等价于设置 styleIsolation: apply-shared ,但设置了 styleIsolation 选项后这个选项会失效。
外部样式
有事组件希望接受外部传入的样式类,此时可以在Component中用externalClasses定义段若干个外部样式类
这个特性可以用于实现类似于view组件的hover-class
使用父页面的样式~
使用父组件样式^
虚拟化组件节点
自定义组件本身的节点是一个普通的节点,使用时可以在这个节点上设置calss style动画flex布局等,就如同普通的view组件节点一样
<my-component style="background-color:red" inner-text="Some text">
你好我是内部插槽
</my-component>
有时候自定义组件并不希望这个节点本身可以设置样式,响应flex布局等,二十希望自定义组件内部的第一层节点能够响应flex布局或者样式由自定义组件本身完全决定
这种情况下使用虚拟化组件节点
options: {
virtualHost: true
},
设置了虚拟化组件以后设置在自定义组件镖标签上的class和style将失效,可以在组件内部第一层节点设置对应的flex等响应布局
如果还想生效外部传入的style和class可以如下设定
- 将style定义成properties属性来获取style上设置的值
- 将class定义成externalClasses外部样式类是的自定义组件wxml可以使用class值
/* 组件 custom-component.js */
Component({
externalClasses: ['my-class']
})
<!-- 组件 custom-component.wxml -->
<custom-component class="my-class">这段文本的颜色由组件外的 class 决定</custom-component>
component构造器
Component构造器可用于定义组件,调用构造器可以指定组件的属性、数据、方法等
Component({
behaviors: [],
properties: {
myProperty: { // 属性名
type: String,
value: ''
},
myProperty2: String // 简化的定义方式
},
data: {}, // 私有数据,可用于模板渲染
lifetimes: {
// 生命周期函数,可以为函数,或一个在methods段中定义的方法名
attached: function () { },
moved: function () { },
detached: function () { },
},
// 生命周期函数,可以为函数,或一个在methods段中定义的方法名
attached: function () { }, // 此处attached的声明会被lifetimes字段中的声明覆盖
ready: function() { },
pageLifetimes: {
// 组件所在页面的生命周期函数
show: function () { },
hide: function () { },
resize: function () { },
},
methods: {
onMyButtonTap: function(){
this.setData({
// 更新属性和数据的方法与更新页面数据的方法类似
})
},
// 内部方法建议以下划线开头
_myPrivateMethod: function(){
// 这里将 data.A[0].B 设为 'myPrivateData'
this.setData({
'A[0].B': 'myPrivateData'
})
},
_propertyChange: function(newVal, oldVal) {
}
}
})
使用Component构造器构造页面
组件的属性可以用于接收页面的参数,如访问页面 /pages/index/index?paramA=123¶mB=xyz ,如果声明有属性 paramA 或 paramB ,则它们会被赋值为 123 或 xyz
页面的生命周期方法(即 on 开头的方法),应写在 methods 定义段中
{
"usingComponents": {}
}
Component({
properties: {
paramA: Number,
paramB: String,
},
methods: {
onLoad: function() {
this.data.paramA // 页面参数 paramA 的值
this.data.paramB // 页面参数 paramB 的值
}
}
})
使用Component构造器来构造页面的好处是可以使用behaviors来提取所有页面中公用的代码段
例如吧所有页面的创建和销毁都要执行的同一段代码,就可以吧这段代码提到behaviour中
//page-common-behaviors
module.exports = Behavior({
attached:function(){
//页面创建时执行
},
detached:function(){
//页面销毁时调用
}
})
//PageA
var pageCommonBehavior = require('./page-common-behavior')
Component({
behavior:[pageCommonBehavior],
data:{},
methods:{}
})
//PageB
var pageCommonBehavior = require('./page-common-behavior')
Component({
behavior:[pageCommonBehavior],
data:{},
methods:{}
})
组件通讯
组件间基本通信有以下几种
- WXML数据绑定:用于父组件向子组件的指定属性设置数据,仅能设置JSON兼容数据
- 事件:用于子组件向父组件传递数据,可以传递任意数据
- 如果如上两种手段不能满足要求,父组件可以通过this.selectComponent方法获取子组件实例对象,这样就可以访问组件的任意数据和方法
监听事件
<!-- 当自定义组件触发“myevent”事件时,调用“onMyEvent”方法 -->
<component-tag-name bindmyevent="onMyEvent" />
<!-- 或者可以写成 -->
<component-tag-name bind:myevent="onMyEvent" />
Page({
onMyEvent: function(e){
e.detail // 自定义组件触发事件时提供的detail对象
}
})
<!-- 在自定义组件中 -->
<button bindtap="onTap">点击这个按钮将触发“myevent”事件</button>
Component({
properties: {},
methods: {
onTap: function(){
var myEventDetail = {} // detail对象,提供给事件监听函数
var myEventOption = {} // 触发事件的选项
this.triggerEvent('myevent', myEventDetail, myEventOption)
}
}
})
触发事件的选项
- bubbles 事件是否冒泡
- composed 事件是否可以穿越组件边界,为false时,事件将只能在引用组件的节点树上触发,不进入其他任何组件内部
- capturePhase事件是否拥有捕获阶段
获取组件实例
可在父组件里调用 this.selectComponent ,获取子组件的实例对象。(插件的自定义组件将返回 null)
调用时需要传入一个匹配选择器 selector,如:this.selectComponent(".my-component")。
若需要自定义 selectComponent 返回的数据,可使用内置 behavior: wx://component-export
从基础库版本 2.2.3 开始提供支持。
使自定义组件中支持 export 定义段,这个定义段可以用于指定组件被 selectComponent 调用时的返回值。
组件生命周期
- 组件实例刚刚被创建好时, created 生命周期被触发。此时,组件数据 this.data 就是在 Component 构造器中定义的数据 data 。 此时还不能调用 setData 。 通常情况下,这个生命周期只应该用于给组件 this 添加一些自定义属性字段。
- 在组件完全初始化完毕、进入页面节点树后, attached 生命周期被触发。此时, this.data 已被初始化为组件的当前值。这个生命周期很有用,绝大多数初始化工作可以在这个时机进行。
- 在组件离开页面节点树后, detached 生命周期被触发。退出一个页面时,如果组件还在页面节点树中,则 detached 会被触发
在 behaviors 中也可以编写生命周期方法,同时不会与其他 behaviors 中的同名生命周期相互覆盖。但要注意,如果一个组件多次直接或间接引用同一个 behavior ,这个 behavior 中的生命周期函数在一个执行时机内只会执行一次。
- create 组件刚刚被实例化
- attached 组件已经在家到页面节点树
- ready 组件在视图布局完成后执行
- move 组件实例被移动到节点树的另一个位置时执行
- detached 组件实例被从页面节点树移除时执行
- error 组件方法抛出错误时执行
还有一些特殊的生命周期,它们并非与组件有很强的关联,但有时组件需要获知,以便组件内部处理。这样的生命周期称为“组件所在页面的生命周期”,在 pageLifetimes 定义段中定义。其中可用的生命周期包括:
- show
- hidden
- resize
Component({
pageLifetimes: {
show: function() {
// 页面被展示
},
hide: function() {
// 页面被隐藏
},
resize: function(size) {
// 页面尺寸变化
}
}
})
behavior
behaviors 是用于组件间代码共享的特性,类似于一些编程语言中的 “mixins” 或 “traits”。
每个 behavior 可以包含一组属性、数据、生命周期函数和方法。组件引用它时,它的属性、数据和方法会被合并到组件中,生命周期函数也会在对应时机被调用。 每个组件可以引用多个 behavior ,behavior 也可以引用其它 behavior
同名字段的覆盖和组合规则
组件和它引用的 behavior 中可以包含同名的字段,对这些字段的处理方法如下:
- 如果有同名的属性 (properties) 或方法 (methods):
- 若组件本身有这个属性或方法,则组件的属性或方法会覆盖 behavior 中的同名属性或方法
- 若组件本身无这个属性或方法,则在组件的 behaviors 字段中定义靠后的 behavior 的属性或方法会覆盖靠前的同名属性或方法;
- 在 2 的基础上,若存在嵌套引用 behavior 的情况,则规则为:父 behavior 覆盖 子 behavior 中的同名属性或方法。
- 如果有同名的数据字段 (data):
- 若同名的数据字段都是对象类型,会进行对象合并;
- 其余情况会进行数据覆盖,覆盖规则为:组件 > 父 behavior > 子 behavior 、 靠后的 behavior > 靠前的 behavior。(优先级高的覆盖优先级低的,最大的为优先级最高)
- 生命周期函数不会相互覆盖,而是在对应触发时机被逐个调用:
- 对于不同的生命周期函数之间,遵循组件生命周期函数的执行顺序;
- 对于同种生命周期函数,遵循如下规则:
- behavior 优先于组件执行;
- 子 behavior 优先于 父 behavior 执行;
- 靠前的 behavior 优先于 靠后的 behavior 执行;
- 如果同一个 behavior 被一个组件多次引用,它定义的生命周期函数只会被执行一次。
内置behaviour
自定义组件可以通过引用内置的behaviour来获得内置组建的一些行为
Component({
behaviors: ['wx://form-field']
})
在上例中, wx://form-field 代表一个内置 behavior ,它使得这个自定义组件有类似于表单控件的行为。
内置 behavior 往往会为组件添加一些属性。在没有特殊说明时,组件可以覆盖这些属性来改变它的 type 或添加 observer
- wx://form-field
- wx://form-field-group
- wx://form-field-button
- wx://component-export 使自定义组件支持 export 定义段。这个定义段可以用于指定组件被 selectComponent 调用时的返回值。
组件间的关系
<custom-ul>
<custom-li> item 1 </custom-li>
<custom-li> item 2 </custom-li>
</custom-ul>
// path/to/custom-ul.js
Component({
relations: {
'./custom-li': {
type: 'child', // 关联的目标节点应为子节点
linked: function(target) {
// 每次有custom-li被插入时执行,target是该节点实例对象,触发在该节点attached生命周期之后
},
linkChanged: function(target) {
// 每次有custom-li被移动后执行,target是该节点实例对象,触发在该节点moved生命周期之后
},
unlinked: function(target) {
// 每次有custom-li被移除时执行,target是该节点实例对象,触发在该节点detached生命周期之后
}
}
},
methods: {
_getAllLi: function(){
// 使用getRelationNodes可以获得nodes数组,包含所有已关联的custom-li,且是有序的
var nodes = this.getRelationNodes('path/to/custom-li')
}
},
ready: function(){
this._getAllLi()
}
// path/to/custom-li.js
Component({
relations: {
'./custom-ul': {
type: 'parent', // 关联的目标节点应为父节点
linked: function(target) {
// 每次被插入到custom-ul时执行,target是custom-ul节点实例对象,触发在attached生命周期之后
},
linkChanged: function(target) {
// 每次被移动后执行,target是custom-ul节点实例对象,触发在moved生命周期之后
},
unlinked: function(target) {
// 每次被移除时执行,target是custom-ul节点实例对象,触发在detached生命周期之后
}
}
}
})
==注意:必须在两个组件定义中都加入relations定义,否则不会生效。==
关联一类组件
<custom-form>
<view>
input
<custom-input></custom-input>
</view>
<custom-submit> submit </custom-submit>
</custom-form>
custom-form 组件想要关联 custom-input 和 custom-submit 两个组件。此时,如果这两个组件都有同一个behavior:
// path/to/custom-form-controls.js
module.exports = Behavior({
// ...
})
// path/to/custom-input.js
var customFormControls = require('./custom-form-controls')
Component({
behaviors: [customFormControls],
relations: {
'./custom-form': {
type: 'ancestor', // 关联的目标节点应为祖先节点
}
}
})
// path/to/custom-submit.js
var customFormControls = require('./custom-form-controls')
Component({
behaviors: [customFormControls],
relations: {
'./custom-form': {
type: 'ancestor', // 关联的目标节点应为祖先节点
}
}
})
则在 relations 关系定义中,可使用这个behavior来代替组件路径作为关联的目标节点:
// path/to/custom-form.js
var customFormControls = require('./custom-form-controls')
Component({
relations: {
'customFormControls': {
type: 'descendant', // 关联的目标节点应为子孙节点
target: customFormControls
}
}
})
| 选项 | 类型 | 是否必填 | 描述 |
|---|---|---|---|
| type | String | 是 | 目标组件的相对关系,可选的值为 parent 、 child 、 ancestor 、 descendant |
| linked | Function | 否 | 关系生命周期函数,当关系被建立在页面节点树中时触发,触发时机在组件attached生命周期之后 |
| linkChanged | Function | 否 | 关系生命周期函数,当关系在页面节点树中发生改变时触发,触发时机在组件moved生命周期之后 |
| unlinked | Function | 否 | 关系生命周期函数,当关系脱离页面节点树时触发,触发时机在组件detached生命周期之后 |
| target | String | 否 | 如果这一项被设置,则它表示关联的目标节点所应具有的behavior,所有拥有这一behavior的组件节点都会被关联 |
数据监听器
数据监听器可以用于监听和响应任何属性和数据字段的变化
Component({
attached: function() {
this.setData({
numberA: 1,
numberB: 2,
})
},
observers: {
'numberA, numberB': function(numberA, numberB) {
// 在 numberA 或者 numberB 被设置时,执行这个函数
this.setData({
sum: numberA + numberB
})
}
}
})
监听字段语法
数据监听器支持监听属性或内部数据的变化,可以同时监听多个。一次 setData 最多触发每个监听器一次。
同时,监听器可以监听子数据字段,如下例所示。
Component({
observers: {
'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]
},
}
})
如果需要监听所有子数据字段的变化,可以使用通配符 ** 。
Component({
observers: {
'some.field.**': function(field) {
// 使用 setData 设置 this.data.some.field 本身或其下任何子数据字段时触发
// (除此以外,使用 setData 设置 this.data.some 也会触发)
field === this.data.some.field
},
},
attached: function() {
// 这样会触发上面的 observer
this.setData({
'some.field': { /* ... */ }
})
// 这样也会触发上面的 observer
this.setData({
'some.field.xxx': { /* ... */ }
})
// 这样还是会触发上面的 observer
this.setData({
'some': { /* ... */ }
})
}
})
特别地,仅使用通配符 ** 可以监听全部 setData 。
Component({
observers: {
'**': function() {
// 每次 setData 都触发
},
},
})
- 数据监听器监听的是 setData 涉及到的数据字段,即使这些数据字段的值没有发生变化,数据监听器依然会被触发。
- 如果在数据监听器函数中使用 setData 设置本身监听的数据字段,可能会导致死循环,需要特别留意。
- 数据监听器和属性的 observer 相比,数据监听器更强大且通常具有更好的性能。
纯数据字段
纯数据字段是一些不用于界面渲染的 data 字段,可以用于提升页面更新性能
组件数据中的纯数据字段
有些情况下,某些 data 中的字段(包括 setData 设置的字段)既不会展示在界面上,也不会传递给其他组件,仅仅在当前组件内部使用。
此时,可以指定这样的数据字段为“纯数据字段”,它们将仅仅被记录在 this.data 中,而不参与任何界面渲染过程,这样有助于提升页面更新性能。
指定“纯数据字段”的方法是在 Component 构造器的 options 定义段中指定 pureDataPattern 为一个正则表达式,字段名符合这个正则表达式的字段将成为纯数据字段。
Component({
options: {
pureDataPattern: /^_/ // 指定所有 _ 开头的数据字段为纯数据字段
},
data: {
a: true, // 普通数据字段
_b: true, // 纯数据字段
},
methods: {
myMethod() {
this.data._b // 纯数据字段可以在 this.data 中获取
this.setData({
c: true, // 普通数据字段
_d: true, // 纯数据字段
})
}
}
})
<view wx:if="{{a}}"> 这行会被展示 </view>
<view wx:if="{{_b}}"> 这行不会被展示 </view>
组件属性中的纯数据字段
属性也可以被指定为纯数据字段(遵循 pureDataPattern 的正则表达式)。
性中的纯数据字段可以像普通属性一样接收外部传入的属性值,但不能将它直接用于组件自身的 WXML 中
Component({
options: {
pureDataPattern: /^_/
},
properties: {
a: Boolean,
_b: {
type: Boolean,
observer() {
// 不要这样做!这个 observer 永远不会被触发
}
},
}
})
==注意:属性中的纯数据字段的属性 observer 永远不会触发!如果想要监听属性值变化,使用 数据监听器 代替。==
从小程序基础库版本 2.10.1 开始,也可以在页面或自定义组件的 json 文件中配置 pureDataPattern (这样就不需在 js 文件的 options 中再配置)。此时,其值应当写成字符串形式:
{
"pureDataPattern": "^_"
}
使用数据监听器监听纯数据字段
数据监听器 可以用于监听纯数据字段(与普通数据字段一样)。这样,可以通过监听、响应纯数据字段的变化来改变界面。
Component({
options: {
pureDataPattern: /^timestamp$/ // 将 timestamp 属性指定为纯数据字段
},
properties: {
timestamp: Number,
},
observers: {
timestamp: function () {
// timestamp 被设置时,将它展示为可读时间字符串
var timeString = new Date(this.data.timestamp).toLocaleString()
this.setData({
timeString: timeString
})
}
}
})
<view>{{timeString}}</view>
抽象节点
有时,自定义组件模板中的一些节点,其对应的自定义组件不是由自定义组件本身确定的,而是自定义组件的调用者确定的。这时可以把这个节点声明为“抽象节点”。
<!-- selectable-group.wxml -->
<view wx:for="{{labels}}">
<label>
<selectable disabled="{{false}}"></selectable>
{{item}}
</label>
</view>
其中,“selectable”不是任何在 json 文件的 usingComponents 字段中声明的组件,而是一个抽象节点。它需要在 componentGenerics 字段中声明:
{
"componentGenerics": {
"selectable": true
}
}
使用包含抽象节点的组件
<selectable-group generic:selectable="custom-radio" />
<selectable-group generic:selectable="custom-checkbox" />
抽象节点的默认组件
抽象节点可以指定一个默认组件,当具体组件未被指定时,将创建默认组件的实例。默认组件可以在 componentGenerics 字段中指定
{
"componentGenerics": {
"selectable": {
"default": "path/to/default/component"
}
}
}
节点的 generic 引用 generic:xxx="yyy" 中,值 yyy 只能是静态值,不能包含数据绑定。因而抽象节点特性并不适用于动态决定节点名的场景。
自定义组件扩展
// behavior.js
module.exports = Behavior({
definitionFilter(defFields) {
defFields.data.from = 'behavior'
},
})
// component.js
Component({
data: {
from: 'component'
},
behaviors: [require('behavior.js')],
ready() {
console.log(this.data.from) // 此处会发现输出 behavior 而不是 component
}
})
Behavior() 构造器提供了新的定义段 definitionFilter ,用于支持自定义组件扩展。 definitionFilter 是一个函数,在被调用时会注入两个参数,第一个参数是使用该 behavior 的 component/behavior 的定义对象,第二个参数是该 behavior 所使用的 behavior 的 definitionFilter 函数列表。
// behavior3.js
module.exports = Behavior({
definitionFilter(defFields, definitionFilterArr) {},
})
// behavior2.js
module.exports = Behavior({
behaviors: [require('behavior3.js')],
definitionFilter(defFields, definitionFilterArr) {
// definitionFilterArr[0](defFields)
},
})
// behavior1.js
module.exports = Behavior({
behaviors: [require('behavior2.js')],
definitionFilter(defFields, definitionFilterArr) {},
})
// component.js
Component({
behaviors: [require('behavior1.js')],
})
- 当进行 behavior2 的声明时就会调用 behavior3 的 definitionFilter 函数,其中 defFields 参数是 behavior2 的定义段, definitionFilterArr 参数即为空数组,因为 behavior3 没有使用其他的 behavior 。
- 当进行 behavior1 的声明时就会调用 behavior2 的 definitionFilter 函数,其中 defFields 参数是 behavior1 的定义段, definitionFilterArr 参数是一个长度为1的数组,definitionFilterArr[0] 即为 behavior3 的 definitionFilter 函数,因为 behavior2 使用了 behavior3。用户在此处可以自行决定在进行 behavior1 的声明时要不要调用 behavior3 的 definitionFilter 函数,如果需要调用,在此处补充代码 definitionFilterArr0 即可,definitionFilterArr 参数会由基础库补充传入。
- 同理,在进行 component 的声明时就会调用 behavior1 的 definitionFilter 函数。