分类目录归档:前端

iOS 11 中 fixed 容器中 输入框光标错位的问题处理

iOS11 之后,fixed浮层内的输入框光标会发生偏移。即 fixed 定位的容器中输入框光标的位置显示不正确,没有正常地显示在输入框中,而是偏移到了输入框外面。

这个问题很容易复现,只要满足以下条件即可触发此问题:

1、页面 body 高度超过视窗高度,即页面有滚动条。
2、点击页面中 fixed 定位的容器中的输入框,键盘弹起如果发生页面滚动或者键盘弹起后手动滚动页面则会出现如上面视频中光标偏移的问题。偏移量为页面滚动的距离。

目测确实是 iOS 11 新版 WebKit 引入 的 bug。目前处理的方案有两个。

一:在弹窗显示时,通过给 body 设置相关样式,使body不可滚动:

height: 100%!important;
overflow: hidden!important;

关闭弹窗后,注意要把相关样式移除。

二:在输入框blur的时候将页面滚动到顶部(起始位置):

<input type="tel" @blur.prevent="scrollTop()" @focus.prevent="stopScroll()" class="login-phone" v-model.trim="loginPhone" placeholder="请输入手机号"/>
......
scrollTop: function(){
	if(this.$fun.isIOS()){
		this.scrollTimer = setTimeout(()=>{
			document.body.scrollTop = 0
    		document.documentElement.scrollTop = 0
		},10)
	}
},
stopScroll: function(){
	if(this.scrollTimer && this.$fun.isIOS()) clearTimeout(this.scrollTimer)
},

这个方案用在了vue项目,需要注意是input blur的时候需要做一下延迟,否则会有抖动现象,focus的时候取消定时。

vue-awesome-swiper 使用问题汇总

因为之前都是使用Swiper插件来实现各种轮播效果,在新的vue项目中,就采用了vue-awesome-swiper这个基于Swiper的组件。但是使用的过程,出现不少的问题,现将出现的问题及解决方法记录下来。

这里先贴出最终的主要代码片段:

<div class="banner" ref="banner">
  <div v-if="bannerList.length"><!-- 使用v-if,处理异步请求数据,导致轮播第二次之后,首屏会自动跳过 -->
    <swiper :options="bannerOption" ref="bannerSwiper">
      <swiper-slide v-for="(item,index) in bannerList" :key="index">
        <img :src="item.img" alt="" class="swiper-lazy">
      </swiper-slide>
    </swiper>
  </div>
</div>
export default {
  data() {
    return {
      bannerOption: {
        slidesPerView: 1,
        loop: true,
        autoplay: {//定时轮播的正确写法
          delay: 3000,
          disableOnInteraction: false //处理滑动之后不能自动轮播的问题
        },
        on:{
          click:()=>{//点击事件需放在这里处理,否则无法触发最后一个轮播图
            let index = this.$refs.bannerSwiper.swiper.realIndex
            let link = this.bannerList[index]['link']

            window.location.href = link
          }
        }
      },
      bannerList: []
    }
  },
  activated(){

    if(this.bannerList.length) this.$refs.bannerSwiper.swiper.slideNext()//处理从其他页面切换回来轮播图不滚动问题
  }
}

问题1:定时轮播,不能直接给autoplay赋值毫秒数,需要使用上面的例子中写法。

问题2:异步请求数据,轮播第二次之后,首屏会自动跳过,原因是由于数据还没加载完成,swiper就初始化了,解决的方法是采用v-if的方式,等待数据加载完成后再初始化。

问题3:最后一个轮播图点击无效,目前处理的方法是将点击的事件放在swiper的on方法里面。

问题4:在vue单页应用中,从其他组件切换回来,轮播图不滚动了,处理方法是在activated钩子中调用swiper的slideNext方法

使用CSS3和canvas实现多点移动动画

最近在做一款太鼓达人的游戏,其中需要实现多个点快速移动动画,一开始使用CSS3的transform来实现,发现超过5个点后,动画效果开始出现卡顿。尝试各种优化方法都无效果,PC端上运行一直都会出现卡顿(测试了下iOS下确是很流畅)。最后改用canvas实现,性能上好了很多。

先来看看CSS3的效果

canvas主要是实现原理是通过遍历所有点的数组,并不断更新点的Y轴。主要实现代码如下:
canvas效果

