分类目录归档:教程

移动端实现块拖动功能

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

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

移动端 1px border 最佳实践

在移动端Retina屏,我们用CSS定义1px的边框时,会发现边框线条很粗。实际的原因是CSS中的px单位定义的是逻辑像素值,而实际显示的效果会以物理像素呈现,在Retina屏下,1px不止占了一个像素。

具体1px占了多少个像素,我们可以通过window.devicePixelRatio来获取,devicePixelRatio是设备上物理像素和设备独立像素(device-independent pixels (dips))的比例,公式表示就是:devicePixelRatio = 物理像素 / dips。如iPhone 4s, 纵向显示的时候,屏幕物理像素640像素。当用户设置的时候,其视区宽度并不是640像素,而是320像素。这样,在视网膜屏幕的iPhone上,屏幕物理像素640像素,独立像素是320像素,因此,window.devicePixelRatio等于2,1px在iPhone 4s就占了2个像素。

如果在Retina屏幕上显示1个物理像素的边框线条,方法有很多种:包括设置border-width: 0.5px、border-image、动态viewport,这些方法有兼容、不支持圆角等问题,不够方便。通过个人实践,目前兼容较好且副作用较少的方式是通过:伪对象 + transform 缩放。

原理:在 Retina 屏幕上,去掉容器的原有 border,利用 :before 或 :after 制造容器尺寸 2 倍或 3 倍的绝对定位伪对象,使用 1px 的 border 定义伪对象边框后,用 transform 的 scale 把伪对象缩小到一半或 1/3,这样看上去伪对象就和容器一样大了,border也相应缩小到单个物理像素。

具体实现方式及主要实现代码如下:

