實(shí)現(xiàn)Web端自定義截屏(原生JS版)
本文轉(zhuǎn)載自微信公眾號(hào)「神奇的程序員K」,作者神奇的程序員K。轉(zhuǎn)載本文請(qǐng)聯(lián)系神奇的程序員K公眾號(hào)。
前言
前幾天我發(fā)布了一個(gè)web端自定義截圖的插件,在使用過(guò)程中有開(kāi)發(fā)者反饋這個(gè)插件無(wú)法在vue2項(xiàng)目中使用,于是,我就開(kāi)始找問(wèn)題,發(fā)現(xiàn)我的插件是基于Vue3的開(kāi)發(fā)的,由于Vue3的插件和Vue2的插件完全不兼容,因此插件也就只能在Vue3項(xiàng)目中使用。
經(jīng)過(guò)一番考慮后,我決定用原生js來(lái)重構(gòu)這個(gè)插件,讓其不依賴(lài)任何庫(kù),這樣它就能運(yùn)行在任意一臺(tái)支持js的設(shè)備上,本文就跟大家分享下我重構(gòu)這個(gè)插件的過(guò)程,歡迎各位感興趣的開(kāi)發(fā)者閱讀本文。
運(yùn)行結(jié)果視頻:(請(qǐng)看原文)
使用Vue實(shí)現(xiàn)Web端的自定義截屏,效果如視頻所示,文章,教程,體驗(yàn)地址明天和大家分享[壞笑] #Vue #截屏 #自定義截屏 #Web前端
寫(xiě)在前面
本文不講解插件的具體實(shí)現(xiàn)思路,對(duì)插件實(shí)現(xiàn)思路感興趣的開(kāi)發(fā)者請(qǐng)移步:實(shí)現(xiàn)Web端自定義截屏
搭建開(kāi)發(fā)環(huán)境
我想使用ts、scss、eslint、prettier來(lái)提升插件的可維護(hù)性,又嫌麻煩,不想手動(dòng)配置webpack環(huán)境,于是我決定使用Vue CLI來(lái)搭建插件開(kāi)發(fā)環(huán)境。
本文不細(xì)講Vue CLI搭建插件開(kāi)發(fā)環(huán)境的過(guò)程,對(duì)此感興趣的開(kāi)發(fā)者請(qǐng)移步:使用CLI開(kāi)發(fā)一個(gè)Vue3的npm庫(kù)。
移除vue相關(guān)依賴(lài)
我們搭建好插件的開(kāi)發(fā)環(huán)境后,CLI默認(rèn)會(huì)在package.json中添加Vue的相關(guān)包,我們的插件不會(huì)依賴(lài)于vue,因此我們把它刪除即可。
- {
- - "vue": "^3.0.0-0",
- - "vue-class-component": "^8.0.0-0"
- }
創(chuàng)建DOM
為了方便開(kāi)發(fā)者使用dom,這里選擇使用js動(dòng)態(tài)來(lái)創(chuàng)建dom,最后將其掛載到body中,在vue3版本的截圖插件中,我們可以使用vue組件來(lái)輔助我們,這里我們就要基于組件來(lái)使用js來(lái)創(chuàng)建對(duì)應(yīng)的dom,為其綁定對(duì)應(yīng)的事件。
部分實(shí)現(xiàn)代碼如下,完整代碼請(qǐng)移步:CreateDom.ts
- import toolbar from "@/lib/config/Toolbar";
- import { toolbarType } from "@/lib/type/ComponentType";
- import { toolClickEvent } from "@/lib/split-methods/ToolClickEvent";
- import { setBrushSize } from "@/lib/common-methords/SetBrushSize";
- import { selectColor } from "@/lib/common-methords/SelectColor";
- import { getColor } from "@/lib/common-methords/GetColor";
- export default class CreateDom {
- // 截圖區(qū)域canvas容器
- private readonly screenShortController: HTMLCanvasElement;
- // 截圖工具欄容器
- private readonly toolController: HTMLDivElement;
- // 繪制選項(xiàng)頂部ico容器
- private readonly optionIcoController: HTMLDivElement;
- // 畫(huà)筆繪制選項(xiàng)容器
- private readonly optionController: HTMLDivElement;
- // 文字工具輸入容器
- private readonly textInputController: HTMLDivElement;
- // 截圖工具欄圖標(biāo)
- private readonly toolbar: Array<toolbarType>;
- constructor() {
- this.screenShortController = document.createElement("canvas");
- this.toolController = document.createElement("div");
- this.optionIcoController = document.createElement("div");
- this.optionController = document.createElement("div");
- this.textInputController = document.createElement("div");
- // 為所有dom設(shè)置id
- this.setAllControllerId();
- // 為畫(huà)筆繪制選項(xiàng)角標(biāo)設(shè)置class
- this.setOptionIcoClassName();
- this.toolbar = toolbar;
- // 渲染工具欄
- this.setToolBarIco();
- // 渲染畫(huà)筆相關(guān)選項(xiàng)
- this.setBrushSelectPanel();
- // 渲染文本輸入
- this.setTextInputPanel();
- // 渲染頁(yè)面
- this.setDomToBody();
- // 隱藏所有dom
- this.hiddenAllDom();
- }
- /** 其他代碼省略 **/
- }
插件入口文件
在開(kāi)發(fā)vue插件時(shí)我們需要暴露一個(gè)install方法,由于此處我們不需要依賴(lài)vue,我們就無(wú)需暴露install方法,我的預(yù)想效果是:用戶(hù)在使用我插件時(shí),直接實(shí)例化插件就能正常運(yùn)行。
因此,我們默認(rèn)暴露出一個(gè)class,無(wú)論是使用script標(biāo)簽引入插件,還是在其他js框架里使用import來(lái)引入插件,都只需要在使用時(shí)new一下即可。
部分代碼如下,完整代碼請(qǐng)移步:main.ts
- import CreateDom from "@/lib/main-entrance/CreateDom";
- // 導(dǎo)入截圖所需樣式
- import "@/assets/scss/screen-short.scss";
- import InitData from "@/lib/main-entrance/InitData";
- import {
- cutOutBoxBorder,
- drawCutOutBoxReturnType,
- movePositionType,
- positionInfoType,
- zoomCutOutBoxReturnType
- } from "@/lib/type/ComponentType";
- import { drawMasking } from "@/lib/split-methods/DrawMasking";
- import { fixedData, nonNegativeData } from "@/lib/common-methords/FixedData";
- import { drawPencil, initPencil } from "@/lib/split-methods/DrawPencil";
- import { drawText } from "@/lib/split-methods/DrawText";
- import { drawRectangle } from "@/lib/split-methods/DrawRectangle";
- import { drawCircle } from "@/lib/split-methods/DrawCircle";
- import { drawLineArrow } from "@/lib/split-methods/DrawLineArrow";
- import { drawMosaic } from "@/lib/split-methods/DrawMosaic";
- import { drawCutOutBox } from "@/lib/split-methods/DrawCutOutBox";
- import { zoomCutOutBoxPosition } from "@/lib/common-methords/ZoomCutOutBoxPosition";
- import { saveBorderArrInfo } from "@/lib/common-methords/SaveBorderArrInfo";
- import { calculateToolLocation } from "@/lib/split-methods/CalculateToolLocation";
- export default class ScreenShort {
- // 當(dāng)前實(shí)例的響應(yīng)式data數(shù)據(jù)
- private readonly data: InitData;
- // video容器用于存放屏幕MediaStream流
- private readonly videoController: HTMLVideoElement;
- // 截圖區(qū)域canvas容器
- private readonly screenShortController: HTMLCanvasElement | null;
- // 截圖工具欄dom
- private readonly toolController: HTMLDivElement | null;
- // 截圖圖片存放容器
- private readonly screenShortImageController: HTMLCanvasElement;
- // 截圖區(qū)域畫(huà)布
- private screenShortCanvas: CanvasRenderingContext2D | undefined;
- // 文本區(qū)域dom
- private readonly textInputController: HTMLDivElement | null;
- // 截圖工具欄畫(huà)筆選項(xiàng)dom
- private optionController: HTMLDivElement | null;
- private optionIcoController: HTMLDivElement | null;
- // 圖形位置參數(shù)
- private drawGraphPosition: positionInfoType = {
- startX: 0,
- startY: 0,
- width: 0,
- height: 0
- };
- // 臨時(shí)圖形位置參數(shù)
- private tempGraphPosition: positionInfoType = {
- startX: 0,
- startY: 0,
- width: 0,
- height: 0
- };
- // 裁剪框邊框節(jié)點(diǎn)坐標(biāo)事件
- private cutOutBoxBorderArr: Array<cutOutBoxBorder> = [];
- // 當(dāng)前操作的邊框節(jié)點(diǎn)
- private borderOption: number | null = null;
- // 點(diǎn)擊裁剪框時(shí)的鼠標(biāo)坐標(biāo)
- private movePosition: movePositionType = {
- moveStartX: 0,
- moveStartY: 0
- };
- // 鼠標(biāo)點(diǎn)擊狀態(tài)
- private clickFlag = false;
- private fontSize = 17;
- // 最大可撤銷(xiāo)次數(shù)
- private maxUndoNum = 15;
- // 馬賽克涂抹區(qū)域大小
- private degreeOfBlur = 5;
- // 文本輸入框位置
- private textInputPosition: { mouseX: number; mouseY: number } = {
- mouseX: 0,
- mouseY: 0
- };
- constructor() {
- // 創(chuàng)建dom
- new CreateDom();
- this.videoController = document.createElement("video");
- this.videoController.autoplay = true;
- this.screenShortImageController = document.createElement("canvas");
- // 實(shí)例化響應(yīng)式data
- this.data = new InitData();
- // 獲取截圖區(qū)域canvas容器
- this.screenShortController = this.data.getScreenShortController() as HTMLCanvasElement | null;
- this.toolController = this.data.getToolController() as HTMLDivElement | null;
- this.textInputController = this.data.getTextInputController() as HTMLDivElement | null;
- this.optionController = this.data.getOptionController() as HTMLDivElement | null;
- this.optionIcoController = this.data.getOptionIcoController() as HTMLDivElement | null;
- this.load();
- }
- /** 其他代碼省略 **/
- }
對(duì)外暴露default屬性
做完上述配置后我們的插件開(kāi)發(fā)環(huán)境就搭建好了,我執(zhí)行build命令打包插件后,在vue2項(xiàng)目中使用import形式正常運(yùn)行,在使用script標(biāo)簽時(shí)引入時(shí)卻報(bào)錯(cuò)了,于是我將暴露出來(lái)的screenShotPlugin變量打印出來(lái)后發(fā)現(xiàn)他還有個(gè)default屬性,default屬性才是我們插件暴露出來(lái)的東西。
求助了下我朋友@_Dreams找到了解決方案,需要配置下webpack中的output.libraryExport屬性,我們的插件是使用Vue CLI開(kāi)發(fā)的,有關(guān)webpack的配置需要在需要在vue.config.js中進(jìn)行配置,代碼如下:
- module.exports = {
- // 自定義webpack配置
- configureWebpack: {
- output: {
- // 對(duì)外暴露default屬性
- libraryExport: "default"
- }
- }
- }
這一塊的配置在Vue CLI文檔中也有被提到,感興趣的開(kāi)發(fā)者請(qǐng)移步:build-targets.html#vue-vs-js-ts-entry-files
使用webrtc截取整個(gè)屏幕
插件一開(kāi)始使用的是html2canvas來(lái)將dom轉(zhuǎn)換為canvas的,因?yàn)樗闅v整個(gè)body中的dom,然后再轉(zhuǎn)換成canvas,而且圖片還不能跨域,如果頁(yè)面中圖片一多,它會(huì)變得非常慢。
在上一篇文章的評(píng)論區(qū)中有位開(kāi)發(fā)者 @名字什么的都不重要 建議我使用webrtc來(lái)替代html2canvas,于是我就看了下webrtc的相關(guān)文檔,最終實(shí)現(xiàn)了截屏功能,它截取出來(lái)的東西更精確、性能更好,不存在卡頓問(wèn)題也不存在css問(wèn)題,而且它把選擇權(quán)交給了用戶(hù),讓用戶(hù)決定來(lái)共享屏幕的那一部分內(nèi)容。
實(shí)現(xiàn)思路
接下來(lái)就跟大家分享下我的實(shí)現(xiàn)思路:
- 使用getDisplayMedia來(lái)捕獲屏幕,得到MediaStream流
- 將得到的MediaStream流輸出到video標(biāo)簽中
- 使用canvas將video標(biāo)簽中的內(nèi)容繪制到canvas容器中
有關(guān)getDisplayMedia的具體用法,請(qǐng)移步:使用屏幕捕獲API
實(shí)現(xiàn)代碼
接下來(lái),我們來(lái)看下具體的實(shí)現(xiàn)代碼,完整代碼請(qǐng)移步:main.ts
- // 加載截圖組件
- private load() {
- // 設(shè)置截圖區(qū)域canvas寬高
- this.data.setScreenShortInfo(window.innerWidth, window.innerHeight);
- // 設(shè)置截圖圖片存放容器寬高
- this.screenShortImageController.width = window.innerWidth;
- this.screenShortImageController.height = window.innerHeight;
- // 顯示截圖區(qū)域容器
- this.data.showScreenShortPanel();
- // 截取整個(gè)屏幕
- this.screenShot();
- }
- // 開(kāi)始捕捉屏幕
- private startCapture = async () => {
- let captureStream = null;
- try {
- // eslint-disable-next-line @typescript-eslint/ban-ts-ignore
- // @ts-ignore
- // 捕獲屏幕
- captureStream = await navigator.mediaDevices.getDisplayMedia();
- // 將MediaStream輸出至video標(biāo)簽
- this.videoController.srcObject = captureStream;
- } catch (err) {
- throw "瀏覽器不支持webrtc" + err;
- }
- return captureStream;
- };
- // 停止捕捉屏幕
- private stopCapture = () => {
- const srcObject = this.videoController.srcObject;
- if (srcObject && "getTracks" in srcObject) {
- const tracks = srcObject.getTracks();
- tracks.forEach(track => track.stop());
- this.videoController.srcObject = null;
- }
- };
- // 截屏
- private screenShot = () => {
- // 開(kāi)始捕捉屏幕
- this.startCapture().then(() => {
- setTimeout(() => {
- // 獲取截圖區(qū)域canvas容器畫(huà)布
- const context = this.screenShortController?.getContext("2d");
- if (context == null || this.screenShortController == null) return;
- // 賦值截圖區(qū)域canvas畫(huà)布
- this.screenShortCanvas = context;
- // 繪制蒙層
- drawMasking(context);
- // 將獲取到的屏幕截圖繪制到圖片容器里
- this.screenShortImageController
- .getContext("2d")
- ?.drawImage(
- this.videoController,
- 0,
- 0,
- this.screenShortImageController?.width,
- this.screenShortImageController?.height
- );
- // 添加監(jiān)聽(tīng)
- this.screenShortController?.addEventListener(
- "mousedown",
- this.mouseDownEvent
- );
- this.screenShortController?.addEventListener(
- "mousemove",
- this.mouseMoveEvent
- );
- this.screenShortController?.addEventListener(
- "mouseup",
- this.mouseUpEvent
- );
- // 停止捕捉屏幕
- this.stopCapture();
- }, 300);
- });
- };
插件地址
至此,插件的實(shí)現(xiàn)過(guò)程就分享完畢了。
- 插件在線體驗(yàn)地址:chat-system
- 插件GitHub倉(cāng)庫(kù)地址:screen-shot
- 開(kāi)源項(xiàng)目地址:chat-system-github