当前位置:   article > 正文

微信小程序自定义组件/插件等解析

微信插件 (using by "plugin-privat

自定义组件

从小程序基础库版本 1.6.3 开始,小程序支持简洁的组件化编程。所有自定义组件相关特性都需要基础库版本 1.6.3 或更高。

开发者可以将页面内的功能模块抽象成自定义组件,以便在不同的页面中重复使用;也可以将复杂的页面拆分成多个低耦合的模块,有助于代码维护。自定义组件在使用时与基础组件非常相似

创建自定义组件

类似于页面,一个自定义组件由 json/wxml/wxss/js 4个文件组成。要编写一个自定义组件,首先需要在 json 文件中进行自定义组件声明(将component字段设为true可这一组文件设为自定义组件):

  1. {
  2. "component": true
  3. }

同时,还要在 wxml 文件中编写组件模版,在 wxss 文件中加入组件样式,它们的写法与页面的写法类似

  1. <!-- 这是自定义组件的内部WXML结构 -->
  2. <view class="inner">
  3. {{innerText}}
  4. </view>
  5. <slot></slot>
  6. /* 这里的样式只应用于这个自定义组件 */
  7. .inner {
  8. color: red;
  9. }

注意:在组件wxss中不应使用ID选择器、属性选择器和标签名选择器。

在自定义组件的 js 文件中,需要使用 Component() 来注册组件,并提供组件的属性定义、内部数据和自定义方法。

组件的属性值和内部数据将被用于组件 wxml 的渲染,其中,属性值是可由组件外部传入的

  1. Component({
  2. properties: {
  3. // 这里定义了innerText属性,属性值可以在组件使用时指定
  4. innerText: {
  5. type: String,
  6. value: 'default value',
  7. }
  8. },
  9. data: {
  10. // 这里是一些组件内部数据
  11. someData: {}
  12. },
  13. methods: {
  14. // 这里是一个自定义方法
  15. customMethod: function(){}
  16. }
  17. })
使用自定义组件

使用已注册的自定义组件前,首先要在页面的 json 文件中进行引用声明。此时需要提供每个自定义组件的标签名和对应的自定义组件文件路径:

  1. {
  2. "usingComponents": {
  3. "component-tag-name": "path/to/the/custom/component"
  4. }
  5. }

这样,在页面的 wxml 中就可以像使用基础组件一样使用自定义组件。节点名即自定义组件的标签名,节点属性即传递给组件的属性值

  1. <view>
  2. <!-- 以下是对一个自定义组件的引用 -->
  3. <component-tag-name inner-text="Some text"></component-tag-name>
  4. </view>

自定义组件的 wxml 节点结构在与数据结合之后,将被插入到引用位置内。

Tips:

1.对于基础库的1.5.x版本, 1.5.7 也有部分自定义组件支持。

2.因为WXML节点标签名只能是小写字母、中划线和下划线的组合,所以自定义组件的标签名也只能包含这些字符。

3.自定义组件也是可以引用自定义组件的,引用方法类似于页面引用自定义组件的方式(使用 usingComponents 字段)。

4.自定义组件和使用自定义组件的页面所在项目根目录名不能以“wx-”为前缀,否则会报错。

5.旧版本的基础库不支持自定义组件,此时,引用自定义组件的节点会变为默认的空节点。

组件模版和样式

类似于页面,自定义组件拥有自己的 wxml 模版和 wxss 样式

组件模版

组件模版的写法与页面模板相同。组件模版与组件数据结合后生成的节点树,将被插入到组件的引用位置上。

在组件模板中可以提供一个<slot>节点,用于承载组件引用时提供的子节点

  1. <!-- 组件模板 -->
  2. <view class="wrapper">
  3. <view>这里是组件的内部节点</view>
  4. <slot></slot>
  5. </view>
  6. <!-- 引用组件的页面模版 -->
  7. <view>
  8. <component-tag-name>
  9. <!-- 这部分内容将被放置在组件 <slot> 的位置上 -->
  10. <view>这里是插入到组件slot中的内容</view>
  11. </component-tag-name>
  12. </view>

注意,在模版中引用到的自定义组件及其对应的节点名需要在 json 文件中显式定义,否则会被当作一个无意义的节点。除此以外,节点名也可以被声明为抽象节点

模版数据绑定

与普通的 WXML 模版类似,可以使用数据绑定,这样就可以向子组件的属性传递动态数据

  1. <!-- 引用组件的页面模版 -->
  2. <view>
  3. <component-tag-name prop-a="{{dataFieldA}}" prop-b="{{dataFieldB}}">
  4. <!-- 这部分内容将被放置在组件 <slot> 的位置上 -->
  5. <view>这里是插入到组件slot中的内容</view>
  6. </component-tag-name>
  7. </view>

在以上例子中,组件的属性 propA 和 propB 将收到页面传递的数据。页面可以通过 setData 来改变绑定的数据字段。

注意:这样的数据绑定只能传递 JSON 兼容数据。自基础库版本 2.0.9 开始,还可以在数据中包含函数(但这些函数不能在 WXML 中直接调用,只能传递给子组件)

组件wxml的slot

在组件的wxml中可以包含 slot 节点,用于承载组件使用者提供的wxml结构。

默认情况下,一个组件的wxml中只能有一个slot。需要使用多slot时,可以在组件js中声明启用

  1. Component({
  2. options: {
  3. multipleSlots: true // 在组件定义时的选项中启用多slot支持
  4. },
  5. properties: { /* ... */ },
  6. methods: { /* ... */ }
  7. })

此时,可以在这个组件的wxml中使用多个slot,以不同的 name 来区分

  1. <!-- 组件模板 -->
  2. <view class="wrapper">
  3. <slot name="before"></slot>
  4. <view>这里是组件的内部细节</view>
  5. <slot name="after"></slot>
  6. </view>

使用时,用 slot 属性来将节点插入到不同的slot上

  1. <!-- 引用组件的页面模版 -->
  2. <view>
  3. <component-tag-name>
  4. <!-- 这部分内容将被放置在组件 <slot name="before"> 的位置上 -->
  5. <view slot="before">这里是插入到组件slot name="before"中的内容</view>
  6. <!-- 这部分内容将被放置在组件 <slot name="after"> 的位置上 -->
  7. <view slot="after">这里是插入到组件slot name="after"中的内容</view>
  8. </component-tag-name>
  9. </view>
组件样式

组件对应 wxss 文件的样式,只对组件wxml内的节点生效。编写组件样式时,需要注意以下几点:

1.组件和引用组件的页面不能使用id选择器(#a)、属性选择器([a])和标签名选择器,请改用class选择器。

2.组件和引用组件的页面中使用后代选择器(.a .b)在一些极端情况下会有非预期的表现,如遇,请避免使用。

3.子元素选择器(.a>.b)只能用于 view 组件与其子节点之间,用于其他组件可能导致非预期的情况。

4.继承样式,如 font 、 color ,会从组件外继承到组件内。

5.除继承样式外, app.wxss 中的样式、组件所在页面的的样式对自定义组件无效。

  1. #a { } /* 在组件中不能使用 */
  2. [a] { } /* 在组件中不能使用 */
  3. button { } /* 在组件中不能使用 */
  4. .a > .b { } /* 除非 .a 是 view 组件节点,否则不一定会生效 */

除此以外,组件可以指定它所在节点的默认样式,使用 :host 选择器(需要包含基础库 1.7.2 或更高版本的开发者工具支持)

  1. /* 组件 custom-component.wxss */
  2. :host {
  3. color: yellow;
  4. }
  5. <!-- 页面的 WXML -->
  6. <custom-component>这段文本是黄色的</custom-component>
外部样式类

有时,组件希望接受外部传入的样式类(类似于 view 组件的 hover-class 属性)。此时可以在 Component 中用 externalClasses 定义段定义若干个外部样式类。这个特性从小程序基础库版本 1.9.90 开始支持。

注意:在同一个节点上使用普通样式类和外部样式类时,两个类的优先级是未定义的,因此最好避免这种情况

  1. /* 组件 custom-component.js */
  2. Component({
  3. externalClasses: ['my-class']
  4. })
  5. <!-- 组件 custom-component.wxml -->
  6. <custom-component class="my-class">这段文本的颜色由组件外的 class 决定</custom-component>

这样,组件的使用者可以指定这个样式类对应的 class ,就像使用普通属性一样

  1. <!-- 页面的 WXML -->
  2. <custom-component my-class="red-text" />
  3. .red-text {
  4. color: red;
  5. }
Component构造器
定义段与示例方法

Component构造器可用于定义组件,调用Component构造器时可以指定组件的属性、数据、方法等

定义段类型是否必填描述
propertiesObject Map组件的对外属性,是属性名到属性设置的映射表,属性设置中可包含三个字段:type-属性类型,value-属性初始值,observer-属性值被更改时的响应函数
dataObject组件的内部数据,和 properties 一同用于组件的模版渲染
methodsObject Map组件的方法,包括事件响应函数和任意的自定义方法,关于事件响应函数的使用
behaviorsString Array类似于mixins和traits的组件间代码复用机制
createdFunction组件生命周期函数,在组件实例进入页面节点树时执行,注意此时不能调用 setData
attachedFunction组件生命周期函数,在组件实例进入页面节点树时执行
readyFunction组件生命周期函数,在组件布局完成后执行,此时可以获取节点信息(使用 SelectorQuery )
movedFunction组件生命周期函数,在组件实例被移动到节点树另一个位置时执行
detachedFunction组件生命周期函数,在组件实例被从页面节点树移除时执行
relationObject组件间关系定义
externalClassesString Array组件接受的外部样式类
optionsObject Map一些组件选项

生成的组件实例可以在组件的方法、生命周期函数和属性 observer 中通过 this 访问。组件包含一些通用属性和方法

属性名类型描述
isString组件的文件路径
idString节点id
datasetString节点dataset
dataObject组件数据,包括内部数据和属性值
propertiesObject组件数据,包括内部数据和属性值(与 data 一致)
方法名参数描述
setDataObject newData设置data并执行视图层渲染
hasBehaviorObject behavior检查组件是否具有 behavior (检查时会递归检查被直接或间接引入的所有behavior)
triggerEventString name, Object detail, Object options触发事件
createSelectorQuery 创建一个 SelectorQuery 对象,选择器选取范围为这个组件实例内
selectComponentString selector使用选择器选择组件实例节点,返回匹配到的第一个组件实例对象
selectAllComponentsString selector使用选择器选择组件实例节点,返回匹配到的全部组件实例对象组成的数组
getRelationNodesString selector获取所有这个关系对应的所有关联节点
  1. Component({
  2. behaviors: [],
  3. properties: {
  4. myProperty: { // 属性名
  5. type: String, // 类型(必填),目前接受的类型包括:String, Number, Boolean, Object, Array, null(表示任意类型)
  6. value: '', // 属性初始值(可选),如果未指定则会根据类型选择一个
  7. observer: function(newVal, oldVal, changedPath) {
  8. // 属性被改变时执行的函数(可选),也可以写成在methods段中定义的方法名字符串, 如:'_propertyChange'
  9. // 通常 newVal 就是新设置的数据, oldVal 是旧数据
  10. }
  11. },
  12. myProperty2: String // 简化的定义方式
  13. },
  14. data: {}, // 私有数据,可用于模版渲染
  15. // 生命周期函数,可以为函数,或一个在methods段中定义的方法名
  16. attached: function(){},
  17. moved: function(){},
  18. detached: function(){},
  19. methods: {
  20. onMyButtonTap: function(){
  21. this.setData({
  22. // 更新属性和数据的方法与更新页面数据的方法类似
  23. })
  24. },
  25. // 内部方法建议以下划线开头
  26. _myPrivateMethod: function(){
  27. // 这里将 data.A[0].B 设为 'myPrivateData'
  28. this.setData({
  29. 'A[0].B': 'myPrivateData'
  30. })
  31. },
  32. _propertyChange: function(newVal, oldVal) {}
  33. }
  34. })

注意:在 properties 定义段中,属性名采用驼峰写法(propertyName);在 wxml 中,指定属性值时则对应使用连字符写法(component-tag-name property-name="attr value"),应用于数据绑定时采用驼峰写法(attr="{{propertyName}}")

使用 Component 构造器构造页面

事实上,小程序的页面也可以视为自定义组件。因而,页面也可以使用 Component 构造器构造,拥有与普通组件一样的定义段与实例方法。但此时要求对应 json 文件中包含 usingComponents 定义段。

此时,组件的属性可以用于接收页面的参数,如访问页面 /pages/index/index?paramA=123¶mB=xyz ,如果声明有属性 paramA 或 paramB ,则它们会被赋值为 123 或 xyz

  1. {
  2. "usingComponents": {}
  3. }
  4. Component({
  5. properties: {
  6. paramA: Number,
  7. paramB: String,
  8. },
  9. methods: {
  10. onLoad: function() {
  11. this.data.paramA // 页面参数 paramA 的值
  12. this.data.paramB // 页面参数 paramB 的值
  13. }
  14. }
  15. })

Bug & Tips:

1.使用 this.data 可以获取内部数据和属性值,但不要直接修改它们,应使用 setData 修改。

2.生命周期函数无法在组件方法中通过 this 访问到。

3.属性名应避免以 data 开头,即不要命名成 dataXyz 这样的形式,因为在 WXML 中, data-xyz="" 会被作为节点 dataset 来处理,而不是组件属性。

4.在一个组件的定义和使用时,组件的属性名和data字段相互间都不能冲突(尽管它们位于不同的定义段中)。

5.bug : 对于 type 为 Object 或 Array 的属性,如果通过该组件自身的 this.setData 来改变属性值的一个子字段,则依旧会触发属性 observer ,且 observer 接收到的 newVal 是变化的那个子字段的值, oldVal 为空, changedPath 包含子字段的字段名相关信息

组件间通信与事件
组件间通信

组件间的基本通信方式有以下几种。

1.WXML 数据绑定:用于父组件向子组件的指定属性设置数据,仅能设置 JSON 兼容数据(自基础库版本 2.0.9 开始,还可以在数据中包含函数)

2.事件:用于子组件向父组件传递数据,可以传递任意数据。

3.如果以上两种方式不足以满足需要,父组件还可以通过 this.selectComponent 方法获取子组件实例对象,这样就可以直接访问组件的任意数据和方法。

监听事件

事件系统是组件间通信的主要方式之一。自定义组件可以触发任意的事件,引用组件的页面可以监听这些事件

监听自定义组件事件的方法与监听基础组件事件的方法完全一致:

  1. <!-- 当自定义组件触发“myevent”事件时,调用“onMyEvent”方法 -->
  2. <component-tag-name bindmyevent="onMyEvent" />
  3. <!-- 或者可以写成 -->
  4. <component-tag-name bind:myevent="onMyEvent" />
  5. Page({
  6. onMyEvent: function(e){
  7. e.detail // 自定义组件触发事件时提供的detail对象
  8. }
  9. })
触发事件

自定义组件触发事件时,需要使用 triggerEvent 方法,指定事件名、detail对象和事件选项:

  1. <!-- 在自定义组件中 -->
  2. <button bindtap="onTap">点击这个按钮将触发“myevent”事件</button>
  3. Component({
  4. properties: {}
  5. methods: {
  6. onTap: function(){
  7. var myEventDetail = {} // detail对象,提供给事件监听函数
  8. var myEventOption = {} // 触发事件的选项
  9. this.triggerEvent('myevent', myEventDetail, myEventOption)
  10. }
  11. }
  12. })

触发事件的选项包括:

选项名类型是否必填默认值描述
bubblesBooleanfalse事件是否冒泡
composedBooleanfalse事件是否可以穿越组件边界,为false时,事件将只能在引用组件的节点树上触发,不进入其他任何组件内部
capturePhaseBooleanfalse事件是否拥有捕获阶段
  1. // 页面 page.wxml
  2. <another-component bindcustomevent="pageEventListener1">
  3. <my-component bindcustomevent="pageEventListener2"></my-component>
  4. </another-component>
  5. // 组件 another-component.wxml
  6. <view bindcustomevent="anotherEventListener">
  7. <slot />
  8. </view>
  9. // 组件 my-component.wxml
  10. <view bindcustomevent="myEventListener">
  11. <slot />
  12. </view>
  13. // 组件 my-component.js
  14. Component({
  15. methods: {
  16. onTap: function(){
  17. this.triggerEvent('customevent', {}) // 只会触发 pageEventListener2
  18. this.triggerEvent('customevent', {}, { bubbles: true }) // 会依次触发 pageEventListener2 、 pageEventListener1
  19. this.triggerEvent('customevent', {}, { bubbles: true, composed: true }) // 会依次触发 pageEventListener2 、 anotherEventListener 、 pageEventListener1
  20. }
  21. }
  22. })
behaviors
定义和使用 behaviors

behaviors 是用于组件间代码共享的特性,类似于一些编程语言中的“mixins”或“traits”。

每个 behavior 可以包含一组属性、数据、生命周期函数和方法,组件引用它时,它的属性、数据和方法会被合并到组件中,生命周期函数也会在对应时机被调用。每个组件可以引用多个 behavior 。 behavior 也可以引用其他 behavior 。

behavior 需要使用 Behavior() 构造器定义

  1. // my-behavior.js
  2. module.exports = Behavior({
  3. behaviors: [],
  4. properties: {
  5. myBehaviorProperty: {
  6. type: String
  7. }
  8. },
  9. data: {
  10. myBehaviorData: {}
  11. },
  12. attached: function(){},
  13. methods: {
  14. myBehaviorMethod: function(){}
  15. }
  16. })

组件引用时,在 behaviors 定义段中将它们逐个列出即可

  1. // my-component.js
  2. var myBehavior = require('my-behavior')
  3. Component({
  4. behaviors: [myBehavior],
  5. properties: {
  6. myProperty: {
  7. type: String
  8. }
  9. },
  10. data: {
  11. myData: {}
  12. },
  13. attached: function(){},
  14. methods: {
  15. myMethod: function(){}
  16. }
  17. })

在上例中, my-component 组件定义中加入了 my-behavior ,而 my-behavior 中包含有 myBehaviorProperty 属性、 myBehaviorData 数据字段、 myBehaviorMethod 方法和一个 attached 生命周期函数。这将使得 my-component 中最终包含 myBehaviorProperty 、 myProperty 两个属性, myBehaviorData 、 myData 两个数据字段,和 myBehaviorMethod 、 myMethod 两个方法。当组件触发 attached 生命周期时,会依次触发 my-behavior 中的 attached 生命周期函数和 my-component 中的 attached 生命周期函数

字段的覆盖和组合规则

组件和它引用的 behavior 中可以包含同名的字段,对这些字段的处理方法如下:

1.如果有同名的属性或方法,组件本身的属性或方法会覆盖 behavior 中的属性或方法,如果引用了多个 behavior ,在定义段中靠后 behavior 中的属性或方法会覆盖靠前的属性或方法;

2.如果有同名的数据字段,如果数据是对象类型,会进行对象合并,如果是非对象类型则会进行相互覆盖;

3.生命周期函数不会相互覆盖,而是在对应触发时机被逐个调用。如果同一个 behavior 被一个组件多次引用,它定义的生命周期函数只会被执行一次。

内置 behaviors

自定义组件可以通过引用内置的 behavior 来获得内置组件的一些行为

  1. Component({
  2. behaviors: ['wx://form-field']
  3. })

在上例中, wx://form-field 代表一个内置 behavior ,它使得这个自定义组件有类似于表单控件的行为。

内置 behavior 往往会为组件添加一些属性。在没有特殊说明时,组件可以覆盖这些属性来改变它的 type 或添加 observer 。

wx://form-field

使自定义组件有类似于表单控件的行为。 form 组件可以识别这些自定义组件,并在 submit 事件中返回组件的字段名及其对应字段值。这将为它添加以下两个属性

属性名类型描述最低版本
nameString在表单中的字段名1.6.7
value任意在表单中的字段值1.6.7
组件间关系
定义和使用组件间关系

有时需要实现这样的组件:

  1. <custom-ul>
  2. <custom-li> item 1 </custom-li>
  3. <custom-li> item 2 </custom-li>
  4. </custom-ul>

这个例子中, custom-ul 和 custom-li 都是自定义组件,它们有相互间的关系,相互间的通信往往比较复杂。此时在组件定义时加入 relations 定义段,可以解决这样的问题。示例:

  1. // path/to/custom-ul.js
  2. Component({
  3. relations: {
  4. './custom-li': {
  5. type: 'child', // 关联的目标节点应为子节点
  6. linked: function(target) {
  7. // 每次有custom-li被插入时执行,target是该节点实例对象,触发在该节点attached生命周期之后
  8. },
  9. linkChanged: function(target) {
  10. // 每次有custom-li被移动后执行,target是该节点实例对象,触发在该节点moved生命周期之后
  11. },
  12. unlinked: function(target) {
  13. // 每次有custom-li被移除时执行,target是该节点实例对象,触发在该节点detached生命周期之后
  14. }
  15. }
  16. },
  17. methods: {
  18. _getAllLi: function(){
  19. // 使用getRelationNodes可以获得nodes数组,包含所有已关联的custom-li,且是有序的
  20. var nodes = this.getRelationNodes('path/to/custom-li')
  21. }
  22. },
  23. ready: function(){
  24. this._getAllLi()
  25. }
  26. })
  27. // path/to/custom-li.js
  28. Component({
  29. relations: {
  30. './custom-ul': {
  31. type: 'parent', // 关联的目标节点应为父节点
  32. linked: function(target) {
  33. // 每次被插入到custom-ul时执行,target是custom-ul节点实例对象,触发在attached生命周期之后
  34. },
  35. linkChanged: function(target) {
  36. // 每次被移动后执行,target是custom-ul节点实例对象,触发在moved生命周期之后
  37. },
  38. unlinked: function(target) {
  39. // 每次被移除时执行,target是custom-ul节点实例对象,触发在detached生命周期之后
  40. }
  41. }
  42. }
  43. })

注意:必须在两个组件定义中都加入relations定义,否则不会生效

关联一类组件
  1. <custom-form>
  2. <view>
  3. input
  4. <custom-input></custom-input>
  5. </view>
  6. <custom-submit> submit </custom-submit>
  7. </custom-form>

custom-form 组件想要关联 custom-input 和 custom-submit 两个组件。此时,如果这两个组件都有同一个behavior:

  1. // path/to/custom-form-controls.js
  2. module.exports = Behavior({
  3. // ...
  4. })
  5. // path/to/custom-input.js
  6. var customFormControls = require('./custom-form-controls')
  7. Component({
  8. behaviors: [customFormControls],
  9. relations: {
  10. './custom-form': {
  11. type: 'ancestor', // 关联的目标节点应为祖先节点
  12. }
  13. }
  14. })
  15. // path/to/custom-submit.js
  16. var customFormControls = require('./custom-form-controls')
  17. Component({
  18. behaviors: [customFormControls],
  19. relations: {
  20. './custom-form': {
  21. type: 'ancestor', // 关联的目标节点应为祖先节点
  22. }
  23. }
  24. })

则在 relations 关系定义中,可使用这个behavior来代替组件路径作为关联的目标节点:

  1. // path/to/custom-form.js
  2. var customFormControls = require('./custom-form-controls')
  3. Component({
  4. relations: {
  5. 'customFormControls': {
  6. type: 'descendant', // 关联的目标节点应为子孙节点
  7. target: customFormControls
  8. }
  9. }
  10. })
relations 定义段

relations 定义段包含目标组件路径及其对应选项,可包含的选项见下表

选项类型是否必填描述
typeString目标组件的相对关系,可选的值为 parent 、 child 、 ancestor 、 descendant
linkedFunction关系生命周期函数,当关系被建立在页面节点树中时触发,触发时机在组件attached生命周期之后
linkChangedFunction关系生命周期函数,当关系在页面节点树中发生改变时触发,触发时机在组件moved生命周期之后
unlinkedFunction关系生命周期函数,当关系脱离页面节点树时触发,触发时机在组件detached生命周期之后
targetString如果这一项被设置,则它表示关联的目标节点所应具有的behavior,所有拥有这一behavior的组件节点都会被关联
抽象节点
在组件中使用抽象节点

有时,自定义组件模版中的一些节点,其对应的自定义组件不是由自定义组件本身确定的,而是自定义组件的调用者确定的。这时可以把这个节点声明为“抽象节点”。

例如,我们现在来实现一个“选框组”(selectable-group)组件,它其中可以放置单选框(custom-radio)或者复选框(custom-checkbox)。这个组件的 wxml 可以这样编写:

  1. <!-- selectable-group.wxml -->
  2. <view wx:for="{{labels}}">
  3. <label>
  4. <selectable disabled="{{false}}"></selectable>
  5. {{item}}
  6. </label>
  7. </view>

其中,“selectable”不是任何在 json 文件的 usingComponents 字段中声明的组件,而是一个抽象节点。它需要在 componentGenerics 字段中声明:

  1. {
  2. "componentGenerics": {
  3. "selectable": true
  4. }
  5. }
使用包含抽象节点的组件

在使用 selectable-group 组件时,必须指定“selectable”具体是哪个组件:

<selectable-group generic:selectable="custom-radio" />

这样,在生成这个 selectable-group 组件的实例时,“selectable”节点会生成“custom-radio”组件实例。类似地,如果这样使用:

<selectable-group generic:selectable="custom-checkbox" />

“selectable”节点则会生成“custom-checkbox”组件实例。

注意:上述的 custom-radio 和 custom-checkbox 需要包含在这个 wxml 对应 json 文件的 usingComponents 定义段中。

  1. {
  2. "usingComponents": {
  3. "custom-radio": "path/to/custom/radio",
  4. "custom-checkbox": "path/to/custom/checkbox"
  5. }
  6. }
抽象节点的默认组件

抽象节点可以指定一个默认组件,当具体组件未被指定时,将创建默认组件的实例。默认组件可以在 componentGenerics 字段中指定:

  1. {
  2. "componentGenerics": {
  3. "selectable": {
  4. "default": "path/to/default/component"
  5. }
  6. }
  7. }

Tips:节点的 generic 引用 generic:xxx="yyy" 中,值 yyy 只能是静态值,不能包含数据绑定。因而抽象节点特性并不适用于动态决定节点名的场景

插件

插件的开发和使用自小程序基础库版本 1.9.6 开始支持。

插件是对一组 js 接口、自定义组件或页面的封装,用于提供给第三方小程序调用。插件必须嵌入在其他小程序中才能被用户使用。

插件开发者可以像开发小程序一样编写一个插件并上传代码,在插件发布之后,其他小程序方可调用。小程序平台会托管插件代码,其他小程序调用时,上传的插件代码会随小程序一起下载运行。

相对于普通 js 文件或自定义组件,插件拥有更强的独立性,拥有独立的 API 接口、域名列表等,但同时会受到一些限制,如一些 API 无法调用或功能受限。对于一些特殊的接口,如 wx.login 和 wx.requestPayment ,虽然插件不能直接调用,但可以使用 插件功能页 来间接实现

开发插件

创建插件项目

插件类型的项目可以在开发者工具中直接创建1
新建插件类型的项目后,如果创建示例项目,则项目中将包含两个目录:

1.plugin 目录:插件代码目录

2.miniprogram 目录:放置一个小程序,用于调试插件

3.此外,还可以加入一个 doc 目录,用于放置插件开发文档

miniprogram 目录内容可以当成普通小程序来编写,用于插件调试、预览和审核。下面的内容主要介绍 plugin 的编写方法

插件目录结构

一个插件可以包含若干个自定义组件、页面,和一组js接口。插件的目录内容如下

plugin
├── components
│ ├── hello-component.js // 插件提供的自定义组件(可以有多个)
│ ├── hello-component.json
│ ├── hello-component.wxml
│ └── hello-component.wxss
├── pages
│ ├── hello-page.js // 插件提供的页面(可以有多个,自小程序基础库版本 2.1.0 开始支持)
│ ├── hello-page.json
│ ├── hello-page.wxml
│ └── hello-page.wxss
├── index.js // 插件的 js 接口
└── plugin.json // 插件配置文件

插件配置文件

插件配置文件 plugin.json 主要说明有哪些自定义组件可以供插件外部调用,并标识哪个js文件是插件的js接口文件,如:

  1. {
  2. "publicComponents": {
  3. "hello-component": "components/hello-component"
  4. },
  5. "pages": {
  6. "hello-page": "pages/hello-page"
  7. },
  8. "main": "index.js"
  9. }
插件页面跳转

插件的页面从小程序基础库版本 2.1.0 开始支持。

插件执行页面跳转时,可以用 navigator 组件。当插件跳转到自身页面时, url 应设置为这样的形式:plugin-private://PLUGIN_APPID/PATH/TO/PAGE 。需要跳转到其他插件时,也可以这样设置 url

  1. <navigator url="plugin-private://wxidxxxxxxxxxxxxxx/pages/hello-page">
  2. Go to pages/hello-page!
  3. </navigator>

自基础库版本 2.2.2 开始,在插件自身的页面中,插件还可以调用 wx.navigateTo 来进行页面跳转, url 格式与使用 navigator 组件时相仿

插件对外接口

插件内的自定义组件与普通的自定义组件相仿。插件可以定义若干个自定义组件,这些自定义组件都可以在插件内相互引用。其中,提供给外部使用的自定义组件,必须在插件配置文件中显式声明

插件的 js 接口文件 index.js 中可以 export 一些 js 接口,插件的使用者可以使用 requirePlugin 来获得这些接口

  1. module.exports = {
  2. hello: function() {
  3. console.log('Hello plugin!')
  4. }
  5. }
预览、上传和发布

插件可以像小程序一样预览和上传,但插件没有体验版。

插件会同时有多个线上版本,由使用插件的小程序决定具体使用的版本号。

注意:目前,手机预览插件时将使用一个特殊分配的小程序(即“插件开发助手”)来套用这个插件,这个小程序的 appid 与插件的 appid 不同。服务器端处理插件的网络请求时请留心这个问题。

插件开发文档

除了插件代码本身,小程序开发者可以另外上传一份插件开发文档。这份文档必须放置在插件项目根目录中的 doc 目录下,目录结构如下:

doc
├── README.md // 插件文档,应为 markdown 格式
└── picture.jpg // 其他资源文件,仅支持图片

其中,引用到的图片资源不能是网络图片,必须放在这个目录下。编辑 README.md 之后,可以使用开发者工具预览插件文档和单独上传插件文档。

在开发者工具中上传文档之后,文档不会立刻发布。此时可以使用帐号和密码登录 管理后台 ,在 小程序插件 > 基本设置 中预览、发布插件文档

插件请求签名

插件在使用 wx.request 等 API 发送网络请求时,将会额外携带一个签名 HostSign ,用于验证请求来源于小程序插件。这个签名位于请求头中,形如:

X-WECHAT-HOSTSIGN: {"noncestr":"NONCESTR", "timestamp":"TIMESTAMP", "signature":"SIGNATURE"}

其中, NONCESTR 是一个随机字符串, TIMESTAMP 是生成这个随机字符串和 SIGNATURE 的 UNIX 时间戳。它们是用于计算签名 SIGNATRUE 的参数,签名算法为:

SIGNATURE = sha1([APPID, NONCESTR, TIMESTAMP, TOKEN].sort().join(''))

具体来说,这个算法分为几个步骤:

1.sort 对 APPID NONCESTR TIMESTAMP TOKEN 四个值表示成字符串形式,按照字典序排序(同 JavaScript 数组的 sort 方法);

2.join 将排好序的四个字符串直接连接在一起;

3.对连接结果使用 sha1 算法,其结果即 SIGNATURE 。

插件开发者可以在服务器上使用这个算法校验签名。其中, APPID 是所在小程序的 AppId (可以从请求头的 referrer 中获得); TOKEN 是插件 Token ,可以在小程序插件基本设置中找到。

自基础库版本 2.0.7 开始,在小程序运行期间,若网络状况正常, NONCESTR 和 TIMESTAMP 会每 10 分钟变更一次。如有必要,可以通过判断 TIMESTAMP 来确定当前签名是否依旧有效

使用插件

申请使用插件

在使用插件前,首先要在小程序管理后台的“设置-第三方服务-插件管理”中添加插件。开发者可登录小程序管理后台,通过appId查找插件并添加。插件开发者通过申请后,方可在小程序中使用相应的插件

引入插件代码包

对于插件的使用者,使用插件前要在 app.json 中声明需要使用的插件,例如:

  1. {
  2. "plugins": {
  3. "myPlugin": {
  4. "version": "1.0.0",
  5. "provider": "wxxxxxxxxxxxxxxxxx"
  6. }
  7. }
  8. }

如上例所示, plugins 定义段中可以包含多个插件声明,每个插件声明中都必须指明插件的 appid 和需要使用的版本号

使用插件的 js 接口

在引入插件代码包之后,就可以在这个小程序中使用插件提供的自定义组件或者 js 接口。如果需要使用插件的 js 接口,可以使用 requirePlugin 方法:

  1. var myPluginInterface = requirePlugin('myPlugin')
  2. myPluginInterface.hello()
使用插件的自定义组件

使用插件提供的自定义组件,和使用普通自定义组件的方式相仿。在 json 文件定义需要引入的自定义组件时,使用 plugin:// 协议即可,例如:

  1. {
  2. "usingComponents": {
  3. "hello-component": "plugin://myPlugin/hello-component"
  4. }
  5. }

出于对插件的保护,插件提供的自定义组件在使用上有一定的限制:

1.页面中的 this.selectComponent 接口无法获得插件的自定义组件实例对象;

2.wx.createSelectorQuery 等接口的 >>> 选择器无法选入插件内部。

使用插件的页面

插件的页面从小程序基础库版本 2.1.0 开始支持。

需要跳转到插件页面时, url 应使用 plugin:// 前缀

  1. <navigator url="plugin://myPlugin/hello-page">
  2. Go to pages/hello-page!
  3. </navigator>

插件调用 API 的限制

插件可以调用的 API 与小程序不同,主要有两个区别:

1.插件的请求域名列表与小程序相互独立;

2.一些 API 不允许插件调用(这些函数不存在于 wx 对象下)

插件功能页

插件功能页从小程序基础库版本 2.1.0 开始支持。

插件不能直接调用 wx.login 等较为敏感的接口。在需要访问一些敏感接口时,可以使用插件功能页的方式。使用插件功能页可以实现以下这些功能:

1.获取用户信息,包括 openid 和昵称等(相当于 wx.login 和 wx.getUserInfo 的功能)。

2.支付(相当于 wx.requestPayment )。

需要注意的是:插件使用支付功能,需要进行额外的权限申请,申请位置位于管理后台的“小程序插件 -> 基本设置 -> 支付能力”设置项中。另外,无论是否通过申请,主体为个人小程序在使用插件时,都无法正常使用插件里的支付功能。

在具体使用功能页时,插件可以在插件的自定义组件中放置一个 组件,用户在点击这个组件区域时,可以跳转到一个固定的页面,允许用户执行登录或其他操作

激活功能页特性

功能页是 插件所有者小程序 中的一个特殊页面。

插件所有者小程序,指的是与插件 AppID 相同的小程序。例如,“小程序示例”小程序开发了一个“小程序示例插件”,无论这个插件被哪个小程序使用,这个插件的插件所有者小程序都是“小程序示例”。

启用插件功能页时,需要在插件所有者小程序 app.json 文件中添加 functionalPages 定义段,其值为 true 。

  1. {
  2. "functionalPages": true
  3. }

注意,新增或改变这个字段时,需要这个小程序发布新版本,才能在正式环境中使用插件功能页

跳转到功能页

在插件需要登录时,可以在插件的自定义组件中放置一个 组件

  1. <functional-page-navigator name="loginAndGetUserInfo" args="" version="develop" bind:success="loginSuccess">
  2. <button>登录到插件</button>
  3. </functional-page-navigator>

用户在点击这个区域时,会自动跳转到插件所有者小程序的功能页。功能页会提示用户进行登录或其他相应的操作。操作结果会以组件事件的方式返回

真机开发测试的常规步骤

功能页的跳转目前不支持在开发者工具中调试,请在真机上测试。初次进行真机开发测试时,通常步骤如下。

1.在开发者工具上的 普通小程序项目 模式创建或编辑插件所有者小程序项目,在 app.json 中设置 "functionalPages": true 来激活功能页特性,并点击“预览”。

2.用测试用的真机扫一下预览二维码,此时会进入插件所有者小程序,进入后可以直接退出这个小程序。

3.在开发者工具上另开一个 插件项目 来创建或编辑插件,放置 functional-page-navigator 组件并将属性设置为 version="develop" 。

4.此时点击预览可以生成插件预览二维码,用测试用的真机扫码即可预览功能页;如果更改了插件代码,通常重复3、4两个步骤即可。

5.如果过了一段时间之后,跳转功能页时出现“开发版已过期”这样的提示,从第1步开始重试一次。

注意,在插件提审前,需要:

1.确保已发布设置了 "functionalPages": true 的插件所有者小程序;

2.确保所有的 functional-page-navigator 组件属性设置为 version="release" 。

功能页函数

在使用支付功能页时,插件所有者小程序需要提供一个函数来响应支付请求。这个响应函数应当写在小程序根目录中的 functional-pages/request-payment.js 文件中,名为 beforeRequestPayment 。如果不提供这段代码,将通过 fail 事件返回失败。

注意:功能页函数不用 require 其他非 functional-pages 目录中的文件,其他非 functional-pages 目录中的文件也不用 require 这个目录中的文件。这样的 require 调用在未来将不被支持

  1. // functional-pages/request-payment.js
  2. exports.beforeRequestPayment = function(paymentArgs, callback) {
  3. paymentArgs // 就是 functional-page-navigator 的 args 属性中 paymentArgs
  4. // 在这里可以执行一些支付前的参数处理逻辑,包括通知后台调用统一下单接口
  5. // 小程序 API 也可以调用,如 wx.login 和 wx.getStorage (如同在插件所有者小程序代码中调用这些接口)
  6. // 在 callback 中需要返回两个参数: err 和 requestPaymentArgs
  7. // err 应为 null (或者一些失败信息)
  8. // requestPaymentArgs 将被用于调用 wx.requestPayment
  9. callback(null, {
  10. // 这里的参数与 wx.requestPayment 相同,除了 success/fail/complete 不被支持
  11. timeStamp: timeStamp,
  12. nonceStr: nonceStr,
  13. package: package,
  14. signType: signType,
  15. paySign: paySign,
  16. })
  17. }

这个目录和文件应当被放置在插件所有者小程序代码中(而非插件代码中),它是插件所有者小程序的一部分(而非插件的一部分)。 如果需要新增或更改这段代码,需要发布插件所有者小程序,才能在正式版中生效;需要重新预览插件所有者小程序,才能在开发版中生效

分包加载

微信 6.6 客户端,1.7.3 及以上基础库开始支持,请更新至最新客户端版本,开发者工具请使用 1.01.1712150 及以上版本

某些情况下,开发者需要将小程序划分成不同的子包,在构建时打包成不同的分包,用户在使用时按需进行加载。

在构建小程序分包项目时,构建会输出一个或多个功能的分包,其中每个分包小程序必定含有一个主包,所谓的主包,即放置默认启动页面/TabBar 页面,以及一些所有分包都需用到公共资源/JS 脚本,而分包则是根据开发者的配置进行划分。

在小程序启动时,默认会下载主包并启动主包内页面,如果用户需要打开分包内某个页面,客户端会把对应分包下载下来,下载完成后再进行展示。

目前小程序分包大小有以下限制:

整个小程序所有分包大小不超过 8M

单个分包/主包大小不能超过 2M

对小程序进行分包,可以优化小程序首次启动的下载时间,以及在多团队共同开发时可以更好的解耦协作

使用方法

假设支持分包的小程序目录结构如下:

├── app.js
├── app.json
├── app.wxss
├── packageA
│ └── pages
│ ├── cat
│ └── dog
├── packageB
│ └── pages
│ ├── apple
│ └── banana
├── pages
│ ├── index
│ └── logs
└── utils

开发者通过在 app.json subPackages 字段声明项目分包结构:

{
"pages":[

  1. "pages/index",
  2. "pages/logs"

],
"subPackages": [

  1. {
  2. "root": "packageA",
  3. "pages": [
  4. "pages/cat",
  5. "pages/dog"
  6. ]
  7. }, {
  8. "root": "packageB",
  9. "pages": [
  10. "pages/apple",
  11. "pages/banana"
  12. ]
  13. }

]
}

打包原则

1.声明 subPackages 后,将按 subPackages 配置路径进行打包,subPackages 配置路径外的目录将被打包到 app(主包) 中

2.app(主包)也可以有自己的 pages(即最外层的 pages 字段)

3.subPackage 的根目录不能是另外一个 subPackage 内的子目录

4.首页的 TAB 页面必须在 app(主包)内

引用原则

1.packageA 无法 require packageB JS 文件,但可以 require app、自己 package 内的 JS 文件

2.packageA 无法 import packageB 的 template,但可以 require app、自己 package 内的 template

3.packageA 无法使用 packageB 的资源,但可以使用 app、自己 package 内的资源

低版本兼容

由微信后台编译来处理旧版本客户端的兼容,后台会编译两份代码包,一份是分包后代码,另外一份是整包的兼容代码。 新客户端用分包,老客户端还是用的整包,完整包会把各个 subpackage 里面的路径放到 pages 中

多线程 Worker

一些异步处理的任务,可以放置于 Worker 中运行,待运行结束后,再把结果返回到小程序主线程。Worker 运行于一个单独的全局上下文与线程中,不能直接调用主线程的方法。 Worker 与主线程之间的数据传输,双方使用 Worker.postMessage() 来发送数据,Worker.onMessage() 来接收数据,传输的数据并不是直接共享,而是被复制的

步骤

配置 Worker 信息

在 app.json 中可配置 Worker 代码放置的目录,目录下的代码将被打包成一个文件:

  1. {
  2. "workers": "workers"
  3. }
添加 Worker 代码文件

根据步骤 1 中的配置,在代码目录下新建以下两个入口文件:

  1. workers/request/index.js
  2. workers/request/utils.js
  3. workers/response/index.js

添加后,目录结构如下:

├── app.js
├── app.json
├── project.config.json
└── workers

  1. ├── request
  2. │ ├── index.js
  3. │ └── utils.js
  4. └── response
  5. └── index.js
编写 Worker 代码

在 workers/request/index.js 编写 Worker 响应代码

  1. var utils = require('./utils')
  2. // 在 Worker 线程执行上下文会全局暴露一个 `worker` 对象,直接调用 worker.onMeesage/postMessage 即可
  3. worker.onMessage(function (res) {
  4. console.log(res)
  5. })
在主线程中初始化 Worker

在主线程的代码 app.js 中初始化 Worker

var worker = wx.createWorker('workers/request/index.js') // 文件名指定 worker 的入口文件路径,绝对路径
主线程向 Worker 发送消息
  1. worker.postMessage({
  2. msg: 'hello worker'
  3. })

Tips

1.Worker 最大并发数量限制为 1 个,创建下一个前请用 Worker.terminate() 结束当前 Worker

2.Worker 内代码只能 require 指定 Worker 路径内的文件,无法引用其它路径

3.Worker 的入口文件由 wx.createWorker() 时指定,开发者可动态指定 Worker 入口文件

4.Worker 内不支持 wx 系列的 API

5.Workers 之间不支持发送消息

基础库

基础库更新

为了避免新版本的基础库给线上小程序带来未知的影响,微信客户端都是携带 上一个稳定版 的基础库发布的。

在新版本客户端发布后,再通过后台灰度新版本基础库,灰度时长一般为 12 ~ 24 小时,在灰度结束后,用户设备上才会有新版本的基础库。

以微信 6.5.8 为例,客户端在发布时携带的是 1.1.1 基础库(6.5.7 上已全量的稳定版)发布,在 6.5.8 发布后,我们再通过后台灰度 1.2.0 基础库。

基础库与客户端之间的关系

小程序的能力需要微信客户端来支撑,每一个基础库都只能在对应的客户端版本上运行,高版本的基础库无法兼容低版本的微信客户端。通常:

第 1(major)、2(minor)位版本号更新需要依赖新版本的客户端,如:基础库 v2.1.3 运行在 v6.6.7 客户端,基础库 v2.2.0 需要 v6.7.0 客户端。

第 3(patch) 位版本号的更新不需要依赖客户端更新,如:基础库v2.1.0 ~ v2.1.3 都运行在 v6.6.7 客户端,新版本发布会覆盖旧版本
1

兼容

小程序的功能不断的增加,但是旧版本的微信客户端并不支持新功能,所以在使用这些新能力的时候需要做兼容。

文档会在组件,API等页面描述中带上各个功能所支持的版本号。

可以通过 wx.getSystemInfo 或者 wx.getSystemInfoSync 获取到小程序的基础库版本号。

也可以通过 wx.canIUse 来判断是否可以在该基础库版本下直接使用对应的API或者组件

兼容方式 - 版本比较

微信客户端和小程序基础库的版本号风格为 Major.Minor.Patch(主版本号.次版本号.修订号)。 开发者可以根据版本号去做兼容,以下为参考代码:

  1. function compareVersion(v1, v2) {
  2. v1 = v1.split('.')
  3. v2 = v2.split('.')
  4. var len = Math.max(v1.length, v2.length)
  5. while (v1.length < len) {
  6. v1.push('0')
  7. }
  8. while (v2.length < len) {
  9. v2.push('0')
  10. }
  11. for (var i = 0; i < len; i++) {
  12. var num1 = parseInt(v1[i])
  13. var num2 = parseInt(v2[i])
  14. if (num1 > num2) {
  15. return 1
  16. } else if (num1 < num2) {
  17. return -1
  18. }
  19. }
  20. return 0
  21. }
  22. compareVersion('1.11.0', '1.9.9') // 1

兼容方式 - 接口

对于新增的 API,可以用以下代码来判断是否支持用户的手机

  1. if (wx.openBluetoothAdapter) {
  2. wx.openBluetoothAdapter()
  3. } else {
  4. // 如果希望用户在最新版本的客户端上体验您的小程序,可以这样子提示
  5. wx.showModal({
  6. title: '提示',
  7. content: '当前微信版本过低,无法使用该功能,请升级到最新微信版本后重试。'
  8. })
  9. }

兼容方式 - 参数

对于 API 的参数或者返回值有新增的参数,可以判断用以下代码判断

  1. wx.showModal({
  2. success: function(res) {
  3. if (wx.canIUse('showModal.cancel')) {
  4. console.log(res.cancel)
  5. }
  6. }
  7. })

兼容方式 - 组件

对于组件,新增的组件或属性在旧版本上不会被处理,不过也不会报错。如果特殊场景需要对旧版本做一些降级处理

  1. Page({
  2. data: {
  3. canIUse: wx.canIUse('cover-view')
  4. }
  5. })
  6. <video controls="{{!canIUse}}">
  7. <cover-view wx:if="{{canIUse}}">play</cover-view>
  8. </video>

运行机制

小程序启动会有两种情况,一种是「冷启动」,一种是「热启动」。 假如用户已经打开过某小程序,然后在一定时间内再次打开该小程序,此时无需重新启动,只需将后台态的小程序切换到前台,这个过程就是热启动;冷启动指的是用户首次打开或小程序被微信主动销毁后再次打开的情况,此时小程序需要重新加载启动

更新机制

小程序冷启动时如果发现有新版本,将会异步下载新版本的代码包,并同时用客户端本地的包进行启动,即新版本的小程序需要等下一次冷启动才会应用上。 如果需要马上应用最新版本,可以使用 wx.getUpdateManager API 进行处理

运行机制

小程序没有重启的概念

当小程序进入后台,客户端会维持一段时间的运行状态,超过一定时间后(目前是5分钟)会被微信主动销毁

当短时间内(5s)连续收到两次以上收到系统内存告警,会进行小程序的销毁

再次打开逻辑

基础库 1.4.0 开始支持,低版本需做兼容处理

用户打开小程序的预期有以下两类场景:

A. 打开首页: 场景值有 1001, 1019, 1022, 1023, 1038, 1056

B. 打开小程序指定的某个页面: 场景值为除 A 以外的其他

当再次打开一个小程序逻辑如下:

上一次的场景当前打开的场景效果
AA保留原来的状态
BA清空原来的页面栈,打开首页(相当于执行 wx.reLaunch 到首页)
A 或 BB清空原来的页面栈,打开指定页面(相当于执行 wx.reLaunch 到指定页)

性能

目前,小程序提供了两种性能分析工具,和几个性能优化上的建议,开发者可以参考使用

优化建议

setData

setData 是小程序开发中使用最频繁的接口,也是最容易引发性能问题的接口。在介绍常见的错误用法前,先简单介绍一下 setData 背后的工作原理

工作原理

小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。在架构上,WebView 和 JavascriptCore 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。

而 evaluateJavascript 的执行会受很多方面的影响,数据到达视图层并不是实时的

常见的 setData 操作错误
  1. 频繁的去 setData

在我们分析过的一些案例里,部分小程序会非常频繁(毫秒级)的去setData,其导致了两个后果:

1).Android 下用户在滑动时会感觉到卡顿,操作反馈延迟严重,因为 JS 线程一直在编译执行渲染,未能及时将用户操作事件传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层;

2).渲染有出现延时,由于 WebView 的 JS 线程一直处于忙碌状态,逻辑层到页面层的通信耗时上升,视图层收到的数据消息时距离发出时间已经过去了几百毫秒,渲染的结果并不实时;

