Listentolife

简单就好

  • 首页
  • 标签
  • 分类
  • 归档

工作总结(10) - 处理文件流

发表于 2020-05-05 | 分类于 JavaScript , 工作总结

这篇工作总结将整理一下如何处理文件流。

  • 图片blob对象转为本地地址

一般图片blob对象转为本地地址的需求场景是需要后端生成一个二维码,一般返回是一个blob对象,然后前端需要通过这个blob对象生成一张图片展示出来。

这个实现的方案比较简单,主要依靠web API URL.createObjectURL()。所以首先需要判断浏览器是否支持URL.createObjectURL()方法。

发请求的时候,因为需要后端返回一个blob类型,所以请求头需要设置responseType为blob,返回就是blob对象。

然后调用URL.createObjectURL()方法,把blob对象传入,这个方法将创建并返回一个URL对象,这个url就可以用于页面上图片的显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 这里使用axios来演示
if (window.URL.createObjectURL) {
this.axios.post(api, {
url: url
}, {
responseType: 'blob'
}).then(response => {
let url = window.URL.createObjectURL(response.data)
// ...
})
} else {
// ...
}

不过MDN上有提示到如果不需要这些URL对象的时候,最好通过URL.revokeObjectURL()方法来释放掉,虽然浏览器在document卸载的时候会自动释放这些URL对象。这个情况需要根据业务需要进行处理了。

  • 文档blob对象转为文档文件下载

处理文档blob对象转文档文件也是要涉及到图片blob对象转图片地址的处理,不过因为文档文件的格式各有不同,下载的文件还有文件名,而且转出来的文档文件还需要实现下载,所以实现的步骤就比较多。

首先,发请求的时候仍然需要按照上述请求图片blob对象一样,请求头需要设置responseType为blob。

1
2
3
4
5
6
7
8
9
10
11
12
// 这里使用axios来演示
if (window.URL.createObjectURL) {
this.axios.post(api, {
url: url
}, {
responseType: 'blob'
}).then(response => {
// ...
})
} else {
// ...
}

响应返回之后,响应头中有一个值需要用到,Content-Disposition。Content-Disposition中提供了文件的默认文件名,一般格式为attachment;filename=FileName.txt。所以我们需要先获取到文件的文件名:

1
2
3
4
5
6
let fileName = res.headers['content-disposition'].split(';')[1].split('=')[1];
// 如果涉及到文件名为中文,以上代码最后得到的字符串可能是一串乱码,所以才要再做一步处理
// decodeURIComponent方法传入一个编码后的部分URI,返回一个解码后URI字符串
// escape方法可以生成新的由十六进制转义序列替换的字符串,但方法已经被Web标准废弃,MDN建议使用 encodeURI 或 encodeURIComponent 代替
// decodeURI方法返回一个给定编码统一资源标识符(URI)的未编码版本的新字符串
fileName = decodeURI(escape(decodeURIComponent(fileName)));

下一步就是要把blob对象转化为文档文件。因为不同文件的文件类型不同,如果返回的blob对象的文档类型不是对应的文件类型(可以通过blob对象中的type或者响应头的Content-Type获得),则需要把blob对象先转为对应文件类型的blob对象,然后调用URL.createObjectURL():

1
2
// 这是转为xlxs格式,'application/vnd.ms-excel;charset=utf-8'是xlxs的固定写法
let url = window.URL.createObjectURL(new Blob([res.data], {type: 'application/vnd.ms-excel;charset=utf-8'}));

因为还要实现自动下载,所以可以生成一个a标签,link属性指向上述生成的url,然后js控制点击,就可以实现下载:

1
2
3
4
5
6
let link = document.createElement('a');
link.style.display = 'none';
link.href = url;
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();

完整代码就不贴了,基本实现流程就是这样。

封装axios

发表于 2019-12-06 | 分类于 JavaScript , Vue , Axios

最近做项目中,我尝试对axios做封装,以简化操作逻辑。所以这篇主要整理一下在项目中封装axios。

axios本身有create方法,可以处理各种请求,有拦截器,有cancelToken,可以根据请求的不同做不同的处理。封装的目的,就是让请求独立,的拦截可以分类,同时需要使用cancelToken的请求可以很方便的使用,把axios封装到一个类中,把axios的功能集中起来,调用的时候只要创建一个实例就可以了。

因此,这里封装就声明了一个Ajaxquest类,引入的时候其实是引入一个Ajaxquest实例。

1
2
3
4
5
class Ajaxquest {
// ...
}

export default new Ajaxquest()

初始化

一般我们在使用axios的时候,有一些属性是可以设置固定的,比如timeout,baseURL等等,这里我把这些属性放在constructor中来初始化化:

1
2
3
4
5
6
constructor () {
this.baseURL = baseUrl
this.withCredentials = true
this.timeout = 60000
this.cancelToken = null // 关于cancelToken后面会单独整理
}

request

这个Ajaxquest类在外部使用的时候是直接使用它的实例的,所以实际是调用实例的方法来使用,而且一般我们调用axios都是设置一些配置,所以我们需要有一个request的方法,这个方法支持传入axios的配置,然后在方法里面处理请求跟拦截。

axios的请求方法比较多,比如axios.create(options),axios.get(options)等。这里我们直接使用axios.create(options),因为它的灵活性更高,可以配置methods:

1
2
3
4
5
6
7
8
9
10
request (config) {
// ...
const service = axios.create({
baseURL: this.baseURL,
withCredentials: this.withCredentials,
timeout: this.timeout
})
// ...
return service(config)
}

在return service(config)之前,我们还要处理一步拦截,因为很多时候,一些请求是需要在头部添加一些信息的,而这些信息有些是通用的,有些是个别请求要求携带的,所以可以通过config传进来,然后再传到拦截器中处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
request (config) {
// ...
const service = axios.create({
baseURL: this.baseURL,
withCredentials: this.withCredentials,
timeout: this.timeout
})
service.interceptors.request.use(config => this._interceptorsRequest(config), error => {
console.log('service error: ', error)
// Do something with request error
return Promise.reject(error)
})
service.interceptors.response.use(response => this._interceptorsResponse(response), error => this._interceptorsReject(error))
return service(config)
}

这里面拦截器中的callback,除了请求发送前拦截错误的callback,其他都写成内部方法。这些callback的作用是对所有请求的共性拦截处理。

拦截

拦截器有请求前的拦截跟请求完毕后的拦截,每个拦截都会处理两个callback,因为请求前报错的处理比较少,只是打印报错信息跟返回Promise reject,所以只简单写了callback函数,没有写成内部方法。

请求前的拦截一般是为了配置头部信息,不在request方法的axios.create()中处理只是为了分开处理,是可以放在一起处理的。一些请求不需要配置头部信息,直接过滤掉,然后定义一些配置头部字段的,判断请求配置中是否带有这些属性,有则配置对应的头部信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
_interceptorsRequest (config) {
//当url为/admin/Image ,/admin/File, 不设置X-token请求头,图片和文件上传接口特殊要求
if (/*判断哪些请求不需要配置头部信息*/) {
return config
}

// 自定义配置头部信息的属性,判断带有这些属性,就配置
if (config.areaID) {
config.headers['x-area-id'] = config.areaID
delete config.areaID
}

console.log('service config: ', config)
return config
}

请求完成之后,也做拦截,把请求成功跟失败后的共性处理分别独立放到一个callback中。

请求成功之后,一些全局性的信息就可以在拦截器中处理。比如用户的信息更新,就可以在拦截器中直接处理;

1
2
3
4
5
6
_interceptorsResponse (response) {
console.log('response success: ', response)
const config = response.config
// 处理全局信息
return response
}

如果请求失败了,拦截就可以做一些请求失败的报错信息打印。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 这里用了vant的Toast组件
_interceptorsReject (error) {
console.log('response fail response: ', error.response)

if (error.message === 'cancel a request') return Promise.reject(error)

// 打印错误信息
let errorMessage = '服务器繁忙,请刷新页面重试!'
if (error.response && error.response.headers['x-info'] !== undefined && error.response.headers['x-info'] !== '') {
errorMessage = decodeURIComponent(error.response.headers['x-info'])
} else if (error.message) {
errorMessage = error.message.toLowerCase()
if (errorMessage.indexOf('timeout') > -1) {
errorMessage = '服务器长时间未响应,请刷新页面重试!'
} else if (errorMessage.indexOf('abort') > -1) {
errorMessage = '服务器连接中断 abort!'
}
}
Toast.fail(errorMessage)

return Promise.reject(error)
}

cancelToken

axios提供了cancelToken来处理取消请求。这里我们也来实现cancelToken的封装。

因为我们导出的是实例,写请求方法的时候只是调用了request()方法,所以需要提供另一个方法来处理取消请求。另外,因为cancelToken的处理是需要在请求的时候先创建一个cancel token,所以就是说需要在调用request()方法时提出需要创建cancel token,所以在request()方法需要增加一个逻辑:

1
2
3
4
5
6
7
request (config) {
// ...
if (config.cancelToken) {
this.cancelToken = axios.CancelToken.source()
}
// 接着再处理service跟拦截器
}

这样外部传入一个cancelToken的布尔值,方法内部判断是否为真,是则创建cancelToken。

使用的时候,当需要取消请求,就可以直接调用abort()方法:

1
2
3
4
5
6
abort (notice = 'cancel a request') {
console.log('abort')
if (this.cancelToken) {
this.cancelToken.cancel(notice)
}
}

最后

这个封装其实只是一个结构的封装,内部的配置可以根据不同的项目,不同的请求分类去做差异化的处理。封装的功能也主要是一些常用的请求处理,有其他需求还可以在这个基础上继续封装。

工作总结(9) - 关于移动端底部评论框效果的实现

发表于 2019-12-06 | 分类于 JavaScript , Vue , 工作总结

之前在工作总结(六)中就整理了自己实现的移动端底部评论框的方案。这个方案整体效果可以,但是还是有些细节不够好。最近项目中再次遇到同样的需求,重新做了分析,用另一种不那么复杂的方案又实现了一次。这里就来总结一下这次实现方案。

需求

这次的需求跟上次的一样,只是因为考虑到可能有多个地方复用,所以打算做成一个底部评论框组件。需求是底部有一个假的评论栏,点击之后会弹出真的评论栏,并附着在底部和软键盘。

这个需求之前实现的思路是切换评论栏的布局,然后短时间内监听距离视窗顶部的高度来调整。这个方案能实现效果,但是在软键盘升起的时候会出现抖动现象。所以这次改变思路,直接使用绝对定位布局处理。

之前其实也考虑过这个思路,但是因为只考虑了评论框做绝对定位,所以那种处理的方案需要一直计算位置。这次思路的实现,是把整个页面的布局做调整:body元素直接限定高度为视窗高度,内容分块也全部使用绝对定位,内容的滚动是在内容块内处理。这样,评论框是相对于body做绝对定位,位置一定在视窗的底部。

这次还是用vue来实现。

页面处理

页面有几个需要处理的地方。因为每个页面都是一个vue组件,所以只要组件最外层限定为视窗的高度,body的高度就跟视窗高度一致,同时设置为相对定位:

1
2
3
4
5
6
7
8
body {
min-height: 100vh;
}

.wrapper {
position: relative;
height: 100vh;
}

然后.wrapper内的所有子节点都要使用绝对定位。内容块如果高度超过视窗高度,一般可以使用overflow:auto来处理,不过在移动端的效果有个不太好的情况的滑动不流畅,这个问题可以使用下面的写法处理:

1
2
3
4
5
6
7
height: 100vh;
overflow: auto;
-webkit-overflow-scrolling : touch;

&::-webkit-scrollbar { /* WebKit的浏览器隐藏滚动条 */
display: none;
}

遮罩层的设计

一般评论框弹起是需要用户避免操作其他功能,所以这里在评论组件上增加一个全屏的遮罩层。而且增加一个点击隐藏的功能。这个功能必须要点击遮罩层才能触发,所以用vue开发就是使用`@click.self`来设计点击事件。

评论框的设计

因为这个页面的效果先是视窗底部有一个假的评论框,点击之后这个假的评论框会隐藏,真的评论框会弹出来,所以实际上是需要做两个,但是这个假的评论框并不影响这些评论框的设计,所以只要保证它的布局时绝对定位就可以了。

首先,这个评论框的布局一样是绝对定位,定位在组件最外层元素的底部,也即在视窗的底部,这样当软键盘弹起的时候,整个页面被顶起,评论框依然能固定在页面的底部,贴着软键盘的顶部,而且不需要做任何计算高度的处理。

这里需要注意的是,评论框的绝对定位必须使用bottom属性来定位,而不能用top属性,因为使用top属性的情况下,因为软键盘弹起,视窗高度被压缩,会导致评论框还是按top属性的设置隐藏到软键盘后面,而用bottom属性可以使评论框一直定位在视窗的底部,所以不会被隐藏。

还有一个会影响效果的,是需要设计评论框的高度。因为在iOS上,当H5的input标签获得聚焦,弹出软键盘时,iOS会先把页面所有fixed定位改成绝对定位,然后判断输入框的位置,根据判断滚动页面。一方面软键盘的弹出会导致视窗高度变小,这会影响使用vh布局的样式,另一方面是滚动页面也会让从fixed定位改绝对定位的容器上移,而且高度不确定。所以最后会导致本身输入框会上移,然后视窗缩小也是顶起,最后效果就会变成输入框偏移更多,显示不全或各种难堪的页面显示。因此可以先设定一个高度,然后在输入框获得聚焦时把高度改高一些,这样在经历视窗缩小跟滚动之后,评论框的显示效果也不会差。这里就不上代码了,思路已经整理了。

实际开发出来的效果还是可以的。这里说的效果是安卓跟iOS13版本之前。安卓效果最好,而且其实在输入框获得聚焦的时候也不需要调整评论框高度的。但因为自iOS13出来之后,这个方案实现的效果跟实际效果又有了新的偏差:软键盘弹起后,当输入框失焦时,软键盘虽然隐藏了,但是视窗竟然没有恢复,导致页面下方原来软键盘的部分空白。这个原因目前不明,当然我只在我们公司的app上测试出来,并没有在其他地方测试过,可能跟webview有关。

工作总结(8)

发表于 2019-12-06 | 分类于 JavaScript , Vue , 工作总结

这篇工作总结将整理一下如何实现html生成图片。

生成图片一般的思路都是将需要生成图片的dom绘制到canvas中,再调用canvas.toDataURL()将绘制好的canvas转成file对象,然后再上传服务器,拿到线上地址,再显示出来。

最麻烦的就是将dom绘制到canvas,摘自网上的说法,需要先遍历需要生成图片区域的所有元素,提取DOM数,然后获取渲染之后的每个DOM节点的内联、外链CSS属性,最后将样式转换成canvas的属性,利用offset等属性辅助摆放位置,将节点对应到canvas上。所以更多人实现的方案是使用第三方库html2canvas。

html2canvas用法也很简单,就是一个html2canvas()方法,传入需要生成图片的dom跟配置,然后这个方法支持Promise,可以使用html2canvas(dom).then方法写入一个回调函数,拿到绘制完成的canvas,就可以完成接下来的处理了。

这里整理一下开发中遇到的需求跟处理。一般生成的图片会跟页面显示不同,因为可能会加一些内容。这样就需要在调用html2canvas()方法先调整需要生成图片的dom的样式,改成需要的样式效果。然后在回调函数中,等拿到canvas之后,再把dom的样式恢复。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 使用vue开发
// 先引入html2canvas
htmlToImage () {
// 返回一个Promise,调用的时候可以知道什么时候结束,支持异步处理
return new Promise((resolve, reject) => {
// 保存需要修改的样式的当前值
// ...
// 设置需要打印的样式的宽高
// ...
// 调用$nextTick确保样式已经更新
this.$nextTick(() => {
// 调用html2canvas,传入需要生成图片的dom跟配置,配置可以参考官网
html2canvas(this.$refs.print, {
allowTaint: true, // 不允许跨域图片污染画布
useCORS: true, // 允许加载跨域图片
width: this.$refs.print.offsetWidth, // canvas宽度
height: this.$refs.print.offsetHeight, // canvas高度
}).then(canvas => {
// 拿到的就是绘制了dom的canvas
// 恢复修改前的样式
//...
})
}, 1000)
})
},

关于图片模糊的问题

上面的操作可以拿到canvas,然后就可以转file对象,上传,拿到服务端链接,然后打开一看,图片很模糊。

这个问题也很常见,一方面跟设备的dpi有关,另一方面跟生成的图片太小,拉伸之后也会出现这个问题。所以解决的方案是在调用html2canvas()方法先把需要生成图片的dom的整体样式放大数倍,然后再生成图片,同时在调用方法时设置dpi跟scale两个配置,这样能保证图片效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
htmlToImage () {
// 返回一个Promise,调用的时候可以知道什么时候结束,支持异步处理
return new Promise((resolve, reject) => {
// //保存需要修改的样式的当前值
let height = this.$refs.print.offsetHeight;
let width = this.$refs.print.offsetWidth;
// //设置需要打印的样式的宽高
this.$refs.print.style.height = height * 2 + 'px';
this.$refs.print.style.width = width * 2 + 'px';
// 调用$nextTick确保样式已经更新
this.$nextTick(() => {
// 调用html2canvas,传入需要生成图片的dom跟配置,配置可以参考官网
html2canvas(this.$refs.print, {
dpi: window.devicePixelRatio * 2, // dpi设置 每英寸的像素
scale: 2, // 放大倍数
allowTaint: true, // 不允许跨域图片污染画布
useCORS: true, // 允许加载跨域图片
width: this.$refs.print.offsetWidth, // canvas宽度
height: this.$refs.print.offsetHeight, // canvas高度
}).then(canvas => {
// 拿到的就是绘制了dom的canvas
// 恢复修改前的样式
//...
})
}, 1000)
})
},

