Skip to content

3 组件

约 2247 个字 304 行代码 预计阅读时间 11 分钟

1 自定义组件

  • 组件的 .json 文件中需要声明 components: true
  • 组件的 .js 文件中调用 Component() 函数以进行初始化
  • 组件的事件处理函数需要定义到 methods 节点中(进行一个套娃)

1.1 创建组件

  1. 在根目录下新建 components 文件夹,用于存放所有组件
  2. 新建组件文件夹 test ,在该路径下右键选择 “新建 Component”
  3. 输入组件名称,自动生成组件对应的 4 个文件

1.2 引用组件

分为“全局引用” & “局部引用”

  • 全局引用的组件可以在每一个页面中使用
  • 局部引用的组件仅能在当前页面中使用
    {
        "usingComponents": {
            "comp-name": "/components/name/name"
        }
    }
    
    <comp-name></comp-name>
    

1.3 组件样式

  • 组件样式具有隔离性
    • app.wxss 中定义的全局样式对组件无效(类选择器)
    • id 选择器、属性选择器、标签选择器不受样式隔离影响
    • 所以建议大家在引用了组件的界面中尽量使用class选择器,避免样式冲突
  • 修改样式隔离选项
    • isolated - 默认,内外互不干扰
    • apply-shared - 页面覆盖组件
    • shared - 互相影响,包括页面中其他设置了 apply-shared / shared 的组件
      @comp.js
      Components({
          options: {
              styleIsolation: 'isolated / shared / apply-shared'
          }
      })
      

1.4 数据 / 方法 / 属性

  • 用于组件渲染的私有数据需要定义到组件的data节点中
    data: {count: 0}
    
  • 组件的 事件处理函数 / 自定义方法 需要定义到 method 节点中
    methods: {
        addCount() {},  事件处理函数
        _showCount() {} 自定义方法下划线开头
    }
    
  • properties 是组件的对外属性,用于接收外界传递给组件的数据
    properties: {
        max: { // 需要指定默认值的定义方式
            type: Number, // 数据类型
            value: 10 // 默认值
        },
        max: Number // 不需要指定默认值的定义方式
    }
    
    <my-comp max="10"></my-comp> %%从外界传入数据%%
    
  • data & properties 均为可读可写(这俩实际上指向同一块空间),都可以用 setData重新赋值

1.5 数据监听器

用于监听&响应任何 数据/属性 字段的变化,从而执行特定操作

<view>{{n1}} + {{n2}} = {{sum}}</view>
<view style="background-color: {{color}}">颜色值:{{color}}</view>
data: { n1: 0, n2: 0, sum: 0,
        rgb:{ r:0, g:0, b:0},
        color: '0,0,0'
      },
observe: {
    'key_1, key_2': funtcion(neo_val_1, neo_val_2) {
        do sthing;
    },
    // 监听数据变化
    'n1, n2': fucntion(v1, v2) {
        this.setData({sum: v1 + v2})
    },
    // 监听属性变化
    'rgb.r rgb.g rgb.b': function(v1, v2, v3) {
        this.setData({ color: `${v1}, ${v2}, ${v3}`})
    },
    // 监听某个对象的所有属性
    'rgb.**': function(obj) {
        this.setData({ color: `${obj.r}, ${obj.g}, ${obj.b}`})
    }
}

1.6 纯数据字段

既不用于页面渲染,又不传递给其他组件的数据

  • Component - options 中设置pureDataPattern: 正则表达式,符合规则的字段将被视为“纯数据字段”
    options: {
        pureDataPattern: /^_/ //以_开头的为纯数据字段
    },
    data: {
        a: true, //普通数据字段
        _a: true //纯数据字段
    }
    
  • 纯数据字段可以被监听

1.7 组件的生命周期函数

函数名 描述
created 被创建时执行
attached 实例进入页面节点树时执行
ready 在渲染完成后执行
moved 实例被移动到节点树另一位置时执行
detached 实例从节点树中移除时执行
error(Obj err) 每当抛出错误就执行
  • created
    • 不能调用 setData
    • 一般用于给 this 指针添加自定义属性字段
  • attached
    • this.data 初始化完毕
    • 进行一些初始化工作(比如拉取初始数据)
  • detached
    • 退出页面时,会触发当前页面中每一个组件的 detached 函数
    • 适合做一些清理工作
  • 以上生命周期函数应当在 lifetimes 节点中进行声明
    lifetimes: {
        attached() {},
        detached() {}
    }
    

