第一部分 工作中遇到的问题和解决思路
微信小程序相关
scroll-view 组件如何自动滚动到底部
需求分析:实现聊天功能的时候,每次进入聊天界面,都要自动滑动到底部,展示最近的消息,收到新消息的时候,继续滑动到底部,发送新消息也要这样展示。
解决思路:借助 scroll-view 组件的一些属性可以方便的实现此功能
scroll-into-view (参考微信小程序开发文档)
prop | type | default | required | desc |
---|---|---|---|---|
scroll-into-view | string | false | 值应为某子元素id(id不能以数字开头)。设置哪个方向可滚动,则在哪个方向滚动到该元素 |
所以只要在 .js 文件的 data 中设置一个变量,用来指定当前最底部某子元素的 id,在循环渲染聊天列表数据的时候给每个聊天内容展示子项绑定一个 id 值,手动设置前面的变量为最后一项聊天内容的子项的 id 值即可,这个 id 值不能直接设置为 index,因此可以通过拼接的方式实现,上代码:
chatPanel.wxml
<!--
scroll-into-view="{{bottomId}}" 这个 bottomId 即为动态设置的值
因为不能直接用 index ,所以在前面拼接一个 item,当然你可以把这个换成任何你想要的字符
-->
<scroll-view
scroll-y="{{true}}"
class="chat-content"
scroll-top='0'
scroll-into-view="{{bottomId}}"
refresher-enabled="{{true}}"
bindscrolltoupper="getMoreChatRecord"
style="max-height: {{maxHeight}}rpx">
<view
wx:for="{{chatRecordList}}"
wx:key="index"
scroll-with-animation="{{true}}"
id="item{{index+1}}"
class="content-item {{item.msg_source == 1 ? 'right' : 'left'}}">
</view>
</scroll-view>
我们要个 scroll-view 设置 scroll-y="{{true}}",允许纵向滚动,默认值为false 即不可滚动,
这里有两点要注意一下:
1、scroll-view 需要设置 scroll-y="{{true}}",允许纵向滚动,因为它的默认值为 false;
2、scroll-view 需要指定高度,不然它会随子元素撑高,不会出现滚动的效果。
这里关于高度的设置又会有些问题,不能设置一个固定的高度,因为每个手机的屏幕高度其实是不一样的,现在的主流手机屏幕都比较修长,在开发适配的时候,我们针对某一型号的手机设置一个高度,距离底部的输入框距离刚好合适,达到互不干扰的目的。
但是这样的高度到了比当前设备更长的屏幕时,可能就会出现聊天内容距离底部输入框有一大段距离,导致内容的缺失,带来不好的用户体验;同理,遇到更短的手手机屏幕,聊天内容区又会与输入框距离过小或者直接跟输入框重叠了,导致用户永远也看不到底部的消息内容,这个解决思路也很简单,后面会详细介绍。
chatPanel.js
// 进入聊天界面,或许获取消息的处理
data: {
chatRecordList: [],
bottomId: '',
},
// 获取与某一特定用户之间的聊天记录
async getChatRecord() {
const {brandId, storeCode, openUserId} = this.data
console.log(brandId, storeCode, openUserId)
const res = await Message.getChatOfUser({brandId, storeCode, openUserId})
if (res.resultCode == '8003') {
this.setData({
chatRecordList: res.result
})
this.setData({
/* 获取到最后一条消息的拼接 id,设置 bottomId 即可 */
bottomId: `item${this.data.chatRecordList.length}`
})
}
console.log(this.data.chatRecordList)
},
了解了基本的实现原理,在接收消息和发送消息的时候,也可以故技重施,在接收或发送消息成功之后插入消息列表,再把 bottomId 设置为最后一条消息的 id 值即可。
第二个问题是上面提到的如何正确设置 scorll-view 的高度
使用微信提供的接口 wx.getSystemInfo() 获取当前设备的信息,根据屏幕高度设置 scroll-view 的高度
/* 设置 scroll-view 高度 */
setScrollViewHeight() {
const height = wx.getSystemInfoSync().windowHeight
/* 根据其他元素的高度及配合可使用窗口高度,合理设置高度值即可 */
let maxHeight = 1220
if (height < 500) {
maxHeight = 1180
} else if (height < 700) {
maxHeight = 1220
} else {
maxHeight = 1250
}
this.setData({
maxHeight
})
},
3、微信小程序 image 组件,如何做到宽度固定,高度自适应,比例不变
web 中 img 标签与微信小程序中的 image 标签对比:
html 中 img 标签的宽度是可以自适应的,不设置宽高,会取图片的原始宽高,设置了宽度,高度会自适应;设置了高度,宽度会自适应。
小程序中 image 标签会有一个默认的宽高 320*240px,不会自适应,需要通过设置其 mode 属性实现宽或高固定,高或宽等比例缩放
<!-- 需要指定 image 组件的宽度 -->
<image mode="widthFix" src="{{item.msg_content}}"></image>
4、微信小程序输入框,弹出键盘
问题描述:输入框通过 fixed 定位与窗口底部,手机输入时弹出键盘,期望的效果是输入框能够相当与键盘底部定位,实际的情况是键盘上去了,输入框还是停留在页面最底部,且被键盘挡住。
解决方法:动态调整输入框所在容器相对于底部定位的 bottom 值,当弹出键盘时,bottom 值就等于键盘高度的值;键盘消失,bottom 值在回归为 0
chatPanel.wxml
<view
class="input-panel"
style="transform: translateY({{-inputBottom}}px)">
<input
adjust-position="{{true}}"
bindfocus="focus"
bindblur="blur"
class="input-value"
bindinput="inputContent"
value="{{chatContent}}"
placeholder="请输入内容"
type="text"/>
<view
bind:tap="chooseImage"
class="add center">
+
</view>
<view bind:tap="sendMessageToUser" class="send center">发送</view>
</view>
chatPanel.js
/**
* 输入键盘弹出处理
* 聚焦
* 失焦
*/
focus(e) {
this.setData({
// 通过 e.detail.height 可获取键盘高度值
inputBottom: e.detail.height
})
},
blur() {
this.setData({
inputBottom: 0
})
},
5、点击某张图片实现全屏展示
描述:如果时多张照片,可以实现左右滑动
可通过微信小程序提供的方法实现
wx.previewImage({
current: url, // 当前展示图片 url
urls: userContractUrl, // 需要全屏展示的 url 列表
})
健康证正反两面的查看:
.wxml
<view class="common-img">
<view
wx:for="{{userHealthUrl}}"
wx:key="item"
class="img-item">
<image
data-url="{{item}}"
bind:tap="previewHealthImage"
src="{{item}}">
</image>
</view>
</view>
.js
previewHealthImage(e) {
const {url} = e.currentTarget.dataset
// urlHelathUrl 为正反两面图片集合
// [urlString, urlString]
const {userHealthUrl} = this.data
this.coverImage(url, userHealthUrl)
},
coverImage(current, urls) {
wx.previewImage({current, urls})
},
6、如何方便的实现一个单选功能
之前一致的解决思路:给列表的每一项设一个标识,通常是 isActive,通过这个字段的 true/false 的切换来表示选中的项。
这样做比较麻烦,在切换选项的时候需要判断当前选择的是不是已经选中的,如果是已经选中的,只要把当前选中的 isActive 设置为 false 即可,如果当前选择的不是已经选中的,需要对整个列表遍历做一次判断,先把所有的都设置为false,再设置选中项为 true,比较麻烦。
所以只需要再 data 中额外定义一个变量,可以是 index(不建议,没有语义化,当有多个列表时也不能每个都用index),建议用一个能表示当前唯一值的标识,通常是id。
.js
data: {
activeTabId: '10001',
tabList: [{
id: '10001',
text: '首页'
}, {
id: '10002',
text: '购物车'
}, {
id: '10003',
text: '个人中心,'
}]
}
changeActiveTab
changeActiveTab(e) {
const {id} = e.currentTarget.dataset
this.setData({activeTabId: id})
}
接下来便可以通过选中的 id 值从列表中获取到,相关的信息,在有需要的时候。
7、小程序自定义组件 wxss 警告
Some selectors are not allowed in component wxss, including tag name selectors, ID selectors, and attribute selectors.
通过查阅文档可知,在组件样式书写中有以下需要注意的地方:
- 不能使用 id 选择器
- 不能使用后代选择器 .a .b
- 不能使用 app.wxss 中的样式,可通过 @import 的方式引入或者在组件 wxss 中重写一遍
8、微信小程序在 Page({}) 外部书写的代码
Page 外面书写的代码,会在小程序打开之后就会调用,不管当前是不是在代码书写的 page 页面,所以尽量不要在外部书写过多的逻辑代码,可能会影响性能,因为不管是否切换到了那个页面,都会加载。
一般会在顶部引入一些第三方的库文件,或者一些通用的方法,通常是请求后端接口的业务逻辑。
import {Message} from '../../model/message';
import {Common} from '../../model/common';
const common = new Common()
第二部分、一些需要注意的点
1、对一个数组进行操作的时候,不要总是用循环,要优先考虑一些数组已经实现的内置方法
比如,通过 id 值从一个列表中找到该项的完整数据
使用 for 循环
const item
for (let i = 0; i < arr.length; i ++) {
if (arr[i].id === id) {
item = arr[i]
break;
}
}
使用 Array.prototype.find()
const item = arr.find((ele) => ele.id === id)
使用 Array.prototype.filter()
// filter() 返回的是一个数组
const list = arr.filter((ele) => ele.id === id)
const item = list[0]
这样做的好处之一是语义更加清晰,通过 find filter 这样的关键词就能很大成程度上知道这段代码的目的;好处之二自然也是更好的熟悉更“高级”的语法,活学活用。
2、避免使用无意义的标识 00 01 类似这种状态标识,建议使用常量保存起来,语义化更加清晰(加上必要的注释)
场景分析:页面中有两个 tab 标签可以点击切换,
一般情况下,我们都是通过给每一个tab栏指定一个标识 00 01,然后再切换的时候,更新当前选中的状态值。其实这样做不好,我们自己知道当时写代码的时候 00 01 代表啥,可是到将来他人维护的时候,看到这个标识肯定会一脸费解
所以我们可以考虑定义一个常量,用来存储每个 tab 栏的标识,能够提高代码的可读性。
第三部分、日常思考
1、闭包的初中高级应用
初级:定义
闭包是一种现象,一种内部函数可以访问外部函数作用的现象,常见的形式就是在一个函数体内部定义一个内部函数,这个内部函数可以访问函数体内的任何变量,即使通过 return 的方式被传递到其他执行环境中,依然可以访问此前外部函数的变量,且函数体得不到内存回收,会造成内存泄漏
function foo() {
var count = 1
function bar() {
count ++
console.log(count)
}
return bar
}
var outer = foo()
outer() // 2
outer() // 3
outer() // 4
中级:常见形式
高级:常见应用
模拟私有方法
闭包 + 立即执行函数
// 一个简单的计数器
let counter = (function () {
let privateCount = 0
function changePrivateCount(count) {
privateCount += count
}
return {
addCount: function (val) {
changePrivateCount(1)
},
reduceCount: function (val) {
changePrivateCount(-1)
},
countValue: function () {
return privateCount
}
}
})()
let counterOne = counter
counterOne.addCount()
let counterTwo = counter
counterTwo.addCount()
counterOne.countValue() // 2
counterTwo.countValue() // 2
// 两个 counter 操作的是同一个词法作用域
// 生成多个 counter,操作独立的词法作用域(不使用立即执行函数)
Creating closures in loops: A common mistake(在循环中创建闭包:一个常见的错误)
正常情况下
for (var i = 0; i < 10; i ++) {
console.log(i)
}
按照顺序打印出 0 ~ 9
接下来我们期望每过一秒钟,打印一个递增的数值
for(var i = 0; i < 10; i ++) {
setTimeout(() => {
console.log(i)
}, 1000 * i)
}
实际上,隔了十秒后,打印出十个 10
for(var i = 0; i < 10; i ++) {
setTimeout(() => {
console.log(i)
}, 1000 * i)
//
setTimeout(() => {
console.log(i)
}, 1000 * 0)
setTimeout(() => {
console.log(i)
}, 1000 * 1)
setTimeout(() => {
console.log(i)
}, 1000 * 2)
setTimeout(() => {
console.log(i)
}, 1000 * 3)
}
for 循环是同步的,但是 setTimeout 内部执行的函数时异步的,也就是说在执行 setTimeout 内部的代码时,循环已经结束了,最终值时 10
共用了同一个作用域下的变量
如何改动,使其的得到我们期望的效果?
立即执行函数
块级作用域
2、防抖节流的实现
场景的引入:在某些用户的提交操作,需要加上防抖的判断,防止用户多次点击触发多次提交请求。当然这只是第一层,只能控制再一定的时常内只触发一次提交操作。
如果用户的网路过慢,在用户触发请求过后,一段时间内还没得到响应的情况下,又一次触发了提交操作,这时候防抖和节流都是控制不了的。
此时,我们需要一个锁,这个锁默认是开着的,一旦用户发起了请求,立马关闭这个锁,这个时候无论用户出发了多少次提交操作都走不到发送请求的那步。
然后在得到成功响应的时候,把锁打开,这个时候用户可以再次发送请求。当然,有成功的响应,自然也有失败的响应。失败的响应又分为以下几种情况:
- 请求参数等异常,导致请求失败了
- 网络在那一瞬间出问题了
- 服务器在响应那次请求的时候发生了异常(500)
以上几种情况,我们都需要手动把锁打开,不然用户无法执行下一次的操作。
这种做法在处理用户登录的情况,感觉可以取代防抖这类的函数,直接用一个锁来控制即可。毕竟,防抖节流在这种的应用场景下就显得很鸡肋了。