2.每次 setData 都传递大量新数据

由setData的底层实现可知,我们的数据传输实际是一次 evaluateJavascript 脚本过程,当数据量过大时会增加脚本的编译执行时间,占用 WebView JS 线程

  1. 后台态页面进行 setData

当页面进入后台态(用户不可见),不应该继续去进行setData,后台态页面的渲染用户是无法感受的,另外后台态页面去setData也会抢占前台页面的执行

图片资源

目前图片资源的主要性能问题在于大图片和长列表图片上,这两种情况都有可能导致 iOS 客户端内存占用上升,从而触发系统回收小程序页面

图片对内存的影响

在 iOS 上,小程序的页面是由多个 WKWebView 组成的,在系统内存紧张时,会回收掉一部分 WKWebView。从过去我们分析的案例来看,大图片和长列表图片的使用会引起 WKWebView 的回收

图片对页面切换的影响

除了内存问题外,大图片也会造成页面切换的卡顿。我们分析过的案例中,有一部分小程序会在页面中引用大图片,在页面后退切换中会出现掉帧卡顿的情况。

当前我们建议开发者尽量减少使用大图片资源

代码包大小的优化

小程序一开始时代码包限制为 1MB,但我们收到了很多反馈说代码包大小不够用,经过评估后我们放开了这个限制,增加到 2MB 。代码包上限的增加对于开发者来说,能够实现更丰富的功能,但对于用户来说,也增加了下载流量和本地空间的占用。