关于跨域图片不显示的问题

所以又按照上面的思路改了一下,图片是终于清晰了,但是还有一个问题没解决,就是本来应该显示的图片没有出现。这个问题我搜索过之后才知道原来是图片跨域的问题。

一般图片跨域没什么问题,但是在canvas上,跨域图片会导致画布被污染,所以跨域图片不会被绘制到canvas上,最终生成的图片也没有正常显示该图片。

解决这个问题,我试过一些网上的简单的处理,都无效,所以只能把跨域图片转为本地图片,再设置html2canvas()的useCORS为true。

跨域图片转为本地图片,目前我的解决方案是让服务器返回二进制数据,然后转成Blob对象,再调用window下的createObjectURL()方法转成本地图片url。

关于base64转Blob对象转file对象

这个问题打算单独整理,就不再这里写了。

工作总结(7)

发表于 2019-12-06 | 分类于 JavaScript , Vue , 工作总结

这篇工作总结会整理一下使用Canvas实现随机生成验证码。

随机生成验证码是基于canvas绘制而成的,最基本的实现是随机生成多位验证码,然后验证码的每一位码按不同的颜色,大小,倾斜角度跟位置来绘制到画布上。更上一层是增加干扰项,比如划线,非验证码范围字符,验证码扭曲,定时更新验证码等等。这里实现的验证码只是增加了干扰项跟点击更新,定时更新可以实现,但这里就不整理了。

这里的代码实现基于vue实现的。

初始化工作

要实现随机验证码,有两个基本要素:canvas画布,随机生成的验证码(这里演示4位验证码)字符范围。

canvas绘制不难,主要是使用绘制的方法,这个后面整理。先要在页面上获得canvas画布,这一步需要在mounted中执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- html template -->
<canvas id="verCode"></canvas>

// js
data () {
// ...
}
mounted () {
this.initCanvas();
},
methods: {
initCanvas () {
this.container = document.querySelector('#verCode');
this.width = this.container.width;
this.height = this.container.height;
this.ctx = this.container.getContext('2d');
this.ctx.textBaseline = 'bottom';
},
}

上面通过.getContext('2d')获取了画布,然后在获得画布的宽高,设置了画布的文字基线。

生成code

接下来就是生成code。生成code是比较简单的,思路是从特定的字符集合中随机取出字符组成code。这里把特定的字符放到一个字符串中,然后随机生成数组下标,取出字符串。随机生成的code的长度可以从外部传进来,也可以使用默认的四位:

1
2
3
4
5
6
7
8
9
10
getCode () {
let chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
let code = [];
let radix = chars.length;
for (let i = 0; i < this.num; i++) {
code[i] = chars[0 | Math.random() * radix];
}
this.code = code.join('');
console.log(this.code);
},

画code

生成code之后,就可以开始在canvas画布上画code了。这里用到的是canvas的fillText()方法,这个方法可以把文字绘画到canvas上。在调用fillText()方法之前,还可以给文字增加属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
drawCode () {
for (let i = 0; i < this.num; i++) {
let x = (this.width - 20) / this.num * i + 15;
let y = this.randomNum(this.height * 0.92, this.height * 0.92);
let deg = this.randomNum(-45, 45);
let txt = this.code.charAt(i);

this.ctx.fillStyle = this.randomColor(10, 100);
this.ctx.font = `${this.randomNum(80, 100)}px SimHei`;
this.ctx.translate(x, y);
this.ctx.rotate(deg * Math.PI / 180);
this.ctx.fillText(txt, 0, 0);
this.ctx.rotate(-deg * Math.PI / 180);
this.ctx.translate(-x, -y);
}
},

干扰项

干扰项一般增加直线跟曲线就可以了,如果想要更复杂,也是可以增加符号干扰的,不过要注意控制样式,否则真的会干扰到用户的判断。

这里我爸直线跟曲线的绘画分成两个方法来调用了。绘制的数量我都按code的长度来控制。然后在一定范围内随机生成长度,位置坐标,颜色等。最后调用canvas绘制路线的方法绘制即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
drawLine () {
for (let i = 0; i < this.num; i++) {
this.ctx.strokeStyle = this.randomColor(90, 180);
this.ctx.beginPath();
this.ctx.moveTo(this.randomNum(0, this.width), this.randomNum(0, this.height));
this.ctx.lineTo(this.randomNum(0, this.width), this.randomNum(0, this.height));
this.ctx.stroke();
}
},
drawArc () {
for (let i = 0; i < this.num; i++) {
this.ctx.fillStyle = this.randomColor(0, 255);
this.ctx.beginPath();
this.ctx.arc(this.randomNum(0, this.width), this.randomNum(0, this.height), 1, 0, 2 * Math.PI);
this.ctx.fill();
}
},

点击刷新验证码及验证

点击刷新验证码实现很简单,给canvas增加一个点击监听事件,点击之后调用getCode()方法就可以完成刷新。

验证验证码的实现是先实现一个验证方法,然后在父组件使用验证码组件是使用this.$refs来调用验证码组件的验证方法,传入用户输入的code进行比对,然后返回一个布尔值。

以上就是验证码组件的实现。

微信小程序开发-倾听天气(二)

发表于 2019-08-25 | 分类于 JavaScript , 微信小程序

我的微信小程序,倾听天气终于更新了0.2.1版本了,这个版本页面效果要比第一版的好很多了,主要的原因在于我申请的和风天气开发者权限通过了,所以获得更多的数据。比如24小时内间隔2小时的天气数据,最近7天的天气数据。

因此我的小程序所展示的数据可以更加详细。此外,我又在iconfont.cn搜集了一些icon来优化小程序的展示,并给逐小时天气预报做了一个折线图。下面我就以这几点来整理一下小程序的开发。

申请过程

这里就简单说一下申请的过程:和风天气的认证个人开发者很简单。只要申请的时候实名并提供自己的作品中使用了和风天气的api即可。我是在完成了倾听天气的第一版之后申请中,中间还给他们发了一封邮件,因为申请的信息要求的作品选项没有包括微信小程序,所以我发了邮件去咨询了一下。很快就回复了我微信小程序可以申请。所以我在申请的时候直接在作品一栏写明是微信小程序倾听天气。虽然那天刚好申请后隔天是周末,但到了周日就来邮件说申请通过了,周末都处理申请,好厉害的样子…

之后我在我的小程序中就发现原来3天的天气预报数据变成7天了,说明api的权限已经开好了。

开发总结

这里整理一下在0.2.1版本中涉及到的一些开发总结。主要分微信小程序滚动组件,iconfront引入及canvas绘制折线图。

滚动组件scroll-view

之前整个小程序中都没有用到这个组件是因为没有太多的数据需要做这样的显示。不过当原来3天的天气预报数据变成7天之后,在屏幕上横着显示7天的数据就比较拥挤了,所以这个时候就需要用到滚动组件scroll-view了。

scroll-view用起来不难,首先需要有一个父级组件包住scroll-view,然后通过scroll-x或scroll-y来控制滚动的方向。最后,如果是垂直方向的滚动需要给scroll-view增加一个高度的样式,而如果是水平方向的话,给父级标签增加一个固定宽度就可以了。

我这是因为是要水平方向展示7天的天气预报数据,所以我给组件scroll-view一个父级组件,固定width为100%,然后给组件scroll-view一个类,这里使用flex布局,让内部的子组件都并排在一行内,并固定宽度。这样的基本完成了水平滚动的实现了:

1
2
3
4
5
6
7
8
9
10
11
12
13
<view class='forecast-wrapper' wx:if="forecast.daily_forecast">
<scroll-view scroll-x enable-flex class='scroll-view-x'>
<view
wx:for="{{ forecast.daily_forecast }}"
wx:for-index="index"
wx:for-item="item"
wx:key="item.date"
class='forecast-item'
>
<!-- -->
</view>
</scroll-view>
</view>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.forecast-wrapper {
width: 100%;
overflow: hidden;
// ...
}

.scroll-view-x {
// display: flex;
width: auto;
white-space: nowrap;
// flex-wrap: nowrap 测试无效
}

.forecast-item {
display: inline-block;
width: 20%;
}

scroll-view组件的enable-flex属性相当于直接设置flex布局。还有很多属性可以设置,比如scroll-into-view滚动到某一子节点,enable-back-to-top支持垂直方向滚动的点击屏幕顶部后滚动条返回顶部,等等。具体可以去读官网文档。

iconfont引入

其实iconfont的引入本应没什么可以说的,但是鉴于微信小程序中写样式的文件叫wxss文件,我们平时写的样式文件是css,所以还是有点不一样的。

网上找到的解决方案一般都是先下载icon文件,然后把.ttf文件转码base64格式,然后再放到对应的wxss文件中,最后再引用。

