组件和系统API

743 阅读12分钟

组件

如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展

但如果,我们将一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了

LYcREG.png

  • 我们将一个完整的页面 分成很多个组件

  • 每个组件都用于实现页 面的一个功能块

  • 每一个组件又可以进行细分

LYcqh4.png

自定义组件由json wxml wxss js 4个文件组成

在小程序中组件的结构和页面的结构是一模一样的

只不过将当json中的component字段设置为true的时候,

就说明这是一个组件,而不是一个页面

组件的json文件

{
  "component": true,  // 说明这是一个组件
  "usingComponents": {} // 组件内部也是可以使用其它组件的
}

使用组件

<myCpn />
{
  // 所以被用到的组件都需要在这里进行注册
  "usingComponents": {
    // 组件名: 组件路径
    "myCpn": "/components/my-cpn/my-cpn"
  }
}

注意的细节:

  • 因为 WXML 节点标签名只能是 小写字母、中划线和下划线 的组合,所以自定义组件的标签名也只能包含这些字符
  • 自定义组件也是可以引用自定义组件的,引用方法类似于页面引用自定义组件的方式(使用 usingComponents 字段)
  • 自定义组件和页面所在项目根目录名 不能以“wx-”为前缀,否则会报错
  • 如果在app.json的usingComponents声明某个组件, 那么这个组件就是全局组件

样式

组件内的class样式和组件外的class样式, 默认是有一个隔离效果的,

也就是默认组件内样式和页面样式是不会冲突的

如果需要让组件和页面间的同名样式产生相互影响

在Component对象中,可以传入一个options属性,其中options属性中有一个styleIsolation(隔离)属性。

styleIsolation有三个取值:

说明
isolated默认值 组件和页面之间的样式相互不影响
apply-shared页面的样式可以影响组件,组件的样式无法影响页面
如果组件内部样式和页面样式冲突的时候,组件内部的样式会覆盖页面的样式
shared组件样式和页面样式是可以相互影响的
Component({
  options: {
    styleIsolation: 'apply-shared'
  }
})
  1. 组件内不能使用id选择器、属性选择器、标签选择器,只能使用class选择器

  2. 页面中可以使用class选择器,id选择器、属性选择器、标签选择器

  3. 默认情况下,如果在外部使用了标签选择器(类似于view { ... }) 是会对组件内部样式产生影响

    但是class选择器,id选择器、属性选择器并不会对组件内部样式产生影响

    所以推荐在小程序中统一使用class选择器进行样式的设置

页面通信

很多情况下,组件内展示的内容(数据、样式、标签),并不是在组件内写死的,而且可以由使用者来决定

LYcecT.png

properties

大部分情况下,组件只负责布局和样式,内容是由使用组件的对象决定的

我们经常需要从外部传递数据给我们的组件,让我们的组件来进行展示

此时我们可以使用properties属性

properties支持的类型; String、Number、Boolean、Object、Array、null(不限制类型)

父组件

<myCpn name="klaus" />
<myCpn />

子组件

<view class="title">{{ name }}</view>
Component({
  properties: {
    name: String
  }
})

上面的写法是一种简化方式,实际开发中,更常见的写法可以如下:

Component({
  properties: {
    name: {
      type: String,

      // 自定义默认值 --- 去覆盖原本的默认值
      value: 'defaule name',

      // 属性的watch选项
      // 默认可以进行深度监听
      // immediate选项默认是true 
      //   --- 第一次的oldValue会根据类型自动进行初始化操作(如string为空字符串,数值为0)
      //   --- 如果传递的props和需要的props类型不同,会尽可能的进行类型转换
      //   --- 如果转换成功,就是要转换后的值 如果转换失败,就使用默认值   
      observer(newName, oldName) {
        console.log(newName, oldName)
      }
    }
  }
})
Component({
  properties: {
    foo: {
      // 如果props的类型可以为多个的时候,需要将type和optionalTypes结合使用
      // type必填 --- 用于进行多个类型的默认初始值设定,例如在这里不设置的时候默认初始值就是0
      type: Number,
      // 可选的其它类型 --- value的类型为array
      optionalTypes: [String, Boolean]
    }
  }
})

externalClasses

有时候,我们不希望将样式在组件内固定不变,而是外部可以决定样式

也就是给子组件传递一个样式名,而样式的定义位置是在父组件

父组件

<!-- 
  属性title --- 在子组件中定义在externalClasses中,所以其值会被子组件认为是父组件传递过来的class样式
  属性title的值 ---- 对应的样式名
-->
<myCpn title="red" foo="Klaus" />
.red {
  color: red;
}

子组件