开发者在实现业务逻辑同时也有必要尽量减少代码包的大小,因为代码包大小直接影响到下载速度,从而影响用户的首次打开体验。除了代码自身的重构优化外,还可以从这两方面着手优化代码大小:

1.控制代码包内图片资源

小程序代码包经过编译后,会放在微信的 CDN 上供用户下载,CDN 开启了 GZIP 压缩,所以用户下载的是压缩后的 GZIP 包,其大小比代码包原体积会更小。 但我们分析数据发现,不同小程序之间的代码包压缩比差异也挺大的,部分可以达到 30%,而部分只有 80%,而造成这部分差异的一个原因,就是图片资源的使用。GZIP 对基于文本资源的压缩效果最好,在压缩较大文件时往往可高达 70%-80% 的压缩率,而如果对已经压缩的资源(例如大多数的图片格式)则效果甚微

2.及时清理没有使用到的代码和资源

在日常开发的时候,我们可能引入了一些新的库文件,而过了一段时间后,由于各种原因又不再使用这个库了,我们常常会只是去掉了代码里的引用,而忘记删掉这类库文件了。目前小程序打包是会将工程下所有文件都打入代码包内,也就是说,这些没有被实际使用到的库文件和资源也会被打入到代码包里,从而影响到整体代码包的大小