1.8 监听外层页面生命周期

自定义组件可以监听外界页面的三个生命周期:show / hide / resize(Obj size)

  • 用于监听页面生命周期的函数应当在 pageLifetimes 节点中定义
    pageLifetimes: {
        show: function() {},
        hide: function() {},
        resize: function(size) {}
    }
    

4.2 独立分包

也是分包的一种,但是可以独立于主包/其他分包单独运行

  • 一个小程序可以同时拥有多个独立分包
  • 独立分包 与 普通分包&&主包 之间互相隔绝,不能相互引用资源 ⚠️ 独立分包不能使用主包中的公共资源
  • 普通分包必须依赖于主包运行
  • 应用场景:具有一定功能独立性的页面(不用下载主包,提升分包页面启动速度)
  • 配置方法
    // 增加 independent 配置项
    "subpackages": [ {// 分包结构 - 有几个对象就有几个分包
        "root": "packageA", // 分包根目录
        "name": "pkgA", // 别名(可选)
        "pages": [
                "pages/cat",
                "pages/dog"
            ],
        "independent": true
    }]
    

1.9 插槽 slot

生效于 组件中 ,类似于一个 html 占位符,后续视情况由 使用者 插入具体的 wxml 内容

  • 在小程序中,默认每个自定义组件中 只允许使用一个 <slot> 标签进行占位
    %%组件封装%%
    <view class="wrapper">
        <view>组件内部节点</view>
        <slot></slot>
    </view>
    
    %%组件使用%%
    <component-tag-name>
        %%以下内容将被插入组件的 slot 中%%
        <view>这是将被插到 slot 标签中的内容</view>
    </component-tag-name>
    
  • 启用多个插槽 需要手动在 page.js 中做如下设置
    Component({
        options: {
            multipleSlots: true
        }
    })
    
    • 启用多个插槽时,需要通过 name 来对不同插槽进行区分
    • 在使用时,通过 slot 来指定内容插入的具体位置
      %%组件封装%%
      <view class="wrapper">
          <slot name="before"></slot>
          <view>这是中间的固定文本</view>
          <slot name="after"></slot>
      </view>
      %%组件使用%%
      <component-tag-name>
          <view slot="before">这是将被插到 slot-before 标签中的内容</view>
          <view slot="after">这是将被插到 slot-after 标签中的内容</view>
      </component-tag-name>
      

1.10 父子组件通信

实现父子组件通信有以下三种方式:

  1. 属性绑定:只能 父 to 子,只能兼容 json 格式的数据(不能传递方法)

    • 子组件中的对应数据发生修改不会影响父组件中的源数据
      //父组件数据定义
      data: { count: 0 }
      
      // 在子组件的 properties 节点中声明并使用属性
      properties: { count: Number }
      
      %%父组件 wxml 结构%%
      <comp count="{{count}}"></comp> %%通过属性绑定方式向子组件传递%%
      <view> ------ </view>
      <view> 父组件中的 count = {{count}} </view>
      
      %%子组件 wxml 结构%%
      <view>子组件中的 count = {{count}} </view>
      
  2. 事件绑定:只能 子 to 父,可以传递任意数据

    1. 父组件js 文件中定义函数,以通过自定义事件进行数据传递
    2. 父组件html 文件中通过自定义事件引用(1)中定义的函数
    3. 子组件js 文件中调用 this.triggerEvent('eventName', {/*参数*/}),从而将参数发送给父组件
    4. 父组件js 文件中调用 e.detail 获取子组件传递的数据
      // 1 - 在父组件中定义 供子组件 调用的函数
      syncCount() { do sthing; }
      // 3 - 子组件向父组件传参
      // 假设我们在子组件的 +1按钮 上绑定了 addCount 函数
      methods: {
          addCount() {
              this.setData({ count: this.properties.count + 1 });
              // 同步修改
              this.triggerEvent('sync', {value: this.properties.count});
          }
      }
      // 4 - 在父组件中获取修改(修改 1 中的定义项得到)
      syncCount(e) {
          this.setData({ count: e.detail.value })
      }
      
      %% 2 - 在父组件中引用事先定义的函数,从而传递给子组件  两种方法都可以 %%
      %% 其中,‘sync’ 是我们自定义的事件名称 %%
      <comp count="{{count}}" bind:sync="syncCount"></comp>
      <comp count="{{count}}" bindsync="syncCount"></comp>
      
  3. 获取组件实例:

    父组件可以通过 this.selectComponent("id / class选择器") 获取子组件实例对象,访问子组件 的所有数据和方法

    <comp count="{{count}}" bind:sync="syncCount" class="customA" id="cA"></comp>
    <button bindtap="getChild">获取子组件实例</button>
    
    getChild() {
        const child = this.selectComponent('.customA'); // 或使用 '#cA'
        child.setData({ count: child.properties.count + 1 }); // 可以修改数据
        child.addCount(); // 也可以调用方法 - 该方法会通过 sync 事件向父组件同步修改
    }
    