<!-- 
  样式是在子组件中使用的 --- 注意: 这里使用的样式名是title不是red(★★★)
-->
<view class="title">{{ name }}</view>
Component({
  data: {
    name: 'Klaus'
  },

  // 接收父组件传入的样式 --- 值的类型为数组
  externalClasses: ['title']
})

自定义事件

父组件

<view>{{ counter }}</view>

<!-- 监听自定义事件 -->
<cpn  bindincrement="increment" />
Page({
  data: {
    counter: 0
  },

  increment() {
    this.setData({
      counter: this.data.counter + 1
    })
  }
})

子组件

<button size="mini" bindtap="increment">increment</button>
Component({
  // 组件的方法需要被定义到methods选项中
  methods: {
    increment() {
      // 使用triggerEvent触发自定义事件
      // 参数1 --- 自定义事件名
      // 参数2 --- 需要传递的参数  ---- 类似是对象
      // 参数3 ---- 配置对象 ---- 一般不常使用 --- 传递个空对象就可
      this.triggerEvent('increment', {}, {})
    }
  }
})

阶段案例

实现一个简易的tab切换组件

父组件

<tabs 
  users="{{ users }}" 
  bind:changeTab="changeTab"
/>

<text>{{ user }}</text>
Page({
  data: {
    users: ['Klaus', 'Alex', 'Steven'],
    user: 'Klaus'
  },

  changeTab(e) {
    this.setData({
      user: e.detail.user
    })
  }
})

子组件

<view class="users">
 <!-- 可以在class中动态绑定对应的样式 -->
 <text 
  wx:for="{{ users }}" 
  wx:key="user"
  wx:for-item="user"
  class="user {{ user === activeUser ? 'active' : ''}}"
  bindtap="changeActiveTab"
  data-user="{{ user }}"
 >
  {{ user }}
 </text>
</view>
Component({
  properties: {
    users: {
      type: Array,
      value: []
    }
  },

  data: {
    activeUser: 'Klaus'
  },

  methods: {
    changeActiveTab(e) {
      const user = e.currentTarget.dataset.user

      this.setData({ 
        activeUser: user
      })
      this.triggerEvent('changeTab', {
        user
      })
    }
  }
})

selectComponent

可以在页面或组件中选取当前页面或组件中使用的组件

参数为id选择器或class选择器

<tabs id="cpn" />
Page({
  onReady() {
    console.log(this.selectComponent('#cpn'))
  }
})

slot

组件的插槽 (slot):

  • 组件的插槽也是为了让我们封装的组件更加具有扩展性
  • 让使用者可以决定组件内部的一些内容到底展示什么

单个插槽

父组件

<cpn>
  <slider value="60" />
</cpn>

子组件

<view>start</view>

<!-- 小程序中无法设置组件的默认值 -->

<!--
   注意:如果存在多个默认插槽,在使用的时候
   只会有第一个默认插槽生效,后边的默认插槽都会失效
   这是和Vue中的slot不一样的地方
-->
<slot />

<view>end</view>

多个插槽

父组件

<cpn>
  <view slot="left">left</view>
  <view slot="center">center</view>
  <view slot="right">right</view>
</cpn>

子组件

<slot name="left" />
<slot name="center" />
<slot name="right" />
Component({
  options: {
    // 只有在子组件中开启这个选项之后,才可以使用多插槽
    multipleSlots: true
  }
})

Component

Component({
  // 用于接收props的选项
  properties: {},

  // 用于定义组件内部数据的选项
  data: {},

  // 外部传入的样式
  externalClasses: [],

  // 组件内部的方法
  methods: {},

  // watch选项 --- 可以监听data/properties中对应状态的改变
  // 但是这个选项中的observer方法的参数只有newValue
  // 没有oldValue
  observers: {},

  // 组件内部的配置选项
  // 例如multipleSlots和styleIsolation
  options: {
    multipleSlots: true
  },

  // 页面的生命周期
  pageLifetimes: {
    show() {
      console.log('页面显示出来')
    },

    hide() {
      console.log('页面隐藏起来')
    },

    resize() {
      console.log('页面尺寸发生了改变')
    }
  },

  // 组件的生命周期函数
  lifetimes: {
    created() {
      console.log('组件被创建出来')
    },

    attached() {
      console.log('组件被添加到页面')
    },

    ready() {
      console.log('组件渲染完毕')
    },

    moved() {
      console.log('组件发生了移动')
    },

    detached() {
      console.log('组件被移除')
    }
  }
})

LrSzs6.png

系统API

网络请求

默认情况下,小程序去请求的接口API域名地址,必须是在小程序后台配置过的域名