分析工具

性能 Trace 工具

微信 Andoid 6.5.10 开始,我们提供了 Trace 导出工具,开发者可以在开发者工具 Trace Panel 中使用该功能

使用方法

PC 上需要先安装 adb 工具,可以参考一些主流教程进行安装,Mac 上可使用 brew 直接安装。

确定 adb 工具已成功安装后,在开发者工具上打开 Trace Panel,将 Android 手机通过 USB 连接上 PC,点击「Choose Devices」,此时手机上可能弹出连接授权框,请点击「允许」。

选择设备后,在手机上打开你需要调试的开发版小程序,通过右上角菜单,打开性能监控面板,重启小程序;

重启后,在小程序上进行操作,完成操作后,通过右上角菜单,导出 Trace 数据;

此时开发者工具 Trace Panel 上会自动拉取 Trace 文件,选择你要分析的 Trace 文件即可;

可以通过 adb devices 命令确定设备是否已和 PC 建立起连接
1

性能面板

从微信 6.5.8 开始,我们提供了性能面板让开发者了解小程序的性能。开发者可以在开发版小程序下打开性能面板,打开方法:进入开发版小程序,进入右上角更多按钮,点击「显示性能窗口」
1

性能面板指标说明
指标说明
CPU小程序进程的 CPU 占用率,仅 Android 下提供
内存小程序进程的内存占用(Total Pss),仅 Android 下提供
启动耗时小程序启动总耗时
下载耗时小程序包下载耗时,首次打开或资源包需更新时会进行下载
页面切换耗时小程序页面切换的耗时
帧率/FPS
首次渲染耗时页面首次渲染的耗时
再次渲染耗时页面再次渲染的耗时(通常由开发者的 setData 操作触发)
数据缓存小程序通过 Storage 接口储存的缓存大小