1.11 behaviors - 不太用

用于实现 不同组件间 共享代码

  • behavior 可以包含一组 属性 / 数据 / 生命周期函数 / 方法,当被引用时,上述内容将被自动合并到引用的组件中
    • 一个组件可以引用多个 behavior
    • 一个 behavior 可以引用其他 behavior
  • 创建 behavior - 在 root/behaviors 下单独的 js 文件中进行
    // 调用 Behavior() 方法,创建 behavior 实例
    // 使用 modlue.exports() 共享该实例
    module.exports = Behavior({
        properties: {},
        data: { username: 'zs'},
        methods: {},
        behavoirs: [相互引用的behavior列表],
        something_else
    })
    
  • 导入并使用 behavior
    @component.js
    // 使用 require 方法导入需要的 behavior 模块
    const myBehavior = require("../../behaviors/my-behavior");
    Component({
        // 将导入对象挂载到 behavoirs 节点的数组中,使其生效
        behaviors: [myBehavior, 其他behavior],
        other_points
    })
    
    @component.wxml
    <view>在behavior中定义的用户名是:{{username}}</view>
    
  • 同名字段的覆盖 & 组合 组件及其引用的 behavior可以拥有同名字段,处理规则详见官方文档

2 使用 npm 包

  • 小程序中使用 npm 第三方包的限制 - 能用的不多
    1. 不支持依赖于 Node.js 内置库的包
    2. 不支持依赖于 浏览器内置对象 的包(比如 ajax / jquery
    3. 不支持依赖于 C++插件 的包

下面以使用 vant 组件库为例,vant 官方文档

  • 可以通过在 element / :root 下定义 CSS 变量实现自定义主题样式

API Promise 化

  • 默认情况下,小程序官方提供的 异步API 都是基于 回调函数 实现的(比如 request 要求的 success / fail / complete)
  • API 的 Promise 化指:通过额外配置,将官方基于回调的 API 改装为基于 Promise 的 API,从而提高代码的可维护性
  • 小程序的 API Promise 化主要依赖于第三方包 miniprogram-api-promise
    • 安装命令 npm install --save miniprogram-api-promise@1.0.4(根目录下)
    • 构建 npm 包
      1. 删除 mini-program 文件目录
      2. 点击 ‘工具’ - ‘构建 npm’
      3. 展开 mini-program 目录,查看是否构建完成ß
    • 按需导入 npm 包
      import {promisifyAll} from 'miniprogram-api-promise';
      const wxp = wx.p = {}
      // promisify all wx's api
      promisifyAll(wx, wxp);
      // 调用 `Promise` 化后的异步 API
      async funcName() {
          const { data:res } = await wx.p.request({
              url: '', method: '', data: {}
          })
      }
      

3 全局共享 - MobX

用于解决组件之间 数据共享 的问题

一直报错可以使用 工具-构建npm

  • 小程序中一般使用 mobx-miniprogram + mobx-miniprogram-bindings 作为解决方案:

    • mobx-miniprogram 用于创建 Store 实例对象

    • mobx-miniprogram-bindings 将 Store 中用于共享的 数据 / 方法 绑定到 组件 / 页面 中进行使用

  • MobX 安装

    • 在项目根目录下执行如下命令

      npm install --save mobx-miniprogram@4.13.2 mobx-miniprogram-bindings@1.2.1

    • 构建

  • MobX 使用

    1. 创建 Store 实例 - 在根目录下创建 ./store 路径,创建 store.js
      @store.js
      import {observable} from 'mobx-miniprogram';
      // 类似于 java 中的一个工具类?
      export const store = observable({
          // 数据字段
          numA: 1, numB: 2,
          // 计算属性 - 结果依赖于数据字段
          get sum() {
              return this.numA + this.numB
          },
          // actions -  方法,用于修改 store 中的数据
          updataNumA: action(function(step) { this.numA += step; }),
          updataNumB: action(function(step) { this.numB += step; })
      })
      
    2. 将 Store 绑定到页面
      @page.js
      import {createStoreBindings} from 'mobx-miniprogram-bindings'
      import {store} from '../../store/store' // export 的数据源对象
      
      Page({
          onLoad: function() { // 绑定
              this.storeBindings = createStoreBindings(this, {
                  store, // 数据源名称
                  fields: ['numA', 'numB', 'sum'], // 需要的数据字段(按需)
                  actions: ['updateNumA'] // 需要的方法(按需)
              })
          },
          onUnLoad: function() { // 清理
              this.storeBindings.destroySotreBindings();
          }
      })
      
    3. 在页面中使用 Store 成员
      <view> {{numA}} + {{numB}} = {{sum}} </view>
      <button bindtap="tapHandler" data-step="{{1}}">  numA + 1 </button>
      <button bindtap="tapHandler" data-step="{{-1}}"> numA - 1 </button>
      
      tapHandler(e) {
          this.updateNumA(e.detail.dataset.step);
      }
      
    4. 在组件中绑定 Store 成员(使用略)
      import {storeBindingBehavior} from 'mobx-miniprogram-bindings'
      import {store} from '../../store/store'
      
      Component({
          behaviors: [storeBindingBehavior], // 通过该behavior自动绑定
          storeBindings: {
              store,
              fields: { // 按需绑定数据字段,下面列出三种绑定方式
                  numA: () => store.numA,
                  numB: (store) => store.numB,
                  sum: 'sum'
                  // 前:映射后的名字(本页中使用的名字),后:源数据名字
              },
              actions: { // 按需绑定方法
                  updateNumB: 'updateNumB'
              }
          }
      })
      

4 分包

  • 我们将完整的小程序项目,按照需求划分为不同的子包,并在构建时分别打包,便于缩短首次下载时间
  • 分包前:小程序中的所有 页面 & 资源 被打包在一起,导致项目体积大,首次运行需要的下载时间长
  • 分包后:1 * 主包 + n * 分包
    • 主包:启动页面 & tabBar页面 + 公共资源
    • 分包:仅包含与当前分包有关的页面 + 私有资源(不能被其他包访问)
  • 加载规则
    • 小程序启动时,默认下载主包,并启动主包内界面(包含所有 tabBar 页面)
    • 用户进入分包页面时,按需下载分包冰进行展示(非 tabBar 页面可以视情况分包)
  • 体积限制
    • 整个程序 < 16M
    • 单个包 < 2M

4.1 配置分包

  • 目录结构
    ├---- app.js
    ├---- app.json
    ├---- app.wxss
    ├---- pages    // 主包包含的所有页面
    |       ├---- index
    |       └---- logs
    ├---- packageA
    |       └---- pages // 分包A 包含的所有页面
    |               ├---- cat
    |               └---- dog
    ├---- packageB
    |       └---- pages // 分包B 包含的所有页面
    |               ├---- apple
    |               └---- banana
    └---- utils
    
  • app.jsonsubpackages 节点中声明分包结构
    {
        "pages": [ // 主包包含的所有页面
            "pages/index",
            "pages/logs"
        ],
        "subpackages": [ {// 分包结构 - 有几个对象就有几个分包
                "root": "packageA", // 分包根目录
                "name": "pkgA", // 别名(可选)
                "pages": [
                    "pages/cat",
                    "pages/dog"
            ]}, {
                "root": "packageB",
                "pages": [
                    "pages/apple",
                    "pages/banana"
            ]}
        ]
    }
    
  • 查询分包体积 “详情” - “基本信息” - “ 本地代码” - 展开详情,即可查看主包及各分包的体积
  • 打包原则
    1. 小程序会按照 subpackages 中的配置进行分包,没有在该节点中声明的目录将被打包到主包当中
    2. 主包可以拥有自己的 pages 目录(即根目录下的 pages 字段及路径)
    3. TabBar 页面必须位于主包内
    4. 分包之间不能互相嵌套
  • 引用原则
    1. 主包无法引用分包内的私有资源
    2. 分包之间不能互相引用别人的私有资源
    3. 分包可以引用主包内的公共资源

4.3 分包预下载

在进入某个特定页面时,自动下载可能使用的分包,提升分包页面启动速度

  • 预下载行为会在 进入页面 时触发,相关规则需要在 preloadRule 中进行定义
    @app.json
    {
        "preloadRule": {
            "pages/contact/contact": { // 进入 contact 页面时的相关行为
                "network": "all", // 可选值:wifi(默认) / all - 预下载的网络模式
                "pakages": ["pkgA"] // 通过 root / name 指定需要下载的分包
            }
        }
    }
    
  • 同一个分包(主包)中 所有页面 涉及的预下载总大小 <= 2M

5「SAMPLE」自定义 TabBar

5.1 配置自定义 TabBar

  1. 配置自定义 TabBar
    {
        "tabBar": {
            "custom": true, // 开启自定义 tabBar
            "color": ,...   // 其余属性也需要补全(但是不会应用于 tabBar 的渲染)
        }
    }
    
  2. 添加 TabBar 代码文件,随后在下列文件中编写相应代码
    在根目录下新增以下文件(文件夹名字固定)
    custom-tab-bar/index.wxml
    custom-tab-bar/index.wxss
    custom-tab-bar/index.js
    custom-tab-bar/index.json
    

随后,这些代码会被自动渲染成为 TabBar

5.2 渲染数字徽标

  • 使用 vant 提供的 TabBar 组件时,只需要为 vant-tabbar-item 组件添加 info="n" 的属性即可

    • 由于只有部分 icon 需要渲染数字徽标,我们可以使用以下的按需渲染方式
      <vant-tabbar active="{{active}}" bindchange="onChange">
          <vant-tabbar-item wx:for="{{list}}" wx:key="index" 
           info="{{item.info ? item.info : ''}}">
           // 此处的 info 为三元表达式,若不存在 info 属性,则赋为空字符串 - 不被渲染
           // 其实 info="0" 也不会被渲染出来
          </vant-tabbar-item>
      </vant-tabbar>
      
    • 如果需要使用 Store 中的数据进行动态绑定的话,就参见上面的配置步骤

      此时需要通过数据监听器监听 Store 中的数值变化,并使其同步到 data

      @component.js // 假设 store 中的数据名为 sum
      Component({
          observers: {
              'sum': function(val) {
                  this.setData(
                      'list[1].info': val // 更新 list[1] 的 info 属性
                  )
              }
          }
      })
      

  • 重置样式

    有些样式可能是通过 CSS变量 实现的,此时我们可以通过在父节点重新定义变量进行覆盖

    保险起见可以再加一个默认值 tag: var(--var_name, 0px);

    • 需要覆盖 vant 组件样式时,需要在 comp.json 中进行如下配置
      Component({
          options: [
              styleIsolation: 'shared'
          ]
      })
      
    • 重置 vant-tabbar 的选中文本颜色 <vant-tabbar active-color="#??????">

5.3 自定义 TabBar 页面跳转

因为 vant 的组件里不包含超链接 orz

  1. 每次点击都会触发 onChange 事件,我们可以拿到其中的 event.detail 以得到 active 页面
  2. 因为是列表渲染,我们就可以通过 index 获取对应的页面路径
  3. 根据拿到的路径(/ 开头),我们可以调用 wx.switchTab() 进行跳转

    其中 url: this.data.list[event.detail].path

「问题」图标高亮无法同步变化

  • activeTabBarIndex 属性放置到 Store 中进行存储
  • 同时,提供提供 action 对其进行更新(接受 onChange 事件获取的值进行更新)

    updataActiveTabBarIndex: action(function(idx) {this.activeTabBarIndex = idx;})

  • 在组件中映射 activeTabBarIndex,并使用插值语法绑定给 vant-tabbar.active

  • onChange 事件触发时进行同步更改
    methods: {
        onChange(e) {
            this.updateActiveTabBarIndex(event.detail); // 同步修改至 Store
            wx.switchTab({ url: this.data.list[event.detail].path});
        }
    }