/* 在头部获取设置devicePixelRatio */
!function(win) {
	var doc = win.document;
	var dpr = win.devicePixelRatio;
	if (dpr > 2) {
		dpr = 3
	} else {
		if (dpr > 1) {
			dpr = 2
		} else {
			dpr = 1
		}
	}
}(window);
/*根据dpr缩放border*/
@mixin dpr-border($class, $color, $radius:0, $position:all) {
%border {
  @if $position == 'all' {
    border: 1px solid $color;
  } @else if $position == 'right' {
    border-right: 1px solid $color;
  } @else if $position == 'left' {
    border-left: 1px solid $color;
  } @else if $position == 'top' {
    border-top: 1px solid $color;
  } @else if $position == 'bottom' {
    border-bottom: 1px solid $color;
  }
}
.#{$class} {
  @extend %border;
  border-radius: $radius;
  position: relative;
}
$dpr: (2, 3);
@each $value in $dpr {
  [data-dpr^='#{$value}'] .#{$class} {
    border: none;
    $rValue: 1/$value;
    &:before{
   content: ' ';
   position: absolute;
   left: 0;
   top: 0;
   width: 100%*$value;
   height: 100%*$value;
   @extend %border;
   border-radius: $radius;
   -webkit-transform: scale($rValue) translate(-50% * ($value - 1),-50% * ($value - 1));
    transform: scale($rValue) translate(-50% * ($value - 1),-50% * ($value - 1));
  }
}
}
}
/* 引用方法,设border */
@import '../base/mixins';
@import '../base/function';
.border {
  width: 100px;
  height: 100px;
  margin: 20px auto;
  border: 1px solid #999;
}
.border2,.border3 {
  margin: 20px auto;
  width: 100px;
  height: 100px;
}
@include dpr-border(border2,#999);
@include dpr-border(border3,#999,20px);

最终效果:点击查看
二维码扫一扫:

1border

通过 Sublime Snippet 定制自己的代码片段

在编写代码的时候,总会遇到一些需要反复使用的代码片段。这时候就需要反复的复制和黏贴,大大影响效率。我们利用Sublime Text的snippet功能,就能很好的解决这一问题。

创建方法:Tools > New Snippet,可以看到新建的文件格式:

<snippet>
<content><![CDATA[
Hello, ${1:this} is a ${2:snippet}.
]]></content>
<!– Optional: Set a tabTrigger to define how to trigger the snippet –>
<!– <tabTrigger>hello</tabTrigger> –>
<!– Optional: Set a scope to limit where the snippet will trigger –>
<!– <scope>source.python</scope> –>
</snippet>

简要介绍一下snippet四个组成部分:

content:其中必须包含<![CDATA[…]]>,否则无法工作, Type your snippet here用来写你自己的代码片段
tabTrigger:用来引发代码片段的字符或者字符串, 比如在以上例子上, 在编辑窗口输入hello然后按下tab就会在编辑器输出Type your snippet here这段代码片段

scope: 表示你的代码片段会在那种语言环境下激活, 比如上面代码定义了source.python, 意思是这段代码片段会在python语言环境下激活.

description :展示代码片段的描述, 如果不写的话, 默认使用代码片段的文件名作为描述

${1:name}表示代码插入后,光标所停留的位置,可同时插入多个。其中:name为自定义参数(可选)。

${2}表示代码插入后,按Tab键,光标会根据顺序跳转到相应位置(以此类推)。

创建完毕以后,保存在\Packages\User目录下,文件后缀名为:.sublime-snippet。

附上 list of scopes:

ActionScript: source.actionscript.2
AppleScript: source.applescript
ASP: source.asp
Batch FIle: source.dosbatch
C#: source.cs
C++: source.c++
Clojure: source.clojure
CoffeeScript: source.coffee
CSS: source.css
D: source.d
Diff: source.diff
Erlang: source.erlang
Go: source.go
GraphViz: source.dot
Groovy: source.groovy
Haskell: source.haskell
HTML: text.html(.basic)
JSP: text.html.jsp
Java: source.java
Java Properties: source.java-props
Java Doc: text.html.javadoc
JSON: source.json
Javascript: source.js
BibTex: source.bibtex
Latex Log: text.log.latex
Latex Memoir: text.tex.latex.memoir
Latex: text.tex.latex
LESS: source.css.less
TeX: text.tex
Lisp: source.lisp
Lua: source.lua
MakeFile: source.makefile
Markdown: text.html.markdown
Multi Markdown: text.html.markdown.multimarkdown
Matlab: source.matlab
Objective-C: source.objc
Objective-C++: source.objc++
OCaml campl4: source.camlp4.ocaml
OCaml: source.ocaml
OCamllex: source.ocamllex
Perl: source.perl
PHP: source.php
Regular Expression(python): source.regexp.python
Python: source.python
R Console: source.r-console
R: source.r
Ruby on Rails: source.ruby.rails
Ruby HAML: text.haml
SQL(Ruby): source.sql.ruby
Regular Expression: source.regexp
RestructuredText: text.restructuredtext
Ruby: source.ruby
SASS: source.sass
Scala: source.scala
Shell Script: source.shell
SQL: source.sql
Stylus: source.stylus
TCL: source.tcl
HTML(TCL): text.html.tcl
Plain text: text.plain
Textile: text.html.textile
XML: text.xml
XSL: text.xml.xsl
YAML: source.yaml

相关资料:

http://docs.sublimetext.info/en/latest/extensibility/snippets.html

http://www.imooc.com/video/6402

在Dcloud 下开发 iOS App 的上架 App Store 流程

在Dcloud开发app也有一段时间了,针对iOS App,一直是使用企业账号进行打包,并上传到fir.im进行分发测试。最近申请了公司账号,打包后不能再使用企业账号的in-house方式打包了,只能使用ad-hoc方式(需要为每个测试的设备添加UDID,并且最多支持100台设备)。

下面来说说打包发布 App Store 的过程,首先,按照官方提供的教程申请发布(Distribution)证书和描述文件,注意是发布的证书,不是开发的,完成后,在HBuilder中采用刚才申请的发布证书和描述文件进行打包。

接着下载ApplicationLoader,安装后,通过公司账号登录,并上传刚才打包的app,在iTunes Connect会使用到。

登录iTunes Connect,点击进入My App ,创建新的App,并填写相关的资料,在构建版本选择刚才在ApplicationLoader上传的app。App 预览和屏幕快照的图片,可以通过iOS模拟器截图,快捷键Command + s。完成后,点击提交审核即可。