但是我嫌这种方案比较复杂,而且需要下载icon文件存放在目录下也可能需要费些空间(我的项目还好,只是想找懒)。都知道微信小程序是有限项目大小的,所以有没有更好的方案。

有,很简单,就是通过iconfont直接生成unicode,把unicode复制放到wxss文件中,再下载icon文件,把icon的css样式在复制放到wxss文件中(我是直接放到app.wxss中,暂时先这样处理),然后就可以快速的使用了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**iconfont online**/
@font-face {
font-family: 'iconfont'; /* project id 1265794 */
src: url('//at.alicdn.com/t/font_1265794_rsipcjqri8.eot');
src: url('//at.alicdn.com/t/font_1265794_rsipcjqri8.eot?#iefix') format('embedded-opentype'),
url('//at.alicdn.com/t/font_1265794_rsipcjqri8.woff2') format('woff2'),
url('//at.alicdn.com/t/font_1265794_rsipcjqri8.woff') format('woff'),
url('//at.alicdn.com/t/font_1265794_rsipcjqri8.ttf') format('truetype'),
url('//at.alicdn.com/t/font_1265794_rsipcjqri8.svg#iconfont') format('svg');
}

/**iconfont.css**/
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

这样就不需要把icon文件都放到项目中了。

目前项目中已经大面积的使用icon显示一些信息了,比如天气状态,生活指数,一些常用的功能icon。看起来要比原来纯文字的效果要好很多。

canvas绘制

canvas绘制应该是这一版本中最难的部分。原本想引入ECharts微信小程序版来实现24小时温度折线图的,但是因为没有用过,一下子使用有难度,再者最近看到不少canvas绘制图表的博文,所以想自己试试,所以最后选择了canvas绘制。

其实canvas绘制不难,它的操作逻辑就是在页面上建立一张画布,然后用坐标标识绘制的关键点,最后把方法当成指令这样使用就可以了,剩下就是想画好看点就用一些。

首先还是要对数据做处理。绘制一个24小时逐小时温度折线图需要确定要画布的宽跟高,这样才能确定最高温度跟最低温度的y轴值,最近时间温度跟最远时间温度的x轴值;然后找到所有温度数据中的最大值刚跟最小值,可以确定每个温度阶的间隔值,最后再计算每个时间温度对应的坐标。

这里我使用wx.getSystemInfo来获得屏幕的宽度,然后动态的计算画布的宽度跟高度(宽度为屏幕宽度,高度为宽度的1/5)。再用getTmps方法获得温度数据的数组跟最高最低温度值。最后用getCoordinate方法计算获得所有的坐标,最后在drawHourly方法中绘制。

微信小程序中canvas的绘制跟H5中有一点区别,但逻辑是一样的。首先要创建一个content,然后根据折线图的需要绘制线,绘制圆,绘制最大温度跟最小温度标识,最后把content绑定到页面上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
drawHourly(arr, maxCoo, minCoo, maxTmp, minTmp) {
var context = wx.createContext();
context.draw(); // 清空
context.setStrokeStyle("#fff");
context.setLineWidth(1);
context.moveTo(arr[0].x, arr[0].y);
for (let i = 1; i < arr.length; i++) {
context.lineTo(arr[i].x, arr[i].y);
}
context.stroke();

for (let i = 0; i < arr.length; i++) {
// 设置描边颜色
context.setStrokeStyle("#ffffff");
context.moveTo(arr[i].x, arr[i].y);
context.arc(arr[i].x, arr[i].y, 2, 0, 2 * Math.PI, false);
context.closePath();
// 填充路径
// context.fill();
context.stroke();
}

context.setFontSize(10);
context.setFillStyle("#fff");
context.fillText(`${maxTmp}℃`, maxCoo.x - 10, maxCoo.y + 5);
context.fillText(`${minTmp}℃`, minCoo.x - 10, minCoo.y + 5);

wx.drawCanvas({
canvasId: 'hourlyCanvas',
actions: context.getActions()
});
}

这个绘制也是比较简单,也没有做效果更好的平滑曲线,达到效果就可以了。

以上就是新版本倾听天气小程序开发的整理。接下来会做新的优化,具体会在下一个版本出来后再整理。

微信小程序开发-倾听天气(一)

发表于 2019-08-25 | 分类于 JavaScript , 微信小程序

