十七岁呀十七岁

Author Avatar
mxx 9月 02, 2017
  • 在其它设备中阅读本文章

文字打字机

Typewriter

打字机原理是开启定时器一个字一个字的将其展示出来

  let code = document.querySelector('#code')
  let cont = code.innerHTML
  let len = 0
  let timer = setInterval(() => {
    code.innerHTML = cont.substring(0, len) + '_'
    len ++
    if (len > cont.length) {
      clearInterval(timer)
    }
  }, 75)

当全部的内容都显示出来之后 要将定时器清除掉

打字机效果demo在线演示

图片上传

图片上传可以借助 input 元素 获取图片信息

<input type='file' />

通过监听元素的 change 事件 [当选择的元素发生变动] 可以获取上传的文件信息

let input = document.querySelector('input')
input.onchange = () => {
  console.log(input.files[0])
}

mobile

图片预览

当获取到图片文件信息之后 可以进行预览操作

File文件上传之后 在函数中得到的是File格式的内容 如果想要将其以图片格式展现 可以将其转换化为可预览的格式

FileReader

FileReader 允许Web应用程序异步读取存储在用户计算机上的文件

let reader = new FileReader()
  • readAsDataURL(file)

此方法 将图片资源转为数据URL的形式保存在result中

  • window.URL

此方法不必把文件内容读取到JavaScript中 而是直接使用文件内容

但是在不同浏览器中 是有兼容性问题的 因此可以使用此函数

function creatObjUrl (blob) {
  if (window.URL) {
    return window.URL.createObjectURL(blob)
  } else if (window.webkitURL) {
    return window.webkitURL.createObjectURL(blob)
  } else {
    return null
  }
}

此时得到的其实是一个 Blob 类型的对象

mobile

但是需要注意的是 这种方式是占用内存的 所以当使用结束 需要手动进行内存的释放

function revokeObjectUrl (url) {
  if (window.URL) {
    window.URL.revokeObjectURL(url)
  } else {
    window.webkitURL.revokeObjectURL(url)
  }
}

或者也可以等待页面关闭自己会进行内存释放

关于图片上传以及预览的demo

格式转化

文件格式可以是 base64 Blob File 这几种 这里的file 指的是 form表单提交的时候的File对象

canvas 直接获得 Blob

canvas.toBlob((blob) => {
  console.log(blob)
})

canvas.toBlob(callback, type, encoderOptions)

参数 说明
callback 回到函数 参数为Blob
type 图片格式,默认格式为image/png
encoderOptions 指定图片展示质量 0-1

创造Blob对象,用以展示canvas上的图片;这个图片文件可以被缓存或保存到本地,由用户代理端自行决定。如不特别指明,图片的类型默认为 image/png,分辨率为96dpi

浏览器支持情况不好

mobile
mobile

base64 转化为 Blob

function convertBase64UrlToBlob(urlData){
  //去掉url的头,并转换为byte
  var bytes = window.atob(urlData.split(',')[1])   
  //处理异常,将ascii码小于0的转换为大于0
  var ab = new ArrayBuffer(bytes.length)
  var ia = new Uint8Array(ab)
  for (var i = 0; i < bytes.length; i++) {
    ia[i] = bytes.charCodeAt(i)
  }
  return new Blob( [ab] , {type : 'image/png'})
}

Blob 转为 file 格式

Blob转为 file格式 是模拟了一个form表单的提交过程

let form = new FormData()
formData.append('photo', imgBlob)
$.ajax({
  url: 'upload',
  type: 'POST',
  data: formData,
  processData: false,  // 不处理数据
  contentType: false   // 不设置内容类型
}).done(() => {
  console.log('success')
}).fail(() => {
  console.log('error')
})

截屏处理

截屏处理的部分使用了第三方插件 以下两个第三方插件无法在静态页面实现截屏效果

html2canvas(ele, {})

Github

参数 说明
ele 裁剪区域目标元素
onrendered 回调函数 参数是截取图片之后的canvas对象
useCORS 图片跨域
width 想要截取的 canvas 图片宽度
height 想要截取的 canvas 图片高度
html2canvas(document.getElementById('view'), {
  onrendered: (canvas) => {
    image = canvas.toDataURL('image/png')
  }
})