文件系统

文件系统是小程序提供的一套以小程序和用户维度隔离的存储以及一套相应的管理接口。通过 wx.getFileSystemManager() 可以获取到全局唯一的文件系统管理器,所有文件系统的管理操作通过 FileSystemManager 来调用

var fs = wx.getFileSystemManager()

文件主要分为两大类:

1.代码包文件:代码包文件指的是在项目目录中添加的文件

2.本地文件:通过调用接口本地产生,或通过网络下载下来,存储到本地的文件

其中本地文件又分为三种:

1)本地临时文件:临时产生,随时会被回收的文件。不限制存储大小。

2)本地缓存文件:小程序通过接口把本地临时文件缓存后产生的文件,不能自定义目录和文件名。除非用户主动删除小程序,否则不会被删除。跟本地用户文件共计最多可存储 50MB 文件。

3)本地用户文件:小程序通过接口把本地临时文件缓存后产生的文件,允许自定义目录和文件名。除非用户主动删除小程序,否则不会被删除。最多可存储 50MB 文件。

代码包文件

由于代码包文件大小限制,代码包文件适用于放置首次加载时需要的文件,对于内容较大或需要动态替换的文件,不推荐用添加到代码包中,推荐在小游戏启动之后再用下载接口下载到本地

访问代码包文件