最近开始尝试开发微信小程序。之前多多少少了解了微信小程序的开发,了解了wxml,wxss,wxs跟一些微信小程序的api,但是并没有什么想法应该要拿什么来练手。最近找到和风天气(https://www.heweather.com/)有向普通开发者提供开发api,看了文档也觉得文档很友好,数据也很足够,所以就想尝试开发一款天气预报的微信小程序。

简单的说,最基本的需要是用户需要查看所在地的天气情况跟最近几天的天气预报,并获得一些建议,而和风天气开放给普通用户也有这些信息,所以这个需求可以很容易实现。目前我也是打算实现最基础的展示功能先上线,走完整个开发到上线的流程,看看有什么坑。

话不多说,先上小程序码,可以去看看效果:

倾听天气 小程序码

创建小程序账号

第一步,肯定是要到微信公众平台申请一个小程序的账号,这个账号需要一个未申请过微信账号或其他开发账号的邮箱,然后申请之后还需要一个管理员。申请之后,可以得到一个AppID,这个AppID就是你开发小程序时需要绑定的。

然后就下载微信开发者工具,安装。打开工具,选择小程序,然后在新建项目那里,在AppID填入你刚才注册获得的Appid。选择好目录,项目名称跟后端服务(目前我没有使用云服务,但计划之后的版本会加入),点击新建即可新建小程序项目。

腾讯地图开发者及和风天气开发者账号

从需求中分析到,需要获得用户的定位信息跟天气数据,前者可以通过微信小程序内置api获取,后者可以通过和风天气api获取。不过长远考虑可以获得更多地图数据以丰富小程序的功能,所以这里一并申请了腾讯地图开发者。

其实申请也不难,腾讯地图api是在腾讯位置服务申请的开发者账号,只要用QQ或微信扫一下二维码就可以注册成功了,简直0操作成本。然后在控制台的key管理那里创建新密钥。通过这个密钥就可以获得腾讯地图的数据了

注册和风天气的开发者账号也不难,进入官网之后进行注册。开发者的权限是跟开发者账号绑定的,一般的开发者(比如我)可以直接注册普通开发者,然后创建key,就可以获得最基础的数据。如果要申请认证开发者就需要提供开发上线的应用(目前我上线小程序之后就开始申请了)。申请就还需要提供个人信息了,但是因为小程序没有链接,所以我就直接写让他们去微信里面搜索了……

最后,还需要在微信公众平台,小程序账号的开发一栏,进入开发设置,设置服务器域名的request合法域名。设置的是把接口的域名添加进去。注意,这个操作每个月是有显示修改次数的。

这样,就完成了开发前的准备了。

开发

开发的过程耗时不长,只是因为每天比较忙,所以只能腾三个晚上每天大概6个小时从各种注册到开发到上线申请。

遇到的坑也不算太多,简单的百度一下也可以解决,不过既然是整理开发过程,我觉得还是可以拿一些出来说说的:

用户权限

因为是第一个版本,没打算获取用户太多信息,只需要获得定位信息就可以了,所以目前只获取用户定位权限。

现在微信小程序的用户权限都需要做多一些配置。比如获取定位权限,就需要先在app.json配置permission:

1
2
3
4
5
6
7
8
9
{
// ...
"permission": {
"scope.userLocation": {
"desc": "你的位置信息将用于获取你所在地区的天气信息"
}
},
// ...
}

然后再在js文件中调用wx.getLocation()方法,这里我是在index.js中onLoad中调用getUserLocation(),在这个方法中获取定位的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
onLoad: function () {
// 获取用户经纬度
this.getUserLocation()
},
getUserLocation () {
wx.getLocation({
type: 'gcj02',
success: res => {
this.updateLocation(res);
},
fail: err => {
console.log(err)
}
})
},

腾讯位置服务获取位置描述

毕竟是腾讯的产品,腾讯位置服务也有微信小程序JS SDK,不需要使用API调用。引入的方法也不难。

前面已经获得了key,然后在腾讯位置服务页面下载SDK,然后放到项目目录下(我的项目是放在了util目录下)。再在js文件中引入,并声明一个实例(官网也有详细的代码介):

1
2
3
4
5
6
7
// 引入腾讯地图SDK核心类
let QQMapWX = require('./qqmap-wx-jssdk.min.js');

// 实例化地图API核心类
let qqmapsdk = new QQMapWX({
key: config.MAP_API_KEY
});

这样就可以开始使用了。前面已经获得了用户的位置信息,这个位置信息主要可以获得位置的经纬度,然后我们就要考虑使用经纬度获取位置的描述信息了。这个可以使用腾讯位置服务SDK的逆地址解析接口reverseGeocoder(options:Object)。

这个接口可以不传入任何字段,默认为当前位置坐标。然后返回的是经纬度对应的地理描述信息,不传则返回的是当前位置的描述信息。这个就有点有趣了,也就是可能可以绕过微信小程序的用户授权,直接获取用户的地理信息。不过为了不出问题,我们还是通过用户授权获取位置信息吧。

和风天气获取常规天气数据

天气数据我通过和风天气的常规天气数据api获取的。前面已经获取了和风天气普通开发者的key了,这里使用wx.request向接口https://free-api.heweather.net/s6/weather/{weather-type}?{parameters}发出请求。接口的要求官网也是有说明的,这里就不多说。我是把腾讯位置服务跟和风天气的请求操作都做了封装,放在util目录的api.js中,这样可以减少在页面js文件中写请求内容,请求操作统一开发。这里我使用了Promise.all把常规天气数据请求都包起来了,方便全部请求完成之后再关闭加载弹窗。

1
2
3
4
5
6
7
8
getHefengData (lat, lon) {
wx.showLoading({
title: '获取天气信息中',
})
Promise.all([this.getWthNow(lat, lon), this.getWthForecast(lat, lon), this.getWthHourly(lat, lon), this.getWthLifestyle(lat, lon)]).then(res => {
wx.hideLoading();
})
},

wxs的使用

因为微信小程序不支持在wxml中做数据处理,所以wxs的出现我觉得是js到wxml的中介物。数据或者状态通过wxs的处理后才能渲染到wxml。

比如说这个项目中,接口获取的天气状态码需要转换成天气状态描述跟对应的图标,这个时候就需要通过wxs的处理。所以,在微信小程序中,js扮演的角色更多是获取数据,保存状态,数据通过wxs的处理变成需要渲染的内容放到wxml中。

使用也不难,新建一个对应的wxs,把处理逻辑放进去,通过<wxs src="./xxx.wxs" module="xxx" />引入到wxml中,然后通过xxx.fn来使用方法:

1
<text class='iconfont {{tools.getCondIcon(now.now.cond_code)}}'></text>

因为和风天气的天气状态比较多,所以我把天气状态对应的描述都放到app.wxs中,再在index.wxs引用。

结尾

页面设计其实比较简陋,所以没什么好写的,而且显示的都是文字,展示效果并不那么好,所以等之后的迭代有比较好的设计再看有没有什么可以整理的吧。

手写一个Promise

发表于 2019-08-25 | 分类于 JavaScript , Promise

最近在掘金上看了一些关于promise的文章,然后学习了一番,发现是可以自己手写实现一套promise的,于是做了一番学习,现在来整理一下。

Promise状态

promise是有一套规范的,只要实现的promise符合这套规范,就说明显示的promise是可以正常使用的。这套规范的地址是 https://promisesaplus.com/ 。上面除了有规范,还给出了测试工具,方便测试代码。

promise的规范写的很详细,包括用语,各种情况下promise应该有的处理方式。这个就开始实现吧。

规范的一开始就明确定义了(1.1)promise是一个带有then方法的对象或者函数,所以我们实现的promise需要有一个then方法;(2.1)promise有三个状态,pending等待态,fulfilled成功态,rejected失败态,而关于这三种状态关系,在(2.1)中有详细的说明:

  1. 等待态可以转化为成功态或失败态;
  2. 成功态不能转化为其他状态,而且需要有一个value;
  3. 失败态不能转化为其他状态,而且需要有一个reason;
    (value跟reason值不可变,但如果指向是对象的话则指向不可变)

基于以上规范,promise需要有一个状态state和对应成功态的value跟失败态的reason:

1
2
3
4
5
6
7
8
9
10
11
12
13
const PENDING = 'PENDING';
const FULFILLED = 'FULFILLED';
const REJECTED = 'REJECTED';

class Promise {
constructor (executor) {
this.state = PENDING;
this.value = undefined;
this.reason = undefined;
// ...
}
// ...
}

Promise executor

上面代码中constructor传入了一个executor,这个是按照原生Promise的写法,声明一个Promise对象实例需要传入executor,这个executor需要带有两个参数,包括resolve跟reject。

resolve跟reject是用来判断promise状态的两个函数,要求Promise转化为成功态时要调用resolve,并传入value,而转化为失败态时要调用reject,并传入reason。而且,executor是个立即执行的,是同步的。

当resolve获得value时,会把值赋给promise的value,并判断当前是否为等待态,是则会把状态改为成功态;而reject操作类似,把获得的reason赋值给promise的reason,然后判断等待态后把状态改为失败态。

executor的执行可能出现运行报错,所以需要用try catch来捕获报错,捕获的报错则传给reject。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Promise {
constructor (executor) {
this.state = PENDING;
this.value = undefined;
this.reason = undefined;
let resolve = (value) => {
// ...
if (this.state === PENDING) {
this.state = FULFILLED;
this.value = value;
this.onFulfilledStack.forEach(fn => fn());
}
};
let reject = (reason) => {
if (this.state === PENDING) {
this.state = REJECTED;
this.reason = reason;
this.onRejectedStack.forEach(fn => fn());
}
};

try {
executor(resolve, reject);
} catch (exception) {
reject(exception);
}
}
// ...
}

Promise then

promise/A+规范要求实现的promise需要有then方法。这个方法需要传入两个回调函数,onFulfilled和onRejected。因为支持链式调用,所以也要求函数返回的是一个promise。

首先,对于以上的要求,可以写出下面then方法的结构:

1
2
3
4
5
6
7
8
9
10
11
class Promise {
// constructor...
then (onFulfilled, onRejected) {
// ...
let promise2;
promise2 = new Promise((resolve, reject) => {
// ...
});
return promise2;
}
}

then方法中的onFulfilled和onRejected跟promise2规范中有比较详细的要求,所以我们单独的实现。

onFulfilled和onRejected调用要求

首先,只有当promise的状态为成功态时,才可以调用onFulfilled回调,并传入value,而只有为失败态时,才可以调用onRejected回调,并传入reason,而等待态的情况下,两个回调均不执行,会等待到状态转化之后才会根据状态执行。

同时,由于then方法支持多次调用,也就是说在promise等待态时,then方法调用的情况,所以需要在promise内部使用两个数组来储存所有的onFulfilled和onRejected方法,并在转化状态后按照先后顺序调用执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Promise constructor
this.onFulfilledStack = [];
this.onRejectedStack = [];
let resolve = (value) => {
if (this.state === PENDING) {
this.state = FULFILLED;
this.value = value;
this.onFulfilledStack.forEach(fn => fn());
}
};
let reject = (reason) => {
if (this.state === PENDING) {
this.state = REJECTED;
this.reason = reason;
this.onRejectedStack.forEach(fn => fn());
}
};

// then的promise2内部
if (this.state === FULFILLED) {
onFulfilled(this.value);
}
if (this.state === REJECTED) {
onRejected(this.reason);
}
if (this.state === PENDING) {
this.onFulfilledStack.push(() => {
onFulfilled(this.value);
})
this.onRejectedStack.push(() => {
onRejected(this.reason);
});

onFulfilled和onRejected结果判断

由于Promise链式调用,在then中执行onFulfilled和onRejected返回的值有可能也是一个Promise对象,也有可能是一个其他普通的值,还有可能是抛出一个错误,而这些值都有各自不同的处理要求:

  1. 如果返回的值是普通值,则调用promise2的resolve;
  2. 如果返回的值是一个promise,则需要等待执行完把这个promise的value或reason传给promise2的resolve或reject;
  3. 如果是抛出一个错误,则调用promise2的reject;
  4. 如果返回的值等于promise2,则抛出类型错误;

基于上面的要求,可以使用一个resolvePromise来处理,需要传入以下值:

  1. 执行onFulfilled和onRejected之后返回的值,x,用来判断处理结果;
  2. promise2的resolve或reject,用来接收x;
  3. promise2,用来判断x是否与之引用相等。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
promise2 = new Promise((resolve, reject) => {
if (this.status === SUCCESS) {
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject)
} catch (err) {
reject(err)
}
})
}
if (this.status === FAIL) {
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject)
} catch (err) {
reject(err)
}
})
}
if (this.status === PENDING) {
this.onResolvedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject)
} catch (err) {
reject(err)
}
})
});
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject)
} catch (err) {
reject(err)
}
})
});
}
})

返回值x的判断

Promise A+规范提供了对x的判断方案。首先,判断x是否为一个方法或者一个对象,因为Promise的实现可以通过Function或Object来实现。其次,x带有一个then方法,这是本身Promise规范要求
的方法。除此之外,都判断为普通值。

另外,如果x是一个Promise,那么作为x成功态返回的值,同样也要判断是否为Promise,这个判断的处理跟x的处理相同,所以resolvePromise可以以递归方法的形式实现。

