开发实战篇之七
前言
实战篇内容参考:
1、Lin Ui开源组件源码分析。https://doc.mini.talelin.com/
2、开发过程遇到问题。
0、知识点补充
0.1 $emit()函数
// 点击遮罩层关闭popup
onClose() {
// 父组件通知close事件
this.$emit('close');
}
0.2 wxs
1、每一个
.wxs
文件和<wxs>
标签都是一个单独的模块。
2、每个模块都有自己独立的作用域。即在一个模块里面定义的变量与函数,默认为私有的,对其他模块不可见。
3、一个模块要想对外暴露其内部的私有变量与函数,只能通过module.exports
实现。
- ⚠️即使将
wxs
写在wxml
中,使用时仍需要使用内置的module
对象exports
。 - wxs可以写在
<wxs></wxs>
包裹中和.wxs
文件中。
<!-- 导出状态可以被使用 -->
module.exports = {
setStatus:setStatus,
statusStyle:statusStyle,
dotStyle:dotStyle,
isReverse:isReverse
}
0.3 ⚡组件间关系
详情请参照微信小程序官方文档—组件间关系:
组件间关系详解
⚠️组件间关系可以通过data-
传递数据,但是要进行更复杂的联系,就需要使用组件的relations
属性。例如下面的steps
进度条组件中的子组件step
进度条元素。
<x-steps direction="column" step-min-height="160">
<x-step title="已支付" describe="11:30"></x-step>
<x-step title="备餐中" describe="12:30"></x-step>
<x-step title="已出餐" describe="13:30"></x-step>
</x-steps>
使用relations
属性解决steps
和step
之间的关系:
选项 | 类型 | 是否必填 | 描述 |
---|---|---|---|
type | String | 是 | 目标组件的相对关系,可选值为parent、child、ancestor、descendant |
linked | Function | 否 | 关系生命周期函数,当关系被建立在页面节点树中时触发,触发时机在组件attached生命周期之后 |
linkChanged | Function | 否 | 关系生命周期函数,当关系在页面节点树中时发生改变触发,触发时机在组件moved生命周期之后 |
unlinked | Function | 否 | 关系生命周期函数,当关系脱离页面节点树中时触发,触发时机在组件detached生命周期之后 |
// steps.js父组件
relations: {
'../step/step': {
type: 'child', // 关联的目标节点为子节点
linked(target) { // 每次有step被插入时执行,target是子节点实例对象,this还是父组件实例对象,触发在该节点attached之后。
this._initSteps(); // 调用初始化steps数据的方法。
},
unlinked(target) { // 每次有step被移除时执行,触发在该节点detached之后。
this._initSteps();
}
},
},
...
observers: {
// 手动控制进度条的变化,需要更新每个step的状态,这就需要监听activeIndex的变化。
'activeIndex': function () {
this._initSteps();
}
},
/**
* 组件的方法列表
*/
methods: {
_initSteps() {
// 1、获取自定义组件的实例对象
const query = wx.createSelectorQuery().in(this);
// 2、找到steps组件执行{...}中方法
query.select('.steps-container').boundingClientRect().exec(res => {
// 3、获取该steps下所有得到子节点step
let steps = this.getRelationNodes('../step/step');
this.data.length = steps.length;
if (this.data.length > 0) {
// 4、调用子组件step中更新数据方法updateDataChange()
// index为当前的step,不一定是active的step
steps.forEach((step, index) => {
// console.log(steps.length); // 3
// console.log(res[0].width); // 375
// console.log(this.data.status);
step.updateDataChange({
// 当前index会改变
index,
...this.data,
stepsWidth: res[0].width
});
});
}
});
}
}
// step.js子组件
relations: {
// 绑定与steps的关系
'../steps/steps': {
type: 'parent'
}
},
/**
* 组件的方法列表
*/
methods: {
// 与父组件通信并绑定相关配置数据
updateDataChange(options) {
this.setData({
...options
});
}
}
1、steps进度条组件
总体分析steps的编写思路:
- 需求:进度条是由多个进度单元组成(个数应该由用户定制);并且每个进度单元样式应可以自定义;进度应该可以手动控制;数据绑定应该较为简单,尽可能封装样式和属性等,到组件中。
- 采用整体
steps
父组件,传入进度条所需的整体大小、当前进度(activeIndex
)、当前进度状态status
(process
还是error
)、布局方向、进度条节点样式。子组件step
主要进行数据绑定、根据父组件设置的布局、当前进度、进度条样式等属性进行设置布局和样式。
对steps父组件和step子组件进行详细分析:
steps
组件主要是一个布局steps-container
,然后在布局中使用多插槽添加子组件step
(⚠️slot
非常灵活,可以很好的分割steps
和step
,减少steps
和step
的耦合度)。steps
组件只用来控制step
排列的方向。(flex
布局的flex-direction: row-reverse或flex-direction: column-reverse
来实现)。当然在step
中需要对逆序之后的样式进行调整,具体就不展开说了。steps
和step
的联系使用relations
属性关联,数据传递在父组件中使用_initSteps()
调用子组件的updateDataChange()
绑定数据。
1.1、step进度条单元的骨架文件wxml
- 整体布局:首先根据父组件传入的属性
direction
指定布局方向。如果是横向,就根据进度条宽度个数,平均分配宽度。纵向就设定最小高度。 - 整体基本由三个部分组成:进度条icon、进度条内容、进度条line。
- 进度条icon可以由
custom
属性,控制是否自定义设置。如果不需要自定义设置,就按默认样式step-type step-icon
,这里对每个step
都需要判断当前的状态status
,共有“process”、“wait”、“finish”和“error”四种状态,“error”是“process”等价,但由用户设定。
⚠️这里通过Mustache
语法判断icon/dot
样式,如果是icon
是哪一种icon
。具体逻辑是“如果设置了dot
就按dot
的样式,否则就设置成step-icon
,空心圆圈的样式。”
{{dot && !icon?'step-dot-'+currentStatus.setStatus(activeIndex,index,status) + ' step-dot':'step-'+currentStatus.setStatus(activeIndex,index,status)+' step-icon'}}
⚠️这里就不得不提到获取状态的方法:setStatus(activeIndex, index, status)
写在<wxs>
中的简单判断,如果当前位置index
和当前进度位置activeIndex
相同,那么,当前状态就是“process/error”。当index
大于activeIndex
时,那代表已完成进度“finish”,反之小于就是还没轮到处理,就是“wait”。
⚠️下面的逻辑就是有设置icon
就按icon
,没有就显示对应的序号
。
- 进度条内容title根据
direction
设定内容方向step-content-direction
,然后是title
和describe
部分。标题字体颜色,根据是否是activeIndex
,置为step-title-process
或step-title
。 - 进度条line根据
direction
设定line的方向step-line-direction
,根据是否是最后一个step
决定显示与否。
<!--components/step/step.wxml-->
<view class="step {{'step-'+direction}} x-class"
style="{{direction==='row'?'width:'+(1/length)*stepsWidth+'px':'min-height:'+stepMinHeight+'rpx'}}">
<!-- 进度条icon样式 -->
<view class="step-container x-step-class {{'step-container-'+direction}}">
<!-- 进度条icon自定义样式 -->
<view wx:if="{{custom}}" class="step-custom">
<slot name="dot" />
</view>
<!-- 进度条icon样式---进度数字/icon选择 -->
<view wx:else
class="x-step-class {{dot && !icon?'step-dot-'+currentStatus.setStatus(activeIndex,index,status) + ' step-dot':'step-'+currentStatus.setStatus(activeIndex,index,status)+' step-icon'}}"
style="{{dot ? currentStatus.dotStyle(activeIndex,index,color) : currentStatus.statusStyle(activeIndex,index,color,status)}}">
<x-icon wx:if="{{icon}}" name="{{icon}}" size="{{iconSize}}" color="{{(currentStatus.setStatus(activeIndex,index,status)==='process'?'#3963bc':iconColor)}}"/>
<block wx:if="{{!dot && !icon}}">
<view wx:if="{{currentStatus.setStatus(activeIndex,index,status)==='error' || currentStatus.setStatus(activeIndex,index,status)==='finish'}}"
class="iconfont icon-{{currentStatus.setStatus(activeIndex,index,status)}}"></view>
<block wx:else>{{index+1}}</block>
</block>
</view>
</view>
<!-- 进度条内容title -->
<view class="step-content {{'step-content-'+direction}}">
<view class="x-title-class {{activeIndex===index?'step-title-process':'step-title'}}">
{{title}}
</view>
<view class="x-describe-class step-describe">
{{describe}}
<slot name="describe"/>
</view>
</view>
<!-- 进度条line -->
<view class="step-line x-line-class {{'step-line-'+ direction}} {{currentStatus.isReverse(reverse,activeIndex,index)?'step-line-finish':'step-line-wait'}}"
style="{{activeIndex>index?('background-color:'+color):''}}"
wx:if="{{reverse?(index!==0):(length!==index+1)}}">
</view>
</view>
<!-- step status设置 -->
<wxs module="currentStatus">
var setStatus = function(activeIndex,index,status){
if(activeIndex === index){
return status || 'process'
}else if(activeIndex > index){
return 'finish'
}else {
return 'wait'
}
}
var statusStyle = function(activeIndex,index,color,status) {
if(activeIndex == index){
return status=='error'?'':('background-color:' + color)
}else if(activeIndex > index){
return ('border-color:' + color + ';color:' + color)
}else {
return ''
}
}
var dotStyle = function(activeIndex,index,color) {
if(activeIndex>=index){
return ('background-color:' + color)
}else {
return ''
}
}
var isReverse = function(reverse,activeIndex,index) {
if(reverse){
return (activeIndex>=index)
}
return (activeIndex>index)
}
module.exports = {
setStatus:setStatus,
statusStyle:statusStyle,
dotStyle:dotStyle,
isReverse:isReverse
}
</wxs>
1.2、step进度条单元的js
// components/step/step.js
Component({
/**
* 组件的属性列表
*/
externalClasses: [
'x-class',
'x-step-class',
'x-title-class',
'x-describe-class',
'x-line-class'
],
options: {
multipleSlots: true
},
relations: {
// 绑定与steps的关系
'../steps/steps': {
type: 'parent'
}
},
properties: {
icon: String,
title: String,
describe: String,
iconSize: {
type: Number,
value: 24
},
iconColor: String,
// 是否自定义步骤条元素step
custom: Boolean
},
/**
* 组件的初始数据
*/
data: {
},
/**
* 组件的方法列表
*/
methods: {
// 与父组件通信并绑定相关配置数据
// 父组件调用绑定数据
updateDataChange(options) {
// console.log(options.status);
this.setData({
...options
});
}
}
})
1.3、step进度条单元的样式文件wxss
/* components/step/step.wxss */
.step {
display: flex;
position: relative
}
.step-custom {
z-index: 2
}
.step-row {
flex-direction: column;
align-items: center
}
.step-column {
flex-direction: row;
padding-left: 30rpx;
box-sizing: border-box
}
.step-container {
display: flex;
justify-content: center;
z-index: 2;
align-items: center;
background-color: #fff
}
.step-container-row {
width: 70rpx;
height: 40rpx
}
.step-container-column {
height: 60rpx;
width: 40rpx
}
.step-icon {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 22rpx;
transition: .5s;
box-sizing: border-box
}
.step-process {
color: #fff;
background-color: #3963bc;
border-radius: 50%
}
.step-error {
background-color: #fff;
border: 1px solid #f4516c;
color: #f4516c;
border-radius: 50%
}
.step-finish {
background-color: #fff;
color: #3963bc;
border: 1px solid #3963bc;
border-radius: 50%
}
.step-wait {
background-color: #fff;
color: #c4c9d2;
border: 1px solid #c4c9d2;
border-radius: 50%
}
.step-dot {
width: 24rpx;
height: 24rpx;
border-radius: 50%;
transition: .5s
}
.step-dot-process {
background-color: #3963bc
}
.step-dot-wait {
background-color: #c4c9d2
}
.step-dot-error {
background-color: #f4516c
}
.step-dot-finish {
background-color: #3963bc
}
.step-line {
background: #c4c9d2;
transition: .5s;
position: absolute;
z-index: 0
}
.step-line-row {
height: 2rpx;
width: 100%;
left: 50%;
top: 18rpx
}
.step-line-column {
width: 2rpx;
height: 100%;
top: 20rpx;
left: 48rpx
}
.step-line-wait {
background-color: #e8e8e8
}
.step-line-finish {
background-color: #3963bc
}
.step-content {
display: flex;
flex-direction: column
}
.step-content-row {
align-items: center;
width: 100%
}
.step-content-column {
margin-left: 15rpx
}
.step-title {
color: #595959;
font-size: 26rpx;
line-height: 40rpx;
margin: 10rpx 0
}
.step-title-process {
color: #333;
font-size: 28rpx;
line-height: 40rpx;
margin: 10rpx 0
}
.step-describe {
color: #8c98ae;
font-size: 22rpx
}
@font-face {
font-family: iconfont;
src: url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAK8AAsAAAAABpQAAAJvAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCDBgp8gQsBNgIkAwwLCAAEIAWEbQc2G9EFyK4wbuGJaCgrSmkBCSe/ZeUtviBzEXztx/bd3f+oNEiuzaerJhKN6WQSJFqlZELxTOZqTS9l0hsflgVC/bD3z/dzv8lMXAksyeryeByfVkhgV93EvwdIwC+b/hetH5jPyAA+z+X0JtCBXONbltMcw496AcYBBbYnRoEEXEQBcAqyOwK15PsEmg2KiewMTy6yCoWzLBD3lqGwilJKUaRqo1C37C3iRaUxPaUnPMffj18LUUFSZc7a3bshnfV++DD/2GVISBPo4QoZU5hCXLeWThULiSvWbCrpb64VIS2VhYlVpqU+6B8vETWnsxWM0XLiI19G8OGABDKoy5VBzDRje98gRHertt+2fLx2cdC6ZJwcnK226Ifh0uzy4vzK8sLSytxprqHAHexGbz8/6za6C3ePjwW29viP3ailnm7Zz+SntAd5IV0IBHuvvy3Xd/41thTw/osdBRahcefh4Gepwb5iah13LrrKV9mRrBsIJKhCjvrnxLq9Trf0wb2ERn0TIg0GcmSNhsjCTqHSYgm1RutoNmnkcIseUrEoTZiwRRA63SBp94as0zNZ2BdU+v2h1hnFaHYczWe2GAmrakCiQqiDmgdLEz73vFSVdM+jkXUVWdTG/DLK0MpCOpGqVkfQR7nEhjBnZIg4cCk8GCaPoesKCKSwUaOESRS0J5O87UUJTXisyjYJKQjSAVUesGgEHw+Hk6ra5+chQ5ZLIXsEDfllSApZ44O0hBRAjph8SPAoz4RyDBmEcICTBA8YJrOQyyWAoH2WDWlIgjmhEGiXdAdx1JTY3uL93xFo5kzMkSJHUT+g+5BNxVpWMAYAAA==') format('woff2')
}
// finish和error图标使用iconfont图标
.iconfont {
font-family: iconfont !important;
font-size: 24rpx;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale
}
.icon-finish:before {
content: "\e600"
}
.icon-error:before {
content: "\e6cf"
}