这函数中的参数 是一个canvas对象 所以可以借助canvas提供的方法将其转化为可以被预览的图片格式

canvas.toDataURL('image/png')
// 或者
canvas.toBlob((blob) => {

})

此处有坑 canvas.toBlob 无法在iOS中实现

  • 图片跨域

如果截屏区域的图片涉及到跨域问题 则会发现截屏之后 没有该图片

使用一张不同域名的盛世美颜来测试关于图片跨域的问题 当点击截屏按钮会发现并没有将此图片截取到

clip

因此添加一个属性 useCORS 可以用来处理此问题

useCORS: true

clip

  • 使用html2canvas 发生截屏图片模糊问题

使用html2canvas 会发现截屏图片模糊 这是由于像素点的渲染图片的问题

浏览器window中的devicePixelRatio属性决定了浏览器会用几个像素点来渲染一个像素

假设devicePixelRatio=2,在retina屏下, 会用2个像素点的宽度去渲染canvas的1个像素点 ,该canvas在retina屏幕上实际占据的宽高放大了一倍,因此图片会变得模糊

解决原理: 放大canvas的坐标系,然后缩小其显示的宽高

处理方案

dom-to-image

Github

domtoimage.toPng(document.getElementById('view'))
  .then((dataUrl) => {
    console.log(dataUrl)
  })
  .catch(function (error) {
    console.log(error)
  })

dom-to-image 支持 promise 方式使用 非常简单 而且提供了很多功能

此方式返回值 dataUrl 是一个base64格式的字符串 可以直接放入图片的src属性 进行图片预览

然而有坑 Safari is not supported

  • 图片下载

借助第三方插件 FileSaver 实现

FileSaver

let FileSaver = require('file-saver')

domtoimage.toBlob(document.getElementById('view'))
  .then(function (blob) {
      FileSaver.saveAs(blob, 'my-node.png');
  })

其实实现图片下载也可以通过模拟 a 标签的点击

let link = document.createElement('a')
link.download = 'image-name.jpeg'
link.href = dataUrl
link.click()

但是在这种方式是移动端不可以的

关于实现截屏效果以及下载的demo

react-hammer

react-hammer是一个帮助实现移动端拖拽效果的插件

npm

Github

其实React-hammer是基于hammer.js 构建而成的一个更适合在React中使用的触摸插件

使用

let Hammer = require('react-hammerjs')

<Hammer onTap={handleTap} options={options}><div>Tap Me</div></Hammer>

六种手势支持

react-hammer.js 支持六种手势操作

事件 手势
pan 单手指滑动
tap 单手指点触
doubleTap 单手指双击
pinch 两个手指进行缩放动作
press 单手指下压
rotate 双手指旋转
swipe 单手指滑动

默认状态下 是无法进行pinch 与 rotate 操作的 需要手动将其设置为TRUE

As a default, the pinch and rotate events are disabled in hammer.js, as they would make actions on an element “blocking”.

let options = {
  recognizers: {
    pinch: { enable: true },
    rotate: { enable: true }
  }
}

<Hammer options={options} >
  <p>{text}</p>
</Hammer>

这样才能进行缩放以及旋转动作

事件对象

每一个事件的回调函数中都有一个事件对象 包含以下属性 (展示部分常用属性)

事件对象 含义
type 事件类型
deltaX X轴方向移动
deltaY Y轴方向移动
distance 移动距离
direction 移动的方向
srcEvent 事件来源
rotation 多点触摸时已经完成的旋转(deg)
eventType 事件类型,匹配INPUT常量

pan 单手滑动

事件 含义
pan 单手指滑动 整个滑动周期
panstart 开始滑动
panmove 滑动
panend 滑动结束
pancancel 滑动取消

默认只能进行水平方向的滑动 无法处理垂直方向的滑动 因此需要手动添加属性 direction

npm 中这样说道

If you provide the prop direction the pan and swipe events will support Hammer.DIRECTION_(NONE/LEFT/RIGHT/UP/DOWN/HORIZONTAL/VERTICAL/ALL).

<Hammer direction='DIRECTION_ALL'>
 ...
</Hammer>

关于 pancancel