在判断完x为Promise之后,Promise A+规范有一个比较严谨的考虑,就是为了防止同时出现成功态跟失败态,要在调用promise2的resolve或reject之后,禁止再调用promise2的reject或resolve。

因此,resolvePromise实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function resolvePromise (promise2, x, resolve, reject) {
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise #<my Promise>'))
}
let called;
if (typeof x === 'function' || (typeof x === 'object' && x != null)) {
try {
let then = x.then
if (typeof then === 'function') {
then.call(x, y => {
if (called) return
called = true
resolvePromise (promise2, y, resolve, reject)
}, r => {
if (called) return // 2)
called = true
reject(r)
})
} else {
resolve(x)
}
} catch (e) {
if (called) return
called = true
reject(e)
}
} else {
resolve(x)
}
}

测试

以上基本实现基于Promise A+规范的Promise类。最后可以安装promise-aplus-tasts库对实现的Promise做检验。测试之前需要加以下代码:

1
2
3
4
5
6
7
8
Promise.defer = Promise.deferred = function () {
let dfd = {}
dfd.promise = new Promise((resolve, reject) => {
dfd.resolve = resolve;
dfd.reject = reject;
});
return dfd;
}

这段代码本身也实现了一种异步操作代码同步化的处理。最后使用命令promises-aplus-tests xxx.js执行测试。

工作总结(6)

发表于 2019-06-09 | 分类于 JavaScript , Vue , 工作总结

这篇工作总结,主要针对移动端网页开发中遇到的软键盘坑来整理的。

最近在做移动端页面,然后遇到一个很多人都遇到的坑:当页面有固定在底部的评论框的情况下,当点击评论框获得聚焦之后,底部评论框会被软键盘顶起,但是不会贴着软键盘顶部,而且后面的页面也会向上滚动一定高度,而且仍然可以滚动。这个问题的结果就是,在评论框获得聚焦时,整个页面会滚动,输入框位置很突兀,如果输入框是textarea,则有可能有一部分推到页面顶部一样,用户体验非常差。

这个问题拆解出来,有几个需要解决的问题:

  1. 背景页面仍然可以滚动。我尝试了增加遮罩层,但是还是不受控制;

  2. 底部评论框顶起的位置不正确。这个问题在安卓系统不会出现,所以是iOS的坑;

  3. 底部评论框获得聚焦的时候,背景页面会被顶起。

这个坑我在网上找了很多帖子,发现真的是一个很难解决的问题。我也有找到一些解决方案,但是实际实现下来效果还是很差。这里我们一个个问题来整理如何解决。

背景页面滚动问题

这个问题不管在安卓还是iOS系统上都会出现。原因应该出现在本身页面高度超过窗口高度,而且移动端有touchmove事件,虽然有遮罩层,但是其实body是遮罩层的根节点,所以存在事件捕获跟冒泡的问题。

因此,主要的问题是要阻止touchmove事件。解决方案需要完成两步操作:

1. 禁止body的touch事件。这样可以确保touchmove事件不会从body元素开始捕获

处理的办法是设置body元素的touchAction属性为none:

1
document.body.style.touchAction = 'none';

这样就可以禁止body的touch事件。不过这个只是适用于安卓系统,在safari上并不适用,因为safari不支持该属性

2. 阻止body发生默认的行为。这样可以保证touchmove事件不会触发body元素的滚动

处理的办法是使用e.preventDefault(),同时在绑定addEventListener的时候,需要再传一个options。

1
2
3
4
5
document.body.addEventListener('touchmove', this.stopTouch, { passive: false, capture: true });

stopTouch (e) {
e.preventDefault();
}

addEventListener的第三个参数options的写法比较少用到,我们一般用到的是useCapture,而且一般也是使用默认值false。在MDN有详细的说明:

1
2
3
4
5
options
一个指定有关 listener 属性的可选参数对象。可用的选项如下:
capture: Boolean,表示 listener 会在该类型的事件捕获阶段传播到该 EventTarget 时触发。
once: Boolean,表示 listener 在添加之后最多只调用一次。如果是 true, listener 会在其被调用之后自动移除。
passive: Boolean,设置为true时,表示 listener 永远不会调用 preventDefault()。如果 listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告。

我们在这里使用{ passive: false },这样使用preventDefault()时Chrome等一些浏览器就不会报错;而使用{ capture: true }则可以是事件尽早被阻止。

第一个问题完整的解决方案代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
watch: {
show(newVal) {
this.toggleContainerTouchAction(newVal);
if (newVal) {
document.body.addEventListener('touchmove', this.stopTouch, { passive: false, capture: true });
} else {
document.body.removeEventListener('touchmove', this.stopTouch, { capture: true });
}
}
}

method: {
toggleContainerTouchAction (flag) {
document.body.style.touchAction = flag ? 'none' : 'auto';
},
stopTouch (e) {
e.preventDefault();
},
}

底部评论框顶起的位置

首先,这个问题的根本问题是iOS在处理fixed布局时,当页面输入框获得焦点的时候,会把fixed改成absolute绝对定位,同时计算fixed容器在页面的位置,以此作为绝对定位的位置。正常来说其实会让底部评论框顶到软键盘顶部的位置,虽然如果滚动页面的话底部评论框是会跟软键盘分开,但是上面已经有解决方案了,所以其实还好。问题在于因为在软键盘上滑的同时,页面整体也会被动向上滚动,但评论框的位置是在页面滚动前计算的,所以才会出现顶起后还跟软键盘分离的情况。

因此,首先可以想到的是,不用fixed布局,而改用absolute布局,同时在输入框获得焦点的时候,动态计算输入框容器的高度。

不过直接放弃fixed布局其实是很可惜的,因为如果一开始写布局的时候就用绝对定位,那底部评论框就得一直动态计算位置,而且这个问题只是iOS系统才会出现,安卓系统是没问题的。

所以在处理的时候,我把问题拆分了:

  1. 输入框在布局上,iOS跟安卓是分开处理的;
  2. 其次,iOS系统的布局,也按软键盘调起(等同于输入框获得焦点)与否分开处理;
  3. 因为页面滚动已经被设置为不可滚动,所以动态计算可以控制在一定次数以内。这三个优化可以让方案实施时减少JS的操作,提高页面性能,增加CSS样式操作。
1. 样式布局

样式布局上,我把fixed布局跟absolute布局从原来class.input-container中分离出来:

1
2
3
4
5
6
7
8
9
.input-container-absolute {
position: absolute;
top: 0;
}

.input-container-fixed {
position: fixed;
bottom: 0;
}

然后通过v-bind先把.input-container-fixed绑到输入框容器上。当输入框获得焦点的时候,判断平台是iOS还是安卓,如果是iOS,就改成.input-container-absolute,如果是安卓,就不变:

1
2
3
4
5
6
7
8
handleComment () {
if (this.tp.platformInfo === 'iOS') {
this.inputCls = 'input-container-absolute input-container';
/* ... */
} else {
/*...*/
}
},

当输入框失去焦点的时候,再改回fixed布局:

1
2
3
4
5
6
7
handleBlur () {
if (this.tp.platformInfo === 'iOS') {
/* ... */
this.$refs.inputCon.style.top = '';
this.inputCls = 'input-container-fixed input-container';
}
},

这样对于安卓系统来说,解决iOS系统的js操作跟安卓系统来说一点影响都没有。

2. iOS系统的布局

其实上面的代码也基本说明了,在iOS系统上,布局也不是完全使用绝对布局,就上面的代码,就可以保证在软键盘没有被调起的时候,不需要js控制布局。

3. 动态计算

这个问题是整个问题的核心点。首先,使用什么值来确定输入框的位置。因为使用了绝对定位,而且这个位置问题其实是y轴上滚动造成的,所以问题缩小为top跟bottom的选择上。一般fixed布局使用bottom,绝对定位也使用bottom可以统一定位方式,但是实际在.input-container-fixed跟.input-container-absolute的样式上,其实已经说明了绝对定位时,我是选择top来控制输入框的位置的。因为我在尝试使用bottom,发现计算不出来,尴尬…

所以我是计算了top。怎么计算?我们这么考虑,应该是这样的公式:

1
top = 页面滚动高度 + 视窗高度 - 输入框容器高度

首先,页面滚动高度可以通过document.documentElement.scrollTop或document.body.scrollTop获得;输入框是在软键盘的顶部,而且其实当软键盘调起之后,实际的视窗高度是原来视窗高度减去软键盘高度。这部分不需要计算,直接使用window.innerHeight;输入框容器高度可以通过offsetHeight获得。因此,公式转换成:

1
top = document.documentElement.scrollTop/document.body.scrollTop + window.innerHeight - this.$refs.input.offsetHeight

这里简单说一下,bottom的计算我就是用上面的公式,就是不减去容器高度而已,但是显示效果就是不对…

所以就有下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
getScrollTop () {
let scroll_top = 0;
if (document.documentElement && document.documentElement.scrollTop) {
scroll_top = document.documentElement.scrollTop;
} else if (document.body) {
scroll_top = document.body.scrollTop;
}
return scroll_top;
},