代码包文件的访问方式是从项目根目录开始写文件路径,不支持相对路径的写法

修改代码包文件

代码包内的文件无法在运行后动态修改或删除,修改代码包文件需要重新发布版本

本地文件

本地文件指的是小程序被用户添加到手机后,会有一块独立的文件存储区域,以用户维度隔离。即同一台手机,每个微信用户不能访问到其他登录用户的文件,同一个用户不同 appId 之间的文件也不能互相访问

本地文件的文件路径均为以下格式:

{{协议名}}://文件路径

其中,协议名在 iOS/Android 客户端为 "wxfile",在开发者工具上为 "http",开发者无需关注这个差异,也不应在代码中去硬编码完整文件路径

本地临时文件

本地临时文件只能通过调用特定接口产生,不能直接写入内容。本地临时文件产生后,仅在当前生命周期内有效,重启之后即不可用。因此,不可把本地临时文件路径存储起来下次使用。如果需要下次在使用,可通过 FileSystemManager.saveFile() 或 FileSystemManager.copyFile() 接口把本地临时文件转换成本地缓存文件或本地用户文件

  1. wx.chooseImage({
  2. success: function (res) {
  3. var tempFilePaths = res.tempFilePaths // tempFilePaths 的每一项是一个本地临时文件路径
  4. }
  5. })