个人感觉应该是在快速拖动导致手势丢失的时候 会触发此事件发生 然而测试中并没有发现有什么用 因为一直会被 panend 被捕捉而不是 pancancel

拖拽

此函数会一直被执行 从整个滑动开始 正在滑动 滑动结束

元素跟随手指滑动变动位置的原理是 获取元素初始位置 + 手指滑动距离 然后在手指抬起瞬间 更新元素初始位置

let eleLeft = startX + deltaX

结束手指滑动时候 更新元素位置

之前一直是在 Panend 中处理这个问题 但是发现实际页面拿到的left数据 一旦发生一次滑动结束 再次滑动的时候 里面的值就是undefined

调试很久之后 将事件处理到 pan 事件中

通过监听事件对象的 eventType === 4 来判断词此时是要结束滑动 然后更新元素初始位置

有一个坑

当手指离开屏幕 也就是结束滑动的时候

// 不触发方向运动 也就是手指离开的时候
if (direction === 1) {
  end = translateX
  topEnd = translateY
}

不属于任何一个判断条件 但是此时也要处理 左 上 的值 否则也会发现下次出现undefined

建议不要使用定位变动元素位置

最开始通过position将元素进行定位 在滑动过程中更改left top 坐标点 但是这种处理方式中 元素滑动并不流畅 体验性很差

可以通过transform来移动元素

transform: `translateX(${translateX}px) translateY(${translateY}px)`,

缩放

事件 含义
pinch 两个手指缩放 整个缩放周期
pinchstart 开始滑动
pinchIn 缩小
pinchOut 放大
PinchEnd 缩放取消
PinchCancel 滑动取消

缩放监听的是两个手指捏合的动作

元素缩小

pinchIn () {
  let width = this.state.width - 1
  let height = this.state.height - 1
}

元素放大

pinchOut () {
  let width = this.state.width + 1
  let height = this.state.height + 1
}

由于手指捏合速度很快 所以这里直接将元素进行宽度与长度的增减

最好不要将元素大小变化的处理放入 onPinch 事件 防止手势丢失导致元素一直放大或者缩小

旋转 rotate

hammer.js官网中提供的 旋转案例 中 通过判断两个手指之间的角度差 来更改元素旋转角

onRotate (ev) {
  transform.angle = initAngle + ev.rotation
}

但是这样是有一个问题如果最开始两个手指之间不在同一个水平线上 同样检测到两个手指之间的差值 元素会马上发生转动

所以这个属性不能准确读取到手指旋转角度

测试中发现 函数中事件对象 ev 有一个 srcEvent 参数 它也是一个对象 里面也有一个 rotation 属性 这个真正的表示了每一次手指旋转的角度值

此处有坑 只在iOS中有此属性

所以更改为使用此属性值来进行元素的旋转效果

handleRotate (ev) {
  let {startRotate} = this.state
  let {deltaTime, srcEvent} = ev
  // 点触事件  时间太短不去触发旋事件
  if (deltaTime < 100) return

  let {rotation = ''} = srcEvent || {} // 每次手指旋转的角度
  let end
  if (rotation) {
    // 如果存在则表示是在iOS中 rotation 从srcEvent 中获取 表示每一次旋转的角度
    end = startRotate + rotation
  }
}

按压 press

事件 含义
press 按压周期
pressUp 按压结束 手指抬起
  • 旋转的补充方案

由于在安卓机中没有拿到每一次旋转角度 因此使用plan B 通过添加两个按钮 当点击按钮或者长按按钮 可以进行图片的旋转

按压过程 通过开启定时器 来不断旋转目标元素

timer = setInterval(() => {
  old--
  this.setState({rotateArg: old})
}, 25)

在按压结束的时候 清除定期器

handlePressUp () {
  timer = window.clearInterval(timer)
}

bug 处理 手势并发 导致不停旋转

后来发现 如果在点击旋转按钮的时候 同时触发了别的手势 比如滑动 那么一旦手指离开屏幕 会发现元素会一直转个不停

这是因为定时器的清除操作只写在了 PressUp 这个手势中 而多手势的时候 有时候会读不到此事件

修正措施: 在每一种手势结束的时候 都清除一次定时器 比如 tapEnd 、 pinchEnd等