在本地调试的时候,可以暂时关闭对应的域名校验

wx.request

wx.request({
  url: 'https://httpbin.org/get',
  
  // 请求参数 -- 对于get请求,参数可以写在data选项里面,也可以写在url请求域名后边
  data: {
    name: 'Klaus',
    age: 23
  },
  
  // 成功回调
  success(res) {
    console.log(res.data.args)
  }
})
wx.request({
  url: 'https://httpbin.org/post',

  // 请求方式 --- 默认值是get
  method: 'POST',

  // post请求的参数必须放置在data选项中
  data: {
    name: 'Klaus',
    age: 23
  },

  success(res) {
    console.log(res.data.json)
  }
})

LrYabu.png

在小程序中,可能多个地方都需要发送对应的网络请求,

所以我们需要对网络请求进行封装,以便于整个项目中的网络请求调用的都是自己封装的接口

从而实现请求和api的解耦,如果后期需要进行修改的时候,直接修改封装的API请求方法即可

封装

import { BASE_URL } from './consts'

class Api {
  request(path, method, params) {
    return new Promise((resolve, reject) => {
      wx.request({
        url: BASE_URL + path,
        method: method || 'get',
        data: params || {},
        success: resolve,
        fail: reject
      })
    })
  }

  get(path, query) {
    return this.request(path, 'get', query)
  }

  post(path, params) {
    return this.request(path, 'post', params)
  }
}

export default new Api()

测试

// 引入文件必须使用相对路径,不可以使用绝对路径
import api from '../lib/api'

export function fetchTopMv(offset = 0, limit = 10) {
  return api.get('/top/mv', {
    offset,
    limit
  })
}

弹窗

showToast

LrYZyp.png

wx.showToast({
  title: 'toast',
  icon: 'loading',
  duration: 3000, // toast出现的持续时间,默认值是1500
  mask: true // 出现遮罩层,在toast出现的时候,不允许和toast层以下层级的元素进行交互
})

showModal

LrrI76.png

wx.showModal({
  title: 'title',
  content: 'content',
  success(res) {
    // res对象中存在属性 
    // 1. cancel --- 值为true的时候,用户退出了弹窗
    // 2. confirm --- 值为true的时候,用户确认了弹窗
    console.log(res)
  }
})

showLoading

LrrLNT.png

showLoding和icon属性为loading的showToast的展示效果是一致的

唯一的区别是shoToast在一定时间后,会自动关闭

showLoading需要手动调用hideLoading方法才会关闭loading弹框

wx.showLoading({
  title: 'loading...',
})

// 需要手动调用hideLoading方法来关闭loading弹窗
setTimeout(() => wx.hideLoading({}), 3000)

showActionSheet

LrrqPQ.png

wx.showActionSheet({
  itemList: ['拍照', '图库'],
  success(res) {
    // res中存在属性tapIndex 其索引值对应着itemList中对应索引位置的元素  
    console.log(res)
  }
})

分享

分享是小程序扩散的一种重要方式,小程序中有两种分享方式:

  1. 点击右上角的菜单按钮,之后点击转发
Page({
  // onShareAppMessage是在Page中和生命周期函数同级的方法
  onShareAppMessage() {
    // 返回自定义配置项
    return {
      title: '分享',
      // 默认分享进入的是首页
      // path属性可以决定分享的小程序点击进入后具体进入到那个页面
      path: '/pages/about/about.js'
    }
  }
})

onShareAppMessage可以设置的属性值如下:

LrrWbh.png

  1. 点击某一个按钮,直接转发
<!--
  当一个button组件的open-type属性被设置为share的时候
  点击该按钮会自动调用onShareAppMessage方法
-->
<button open-type="share">share</button>

登录

Lrr5MW.png

// 常量统一抽离到单独的位置,便于后期的维护和修改
const TOKEN = 'token'

App({
  // 将token定义为全局变量
  globalData: {
    token: ''
  },

  // 小程序是登录操作推荐在onLaunch方法中进行操作
  onLaunch: function () {
    // 1.先从缓冲中取出token
    const token = wx.getStorage(TOKEN)

    // 2.判断token是否有值
    if (token && token.length !== 0) { // 已经有token,验证token是否过期
      this.check_token(token) // 验证token是否过期
    } else { // 没有token, 进行登录操作
      this.login()
    }
  },
  
  check_token(token) {
    wx.request({
      // 自家后台服务器
      url: 'http://www.example.com/auth',
      method: 'post',
      header: {
        token
      },
      success: (res) => {
        if (!res.data.errCode) {
          // 获取到token后,将token存储到globalData中
          this.globalData.token = token;
        } else {
          // token过期了,重新登录
          this.login()
        }
      },
      fail: function(err) {
        console.log(err)
      }
    })
  },
  login() {
    wx.login({
      // 从微信服务器获取到的code有效期只有5分钟
      success: (res) => {
        // 1.获取code
        const code = res.code;

        // 2.将code发送给我们的服务器
        wx.request({
          url: 'http://www.example.com/login',
          method: 'post',
          data: {
            code
          },
          success: (res) => {
            // 1.取出token
            const token = res.data.token;

            // 2.将token保存在globalData中
            this.globalData.token = token;

            // 3.进行本地存储
            // 小程序的storage可以在小程序的调试器的storage选项卡中进行查看和修改
            wx.setStorage(TOKEN, token)
          }
        })
      }
    })
  }
})