本地缓存文件

本地缓存文件只能通过调用特定接口产生,不能直接写入内容。本地缓存文件产生后,重启之后仍可用。本地缓存文件只能通过 FileSystemManager.saveFile() 接口将本地临时文件保存获得

  1. fs.saveFile({
  2. tempFilePath: '', // 传入一个本地临时文件路径
  3. success(res) {
  4. console.log(res.savedFilePath) // res.savedFilePath 为一个本地缓存文件路径
  5. }
  6. })

注意:本地缓存文件是最初的设计,1.7.0 版本开始,提供了功能更完整的本地用户文件,可以完全覆盖本地缓存文件的功能,如果不需要兼容低于 1.7.0 版本,可以不使用本地缓存文件

本地用户文件

本地用户文件是从 1.7.0 版本开始新增的概念。我们提供了一个用户文件目录给开发者,开发者对这个目录有完全自由的读写权限。通过 wx.env.USER_DATA_PATH 可以获取到这个目录的路径

  1. // 在本地用户文件目录下创建一个文件 a.txt,写入内容 "hello, world"
  2. const fs = wx.getFileSystemManager()
  3. fs.writeFileSync(`${wx.env.USER_DATA_PATH}/hello.txt`, 'hello, world', 'utf8')
读写权限
接口、组件
代码包文件
本地临时文件
本地缓存文件
本地用户文件
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/88369
推荐阅读
相关标签
  

闽ICP备14008679号

        
cppcmd=keepalive&