handleScroll () {
if (this.commentVisible) {
this.scrollHeight = this.getScrollTop();
// +5px是为了确保输入框跟软键盘是贴在一起的,所以让输入框有一点点藏在软键盘下面是最保险的做法
this.$refs.inputCon.style.top = `${this.scrollHeight + window.innerHeight - this.$refs.inputCon.offsetHeight + 5}px`;
}
},

其实直接使用上面的代码也不能解决,因为软键盘弹出是有时间的,也就是如果一获取焦点就计算,那就会导致输入框还在软键盘弹出前视窗的底部。所以要动态计算,即在一定时间内,需要使用定时器setInterval不断计算输入框的位置,这样在软键盘调起的过程,输入框也能一直附在软键盘顶部:

1
2
3
4
5
6
7
8
9
10
11
scrollContent () {
clearInterval(this.interval);
let i = 0;
this.interval = setInterval(() => {
this.handleScroll();
i++;
if (i > 20) {
this.clearSrcoll();
}
}, 50)
},

这里控制了只计算20次,是因为软键盘被调起的时间其实很短,测试情况大概1秒内就可以完成,不用一直动态计算,而且定时器间隔时间是50,所以就计算为20次。

因此,样式布局的方法就得增加一些代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
clearSrcoll() {
clearInterval(this.interval);
},

handleComment () {
if (this.tp.platformInfo === 'iOS') {
this.inputCls = 'input-container-absolute input-container';
setTimeout(() => {
this.scrollContent();
}, 10)
} else {
/*...*/
}
},

handleBlur () {
if (this.tp.platformInfo === 'iOS') {
/* ... */
this.clearSrcoll();
this.$refs.inputCon.style.top = '';
this.inputCls = 'input-container-fixed input-container';
}
},

这里要说明的是,跟业务代码是有不同的,因为实际上页面一开始放的评论框容器没有input标签,只是放了一个像输入框的底部容器,加了点击事件后,会控制显示输入框容器的显示,然后在DOM刷新之后(this.$nextTick)使用focus()触发输入框获得焦点。所以就不放完整代码了。

页面顶起

这个问题其实没有解决,考虑到影响不大,而且当页面高度只有一屏时,也不会出现(主要是没有找到很好的解决方案),所以就不处理了。但是这个问题在其他情况下有解决方案,详情可以看工作总结(五)中关于iOS下微信H5页面的坑解决方案的整理。

上面就是全部内容。

工作总结(5)

发表于 2019-06-09 | 分类于 JavaScript , Vue , 工作总结

本篇总结主要涉及微信H5开发中的比较重要的内容,整理下来也是为了以后可以方便开发。

其实微信接口的使用在微信公众号文档中都有详细的讲解,所以我会整理工作中使用微信接口的代码逻辑。所有代码都是在Vuejs框架下开发的。

微信网页授权

微信网页授权是微信H5中最为重要的,因为如果授权不成功,用户也进不了页面中来。

微信网页授权分两种情况,用scope来体现:当scope为snsapi_base时,网页授权可以获得用户的openid,而且是静默授权,并自动跳转到回调页中,相当于直接进入H5页面中;当scope为snsapi_userinfo时,网页授权可以获得用户的基本信息,但是需要用户手动同意。还有一种是涉及用户关注公众号的,暂且不提。这里我们主要也是整理前一种网页授权方式。

微信公众号文档中对网页授权流程有分步骤,按照以获取用户openid为目的,操作就是引导用户打开以下链接:

1
2
3
4
5
6
7
// appid为公众号唯一标识
// redirect_uri是授权后重定向的回调链接,对应业务链接
// response_type是返回类型,请填写code
// scope是授权类型
// state为可选的参数,重定向后会带上,业务可以带一些参数
// #wechat_redirect是无论直接打开还是做页面302重定向时候,必须带此参数
https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect

然后静默获得用户授权之后,会跳转到以下链接:

1
2
3
4
// redirect_uri是重定向的回调链接
// code是换取access_token的票据
// state是网页授权时带上的可选参数
redirect_uri/?code=CODE&state=STATE

我在搜索相关开发博客的时候也有看到在前端完成上面跳转的,简单说就是先判断链接是否带有code参数,没有的话就把window.location.href指向网页授权链接,重定向之后获得code参数:

1
2
3
4
5
6
7
8
this.wxCode = this.$route.query.code ? code : '';
let local = window.location.href;
if (this.wxCode == null || this.wxCode === '') { // 如果获取不到code,则重定向
window.location.href = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${APPID}&redirect_uri=${encodeURIComponent(local)}&response_type=${this.wxCode}&scope=${wxConfig.scope}#wechat_redirect`;
} else {
// 如果获取到code,发请求后端
/* ... */
}

但是实际开发中我们并没有在前端做这部分内容,而且交给后端做重定向。因为网页授权链接中带有appid,是公众号唯一标识,比较重要,考虑到保险起见,不放在前端代码中。

上面的代码可以看到,获取code之后,还需要后端获取access_token。文档也有提到,这部分安全级别非常高,涉及公众号secret跟用户的access_token,所以不允许在客户端请求。因此,获得code之后,我们可以把code通过接口传到服务器,在服务器进行access_token的获取。这一步可以获取到用户的openid,这个openid是用户跟公众号唯一的openid,所以非常有用。所以最终的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
computed: {
inWx () { // 判断是否在微信环境中
const userAgent = navigator.userAgent.toLowerCase();
return /micromessenger/.test(userAgent);
}
},
created () {
if (this.inWx) {
this.initInfo();
}
},
methods: {
initInfo () {
this.wxCode = this.$route.query.code;
// 由于是后端实现网页重定向跟授权的,所以其实前端只要获得code并发起请求传给服务器就可以了
/* ... */
}
}

微信SDK

首先,使用微信sdk的前提是域名已绑定微信公众号。然后步骤是:引入js文件,通过config配置权限,通过ready接口跟error接口处理验证,最后用checkJsApi接口判断接口支持情况。

引入js文件

在html文件中引入:

1
2
3
4
<script src="http://res.wx.qq.com/open/js/jweixin-1.2.0.js"></script>
// 或
// <script src="http://res2.wx.qq.com/open/js/jweixin-1.4.0.js"></script>
// 都支持https

config配置

根据微信公众号文档,config配置需要以下字段:

1
2
3
4
5
6
7
8
 wx.config({
debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: '', // 必填,公众号的唯一标识
timestamp: , // 必填,生成签名的时间戳
nonceStr: '', // 必填,生成签名的随机串
signature: '',// 必填,签名
jsApiList: [] // 必填,需要使用的JS接口列表
});