页面跳转

navigator

<!-- 在navigator中的url参数所对应的那个路径必须是绝对路径,不可以是相对路径 -->
<navigator url="/pages/profile/profile" open-type="switchTab">跳转</navigator>

open-type的可选值

说明
navigate默认值 保留原本的页面,新的页面以入栈的方式进行插入,但是不能跳到 tabbar 页面
此操作是入栈操作,所以会在导航栏上出现回退按钮
redirectTo跳转到新的页面,但是不保存原本的页面,但是不能跳到 tabbar 页面
导航栏上不会出现回退按钮
switchTab跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面
多个tabBar的切换是通过底部tab栏进行切换的,所以导航栏上也不会出现回退按钮
reLaunch关闭所有页面后再打开到应用内的某个页面
导航栏上不会出现回退按钮
navigateBack关闭当前页面,返回上一页面或多级页面
<!-- 
  delta -> 指定返回的层级,open-type必须是navigateBack才生效
  如果delta的值大于历史记录栈中的长度,那么就会将栈的页面,除了栈底的那个页面外全部弹出
  也就是回到最开始的那个页面
-->
<navigator open-type="navigateBack" delta="2">回退</navigator>

在页面跳转的时候传递参数

  • 首页 -> 详情页 ---- 页面跳转 --- 使用URL中的query字段
  • 详情页 -> 首页 --- 页面回退 --- 在详情页内部拿到首页的页面对象,直接修改数据

Lrsgey.png

页面跳转

<!-- 在进行路由跳转的时候,如果需要传递参数可以使用路径的query参数进行传参 -->
<navigator url="/pages/categories/categories?name=Klaus&age=23">跳转</navigator>
Page({
  // 如果页面跳转过来的时候携带了query参数
  // 那么对应的query参数会被转换为对象并作为onLoad方法的参数被传入
  onLoad(options) {
    console.log(options)
  }
})

页面回退

因为页面回退即可用过按钮来进行回退,也可以通过导航栏的返回键进行回退

也就是页面的回退方式有很多,所以小程序如果需要在页面回退的时候传入对应的参数

可以在onUnload方法中实现对应的逻辑

Page({
  onUnload() {
    // getCurrentPages方法可以获取到所有的活跃页面 --- 也就是在历史记录栈中的页面
    const pages = getCurrentPages()

    // 向前一个页面传递数据
    // pages.length的值为活跃page的个数,而数组的索引从0开始计算
    // 所以pages中最后一个page也就是当前page在pages中的索引是pages.length-1
    // 因此上一个page在pages中的索引为pages.length-2
    const page = pages[pages.length - 2] 

    page.setData({
      msg: {
        name: 'Klaus',
        age: 23
      }
    })
  }
})

通过wx的api来实现

在小程序中每一个naviagtor的跳转方式都对应这一个对应的wxAPI

Lrszsr.png

wx.navigateTo({
  url: '/pages/about/about?name=Klaus&age=23'
})
// 如果不传配置对象,那么默认的delta属性的值就是1,也就是回退到上一个page页面
wx.navigateBack({
  // delta的设置规则和当open-type为naviagorBack的navigator的delta属性的设置规则一致
  delta: 2
})

home.js

Page({
  handleTap() {
    wx.navigateTo({
      url: '/pages/categories/categories?name=Klaus&age=23',
      events: {
        // 设置事件监听
        getMsg(v) {
          console.log(v)
        }
      }
    })
  }
})

categories.js

Page({
  onLoad(options) {
    // 获取传入的参数
    console.log(options)

    // 获取对应的事件总线
    const eventChannel = this.getOpenerEventChannel()
    // 通过事件总线向跳转过来的源页面(这里就是home页面)
    // 去触发对应events中注册的事件,以达到回传数据的目的
    eventChannel.emit('getMsg', {
      msg: 'Hello World'
    })
  }
})