Canvas和websocket总结

384 阅读9分钟

前言

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);
                    }
                })
            })
        },
    }

H5-Canvas文档

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.");
};   
  • 实例化对象包含的属性 cb82a86afdd5461da8a264dee3b24d0b~tplv-k3u1fbpfcp-zoom-in-crop-mark_1512_0_0_0.webp

每个属性的含义:

  • 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连接,用于发送和接收消息以及一对一的消息传输。