Skip to content

微信小程序2

八、自定义组件

1. 创建-注册-使用组件

1.1 组件介绍

小程序支持组件化开发,可以将页面中的功能模块抽取成自定义组件,以便在不同的页面中重复使用;

也可以将复杂的页面拆分成多个低耦合的模块,有助于代码维护。

开发中常见的组件有两种:

  1. 公共组件:将页面内的功能模块抽象成自定义组件,以便在不同的页面中重复使用,建议将其放在小程序的目录下的 components 文件夹中
  2. 页面组件:将复杂的页面拆分成多个低耦合的模块,有助于代码维护,建议将其放在小程序对应页面目录下,当然你也可以放到页面的 components 文件夹中

同时建议:一个组件一个文件夹,文件夹名称和组件名称保持一致

📌 注意事项

  1. 自定义组件的需要在 .json 文件中需要配置 component 字段设为 true
  2. 自定义组件通过 Component 构造器进行构建,在构造器中可以指定组件的属性、数据、方法等
1.2 创建自定义组件:

创建组件的步骤很简单,以公共组件为例,创建的步骤如下:

  1. 在小程序的根目录下新建 components 文件夹

  2. components 文件夹上,点击右键,选择新建文件夹 ,然后输入文件夹名称,建议文件夹的名称和组件的名称保持一致,这样方便后期对组件进行维护。

  3. 在新建的文件夹上,点击右键,选择新建 Component,然后输入组件的名称,组件的名称建议和文件夹保持一致

  4. 此时就已经创建了一个功能组件

1.3 使用自定义组件

开发中常见的组件主要分为 公共组件页面组件 两种,因此注册组件的方式也分为两种:

  1. 公共组件全局注册:

    app.json 文件中配置 usingComponents 进行引用声明,注册后可在任意组件使用

  2. 页面组件局部注册:

    在页面的 .json 文件中配置 usingComponents 进行引用声明,只可在当前页面使用

在配置 usingComponents 节点进行引用声明时,需要提供自定义组件的标签名和对应的自定义组件文件路径,语法如下:

json
{
  "usingComponents": {
    "自定义组件的标签名": "自定义组件文件路径"
  }
}

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

json
{
  "usingComponents": {
    "custom-checkbox": "/components/custom-checkbox/custom-checkbox"
  }
}
html
<!--pages/index/index.wxml-->
<view>
  <!-- 将导入的自定义组件当成标签使用 -->
  <custom-checkbox />
</view>

2. 自定义组件-数据和方法

在组件的 .js 中,需要调用 Component 方法创建自定义组件,Component 中有以下两个属性:

data 数据:组件的内部数据

methods 方法:在组件中事件处理程序需要写到 methods 中才可以

落地代码:

➡️ components/custom-checkbox/custom-checkbox.wxml

html
<view class="custom-checkbox-container">
  <view class="custom-checkbox-box">
    <checkbox checked="{{ isChecked }}" bindtap="updateChecked" />
  </view>
</view>

➡️ components/custom-checkbox/custom-checkbox.wxss

css
.custom-checkbox-container {
  display: inline-block;
}

➡️ components/custom-checkbox/custom-checkbox.js

js
Component({
  // 组件的属性列表
  properties: {},

  // 用来定义当前组件内部所需要使用的数据
  data: {
    // 控制复选框是否选中
    isChecked: false,
  },

  // 组件的方法列表
  methods: {
    // 点击复选框的事件
    updateChecked() {
      this.setData({
        isChecked: !this.data.isChecked,
      });
    },
  },
});

➡️ app.json

json
{
  "usingComponents": {
    "custom-checkbox": "./components/custom-checkbox/custom-checkbox"
  }
}

➡️ index.wxml

html
<custom-checkbox />
<view class="line"></view>
<custom-checkbox />

注意:

​ 组件中的数据是通过 Component 构造器进行构建的,因此每次调用组件时的数据都是全新的数据,不存在引用关系。

3. 自定义组件-属性

组件的 Properties 属性用来接收组件使用者传递给组件内部的数据,和 data 一同用于组件的模板渲染

使用方法:

js
Component({
  // 组件的属性列表:组件的对外属性,主要用于组件使用者传递给组件内部的属性及数据
  properties: {
    // 接收组件使用者传递的数据
    // 方式一:简写
    // label: String,
    // 方式二:全写
    label: {
      // type 组件使用者传递数据的类型
      type: String, // String|Number|Boolean|Object|Array|Null(不限制类类型)
      value: "", // 设置默认值
    },
  },
}

📌 注意事项:

  1. 设置属性类型需要使用 type 属性,属性类型是必填项,value 属性为默认值
  2. 属性类型可以为 StringNumberBooleanObjectArray ,也可以为 null 表示不限制类型

示例:

➡️ index.wxml

html
<!-- label 文本显示的内容 -->
<!-- position 控制文本显示的位置 -->
<custom-checkbox label="我已阅读并同意 用户协议 和 隐私协议" position="right" />
<view class="line"></view>
<custom-checkbox label="匿名提交" position="left" />

➡️ components/custom-checkbox/custom-checkbox.wxml

html
<!--components/custom-checkbox/custom-checkbox.wxml-->
<!-- <text>我是自定义组件</text> -->
<view class="custom-checkbox-cintainer">
+  <!-- 通过动态类名设置文本和复选框的位置 -->
+  <view class="custom-checkbox-box {{ position }}">
+    <checkbox checked="{{ isChecked }}" bind:tap="updateChecked" style="margin-left: 10rpx;" />
+    <view>
+      <text>{{ label }}</text>  <!-- 使用父组件传递过来的数据 --> 
    </view>
  </view>
</view>

➡️ components/custom-checkbox/custom-checkbox.scss

scss
/* components/custom-checkbox/custom-checkbox.scss */
.custom-checkbox-cintainer {
  display: inline-block;
  .custom-checkbox-box {
    display: flex;
    align-items: center;
    // 文字处于左侧的类名
    &.left {
      flex-direction: row-reverse; // 反转
    }
    // 文字处于右侧的类名
    &.right {
      flex-direction: row;
    }
  }
}

➡️ components/custom-checkbox/custom-checkbox.js

js
Component({
  // 组件的属性列表:组件的对外属性,主要用于组件使用者传递给组件内部的属性及数据
  properties: {
    // 接收组件使用者传递的数据
    // 方式一:简写
    // label: String,
    // 方式二:全写
    label: {
      // type 组件使用者传递数据的类型
      type: String, // String|Number|Boolean|Object|Array|Null(不限制类类型)
      value: "", // 设置默认值
    },
    position: {
      type: String,
      value: "right",
    },
  },

  // 用来定义当前组件内部所需要使用的数据
  data: {
    // 控制复选框是否选中
    isChecked: false,
  },

  // 组件的方法列表
  methods: {
    // 点击复选框的事件
    updateChecked() {
      this.setData({
        isChecked: !this.data.isChecked,
        // 修改 properties 中的数据也需要通过 this.setData进行修改
        label: "在组件内部也可以修改properties中的数据",
      });
      // 在方法中获取 properties 中的属性
      console.log(this.properties.label);
      // 在js中可以访问和获取properties中的数据,但不建议进行修改,因为会造成数据流的混乱
    },
  },
});

Tips:

​ 在组件的 properties属性中声明父组件传递的数据后可以直接在 .wxml中使用

​ 在 js中可以访问和获取properties中的数据,但不建议进行修改,会造成数据流的混乱

4. 组件 wxml 的 slot

在使用基础组件时,可以给组件传递子节点传递内容,从而将内容展示到页面中,自定义组件也可以接收子节点内容

只不过在组件模板中需要定义 <slot /> 节点,用于承载组件引用时提供的子节点

默认情况下,一个组件的 wxml 中只能有一个默认的 slot(默认插槽) 需要使用多 slot 时,可以在组件 js 中声明启用。同时需要给 slot 添加 name 来区分不同的 slot(具名插槽),给子节点内容添加 slot 属性来将节点插入到对应的 slot 中

用法:

定义插槽➡️ custom01.html

html
<view>
  <!-- slot 就是用来接收,承载父组件传递过来的内容 -->
  <!-- slot 只是一个占位符,父组件传递的内容会将slot替换 -->
  <view>
    <!-- 具名插槽 -->
    <slot name="header" />
  </view>
  <view>
    <!-- 默认插槽 -->
    <slot />
  </view>
  <view>
    <!-- 具名插槽 -->
    <slot name="footer" />
  </view>
</view>

声明使用多插槽➡️ custom01.js

js
// components/custom01/custom01.js
Component({
  options: {
    // 启用多 slot 支持(可以使用具名插槽)
    multipleSlots: true
  }
})

使用插槽➡️ cart.wxml

html
<custom01>
    <!-- 默认情况下,自定义组件的子节点内容不会进行展示,如果想内容进行展示,需要在组自定义组件中定义 slot 节点 -->
    <text slot="header">顶部插槽</text>
    <!-- 未指定插槽名称传递到默认插槽中 -->
    <text>默认插槽</text>
    <text slot="footer">底部插槽</text>
</custom01>

完善复选框案例

➡️ custom-checkbox.html

html
<!--components/custom-checkbox/custom-checkbox.wxml-->
<view class="custom-checkbox-cintainer">
  <!-- 通过动态类名设置文本和复选框的位置 -->
  <view class="custom-checkbox-box {{ position }}">
    <checkbox checked="{{ isChecked }}" bind:tap="updateChecked" style="margin-left: 10rpx;" />
    <view>
      <text>
        <!-- 如果传递了label属性就不展示插槽 -->
        <text wx:if="{{ label !== ''}}">{{ label }}</text>
        <slot wx:else />
      </text>
    </view>
  </view>
</view>

➡️ index.html

html
  <!-- 组件使用者向自定义组件传递的数据
      label:文本显示的内容
      position:控制文本显示的位置
    -->
  <custom-checkbox label="我已阅读并同意 用户协议 和 隐私协议" position="right">
    <block>我已阅读并同意 用户协议 和 隐私协议</block>
  </custom-checkbox>
  <view class="line"></view>
  <custom-checkbox label="匿名提交" position="left">
    <block>匿名提交</block>
  </custom-checkbox>
</view>

5. 组件样式以及注意事项

选择器使用注意事项:

类似于页面,自定义组件拥有自己的 wxss 样式,组件对应 wxss 文件的样式,只对当前组件wxml内的节点生效

编写组件样式时,需要注意以下几点:

  1. app.wxsspage.wxss 中使用了标签选择器(如:textview)选择器来直接指定样式,这些选择器会影响到页面和全部组件,通常情况下这是不推荐的做法
  2. 组件和引用组件的页面不能使用 id 选择器(#a)、属性选择器([a]) 和 标签名选择器,请只使用 class 选择器
  3. 组件和引用组件的页面中使用后代选择器(.a .b)在一些极端情况下会有非预期的表现,如遇请避免使用
  4. 子元素选择器(.a>.b)只能用于 view 组件与其子节点之间,用于其他组件可能导致非预期的情况。
  5. 继承样式,在自定义组件的父组件内设置了 fontcolor ,会继承到自定义组件内。
  6. 除继承样式外, 在 app.wxsspage.wxss中有与自定义组件中相同类名的样式对自定义组件无效 (除非更改组件样式隔离选项)

示例:

➡️ custom02.wxml

html
<text id="content" class="content son">
  <text class="label">给自定义组件设置样式</text>
</text>

➡️ custom02.wxss

scss
/* components/custom02/custom02.wxss */
/* 第一个注意事项:在自定义的 wxss 文件中,不允许使用标签选择器,ID 选择器,属性选择器 */
/* 请改为 class 选择器 */
/* text {
  color: lightseagreen;
} */
/* #content {
  color: lightseagreen;
} */
/* [id=content] {
  color: lightseagreen;
} */
/* .content {
  color: lightseagreen;
} */
/* 第二个注意事项:子选择器,只能用于 view 和 子组件,用于其他组件可能会出现样式失效的问题 */
/* .content > .label {
  color: lightseagreen;
} */
/* 第三个注意事项:继承样式,例如:color\font 都会从组件外继承 */
/* 第四个注意事项:全局样式、组件所在页面的样式文件中的样式都对自定义组件无效 */
/* 第五个注意事项:官方不推荐做法 */
/* 不建议在 全局样式文件 以及 父级页面之间使用标签选择器设置样式 */
/* 如果是在全局样式文件中设置样式,会影响项目中全部的相同组件 */
/* 如果是再页面样式文件中设置样式,会影响当前页面所有的相同组件 */
/* 第六个注意事项: */
/* 组件和组件使用者,如果使用了后代选择器,可能会出现一些非预期情况 */
/* 如果出现,请避免 */

➡️ cate.wxml

html
<view class="custom parent">
  <view>
    <custom02 />
    <view class="son test">我是父级页面中的结构</view>
  </view>
</view>

➡️ cate.wxss

scss
/* pages/cate/cate.wxss */
/* .custom  {
  color: lightseagreen;
  font-size: 50rpx;
} */
/* .label {
  color: lightseagreen;
} */
/* text {
  color: lightseagreen;
} */
.parent .son.test {
  color: lightsalmon;
}

➡️ app.wxss

scss
/* .label {
  color: lightseagreen;
} */
/* text {
  color: lightseagreen;
} */

Tips:

​ 为自定义组件设置样式,首选 class 选择器,在全局和页面样式中不建议使用标签选择器

6. 组件样式隔离

默认情况下,自定义组件的样式只受到自定义组件 wxss 的影响。除非以下两种情况:

  1. app.wxss 或页面的 wxss 中使用了标签名(view)选择器(或一些其他特殊选择器)来直接指定样式,这些选择器会影响到页面和全部组件。通常情况下这是不推荐的做法。

  2. 指定特殊的样式隔离选项 styleIsolation

    js
    Component({
      options: {
        styleIsolation: 'isolated'
      }
    })

styleIsolation 选项它支持以下取值:

  • isolated 表示启用样式隔离,即自定义组件和组件使用者如果存在相同类名互不影响,类名互不影响(默认值),但标签选择器会产生影响;
  • apply-shared 表示组件使用者、页面的 wxss 样式能够影响到自定义组件,但自定义组件的样式不会影响组件使用者、页面的 wxss 样式
  • shared 表示组件使用者、页面的 wxss 样式能够影响到组件,自定义组件的样式会影响组件使用者,页面的 wxss 样式,和其他使用了 apply-share 以及 share 属性的自定义组件

落地代码:

➡️ custom03.wxml

html
<!--components/custom03/custom03.wxml-->

<text class="label">演示组件样式隔离</text>

➡️ custom03.wxss

scss
/* components/custom03/custom03.wxss */

.test {
  color: lightseagreen;
  font-size: 50rpx;
}

➡️ custom03.js

js
// components/custom03/custom03.js
Component({

  options: {

    // styleIsolation:配置组件样式隔离

    // isolated:开启样式隔离,默认值
    // 在默认情况下,自定义组件和组件使用者如果存在相同的类名,类名不会相互影响

    // apply-shared:表示组件使用者、页面的 wxss 样式能够影响到自定义组件
    // 但是自定义组件的样式不会影响组件使用者、页面的 wxss 样式
    // styleIsolation: "apply-shared"

    // shared:表示组件使用者、页面的 wxss 样式能够影响到自定义组件
    // 自定义组件的样式会影响组件使用者、页面的 wxss 样式
    // 和其他使用了 apply-share 以及 share 属性的自定义组件
    styleIsolation: 'shared'
  }
})

➡️ cate.wxml

html
<custom03 />

➡️ cate.wxss

scss
.label {
  color: lightsalmon;
}

7. 拓展-小程序修改checkbox样式

技巧:在官方文档,找到官方提供的案例,审查元素,就能看到对应的类名

📌 注意事项

  1. .custom-checkbox .wx-checkbox-input {}:复选框没有选中时默认的样式
  2. .custom-checkbox .wx-checkbox-input-checked {}: 复选框选中时默认的样式
  3. .custom-checkbox .wx-checkbox-input.wx-checkbox-input-checked:before {}:复选框选中时 √ 样式

这几个类名,在全局样式文件、页面样式文件都可以对修改复选框样式,且会影响全局、页面中的复选框的样式,但是在自定义组件内部使用的时候,需要添加 styleIsolation: 'shared' 属性,在使用时需要加上自己为 checkbox添加的类名,避免影响其他页面中的 checkbox的样式

示例:

➡️ components/custom-checkbox/custom-checkbox.wxss

scss
/* 复选框组件是公共组件 */
/* 以后需要再多个页面或者需要再多个项目中进行使用 */
/* 因此需要先给复选框组件准备、设置一些默认样式 */
/* 如果在其他页面或者其他项目中使用的时候,发现样式不符合产品需求 */
/* 可以进行修改、对默认的样式进行修改 */

/* 1. 需要给复选框设置默认样式 */
/* 需要先找到小程序给复选框提供的类名,通过小程序给提供的类名修改才可以 */
/* 需要先打开小程序开发文档,找到复选框文档,审查元素,进行查找 */

/* 在自定义组件中,不能直接修改复选框样式 */
/* 如果需要进行修改,需要设置 styleIsolation:"shared" 才可以 */
/* shared:修改其他页面的样式、组件使用者的样式、以及其他使用了 share 以及 apply-share 的组件 */
/* 这时候,不是想要的结果 需求是:只想影响当前组件,*/
/* 可以添加命名空间,即在.wx-checkbox-input 前添加自己为 checkbox 组件添加的类名 */

/* 复选框没有选中时默认的样式 */
.custom-checkbox .wx-checkbox-input {
  width: 24rpx ;
  height: 24rpx ;
  border-radius: 50% ;
  border: 1px solid #fda007 ;
  margin-top: -6rpx;
}

/* 复选框选中时默认的样式 */
.custom-checkbox .wx-checkbox-input-checked {
  background-color: #fda007 ;
}

/* 复选框选中时 √ 样式 */
.custom-checkbox .wx-checkbox-input.wx-checkbox-input-checked:before {
  font-size: 22rpx;
  color: #fff;
}

➡️ components/custom-checkbox/custom-checkbox.js

js
Component({
  options: {
    styleIsolation: 'shared'
  }
})

➡️ index.wxss

scss
/* 组件使用者修改复选框的样式 */
.custom-checkbox .wx-checkbox-input {
  border: 1px solid lightseagreen !important; // 增加权重
}

.custom-checkbox .wx-checkbox-input-checked {
  background-color: lightseagreen !important;
}

Tips:

  • 如果设置的样式不奏效,需要为样式添加权重 !impoort
  • 在自定义组件中,不能直接修改复选框样式如果需要进行修改,需要设置 styleIsolation:"shared" 才可以,或者将样式文件放在 app.scsspage.scss

8. 数据监听器

数据监视器 observes用于监视 propertiesdata中数据的变化,当使用 this.setData修改数据时,就会触发对应的监听回调函数

用法:

js
Component({
  // 接收父组件传递的数据
  properties: {
    label: String,
  },
  // 组件内部的数据
  data: {
    num: 10,
    count: 100,
    obj: {
      name: "Tom",
      age: 10,
    },
    arr: [1, 2, 3],
  },

  // 监视data和properties中的数据是否的变化
  observers: {
    // 1.基本用法,监视基本类型的属性
    // key:需要监听的数据
    // value:回调函数,接收最新的数据
    num: function (newValue) {
      console.log("num发生了变化最新值为" + newValue);
    },
    count: function (newValue) {
      console.log("count发生了变化最新值为" + newValue);
    },
    // 2.同时监听多个属性
    "num , count": function (newNum, newCount) {
      console.log(newNum, newCount);
    },
    // 3.支持监视属性内部数据的变化
    "obj.name,arr[1]": function (newName, newArr) {
      console.log(newName, newArr);
    },
    // 4.监视对象中全部属性,使用通配符.**
    "obj.**": function (newValue) {
      console.log("obj中的数据发生了变化", newValue);
    },
    "arr.**": function (newValue) {
      console.log("arr中的数据发生了变化", newValue);
    },
    // 5.监视properites中的数据,会立即执行一次
    label: function (newValue) {
      console.log(newValue);
    },
  },

  // 组件的方法
  methods: {
    // 用于更新数据
    updateData() {
      this.setData({
        num: this.data.num + 1,
        count: this.data.count - 1,
        "obj.name": "李四",
        "arr[1]": 99,
        label: "更新后的标题",
      });
    },
  },
});

Tips:

  • observers可以直接监视到对象类型内部的某个属性的变化
  • 在监视回调中会注入,监视属性的最新值
  • 当需要监视 Object类型的全部数据时,需要使用通配符 .**(深度监视)
  • 监视 data中的数据不会立即执行,监视 properties中的数据会立即执行一次

9. 组件间通信与事件

9.1 父往子传值

父组件如果需要向子组件传递指定属性的数据,在 WXML 的子组件标签中,绑定要传递的属性值,并在子组件中使用 properties属性进行接收即可

父组件如果需要向子组件传递数据,只需要两个步骤:

1.在父组件 WXML 中使用 数据绑定 的方式向子组件传递动态数据

2.子组件内部使用 properties 接收父组件传递的数据即可

html
<!-- 引用组件的页面模板 -->
<view>
  <costom prop-a="{{ name }}" prop-b="{{ age }}" />
</view>

在组件内部,需要在 Component 构造器中通过 properties 接收传递的数据,接收方式有两种:

js
Component({
  // 生命接收父组件传递的数据
  properties: {
    propA: {
      type: String, // 传递的数据类型
      value: '' // 默认值
    },
    propB: Number // 简化的定义方式
  },
})

在子组件中也可以通过 this.setData()properties 中的数据进行修改,但是一般不建议修改(单向数据流)

复选框组件案例:

➡️ index.js

js
Page({
  data: {
    isChecked: true
  },
})

➡️ index.wxml

html
<custom-checkbox
  label="我已阅读并同意 用户协议 和 隐私协议"
  position="right"
+   checked="{{ isChecked }}">
  我已阅读并同意 用户协议 和 隐私协议 - 111
</custom-checkbox>

➡️ components/custom-checkbox/custom-checkbox.js

js
Component({
  properties: {
    // 复选框组件是公共组件,需要在多个页面中使用,不同页面的默认选中不同
    // 设置复选框默认未被选中,父组件可以传递属性控制复选框是否选中
+     checked: {
+       type: Boolean,
+       value: false
+     }
  },

  // 用来定义当前组件内部所需要使用的数据
  data: {
    // // 控制复选框是否选中
    isChecked: false,
  },

+   observers: {
+     // 如果需要将 properties 中的数据赋值给 data
+     // 可以使用 observers 进行处理
+     checked: function (newChecked) {
+       this.setData({
+         isChecked: newChecked
+       })
+     }
+   },
  }
})

➡️ components/custom-checkbox/custom-checkbox.wxml

html
<view class="custom-checkbox-cintainer">
  <!-- 通过动态类名设置文本和复选框的位置 -->
  <view class="custom-checkbox-box {{ position }}">
    <checkbox model:checked="{{ isChecked }}" bind:tap="updateChecked" class="custom-checkbox" /> // 使用 model:进行双向数据绑定
    <view class="contnet">
      <!-- 如果传递了label属性就不展示插槽 -->
      <text wx:if="{{ label !== ''}}">{{ label }}</text>
      <slot wx:else />
    </view>
  </view>
</view>

Tips:

使用 properties接收的数据可以直接在模板中使用,但不建议进行修改,如果需要将 properties中的数据赋值给 data中的属性可以使用 observers进行处理

9.2 子往父传值

子组件如果需要向父组件传递数据,可以通过小程序提供的事件系统实现传递传递,可以传递任意数据。

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

  1. 在父组件中使用 bind为自定义组件绑定自定义事件,并将事件回调留在父组件中
  2. 在自定义组件中使用 this.triggerEvent(事件名,数据)触发自定义事件,并传递数据
  3. 在父组件的事件回调中,使用 event.detail获取子组件传递的数据

父组件绑定事件:

html
// cart.wxml
<!-- 在父组件中为自定义组件绑定myEvent事件,用于获取子组件传递的数据 -->
<custom05 bind:myEvent="getData" />
js
// cart.js
Page({
  data: {
    num: "",
  },
  // 自定义事件的处理函数
  getData(event) {
    // 通过 event.detail 获取子组件传递的数据
    console.log(event.detail);
    // 将子组件传递的数据赋值到data中的属性
    this.setData({
      num: event.detail,
    });
  },
}

子组件中触发事件:

html
<button type="primary" plain bind:tap="sendData">传递数据</button>
js
// components/custom05/custom05.js
Component({
  data: {
    num: 66,
  },
  methods: {
    // 将数据传递给父组件
    sendData() {
      // 如果需要将数据传递给父组件,需要使用 triggerEvent 发射自定义事件,第二个参数是携带的参数
      this.triggerEvent("myEvent", this.data.num);
    },
  },
}

复选框组件案例:

➡️ components/custom-checkbox/custom-checkbox.js

js
Component({
  methods: {
    // 更新复选框的状态
    updateChecked () {
+       this.triggerEvent('changechecked', this.data.isChecked)
    }
  }
})

➡️ index.html

html
<custom-checkbox
  label="我已阅读并同意 用户协议 和 隐私协议"
  position="right"
  checked="{{ isChecked }}"
  class="getchild"
+  bind:changechecked="getData"
>
  我已阅读并同意 用户协议 和 隐私协议
</custom-checkbox>

➡️ index.js

js
Page({
  data: {
    isChecked: true
  },
  // 自定义事件处理函数
  getData(event) {
    console.log(event.detail ? "提交" : "请同意协议");
  },
})

9.3 获取组件实例

可在父组件里调用 this.selectComponent() ,获取子组件的实例对象,就可以直接拿到子组件的任意数据和方法。调用时需要传入一个匹配选择器 selector,如:this.selectComponent(".my-component")

html
<!-- 自定义组件-->
<costom bind:myevent="getData" class="custom" />
<button bindtap="getChildComponent"></button>
js
// 父组件
Page({
  data: {},
  getChildComponent: function () {
    const instance = this.selectComponent('.custom')
    console.log(instance.data)
  }
})

Tips:this.selectComponent()获取到子组价的实例对象(相当于子组价中的 this),可以通过 instance.data.xx instance.方法名 直接访问到组件中的 datamethods

复选框组件案例:

➡️ index.html

html
<custom-checkbox
  label="我已阅读并同意 用户协议 和 隐私协议"
  position="right"
  checked="{{ isChecked }}"
+  class="child"
+  id="child"
  bind:changechecked="getData"
>
  我已阅读并同意 用户协议 和 隐私协议 - 111
</custom-checkbox>
<button type="primary" plain bindtap="getChild">获取子组件实例对象</button>

➡️ index.js

js
Page({
  // 获取子组件的实例对象
  getChild () {
    // this.selectComponent 方法获取子组件实例对象
    // 获取到实例对象以后,就能获取子组件所有的数据、也能调用子组件的方法
    const res = this.selectComponent('#child')
    console.log(res.data.isChecked)
  }
})

10. 组件生命周期

组件的生命周期:指的是组件自身的一些钩子函数,这些函数在特定的时间节点时被自动触发

组件的生命周期函数需要在 lifetimes 字段内进行声明

最重要的生命周期是 created(创建) attached (挂载)detached (销毁)

定义段描述
created在组件实例刚刚被创建时执行,注意此时不能调用 setData (还没有对模板解析)
attached在组件实例进入页面节点树时执行 (模板已经解析完毕,并且挂载到页面上)
ready在组件布局完成后执行
moved在组件实例被移动到节点树另一个位置时执行
detached在组件实例被从页面节点树移除时执行 (组件被销毁了)
  1. 【组件实例刚刚被创建好时】, created 生命周期被触发。此时,组件数据 this.data 就是在 Component 构造器中定义的数据 data此时还不能调用 setData 通常情况下,这个生命周期只应该用于给组件 this 添加一些自定义属性字段。

  2. 【在组件完全初始化完毕】、进入页面节点树后, attached 生命周期被触发。此时, this.data 已被初始化为组件的当前值。这个生命周期很有用,绝大多数初始化工作可以在这个时机进行。

  3. 【在组件离开页面节点树后】, detached 生命周期被触发。退出一个页面时,如果组件还在页面节点树中,则 detached 会被触发。

示例:

js
Component({
  data: {
    name: "Tom",
  },
  // 组件生命周期声明对象,组件的生命周期:created、attached、ready、moved、detached
  lifetimes: {
    // created:在组件实例刚刚被创建时执行,注意此时不能调用 setData
    created() {
      console.log("created");
      // 无法对数据进行修改
      this.setData({
        name: "Jerry",
      });
      // 可以通过this的方式给组件添加一些自定义的属性
      this.test = "测试";
    },
    // attached:组件被初始化完毕,模板解析完成,已经把组件挂载到页面上
    attached() {
      console.log("attached");
      console.log(this.test);
      // 可以进行修改
      this.setData({
        name: "Jack",
      });
      // 一般页面中的交互会在此函数中进行实现
    },
    // detached:组件被销毁时触发执行
    detached() {
      console.log("detached");
    },
  },
});

注意:

​ 在 created钩子函数中不能调用 setData函数对数据进行修改

​ 组件生命周期需要在 lifeitems属性中进行声明定义

Tips:

​ 一般在 created钩子中为组件添加自定义属性,在 attached钩子中发起网络请求,进行一些页面的交互

11. 组件所在页面的生命周期

用于组件内部监听父组件的展示、隐藏状态,从而方便组件内部执行一些业务逻辑的处理

组件所在页面的生命周期有 4 个: show、 hide、 resize、 routeDone,需要在 pageLifetimes 字段内进行声明

js
Component({
  // 组件所在页面的生命周期
  pageLifetimes: {
    // 监听组件所在页面展示状态,页面显示/切入前台时触发
    show() {
      console.log("组件所在页面展示了");
    },
    // 监听组件所在页面隐藏状态,页面隐藏/切入后台时触发。
    hide() {
      console.log("组件所在页面隐藏了");
    },
  },
});

注意:组件所在页面的生命周期需要在 pageLifetimes属性中声明定义

12. 小程序生命周期总结

一个小程序完整的生命周期由 应用生命周期页面生命周期组件生命周期 三部分来组成

  1. 小程序冷启动,钩子函数执行的顺序
  1. 保留当前页面(navigate)进入新页面 以及 关闭当前页面(redirect)进入新页面,钩子函数的执行顺序

注意:使用 navigae跳转到非 tabbar页面后点击返回箭头返回 tabbar页面时,会销毁当前页面

  1. 从A页面进入B页面中,B页面中存在自定义组件,钩子函数执行顺序

    image-20240707181027203

  2. 切后台 以及 切前台(热启动)

应用生命周期 :app.js

js
App({
  // 当小程序冷启动,初始化完成时触发(只执行一次)
  onLaunch() {
    console.log("🥇小程序应用-onLaunch");
  },
  // 当小程序启动,或从后台进入前台显示时触发
  onShow() {
    console.log("🥇小程序应用-onShow");
  },
  // 当小程序从前台进入后台时触发
  onHide() {
    console.log("🥇小程序应用-onHide");
  },
});

页面生命周期: market.js

js
Page({
  // 页面加载时触发(只执行一次)
  onLoad(){
    console.log('🥈小程序页面-market-onLoad');
  },
  // 页面展示时触发
  onShow(){
    console.log("🥈小程序页面-market-onShow");
  },
  // 页面初次渲染完毕时触发(只执行一次)
  onReady(){
    console.log("🥈小程序页面-market-onReady");
  },
  // 页面隐藏式触发
  onHide(){
    console.log("🥈小程序页面-market-onHide");
  },
  // 小程序页面卸载时触发(只执行一次)
  onUnload(){
    console.log("🥈小程序页面-market-onUnload");
  }
})

组件生命周期:custom07.js

js
// components/custom07/custom07.js
Component({
  properties: {},
  data: {},
  lifetimes: {
    // 组件实例被创建时触发
    created() {
      console.log("🥉小程序组件-create");
    },
    // 组件挂载到页面上时触发
    attached() {
      console.log("🥉小程序组件-attached");
    },
    // 组件布局完成后执行
    ready() {
      console.log("🥉小程序组件-ready");
    },
    // 组件销毁时执行
    detached() {
      console.log("🥉小程序组件-deached");
    },
  },
});

注意:

  1. 当页面销毁时小程序中的组件也会被销毁
  2. 当页面隐藏时小程序中的组件不会被销毁

13. 拓展:使用 Component 构造页面

Component 方法用于创建自定义组件,小程序的页面也可以视为自定义组件,因此页面也可以使用 Component 方法进行创建,从而实现复杂的页面逻辑开发

📌 注意事项:

  1. 要求对应 .json 文件中包含 usingComponents 定义段

  2. 页面使用 Component 构造器创建,需要定义与普通组件一样的字段与实例方法

  3. 页面中 Page 方法中的钩子函数和事件监听函数必须放在 methods 方法中

  4. 组件的属性 Properties 可以用于接收页面的参数,在 onLoad(options) 中可以通过 options 拿到对应的页面参数

示例:

js
Component({

  properties: {
    id: String,
    title: String,
  },

  data: {
    name: "Tom",
  },
  // 更新name 方法只能写在 methods 中
  // updateName() {
  //   this.setData({
  //     name: "Jack",
  //   });
  // },
  methods: {
    updateName() {
      this.setData({
        name: "Jack",
      });
    },
    onLoad(options) {
      console.log("详情页面 onLoad");
      console.log(options); // 可以接收到页面参数
      console.log(this.data.id); // 将页面参数存储到properties中
      console.log(this.data.title);
      console.log(this.properties.id);
    },
  },
});

Tips:

​ 为什么需要使用 Component 方法构造组件?

​ Component 方法功能比 Page 方法强大很多,如果使用 Component 方法构造页面可以实现更加复杂的页面逻辑开发(如在 Component 方法中有数据监听器)

14. 拓展:behaviors

小程序的 behaviors 方法是一种代码复用的方式,可以将一些通用的逻辑和方法提取出来,然后在多个组件中复用,从而减少代码冗余,提高代码的可维护性。

如果需要 behavior 复用代码,需要使用 Behavior() 方法,每个 behavior 可以包含一组属性、数据、生命周期函数和方法,组件引用它时,它的属性、数据和方法会被合并到组件中,生命周期函数也会在对应时机被调用。

注册 behavior:

如果需要注册一个 behavior,需要借助 Behavior() 方法,接受一个 Object 类型的参数

js
export default Behavior({
  properties: {
    label: {
      type: String,
      value: "我已同意该协议",
    },
  },
  data: {
    name: "Tom",
    obj: {
      name: "Tyke",
      sex: "男",
    },
  },
  methods: {
    updateName() {
      this.setData({
        name: "Jerry",
      });
      console.log("我是behavior中的方法");
    },
  },
  lifetimes: {
    attached() {
      console.log("我是behavior的生命周期函数 attached -------");
    },
  },
});

使用 behavior:

js
// 1.导入behavior
import behavior from "./behavior.js";
Component({
  // 2.注册使用
  behaviors: [behavior],
});

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

  1. 如果有同名的 propertiesmethods,组件会覆盖 behavior 中的同名属性或方法
  2. 如果data中存在相同的基本类型的数据,组件内部会覆盖behavior
  3. 如果有同名的对象类型的数据,会进行合并,有相同子属性,组件内部会覆盖behavior
  4. 生命周期函数和 observers 不会相互覆盖,会是在对应触发时机被逐个调用,也就是都会被执行,会先执行 behavior中的生命周期函数

15. 拓展:外部样式类

默认情况下,组件和组件使用者之间如果存在相同的类名不会相互影响,组件使用者如果想修改组件的样式,需要就解除样式隔离,但是解除样式隔离以后,在极端情况下,会产生样式冲突、CSS 嵌套太深等问题,从而给我们的开发带来一定的麻烦,外部样式类可以解决这个问题。

外部样式类:在使用组件时,组件使用者可以给组件传入 CSS 类名,通过传入的类名修改组件的样式。

如果需要使用外部样式类修改组件的样式,在 Component 中需要用 externalClasses 声明接收父组件传递的类名

外部样式类的使用步骤:

  1. 在组件使用者中为自定义组件传递类名,此类名对应的样式写在父组件的 wxss

    html
    <custom09 属性名="类名"/>
  2. 在自定义组件中的 Component中使用 externalClasses接收父组件传递的类名

    js
    externalClasses: ["属性名"],
  3. 将接收到的样式类用于自定义组件内部

    html
    <view class="属性名">通过外部样式类修改组件的样式</view>

📌注意事项:

​ 在同一个节点上使用普通样式类和外部样式类时,两个类的优先级是未定义的,因此需要为外部样式类中的样式添加 !important 以保证外部样式类的优先级

示例:

➡️ profile.wxml

html
<!-- 传递类名 属性名="类名" -->
<custom09 extend-class="my-class"/>

➡️ profile.scss

scss
.my-class {
  color: hotpink !important; // 增加权重
}

➡️ custom09.js

js
Component({
  // 接收组件使用者传递的外部样式类
  externalClasses: ["extend-class"],
});

➡️ custom09.wxml

html
<!-- 使用父组件传递的外部样式类 -->
<!-- 注意:在同一个节点上,如果存在外部样式类和普通样式类,两个类的优先级是未定义的,在使用外部样式类时样式需要通过 !important 添加权重 -->
<view class="extend-class box">通过外部样式类修改组件的样式</view>

➡️ custom09.wxss

scss
.box{ 
  color: lightblue;// 样式外部样式类覆盖
  font-size: 60rpx;// 样式生效
}

注意:如果设置 styleIsolation: "shared",则externalClasses属性选项会失效,一般使用 externalClasses替代 styleIsolation: "shared"实现父组件控制子组件样式

16. 完善复选框案例并总结自定义组件

总结自定义组件:

  1. 组件基本使用:数据、属性、方法、插槽

  2. 组件样式使用:组件样式、注意事项、样式隔离、外部样式类

  3. 组件通信传值:父往子传值、子往父传值、获取组件实例

  4. 组件生命周期:组件的生命周期、组件所在页面的生命周期、总结了小程序全部的生命周期

  5. 组件数据监听器:observers

  6. 组件拓展:使用 Component 构造页面、组件复用机制 behaviors 等

完善复选框案例

➡️ components/custom-checkbox/custom-checkbox.wxml

html
<!--components/custom-checkbox/custom-checkbox.wxml-->
<!-- <text>我是自定义组件</text> -->

<view class="custom-checkbox-container">
  <view class="custom-checkbox-box {{ position === 'right' ? 'right' : 'left' }}">
+    <label class="custom-label">
      <checkbox class="custom-checkbox" checked="{{ isChecked }}" bindtap="updateChecked" />

      <view class="content">
        <!-- lable 和 子节点内容都进行了展示 -->
        <!-- 要么展示 lable 要么展示 子节点内容 -->
        <!-- 如果用户传递了 lable 属性,就展示 lable -->
        <!-- 如果用户没有传递 lable 属性,就展示 子节点内容 -->
        <text wx:if="{{ label !== '' }}">{{ label }}</text>

        <slot wx:else />
      </view>
+    </label>
  </view>
</view>

➡️ components/custom-checkbox/custom-checkbox.wxss

scss
+ .custom-checkbox-box .custom-label {
  display: flex;
  align-items: center;
}

Tips:

​ 使用 label标签包裹复选框和提示文字,可以实现点击提示文字勾选/取消复选框

修改复选框的样式,放弃原有的在组件内部修改样式和在组件中设置 styleIsolation: "shared"属性

app.scss 修改整个应用的复选框样式

js
// 自定义复选框样式:
.wx-checkbox-input {
  width: 24rpx !important;
  height: 24rpx !important;
  border-radius: 50% !important;
  border: 1px solid #fda007 !important;
  margin-top: -6rpx !important;
}
// 复选框选中时的样式
.wx-checkbox-input-checked {
  background-color: #fda007 !important;
}
// 复选框选中时 √ 的样式
.wx-checkbox-input.wx-checkbox-input-checked:before {
  font-size: 22rpx !important;
  color: #ffffff !important;
}

index.scss 再次自定义 index 页面中复选框的样式

css
// 组件使用者设置组件的样式
.wx-checkbox-input {
  border: 1px solid lightseagreen !important;
}
.wx-checkbox-input-checked {
  background-color: lightskyblue !important;
}

九、npm 支持与VantWeapp使用

1. 构建 npm

目前小程序已经支持使用 npm 安装第三方包,但是这些 npm 包在小程序中不能够直接使用,必须得使用小程序开发者工具进行构建后才可以使用。

为什么需要使用小程序开发者工具构建呢❓

因为 node_modules 目录下的包,不会参与小程序项目的编译、上传和打包,因此在小程序项目中要想使用 npm 包,必须走一遍 构建 npm 的过程。

在构建成功以后,默认会在小程序项目根目录,也就是 node_modules 同级目录下生成 miniprogram_npm目录,里面存放这构建打包后的 npm 包,也就是小程序运行过程中真正使用的包

微信开发者工具如何构建❓

我们以使用 Vant Weapp 小程序 UI 组件库为例,来说明小程序如何安装和构建 npm,构建 npm 的步骤如下:

  1. 初始化 package.json
  2. 通过 npm 安装项目依赖
  3. 通过微信开发者工具构建 npm

📌 注意事项

  1. 小程序运行在微信内部,因为运行环境的特殊性,这就导致并不是所有的包都能够在小程序使用

  2. 我们在小程序中提到的包指专为小程序定制的 npm 包,简称小程序 npm 包,在使用包前需要先确定该包是否支持小程序

  3. 开发者如果需要发布小程序包,需要参考官方规范:https://developers.weixin.qq.com/miniprogram/dev/devtools/npm.html#发布-npm-包

构建的详细步骤:

  1. 初始化 package.json

    shell
    npm init -y
  2. 通过 npm 安装 @vant/weappVant Weapp官网

    shell
    npm i @vant/weapp -S --production

  3. 构建 npm

  4. 修改 app.json

    到这一步 npm 的构建已经完成了,但是 Vant 组件库会和基础组件的样式冲突,因此我们需要继续往下配置

    app.json 中的 "style": "v2" 去除,小程序的新版基础组件强行加上了许多样式,难以覆盖,不关闭将造成部分组件样式混乱

  5. 在页面中使用 vant 提供的小程序组件,这里以 Button 按钮组件为例

    • app.jsonindex.json中引入组件
    • app.json 中注册的组件为全局注册,可以在任意组件中进行使用
    • index.json 中注册组件为组件内部组件,只能在当前组件中进行使用
    • 按照组件提供的使用方式,在页面中使用即可
    json
    "usingComponents": {
      "van-button": "@vant/weapp/button/index"
    }
    html
    <van-button type="default">默认按钮</van-button>
    <van-button type="primary">主要按钮</van-button>
    <van-button type="info">信息按钮</van-button>
    <van-button type="warning">警告按钮</van-button>
    <van-button type="danger">危险按钮</van-button>
  6. 页面预览效果

2. 自定义构建 npm

在实际的开发中,随着项目的功能越来越多、项目越来越复杂,文件目录也变的很繁琐,为了方便进行项目的开发,开发人员通常会对目录结构进行调整优化,将小程序核心源码放到 miniprogramsrc 目录下

image-20240620205251712

但是在调整目录以后,进行构建,发现没有构建成功,并且弹出构建失败的弹框

产生错误原因:

小程序的构建方式有两种:

  1. 默认构建 npm

    默认情况下,miniprogramRoot 是小程序项目根目录,执行 npm install 之后,在项目的根目录下就有 node_modules 文件夹,然后对 node_modules 中进行构建 npm,其构建 npm 的结果是在项目根目录下生成 miniprogram_npm存放构建后的依赖

  2. 自定义构建 npm

    由于开发者修改了项目的根目录,因此需要在 project.config.json 中指定小程序的根目录位置,node_modules 的位置和目标 miniprogram_npm 的位置

解决方法:

project.config.json中配置如下字段:

  1. 新增 miniprogramRoot 字段,指定调整后了的小程序开发目录
  2. 新增 setting.packNpmManually设置为 true,开启指定node_modules 的位置以及构建成功后文件的位置
  3. 新增 setting.packNpmRelationList 项,指定 packageJsonPathminiprogramNpmDistDir 的位置
    • packageJsonPath 表示 node_modules 源对应的 package.json
    • miniprogramNpmDistDir 表示 node_modules 的构建结果目标位置
json
{
+ "miniprogramRoot": "miniprogram/", // 指定小程序根目录
  "setting": {
    // 开启自定义 node_modules 和 miniprogram_npm 位置的构建 npm 方式
+   "packNpmManually": true, 
    // 指定 packageJsonPath 和 miniprogramNpmDistDir 的位置
+   "packNpmRelationList": [
+    {
+    "packageJsonPath": "./package.json", // 指定package.json的位置
+    "miniprogramNpmDistDir": "./miniprogram" // 指定输出nimiprogram_npm的位置
+    }
+   ]
  }
}

Tips:

​ 一般在项目开发时需要更改项目的目录结构,将核心源码放在 miniprogramsrc文件夹下,此时就需要在 project.config.json,中配置项目的根目录和自定义构建npm

3. Vant 组件的使用方式

Vant Weapp 是有赞前端团队开源的小程序 UI 组件库,基于微信小程序的自定义组件开发,可用来快速搭建小程序项目。

在使用 Vant 提供的组件时,只需要两个步骤:

  1. 将组件在 app.json 中进行全局注册 或者 index.json 中进行局部注册

  2. 在引入组件后,可以在 wxml 中直接使用组件

image 组件为例,学习 Vant 组件库的基本使用方式

  1. 将组件进行引入,这里我们进行全局引入
json
// app.json
"usingComponents": {
  "van-image": "@vant/weapp/image/index"
}
  1. 在 wxml 中直接使用组件
html
<van-image width="100" height="100" src="/assets/banner/banner-1.png"  />
<!-- 在使用 van-image 图片组件时,如果需要渲染本地的图片不能使用 ../的方式,需要使用相对于小程序根目录来查找图片 -->
  1. 如果我们想给 van-image 添加一些属性,这时候我们需要查看 API 手册
html
<van-image
  round
  width="100px"
  height="10px"
  src="https://img.yzcdn.cn/vant/cat.jpeg"
/>
  1. 如果我们想给 van-image 添加一些事件,这时候我们需要查看 事件 手册
html
<van-image
  width="100"
  height="100"
  round
  src="/assets/Jerry.png"
  bind:click="imageHandler" // 点击图片触发的事件
/>
js
Page({
  imageHandler () {
    console.log('点击图片时触发点击事件,执行该事件处理函数~~~~')
  }
}
  1. 如果我们想给 van-image 添加一些插槽,这时候我们需要查看 slot 手册
html
<van-image
  width="100"
  height="100"
  round
+  use-loading-slot
+  use-error-slot
  src="/assets/Jerry.png"
  bind:click="imageHandler"
>
+   <van-loading slot="loading" type="spinner" size="20" vertical /> // 自定义加载中的提示内容
+   <text slot="error">加载失败</text> // 自定义加载失败时的提示内容
</van-image>
  1. 如果我们想给 van-image 添加一些外部样式类,这时候我们需要查看 外部样式类 手册
html
<van-image
  width="100"
  height="100"
  round
  use-loading-slot
  use-error-slot
+ custom-class="custom-class"
  src="/assets/Jerry.png"
  bind:click="imageHandler"
>
</van-image>
scss
/* pages/index/index.wxss */
.custom-class {
  border: 10rpx solid lightseagreen !important; // 提升样式权重
}

注意:

​ 在使用 Vant Weapp时需要将 app.json中的 "style":"v2"去除,避免造成样式混乱

​ 可以通过组件提供的外部样式类,来覆盖组件的样式,注意要为样式添加 !important增加权重

4. Vant 组件的样式覆盖

Vant Weapp 基于微信小程序的机制,为开发者提供了以下 3 种修改组件样式的方法

  1. 解除样式隔离(通过类名进行修改):在页面中使用 Vant Weapp 组件时,可直接在页面的样式文件中覆盖样式,默认解除了样式隔离,在自定义组件中想要覆盖时需要手动解除样式隔离(推荐)
  2. 使用外部样式类:需要注意普通样式类和外部样式类的优先级是未定义的,需要添加 !important 保证外部样式类的优先级(推荐)
  3. 使用 CSS 变量:在页面或全局对多个组件的样式做批量修改以进行主题样式的定制

第 1 种:解除样式隔离(通过类名进行修改)

Vant Weapp 的所有组件都开启了addGlobalClass: true接受外部样式的影响(注意:组件与页面之间存在样式隔离),因此我们可以通过审核元素的方式获取当前元素的类名,然后复制到组件的 .wxss 中进行修改

UI 组件添加一个类名(只修改该组件样式),并通过检查元素获取到需要修改部分的类名进行修改

修改日历组件确定按钮圆形样式(并保证其他圆形按钮组件样式不受影响): image-20241014201559974image-20241014201159108

index.wxml

html
  <van-button round type="info">确定</van-button>
  <van-calendar class="calender" show="{{ show }}" bind:close="onClose" bind:confirm="onConfirm" title="生日" color="#07c160">
  </van-calendar>

index.scss

scss
.calender { // 修改指定类名下的圆形按钮样式,其他圆形按钮样式不变
  .van-button--round {
    background-color: #948383 !important;
    color: #879303 !important;
  }
}

注意:解除样式隔离,样式会覆盖拥有该类名全部的 UI组件,使用时在需要修改的 UI组件上添加一个自定义类名,实现只覆盖该组件

第 2 种:使用外部样式类

Vant Weapp 开放了大量的外部样式类供开发者使用,具体的样式类名称可查阅对应组件的 “外部样式类” 部分。

需要注意的是普通样式类和外部样式类的优先级是未定义的,因此使用时请添加!important以保证外部样式类的优先级。

注意:推荐使用,可以单独为组件设置外部样式类,在添加外部样式类时,要为样式添加权重 !important 避免样式不生效

第 3 种:使用 CSS 变量

使用场景:如果需要在多个页面或多个组件中批量修改组件,定制主题

CSS 的变量基础用法如下:

  1. 声明一个全局CSS变量,属性名需要以两个减号(--)开始,属性值则可以是任何有效的 CSS 值
css
/* app.wxss */
/* 声明全局的变量,可在项目中任意组件中使用 */
page {
  --main-bg-color: lightcoral;
}
  1. 使用全局CSS变量 用 var()
scss
.van-button--primary {
  font-size: 28rpx !important;
  background-color: var(--color) !important;
  border: 1px solid var(--color) !important;
}
  1. 声明局部CSS变量并使用
css
.my-button {
  /* 声明局部的变量 */
  /* 只有被当前类名容器包裹住的元素,使用该变量才生效 */
  --color: lightgreen;
  .van-button--primary {
    font-size: 28rpx !important;
    /* 使用一个局部变量时用 var() 函数一个合法的属性值 局部变量会覆盖全局变量*/
    background-color: var(--color) !important;
    border: 1px solid var(--color) !important;
  }
}
  1. 页面中使用该变量
html
<view class="container">
  <van-button
    type="default"
    custom-class="custom-class"
  >
    默认按钮
  </van-button>
</view>

<van-button
  type="default"
  custom-class="custom-class"
>
  默认按钮
</van-button>

![](http://8.131.91.46:6677/mina/base/CSS 变量修改演示.jpg)

也可以在按钮身上添加类名:

html
<!-- 使用 CSS 变量:如果需要再多个页面或者一个组件中 需要批量修改组件、定制主题 -->
<van-button type="primary" class="my-button">主要按钮</van-button>
scss
.my-button {
	--color: rgb(221, 152, 24);
	.van-button--primary {
  	  font-size: 28rpx !important;
 	  background-color: var(--color) !important;
 	  border: 1px solid var(--color) !important;
	}
}

十、分包加载

1. 什么是分包加载

什么是分包加载

小程序的代码通常是由许多页面、组件以及资源等组成,随着小程序功能的增加,代码量也会逐渐增加,体积过大就会导致用户打开速度变慢,影响用户的使用体验。

分包加载是一种小程序优化技术。

将小程序不同功能的代码,分别打包成不同的子包,在构建时打包成不同的分包,用户在使用时按需进行加载,在构建小程序分包项目时,构建会输出一个或多个分包。每个使用分包小程序必定含有一个主包。每个分包可以包含多个页面、组件、样式和逻辑等。当小程序需要使用某个分包时,才会加载该分包中的代码。

**主包:**包含默认启动页面 / TabBar 页面 以及 所有分包都需用到公共资源的包

**分包:**根据开发者的配置进行划分出来的子包

小程序分包后如何加载

在小程序启动时,默认会下载主包并启动主包内页面,在用户访问分包内某个页面时,微信客户端才会把对应分包下载下来,下载完成后再进行展示。

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

  1. 整个小程序所有分包大小不超过 20MB
  2. 单个分包/主包大小不能超过 2MB

📌 注意事项

​ 整个小程序所有分包大小可能会随时调整,截止到目前整个小程序所有分包大小不超过 20M

2.分包的基本使用

在进行分包加载之前,需要对小程序的业务逻辑进行分析,将代码划分成多个模块。每个模块应该有一个明确的功能,并与其他模块之间有明确的依赖关系

需要按照功能拆分分包,并且每个分包都需要与其他包有依赖关系(可以通过 a 分包跳转到 b 分包)

在项目根目录 miniprogramsrc下创建 modules文件夹用于存储分包文件

开发者在小程序的配置文件 app.json 中,通过 subPackages 或者 subpackages字段声明项目分包结构

每个分包需要指定 root 字段、name 字段和 pages 字段

  1. root 字段指定了分包的根目录,该目录下的所有文件都会被打包成一个独立的包
  2. name 字段为分包的名称,用于在代码中引用该分包
  3. pages 字段指定了该分包中包含的页面,可以使用通配符 * 匹配多个页面

image-20240621120934602

示例:

json
// app.json
{
  ...
  "subPackages": [
    {
      "root": "modules/goodModule", // 当前分包的根目录
      "name": "goodModule", // 分包的别名
      "pages": ["pages/list/list", "pages/detail/detail"] // 当前分包中包含的页面
    }
  ]
}
html
// index.wxml
<!-- 注意:如果需要跳转到分包页面,需要在路径之前添加上分包的根目录路径 -->
<navigator url="/modules/goodModule/pages/list/list">跳转到商品列表页面</navigator>
<navigator url="/modules/goodModule/pages/detail/detail">跳转到商品详情页面</navigator>

注意:如果需要跳转到分包页面,需要在路径之前添加上分包的根目录路径(使用 搜索替换 功能)

Tips:分包页面不需要在 app.jsonpages中配置,但需要在 subpackagespages中配置

3. 打包和引用原则(注意事项)

打包原则:

  1. tabBar 页面必须在主包内

  2. 最外层的 pages 字段,属于主包的包含的页面

  3. subpackages 配置路径进行打包,配置路径外的目录将被打包到主包中

  4. 分包之间不能相互嵌套,subpackage 的根目录不能是另外一个 subpackage 内的子目录

引用原则:

  1. 主包不可以引用分包的资源,但分包可以使用主包的公共资源(会先加载主包,在需要的时候加载分包)

  2. 分包与分包之间资源无法相互引用, 分包异步化时不受此条限制

4. 独立分包的配置

什么是独立分包

独立分包:独立分包是小程序中一种特殊类型的分包,可以独立于主包和其他分包运行

从独立分包中页面进入小程序时,不需要下载主包,但是当用户进入普通分包或主包内页面时,主包才会被下载 !

开发者可以将功能相对独立的页面配置到独立分包中,因为独立分包不依赖主包即可运行,可以很大程度上提升独立分包页面的启动速度

  • 如果是独立分包,不需要下载主包,直接就能够访问,独立分包是自己独立运行的

  • 而如果是其他分包,需要先下载主包,通过路径访问,才能加载对应路径的分包

📌 注意事项:

  1. 独立分包中不能依赖主包和其他分包中的资源
  2. 主包中的 app.wxss 对独立分包无效
  3. App 只能在主包内定义,独立分包中不能定义 App,会造成无法预期的行为
  4. 独立分包不可以配置预下载主包

如何配置独立分包:

开发者在app.json中找到需要配置为独立分包的subpackages字段

在该字段配置项中定义independent字段声明对应分包为独立分包。

示例:

json
{
  "subPackages": [
    {
      "root": "modules/marketModule",
      "name": "marketModule",
      "pages": [
        "pages/market/market"
      ],
+     "independent": true
    }
  ]
}

从独立分包进入小程序

优点:独立分包不依赖于主包和其他分包(不需要下载主包),可以单独运行打开速度快,可以通过独立分包进入主包(进入tabBar页面时再加载主包)

Tips:可以通过配置 app.json中的 entryPagePath 设置小程序的入口页面为独立分包页面

json
{
  // 将登录页作为小程序的入口页面
  "entryPagePath": "modules/loginModules/pages/login/login",
  // 注册全局组件
  "usingComponents": {
    "custom-swiper": "./components/custom-swiper/custom-swiper",
    .....
  },
  // tab页面
  "pages": [
    "pages/index/index",
    "pages/about/about",
    "pages/my/my",
    "pages/cart/cart"
  ],
  // 全局配置
  "window": {
    "navigationBarTitleText": "第一个小程序",
    ....
  },
  // tabBar配置
  "tabBar": {
    "color": "#878980",
    .....
  },
  // 分包配置
  "subpackages": [
    // 商品模块分包
    {
      "root": "modules/goodsModules", // 分包根路径
      "name": "goodsModules", // 分包名
      "pages": ["pages/detail/detail", "pages/goods/goods"] // 分包中包含的页面
    },
    {
      "root": "modules/loginModules",
      "name": "loginModules",
      "pages": ["pages/login/login"],
      "independent": true  // 登录独立分包
    }
  ],
   // 分包预下载规则
  "preloadRule": {
    "pages/index/index": {
      "network": "all",
      "packages": ["goodsModules"]
    }
  },
  .....
}

modules\loginModules\pages\login\login.wxml

html
<view>
  <button type="primary" bind:tap="goIndex">登录</button>
</view>

modules\loginModules\pages\login\login.js

js
Page({
  goIndex() {
    wx.switchTab({ // 跳转到tabBar页面
      url: "/pages/index/index",
    });
  },

5. 分包预下载

分包预下载是指访问小程序某个页面时,预先下载分包中的代码和资源,以提高用户的使用体验。

当用户需要访问分包中的页面时,已经预先下载的代码和资源可以直接使用,通过分包预下载加快了页面的加载速度和显示速度

小程序的分包预下载需要在 app.json 中通过 preloadRule 字段设置预下载规则。preloadRule 是一个对象,对象的 key 表示访问哪个路径时进行预加载,value 是进入此页面的预下载配置,具有两个配置项:

字段类型必填默认值说明
packagesStringArray预下载的分包名称,进入页面后预下载分包的 rootname
__APP__ 表示主包。
networkStringwifi在指定网络下预下载,
可选值为: all: 不限网络 wifi: 仅wifi下预下载

示例:

json
{
  ....
  "subPackages": [  // 设置分包
    {
      "root": "modules/goodModule", // 分包路径
      "name": "goodModule", // 分包名称
      "pages": ["pages/list/list", "pages/detail/detail"] // 当前分包中存在的页面
    },
    {
      "root": "modules/marketModule",
      "name": "marketModule", 
      "pages": ["pages/market/market"],
      "independent": true  // 独立分包
    }
  ],
  "preloadRule": {  // 配置分包预下载 
    "pages/index/index": { // 当访问当前路径时进行分包预下载
      "network": "all", // 允许在所有网络下进行预下载
      "packages": ["goodModule"]  // 预下载分包的name/path
    },
    "modules/marketModule/pages/market/market": {
      "network": "all",
      "packages": ["__APP__"] // 下载主包
    }
  }
}

十一、开放能力

1. 获取用户头像

当小程序需要让用户完善个人资料时,我们可以通过微信提供的头像、昵称填写能力快速完善。如图:

想使用微信提供的头像填写能力,需要两步:

  1. button 组件 open-type 的值设置为 chooseAvatar
  2. 当用户选择需要使用的头像之后,可以通过 bindchooseavatar 事件回调获取到头像信息的临时路径
html
<!-- 给 button 添加 open-type 属性,值为 chooseAvatar -->
<!-- 绑定 bindchooseavatar 事件获取回调信息 -->
<button open-type="chooseAvatar" bindchooseavatar="getAvatar">
  按钮
</button>

示例:

html
<view>
<!-- 步骤:
      1.为按钮设置 open-type="chooseAvatar" 
      2.绑定chooseavatar事件 
      3.在事件回调中获取头像路径 -->
  <button class="btn" open-type="chooseAvatar" bindchooseavatar="chooseAvatar">
    <image class="avator" src="" data-missing="{{ avatorUrl }}" mode="" />
  </button>
</view>

index.scss

.btn{
  background-color: transparent;
  &::after{
    border: none;
  }
  .avator{
    width: 200rpx;
    height: 200rpx;
    border-radius: 50%;
  }
}

index.js

js
Page({
  data: {
    avatarUrl: '/assets/tom.png'  // 存储用户头像信息
  },
  // 获取用户头像信息
  getAvatar(e) {
    // 获取临时微信头像路径
    const { avatarUrl } = e.detail
    // 将获取到的头像赋值给 data 中变量同步给页面结构
    this.setData({
      avatarUrl
    })
  }
}

注意:目前获取的微信头像是临时的,有失效时间,在实际开发中需要将临时路径上传到公司服务器中

2. 获取用户昵称

当小程序需要让用户完善个人资料时,我们可以通过微信提供的头像、昵称填写能力快速完善。如图:

想使用微信提供的昵称填写能力,需要三步:

  1. 通过 form 组件中包裹住 input 以及 form-typesubmitbutton 组件

  2. 需要将 input 组件 type 的值设置为 nickname,当用户输入框输入时,键盘上方会展示微信昵称(主要)

  3. form 绑定 submit 事件,在事件处理函数中通过事件对象获取用户昵称

示例:

点击按钮获取用户昵称

html
<!-- 需要使用 form 组件包裹住 input 以及 button 组件 -->
<form bindsubmit="onSubmit">
  <!-- input 输入框组件的 type 属性设置为 nickname,用户点击输入框,键盘上方才会显示微信昵称 -->
  <!-- 如果添加了 name 属性,form 组件就会自动收集带有 name 属性的表单元素的值 -->
  <input type="nickname" name="nickname" placeholder="请输入昵称" />
  <!-- 如果将 form-type="submit" ,就将按钮变为提交按钮 -->
  <!-- 在点击提交按钮的时候,会触发 表单的 bindsubmit 提交事件 -->
  <button type="primary" plain form-type="submit">点击获取昵称</button>
</form>
js
Page({
  // 获取微信昵称
  onSubmit(event) {
    const { nickname } = event.detail.value;
    // 将昵称发送到后端进行存储
    console.log(nickname);
  },
}

使用双向数据绑定获取用户昵称

html
<input type="nickname" model:value="{{nickname}}" placeholder="请输入昵称"/>
// 注意:使用 vant提供的field组件时点击用户名无法收集到数据,需要使用其提供的插槽来实现昵称的收集
<van-field model:value="{{nickName}}" label="用户名" placeholder="请输入用户名" type="nickname" border="{{ false }}"/> // 选择昵称后nickName无法收集到昵称数据
// 解决办法
<van-field model:value="{{nickName}}" label="用户名" placeholder="请输入用户名" type="nickname" border="{{ false }}">
  <input slot="input" type="nickname" model:value="{{nickName}}" placeholder="请输入昵称" />
</van-field>

Tips:

input 组件 type 的值设置为 nickname,当用户在此input进行输入时,键盘上方会展示微信昵称。可以使用 model:value 数据双向绑定获取到用户选择的微信昵称

3. 转发功能

转发功能,主要帮助用户更流畅地与好友分享内容和服务

想实现转发功能,有两种方式:

  1. 页面.js 文件中声明 onShareAppMessage 事件监听函数,并自定义转发内容。只有定义了此事件处理函数,右上角菜单才会显示“转发”按钮

  2. 通过给 button 组件设置属性 open-type=“share“ 实现转发功能。在用户点击按钮后会触发 Page.onShareAppMessage 事件监听函数

image-20240623125315312

官方文档 onShareAppMessage:

示例:

html
<!--pages/index/index.wxml-->
<button open-type="share">转发</button>

index.js

js
Page({
  // 监听页面中按钮的转发以及右上角转发按钮的转发,只有声明了此函数才可以使用页面右上角转发功能
  onShareAppMessage (obj) {
    // console.log(obj)
    // 自定义转发内容
    return {
      // 转发标题
      title: '这是一个非常神奇的页面~~~',
      // 转发路径
      path: '/pages/cate/cate',
      // 自定义转发是显示的图片路径,可以是本地文件路径、代码包文件路径或者网络图片路径
      imageUrl: '../../assets/Jerry.png'
    }
  }
})

注意:

  1. 只有页面的 js 文件中设置了 onShareAppMessage事件监听函数才可以使用页面右上角的转发功能
  2. 为按钮设置属性 open-type="share"可实现点击按钮进行转发,同时也会触发 onShareAppMessage事件回调
  3. 可以在 onShareAppMessage事件回调中自定义转发的标题的封面图

4. 分享到朋友圈

小程序页面默认不能被分享到朋友圈,开发者需主动设置“分享到朋友圈”才可以,实现分享到朋友圈需满足两个条件:

  1. 页面 必须 设置允许“发送给朋友”,页面 js 文件声明 onShareAppMessage 事件监听函数

  2. 页面 必须 需设置允许“分享到朋友圈”,页面 js 文件声明 onShareTimeline 事件监听函数

官方文档 onShareTimeline:

示例:

js
Page({
  // 监听右上角 分享到朋友圈 按钮
  onShareTimeline () {
    // 自定义分享内容。
    return {
      // 自定义标题,即朋友圈列表页上显示的标题
      title: '帮我砍一刀~~~',
      // 自定义页面路径中携带的参数,如 path?a=1&b=2 的 【 “?” 后面部分 】
      query: 'id=1',
      // 自定义图片路径,可以是本地文件或者网络图片
      imageUrl: '../../assets/Jerry.png'
    }
  }
})

5. 手机号验证组件

手机验证组件,用于帮助开发者向用户发起手机号申请,必须经过用户同意后,才能获得由平台验证后的手机号,进而为用户提供相应服务

  1. 手机号快速验证组件:平台会对号码进行验证,但不保证是实时验证

    html
    <button open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber">
  2. 手机号实时验证组件:在每次请求时,平台均会对用户选择的手机号进行实时验证

    html
    <button
      open-type="getRealtimePhoneNumber"
      bindgetrealtimephonenumber="getrealtimephonenumber"
    />

📌注意事项:

  1. 目前该接口针对非个人开发者,且完成了认证的小程序开放(不包含海外主体)

  2. 两种验证组件需要付费使用,每个小程序账号将有 1000 次体验额度

    其他要求和注意事项,参考文档:手机号快速验证组件手机号实时验证组件

示例:

html
<!--pages/cart/cart.wxml-->
<button
  type="primary"
  plain
  open-type="getPhoneNumber"
  bindgetphonenumber="getphonenumber"
>快速验证组件</button>

<button
  type="warn"
  plain
  open-type="getRealtimePhoneNumber"
  bindgetrealtimephonenumber="getrealtimephonenumber"
>实时验证组件</button>
js
Page({
  // 手机号快速验证
  getphonenumber (event) {
    console.log(event)
  },
  // 手机号实时验证
  getrealtimephonenumber (event) {
    console.log(event)
  }
 // 在 event.detail 中可以获取到 code
 // code:动态令牌,可以使用 code 换取用户的手机号。需要将 code 发送给后端,后端在接收到 code 以后也需要调用 API,换取用户的真正手机号。在换取成功以后 ,会将手机号返回给前端
})

注意:

​ 使用手机号验证组件可以在事件回调中获取到code,通过 code再去获取用户真实手机号

6. 客服能力

小程序为开发者提供了客服能力,同时为客服人员提供移动端、网页端客服工作台便于及时处理消息

使用方式:

  1. 需要将 button 组件 open-type 的值设置为 contact,当用户点击后就会进入客服会话

    html
    <button type="warn" plain open-type="contact">联系客服</button>
  2. 在微信公众后台,绑定后的客服账号,可以登陆 网页端客服移动端小程序 客服接收、发送客服消息

十二、补充与扩展

1. 框架接口-getApp

getApp() 用于获取小程序全局唯一的 App 实例,因此可以在 App()方法中添加全局共享的数据、方法,通过小程序应用实例可实现数据或方法的共享(全局共享数据非响应式,不会引起页面更新,且不能直接在 wxml中直接访问)

示例:

➡️ app.js

js
App({
  // 全局共享的数据
  globalData: {
    token: "",
  },
  // 全局共享的方法
  setToken(token) {
    // 如果想获取token可以通过this的方式获取
    this.globalData.token = token;
    // 在app()方法中如果想获取this实例可以通过this的方法进行获取
  },
});

➡️ pages/index/index.js

js
// getApp() 方法用于获取全局唯一App实例
const appInstance = getApp();
Page({
  login() {
    console.log(appInstance); // 可以获取到app()方法中的全部的数据和方法
    console.log(appInstance.globalData.token); // ""
    appInstance.setToken("7432vef33dd");
    console.log(appInstance.globalData.token); // "7432vef33dd"
  },
});

image-20240623170230006

📌 注意事项

  1. 不要在 App() 方法中使用 getApp() ,使用 this 就可以拿到 app 实例
  2. 通过 getApp() 获取实例之后,不要私自调用生命周期函数

2. 小程序页面间通信

如果一个页面通过 wx.navigateTo 打开一个新页面,实现打开页面与被打开页面之间的数据通信

  1. wx.navigateTosuccess 回调中通过 EventChannel 对象发射事件

  2. 被打开的页面可以通过 this.getOpenerEventChannel() 方法获得一个 EventChannel 对象,进行监听、发射事件

  3. wx.navigateTo 方法中可以定义 events 配置项接收被打开页面发射的事件

这两个 EventChannel 对象间可以使用 emiton 方法相互发送、监听事件

注意:必须使用 wx.navigateTo方法进行跳转,简单的数据可以通过在路径上拼接 query参数进行传递

示例:

pages/index.wxml

html
<button type="warn" plain bind:tap="handler">跳转到列表页面</button>

pages/index.js

js
Page({
  // 点击按钮触发的事件处理函数
  handler() {
    wx.navigateTo({ // 通过wx.navigateTo跳转到list页面
      url: "/pages/list/list",
      // 获取被打开页面传递的数据
      events: {
        // key:被打开页面通过eventChannel发射的事件,value:回调函数
        // 为事件添加一个监听器,获取到被打开页面传递给当前页面的数据
        currentEvent: (data) => {
          console.log(data);
        },
      },
      // 向打的页面传递数据 
      success(res) {
        // 通过success回调形参获取eventChannel对象
        res.eventChannel.emit("myevent", { name: "Tom" });
      },
    });
  },
});

pages/list.js

js
Page({
  // 获取页面传递的数据
  onLoad() {
    // 获取eventChannel对象
    const eventChannel = this.getOpenerEventChannel();
    // 通过eventChannel提供的on方法监听页面发射的自定义事件
    eventChannel.on("myevent", (res) => {
      console.log(res); // {name: "Tom"}
    });
    // 通过eventChannel提供的emit方法也可以向上一级页面传递数据,需要使用emit定义自定事件,携带要传递的数据
    eventChannel.emit("currentEvent", { age: "18" });
  },
});

Tips:

​ 通过 wx.navigateTo 打开新页面时,可以通过 eventChannel提供的 emiton方法进行发射和监听自定义事件,实现打开页面和被打开页面之间数据的通信

3. 组件通信-事件总线

随着项目功能的增加,业务逻辑也会变的更加复杂,一个页面可能有多个组件组成,这些组件之间需要进行数据的传递。此时就需要使用事件总线(实现任意组件间的通信)

事件总线是对发布-订阅模式的一种实现,是一种集中式事件处理机制,允许不同组件之间进行彼此通信,常用于两个非父子关系组件和兄弟组件之间通信。借助第三方的发布订阅JS包,来实现事件总线的功能PubSubJS

image-20240623174530805

使用方法:

  1. 安装:npm i pubsub-js -> 构建npm
  2. 在组件中导入:import PubSub from "pubsub-js";
  3. 在要传递数据的组件中发布事件:PubSub.publish('自定义事件名', 要传递的数据)
  4. 在要接收数据的组件中订阅事件:PubSub.subscribe('自定义事件名', 回调函数),回调函数中接收传递的数据参数类型(msg:自定义事件名,data:传递的数据)

示例:

image-20240623183917848image-20240623183937749

自定义子组件a:

custom01.wxml

html
<view class="box">
  <text>子组件a</text>
  <button class="btn" type="default" bind:tap="sendData">传递数据给兄弟</button>
</view>

custom01.js

js
// 引入PubSubJS
import PubSub from "pubsub-js";
Component({
  // 组件中的数据
  data: {
    name: "Tom",
  },
  // 组件中的方法
  methods: {
    // 使用 pubsubJS实现兄弟组件间通信
    sendData() {
      // 发布事件
      // PubSub.publish('自定义事件名', 要传递的数据;
      PubSub.publish("getName", { name: this.data.name, age: 18 });
    },
  },
});

自定义子组件b:

custom02.wxml

html
<view class="box">
  <text> b组件</text>
  <view>a组件传递的数据:姓名:{{name}},年龄:{{age}}</view>
</view>

custom02.js

js
// 导入PubSubJS
import PubSub from "pubsub-js";
Component({
  data: {
    name: "",
    age: 0,
  },
  methods: {},
  lifetimes: {
    attached() {
      // 订阅监听自定义事件
      // PubSub.subscribe('自定义事件名', 回调函数);
      // 回调函数中接收两个参数,msg:自定义事件名 data:传递的数据
      PubSub.subscribe("getName", (msg, data) => {
        console.log(msg, data); // getName {name: "Tom", age: 18}
        // 接收传递过来的数据
        this.setData({
          name: data.name,
          age: data.age,
        });
      });
    },
  },
});

Tips:

properties(props):父组件 -> 子组件

this.triggerEvent(自定义事件):子组件 -> 父组件

PubSubJS(事件总线):可以实现任意组件(页面 -> 组件)之间的通信(前提组件已经被挂载了)

4. 自定义导航栏

小程序默认的导航栏与 APP 一样都位于顶部固定位置。但是默认导航栏可能会影响小程序整体风格,且无法满足特定的设计需求,这时候,就需要进行自定义导航栏。

app.json 或者 page.json 中,配置 navigationStyle:custom,即可 自定义导航栏

在设置以后,就会移除默认的导航栏,只保留右上角胶囊按钮

示例:

json
{
  "usingComponents": {},
  "navigationStyle": "custom"
}
html
// 将轮播图显示在导航栏位置
<swiper class="custom-swiper" indicator-dots indicator-color="#eee" indicator-active-color="pink" autoplay circular interval="2000">
  <swiper-item>
    <image src="" data-missing="banner-1.png" mode="" />
  </swiper-item>
  <swiper-item>
    <image src="" data-missing="banner-2.png" mode="" />
  </swiper-item>
  <swiper-item>
    <image src="" data-missing="banner-3.png" mode="" />
  </swiper-item>
</swiper>
js
.custom-swiper {
  height: 440rpx;
}
.custom-swiper image {
  height: 100%;
  width: 100%;
}

实现一个简单效果image-20241015154238860

page.wxml

html
<view class="container-box">
  <view class="box"></view>
</view>

page.scss

scss
.container-box {
  width: 100%;
  height: 100vh;
  background: url("https://img0.baidu.com/it/u=2356638498,1494133758&fm=253&app=120&size=w931&n=0&f=JPEG&fmt=auto?sec=1729098000&t=158b01504366a2be0feb34578d1b6cad")
    no-repeat center;
  background-size: cover;
  .box {
    width: 100%;
    height: 100%;
    overflow: auto;
  }
}

5. 适配屏幕安全区

如果设置了自定义导航栏,如果不适配屏幕的安全区,当为 "刘海屏","挖孔屏"时就会出现页面内容被遮盖

适配前:

image-20241015174406420image-20241015174426624

wx.getWindowInfo() 获取窗口信息,返回窗口信息对象

const {safeArea} = wx.getWindowInfo()

image-20241015173953350

page.js

js
Page({
  data: {
    safeAreaTop: 0
  },
 // 获取屏幕安全区距离的函数
 getSafeArea() {
    const { safeArea } = wx.getWindowInfo();
    this.setData({
      safeAreaTop: safeArea.top
    });
  },
 onLoad(options) {
    this.getSafeArea();
  },
})

page.wxml

wxml
<view class="box" style="padding-top: {{safeAreaTop}}px;">

适配后:

image-20241015174326322image-20241015174332198

十三、上线与发布

假设我们目前已经使用微信开发者工具,按照小程序的开发规范完成了小程序的全部的全部开发工作,并且完成了本地测试,

这时候我们需要开发对小程序进行发布,小程序上线的流程如下:

开发版本:点击开发者工具上传后的版本,开发版本只保留每人最新的一份上传的代码,是供开发者和团队测试和调试的版本

体验版本:小程序开发者可以将开发版本转换为体验版本,由测试人员以及产品经理进行测试与体验,确认没问题可提交审核

审核版本:小程序开发者可以将开发版本转换为审核版本,由微信的审核团队进行审核,审核周期为1~7天,审核通过可提交发布

线上版本:通过微信小程序平台审核,并由开发者提交发布的正式版本,线上版本是用户可以正常使用的小程序版本

小程序开发成员在开发者工具中点击 上传 按钮,在弹出的界面中选择更新类型、版本号、项目备注,就能够将小程序代码上传至微信公众号后台审核。

在登录到微信公众后台以后,点击左侧的 管理版本管理,就能查看小程序的四个个版本

十四、框架扩展

01. mobx-miniprogram

1.1 mobx-miniprogram 介绍

目前已经学习了 6 种小程序页面、组件间的数据通信方案,分别是:

  1. 数据绑定:properties
  2. 获取组件实例:this.selectComponent()
  3. 事件绑定:this.triggerEvent()
  4. 获取应用实例:getApp()
  5. 页面间通信:EventChannel
  6. 事件总线:pubsub-js

在中小型项目中,使用这些数据通信方式已经能够满足我们项目的需求。

但是随着项目的业务逻辑越来越复杂,组件和页面间通信就会变的非常复杂。例如:有些状态需要在多个页面间进行同步使用,一个地方发生变更,所有使用的地方都需要发生改变,这时候如果使用前面的数据通信方案进行传递数据,给管理和维护将存在很大的问题。

为了方便进行页面、组件之间数据的传递,小程序官方提供了一个扩展工具库: mobx-miniprogram

mobx-miniprogram 是针对微信小程序开发的一个简单、高效、轻量级状态管理库,它基于Mobx状态管理框架实现。

使用 mobx-miniprogram 定义管理的状态是响应式的,当状态一旦它改变,所有关联组件都会自动更新相对应的数据

通过该扩展工具库,开发者可以很方便地在小程序中全局共享的状态,并自动更新视图组件,从而提升小程序的开发效率

需要注意:在使用 mobx-miniprogram 需要安装两个包:mobx-miniprogrammobx-miniprogram-bindings

  1. mobx-miniprogram 的作用:创建 Store 对象,用于存储应用的数据
  2. mobx-miniprogram-bindings 的作用:将状态和组件、页面进行绑定关联,从而在组件和页面中操作数据
shell
npm install mobx-miniprogram mobx-miniprogram-bindings

官方文档:

  1. mobx-miniprogram 官方文档

  2. mobx-miniprogram-bindings 官方文档

1.2 创建 Store 对象

如果需要创建 Store 对象需要使用 mobx-miniprogram ,因此需要先熟悉 mobx-miniprogram 三个核心概念:

  1. observable:用于创建一个被监测的对象,对象的属性就是应用的状态(state),这些状态会被转换成响应式数据。
  2. action:用于修改状态(state)的方法,需要使用 action 函数显式的声明创建。
  3. computed:根据已有状态(state)生成的新值。计算属性是一个方法,在方法前面必须加上 get 修饰符

mobx-miniprogram 详细的使用步骤如下:

  1. 在项目的根目录下创建 store 文件夹,然后在该文件夹下新建 index.js

  2. /store/index.js 导入 observable action 方法

    js
    import { observable, action } from 'mobx-miniprogram'
  3. 使用 observable 方法需要接受一个 store 对象,存储应用的状态

    js
    // observable:用于创建一个被监测的对象,对象的属性就是应用的状态,状态会自动转换为响应数据
    // action:函数是用来显式的定义action方法,用来修改、更新状态
    import { observable, action } from "mobx-miniprogram";
    // 1.创建 store 对象
    export const numStore = observable({
      // 2.定义共享的数据
      numA: 1,
      numB: 2,
      // 3.定义action方法用来修改状态
      update: action(function (e) {
        // 在方法中如需要获取状态,可以通过this进行获取
        (this.numA += 1), (this.numB += 1);
        // 获取事件参数
        console.log(e.currentTarget.dataset)
      }),
      // 4.计算属性 computed,根据已有的状态产生新的状态,计算属性需要使用get修饰符
      get sum() {
        // 计算属性内部必须有返回值
        return this.numA + this.numB;
      },
    });

1.3 在组件中使用数据

如果需要 Page 或者Component中对共享的数据进行读取、更新操作,需要使用 mobx-miniprogram-bindings

mobx-miniprogram-bindings 的作用就是将 Store 和 页面或组件进行绑定关联

如果需要在组件中使用状态,需要 mobx-miniprogram-bindings 库中导入 ComponentWithStore 方法

在使用时:,原本组件配置项也需要写到该方法中

在替换以后,就会新增一个 storeBindings 配置项,配置项常用的属性有以下三个:

  1. store: 指定要绑定的 Store 对象
  2. fields: 指定需要绑定的 data 字段(包括计算属性)
  3. actions: 指定需要映射的 actions 方法

📌 注意事项:

导入的数据会同步到组件的 data 中,导入的方法会同步到组件的 methods 中,在模板中可以直接使用

js
// 如果需要在组件中使用store中的数据以及方法需要使用ComponentWithStore代替Component方法
import { ComponentWithStore } from "mobx-miniprogram-bindings";
// 导入numStore对象
import { numStore } from "../../stores/numstore";
ComponentWithStore({
  storeBindings: {
    // 用于配置当前组件需要使用哪些store
    store: numStore,
    // 配置当前组件需要使用store中的哪些数据
    fields: ["numA", "numB", "sum"],
    // 配置当前组件需要使用store中的哪些方法
    actions: ["update"],
  },
  // 注意:在从store对象中引入数据和方法后,如果是数据会被注入到data对象中,如果是方法会被注入到methods对象中,在模板中可以直接使用
  data: {},
  methods: {},
});

1.4 在页面中使用数据-方式1

Component 方法用于创建自定义组件。小程序的页面也可以视为自定义组件,因此页面也可以使用 Component 方法进行构建,从而实现复杂的页面逻辑开发。

如果我们使用了 Component 方法来构建页面,那么页面中如果想使用 Store 中的数据,使用方式和组件的使用方式是一样的

  1. mobx-miniprogram-bindings 库中导入 ComponentWithStore 方法
  2. Component 方法替换成 ComponentWithStore 方法
  3. 然后配置 storeBindingsStore 中映射数据和方法即可
js
// 小程序页面也可以使用Component方法进行构造,如果使用Component构建页面,在页面中使用Store对象中的数据和组件的使用方式一样
// 1.引入ComponentWithStore方法代替Component方法
import { ComponentWithStore } from "mobx-miniprogram-bindings";
// 2.引入需要使用的 store
import { numStore } from "../../stores/numstore";
ComponentWithStore({
  // 3.配置需要使用仓库的数据和方法
  storeBindings: {
    store: numStore, // 使用的仓库
    fields: ["numA", "numB", "sum"], // 使用的仓库中的数据
    actions: ["update"], // 使用的仓库中的方法
  }
});

注意:如果使用 component构建页面,则需要将页面的数据写在 data配置项中,页面的生命周期写在 methods

1.5 在页面中使用数据-方式2(推荐)

如果不想使用 Component 方法构建页面。这时候需要使用 mobx-miniprogram-bindings 提供的 BehaviorWithStore 方法来和 Store 建立关联。

小程序的 behavior 方法是一种代码复用的方式,可以将一些通用的逻辑和方法提取出来,然后在多个组件中复用,从而减少代码冗余,提高代码的可维护性。

使用方式如下:

  1. 新建 behavior 文件,从 mobx-miniprogram-bindings 库中导入 BehaviorWithStore 方法
  2. BehaviorWithStore 方法中配置 storeBindings 配置项从 Store 中映射数据和方法
  3. Page 方法中导入创建的 behavior ,然后配置 behavior 属性,并使用导入的 behavior
js
// numbehavior.js
// 小程序页面如果想使用store对象中的数据或者方法
// 1.从mobx-miniprogram-bindings中导入BehaviorWithStore方法
import { BehaviorWithStore } from "mobx-miniprogram-bindings";
// 2.导入需要使用的store
import { numStore } from "../../stores/numstore";
// 3.BehaviorWithStore方法返回一个behavior并进行暴露,在页面js文件中导入使用
export const cartBehavior = BehaviorWithStore({
  // 3.使用storeBindings方法进行配置
  storeBindings: {
    store: numStore, // 使用哪个store
    fields: ["numA", "numB", "sum"], // 使用store中的哪些属性
    actions: ["update"], // 使用store中的哪些方法
  },
});
js
// cart.js
// 1.导入用于与仓库建立连接的behavior
import { cartBehavior } from "./behaviors";
Page({
  // 2.注册behavior
  behaviors: [cartBehavior],
  // 3.可以在模板中直接使用数据和方法
});

使用 behavior的方式可以在多个页面和组件(必须使用Component构造页面)中复用,减少代码量

1.6 fields、actions 对象写法

fieldsactions 有两种写法:数组(推荐) 或者 对象。

作用:可以将 store 中的属性和方法名进行重命名

如果 fields 写成对象方式,有两种写法:

  1. 映射形式:指定 data 中哪些字段来源于 store 以及它们在 store 中对应的名字。

    • 例如 { a: 'numA', b: 'numB' }
  2. 函数形式:指定 data 中每个字段的计算方法

    • 例如 { a: () => store.numA, b: () => anotherStore.numB }

如果 actions 写成对象方式,只有两种写法:

  1. 映射形式:指定模板中调用的哪些方法来源于 store 以及它们在 store 中对应的名字。
    • 例如 { buttonTap: 'update' }
js
// 如果需要在组件中使用store中的数据以及方法需要使用ComponentWithStore代替Component方法
import { ComponentWithStore } from "mobx-miniprogram-bindings";
// 导入numStore对象
import { numStore } from "../../stores/numstore";
ComponentWithStore({
  storeBindings: {
    // 用于配置当前组件需要使用哪些store
    store: numStore,
    // fields和actions有两种方法,数据/对象写法
    // 1.数组写法
    // 配置当前组件需要使用store中的哪些数据
    /* fields: ["numA", "numB", "sum"],
    // 配置当前组件需要使用store中的哪些方法
    actions: ["update"], */
    // 2.对象写法
    fields: {
      // 如果fileds为对象写法,数据也有两种写法
      // 1.映射式
      /* A: "numA",
      B: "numB",
      total: "sum", */
      // 2.函数式
      A: () => numStore.numA,
      B: () => numStore.numB,
      total: () => numStore.sum,
    },
    actions: {
      // 只有映射写法
      update: "update",
    },
  },
  // 注意:在从store对象中引入数据和方法后,如果是数据会被注入到data对象中,如果是方法会被注入到methods对象中,在模板中可以直接使用
  data: {},
  methods: {},
});

1.7 绑定多个 store 以及命名空间

在实际开发中,一个页面或者组件可能会绑定多个 Store ,这时候我们可以将 storeBindings 改造成数组。数组每一项就是一个个要绑定的 Store

如果多个 Store 中存在相同属性名的数据和同名方法,显示会出现异常

可以通过 filedsactions的对象写法 或 namespace 属性给当前 Store 开启命名空间,在开启命名空间以后,访问数据的时候,需要加上 namespace 的名字才可以

js
// behavior.js
// 小程序页面如果想使用store对象中的数据或者方法
// 1.从mobx-miniprogram-bindings中导入BehaviorWithStore方法
import { BehaviorWithStore } from "mobx-miniprogram-bindings";
// 2.导入需要使用的store
import { numStore } from "../../stores/numstore";
import { cloneStore } from "../../stores/clonestore";
// 3.BehaviorWithStore方法返回一个behavior并进行暴露,在页面js文件中导入使用
export const cartBehavior = BehaviorWithStore({
  // 3.使用storeBindings方法进行配置
  // 如果一个组件/页面需要使用多个store,则需要将storeBindings改为一个数组
  // 在不同的store中存在相同的属性和方法就会出现错误
  storeBindings: [
    {
      // 解决方法1:将fields和actions改为对象格式,对属性和方法进行重命名
      store: numStore,
      // fields: ["numA", "numB", "sum"],
      fields: {
        A: "numA",
        B: "numB",
        total: "sum",
      },
      // actions: ["update"],
      actions: {
        changeNum: "update",
      },
    },
    {
      // 解决方法2:添加命名空间
      // 添加命名空间只能解决属性名相同,不能解决方法名相同,方法依旧需要使用对象格式
      // 注意:在为属性添加命名空间后,访问属性时需要在属性名前加上命名空间名
      namespace: "cloneStore",
      store: cloneStore,
      fields: ["numA", "numB", "sum"],
      // actions: ["update"],
      actions: {
        changeNum: "update",
      },
    },
  ],
});
html
// index/index.wxml
<view> {{ cloneStore.numA }} + {{ cloneStore.numB }}={{ cloneStore.sum }}</view>
<button type='primary' plain bind:tap="changeNum">点我进行更新</button>

注意:

  1. 添加命名空间只能解决不同 store 中属性名相同,不能解决方法名相同,方法依旧需要使用对象格式进行重命名

  2. 在为属性添加命名空间后,访问属性时需要在属性名前加上命名空间名,访问方法不需要添加命名空间

  3. 一个页面/组件只能执行一个 behavior中的 storeBindings,即如果需要在一个页面中使用多个仓库应将多个仓库的配置以数组形式写在一个 storeBindings中,不可以引入多个 behavior

js
behaviors: [counterStoreBehavior, userStoreBehavior] // 错误写法
behaviors: [counterAnduserStoreBehavior] // 正确写法

behaviors\counterStoreBehavior.js

js
import { BehaviorWithStore } from "mobx-miniprogram-bindings";
import { counterStore } from "../store/counterStore";
const counterStoreBehavior = BehaviorWithStore({
  storeBindings: {
    // 开启命名空间,访问属性时需要加上命名空间名eg:counterStore.sum(避免与组件或其他仓库中的属性同名)
    namespace: "counterStore",
    store: counterStore,
    fields: ["sum", "bigSum"],
    actions: ["add", "decrement"],
  },
});
export default counterStoreBehavior;

behaviors\userStoreBehavior.js

js
import { BehaviorWithStore } from "mobx-miniprogram-bindings";
import userStore from "../store/userStore";
const userStoreBehavior = BehaviorWithStore({
  storeBindings: {
    namespace: "userStore",
    store: userStore,
    fields: ["token", "userInfo"],
    actions: ["setUserInfo", "setToken"],
  },
});
export default userStoreBehavior;

behaviors\counterAnduserStoreBehavior.js

js
import { BehaviorWithStore } from "mobx-miniprogram-bindings";
import { counterStore } from "../store/counterStore";
import userStore from "../store/userStore";
const counterAnduserStoreBehavior = BehaviorWithStore({
  storeBindings: [
    {
      // 开启命名空间,访问属性时需要加上命名空间名eg:counterStore.sum(避免与组件或其他仓库中的属性同名)
      namespace: "counterStore",
      store: counterStore,
      fields: ["sum", "bigSum"],
      actions: ["add", "decrement"],
    },
    {
      namespace: "userStore",
      store: userStore,
      fields: ["token", "userInfo"],
      actions: ["setUserInfo", "setToken"],
    },
  ],
});
export default counterAnduserStoreBehavior;

02. miniprogram-computed

小程序框架没有提供计算属性相关的 api ,但是官方为开发者提供了拓展工具库 miniprogram-computed

该工具库提供了两个功能:

  1. 计算属性 computed
  2. 监听器 watch

2.1 计算属性 computed

知识点:

如果需要在组件中使用计算属性功能,需要 miniprogram-computed 库中导入 ComponentWithComputed 方法

在使用时:,原本组件配置项也需要写到该方法中,在替换以后,就可以新增 computed 以及 watch 配置项。

安装 miniprogram-computed, 在安装以后,需要点击 构建 npm,进行本地构建

shell
npm install miniprogram-computed

📌 注意事项

computed 函数中不能访问 this ,但是提供了形参,代表 data 对象

​ 计算属性函数的返回值会被设置到组件的 data 字段中

计算属性具有缓存特性,计算属性只在初始化和依赖计算的属性发生变化时才会执行

官方文档:miniprogram-computed

落地代码:

计算属性computed的使用

js
// 如果需要在组件中使用计算属性和监视器功能,需要在组件中导入ComponentWithComputed方法
import { ComponentWithComputed } from "miniprogram-computed";
// 使用导入的ComponentWithComputed方法替换Component方法
ComponentWithComputed({
  data: {
    a: 1,
    b: 2,
  },
  // 计算属性具有缓存特性,计算属性只在初始化和依赖计算的属性发生变化时才会执行
  computed: {
    total(data) {
      // 计算属性方法内部必须有返回值,方法名为计算属性名
      // 在计算属性内不能使用this获取data中的数据,需要使用形参
      console.log(this, data); // undefined Proxy {a: 1, b: 2}
      return data.a + data.b;
    },
  },
  // 当依赖计算的属性发生变化时会重新进行计算
  methods: {
    updateA() {
      this.setData({
        a: this.data.a + 1,
      });
    },
  },
});

2.2 监听器 watch

在使用时:,原本组件配置项也需要写到该方法中

在替换以后,就可以新增 computed 以及 watch 配置项。

js
// 如果需要在组件中使用计算属性和监视器功能,需要在组件中导入ComponentWithComputed方法
import { ComponentWithComputed } from "miniprogram-computed";
// 使用导入的ComponentWithComputed方法替换Component方法
ComponentWithComputed({
  data: {
    a: 1,
    b: 2,
  },
  // 计算属性具有缓存特性,计算属性只在初始化和依赖计算的属性发生变化时才会执行
  computed: {
    total(data) {
      // 计算属性方法内部必须有返回值,方法名为计算属性名
      // 在计算属性内不能使用this获取data中的数据,需要使用形参
      console.log(this, data); // undefined Proxy {a: 1, b: 2}
      return data.a + data.b;
    },
  },
  // 数据监听器,用于监听数据是否发生了变化,在数据变化后执行相应的逻辑
  watch: {
    // key:需要监听的数据 value:回调函数接收改变后的最新数据
    a(newValue) {
      console.log("a发生变化了最新值为:" + newValue);
    },
    // 同时监听多个数据,数据与数据间使用,进行分隔,只要有一个数据发生变化回调函数就会执行
    "b,total"(b, total) {
      console.log("b或total发生了变化");
      console.log("b最新值为:" + b);
      console.log("total最新值为:" + total);
    },
  },
  // 当依赖计算的属性发生变化时会重新进行计算
  methods: {
    updateA() {
      this.setData({
        a: this.data.a + 1,
      });
    },
  },
});

Tips:

  1. 如果想在页面中使用 storecomputedwatch/observers推荐使用 Component构建页面,将页面生命周期函数写在 methods
  2. Component提供的 observers功能与 ComponentWithComputed功能相同,均为监视,也可以使用 observers实现 Computed效果
  3. 推荐使用 Component构建页面,可以实现更多功能(将页面生命周期写在 methods中),自定义组件使用组件特有的生命周期在 lifetimes配置项中

03.拓展:Mobx 与 Computed 结合使用

两个框架扩展提供的 ComponentWithStoreComponentWithComputed 方法无法结合使用。

如果需要在一个组件中既想使用 mobx-miniprogram-bindings 又想使用 miniprogram-computed

解决方案是:

  1. 使用旧版 API

  2. 使用兼容写法(推荐)

    • 即要么使用 ComponentWithStore 方法构建组件,要么使用 ComponentWithComputed 方法构建组件

    • 如果使用了 ComponentWithStore 方法构建组件,计算属性写法使用旧版 API

    • 如果使用了 ComponentWithComputed 方法构建组件,Mobx写法使用旧版 API

我们演示使用兼容写法:

  1. 如果使用了 ComponentWithStore 方法构建组件,计算属性写法使用旧版 API ,使用 computedwatch

    js
    // 导入ComponentWithStore方法
    import { ComponentWithStore } from "mobx-miniprogram-bindings";
    // 导入需要使用的store
    import { numStore } from "../../stores/numstore";
    // 使用ComponentWithStore方法替换Component方法
    // 如果使用ComponentWithStore构造组件,计算属性扩展库则需要使用旧版API
    // 1.导入计算属性 behavior
    const computedBehavior = require("miniprogram-computed").behavior;
    ComponentWithStore({
      data: {
        a: 1,
        b: 2,
      },
      methods: {
        updateData() {
          this.setData({
            a: this.data.a + 1,
            b: this.data.b + 1,
          });
        },
      },
      // 对使用的仓库进行配置
      storeBindings: {
        store: numStore,
        fields: ["numA", "numB", "sum"],
        actions: ["update"],
      },
      // 2.注册behavior
      behaviors: [computedBehavior],
      // 3.使用computed和watch
      computed: {
        total(data) {
          return data.a + data.b;
        },
      },
      watch: {
        "a,b"(a, b) {
          console.log(a, b);
        },
      },
    });
  2. 使用了 ComponentWithComputed 方法构建组件,Mobx写法使用旧版 API ,绑定 store

    js
    // 导入ComponentWithComputed
    import { ComponentWithComputed } from "miniprogram-computed";
    import { numStore } from "../../stores/numstore";
    // 在使用了ComponentWithComputed构建的组件中绑定store
    // 1.导入storeBindingsBehavior
    import { storeBindingsBehavior } from "mobx-miniprogram-bindings";
    // 使用ComponentWithComputed代替Component
    ComponentWithComputed({
      // 2.注册使用storeBindingsBehavior
      behaviors: [storeBindingsBehavior],
      // 3.设置store配置项
      storeBindings: {
        store: numStore,
        fields: ["numA", "numB", "sum"],
        actions: ["update"],
      },
      data: {
        a: 10,
        b: 20,
      },
      // 使用计算属性
      computed: {
        total(data) {
          return data.a + data.b;
        },
      },
    });