|
|
|
|
公众号矩阵

实现Web端自定义截屏(原生JS版)

前几天我发布了一个web端自定义截图的插件,在使用过程中有开发者反馈这个插件无法在vue2项目中使用,于是,我就开始找问题,发现我的插件是基于Vue3的开发的,由于Vue3的插件和Vue2的插件完全不兼容,因此插件也就只能在Vue3项目中使用。

作者: 神奇的程序员K 来源:神奇的程序员k|2021-02-11 13:56

本文转载自微信公众号「神奇的程序员K」,作者神奇的程序员K。转载本文请联系神奇的程序员K公众号。

前言

前几天我发布了一个web端自定义截图的插件,在使用过程中有开发者反馈这个插件无法在vue2项目中使用,于是,我就开始找问题,发现我的插件是基于Vue3的开发的,由于Vue3的插件和Vue2的插件完全不兼容,因此插件也就只能在Vue3项目中使用。

经过一番考虑后,我决定用原生js来重构这个插件,让其不依赖任何库,这样它就能运行在任意一台支持js的设备上,本文就跟大家分享下我重构这个插件的过程,欢迎各位感兴趣的开发者阅读本文。

运行结果视频:(请看原文)

使用Vue实现Web端的自定义截屏,效果如视频所示,文章,教程,体验地址明天和大家分享[坏笑] #Vue #截屏 #自定义截屏 #Web前端

写在前面

本文不讲解插件的具体实现思路,对插件实现思路感兴趣的开发者请移步:实现Web端自定义截屏

搭建开发环境

我想使用ts、scss、eslint、prettier来提升插件的可维护性,又嫌麻烦,不想手动配置webpack环境,于是我决定使用Vue CLI来搭建插件开发环境。

本文不细讲Vue CLI搭建插件开发环境的过程,对此感兴趣的开发者请移步:使用CLI开发一个Vue3的npm库。

移除vue相关依赖

我们搭建好插件的开发环境后,CLI默认会在package.json中添加Vue的相关包,我们的插件不会依赖于vue,因此我们把它删除即可。

  1. "vue""^3.0.0-0"
  2. "vue-class-component""^8.0.0-0" 

创建DOM

为了方便开发者使用dom,这里选择使用js动态来创建dom,最后将其挂载到body中,在vue3版本的截图插件中,我们可以使用vue组件来辅助我们,这里我们就要基于组件来使用js来创建对应的dom,为其绑定对应的事件。

