Внедрение пользовательских веб-снимков экрана (версия JS)

JavaScript Canvas
Внедрение пользовательских веб-снимков экрана (версия JS)

предисловие

Несколько дней назад я выпустил плагин для пользовательских скриншотов на веб-странице.В процессе использования некоторые разработчики сообщали, что этот плагин нельзя использовать в проекте vue2.Поэтому я начал искать проблемы и обнаружил, что мой плагин был разработан на основе Vue3., так как плагин Vue3 полностью несовместим с плагином Vue2, плагин можно использовать только в проекте Vue3.

После некоторого размышления я решил провести рефакторинг этого плагина с нативным js, чтобы он не зависел ни от какой библиотеки, чтобы он мог работать на любом устройстве, поддерживающем js.В этой статье я поделюсь с вами тем, как я рефакторил этот плагин. приглашаем всех заинтересованных разработчиков прочитать эту статью.

Видео результатов прогона:Внедрение пользовательских скриншотов в Интернете

написать впереди

В этой статье не объясняются конкретные идеи реализации подключаемых модулей. Разработчикам, интересующимся идеями реализации подключаемых модулей, следует перейти к:Реализация пользовательских скриншотов в Интернете

Создайте среду разработки

Я хочу использовать ts, scss, eslint и prettier, чтобы улучшить ремонтопригодность плагина, но вручную настраивать среду webpack слишком сложно, поэтому я решил использовать Vue CLI для создания среды разработки плагинов.

В этой статье не рассматривается процесс создания среды разработки подключаемых модулей с помощью Vue CLI. Заинтересованные разработчики могут перейти по адресу:Разработать библиотеку VUE3 NPM с помощью CLI.

Удалить зависимости, связанные с vue

После того, как мы настроим среду разработки плагина, CLI по умолчанию добавит пакеты, связанные с Vue, в package.json Наш плагин не будет зависеть от vue, поэтому мы можем его удалить.

{
- "vue": "^3.0.0-0",
- "vue-class-component": "^8.0.0-0"
}

Создать DOM

Чтобы облегчить использование dom разработчиками, здесь мы решили использовать js для динамического создания dom и, наконец, смонтировать его в теле.В плагине скриншота версии 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 нам необходимо предоставить метод установки. Поскольку здесь нам не нужно полагаться на vue, нам не нужно раскрывать метод установки. Ожидаемый эффект заключается в том, что когда пользователи используют мой подключаемый модуль, in, они могут напрямую создать экземпляр подключаемого модуля для нормальной работы.

Таким образом, мы предоставляем класс по умолчанию.Используем ли мы тег script для импорта подключаемых модулей или используем импорт для импорта подключаемых модулей в других средах js, нам нужно использовать только новый при его использовании.

Часть кода выглядит следующим образом, перейдите к полному коду: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();
  }
  
  /** 其他代码省略 **/
}

Предоставьте внешнему миру свойство по умолчанию

После завершения приведенной выше конфигурации наша среда разработки подключаемого модуля настроена.После выполнения команды сборки для упаковки подключаемого модуля я использую форму импорта для нормальной работы в проекте vue2, но при использовании сообщается об ошибке тег script для импорта, поэтому я выставлю выставленныйscreenShotPluginПосле того, как переменная напечатана, обнаруживается, что у него тоже есть атрибут по умолчанию, который и выставляет наш плагин.

помогите моему другу@_DreamsНашел решение, нужно настроить вебпак вoutput.libraryExportсвойство, наш плагин разработан с использованием Vue CLI, конфигурация веб-пакета должна быть в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, потому что ему нужно пройти dom по всему телу, а затем преобразовать его в canvas, а картинки не могут пересекать домены.Если на странице слишком много картинок, это станет очень медленным.

существуетпредыдущий постРазработчик в разделе комментариев@имя ничего не имеет значенияБыло предложено использовать webrtc для замены html2canvas, поэтому я прочитал соответствующие документы webrtc и, наконец, реализовал функцию захвата экрана.Предоставьте пользователю выбор и позвольте пользователю решить, какой частью экрана поделиться.

Реализовать идеи

Далее я поделюсь с вами своими идеями реализации:

  • использоватьgetDisplayMediaчтобы сделать снимок экрана, получитеMediaStreamпоток
  • получитеMediaStreamВывод потока в тег видео
  • Используйте холст, чтобы нарисовать содержимое тега видео в контейнере холста.

Для конкретного использования 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

напиши в конце

  • Если в статье есть ошибки, исправьте их в комментариях, если статья вам поможет, ставьте лайк и подписывайтесь 😊
  • Эта статья была впервые опубликована на Наггетс, перепечатка без разрешения запрещена 💌