组件
如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展
但如果,我们将一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了
-
我们将一个完整的页面 分成很多个组件
-
每个组件都用于实现页 面的一个功能块
-
每一个组件又可以进行细分
自定义组件由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'
}
})
组件内不能使用id选择器、属性选择器、标签选择器,只能使用class选择器
页面中可以使用class选择器,id选择器、属性选择器、标签选择器
默认情况下,如果在外部使用了标签选择器(类似于view { ... }) 是会对组件内部样式产生影响
但是class选择器,id选择器、属性选择器并不会对组件内部样式产生影响
所以推荐在小程序中统一使用class选择器进行样式的设置
页面通信
很多情况下,组件内展示的内容(数据、样式、标签),并不是在组件内写死的,而且可以由使用者来决定
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('组件被移除')
}
}
})
系统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)
}
})
在小程序中,可能多个地方都需要发送对应的网络请求,
所以我们需要对网络请求进行封装,以便于整个项目中的网络请求调用的都是自己封装的接口
从而实现请求和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
wx.showToast({
title: 'toast',
icon: 'loading',
duration: 3000, // toast出现的持续时间,默认值是1500
mask: true // 出现遮罩层,在toast出现的时候,不允许和toast层以下层级的元素进行交互
})
showModal
wx.showModal({
title: 'title',
content: 'content',
success(res) {
// res对象中存在属性
// 1. cancel --- 值为true的时候,用户退出了弹窗
// 2. confirm --- 值为true的时候,用户确认了弹窗
console.log(res)
}
})
showLoading
showLoding和icon属性为loading的showToast的展示效果是一致的
唯一的区别是shoToast在一定时间后,会自动关闭
showLoading需要手动调用hideLoading方法才会关闭loading弹框
wx.showLoading({
title: 'loading...',
})
// 需要手动调用hideLoading方法来关闭loading弹窗
setTimeout(() => wx.hideLoading({}), 3000)
showActionSheet
wx.showActionSheet({
itemList: ['拍照', '图库'],
success(res) {
// res中存在属性tapIndex 其索引值对应着itemList中对应索引位置的元素
console.log(res)
}
})
分享
分享是小程序扩散的一种重要方式,小程序中有两种分享方式:
- 点击右上角的菜单按钮,之后点击转发
Page({
// onShareAppMessage是在Page中和生命周期函数同级的方法
onShareAppMessage() {
// 返回自定义配置项
return {
title: '分享',
// 默认分享进入的是首页
// path属性可以决定分享的小程序点击进入后具体进入到那个页面
path: '/pages/about/about.js'
}
}
})
onShareAppMessage可以设置的属性值如下:
- 点击某一个按钮,直接转发
<!--
当一个button组件的open-type属性被设置为share的时候
点击该按钮会自动调用onShareAppMessage方法
-->
<button open-type="share">share</button>
登录
// 常量统一抽离到单独的位置,便于后期的维护和修改
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字段
- 详情页 -> 首页 --- 页面回退 --- 在详情页内部拿到首页的页面对象,直接修改数据
页面跳转
<!-- 在进行路由跳转的时候,如果需要传递参数可以使用路径的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
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'
})
}
})