addBall: function(type){
      if(type == 0){
        var img = TG.options.ballImg1;
      }else if(type == 1){
        var img = TG.options.ballImg2;
      }
      // 创建新的ball对象
      var ball = {
        'img': img,
        'x': 1200,
        'y': 20,
        'w': 60,
        'h': 60
      }
      // 将其保存在balls数组中
      TG.options.balls.push(ball);
    },

drawFrame: function() {
      if(TG.options.balls.length == 20){
        TG.options.balls.splice(0,1);
      }
      // 清除画布
      TG.options.context.clearRect(0, 0, TG.options.canvas.width, TG.options.canvas.height);
     
      TG.options.context.beginPath();
     
      // 循环所有的球
      for(var i=0; i<TG.options.balls.length; i++) {
        // 把每个球移动到新的位置
        var ball = TG.options.balls[i];
        ball.x -= 4;
        TG.drawImg(TG.options.context,ball);
      }

      (TG.requestAnimFrame)()(TG.drawFrame);
    },

移动端实现块拖动功能

主要的原理是通过获取手指拖动时的位置坐标,来计算块移动的相对位置(判断是否超出界面)。拖动的同时需要禁止页面滚动,拖动结束后恢复。具体的实现代码如下:

var helper=document.querySelector('#helper');
var maxW=document.body.clientWidth-helper.offsetWidth;//可移动的最大宽度
var maxH=document.body.clientHeight-helper.offsetHeight;//可移动的最大高度

helper.addEventListener('touchstart',function(e){
    var ev = e || window.event;
    var touch = ev.targetTouches[0];
    oL = touch.clientX - helper.offsetLeft;//鼠标所点位置的坐标
    oT = touch.clientY - helper.offsetTop;
    //阻止页面滚动
    document.body.ontouchmove=function(e){
	    e.preventDefault();
	}

	//兼容android微信
	document.body.style.height = '100%';
	document.body.style.overflow = 'hidden';
})

helper.addEventListener('touchmove',function(e){
   var ev = e || window.event;
   var touch = ev.targetTouches[0];
   var oLeft = touch.clientX - oL;
   var oTop = touch.clientY - oT;
   if(oLeft<0){
       oLeft=0;
   }else if (oLeft>=maxW) {
       oLeft=maxW;
   }
   if(oTop<0){
       oTop=0;
   }else if (oTop>=maxH) {
       oTop=maxH;
   }

   helper.style.left = oLeft + 'px';
   helper.style.top = oTop + 'px';
})

helper.addEventListener('touchend',function(){
	//恢复页面滚动
    document.body.ontouchmove=function(e){}

   	//兼容android微信
    document.body.style.height = 'auto';
	document.body.style.overflow = 'auto';
});

Demo:(兼容安卓、iOS及相应微信端)

drap

微信语音识别功能实战

最近在微信上实现一个长按录音并识别语音功能,即在用户按下时调用微信 JS-SDK 录音接口,然后在用户松开时停止录音并识别录下了的语音。

流程如下:

touchstart -> wx.startRecord()
touchend -> wx.stopRecord()-> wx.translateVoice()

嗯,看起来流程很简单,实现起来也很快,但是测试的时候马上发现了问题:

1.短按会出现无法结束录音

如果点击了一下录音按钮, 相当于快速地startRecord然后stopRecord,那么stopRecord是极有可能是无效的,不会执行任何callback。因为微信JSSDK的调用是异步的。你调用startRecord的时间,和startRecord的success的callback被执行的时间可能间隔了若干毫秒甚至秒。这意味着,用户点击按钮可能会造成:虽然是先调用startRecord再调用stopRecord,但是可能stopRecord先于startRecord调用成功。这样就造成无法结束录音的情况。

解决的方法就是记录touchend 的时间点A,然后在startRecord调用成功时判断A是否大于此时的时间点,如果小于,则再次调用结束录音方法。

2.部分安卓机在调用startRecord后无法触发touchend

通过不断的测试,得出以下的异常表现(目前测试到的有vivo手机):

当 touchstart 时调用了 startRecord,触发的事件为: touchstart -> touchcancel(此时还未松开手指)

当 touchstart 时不调用 startRecord,触发的事件为: touchstart -> touchend(松开手指后)

可以看到:通过 touchstart 调用了 startRecord 后会立即触发元素的 touchcancel 事件,继而造成 touchend 事件就不触发了。

目前还没找到好的解决方法,只能针对安卓更好交互方式:点击录音->再次点击结束录音并识别。

相关实现代码:

<div class="voice-remote">
    <span class="cover"></span>
    <span class="icon"></span>
</div>

 

.voice-remote{border-radius:100%;width:1.8rem;height:1.8rem;position:absolute;background:#f6f6f6;bottom:1.5rem;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%);-webkit-transition:all .2s;transition:all .2s}
.voice-remote.active{width:2.8rem;height:2.8rem;bottom:1rem;border:1px solid #e7e7e7}
.voice-remote:before{content:"";width:100%;height:100%;position:absolute;z-index:2;top:0;left:0;border-radius:100%;background-image:-webkit-linear-gradient(-90deg,transparent 50%,#f6720e 50%);background-image:linear-gradient(-90deg,transparent 50%,#f6720e 50%)}
.voice-remote:after{content:"";width:100%;height:100%;position:absolute;z-index:3;bottom:0;left:0;border-radius:100%;-webkit-background-image:linear-gradient(-90deg,transparent 50%,#f6720e 50%);background-image:linear-gradient(-90deg,transparent 50%,#f6720e 50%)}
.voice-remote .cover{position:absolute;border-radius:100%;width:100%;height:100%;z-index:4;top:0;left:0;-webkit-background-image:linear-gradient(-90deg,transparent 50%,#f6f6f6 50%);background-image:linear-gradient(-90deg,transparent 50%,#f6f6f6 50%)}
.voice-remote .icon{position:absolute;width:100%;height:100%;top:0;left:0;background:#f6f6f6 url(/public/wap/app/images/voice.png) no-repeat center center;background-size:100%;border-radius:100%;z-index:5}
.voice-remote .icon:before{content:'长按说话';position:absolute;top:-.65rem;left:0;color:#f6720e;width:100%;text-align:center;font-size:.32rem}
.voice-remote.android .icon:before{content:"点击说话"}
.voice-remote.active .icon:before{content:'松开完成';top:-.85rem}
.voice-remote.android.active .icon:before{content:"点击完成"}
.voice-remote.active .icon{width:90%;height:90%;top:5%;left:5%;background-size:60%}
.voice-remote.active:before{-webkit-animation:scoll linear 2.5s;animation:scoll linear 2.5s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}
.voice-remote.active:after{-webkit-animation:xscoll linear 5s;animation:xscoll linear 5s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}
.voice-remote.active .cover{-webkit-animation:hide linear 5s;animation:hide linear 5s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}
@-webkit-keyframes scoll{0%{-webkit-transform:rotate(0)}
100%{-webkit-transform:rotate(180deg)}
}
@keyframes scoll{0%{transform:rotate(0)}
100%{transform:rotate(180deg)}
}
@-webkit-keyframes xscoll{0%{-webkit-transform:rotate(0)}
100%{-webkit-transform:rotate(360deg)}
}
@keyframes xscoll{0%{transform:rotate(0)}
100%{transform:rotate(360deg)}
}
@-webkit-keyframes hide{0%{opacity:1}
49.9%{opacity:1}
50%{opacity:0}
100%{opacity:0}
}
@keyframes hide{0%{opacity:1}
49.9%{opacity:1}
50%{opacity:0}
100%{opacity:0}
}

 

var VR = {
	options: {
		point: 0,
		tpoint: 0,
		epoint: 0,
		timer: 0
	},
	keyword: [
		{
			data: '声音大、音乐大、音乐调大、音乐调高、声音调大',
			type: 1,
			cmdid: '203',
			reply: '小主,已为你调了音乐声音~',
			error: '音乐没调成功,再试一次喔~'
		},
		{
			data: '声音小、音乐小、音乐调小、音乐调低、声音调小',
			type: 1,
			cmdid: '204',
			reply: '小主,已为你调了音乐声音~',
			error: '音乐没调成功,再试一次喔~'
		},
		{
			data: '评分开、开评分、开启评分、评分开启',
			type: 1,
			cmdid: '541',
			reply: '小主,已为你开启评分~',
			error: '评分开启失败,再试一次喔~'
		},
		{
			data: '评分关、关评分、关掉评分、评分关掉',
			type: 1,
			cmdid: '541',
			reply: '小主,已为你关闭评分~',
			error: '评分关闭失败,再试一次喔~'
		}
	],
	recode: function(){//定时最长5s后结束录音
		VR.options.timer = setInterval(function(){
			var time = +new Date() - VR.options.point;
			if(time >= 5000){
				if(VR.getPlatformType() == 2){
					$('.voice-remote').removeClass('active');
				}
				clearInterval(VR.options.timer);
				setTimeout(function(){
	        		VR.translate();
	        	},100);
			}
		},1000);
	},
	cmdMatch: function(words){//根据语音识别处理的内容匹配关键词
		for(i in VR.keyword){
			var key = VR.keyword[i]['data'].split('、');
			for(n in key){
				//console.log(key[n]);
				if(words.indexOf(key[n]) > -1){
					VR.runCmd(i);//匹配到关键词执行相应方法
					return true;
				}else {
					if(i == VR.keyword.length-1 && n == key.length - 1){
						window.location.href = '/Wap/app?searchby=voice&searchkey='+words+'#pageSearchList';
					}
				}
			}
		}
	},
	runCmd: function(index){
		var cmd = VR.keyword[index];
		//执行方法...
	},
	translate: function(){//结束录音并识别语音
		wx.stopRecord({
            success: function(res) {
                localId = res.localId;
                wx.translateVoice({
		            localId: localId,
		            complete: function(res) {
		                if (res.hasOwnProperty('translateResult')) {
		                    //alert('识别结果:' + res.translateResult);
		                    VR.cmdMatch(res.translateResult.split('。')[0]);
		                } else {
		                    //alert('无法识别');
		                }
		            }
		        });
            },
            fail: function(res) {
            	alert(JSON.stringify(res));
            }
        });
	},
	getPlatformType: function(){ //1-IOS微信 2-安卓微信 0-未知
        var ua = navigator.userAgent.toLowerCase();
        if(/micromessenger/.test(ua)){
            return /android/.test(ua)?'2':'1';
        }
        return '0';
    },
	init: function(){
		if(VR.getPlatformType() == 2){//android采用点击方式录音,ios默认采用长按方式录音
			$('.voice-remote').addClass('android');
		}

        wx.config({
            debug: false,
            appId: sign_pkg.appId,
            timestamp: sign_pkg.timestamp,
            nonceStr: sign_pkg.nonceStr,
            signature: sign_pkg.signature,
            jsApiList: [
                'checkJsApi',
                'startRecord',
                'stopRecord',
                'translateVoice'
            ]
        });

		wx.ready(function(){
			$('.voice-remote').on('touchstart',function(){

				VR.options.tpoint = +new Date();//记录touchstart时间点

				if(VR.getPlatformType() == 2){//如果是android端,touchstart后判断当前是否处于录音状态,如果是则结束录音并识别语音
					if($('.voice-remote').hasClass('active')){
						$('.voice-remote').removeClass('active');//切换录音按钮状态

						if(time < 5000){
							clearInterval(VR.options.timer);//清除定时结束录音定时器

							setTimeout(function(){//如果录音按钮有动画效果,需延迟结束录音,否则调用结束录音接口时,会导致动画卡顿,延时时长为动画效果时长
				        		VR.translate();
				        	},200);
						}
						return false;
					}
				}

				wx.startRecord({
					success: function(){
						$('.voice-remote').addClass('active');
						VR.options.point = +new Date();//记录开始录音成功时间点
		        		VR.recode();//启用定时结束录音定时器
		        		if(VR.options.point > VR.options.epoint && VR.options.epoint > VR.options.tpoint){//处理因为短按,startRecord还未初始成功,导致无法正常停止录音
		        			clearInterval(VR.options.timer);

				        	$('.voice-remote').removeClass('active');

				        	setTimeout(function(){
				        		VR.translate();
				        	},200);
		        		}
					},
					fail: function(res) {
						alert(JSON.stringify(res));
		            },
		            cancel: function() {
		                alert('您拒绝了授权录音');
		            }
		        });
			});

			if(VR.getPlatformType() == 1){//ios端才监听touchend事件
				$('.voice-remote').on('touchend',function(){
					VR.options.epoint = +new Date();//记录touchend时间点
					$(this).removeClass('active');

					var time = +new Date() - VR.options.point;

					if(time < 5000){//当录音间隔时间小于5s,touchend后清除定时结束录音定时器,并调用结束录音方法
						clearInterval(VR.options.timer);

			        	setTimeout(function(){
			        		VR.translate();
			        	},200);
					}
			    });
			}

		});
	}
}

VR.init();

 

PS:语音识别操作按钮,请看demo

 

 

CSS3 mask 实现心形头像

CSS3 的mask 和和PS中的蒙版很像,即为了得到特殊的显示效果,可以在遮罩层上创建一个任意形状的“视窗”,遮罩层下方的对象可以通过该“视窗”显示出来,而“视窗”之外的对象将不会显示。mask便是创建这样一个遮罩层。

关键属性: mask-image 通过读取透明度对html元素进行遮罩,黑色代表透明,白色代表不透明,灰色为半透明。适用于所有元素。

兼容性:查看 caniuse

查看 DEMO

mask

Vuejs 实用技巧

1.组件如何通过vue-router传递参数:

这里传递的参数希望不显式的添加在url上面,具体代码如下:

路由配置:

new Router({
  routes: [
    { path: '/', component: Index, name: 'index'},
    { path: '/mv/:id', component: Mplayer, name: 'mv'}
  ]
})

组件一设置参数:

<router-link :to="{ name: 'mv', params: { id: mv.id, poster: mv.cover, mvName: mv.name }}"></router-link>

组件二获取参数:

this.video.poster = this.$route.params.poster;
this.mvDetail.name = this.$route.params.mvName;

2.如何在事件内触发DOM元素的原生事件:

以点击回车键收起手机键盘为例,具体代码如下:

<form @submit.prevent="submit" action="">
  <input type="search" placeholder="搜索" v-model="searchKeyword" @keyup.enter="hideKeyboard($event)">
</form>
hideKeyboard: function(ev){
  ev.target.blur();
}

用sinopia搭建私有npm服务

随着公司内部越来越多公用组件、模块的产生,搭建一个本地私有npm库显得非常的必要。

Sinopia 是一个零配置的私有的带缓存功能的npm包管理工具,使用sinopia,你不用安装CouchDB或MYSQL之类的数据库,Sinopia有自己的迷你数据库,如果要下载的包不存在,它将自动去你配置的npm地址上去下载,而且硬盘中只缓存你现在过的包,以节省空间。

1.安装Sinopia:

npm install -g sinopia

启动:

sinopia

使用pm2启动:

npm install -g pm2
pm2 start sinopia

访问:http://localhost:4873 (改过端口)

通过修改config.yaml文件,配置Sinopia的访问权限、代理、文件存储路径等所有配置信息:

#
# This is the default config file. It allows all users to do anything,
# so don't use it on production systems.
#
# Look here for more config file examples:
# https://github.com/rlidwka/sinopia/tree/master/conf
#
# path to a directory with all packages
storage: ./storage //npm包存放的路径
auth:
htpasswd:
file: ./htpasswd //保存用户的账号密码等信息
# Maximum amount of users allowed to register, defaults to "+inf".
# You can set this to -1 to disable registration.
max_users: -1 //默认为1000,改为-1,禁止注册
# a list of other known repositories we can talk to
uplinks:
npmjs:
url: http://registry.npm.taobao.org/ //默认为npm的官网,由于国情,修改 url 让sinopia使用 淘宝的npm镜像地址
packages: //配置权限管理
'@*/*':
# scoped packages
access: $all
publish: $authenticated
'*':
# allow all users (including non-authenticated users) to read and
# publish all packages
#
# you can specify usernames/groupnames (depending on your auth plugin)
# and three keywords: "$all", "$anonymous", "$authenticated"
access: $all
# allow all known users to publish packages
# (anyone can register by default, remember?)
publish: $authenticated
# if package is not available locally, proxy requests to 'npmjs' registry
proxy: npmjs
# log settings
logs:
- {type: stdout, format: pretty, level: http}
#- {type: file, path: sinopia.log, level: info}
# you can specify listen address (or simply a port)
listen: 0.0.0.0:4873 ////默认没有,只能在本机访问,添加后可以通过外网访问。

2.安装nrm:

nrm是 npm registry 管理工具, 能够查看和切换当前使用的registry。

npm install -g nrm

使用命令:

nrm list #列出可用的 npm 镜像地址
nrm add XXXXX http://XXXXXX:4873 #添加本地的npm镜像地址
nrm use XXXX #使用本址的镜像地址

3.验证:

通过nrm添加并切换到本地的npm镜像地址。

本地如果有可用来发布的模块可以直接用,本地没有,使用npm init根据提示创建一个。

初始化创建一个模块

npm init

如果需要登录才能publish则登录

运行npm adduser注册账号,如果已经有账号直接运行 npm login

登录成功时可通过npm whoami查看

执行发布:

npm publish