實現Web端自定義截屏(原生JS版)
本文轉載自微信公眾號「神奇的程序員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,因此我們把它刪除即可。
- {
- - "vue": "^3.0.0-0",
- - "vue-class-component": "^8.0.0-0"
- }
創建DOM
為了方便開發者使用dom,這里選擇使用js動態來創建dom,最后將其掛載到body中,在vue3版本的截圖插件中,我們可以使用vue組件來輔助我們,這里我們就要基于組件來使用js來創建對應的dom,為其綁定對應的事件。
部分實現代碼如下,完整代碼請移步: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 {
- // 截圖區域canvas容器
- private readonly screenShortController: HTMLCanvasElement;
- // 截圖工具欄容器
- private readonly toolController: HTMLDivElement;
- // 繪制選項頂部ico容器
- private readonly optionIcoController: HTMLDivElement;
- // 畫筆繪制選項容器
- private readonly optionController: HTMLDivElement;
- // 文字工具輸入容器
- private readonly textInputController: HTMLDivElement;
- // 截圖工具欄圖標
- 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設置id
- this.setAllControllerId();
- // 為畫筆繪制選項角標設置class
- this.setOptionIcoClassName();
- this.toolbar = toolbar;
- // 渲染工具欄
- this.setToolBarIco();
- // 渲染畫筆相關選項
- this.setBrushSelectPanel();
- // 渲染文本輸入
- this.setTextInputPanel();
- // 渲染頁面
- this.setDomToBody();
- // 隱藏所有dom
- this.hiddenAllDom();
- }
- /** 其他代碼省略 **/
- }
插件入口文件
在開發vue插件時我們需要暴露一個install方法,由于此處我們不需要依賴vue,我們就無需暴露install方法,我的預想效果是:用戶在使用我插件時,直接實例化插件就能正常運行。
因此,我們默認暴露出一個class,無論是使用script標簽引入插件,還是在其他js框架里使用import來引入插件,都只需要在使用時new一下即可。
部分代碼如下,完整代碼請移步:main.ts
- import CreateDom from "@/lib/main-entrance/CreateDom";
- // 導入截圖所需樣式
- 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 {
- // 當前實例的響應式data數據
- private readonly data: InitData;
- // video容器用于存放屏幕MediaStream流
- private readonly videoController: HTMLVideoElement;
- // 截圖區域canvas容器
- private readonly screenShortController: HTMLCanvasElement | null;
- // 截圖工具欄dom
- private readonly toolController: HTMLDivElement | null;
- // 截圖圖片存放容器
- private readonly screenShortImageController: HTMLCanvasElement;
- // 截圖區域畫布
- private screenShortCanvas: CanvasRenderingContext2D | undefined;
- // 文本區域dom
- private readonly textInputController: HTMLDivElement | null;
- // 截圖工具欄畫筆選項dom
- private optionController: HTMLDivElement | null;
- private optionIcoController: HTMLDivElement | null;
- // 圖形位置參數
- private drawGraphPosition: positionInfoType = {
- startX: 0,
- startY: 0,
- width: 0,
- height: 0
- };
- // 臨時圖形位置參數
- private tempGraphPosition: positionInfoType = {
- startX: 0,
- startY: 0,
- width: 0,
- height: 0
- };
- // 裁剪框邊框節點坐標事件
- private cutOutBoxBorderArr: Array<cutOutBoxBorder> = [];
- // 當前操作的邊框節點
- private borderOption: number | null = null;
- // 點擊裁剪框時的鼠標坐標
- private movePosition: movePositionType = {
- moveStartX: 0,
- moveStartY: 0
- };
- // 鼠標點擊狀態
- private clickFlag = false;
- private fontSize = 17;
- // 最大可撤銷次數
- private maxUndoNum = 15;
- // 馬賽克涂抹區域大小
- private degreeOfBlur = 5;
- // 文本輸入框位置
- private textInputPosition: { mouseX: number; mouseY: number } = {
- mouseX: 0,
- mouseY: 0
- };
- constructor() {
- // 創建dom
- new CreateDom();
- this.videoController = document.createElement("video");
- this.videoController.autoplay = true;
- this.screenShortImageController = document.createElement("canvas");
- // 實例化響應式data
- this.data = new InitData();
- // 獲取截圖區域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();
- }
- /** 其他代碼省略 **/
- }
對外暴露default屬性
做完上述配置后我們的插件開發環境就搭建好了,我執行build命令打包插件后,在vue2項目中使用import形式正常運行,在使用script標簽時引入時卻報錯了,于是我將暴露出來的screenShotPlugin變量打印出來后發現他還有個default屬性,default屬性才是我們插件暴露出來的東西。
求助了下我朋友@_Dreams找到了解決方案,需要配置下webpack中的output.libraryExport屬性,我們的插件是使用Vue CLI開發的,有關webpack的配置需要在需要在vue.config.js中進行配置,代碼如下:
- module.exports = {
- // 自定義webpack配置
- configureWebpack: {
- output: {
- // 對外暴露default屬性
- libraryExport: "default"
- }
- }
- }
這一塊的配置在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
- // 加載截圖組件
- private load() {
- // 設置截圖區域canvas寬高
- this.data.setScreenShortInfo(window.innerWidth, window.innerHeight);
- // 設置截圖圖片存放容器寬高
- this.screenShortImageController.width = window.innerWidth;
- this.screenShortImageController.height = window.innerHeight;
- // 顯示截圖區域容器
- this.data.showScreenShortPanel();
- // 截取整個屏幕
- this.screenShot();
- }
- // 開始捕捉屏幕
- 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標簽
- 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 = () => {
- // 開始捕捉屏幕
- this.startCapture().then(() => {
- setTimeout(() => {
- // 獲取截圖區域canvas容器畫布
- const context = this.screenShortController?.getContext("2d");
- if (context == null || this.screenShortController == null) return;
- // 賦值截圖區域canvas畫布
- this.screenShortCanvas = context;
- // 繪制蒙層
- drawMasking(context);
- // 將獲取到的屏幕截圖繪制到圖片容器里
- this.screenShortImageController
- .getContext("2d")
- ?.drawImage(
- this.videoController,
- 0,
- 0,
- this.screenShortImageController?.width,
- this.screenShortImageController?.height
- );
- // 添加監聽
- this.screenShortController?.addEventListener(
- "mousedown",
- this.mouseDownEvent
- );
- this.screenShortController?.addEventListener(
- "mousemove",
- this.mouseMoveEvent
- );
- this.screenShortController?.addEventListener(
- "mouseup",
- this.mouseUpEvent
- );
- // 停止捕捉屏幕
- this.stopCapture();
- }, 300);
- });
- };
插件地址
至此,插件的實現過程就分享完畢了。
- 插件在線體驗地址:chat-system
- 插件GitHub倉庫地址:screen-shot
- 開源項目地址:chat-system-github