部分实现代码如下,完整代码请移步:CreateDom.ts

  1. import toolbar from "@/lib/config/Toolbar"
  2. import { toolbarType } from "@/lib/type/ComponentType"
  3. import { toolClickEvent } from "@/lib/split-methods/ToolClickEvent"
  4. import { setBrushSize } from "@/lib/common-methords/SetBrushSize"
  5. import { selectColor } from "@/lib/common-methords/SelectColor"
  6. import { getColor } from "@/lib/common-methords/GetColor"
  7.  
  8. export default class CreateDom { 
  9.   // 截图区域canvas容器 
  10.   private readonly screenShortController: HTMLCanvasElement; 
  11.   // 截图工具栏容器 
  12.   private readonly toolController: HTMLDivElement; 
  13.   // 绘制选项顶部ico容器 
  14.   private readonly optionIcoController: HTMLDivElement; 
  15.   // 画笔绘制选项容器 
  16.   private readonly optionController: HTMLDivElement; 
  17.   // 文字工具输入容器 
  18.   private readonly textInputController: HTMLDivElement; 
  19.  
  20.   // 截图工具栏图标 
  21.   private readonly toolbar: Array<toolbarType>; 
  22.    
  23.     constructor() { 
  24.     this.screenShortController = document.createElement("canvas"); 
  25.     this.toolController = document.createElement("div"); 
  26.     this.optionIcoController = document.createElement("div"); 
  27.     this.optionController = document.createElement("div"); 
  28.     this.textInputController = document.createElement("div"); 
  29.     // 为所有dom设置id 
  30.     this.setAllControllerId(); 
  31.     // 为画笔绘制选项角标设置class 
  32.     this.setOptionIcoClassName(); 
  33.     this.toolbar = toolbar; 
  34.     // 渲染工具栏 
  35.     this.setToolBarIco(); 
  36.     // 渲染画笔相关选项 
  37.     this.setBrushSelectPanel(); 
  38.     // 渲染文本输入 
  39.     this.setTextInputPanel(); 
  40.     // 渲染页面 
  41.     this.setDomToBody(); 
  42.     // 隐藏所有dom 
  43.     this.hiddenAllDom(); 
  44.   } 
  45.    
  46.   /** 其他代码省略 **/ 
  47.    

插件入口文件

在开发vue插件时我们需要暴露一个install方法,由于此处我们不需要依赖vue,我们就无需暴露install方法,我的预想效果是:用户在使用我插件时,直接实例化插件就能正常运行。

因此,我们默认暴露出一个class,无论是使用script标签引入插件,还是在其他js框架里使用import来引入插件,都只需要在使用时new一下即可。

部分代码如下,完整代码请移步:main.ts

  1. import CreateDom from "@/lib/main-entrance/CreateDom"
  2. // 导入截图所需样式 
  3. import "@/assets/scss/screen-short.scss"
  4. import InitData from "@/lib/main-entrance/InitData"
  5. import { 
  6.   cutOutBoxBorder, 
  7.   drawCutOutBoxReturnType, 
  8.   movePositionType, 
  9.   positionInfoType, 
  10.   zoomCutOutBoxReturnType 
  11. from "@/lib/type/ComponentType"
  12. import { drawMasking } from "@/lib/split-methods/DrawMasking"
  13. import { fixedData, nonNegativeData } from "@/lib/common-methords/FixedData"
  14. import { drawPencil, initPencil } from "@/lib/split-methods/DrawPencil"
  15. import { drawText } from "@/lib/split-methods/DrawText"
  16. import { drawRectangle } from "@/lib/split-methods/DrawRectangle"
  17. import { drawCircle } from "@/lib/split-methods/DrawCircle"
  18. import { drawLineArrow } from "@/lib/split-methods/DrawLineArrow"
  19. import { drawMosaic } from "@/lib/split-methods/DrawMosaic"
  20. import { drawCutOutBox } from "@/lib/split-methods/DrawCutOutBox"
  21. import { zoomCutOutBoxPosition } from "@/lib/common-methords/ZoomCutOutBoxPosition"
  22. import { saveBorderArrInfo } from "@/lib/common-methords/SaveBorderArrInfo"
  23. import { calculateToolLocation } from "@/lib/split-methods/CalculateToolLocation"
  24.  
  25. export default class ScreenShort { 
  26.   // 当前实例的响应式data数据 
  27.   private readonly data: InitData; 
  28.  
  29.   // video容器用于存放屏幕MediaStream流 
  30.   private readonly videoController: HTMLVideoElement; 
  31.   // 截图区域canvas容器 
  32.   private readonly screenShortController: HTMLCanvasElement | null
  33.   // 截图工具栏dom 
  34.   private readonly toolController: HTMLDivElement | null
  35.   // 截图图片存放容器 
  36.   private readonly screenShortImageController: HTMLCanvasElement; 
  37.   // 截图区域画布 
  38.   private screenShortCanvas: CanvasRenderingContext2D | undefined; 
  39.   // 文本区域dom 
  40.   private readonly textInputController: HTMLDivElement | null
  41.   //  截图工具栏画笔选项dom 
  42.   private optionController: HTMLDivElement | null
  43.   private optionIcoController: HTMLDivElement | null
  44.   // 图形位置参数 
  45.   private drawGraphPosition: positionInfoType = { 
  46.     startX: 0, 
  47.     startY: 0, 
  48.     width: 0, 
  49.     height: 0 
  50.   }; 
  51.   // 临时图形位置参数 
  52.   private tempGraphPosition: positionInfoType = { 
  53.     startX: 0, 
  54.     startY: 0, 
  55.     width: 0, 
  56.     height: 0 
  57.   }; 
  58.   // 裁剪框边框节点坐标事件 
  59.   private cutOutBoxBorderArr: Array<cutOutBoxBorder> = []; 
  60.   // 当前操作的边框节点 
  61.   private borderOption: number | null = null
  62.  
  63.   // 点击裁剪框时的鼠标坐标 
  64.   private movePosition: movePositionType = { 
  65.     moveStartX: 0, 
  66.     moveStartY: 0 
  67.   }; 
  68.  
  69.   // 鼠标点击状态 
  70.   private clickFlag = false
  71.   private fontSize = 17; 
  72.   // 最大可撤销次数 
  73.   private maxUndoNum = 15; 
  74.   // 马赛克涂抹区域大小 
  75.   private degreeOfBlur = 5; 
  76.  
  77.   // 文本输入框位置 
  78.   private textInputPosition: { mouseX: number; mouseY: number } = { 
  79.     mouseX: 0, 
  80.     mouseY: 0 
  81.   }; 
  82.   constructor() { 
  83.     // 创建dom 
  84.     new CreateDom(); 
  85.     this.videoController = document.createElement("video"); 
  86.     this.videoController.autoplay = true
  87.     this.screenShortImageController = document.createElement("canvas"); 
  88.     // 实例化响应式data 
  89.     this.data = new InitData(); 
  90.     // 获取截图区域canvas容器 
  91.     this.screenShortController = this.data.getScreenShortController() as HTMLCanvasElement | null
  92.     this.toolController = this.data.getToolController() as HTMLDivElement | null
  93.     this.textInputController = this.data.getTextInputController() as HTMLDivElement | null
  94.     this.optionController = this.data.getOptionController() as HTMLDivElement | null
  95.     this.optionIcoController = this.data.getOptionIcoController() as HTMLDivElement | null
  96.     this.load(); 
  97.   } 
  98.    
  99.   /** 其他代码省略 **/ 

对外暴露default属性

做完上述配置后我们的插件开发环境就搭建好了,我执行build命令打包插件后,在vue2项目中使用import形式正常运行,在使用script标签时引入时却报错了,于是我将暴露出来的screenShotPlugin变量打印出来后发现他还有个default属性,default属性才是我们插件暴露出来的东西。

求助了下我朋友@_Dreams找到了解决方案,需要配置下webpack中的output.libraryExport属性,我们的插件是使用Vue CLI开发的,有关webpack的配置需要在需要在vue.config.js中进行配置,代码如下:

  1. module.exports = { 
  2.     // 自定义webpack配置 
  3.   configureWebpack: { 
  4.     output: { 
  5.       // 对外暴露default属性 
  6.       libraryExport: "default" 
  7.     } 
  8.   } 

这一块的配置在Vue CLI文档中也有被提到,感兴趣的开发者请移步:build-targets.html#vue-vs-js-ts-entry-files

使用webrtc截取整个屏幕

插件一开始使用的是html2canvas来将dom转换为canvas的,因为他要遍历整个body中的dom,然后再转换成canvas,而且图片还不能跨域,如果页面中图片一多,它会变得非常慢。

在上一篇文章的评论区中有位开发者 @名字什么的都不重要 建议我使用webrtc来替代html2canvas,于是我就看了下webrtc的相关文档,最终实现了截屏功能,它截取出来的东西更精确、性能更好,不存在卡顿问题也不存在css问题,而且它把选择权交给了用户,让用户决定来共享屏幕的那一部分内容。

实现思路

接下来就跟大家分享下我的实现思路:

  • 使用getDisplayMedia来捕获屏幕,得到MediaStream流
  • 将得到的MediaStream流输出到video标签中
  • 使用canvas将video标签中的内容绘制到canvas容器中

有关getDisplayMedia的具体用法,请移步:使用屏幕捕获API

实现代码

接下来,我们来看下具体的实现代码,完整代码请移步:main.ts

  1. // 加载截图组件 
  2.   private load() { 
  3.     // 设置截图区域canvas宽高 
  4.     this.data.setScreenShortInfo(window.innerWidth, window.innerHeight); 
  5.     // 设置截图图片存放容器宽高 
  6.     this.screenShortImageController.width = window.innerWidth; 
  7.     this.screenShortImageController.height = window.innerHeight; 
  8.     // 显示截图区域容器 
  9.     this.data.showScreenShortPanel(); 
  10.     // 截取整个屏幕 
  11.     this.screenShot(); 
  12.   } 
  13.  
  14.   // 开始捕捉屏幕 
  15.   private startCapture = async () => { 
  16.     let captureStream = null
  17.  
  18.     try { 
  19.       // eslint-disable-next-line @typescript-eslint/ban-ts-ignore 
  20.       // @ts-ignore 
  21.       // 捕获屏幕 
  22.       captureStream = await navigator.mediaDevices.getDisplayMedia(); 
  23.       // 将MediaStream输出至video标签 
  24.       this.videoController.srcObject = captureStream; 
  25.     } catch (err) { 
  26.       throw "浏览器不支持webrtc" + err; 
  27.     } 
  28.     return captureStream; 
  29.   }; 
  30.  
  31.   // 停止捕捉屏幕 
  32.   private stopCapture = () => { 
  33.     const srcObject = this.videoController.srcObject; 
  34.     if (srcObject && "getTracks" in srcObject) { 
  35.       const tracks = srcObject.getTracks(); 
  36.       tracks.forEach(track => track.stop()); 
  37.       this.videoController.srcObject = null
  38.     } 
  39.   }; 
  40.  
  41.   // 截屏 
  42.   private screenShot = () => { 
  43.     // 开始捕捉屏幕 
  44.     this.startCapture().then(() => { 
  45.       setTimeout(() => { 
  46.         // 获取截图区域canvas容器画布 
  47.         const context = this.screenShortController?.getContext("2d"); 
  48.         if (context == null || this.screenShortController == nullreturn
  49.  
  50.         // 赋值截图区域canvas画布 
  51.         this.screenShortCanvas = context; 
  52.         // 绘制蒙层 
  53.         drawMasking(context); 
  54.         // 将获取到的屏幕截图绘制到图片容器里 
  55.         this.screenShortImageController 
  56.           .getContext("2d"
  57.           ?.drawImage( 
  58.             this.videoController, 
  59.             0, 
  60.             0, 
  61.             this.screenShortImageController?.width, 
  62.             this.screenShortImageController?.height 
  63.           ); 
  64.         // 添加监听 
  65.         this.screenShortController?.addEventListener( 
  66.           "mousedown"
  67.           this.mouseDownEvent 
  68.         ); 
  69.         this.screenShortController?.addEventListener( 
  70.           "mousemove"
  71.           this.mouseMoveEvent 
  72.         ); 
  73.         this.screenShortController?.addEventListener( 
  74.           "mouseup"
  75.           this.mouseUpEvent 
  76.         ); 
  77.         // 停止捕捉屏幕 
  78.         this.stopCapture(); 
  79.       }, 300); 
  80.     }); 
  81.   }; 

插件地址

至此,插件的实现过程就分享完毕了。

  • 插件在线体验地址:chat-system
  • 插件GitHub仓库地址:screen-shot
  • 开源项目地址:chat-system-github

【编辑推荐】

  1. 从微信小程序到鸿蒙js开发【04】-list组件
  2. 我用Vue.js与ElementUI搭建了一个无限级联层级表格组件
  3. 15 个常见的 Node.js 面试问题及答案
  4. 一文详解 CSS-in-JS
  5. 为什么要读Nodejs源码?
【责任编辑:武晓燕 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

订阅专栏+更多

数据湖与数据仓库的分析实践攻略

数据湖与数据仓库的分析实践攻略

助力现代化数据管理:数据湖与数据仓库的分析实践攻略
共3章 | 创世达人

5人订阅学习

云原生架构实践

云原生架构实践

新技术引领移动互联网进入急速赛道
共3章 | KaliArch

32人订阅学习

数据中心和VPDN网络建设案例

数据中心和VPDN网络建设案例

漫画+案例
共20章 | 捷哥CCIE

218人订阅学习

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊

51CTO服务号

51CTO官微