由于appid是公众号唯一标识,最好不要存放在客户端,所以可以通过请求从服务器获得除debug跟jsApiList之外的信息。所以会分两步走:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
 data() {
return {
jsApiList: [
'updateAppMessageShareData',
'updateTimelineShareData',
'onMenuShareTimeline',
'onMenuShareAppMessage',
'onMenuShareQQ',
'onMenuShareWeibo',
'onMenuShareQZone',
'previewImage',
'openLocation',
'getLocation'
],
checkResult: {},
wxShareConfig: {},
wxconfig_share: {
title: '', // 分享标题
desc: '', // 分享描述
link: '', // 分享链接
imgUrl: '', // 分享图标
},
};
},
methods: {
getWechatConfig () {
axios.get(api).then(res => {
this.wxConfig = res.data;
this.wxInit(this.wxConfig);
}).catch(error => {
/* ... */
}
},
wxInit(_rpdata) {
// this.jsApiList = _rpdata.jsApiList;
wx.config({
debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: _rpdata.appId, // 必填,公众号的唯一标识
timestamp: _rpdata.timeStamp, // 必填,生成签名的时间戳
nonceStr: _rpdata.nonceStr, // 必填,生成签名的随机串
signature: _rpdata.signature, // 必填,签名,见附录1
jsApiList: this.jsApiList // 必填,需要使用的JS接口列表
});
},
}

ready接口跟error接口处理

在传入config之后,可以直接调用ready接口处理验证成功的,而error接口则是处理验证失败的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
methods: {
wxInit(_rpdata) {
// this.jsApiList = _rpdata.jsApiList;
wx.config({
debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: _rpdata.appId, // 必填,公众号的唯一标识
timestamp: _rpdata.timeStamp, // 必填,生成签名的时间戳
nonceStr: _rpdata.nonceStr, // 必填,生成签名的随机串
signature: _rpdata.signature, // 必填,签名,见附录1
jsApiList: this.jsApiList // 必填,需要使用的JS接口列表
});
wx.ready(() => {
/* ... */
});
wx.error(function (res) {
/* ... */
});
},
}

checkJsApi接口

验证成功之后,还需要调用checkJsApi接口来判断客户端是否支持指定JS接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
wxInit(_rpdata) {
/* ... */
wx.ready(() => {
wx.checkJsApi({
this.checkJsApi().then(() => {
/* ... */
/* 这里就可以调用各种验证成功的接口了 */
});
})
}
/* ... */
},

checkJsApi() {
return new Promise((resolve) => {
wx.checkJsApi({
jsApiList: this.jsApiList, // 需要检测的JS接口列表,所有JS接口列表见附录2,
success: (res) => {
console.log(res)
console.log(res.checkResult)
this.checkResult = res.checkResult;
// 以键值对的形式返回,可用的api值true,不可用为false
// 如:{"checkResult":{"chooseImage":true},"errMsg":"checkJsApi:ok"}
this.checkResult = res.checkResult;
resolve();
}
});
});
},

以上完成微信SDK基础配置处理。

微信分享

微信分享这块包括了所有分享的平台,使用的也是微信SDK所提供的所有分享相关的接口。目前微信分享有以下这些接口:

1
2
3
4
5
6
7
 updateAppMessageShareData // 分享给朋友及分享到QQ接口
updateTimelineShareData // 分享到朋友圈及分享到QQ空间接口
onMenuShareTimeline // 分享到朋友圈接口(即将废弃)
onMenuShareAppMessage // 分享给朋友接口(即将废弃)
onMenuShareQQ // 分享到QQ接口(即将废弃)
onMenuShareWeibo // 分享到腾讯微博
onMenuShareQZone // 分享到QQ空间

所有接口的写法基本相同,所有这里只列举一个接口的写法作为例子。通过上面的接口验证后,就可以调用这些接口了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 wxShareInit() {
const checkResult = this.checkResult;
if (checkResult.updateAppMessageShareData) {
wx.updateAppMessageShareData({
title: this.wxconfig_share.title, // 分享标题
desc: this.wxconfig_share.desc, // 分享描述
link: this.wxconfig_share.link, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
imgUrl: this.wxconfig_share.imgUrl, // 分享图标
success: function () {
// 用户确认分享后执行的回调函数
console.log('share AppMessage success!')
},
})
}
// 其他接口写法相同
/*...*/
}

微信关注

微信关注这个比较容易,其实就是跳转到历史消息页面,如果用户没有关注公众号的话,这个页面会显示关注按钮。这个链接的形式是:

1
https://mp.weixin.qq.com/mp/profile_ext?action=home&__biz=xxxxxxxx#wechat_redirect

其中,xxxxxxxx用公众号的uin id的base64编码替换。这个公众号的uin id可以在微信公众平台登入账号后,在源代码中查找uin即可找到一串数字,然后把这串数字用base64编码,获得。

不过实际的效果是这个页面的关注按钮有时会出现,有时没有,后来网上查了一下,这个页面的关注按钮已经被微信屏蔽了,除非跟微信投钱合作,才会显示。所以……

微信关注判断

微信关注的判断目前我还没有做过,但是大致的了解到判断的方案。最关键的是需要获得openid,获得access_token,然后通过获取用户基本信息接口获得用户信息,进而判断。

首先,获得openid的方法已经在微信网页授权一节有说明,故可以获得;获得access_token需要前端获取code,然后传给服务器去换access_token,所以这一步也在微信网页授权一节有说明,故也可以获得。

获取用户基本信息接口为:

1
2
// http请求方式: GET
https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN

然后根据微信公众号开发文档所介绍的,返回的数据有这些:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"subscribe": 1,
"openid": "o6_bmjrPTlm6_2sgVt7hMZOPfL2M",
"nickname": "Band",
"sex": 1,
"language": "zh_CN",
"city": "广州",
"province": "广东",
"country": "中国",
"headimgurl":"http://thirdwx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/0",
"subscribe_time": 1382694957,
"unionid": " o6_bmasdasdsad6_2sgVt7hMZOPfL"
"remark": "",
"groupid": 0,
"tagid_list":[128,2],
"subscribe_scene": "ADD_SCENE_QR_CODE",
"qr_scene": 98765,
"qr_scene_str": ""
}

这里可以获得到不少的信息了,这里我们关注的是subscribe这个字段,是指用户是否订阅该公众号标识。0为未关注,1为已关注。这样,我们就可以判断用户是否关注公众号了。

微信支付

微信支付有两种方案,一种是依赖上面的微信JS-SDK,再配置完成之后调用chooseWXPay方法:

1
2
3
4
5
6
7
8
9
10
11
// 官方文档
wx.chooseWXPay({
timestamp: 0, // 支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
nonceStr: '', // 支付签名随机串,不长于 32 位
package: '', // 统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=\*\*\*)
signType: '', // 签名方式,默认为'SHA1',使用新版支付需传入'MD5'
paySign: '', // 支付签名
success: function (res) {
// 支付成功后的回调函数
}
});

可以参考上面微信分享一节的封装方案进行开发。

另一种方案是调用在微信内置浏览器才有的WeixinJSBridge内置对象。这种方案使用起来比较简单,不需要引入什么,配置什么,只要是在微信内置浏览器中,就一定能调用这个内置对象。使用的时候只要先判断是否存在WeixinJSBridge内置对象,存在则可以传入请求参数,然后再判断返回的支付信息。需要传入的参数跟上面的一样,一般需要发起支付请求给服务器,让服务器提供需要的参数,然后在去调用WeixinJSBridge内置对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 微信支付api相关配置文档
onBridgeReady (data) {
if (typeof WeixinJSBridge === 'undefined') {
this.$toast({
message: '请使用微信内置浏览器进行支付',
position: 'bottom'
});
} else {
WeixinJSBridge.invoke(
'getBrandWCPayRequest',
{
appId: data.appId, // 公众号名称,由商户传入
timeStamp: data.timeStamp, // 时间戳,自1970年以来的秒数
nonceStr: data.nonceStr, // 随机串
package: data.package,
signType: data.signType, // 微信签名方式:
paySign: data.paySign // 微信签名
},
res => {
if (res.err_msg === 'get_brand_wcpay_request:ok') {
this.$toast({
message: '支付成功',
position: 'bottom'
});
/* ... */
} else {
this.$toast({
message: '支付失败',
position: 'bottom'
});
}
}
)
}
}

微信地图

在微信H5中调起微信地图也是一个常见的功能点,也是可以通过微信JS-SDK的接口来实现。

一般使用微信地图分两种情况:一种是需要打开已知地点的微信地图,方便用户对地点进行导航;另一种是需要打开用户所在位置的微信地图。

已知地点的微信地图

这里使用的为微信JS-SDK 中的openLocation接口:

1
2
3
4
5
6
7
8
9
// 官方文档
wx.openLocation({
latitude: 0, // 纬度,浮点数,范围为90 ~ -90
longitude: 0, // 经度,浮点数,范围为180 ~ -180。
name: '', // 位置名
address: '', // 地址详情说明
scale: 1, // 地图缩放级别,整形值,范围从1~28。默认为最大
infoUrl: '' // 在查看位置界面底部显示的超链接,可点击跳转
});

根据官方文档,我们需要拿到已知地点的纬度,经度,位置名跟地址详情说明。我们可以通过以下网页获得:

1
http://www.gpsspg.com/maps.htm

然后参考前面微信分享的封装进行开发。

打开用户所在位置的微信地图

这里还需要使用另一个接口:getLocation接口。也很简单,这里直接贴代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
wx.getLocation({
type: 'gcj02',
success: function (res) {
wx.openLocation({
latitude: res.latitude,
longitude: res.longitude,
name: config.name,
address: config.address,
scale: 28,
infoUrl: ''
})
},
cancel: function (res) {
console.log('cancel: ', res);
}
})

微信H5的坑

iOS中微信环境下页面有浮框软键盘隐藏后页面不能点击问题

问题是这样的:浮框中有输入框,使用了fixed布局,可能在页面正中间,也可能出现在页面底部。当输入框失去焦点时,软键盘隐藏,但是页面所有点击事件都发生错乱。

其实和这个问题跟iOS系统有关,也跟微信有关。本身iOS系统下调出软键盘就会影响页面布局,然后滚动页面,然后微信内置浏览器会在软键盘隐藏时恢复页面布局,但是并没有恢复监听事件的位置。也就是在虽然有监听事件的标签元素恢复到原位了,但是监听的区域并没有恢复,因为受到页面滚动的影响。

如何处理?分两种情况:一种情况是浮框只出现在页面第一屏,这种情况下,在软键盘隐藏的同时,操作页面滚动到顶部,即可恢复正常的监听区域:

1
2
3
scrollBack () { // 绑定在blur事件上
window.scrollTo(0, 0);
}

另一种情况是浮框在底部,这种情况主要跟iOS系统有关,会单独放在下一篇工作总结中详细整理。

123…5

Listentolife

Listentolife's Blog

49 日志
19 分类
61 标签
GitHub E-Mail
© 2020 Listentolife
由 Hexo 强力驱动
|
主题 — NexT.Mist v5.1.4
   |