背景在业务中有时需要以周为单位选择日期，将某个周的开始和结束日期回显在界面上。这种需求在pc端上有现成的组件可以使用，比如elementUI的el-date-picker组件即可实现。但是在原生小程序中并没有现成可用的组件实现这种需求，那么只能自定义一个这样的周组件或者基于第三方日历组件二次封装实现一个周组件。二次封装周组件准备工作熟悉第三方日历组件这里采用的是小历同学开发的开源日历组件进行二次封装，使用说明文档可点击第三方日历组件需要熟悉的方法和属性日历组件中的禁用日期属性disableMode、每周第一天是周日还是周一属性firstDayOfWeek、默认选中的日期属性defaultDay、是否开启日期范围选择模式属性chooseAreaMode、取消所有选中日期的方法cancelSelectedDates、选中指定日期范围的方法chooseDateArea、还有日期选择后的事件afterTapDay。获取周时间范围的算法 let year=e.detail.year let month=e.detail.month month=month<10?'0'+month:month let day=e.detail.day day=day<10?'0'+day:day let date=year+'-'+month+'-'+day let weekOfday = moment(date, "YYYY-MM-DD").format("E"); //计算今天是这周第几天 let startTime = moment(date) .subtract(weekOfday - 1, "days") .format("YYYY-MM-DD"); //周一日期 let endTime = moment(date).weekday(7).format("YYYY-MM-DD"); //周日日期 if(moment(date).day()==0){ endTime=date }实现思路（1）当组件第一次加载进来后，根据当前时间算出本周的开始和结束时间范围，然后调用组件的chooseDateArea方法实现周日期的默认选中。（2）当点击改变日历组件上的日期后，调用afterTapDay事件的回调函数afterTapDay：计算出所选日期所在周的日期范围，然后调用chooseDateArea方法将指定范围的日期选中。注意这里要在执行回调函数afterTapDay开始要调用日历组件的cancelSelectedDates方法取消所有已选择的日期，否则会导致周日期选择错乱。（3）在回调函数最后将计算出的周范围的日期放入变量中，当点击确定选择日期后回显到页面并把筛选日期范围传递给调用者（父组件）实现代码html代码<view class="select-period" hidden="" catchtouchmove="return"> <view class="select_top"> <view class="t_singer t2"></view> </view> <view class="sg"></view> <view> <view style="background: #f5f5f5;padding-top: 15rpx;" wx:if="{{selectPeriod=='week'}}"> <view class="date"> <calendar style="height: 100%;" preYear="true" nextYear="true" bind:afterTapDay="afterTapDay" id="mycalendar" calendarConfig="{{calendarConfig}}" bind:whenChangeMonth="whenChangeMonth" ></calendar> <slot> </slot> </view> <view class="btns" style="margin-top: 29rpx;"> <view class="cancel" bindtap="cancel">取消</view> <view class="ok" bindtap="setlect_date">确定</view> </view> </view> <view class="monthly" style="height: {{selectPeriod=='month'?'757rpx':'360rpx'}};" hidden="{{selectPeriod=='week'}}"> <view class="m_year"> <view class="m_title">选择年份</view> <view class="select_year"> <image src="../../../image/y_right.png" bindtap="reduce" class="m_img" style="transform: rotate(180deg);"></image> <view class="year">{{yearRes}}年</view> <image src="../../../image/y_right.png" bindtap="add" class="m_img"></image> </view> </view> <view class="date-box"> <view class="m_month" hidden="{{selectPeriod!='month'}}"> <view class="m_title">选择月份</view> <view class="month_list"> <view wx:for="{{monthList}}" bindtap="selectMonth" data-value="{{item.value}}" data-canSel="{{item.avaliable}}" wx:key="index" class="m_singer {{monthRes==item.value?'m_singer_active':''}}" style="color: {{item.avaliable?'rgba(0,0,0,0.8)':'#c7c7c7'}};">{{item.name}}</view> </view> </view> <!-- <view class="week-box" wx:if="{{selectPeriod=='week'}}"> <view class="m_month" hidden="{{selectPeriod!='week'}}"> <view class="m_title">选择周</view> <view class="week_list"> <view wx:for="{{weekData}}" bindtap="selectMonth" data-value="{{item.value}}" wx:key="index" class="week-item {{weekRes==item.value?'m_singer_active':''}}" data-endTime="{{item.endTime}}" > <view>{{item.name}}</view> <view class="date-range">{{item.dates}}</view> </view> </view> </view> </view> --> <view class="btns" style="margin-top: 29rpx;"> <view class="cancel" bindtap="cancel">取消</view> <view class="ok" bindtap="setlect_month_two">确定</view> </view> </view> </view> </view> </view>css代码/* pages/common/select-period/select-period.wxss */ .select-period{ position: fixed; left: 0; top: 200rpx; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.6); z-index: 999999999999; } .period-content{ width: 100%; height: 413rpx; background: #F5F5F5; } .lable{ display: block; width: 300rpx; height: 90rpx; display: flex; margin: 30rpx 0 0 30rpx; } .radioGroup{ font-size: 28rpx; color: rgba(1, 1, 1, 0.8); font-weight: 400; width: 690rpx; height: 258rpx; margin: 0 auto; display: flex; justify-content: space-between; flex-wrap: wrap; background: #fff; } .btns{ width: 750rpx; height: 120rpx; background-color: #fff; display: flex; padding: 0 31rpx; box-sizing: border-box; justify-content: space-between; align-items: center; position: relative; z-index: 9999; } .cancel{ width: 320rpx; height: 80rpx; background: #FFFFFF; border: 1px solid rgba(0, 0, 0, 0.3); border-radius: 40rpx; font-size: 32rpx; font-family: Source Han Sans SC; font-weight: 400; color: #646464; text-align: center; line-height: 80rpx; position: relative; z-index: 999999999; } .ok{ width: 320rpx; height: 80rpx; background: #505BD2; border-radius: 40rpx; font-size: 32rpx; font-family: Source Han Sans SC; font-weight: 400; color: #fff; text-align: center; line-height: 80rpx; position: relative; z-index: 999999999; } .monthly{ width: 100%; height: 757rpx; background: #F5F5F5; padding-top: 15rpx; box-sizing: border-box; } .m_year{ width: 690rpx; height: 210rpx; background: #FFFFFF; border-radius: 6px; margin: 0 auto; } .m_title{ display: block; font-family: Source Han Sans SC; font-weight: 400; color: rgba(1, 1, 1, 0.8); line-height: 68rpx; font-size: 28rpx; padding-left: 31rpx; border-bottom: 1rpx solid rgba(0, 0, 0, 0.1); } .year{ width: 268rpx; height: 60rpx; background: #FFFFFF; /* border: 1px solid rgba(0, 0, 0, 0.15); */ border-radius: 4rpx; text-align: center; line-height: 60rpx; font-size: 28rpx; font-family: Source Han Sans SC; font-weight: 400; color: rgba(0, 0, 0, 0.8); } .select_year{ display: flex; height: 142rpx; align-items: center; padding: 0 173rpx; box-sizing: border-box; } .reduce{ width: 0; height: 0; border-left: 10rpx solid transparent; border-top: 10rpx solid transparent; border-right: 10rpx solid #000; border-bottom: 10rpx solid transparent; margin-right: 25rpx; } .reduce_dis{ border-right: 10rpx solid #c8c8c8; } .add{ border-left: 10rpx solid #000; border-right: 10rpx solid transparent; margin-left: 25rpx; } .add_dis{ border-left: 10rpx solid #c8c8c8; } .m_month{ width: 690rpx; height: 360rpx; background: #FFFFFF; border-radius: 6px; margin: 0 auto; margin-top: 20rpx; } .month_list{ display: flex; justify-content: space-between; flex-wrap: wrap; } .m_singer{ width: 68rpx; height: 68rpx; background: #fff; border-radius: 50%; font-size: 26rpx; line-height: 68rpx; text-align: center; font-family: Source Han Sans SC; font-weight: 400; color: rgba(0,0,0, 0.8); margin: 16rpx 40rpx; } .m_singer_active{ color: rgba(255,255,255, 0.8); background: #505BD2; } .deviceType{ width: 100%; background: #F5F5F5; height: 685rpx; padding-top: 15rpx; box-sizing: border-box; } .cont{ width: 690rpx; height: 522rpx; background: #FFFFFF; border-radius: 6rpx; margin: 0 auto; padding: 30rpx 20rpx; box-sizing: border-box; } .cont_t{ display: flex; justify-content: space-between; flex-wrap: wrap; } .type_singer{ width: 310rpx; height: 76rpx; background: #F5F5F5; border-radius: 38rpx; font-size: 26rpx; font-family: Source Han Sans SC; font-weight: 400; color: #000000; text-align: center; line-height: 76rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 0 26rpx; box-sizing: border-box; margin-bottom: 20rpx; } .type_singer_active{ background: #505BD2; color: #fff; } .date{ background: #fff; width: 690rpx; /* height: 563rpx; */ border-radius: 6rpx; margin: 0 auto; } .select_top{ width:750rpx; height: 44rpx; padding: 0 31rpx; box-sizing: border-box; position: absolute; left: 0; top: -63rpx; } .select_toptwo{ width:750rpx; height: 44rpx; padding: 0 31rpx; box-sizing: border-box; position: fixed; left: 0; top: 137rpx; z-index:9999999999; } .t_singer{ width: 400rpx; height: 100%; background: #f5f5f5; } .t_active{ display: none; } .t1{ position: absolute; left: 30rpx; top: 0; } .t2{ position: absolute; left: 320rpx; top: 0; } .t3{ position: absolute; left: 512rpx; top: 0; } .sg{ position: absolute; left: 0; top: -20rpx; width: 750rpx; height: 20rpx; background: #F5F5F5; border: none !important; } .check { width: 44rpx; height: 44rpx; background: #fff; display: flex; justify-content: center; align-items: center; border: 1px solid rgba(0, 0, 0, 0.15); border-radius: 50%; margin-right: 10rpx; } .checks{ background: #505BD2 ; } .calendar { width: 100%; text-align: center; font-size: 30rpx; box-sizing: border-box; } .calendar_header { line-height: 70rpx; font-size: 30rpx; text-align: left; padding: 0 60rpx; box-sizing: border-box; background: #fff; height: 84rpx; border-bottom: 1rpx solid rgba(0, 0, 0, 0.1); } .header_left { flex: 1; /* display: inline-flex; */ justify-content: space-between; height: 84rpx; align-items: center; font-size: 24rpx; font-family: Source Han Sans SC; font-weight: 400; color: rgba(0, 0, 0, 0.6); } .header_year{ width: 158rpx; height: 44rpx; background: #FFFFFF; border: 1rpx solid rgba(0, 0, 0, 0.15); border-radius: 4rpx; line-height: 44rpx; margin-right: 20rpx; position: relative; padding-left: 22rpx; box-sizing: border-box; } .header_year::after{ content: ""; width: 10rpx; height: 10rpx; position: absolute; right: 15rpx; top:12rpx; border-right: 1rpx solid #B4B4B4; border-bottom: 1rpx solid #B4B4B4; transform: rotate(45deg); transition: all .2s ease; } .s_ay::after{ content: ""; width: 10rpx; height: 10rpx; position: absolute; right: 15rpx; top:18rpx; border-right: 1rpx solid #B4B4B4; border-bottom: 1rpx solid #B4B4B4; transform: rotate(225deg); transition: all .2s ease; transform: rotate(225deg); } .header_month{ width: 128rpx; height: 44rpx; background: #FFFFFF; border: 1rpx solid rgba(0, 0, 0, 0.15); border-radius: 4rpx; line-height: 44rpx; position: relative; padding-left: 30rpx; box-sizing: border-box; } .header_month::after{ content: ""; width: 10rpx; height: 10rpx; position: absolute; right: 15rpx; top:12rpx; border-right: 1rpx solid #B4B4B4; border-bottom: 1rpx solid #B4B4B4; transform: rotate(45deg); transition: all .2s ease; } .header_left .header_preMonth, .header_nextMonth { min-width: 50rpx; text-align: center; } .header_right { flex: 1; text-align: right; padding: 0 20px; display: inline-block; float: right; } .header_title { min-width: 300rpx; text-align: center; font-size: 28rpx; font-family: Arial; font-weight: 400; color: rgba(0, 0, 0, 0.8); } .calendar_weeks { display: flex; text-align: center; padding: 20rpx 10rpx; box-sizing: border-box; /* border-top: 1rpx solid #e0e0e0; */ /* border-bottom: 1rpx solid #e0e0e0; */ /* background: #f5f5f5; */ } .weeks_item { flex: 1; font-size: 24rpx; font-family: Source Han Sans SC; font-weight: 400; color: rgba(0, 0, 0, 0.5) !important; } .calendar_days { display: flex; flex-wrap: wrap; padding: 10rpx 10rpx 20rpx 10rpx; box-sizing: border-box; } .days_grid { display: inline-block; width: 14.28571428571429%; line-height: 70rpx; position: relative; z-index: 1; } .days_grid .pre, .next { color: #ccc; } .thisDayIsSelect { background: rgb(49, 120, 228); border-radius: 50%; color: #fff; width: 70rpx; height: 70rpx; font-size: 28rpx; font-family: Arial; font-weight: 400; color: rgba(255, 255, 255, 0.8) !important; } .thisDayNoSelect { background: rgb(239, 241, 245); border-radius: 50%; width: 72%; margin: 0 auto; } .calendar_timer { border-top: 1rpx solid #e0e0e0; border-bottom: 1rpx solid #e0e0e0; display: flex; align-items: center; } .timer_picker { flex: 1 } .timer_picker__view { display: flex; justify-content: flex-start; align-items: center; flex: 1; } .timer_picker__view text { padding: 10px; flex: 0.2; } .timer_picker__view input { padding: 10px; flex: 0.8; text-align: left; } .timer_now { padding: 10px; flex: 0.3; } .putThe{ color: #989898 !important; } .noSelect{ font-family: Source Han Sans SC; font-weight: 400; color: rgba(0, 0, 0, 0.5) !important; } .m_img{ width: 16rpx; height: 24rpx; } .y_img{ width: 23rpx; height: 24rpx; } .reduce{ width: 0; height: 0; border-left: 10rpx solid transparent; border-top: 10rpx solid transparent; border-right: 10rpx solid #000; border-bottom: 10rpx solid transparent; } .reduce_dis{ border-right: 10rpx solid #c8c8c8; } .add{ border-left: 10rpx solid #000; border-right: 10rpx solid transparent; } .add_dis{ border-left: 10rpx solid #c8c8c8; } .days_item{ display: block; width: 70rpx; height: 70rpx; text-align: center; line-height: 70rpx; border-radius: 50%; } .week-box{ background-color: white; height: 600rpx; } .date-box{ background-color: white; } .week_list{ display: flex; justify-content: start; flex-wrap: wrap; overflow: scroll; max-height: 520rpx; margin-top: 10rpx; } .week-item{ width: 33.3%; background: #fff; font-size: 26rpx; text-align: center; font-family: Source Han Sans SC; font-weight: 400; color: rgba(0,0,0, 0.8); margin: 15rpx 0; } .date-range{ font-size: 20rpx; }js代码// pages/common/datetime-picker/datetime-picker.js let moment = require('../../../utils/moment.min'); let dayjs=require('../../../utils/dayjs.min') import {chooseDateArea,cancelSelectedDates} from '../weekcalendar/main' Component({ /** * 组件的属性列表 */ properties: { selectPeriod:{ type:String, value:'month' }, selectDateShow:{ type:Boolean, value:false }, dateRange:{ type:Array, value:[] } }, /** * 组件的初始数据 */ data: { yearRes:moment().format("YYYY"), monthRes:moment().format("MM"), monthList:[ {name:'1月',value:'01'},{name:'2月',value:'02'},{name:'3月',value:'03'},{name:'4月',value:'04'}, {name:'5月',value:'05'},{name:'6月',value:'06'},{name:'7月',value:'07'},{name:'8月',value:'08'}, {name:'9月',value:'09'},{name:'10月',value:'10'},{name:'11月',value:'11'},{name:'12月',value:'12'}, ], startTime:'', endTime:'', dateName:'', calendarConfig:{ defaultDay:moment().subtract(1, 'days').format('YYYY-MM-DD'), disableMode: { // 禁用某一天之前/之后的所有日期 type: 'after', // [‘before’, 'after'] date:moment(new Date()).day()==0?moment().format('YYYY-MM-DD'):moment().weekday(7).format("YYYY-MM-DD"), // 无该属性或该属性值为假，则默认为当天 // date:false }, chooseAreaMode:false, firstDayOfWeek: 'Mon', }, weekData:[], //周日历数据 weekRes:'' }, ready() { let tdM = this.getDateYMD().split('-')[1]*1; let mt = this.data.monthList; mt.forEach((m,i)=>{ let mv = m.value*1; if(mv > tdM){ m.avaliable = false; }else{ m.avaliable = true; } }) this.setData({ monthList:mt }) }, /** * 组件的方法列表 */ methods: { getDateYMD(val){ let date = null; if(val){ date = new Date(val); }else{ date = new Date(); } let y = date.getFullYear(); let m = date.getMonth() + 1; let d = date.getDate(); let M,D; M = m<10?'0'+m:m; D = d<10?'0'+d:d; let today = y+'-'+M+'-'+D; return today; }, // whenChangeMonth(e){ console.log(e) // chooseDateArea([this.data.startTime,this.data.endTime]).then(dates => { // console.log('choosed dates: ', dates); // }); }, // 选择周日期回调 afterTapDay(e){ // this.setData({ // 'calendarConfig.chooseAreaMode':false // }) cancelSelectedDates() let year=e.detail.year let month=e.detail.month month=month<10?'0'+month:month let day=e.detail.day day=day<10?'0'+day:day let date=year+'-'+month+'-'+day // let startTime = dayjs(date) // .startOf("week") // .format("YYYY-MM-DD"); // let endTime = dayjs(date) // .endOf("week") // .format("YYYY-MM-DD"); // console.log(startTime,endTime) let weekOfday = moment(date, "YYYY-MM-DD").format("E"); //计算今天是这周第几天 let startTime = moment(date) .subtract(weekOfday - 1, "days") .format("YYYY-MM-DD"); //周一日期 let endTime = moment(date).weekday(7).format("YYYY-MM-DD"); //周日日期 // let endTime=moment().add(7-weekOfday-1, 'days').format('YYYY-MM-DD'); if(moment(date).day()==0){ endTime=date } this.setData({ dateName:startTime+'到'+endTime, startTime:startTime, endTime:endTime }) chooseDateArea([this.data.startTime,this.data.endTime]).then(dates => { console.log('choosed dates: ', dates); }); }, setWeek(date){ // let startTime = dayjs(date) // .startOf("week") // .format("YYYY-MM-DD"); // let endTime = dayjs(date) // .endOf("week") // .format("YYYY-MM-DD"); let weekOfday = moment(date, "YYYY-MM-DD").format("E"); //计算今天是这周第几天 let startTime = moment(date) .subtract(weekOfday - 1, "days") .format("YYYY-MM-DD"); //周一日期 let endTime = moment(date).weekday(7).format("YYYY-MM-DD"); //周日日期 // let endTime=moment().add(7-weekOfday-1, 'days').format('YYYY-MM-DD'); // 解决日期在周日的计算结束时间不正确问题 if(moment(date).day()==0){ endTime=date } console.log(startTime,endTime) this.setData({ dateName:startTime+'到'+endTime, startTime:startTime, endTime:endTime }) }, // 取消 cancel(){ this.setData({ selectDateShow:false, }) this.triggerEvent('closeDateDialog',{cancel:true}) }, // 选择周日期确认 setlect_date(){ if(!this.data.endTime){ let yesterday=moment().subtract(1, 'days').format('YYYY-MM-DD'); } this.triggerEvent('confirmWeekDate',{dateName:this.data.dateName,startTime:this.data.startTime,endTime:this.data.endTime}) }, // 减年份 reduce(){ if(Number(this.data.yearRes)==0){ return } this.setData({ yearRes:Number(this.data.yearRes)-1 }) if(this.data.selectPeriod=='week'){ this.getWeekData(this.data.yearRes) } }, // 加年份 add(){ if(Number(this.data.yearRes)==moment().format("YYYY")){ return } this.setData({ yearRes:Number(this.data.yearRes)+1 }) if(this.data.selectPeriod=='week'){ this.getWeekData(this.data.yearRes) } }, // 选择月份 selectMonth(e){ let item = e.currentTarget.dataset; if(item.cansel){ this.setData({ monthRes:e.currentTarget.dataset.value }) } }, // 选择月份确认 setlect_month_two(){ let datetime='' if(this.data.selectPeriod=='year'){ datetime=this.data.yearRes } if(this.data.selectPeriod=='month'){ datetime=this.data.yearRes+'-'+this.data.monthRes } this.triggerEvent('submitYearMonth',{date:datetime}) }, // 获取指定日期在当年第几周 getWeek(dt) { let d1 = new Date(dt); let d2 = new Date(dt); d2.setMonth(0); d2.setDate(1); let rq = d1 - d2; let days = Math.ceil(rq / (24 * 60 * 60 * 1000)); let num = Math.ceil(days / 7); return num; }, afterCalendarRender(e) { console.log('afterCalendarRender', e) }, // 获取对应年的周日历数据 getWeekData(year){ const nowYear = year ? year : moment().year(); console.log(typeof nowYear) const timestamp = (new Date()).valueOf() // 设置正在处理的年份 let handleYear = moment(new Date(String(nowYear))); // 这一年有多少个周 let totalWeeks = handleYear.endOf('year').isoWeek(); const arryWeek = []; let currentWeek = null; //当前日期是第几周 totalWeeks++ for(let i = 1;i <= totalWeeks;i++){ let startOf = handleYear.week(i).startOf('week').format('MM-DD'); let endOf = handleYear.week(i).endOf('week').format('MM-DD'); let ednyear = handleYear.week(i).endOf('week').format('YYYY'); let startValue= handleYear.week(i).startOf('week').valueOf(); let endValue= handleYear.week(i).endOf('week').valueOf(); if(startValue<=timestamp&&endValue>=timestamp){ currentWeek = i } arryWeek.push({ value: i-1, name: `第${i-1}周`, dates:`(${ednyear>nowYear?nowYear + "-" +startOf :startOf}/${ednyear>nowYear?ednyear + "-" +endOf:endOf})`, startTime: handleYear.week(i).startOf('week').format('YYYY-MM-DD'),// 这周的开始时间 endTime: handleYear.week(i).endOf('week').format('YYYY-MM-DD'), // 这周的结束时间 }) } this.setData({ weekData:arryWeek, weekRes:currentWeek }) }, // 获取两个日期之间所有日期 getDayAll(starDay, endDay) { let arr = []; let dates = []; // 设置两个日期UTC时间 let db = new Date(starDay); let de = new Date(endDay); // 获取两个日期GTM时间 let s = db.getTime() - 24 * 60 * 60 * 1000; let d = de.getTime() - 24 * 60 * 60 * 1000; // 获取到两个日期之间的每一天的毫秒数 for (let i = s; i <= d;) { i = i + 24 * 60 * 60 * 1000; arr.push(parseInt(i)) } // 获取每一天的时间 YY-MM-DD for( let j in arr ){ let time = new Date(arr[j]); let year = time.getFullYear(time); let mouth = (time.getMonth() + 1)>=10?(time.getMonth() + 1):('0'+(time.getMonth() + 1)); let day = time.getDate()>=10?time.getDate():('0'+time.getDate()); let YYMMDD = year + '-' + mouth + '-' + day; dates.push(YYMMDD) } return dates } }, observers: { 'selectDateShow':function(selectDateShow){ if(selectDateShow&&this.data.selectPeriod=='week'){ let date=moment().subtract(1, 'days').format('YYYY-MM-DD') this.setWeek(date) let dateArray= this.getDayAll(this.data.startTime,this.data.endTime) setTimeout(()=>{ chooseDateArea([...this.data.dateRange]).then(dates => { console.log('choosed dates: ', dates); }); },200) } if(selectDateShow&&this.data.selectPeriod=='month'){ setTimeout(()=>{ console.log(this.data.dateRange) this.setData({ monthRes:this.data.dateRange[0] }) },200) } if(selectDateShow&&this.data.selectPeriod=='year'){ setTimeout(()=>{ this.setData({ yearRes:this.data.dateRange[0] }) },200) } } } })json配置文件代码{ "component": true, "usingComponents": { "calendar": "../weekcalendar/index" } }注意这里是基于第三方日历组件进行二次封装的，需要在配置文件json中引入该组件，建议下载使用小历日历组件说明中的源码。调用小历日历组件中的方法需要引入该组件中的main.js方法，例如：import {chooseDateArea,cancelSelectedDates} from '../weekcalendar/main'。实现效果周日历组件最后的效果：第一次触发日历组件会选中指定日期所在周的日期，当点击日历中的任何一个日期会选中点击日期所在周的所有日期，当用户确定改变周日期后会将周日期范围回显在页面上。使用说明引入使用组件微信小程序引入组件和使用组件的方法这里不做过多说明，详情查微信小程序官方使用文档。组件属性和方法属性参数说明类型默认值selectDateShow用于控制周日历组件的显示和隐藏booleanfalseselectPeriod选择的模式：周、月、年string''dateRange回显的日期范围array[]事件事件名称说明回调函数bindcloseDateDialog关闭日历组件的回调函数无bindconfirmWeekDate选择周日期的回调函数dateName：回显的日期范围名称，endTime：周日期范围的结束日期
从后台请求的数据改变，echarts图表未更新问题描述当从后台请求新的数据赋值给echarts时,图表的数据仍是初始化状态。解决造成该问题是没有将改变后的echarts数据重新更新到echarts组件配置中： chartMxin.setOption(mxinOption);上面的chartMxin是echarts.init()方法初始化的对象设置echarts柱状图背景色宽度比柱状图宽的问题示例实现方案 series: [ { data: [], type: "bar", name: "实际产量", // showBackground: true, // backgroundStyle: { // color: "#f1f2fb", // }, itemStyle: { normal: { color: "#505BD2" }, }, barWidth: "10", zlevel: 11 }, { // name: '背景', type: 'bar', barWidth: '20px', barGap: '-150%', data: [], itemStyle: { normal: { color: '#f1f2fb', }, }, tooltip: { show: false }, zlevel: 9 }, ],讲解核心思路再定义一类柱状图用于作为背景，需要使用barGap位移将背景柱状图位移到与柱状图在同一个位置，这里需要设置两者的层级关系，注意：pc端上使用zlevel属性即可，小程序中不可使用zlevel属性否则会报错，应该使用z属性。小程序中echarts图表横向内容超出滚动坐标轴固定的需求描述在移动设备上当x轴内容过多需要内容滚动坐标轴固定时可添加dataZoom属性来实现。实现 dataZoom: [ { type: 'inside', //1平移 缩放 throttle: '50', //设置触发视图刷新的频率。单位为毫秒（ms）。 minValueSpan: 6, //用于限制窗口大小的最小值,在类目轴上可以设置为 5 表示 5 个类目 start: 1, //数据窗口范围的起始百分比 范围是：0 ~ 100。表示 0% ~ 100%。 end: 2, //数据窗口范围的结束百分比。范围是：0 ~ 100。 zoomLock: true //如果设置为 true 则锁定选择区域的大小，也就是说，只能平移，不能缩放。 } ],[scode type="lblue"]通过设置start,end属性可以缩放一屏上元素之间的距离，通过minValueSpan控制一屏展示的元素个数[/scode]如何解决echarts图表层级高在小程序中超出高度无法滚动问题描述由于echarts图表层级高，因此当内容超出高度时，设置元素超出滚动效果的样式是无法奏效的，需要搭配小程序scroll-view原生组件使用使其可以超出高度滚动。实现在echarts图表的外层添加scroll-view组件或者其他第三方scroll-view组件
小程序消息订阅实现消息订阅介绍有关微信小程序消息订阅可参考官方文档：https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/subscribe-message.html消息订阅实现步骤获取消息订阅的模板ID 登录https://mp.weixin.qq.com/微信公众平台添加模板，如果没有合适的模板可以手动添加模板，然后获取模板ID。获取下发权限（弹出授权框需用户操作同意） 一次性订阅消息、长期订阅消息详见接口subscribeMessage.send设备订阅消息详见接口wx.requestSubscribeDeviceMessage调用接口下发订阅消息（此步骤由后端调用） 一次性订阅消息、长期订阅消息详见服务端接口subscribeMessage.send设备订阅消息详见服务端接口hardwareDevice.sendwebsocket监听处理订阅消息推送（后端发送websocket，前端监听） 当后端调用下发订阅消息API后，在小程序中会触发消息订阅通知下达给用户，另外当需要在小程序中查看订阅消息或者对消息订阅进行处理时（比如语音处理）还需要结合websocket来下发订阅消息的具体内容，以及监听新的订阅消息。小程序订阅消息语音提醒实现订阅消息语音提醒介绍订阅消息语音提醒目前官方给出的订阅模板仅能支持银行类目的收款到账通知订阅，官方具体介绍见下图：那么其他类型的订阅模板要想支持语音播放提醒需要结合第三方插件来实现，下面介绍使用微信同声传译插件来实现订阅消息语音提醒。微信同声传译插件接入小程序实现思路使用微信消息订阅功能，当后端发送消息订阅推送时，同时进行websocket推送相关内容（由于前端无法监听官方的推送消息，故采取此种方法）。前端监听websocket，当用户登录小程序时调用websocket监听方法，监听到有订阅消息发来时就调用同声翻译插件的语音合成textToSpeech方法播放相关内容。这样也可以实现订阅消息语音播放。在公众平台添加插件 打开微信公众平台---设置---第三方设置---添加插件---搜索微信同声传译插件---添加该插件在小程序项目的app.json配置插件{ "plugins": { "WechatSI": { "version": "0.3.5", "provider": "wx069ba97219f66d99" } }, }注意：Provider属性值为微信同声传译插件的APPID，在公众平台插件的详情里面可查看，version版本号一般为最新版本号在监听消息订阅推送的地方调用插件的文字播放语音方法（1）在使用插件方法的js文件中引入依赖const plugin = requirePlugin("WechatSI") const manager = plugin.getRecordRecognitionManager()（2）调用语音合成textToSpeech方法 plugin.textToSpeech({ lang: "zh_CN", tts: true, content: "您有新的未读消息", success: function(res) { console.log("succ tts", res.filename) wx.playBackgroundAudio({ dataUrl: res.filename }) }, fail: function(res) { console.log("fail tts", res) } })（3）使用真机调试在手机中查看效果（4）配置小程序退出后台声音的播放当小程序进入后台时语音不能进行播放，此时需要向app.json中添加一项配置，如下：{ pages:[], "sitemapLocation": "sitemap.json" }[scode type="red"]上述实现方式只能在某些业务中使用，大多情况无法适用，由于要结合websocket，当设备进入息屏状态websocket是断开的，因此也就无法实现实时播放语音！[/scode]参考资源微信面对面翻译小程序开源地址：https://github.com/Tencent/Face2FaceTranslator语音识别+语音合成+同声传译 微信小程序案例：https://blog.csdn.net/Resulte/article/details/122545279
分包基础点什么是分包分包指的是把一个完整的小程序项目，按照需求划分为不同的子包，在构建时打包成不同的分包，用户在使用时按需进行加载。分包的好处对小程序进行分包的好处如下：可以优化小程序首次启动的下载时间在多团队共同开发时可以更好的解耦卸载分包的项目构成分包后，小程序项目由1个主包+多个分包组成：主包：一般只包含项目的启动页面或TabBar页面、以及所有分包都需要用到的一些公共资源分包：只包含和当前分包有关的页面和私有资源分包的加载规则在小程序启动时，默认会下载主包并启动主包内页面：tabBar页面需要放到主包中当用户进入分包内某个页面时，客户端会把对应分包下载下来，下载完成后再进行展示：非tabBar页面可以按照功能的不同，划分为不同的分包之后，进行按需下载。分包的体积限制目前，小程序分包的大小有以下两个限制：整个小程序所有分包大小不超过16M（主包+所有分包）单个分包/主包大小不能超过2M使用分包配置方法在app.json的subpackages节点中声明分包的结构，如下：{ "pages":[ "pages/index", "pages/logs" ], "subpackages":[ { "root":"packageA", "pages":[ "pages/cat", "pages/dog" ] },{ "root":"packageB", "name":"pack2", //分包的别名 "pages":[ "pages/apple", "pages/banana" ] } ] }打包原则小程序会按subpackages的配置进行分包，subpackages之外的目录将被打包到主包中主包中也可以有自己的pagestabBar页面必须在主包内分包之间不能互相嵌套引用原则主包无法引用分包内的私有资源分包之间不能相互引用私有资源分包可以引用主包内的公共资源独立分包什么是独立分包独立分包本质也是分包，特殊在于：可以独立于主包和其他分包而独立运行。独立分包和普通分包的区别最主要的区别：是否依赖于主包才能运行普通分包必须依赖主包才能运行独立分包可以在不下载主包的情况下，独立运行独立分包的应用场景开发者可以按需，将某些具有一定功能独立性的页面配置到独立分包中。原因：当小程序从普通的分包页面启动时，需要首先下载主包而独立分包不依赖主包即可运行，可以很大程度上提升分包页面的启动速度 注意：一个小程序中可以有多个独立分包独立分包的配置方法{ "pages":[ "pages/index", "pages/logs" ], "subpackages":[ { "root":"packageA", "pages":[ "pages/cat", "pages/dog" ] },{ "root":"packageB", "name":"pack2", //分包的别名 "pages":[ "pages/apple", "pages/banana" ], "independent":true //通过此节点，声明当前分包为独立分包 } ] }引用原则独立分包和普通分包以及主包之间，是相互隔绝的，不能相互引用彼此资源，例如：主包无法引用独立分包内的私有资源独立分包之间，不能相互引用私有资源独立分包和普通分包之间，不能相互引用私有资源特别注意：独立分包中不能引用主包内的公共资源分包预下载什么是分包预下载分包预下载指：在进入小程序某个页面时，由框架自动预下载可能需要的分包，从而提升进入后续分包页面时的启动速度。配置分包的预下载预下载分包的行为，会在进入指定的页面时触发。在app.json中，使用preloadRule节点定义分包的预下载规则，示例如下：{ "preloadRule":{ "pages/contact/contact":{ "network":"all", "packages":["pkgA"] } } }分包预下载的限制同一个分包中的页面享有共同的预下载大小限额2M
小程序中的全局数据共享方案在小程序中，可使用mobx-miniprogram配合mobx-miniprogram-bindings实现全局数据共享。其中：mobx-miniprogram用来创建Store实例对象mobx-miniprogram-bindings用来把Store中的共享数据或方法绑定到组件或页面中使用MobX相关包的安装在项目中运行如下命令，安装MobX相关的包：npm install --save mobx-miniprogram mobx-miniprogram-bindings创建MobX的Store实例import {observable,action} from 'mob-miniprogram' export const store=observable({ numA:1, numB:2, //计算属性 get sum(){ return this.numA+this.numB }, updateNum1:action(function(step){this.numA+=step}), updateNum2:action(function(step){this.numB+=step}) })将Store中的成员绑定到页面中绑定到页面import {createStoreBindings} from 'mobx-miniprogram-bindings' import {store} from '../../store/store' Page({ onLoad:function(){ this.storeBindings=createStoreBindings(this,{ store, fields:['numA','numB','sum'], actions:['updateNum1'] }) }, onUnload:function(){ this.storeBindings.destroyStoreBindings() } })在页面中使用store成员<view>{{mumA}}+{{numB}}={{sum}}</view> <van-button type="primary" bindtap="btnHandler1" data-step="{{1}}">numA+1</van-button> <van-button type="danger" bindtap="btnHandler1" data-step="{{-1}}">numA-1</van-button> //按钮tap事件的处理函数 btnHandler1(e){ this.updateNum1(e.target.dataset.step) }将Store中的成员绑定到组件中store挂载到组件上import {storeBindingsBehavior} from 'mobx-miniprogram-bindings' import {store} from '../../store/store' Component({ behaviors:[storeBindingsBehavior], storeBindings:{ store, fields:{ numA:()=>store.numA, numB:(store)=>store.numB, sum:'sum' }, actions:{ updateNum2:'updateNum2' } } })在组件中使用store<view>{{numA}}+{{numB}}={{sum}}</view> <van-button type="primary" bindtap="btnHandler2" data-step="{{1}}">numB+1</van-button> <van-button type="danger" bindtap="btnHandler2" data-step="{{-1}}">numB-1</van-button> //组件的方法列表 methods:{ btnHandler2(e){ this.updateNum2(e.target.dataset.step) } }
小程序对npm的支持与限制小程序中使用npm包有如下3个限制：不支持依赖于node.js内置库的包不支持依赖于浏览器内置对象的包不支持依赖于c++插件的包vant Weapp npm包安装vant组件库在小程序中安装vant组件库有如下3步：通过npm安装构建npm包修改app.json具体步骤参考vant官方文档使用vant组件在app.json的usingComponents节点中引入需要的组件，即可在wxml中直接使用组件，示例："usingComponents":{ "van-button":"@vant/weapp/button/index" } <van-button type="primary">按钮</van-button>定制全局主题样式在app.wxss中，写入css变量，即可对全局生效page{ --button-danger-background-color:#c00000; }颜色变量参考官方配置文件API Promise化npm包实现API Promise化小程序中实现API Promise化主要依赖于miniprogram-api-promise这个第三方的npm包。安装使用步骤如下：安装依赖npm install--save miniprogram-api-promise在app.js中配置:只需调用一次promisifyAll()方法即可实现一步API的Promise化imoort {promisifyAll} from 'miniprogram-api-promise' const wxp=wx.p={} promisifyAll(wx,wxp)调用Promise化之后的异步API//wxml <van-button type="danger" bindtap="getInfo">vant按钮</van-button> async getInfo(){ const {data:res}=await wx.p.request({ method:'GET', url:'https://www.escook.cn/api/get', data:{name:'zs',age:20} }) }
什么是wxswxs是小程序独有的一套脚本语言，结合wxml，可以构建出页面的结构。wxs的应用场景wxml中无法调用在页面的.js中定义的函数，但是wxml中可以调用wxs中定义的函数。因此小程序中wxs典型的应用场景是过滤器。wxs和JavaScript的关系类似JavaScript但和JavaScript是不同的语言wxs有自己的数据类型number数值类型、string字符串类型、boolean布尔类型、object对象类型。function函数类型、array数组类型、date日期类型、regexp正则。wxs不支持类似于ES6以上的语法形式不支持：let、const、解构赋值、展开运算符、箭头函数、对象属性简写。。。支持：var定义变量、普通function函数等类似于ES5的语法wxs遵循CommonJS规范module对象、require()函数、module.exports对象基础语法内嵌wxs脚本介绍wxs代码可以编写在wxml文件中的<wxs>标签内，就像JavaScript代码可以编写在html文件中的 标签内一样。wxml文件中的每个<wxs></wxs>标签，必须提供module属性，用来指定当前wxs的模块名称，方便在wxml中访问模块中的成员。示例<view>{{m1.toUpper(username)}}</view> <wxs module="m1"> module.exports.toUpper=function(str){ return str.toUpperCase() } </wxs>定义外联的wxs脚本介绍wxs代码还可以编写在以.wxs为后缀名的文件内，就像JavaScript代码可以编写在.js为后缀名的文件中一样。示例function toLower(str){ return str.toLowerCase() } module.exports={ toLower:toLower }使用外联的wxs脚本介绍在wxml中引入外联的wxs脚本时，必须为<wxs>标签添加module和src属性，其中：module用来指定模块的名称，src用来指定要引入的脚本路径，且必须是相对路径示例<view>{{m2.toLower(country)}}</view> <wxs src="../../utils/tools.wxs" module="m2"></wxs>wxs脚本特点与JavaScript不同与JavaScript类似但是为两种不同的语言不能作为组件的事件回调wxs典型的应用场景就是过滤器，经常配合Mustache语法进行使用，但在wxs中定义的函数不能作为组件的时间回调函数，以下为错误用法： 按钮 隔离性（1）wxs不能调用js中定义的函数（2）wxs不能调用小程序提供的API性能好wxs代码运行效率高
生命周期的分类应用生命周期特指小程序从启动---运行---销毁的过程页面生命周期特指小程序中，每个页面的加载---渲染---销毁的过程其中，页面的生命周期范围较小，应用程序的生命周期范围较大。生命周期函数的分类应用的生命周期函数特指小程序从启动---运行---销毁期间依次调用的那些函数页面的生命周期函数特制小程序中，每个页面从加载---渲染---销毁期间一次调用的那些函数应用的生命周期函数小程序的应用生命周期函数需在app.js中进行声明，示例代码如下：App({ //小程序初始化完成时，执行此函数，全局只触发一次。可以做一些初始化的工作。 onLaunch:function(options){} //小程序启动，或从后台进入前台显示时触发。 onShow:function(options){} //小程序从前台进入后台时触发。 onHide:function(){} })页面的生命周期函数小程序的页面生命周期函数需要在.js文件中进行声明，示例如下：Page({ //监听页面加载，一个页面只调用一次 onLoad:function(options){}, //监听页面显示 onShow:function(){}, //监听页面初次渲染完成，一个页面只调用1次 onReady:function(){}, //监听页面隐藏 onHide:function(){}, //监听页面卸载，一个页面只调用1次 onUnload:function(){} })
组件的创建与引用创建组件在项目的根目录中，鼠标右键，创建components-test文件夹在新建的components-test文件夹上，鼠标右键点击“新建Component”键入组件的名称之后回车，会自动生成组件对应的4个文件，后缀名分别为.js,.json,.wxml和.wxss注意：为目录结构清晰需把不同组件放入单独目录中引用组件局部引用：组件只能在当前被引用的页面内使用全局引用：组件可以在每个小程序页面中使用局部引用组件在页面的.json配置文件中引用组件的方式叫做“局部引用”，示例如下：{ "usingComponents":{ "my-test":"/components/test/test" } } //在页面.wxml中使用组件 <my-test></my-test>全局引用组件在app.json全局配置文件中引用组件的方式加全局引用组件，示例如下：{ "usingComponents":{ "my-test":"/components/test/test" } } //在页面的.wxml文件中使用组件 <my-test></my-test>组件和页面的区别组件的.json文件中需要声明"component":true组件的.js文件中调用的是Component()函数组件的事件处理函数需要定义到methods节点中自定义组件的样式组件样式隔离默认情况下组件的样式是隔离的，即组件间样式互不影响，组件样式不影响小程序页面的样式，小程序样式不影响组件的样式。组件样式隔离注意点app.wxss中的全局样式对组件无效只有class选择器会有样式隔离效果，id选择器、属性选择器、标签选择器不受样式隔离的影响修改组件的样式隔离选项通过styleIsolation可修改组件的样式隔离特性，用法如下：Component({ options:{ styleIsolation:'isolated' } }) //或者在组件的.json文件中新增如下配置 { "styleIsolation":"isolated" }组件styleIsolation配置项|可选值|默认值|描述|isolated是表示启用样式隔离，在自定义组件内外，使用class指定的样式将不会相互影响apply-shared否表示页面wxss样式将影响到自定义组件，但自定义组件wxss中指定的样式不会影响页面shared否表示页面wxss样式将影响到自定义组件，自定义组件wxss中指定的wxss中指定的样式也会影响页面和其他设置了apply-shared或shared的自定义组件自定义组件的数据、方法和属性data数据在小程序组件中，用于组件模板渲染的私有数据，需要定义到data节点中。methods方法在小程序中，事件处理函数和自定义方法需要定义到methods节点中，示例如下：Component({ methods:{ addCount(){ } } })小程序组件中，properties是组件的对外属性，用来接收外界传递到组件中的数据，示例如下：Component({ properties:{ //完整定义可指定默认值 max:{ type:Number, value:10 }, //简化定义 max:Number } }) //在.wxml中使用 <my-test max="10"></my-test>data和properties的区别properties和data的用法相同，都是可读可写的。两个对象都指向同一个地址，因此输出两个对象应完全相等。data更倾向于存储组件的私有数据，properties更倾向于存储外界传递到组件中的数据。使用setData修改properties的值使用setData为properties中的属性重新赋值，示例如下：Component({ properties:{max:Number}, methods:{ addCount(){ this.setData({max:this.properties.max+1}) //使用setData修改属性的值 } } })数据监听器什么是数据监听器用于监听和响应任何属性和数据字段的变化，从而执行特定的操作。类似于vue中的watch,基本语法如下：Component({ observers:{ '字段A,字段B':function(字段A的新值,字段B的新值){} } })监听对象属性的变化数据监听器支持监听对象中单个或多个属性的变化，示例如下：Component({ observers:{ '对象.属性a,对象.属性b':function(属性a的新值,属性b的新值){ //触发监听器的三种情况 //为属性a赋值 //为属性b赋值 //为对象赋值 } } })监听所有属性的变化当监听属性太多可通过通配符**来监听对象中所有属性的变化observers:{ 'rgb.**':function(obj){ this.setData({ fullcolor:`${obj.r},${obj.g},${obj.b}` }) } }纯数据字段什么是纯数据字段指不用于界面渲染的data字段应用场景：不会展示在界面上，也不会传递给其他组件，仅当前组件内部使用的字段作用：纯数据字段有助于提升页面更新的性能使用规则在Component构造器的options节点中，指定pureDataPattern为一个正则表达式，字段名符合这个正则表达式的字段将成为纯数据字段，示例代码如下：Component({ options:{ pureDataPattern:/^_/ }, data:{ a:true, //普通数据字段 _b:true, //纯数据字段 } })组件的生命周期组件的全部生命周期函数小程序组件可用的全部生命周期如下：生命周期函数参数描述说明created无在组件实例刚刚被创建时执行attached无在组件实例进入页面节点树时执行ready无在组件在视图层布局完成后执行moved无在组件实例被移动到节点树另一个位置时执行detached无在组件实例被从页面节点树移除时执行errorobject Error每当组件方法抛出错误时执行组件主要的生命周期函数在小程序中最重要的生命周期函数有3个，分别是created、attached、detached。各自特点如下：组件实例刚被创建好的时候，created生命周期函数会被触发：此时不能调用setData，通常只应用于给组件的this添加一些自定义的属性字段。在组件完全初始化完毕、进入页面节点树后，attached生命周期函数会被触发：此时this.data已被初始化完毕，这个生命周期可进行大多数数据初始化工作。在组件离开页面节点树后，detached生命周期函数会被触发：退出页面会触发detached生命周期函数，适合做页面清理工作。lifetimes节点在小程序组件中，生命周期函数可直接定义在Component构造器的第一级参数中，可以在lifetimes字段内进行生明（这是推荐方式优先级最高），示例如下：Component({ lifetimes:{ attached(){}, detached(){} } })组件所在页面的生命周期什么是组件所在页面的生命周期自定义组件的行为依赖于页面状态的变化，此时需要用到组件所在页面的生命周期，在自定义组件中，组件所在页面的生命周期有如下3个：生命周期函数参数描述show无组件所在的页面被展示时执行hide无组件所在的页面被隐藏时执行resizeObject Size组件所在的页面尺寸变化时执行pageLifetimes节点组件所在页面的生命周期函数，需要定义在pageLifetimes节点中，示例如下：Component({ pageLifetimes:{ show:function(){}, hide:function(){}, resize:function(){} } })小程序的插槽单个插槽在小程序中，默认每个自定义组件中只允许一个<slot>进行占位，这种个数上的限制叫做单个插槽。启用多个插槽在小程序的自定义组件中，需要使用多slot插槽时，可以在组件的.js文件中，通过如下方式进行启用。Component({ options:{ multipleSlots:true } })定义多个插槽可以在组件的.wxml中使用多个slot标签，用不同的name来区分不同的插槽，示例如下：<view> <slot name="n1"></slot> <slot name="n2"></slot> </view>使用多个插槽在使用带有多个插槽的自定义组件时，需要使用slot属性来将节点插入到不同的slot中。示例如下：<component> <view slot="n1">n1</view> <view slot="n2">n2</view> </component>父子组件之间的通信父子组件之间通信的3种方式属性绑定：用于父组件向子组件的指定属性设置数据，仅能设置JSON兼容的数据事件绑定：用于子组件向父组件传递数据，可以传递任意数据获取组件实例：父组件还可以通过this.selectgComponent()获取子组件实例对象，这样就可以直接访问子组件的任意数据和方法属性绑定用于实现父向子传值，而且只能传递普通类型的数据，无法将方法传递给子组件，示例如下：父组件：data:{ count:0 } <my-test count="{{count}}"></my-test>子组件在properties节点中声明对应的属性并使用，示例如下：properties:{ count:Number } //wxml结构 <text>子组件的值为：{{count}}</text>事件绑定事件绑定用于实现子向父传值，可以传递任何类型的数据。使用步骤如下：在父组件的js中，定义一个函数，这个函数即将通过自定义事件的形式，传递给子组件。在父组件的wxml中，通过自定义事件的形式，将步骤1中定义的函数引用，传递给子组件。在子组件的js中，通过调用this.triggerEvent('自定义事件名称',{参数对象}),将数据发送到父组件。在父组件的js中，通过e.detail获取到子组件传递过来的数据。事件绑定步骤示例在父组件的js中，定义一个函数，这个函数即将通过自定义事件的形式，传递给子组件。syncCount(){ console.log('syncCount') }在父组件的wxml中，通过自定义事件的形式，将步骤1中定义的函数引用，传递给子组件。<my-test bind:sync="syncCount"></my-test>在子组件的js中，通过调用this.triggerEvent('自定义事件名称'，{参数对象})，将数据发送到父组件。//wxml结构 +1 //js代码 methods:{ addCount(){ this.setData({ count:this.properties.count+1 }) this.triggerEvent('sync',{value:this.properties.count}) } }在父组件的js中，通过e.detail获取到子组件传递过来的数据syncCount(e){ this.setData({ count:e.detail.value }) }获取组件实例可在父组件里调用this.selectComponent("id或class选择器"),获取子组件的实例对象，从而直接访问子组件的任意数据和方法。调用时需要传入一个选择器，例如 this.selectComponent(".my-component")//wxml结构 <my-test count="{{count}}" bind:sync="syncCount" class="customA"></my-test> //js代码 getChild(){ const child=this.selectComponent('.customA') child.setData({count:child.properties.count+1}) child.addCount() }自定义组件-behaviors什么是behaviors用于实现组件间代码共享的特性。behaviors的工作方式每个behaviors可以包含一组属性、数据、生命周期函数和方法。组件引用它时，它的属性、数据和方法会被合并到组件中。每个组件可以应用多个behavior,behavior也可以引用其他behavior。创建behavior调用Behavior(Object object)方法可以创建一个共享的behavior实例对象，供所有的组件使用：module.exports=Behavior({ properties:{}, data:{username:'zs'}, methods:{}, //其它节点。。。 })导入并使用behavior在组件中，使用require()方法导入需要的behavior，挂载后即可访问behavior中的数据或方法，示例代码如下：const myBehavior=require("../../behaviors/my-behavior") Component({ //将导入的behavior实例对象挂载到behaviors数组节点中，即可生效 behaviors:[myBehavior], })behavior中所有可用的节点|可用的节点|类型|是否必填|描述|propertiesObject Map否同组件的属性dataObject否同组件的数据methodsObject否同自定义组件的方法behaviorsString Array否引入其它的behaviorcreatedFunction否生命周期函数attachedFunction否生命周期函数readyFunction否生命周期函数movedFunction否生命周期函数detachedFunction否生命周期函数同名字段的覆盖和组合规则主要涉及data、properties、methods、生命周期函数。1.有同名的属性或方法（1）若组件本身有这个属性或方法，组件的属性或方法会覆盖behavior中的同名属性或方法。（2）若组件本身没有这个属性或方法，则组件的behaviors字段中定义靠后的behavior的属性或方法会覆盖靠前的同名属性或方法。（3）在（2）的基础上，若存在嵌套引用behavior的情况，则规则为：父behavior覆盖子behavior中的同名属性或方法。有同名的数据字段data（1）若同名的数据字段都是对象类型，会进行对象的合并。（2）其余情况进行数据覆盖，规则为：组件>父behavior>子behavior、靠后的behavior>靠前的behavior。生命周期函数不会相互覆盖，而是在对应触发时机被逐个调用（1）对于不同的生命周期函数之间，遵循组件生命周期函数的执行顺序。（2）对于同种生命周期函数，遵循如下规则：behavior优先于组件执行、子behavior优先于父behavior执行、靠前的behavior优先于靠后的behavior执行。（3）如果同一个behavior被一个组件多次引用，它定义的生命周期函数只会被执行一次。
下拉刷新事件启用下拉刷新启用下拉刷新有两种方式：（1）全局开启下拉刷新：在app.json配置文件中，将enablePullDownRefresh设置为true（2）局部开启下拉刷新：在页面的.json配置文件中，将enablePullDownRefresh设置为true配置下拉刷新窗口的样式在全局或页面的.json配置文件中，通过backgroundColor和backgroundTextStyle来配置下拉刷新窗口的样式，其中：（1）backgroundColor用来配置下拉刷新窗口的背景颜色，仅支持16进制的颜色值。（2）backgroundTextStyle用来配置下拉刷新loading的样式，仅支持dark和light监听页面的下拉刷新事件通过onPullDownRefresh()函数即可监听当前页面的下拉刷新事件。例如，下拉重置count的值<view>count值为：{{count}}</view> +1 countAdd(){ this.setData({ count:this.data.count+1 }) } onPullDownRefresh:function(){ this.setData({ count:0 }) }停止下拉刷新的效果当处理完下拉刷新后，下拉刷新的loading效果会一直显示，不会主动消失，所以需要手动隐藏下拉刷新的loading效果。调用wx.stopPullDownRefresh()可以停止当前页面的下拉刷新，代码：onPullDownRefresh:function(){ this.setData({ conut:0 }) wx.stopPullDownRefresh() }上拉触底事件监听页面的上拉触底事件通过onReachBottom()函数即可监听当前页面的上拉触底事件，代码如下：onReachBottom:function(){ console.log('触发到下拉触底事件') }配置上拉触底距离可以在全局或页面的.json配置文件中，通过onReachBottomDistance属性来配置上拉触底的距离，小程序的默认距离为50px。