前言
1. Canvas开发经验
1.1 项目中的使用场景
手写签名
- 一片小的区域用来放置写好的签名(预览效果)和提交按钮
- 点击外部盒子激活
page-container组件(自动铺满全屏)- 在这个满屏的区域里进行绘制
1.2 实现思路
- 触摸开始、触摸移动、触摸结束。这三个事件的触发都有一个事件对象
TouchEvent。常用属性如下:- pageX
- pageY
- x
- y
- x 、y 都分别代表了目标元素的坐标
- 触摸开始:
- 第一次记录坐标,开始绘制路径;
this.canvasContext.beginPath();
- 第一次记录坐标,开始绘制路径;
- 触摸移动
- 第二次记录位置,将移动的距离赋值给
moveLength,以免用户不规范签字 - 把第一个记录的位置作为参数调用
moveTo()方法,创建路径起点 - 把第二个记录的位置作为参数调用
lineTo()方法,最后路径终点 - 最后在调用
stroke()方法,绘制路径 - 如果有多次绘制,依次循环调用,把结束的坐标作为第二次的起始坐标
- 第二次记录位置,将移动的距离赋值给
- 触摸结束
- 保存当前路径,调用
save()方法
- 保存当前路径,调用
1.3 源码
<view class="image-container" @click="cli" v-if="dealUrl">
<image mode="aspectFit" :src="sign" />
<text v-if="!sign" class="text-center">请在此处签名</text>
</view>
<page-container :show="showPage" :overlay="false" :round="false" :close-on-slideDown="false" z-index="1000">
<view class="flex" style="height: 100vh;">
<view class="sign-btn-container">
<view class="cu-btn" @click="cli">取消/返回</view>
<view class="cu-btn" :class="isSignatureFinish?'bg-blue':''" @click="clearCanvas">重新签名</view>
<view class="cu-btn" :class="isSignatureFinish?'bg-blue':''" @click="confirmSign">完成签名</view>
</view>
<canvas
id="canvas"
:style="showPage?'position:relative;left: ':'position:fixed ; left:100%'"
canvas-id="canvas"
class="canvas"
:disable-scroll="true"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
@touchcancel="handleTouchCancel"
/>
</view>
</page-container>
data(){
return{
sign: null, // 存放写好签名的图片
isSignatureFinish: false, // 是否有效绘制
moveLength: 0, //移动一定距离后才能提交保存
showPage: false, // page-container组件是否激活
canvasTempWidth: uni.getSystemInfoSync().screenWidth,
canvasTempHeight: uni.getSystemInfoSync().screenWidth * 0.5,
img_1:'', //身份证正面照片
}
},
onShow() {
//第一个canvas 创建画布
this.canvasContext = uni.createCanvasContext('canvas');
// 画布初始化 设置线条颜色/粗细/端点结束样式
this.initCanvas()
//第二个canvas
this.ctx2 = uni.createCanvasContext('canvas-temp');
},
methods:{
// 封装上传到oss列表方法
async uploadOss(data,type){
const res = await payRequest({
path:'/api/yishui/img/upload',
method:'POST',
data:{
type
}
},1)
const {host , OSSAccessKeyId:ossAccessKeyId, signature,key,policy} = res[1].data
uni.uploadFile({
url: host,
fileType: 'image',
// #ifdef MP-ALIPAY
fileName: 'file',
// #endif
// #ifdef MP-WEIXIN
name: 'file',
// #endif
filePath:data,
formData: {
key,
policy,
OSSAccessKeyId: ossAccessKeyId,
signature,
success_action_status: '200',
},
success: (res) => {
// 将上传成功状态码设置为200,默认状态码为204。
if (res.statusCode === 200) {
console.log('上传成功');
}
uni.showToast({ title: '上传成功!'});
data = `${host}/${key}`// 回填数据 让图片回填到页面
console.log(data,this.img_1, 'data,');
},
fail: err => {
console.log(err,'err上传失败了');
}
})
},
// 提交签名
async submitSignature(){
if(this.sign){
this.uploadOss(this.sign,'signImg')
}else{
uni.showToast({
icon:'none',
title: '请先完成电子签名,再提交保存',
})
}
},
// 打开/关闭电子签名区域
cli(){
this.showPage = !this.showPage
console.log(this.showPage);
},
drawBackground (ctx, color = 'white', width = 1000, height = 3000, x = 0, y = 0) {
ctx.rect(x, y, width, height)
ctx.setFillStyle(color)
ctx.fill()
},
// 完成签名
confirmSign() {
let _this = this
_this.generateSignImage('canvas').then(res => {
console.log('图片地址:---------', res)
uni.getImageInfo({
src: res,
success(info) {
let width = info.width
let height = info.height
//先重置canvas-temp
_this.ctx2.draw(false, ()=>{
let dx = _this.canvasTempWidth
let dy = _this.canvasTempHeight
//把原点移动到中心点位置
_this.ctx2.translate(dx / 2, dy / 2)
_this.ctx2.rotate(270 * Math.PI / 180)
let dWidth = dx / height * width
let dHeight = dx
//drawImage参数说明
//imageResource
//imageResource的左上角在目标 canvas 上 x 轴的位置
//imageResource的左上角在目标 canvas 上 y 轴的位置
//在目标画布上绘制 imageResource 的宽度,允许对绘制的 imageResource 进行缩放
//在目标画布上绘制 imageResource 的高度,允许对绘制的 imageResource 进行缩放
_this.ctx2.drawImage(res, -dWidth / 2,-dHeight / 2, dWidth, dHeight)
//canvas-temp图片绘制完成后
_this.ctx2.draw(false, ()=>{
_this.generateSignImage('canvas-temp').then(res => {
console.log('图片地址:!!!!!!!', res)
_this.sign= res
_this.showPage = false
})
})
})
}
})
})
},
initCanvas() {
/* 设置线条颜色 */
this.canvasContext.setStrokeStyle('#0081ff'); //2A2A2A
/* 设置线条粗细 */
this.canvasContext.setLineWidth(2);
/* 设置线条的结束端点样式 */
this.canvasContext.setLineCap('round');
},
/* 触摸开始 */
handleTouchStart(e) {
this.drawStartX = e.changedTouches[0].x;
this.drawStartY = e.changedTouches[0].y;
this.canvasContext.beginPath();
},
/* 触摸移动 */
handleTouchMove(e) {
/* 记录当前位置 */
const tempX = e.changedTouches[0].x;
const tempY = e.changedTouches[0].y;
this.moveLength += Math.abs(this.drawStartX - tempX) + Math.abs(this.drawStartY - tempY)
/* 画线 */
this.canvasContext.moveTo(this.drawStartX, this.drawStartY);
this.canvasContext.lineTo(tempX, tempY);
this.canvasContext.stroke();
/* 旧版draw方法,新版本不需要draw */
this.canvasContext.draw(true);
/* 重新记录起始位置 */
this.drawStartX = tempX;
this.drawStartY = tempY;
},
/* 触摸结束 */
handleTouchEnd(e) {
console.log('触摸结束')
this.canvasContext.save();
if(this.moveLength > 100) this.isSignatureFinish = true
},
/* 触摸取消 */
handleTouchCancel(e) {
console.log('触摸取消')
this.canvasContext.save();
},
/* 清空画布 */
clearCanvas() {
console.log('清空画布')
this.canvasContext.draw()
this.initCanvas()
this.moveLength = 0
this.isSignatureFinish= false
},
/* 生成签名图片 */
generateSignImage(canvasId) {
console.log('Canvas生成图片')
return new Promise((resolve, reject) => {
uni.canvasToTempFilePath({
x: 0,
y: 0,
canvasId: canvasId, // 旧版使用id
fileType: 'png',
quality: 1,
success: res => {
resolve(res.tempFilePath)
},
fail: err => {
reject(err);
}
})
})
},
}
1.4 介绍基本接口
a.渲染上下文
- getContext('2d')
- getContext('webgl')
<canvas id="canvas" width="800" height="800" style="background-color: #c1c1
c1;margin:20px">
</canvas>
...
const c = document.getElementById("canvas");
const ctx = c.getContext("2d");
b.绘制图形(通过坐标系)
- 线
- 绘制:
moveTo:路径起点lineTo:路径终点stroke:绘制路径beginPath:起始一条路径,或重置当前路径closePath:创建从当前点回到起始点的路径(用于闭合路径)
- 设置样式:
lineWidth:设置或返回当前的线条宽度strokeStyle:设置或返回用于笔触的颜色、渐变或模式
- 绘制:
- 弧线(弧、圆弧/圆):
arcTo(x1, y1, x2, y2, radius):创建两切线之间的弧/曲线。- 参数:
- 弧的起点的 x 坐标;
- 弧的起点的 y 坐标;
- 弧的终点的 x 坐标;
- 弧的终点的 y 坐标;
- 弧的半径
arc(x,y,r,sAngle,eAngle,counterclockwise):用于创建圆或弧- 参数:
- 圆的中心的 x 坐标。
- 圆的中心的 y 坐标。
- 圆的半径。
- 起始角,以弧度计。(弧的圆形的三点钟位置是 0 度)。
- 结束角,以弧度计。
- 可选。规定应该逆时针还是顺时针绘图。False = 顺时针,true = 逆时针。
- 绘图样式
- 线条
lineWidth:设置或返回当前的线条宽度lineCap:设置或返回线条的结束端点样式round" 和 "square" 会使线条略微变长
- 渐变
createLinearGradient():创建线性渐变(用在画布内容上)createRadialGradient():创建放射状/环形的渐变(用在画布内容上)createPattern(image,"repeat|repeat-x|repeat-y|no-repeat"):在指定的方向内重复指定的元素(纹理)- 规定要使用的图片、画布或视频元素。
- 默认。该模式在水平和垂直方向重复。
- 线条
- 绘制文本
- 绘制方式:
- 描边(线条)
- 填充
fillText()
- 绘制样式:
- font、textAlign、direction、textBaseline
- 阴影:shadowOffsetX 和 shadowOffsetY、shadowBlur、shadowColor
- 绘制方式:
- 绘制图片
drawImage()⽤法:- 方法在画布上绘制图像、画布或视频。
- 也能够绘制图像的某些部分,以及/或者增加或减少图像的尺寸。
drawImage(img,x,y,width,height):在画布上定位图像,并规定图像的宽度和高度:drawImage(img,sx,sy,swidth,sheight,x,y,width,height)剪切图像,并在画布上定位被剪切的部分:sx可选。开始剪切的 x 坐标位置。sy可选。开始剪切的 y 坐标位置。swidth可选。被剪切图像的宽度。sheight可选。被剪切图像的高度。
2. WebSocket
WebSocket是什么
websocket是用来实现客户端和服务器之间数据通信的一种手段;即浏览器和服务器只需要建议一次连接,两者之间就可以实现双向数据传输
问题:已经有了HTTP协议,为什么还需要WebSocket? HTTP协议的缺陷: 通信只能是客户端发起, 不具备服务器推送功能,也就是说服务器不能主动向客户端推送消息. 这种单向通信方式,需要使用[轮询]查询方式,每隔一段时间就发出一次询问,了解服务器有没有新的消息.这种方式效率很低,浪费资源。
WebSocket使用
1. 建立连接
- 使用
WebSocket,通过构造函数实例化
// 构造一个 webSocket 对象
const socket = new WebSocket('ws://localhost:8080');
// const socket = new WebSocket('wss://localhost:8080');
ws.onopen = function(evt) {
console.log("Connection open ...");
ws.send("Hello WebSockets!");
};
ws.onmessage = function(evt) {
console.log( "Received Message: " + evt.data);
};
ws.onclose = function(evt) {
console.log("Connection closed.");
};
- 实例化对象包含的属性
每个属性的含义:
- binaryType:使用二进制的数据类型连接;
- bufferedAmount(只读):未发送至服务器的字节数;
- extensions(只读):服务器选择的扩展;
- onclose:用于指定连接关闭后的回调函数;
- onerror:用于指定连接失败后的回调函数;
- onmessage:用于指定当从服务器接受到信息时的回调函数;
- onopen:用于指定连接成功后的回调函数;
- protocol(只读):用于返回服务器端选中的子协议的名字;
- readyState(只读):返回当前 WebSocket 的连接状态,共有 4 种状态:
- CONNECTING — 正在连接中,对应的值为 0;
- OPEN — 已经连接并且可以通讯,对应的值为 1;
- CLOSING — 连接正在关闭,对应的值为 2;
- CLOSED — 连接已关闭或者没有连接成功,对应的值为 3;
- url(只读):返回值为当构造函数创建 WebSocket 实例对象时 URL 的绝对路径;
WebSocket方法:
- send(data):通过 WebSocket 连接传输至服务器的数据队列,并根据所需要传输的数据的大小来增加 bufferedAmount 的值;
- close():关闭 WebSocket 连接,如果连接已经关闭,则此方法不执行任何操作;
WebSocket事件:
- close:当一个 WebSocket 连接被关闭时触发,也可以通过 onclose 属性来设置;
- error:当一个 WebSocket 连接因错误而关闭时触发,也可以通过 onerror 属性来设置;
- message:当通过 WebSocket 收到数据时触发,也可以通过 onmessage 属性来设置;
- open:当一个 WebSocket 连接成功时触发,也可以通过 onopen 属性来设置;
2. 项目中使用WebSocket
<script>
export default {
data() {
return {
socket: null,
aliveTime: new Date().getTime(),
checkTimer: null
}
},
computed: {
token() {
return this.$store.getters.token
}
},
beforeDestroy() {
clearInterval(this.checkTimer)
this.socket && this.socket.close()
},
mounted() {
if (this.socket && this.socket.readyState === 1) {
clearInterval(this.checkTimer)
this.socket.close()
}
if (this.socket && this.socket.readyState === 3) {
this.initWebSocket()
}
this.getData()
},
methods: {
getData() {
// ......
this.initWebSocket()
},
initWebSocket() {
if (typeof WebSocket === 'undefined') {
this.$message({ message: '您的浏览器不支持WebSocket' })
return false
}
this.checkTimer && clearInterval(this.checkTimer)
this.socket && this.socket.close()
this.aliveTime = new Date().getTime()
const token = this.token.split('Bearer ')[1]
const wsurl = `wss://${process.env.VUE_APP_DOMAIN}/ws?token=${token}`
this.socket = new WebSocket(wsurl)
this.socket.onmessage = this.websocketonmessage
this.socket.onerror = this.websocketonerror
this.checkTimer = setInterval(this.checkWebsocketAlive, 5 * 1000)
},
websocketonmessage(e) {
const response = JSON.parse(e.data)
if (response.message === 'success') {
const data = response.data
// 处理 data
}
// 这里的场景是服务端主动推数据,接收到消息说明连接正常
if (response.message === 'connection alive') {
this.aliveTime = new Date().getTime()
}
},
websocketonerror() {
clearInterval(this.checkTimer)
this.socket.close()
},
checkWebsocketAlive() {
const now = new Date().getTime()
if (now - this.aliveTime > 60 * 1000) {
this.aliveTime = now
this.initWebSocket()
}
},
}
}
</script>
3. 应用场景
- 即时聊天通信
- 多玩家游戏
- 在线协同编辑/编辑
- 实时数据流的拉取与推送
- 体育/游戏实况
- 实时地图位置
解释:
-
即时
Web应用程序:即时Web应用程序使用一个Web套接字在客户端显示数据,这些数据由后端服务器连续发送。在WebSocket中,数据被连续推送/传输到已经打开的同一连接中,这就是为什么WebSocket更快并提高了应用程序性能的原因。 例如在交易网站或比特币交易中,这是最不稳定的事情,它用于显示价格波动,数据被后端服务器使用Web套接字通道连续推送到客户端。 -
游戏应用程序:在游戏应用程序中,你可能会注意到,服务器会持续接收数据,而不会刷新用户界面。屏幕上的用户界面会自动刷新,而且不需要建立新的连接,因此在
WebSocket游戏应用程序中非常有帮助。 -
聊天应用程序:聊天应用程序仅使用
WebSocket建立一次连接,便能在订阅户之间交换,发布和广播消息。它重复使用相同的WebSocket连接,用于发送和接收消息以及一对一的消息传输。