Используйте WebGL для реализации приложения для генерации кода пользовательского интерфейса с помощью перетаскивания.

React.js
Используйте WebGL для реализации приложения для генерации кода пользовательского интерфейса с помощью перетаскивания.

предисловие

Пользовательский интерфейс (пользовательский интерфейс), то есть пользовательский интерфейс, который является средством взаимодействия и обмена информацией между программным обеспечением и пользователями и реализует преобразование между внутренней формой информации и приемлемой для человека формой. Разработка пользовательского интерфейса обычно требует двух процессов: дизайна пользовательского интерфейса и реализации пользовательского интерфейса. Дизайн пользовательского интерфейса - это дизайн взаимодействия программного обеспечения, логики работы и интерфейса.Обычно дизайнеры пользовательского интерфейса и дизайнеры взаимодействия завершают разработку набора интерфейсов пользовательского интерфейса в соответствии с потребностями пользователей в программном обеспечении и, наконец, представляют их в виде дизайна пользовательского интерфейса. черновики (psd, png, jpeg файлы и т.д.). Реализация пользовательского интерфейса — это кодирование проекта дизайна пользовательского интерфейса, созданного на этапе проектирования пользовательского интерфейса, что является частью задачи фронтенд-инженеров.

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

​ В ближнем и внешнем бизнесе прямых трансляций ByteDance часто необходимо разрабатывать страницы событий для нескольких платформ. С другой стороны, активные страницы обычно имеют макет, аналогичную логику, высокую частоту запросов и быструю итерацию. Если вы используете обычный метод разработки для разработки страницы активности, вам необходимо участвовать в разработке продукта, внешнего интерфейса, сервера, тестирования и других сторон, и каждая страница активности имеет длительный онлайн-цикл, который не может отвечать потребностям пользователей. товар быстро. Для разработки страницы события лучше всего использовать визуализацию страницы для создания платформы для достижения промежуточного этапа живого события.Платформа кубика Рубика. Платформа реализует компонентный редактор пользовательского интерфейса на основе DOM и предоставляет хорошо упакованные компоненты пользовательского интерфейса, которые студенты-операторы могут использовать для заполнения активной страницы. В прошлом на разработку страницы активности уходило 4 человеко-дня, а на перетаскивание страницы активности и выход в онлайн требовалось 2 часа, что значительно повышало эффективность разработки страницы.

Однако у платформы Кубика Рубика также есть определенные ограничения: поскольку она должна ориентироваться только на компании, связанные с событиями, платформу можно применять только для создания страниц событий. Благодаря расширению JSON для определения схемы для описания редактируемой страницы пользовательского интерфейса возможности описания схемы на основе JSON ограничены, и страницу пользовательского интерфейса можно восстановить только путем анализа схемы с помощью соответствующего клиента и нельзя применить к другим платформам.

Поэтому на основе платформы Кубика Рубика предлагается более общее приложение для редактирования пользовательского интерфейса, которое использует более общий DSL для описания перетаскиваемых страниц и может компилировать код DSL в код каждой платформы. похож на алиImgcook, Внедрение редактора пользовательского интерфейса на основе WebGL, компиляция в многотерминальный код на основе DSL, повышение эффективности разработки пользовательского интерфейса.

Отображение эффекта операции и используемая технология

Отображение эффекта бега

Главная страница: слева представлены основные компоненты, посередине находится редактор пользовательского интерфейса, реализованный с помощью WebGL, а справа реализовано изменение свойств выбранного компонента пользовательского интерфейса.

компиляция кода: сгенерируйте текущую страницу пользовательского интерфейса в целевой код и экспортируйте соответствующий файл кода.
Страница редактирования DSL: обеспечить редактирование кода DSL и создать страницу пользовательского интерфейса.

Используемая технология

Обычная платформа для создания пользовательского интерфейса с помощью перетаскивания будет превращена в веб-сайт, и в этой статье делается попытка реализовать ее в виде приложения Electron.

  • Electron: Electron — это фреймворк для создания нативных кроссплатформенных настольных приложений с использованием технологий веб-интерфейса (HTML/CSS/JavaScript/React и т. д.). можно использоватьelectron-react-boilerplateШаблон быстро использует React для разработки, но в этой статье используется ручная сборка среды React и использование Webpack и Electron-builder для завершения упаковки ресурсов и создания приложения.Используйте Webpack/React для упаковки и сборки приложений Electron..

  • Node.js: Node.js — это кроссплатформенная среда выполнения JavaScript с открытым исходным кодом, основанная на движке Chrome V8, позволяющая запускать JavaScript в серверной среде. Node.js использует однопоточный, асинхронный неблокирующий ввод-вывод и архитектуру, управляемую событиями, что делает Node.js чрезвычайно эффективным при выполнении задач с интенсивным вводом-выводом.

  • React: React — это библиотека JavaScript для создания веб-интерфейсов, которая позволяет разработчикам создавать пользовательские интерфейсы на основе данных, в компонентной и декларативной манере.

  • WebGL: это протокол трехмерного рисования, работающий на стороне Интернета. Этот протокол рисования сочетает в себе JavaScript и OpenGL ES2.0 для обеспечения аппаратного ускорения трехмерного рендеринга и визуализации трехмерных сцен и моделей в браузере с помощью графических карт. Появление технологии WebGL решает две ключевые проблемы существующей веб-3D-рендеринга: 1. Кросс-платформенная 3D-рендеринг может быть достигнута с помощью собственного тега canvas. 2. Эффективность рендеринга высока, а рендеринг графики реализован на основе базового аппаратного ускорения.

  • Konva: 2D-библиотека JavaScript, разработанная на основе Canvas, которую можно легко использовать для реализации графических эффектов взаимодействия настольных и мобильных приложений, и которая может эффективно реализовывать такие функции, как анимация, преобразование, вложение узлов, локальные операции, фильтры, кэширование, события и т. д. и т.п. Самая большая особенность Konva заключается в том, что с графикой можно взаимодействовать.Вся графика Konva может прослушивать события и реализовывать метод взаимодействия, аналогичный родному DOM. Слушатели событий находятся в слое (Konva.Layer), каждый слой имеет средство визуализации переднего плана для отображения графики и средство визуализации фона для прослушивания событий путем регистрации глобальных событий в средстве визуализации фона для определения графики, запускающей событие в данный момент, и вызова обработки обратного вызова события. Konva в значительной степени заимствует DOM браузера, например, Konva определяет этап (Konva.Stage) для хранения всех графиков, что-то вродеhtmlМетки, определите слои для отображения графики, аналогичноbodyЭтикетка. Среди них вложение узлов, мониторинг событий, поиск узлов и т. д. также используют операции DOM, что позволяет разработчикам интерфейса быстро начать работу с инфраструктурой Konva.

Дизайн приложения

анализ спроса

Основные функции приложения включают редактор пользовательского интерфейса WebGL, редактор кода DSL и компилятор кода DSL.Требования к системным функциям следующие.

  • Основные функции. В системе должны быть реализованы основные функции входа и регистрации, функции выхода из системы и глобальные привязки сочетаний клавиш.

  • Редактор пользовательского интерфейса: Редактор пользовательского интерфейса Visual WebGL, предоставляющий базовую общую библиотеку компонентов пользовательского интерфейса, позволяющий пользователям рисовать страницу пользовательского интерфейса путем перетаскивания компонентов базовой общей библиотеки компонентов пользовательского интерфейса; предоставляющий панель инструментов компонентов, позволяющую пользователям выполнять операции с компонентами на холст Копировать, удалять, вставлять, повторять и т. д. Предоставляет панель свойств компонента, позволяющую пользователям изменять фон, границу, положение, размер и другие свойства компонента; предоставляет панель инструментов построения кода DSL, позволяющую пользователям создавать страницу пользовательского интерфейса на холсте в коде DSL и затем скомпилируйте код DSL в код целевой платформы.

  • Редактор кода DSL: предоставляет редактор для написания кода DSL, поддерживающий такие функции, как выделение кода, копирование, вставка и сохранение. Предоставляет файловую систему, которая позволяет пользователям создавать и удалять файл кода DSL; предоставляет инструмент запуска кода для создания кода DSL для страниц пользовательского интерфейса или объектного кода.

  • Справочный центр: справка по синтаксису кода DSL, справка по редактору пользовательского интерфейса.

Общий архитектурный дизайн

Система принимает режим клиент / сервер для архитектуры, а интерфейс и серверная часть разделены для разработки.Клиентская сторона - приложение Electron, а серверная часть реализована Express.image-20200706093153577

  • На стороне клиента для реализации кроссплатформенного приложения на стороне ПК используются Electron, React и Node.js.

  • Серверная часть основана на серверной части, написанной Node.js Express, и предоставляет соответствующий API для вызова клиентской стороной. Интегрируйте сервисы WebSocket, работайте независимо на стороне Node.js, совместно используйте общедоступные классы и функции, такие как соответствующие подключения к базе данных, и обеспечьте поддержку Socket. И создайте сервер статических ресурсов на основе Niginx для предоставления услуг хранения изображений и других файлов.

  • База данных использует базу данных MySQL/MongoDB, MongoDB хранит информацию о странице пользовательского интерфейса, такую ​​как положение элемента пользовательского интерфейса, размер, стиль и другую информацию, а также другую информацию в форме JSON. MySQL хранит некоторую базовую информацию, такую ​​как информация о пользователе и информация о компонентах.

Дизайн клиентской архитектуры

Клиентская часть представляет собой приложение для ПК, разработанное с использованием технологии Electron. Хотя Electron использует интерфейсную технологию для создания фреймворка для кроссплатформенных приложений, он отличается от традиционных методов разработки веб-сайтов. Electron основан на модели процесса master-slave, то есть он состоит из главного процесса и нескольких процессов рендеринга, а процессы используют IPC для связи. На основе этой процессной модели процессы системы функционально подразделяются:

  • Основной процесс отвечает за взаимодействие между процессами, управление окнами, серверные запросы и загрузку собственных плагинов C++.
  • Процесс рендеринга отвечает только за рендеринг веб-страниц и конкретной бизнес-логики.

Процесс рендеринга разработан с использованием Typescript/React/Redux, с помощью React Hooks общая логика пользовательского интерфейса может быть лучше разделена, а скорость повторного использования кода может быть улучшена. Основной процесс разработан с использованием Typescript/C++, в котором C++ разрабатывает плагины Node.js и упаковывает их в.nodeфайл, загруженный основным процессом.nodeТаким образом, файл вызывает код C++. С помощью инструмента компиляции Webpack весь код процесса рендеринга компилируется вindex.html,renderer.js,style.cssИ выполняйте сжатие кода и оптимизацию сегментации кода, чтобы повысить эффективность выполнения кода. Вся компиляция кода основного процесса компилируется только в одинmain.js, И вmain.jsзагрузить процесс рендеринга вindex.htmlЗавершите работу всей системы. последнее повторное использованиеelectron-builderУпакуйте скомпилированный основной код процесса, код процесса рендеринга и другие файлы ресурсов в один.dmgФайлы приложений для завершения построения всей системы.image-20200706093244240

схема основного процесса

Основной процесс клиента можно разделить на три модуля: модуль виджетов, модуль сервисов и модуль компиляции.image-20200706094043998

  • Модуль Widget отвечает за создание окна и управление им, например, создание окна входа в систему, свертывание, отключение вызовов IPC, таких как окно входа в систему.

  • Модуль «Службы» отвечает за предоставление основных системных служб, включая службу вызовов IPC, которая используется для связи между процессом рендеринга и основным процессом; службу выборки, которая обеспечивает возможности вызова внутреннего интерфейса; службу сеансов, в которой хранятся сеансы пользователей и записывает логин и другую информацию; сервис сокетов, обеспечивает соединение с внутренним сокетом; сервис fileSave, обеспечивает функцию сохранения файла.

  • Модуль Compile отвечает за выполнение компиляции кода DSL и реализацию построения многоплатформенного кода путем реализации нескольких компиляторов.

Дизайн процесса рендеринга

В процессе упаковки процесса рендеринга используется многостраничный дизайн упаковки, чтобы отделить некоторые страницы пользовательского интерфейса от процесса рендеринга и разбить их на несколько независимых новых окон (процессов рендеринга).Во время разработки горячие обновления модулей внедряются в каждое процесс рендеринга, код реализует горячее обновление страницы среды разработки. Добавьте несколько записей страниц в поле ввода Webpack, чтобы получить независимую упаковку, и каждая упакованная страница использует подключаемый модуль HtmlWebpackPlugin для создания соответствующего HTML-файла. Основной процесс создает отдельное окно для загрузки упакованной соответствующей страницы.index.htmlЗавершите создание нового окна.image-20200706094240656

Среди нескольких окон главное окно является основным окном системы. Реализованные модули и функции относительно сложны. Компоненты, разработанные с использованием React Hooks, не могут избежать взаимного общения. Поэтому Redux используется для глобального управления состоянием для оптимизации процесса взаимодействия между компонентами. .image-20200706094512545

В рабочем процессе Redux состояние извлекается в хранилище дерева состояний Redux для хранения черезdispatchдействие введитеreducerобновитьstate, после обновления состояния запустите рендеринг React для обновления представления. Ключевым моментом проектирования дерева состояний Redux является извлечение состояния компонента, извлечение состояния, от которого зависят несколько компонентов, в дерево состояний Redux и использование его в компоненте.useSelectorХуки подписываются на состояние в дереве состояний, используяuseDispatchПолучатьdispatchЧтобы обновить состояние в дереве состояний Redux.image-20200706094417266

В процессе рендеринга главного окна, включая модуль Redux, модуль страницы, модуль компонентов, модуль WebGL.

image-20200706094610522
  • Модуль страницы, страница главного окна похожа на одностраничное приложение, и каждая подстраница реализована на странице, включая подстраницу редактора пользовательского интерфейса, подстраницу редактора кода DSL и т. д.

  • Модуль Redux реализует базовое хранилище потока событий Redux, действие и редьюсер для связи между компонентами.

  • Модуль компонентов, реализация общих компонентов пользовательского интерфейса, таких как тосты, модальные и другие общие компоненты.

  • Модуль WebGL, который реализует холст пользовательского интерфейса и компоненты пользовательского интерфейса, а также некоторые связанные функции инструментов на основе собственного JavaScript WebGL.

Дизайн архитектуры Sever Side

Структуры фреймворка на стороне сервера с использованием Node.js Express, инкапсулированного Express на основе расширения.image-20200706094929896

  • Уровень Core — это инкапсуляция и расширение Express, включая реализациюAppДобрый,Middlewareабстрактный класс,Controllerабстрактные классы иdefineRouterДекораторы маршрутов и т.д.

  • Службы — это инкапсуляция базовых служб и вызов сторонних служб, таких как загрузка и загрузка файлов.

  • Socket — это абстракция службы Socket. Она предоставляет класс Socket для поддержки функции Socket сервера. Нижний уровень основан наSocketIOразработка.

  • Контроллер — это реализация конкретного контроллера бизнес-логики.Он использует класс для абстрагирования бизнеса и использует декоратор маршрута для оформления методов в классе для выражения бизнес-логики.

  • База данных обеспечивает абстракцию для соединений и операций с MySQL и MongoDB.

  • Модель предоставляет базовую модель таблиц базы данных, включаяUserповерхность,WebGLPageстол и т. д.

Сервер реализован на языке программирования Typescript, а во время выполнения согласноtsconfig.jsonчтобы запустить команду tsc, чтобы скомпилировать все файлы Typescript в JavaScript и запустить их в среде Node.js.

Дизайн базы данных

MongoDB — это база данных типа «ключ-значение» со структурой хранения, аналогичной JSON, имеет определенную иерархическую структуру и может хорошо отображать статус редактируемой страницы пользовательского интерфейса. Поэтому система использует эту функцию MongoDB для хранения информации о каждой редактируемой странице пользовательского интерфейса.Структура хранения выглядит следующим образом.image-20200706095702748

Дизайн синтаксиса DSL

DSL (доменный язык), то есть предметно-ориентированный язык, — это язык программирования, предназначенный для конкретной предметной области с ограниченной выразительностью, включая внутренний DSL и внешний DSL:

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

2. Внутренний DSLЭто специфическое грамматическое выражение языка программирования.Код, написанный на внутреннем DSL, является законной программой, но имеет специфический стиль и использует некоторые особенности языка программирования, которые используются только для решения некоторых специфических задач система.

Система использует внешнее определение DSL для описания страницы пользовательского интерфейса и анализирует DSL для создания объектного кода. Дизайн синтаксиса DSL относится к синтаксису SCSS и использует вложенную структуру для выражения вложенных отношений страниц пользовательского интерфейса. Абстракция атрибутов компонентов на странице пользовательского интерфейса определяется следующим синтаксисом DSL:

1. Выразите тип и имя компонента в форме Type.name, начиная с "{" и заканчивая "}", обертывая атрибуты и связанную информацию компонента.

2. Свойства компонента определяются в двух категориях: базовые свойства и свойства стиля.Ключевые слова базовых свойств включают положение, размер, текст и изображение.Свойства стиля определяются ключевыми словами стиля, заключенными в фигурные скобки.тень. Используйте «;», чтобы отделить свойства от свойств.

3. Параметры атрибута разделяются пробелами, а определение атрибута завершается знаком ";" в конце.

4. Используйте клавишу детей, чтобы выразить все подкомпоненты компонента, используйте «[» и «]», чтобы упаковать все подкомпоненты, а подкомпонентный код DSL разделен.

Простой компонент DSL определяется следующим образом.image-20200706095817348

Реализация функции

Реализация услуг, связанных с основным процессом

Клиентская часть разрабатывается в режиме разделения основного процесса и процесса рендеринга.Основной процесс реализует такие сервисы, как управление сессиями, подключение к сокету, вызов интерфейса сервера и постраничная коммуникация.

1. Реализация службы SessionОсновной процесс управляет сеансом глобально и хранит информацию для входа пользователя. Доступно в ЭлектронеsessionAPI для получения текущей сессии

export const Session = {
  setCookies(name: string, value: string) {
    const Session = session.defaultSession; // 主进程中获取默认session
    const Cookies = Session.cookies; // 获取cookies
    return Cookies.set({
      url: domain,
      name,
      value,
    });
  },
  getCookies(name: string | null = null) {
    const Session = session.defaultSession;
    const Cookies = Session.cookies;
    if (!name) return Cookies.get({ url: domain });
    return Cookies.get({ url: domain, name });
  },
  clearCookies(name: string) {
    const Session = session.defaultSession;
    return Session.cookies.remove(domain, name);
  }
};

2. Реализация подключения к сокету и инкапсуляцияиспользование сервераSocketIOВ библиотеке реализован сервис Socket, который также используется в основном процессе.SocketIOбиблиотека для установки соединения через сокет

class SocketService {
  static instance: SocketService | null = null;
  static getInstance() {
    return !SocketService.instance ? (SocketService.instance = new SocketService()) : SocketService.instance;
  }
  private socket: SocketIOClient.Socket;
  constructor() {
    this.socket = SocketIO(url);
    this.socket.on('connect', () => { // 连接
      console.log('connect !');
    })
  }
  emit(event: string, data: any) {
    if (!this.socket.connected) this.socket.connect();
    this.socket.emit(event, data);
  }
  on(event: string, callback: Function) {
    this.socket.on(event, callback)
  }
}

3. Реализация вызова сервера выборки и инкапсуляцияИспользование Node.js в основном процессеrequestМодуль реализует запрос интерфейса сервера, а процесс рендеринга используется косвенно через вызовы IPC.requestмодуль, а затем реализовать запрос интерфейса сервера

export const fetch = {
  get(url: string, data: any) {
    return fetch.handle('GET', url, data);
  },
  post(url: string, data: any) {
    return fetch.handle('POST', url, data);
  },
  handle(method: 'GET' | 'POST', url: string, data: any) { // 封装request模块
    return new Promise((resolve, reject) => {
      const params = {
        method,
        baseUrl,
        url,
        ...(method === 'GET' ? { qs: data } : { form: data })
      };
      request(params, (err, res, body) => {
        try {
          if (err) {
            reject(err);
            return;
          }
          resolve(JSON.parse(res.body));
        } catch (e) {
          reject(e);
        }
      });
    });
  }
};

4. Реализация и инкапсуляция межпроцессного взаимодействия IPCСвязь между процессом рендеринга и основным процессом является ядром всей системы.Разумное определение интерфейса связи может повысить эффективность работы системы. В основном процессе Electron обеспечиваетipcMainобъект для обработки сообщений из процесса рендеринга; в процессе рендеринга используйтеipcRendererОбрабатывать сообщения от основного процесса. Например, вызов IPC логики запроса сервера, основной процесс используетipcMain.handleРегистрация вызова IPC

export const handleFetch = () => {
  ipcMain.handle(IpcEvent.FETCH, async (event, args: { method: 'GET' | 'POST', url: string, data: any }) => {
    return await fetch.handle(args.method, args.url, args.data); // fetch
  });
};

визуализировать вызов процесса

function fetch(method: 'GET' | 'POST', url: string, data: any = null) {
  return ipcRenderer.invoke(IpcEvent.FETCH, {
    method,
    data,
    url
  }).catch(console.error);
}

// fetch('GET', '/user/login', { email, password });

5. Инкапсуляция логики компиляцииПроцесс рендеринга отправляет код DSL в основной процесс через вызов IPC, а основной процесс вызывает службу компиляции, чтобы завершить компиляцию кода и вернуть результат в процесс рендеринга. Как правило, синтаксический анализ кода DSL генерируется для абстрактного синтаксического дерева, а затем узел абстрактного синтаксического дерева модифицируется и, наконец, генерируется для целевого кода. Однако, учитывая, что разработанный DSL относительно прост, вам нужно использовать регулярные выражения только для анализа соответствующих атрибутов и объединения их в JSON.

parser.id_index = 0;
export function parser(str: string): any {
  let childrenMatch = str.match(/children\s*:\s*\[(.+)/);
  const childrenToken = childrenMatch ? childrenMatch[1].trim().replace(/\]\s*\}$/, '').trim() : '';
  if (childrenMatch) {
    str = str.substring(0, childrenMatch.index);
  }

  const children = getChildrenToken(childrenToken); // 子组件token

  let nameMatch = str.match(/^[\w\d\.\s]+\s*{/); // 解析组件type, name
  const [type, name] = nameMatch ? nameMatch[0].replace('{', '').trim().split('.') : ['', ''];
  let positionMatch = str.match(/position\s*:([^;]+);/); // 组件position属性
  const [x = 0, y = 0] = positionMatch ? positionMatch[1].trim().split(' ').map(v => Number.parseInt(v)) : [0, 0];

  let sizeMatch = str.match(/size\s*:([^;]+);/); // 组件size 属性
  const [width = 0, height = 0] = sizeMatch ? sizeMatch[1].trim().split(' ').map(v => Number.parseInt(v)) : [0, 0];

  let backgroundMatch = str.match(/background\s*:([^;]+);/); // 组件background属性
  const [fill = 'white', opacity = 0] = backgroundMatch ? backgroundMatch[1].trim().split(' ') : ['', ''];

  let shadowMatch = str.match(/shadow\s*:([^;]+);/); // 组件shadow属性
  let [offsetX = 0, offsetY = 0, blur = 0, shadowFill = 'white'] = shadowMatch ? shadowMatch[1].trim().split(' ').map((v, i) => {
    if (i === 3) return v;
    return Number.parseInt(v);
  }) : [0, 0, 0, ''];

  let borderMatch = str.match(/border\s*:([^;]+);/); // 组件border属性
  const [borderWidth = 0, radius = 0, borderFill = 'white'] = borderMatch ? borderMatch[1].trim().split(' ').map((v, i) => {
    if (i === 2) return v;
    return Number.parseInt(v);
  }) : [0, 0, ''];


  let textMatch = str.match(/text\s*:([^;]+);/); // 组件text属性
  const textMatchRes = textMatch ? textMatch[1].trim() : '';
  let text = textMatchRes.match(/'(.+)'/);
  if (text) {
    text = (text[0] as any).replace(/^'/, '').replace(/'$/, '');
  }
  let textFill = textMatchRes.split(' ');
  textFill = (textFill[textFill.length - 1] as any).trim();

  let imageMatch = str.match(/image\s*:([^;]+);/); // 组件image属性
  const src = imageMatch ? imageMatch[1].trim().replace(/^'/, '').replace(/'$/, '') : '';
  return { // 拼接JSON
    name,
    type: type.toLocaleUpperCase(),
    id: `${type.toLocaleUpperCase()}-${name}-${parser.id_index++}`,
    props: {
      position: { x , y },
      size: { width, height },
      ...(backgroundMatch ? { background: { fill, opacity: +opacity } } : {}),
      ...(shadowMatch ? {
        shadow: {
          offsetY,
          offsetX,
          blur,
          fill: shadowFill
        }
      } : {}),
      ...(borderMatch ? {
        border: {
          width: borderWidth,
          radius: radius,
          fill: borderFill
        }
      } : {}),
      ...(textMatch ? { text: { text, fill: textFill } } : {}),
      ...(imageMatch ? { image: { src } } : {})
    },
    children: children.map(str => parser(str)) // 递归解析子组件token
  };
}
// 计算子组件token
function getChildrenToken(childrenToken: string) {
  let count = 0;
  let child = '';
  const result = [];
  for (let i = 0; i < childrenToken.length; i++) {
    child += childrenToken[i];
    if (childrenToken[i] === '{') {
      count++;
    }
    if (childrenToken[i] === '}') {
      count--;
    }
    if ((childrenToken[i] === ',' && count === 0) || (count === 0 && i === childrenToken.length - 1)) {
      result.push(child.replace(/,$/, '').trim());
      child = '';
    }
  }
  return result;
}

Процесс создания целевого кода представляет собой условное суждение, основанное на типе компонента объекта JSON.

function compileToElementToken(obj: any): any {
  switch (obj.type) {
    case TYPES.WIDGET: { // 
      return (`<div id="${obj.id}">${obj.children.map((v: any) => compileToElementToken(v)).join('\n')}</div>`);
    }
    case TYPES.BUTTON: {
      return (`<button id="${obj.id}">${obj.props.text ? obj.props.text.text : ''}</button>`);
    }
    case TYPES.SHAPE: {
      return (`<div id="${obj.id}">${obj.children.map((v: any) => compileToElementToken(v)).join('\n')}</div>`);
    }
    case TYPES.TEXT: {
      return (`<div id="${obj.id}">${obj.props.text ? obj.props.text.text : ''}</div>`);
    }
    case TYPES.INPUT: {
      return (`<input id="${obj.id}" placeholder="some text"/>`);
    }
    case TYPES.IMAGE: {
      return (`<img id="${obj.id}" src="${obj.props.image ? obj.props.image.src : ''}" alt="none"/>`);
    }
  }
}

Наконец вставлен в целевой код

const jsonObject = compileToJson(code);
let style = (`
* { box-sizing: border-box; margin: 0; padding: 0 }
html, body { height: 100%; width: 100% }
${compileToStyleToken(jsonObject)}`).replace(/\n(\n)*(\s)*(\n)*\n/g, '\n');
let div = compileToElementToken(jsonObject).replace(/\n(\n)*(\s)*(\n)*\n/g, '\n');
const html = (`<!DOCTYPE>
<html lang="zh">
<head><title>auto ui</title></head>
<style>${style}</style>
<body>${div}</body> 
</html>`);

Многооконное управление основным процессом

Приложение на стороне клиента состоит из нескольких окон, таких как окно информации о пользователе, главное окно, окно входа в систему и окно выбора аватара.Каждое окно представляет собой независимый процесс рендеринга, и основной процесс отвечает за управление всеми окнами. Сам Electron не обеспечивает многооконного управления, поэтому необходимо вручную управлять состоянием каждого окна, логикой взаимодействия между окнами и т.д.

Каждое окно в приложении абстрагировано в класс виджета, особенность окна, каждый из класс виджета основан на модели для создания одного варианта осуществления.image-20200706100823770

отецWidgetвыполнитьIWidgetИнтерфейс для реализации основных функций окна, таких какcreate()создать окно,close()закрыть окно и т.д. Его подкласс представляет собой одноэлементный класс, использующий статические методы.getInstance()получить. Каждое окно является рамочным, то есть украшение строки состояния операционной системы удалено, поэтому вам нужно вручную закрыть, свернуть, развернуть окно и перетащить окно. Для перетаскивания окна в Electron вы можете использовать-webkit-app-region: dragОдна строка свойств CSS для реализации. Для закрытия, минимизации и максимизации окон это реализовано путем вызова зарегистрированных вызовов IPC для закрытия, минимизации и максимизации окон в процессе рендеринга.

WidgetКатегорияcreate()метод является ключевым способом создания окна, используйтеElectron.BrowserWindowдля создания экземпляра окна и использования экземпляра объектаloadURL()илиloadFile()загрузить.htmlФайл отображает страницу и регистрирует соответствующее событие.

// DSL代码预览窗口
export default class CodeWidget extends Widget {
  static instance: CodeWidget | null = null;
  static getInstance() {
    return CodeWidget.instance ? CodeWidget.instance : (CodeWidget.instance = new CodeWidget());
  }

  constructor() {
    super();
    // 窗口关闭事件
    onCloseWidget((event, args: { name: string }) => {
      if (args.name === WidgetType.CODE) {
        if (this._widget) {
          this._widget.close();
        }
      }
    });
  }

  create(parent?: Electron.BrowserWindow, data?: any): void {
    if (this._widget) return;
    // 实例化窗口
    this._widget = new Electron.BrowserWindow({
      ...CustomWindowConfig,
      parent,
      width: 550,
      height: 600,
      resizable: false,
      minimizable: false,
      maximizable: false
    });
    //加载.html文件
    loadHtmlByName(this._widget, WidgetType.CODE);
    // 初始数据
    if (data) {
      this._widget.webContents.on('did-finish-load', () => {
        this._widget?.webContents.send('code', data);
      });
    }
    parent?.on('close', () => this.reset());
    this._widget.on('close', () => this.reset());
  }
}

Нельзя избежать связи между несколькими окнами, такой как связь между окном выбора аватара и окном информации о пользователе. В окне информации о пользователе нажмите Изменить аватар, чтобы открыть окно выбора аватара.После выбора аватара в окне выбора аватара нужно отправить результат выбора в окно информации о пользователе.image-20200706102141623

Самый простой способ связи между окнами — использоватьipcMainобъект иipcRendererОбъект для достижения, то есть сообщение отправляется основному процессу в процессе рендеринга одного окна, а основной процесс отправляет сообщение в процесс рендеринга другого окна для реализации связи между двумя окнами.image-20200706102321896

Но в этом режиме реализации необходимо дополнительно определить имя события и использовать основной процесс для реализации связи между двумя окнами. Поэтому Электрон обеспечивает более удобныйremoteМодули, которые могут обмениваться данными без отправки межпроцессных сообщений. ЭлектронremoteМодуль похож на RMI (Remote Method Invoke) в Java, механизм связи, который использует удаленные объекты для вызова друг друга для обеспечения связи между двумя сторонами. Соответствует окну со структурой родитель-потомок, его нужно использовать только в дочернем окне при общенииremoteМетод может отправить сообщение процессу рендеринга в родительском окне.

remote.getCurrentWindow().getParentWindow().webContents.send('avatar-data', { ...avatar });

Общий принцип механизма удаленной связи заключается в следующем.

image-20200706102513874

Реализация холста пользовательского интерфейса на стороне клиента

Холст пользовательского интерфейса является одним из ядер системы и реализован на основе фреймворка WebGL Konva.

1. Реализация холста пользовательского интерфейсаПри реализации холста с Konva просто используйтеKonva.StageОпределите этап и используйтеKonva.LayerОпределить слой чертежа

this.renderer = new Konva.Stage({
  container: container.id,
  width: CANVAS_WIDTH,
  height: CANVAS_HEIGHT
});
// 管理画布中的所有组件
this.componentsManager = new ComponentManager();
this.layer = new Konva.Layer();
// Redux dispatch,webgl与react通信的核心
this.dispatch = dispatch;
// 像画布中添加辅助线
WebGLEditorUtils.addGuidesLineForLayer(this.layer, this.renderer);
this.renderer.add(this.layer);

2. Добавьте компонент на холст пользовательского интерфейсаПри добавлении компонентов пользовательского интерфейса на холст пользовательского интерфейса сначала привяжите события в Konva к компонентам, включая такие события, как выбор, перетаскивание и изменение размера;Layerслоя; затем скрыть предыдущую точку привязки компонента и отобразить точку привязки перетаскиваемого компонента; определить, находится ли перетаскиваемый компонент в компоненте, если он находится в компоненте, добавить перетаскиваемый компонент к компоненту Внутренне, сформировать вложенный структура; уведомлять о звонкахdispatch, уведомить сторону React, сохранить состояние текущего компонента и, наконец, перекрасить холст.

addComponent(webGLComponent: WebGLComponent) {
  // 为组件添加事件
  this.addSomeEventForComponent(webGLComponent);
  // 将组件添加到绘制层
  webGLComponent.appendToLayer(this.layer);
  this.componentsManager.pushComponent(webGLComponent);
  // 检测拖入的组件是否位于某个组件内部
  const id = WebGLEditorUtils.checkInSomeGroup(
    this.layer,
    this.renderer,
    webGLComponent.getGroup()
  );

  if (id) {
    // 如果在则添加到对应的组件内部
    this.componentsManager.appendComponentById(id, webGLComponent);
  }
  // 通知react侧
  this.dispatch(selectComponent(
    webGLComponent.getId(),
    webGLComponent.getType(),
    webGLComponent.getName(),
    this.componentsManager.getPathOfComponent(webGLComponent).join('>'),
    getComponentProps(webGLComponent)
  ));
  // 重绘画布
  this.render();
}

соответствующийaddSomeEventForComponent()Функция реализована следующим образом, в основном добавление выбранных событий, перетаскивание событий и изменение событий.

addSomeEventForComponent(component: WebGLComponent) {
  component.onSelected(e => { // 组件选中事件
    this.componentsManager.showCurrentComponentTransformer(
      component.getId()
    );
    component.moveToTop();
    this.dispatch(selectComponent(
      component.getId(),
      component.getType(),
      component.getName(),
      this.componentsManager.getPathOfComponent(component).join('>'),
      getComponentProps(component)
    ));
    this.render();
  });

  component.onDragEnd(e => { // 组件拖拽结束事件
    this.dispatch(dragComponent(e.target.position()));
  });

  component.onTransformEnd(e => { // 组件transform结束事件
    this.dispatch(transformComponent(component.getSize()));
  })

  component.onDragEnd(e => { // 组件拖拽结束事件
    const id = WebGLEditorUtils.checkInSomeGroup(
      this.layer,
      this.renderer,
      component.getGroup()
    );


    if (id) {
      this.componentsManager.appendComponentById(id, component);
    }
    this.render();
  });
}

3. Обнаружить, находится ли компонент внутри компонента в холстеВ конце события перетаскивания компонента необходимо определить, находится ли перетаскиваемый компонент внутри компонента, и переместить его в соответствующий целевой компонент, чтобы сформировать вложенную структуру. Сначала получите информацию о координатах и ​​размерах всех компонентов на холсте, кроме перетаскиваемого компонента, и используйте{id, w, h, x, y}форматировать в массивpoints; затем получите координаты и информацию о размере перетаскиваемого компонента, обозначенного какgroupPoint, в формате{id, w, h, x, y}; траверсpointsМассив, определите элементы, которые могут содержать компоненты перетаскивания, и добавьте их вincludePointsВ массиве код такой:

const points = getAllGroupPoints();
const groupPoint = getGroupPoint(group);
const includePoints: PointType[] = [];
points.forEach(point => {
  if (
    groupPoint.x >= point.x &&
    groupPoint.y >= point.y &&
    groupPoint.x + groupPoint.w <= point.x + point.w &&
    groupPoint.y + groupPoint.h <= point.y + point.h
  ) {
    includePoints.push(point);
  }
});

траверсincludePointsДля всех элементов в массиве выберите компонент с наименьшим расстоянием от перетаскиваемого компонента в качестве родительского компонента в соответствии с евклидовым расстоянием.image-20200706103419916

Поток алгоритма для определения того, находится ли компонент внутри компонента, выглядит следующим образом.

let minDistance = Number.MAX_SAFE_INTEGER;
let id = '';
const distance = (p0: { x: number, y: number }, p1: { x: number, y: number }) => {
  return Math.sqrt(Math.pow(p0.x - p1.x, 2) + Math.pow(p0.y - p1.y, 2));
};
includePoints.forEach(point => {
  const diff =
        distance(
          { x: groupPoint.x, y: groupPoint.y },
          { x: point.x, y: point.y }
        ) +
        distance(
          { x: groupPoint.x + groupPoint.w, y: groupPoint.y },
          { x: point.x + point.w, y: point.y }
        ) +
        distance(
          { x: groupPoint.x, y: groupPoint.y + groupPoint.h },
          { x: point.x, y: point.y + point.h }
        ) +
        distance(
          { x: groupPoint.x + groupPoint.w, y: groupPoint.y + groupPoint.h },
          { x: point.x + point.w, y: point.y + point.h }
        );
  if (diff < minDistance) {
    minDistance = diff;
    id = point.id;
  }
});

4. WebGL взаимодействует с ReactХолст, отрисовываемый WebGL, отделен от DOM браузера, а элементы внутри отрисовываются построчно, что отличается от DOM. Связь между WebGL и React реализована с использованием глобального дерева состояний, предоставляемого Redux. Функция диспетчеризации передается при построении холста WebGL, чтобы инициировать изменения в глобальном дереве состояний для уведомления React.image-20200706103808959

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

// 拖动
export function drag(type: string, name: string, event: DragEvent<any>) {
  event.dataTransfer?.setData('component', JSON.stringify({type, name}));
}
// 放下
export function drop(callback: Function, event: DragEvent<any>) {
  event.preventDefault();
  const { type, name } = JSON.parse(event.dataTransfer?.getData('component'));
  callback({
    type,
    name,
    position: {
      clientX: event.clientX,
      clientY: event.clientY
    }
  });
}

Проанализируйте тип и имя перетаскиваемого компонента, а холст пользовательского интерфейса создаст экземпляр объекта компонента в соответствии с типом и именем и добавит его на холст.

export function dropComponentToWebGLEditor(type: string, name: string, position: { x: number, y: number }, editor: CanvasEditorRenderer) {
  const cpn = new (ComponentMap as any)[type][name](position); // 根据type和name实例化对应的组件
  editor.addComponent(cpn);
  return cpn;
}

Реализация компонента пользовательского интерфейса на стороне клиента

Компоненты пользовательского интерфейса по-прежнему реализуются с использованием фреймворка WebGL Konva и инкапсулируются в виде класса Typescript.image-20200706122833576

IWebGLComponentPropsИнтерфейс абстрагирует доступные свойства компонента и методы получения и установки свойств, такие как получение и установка свойств местоположения, получение и установка свойств фона и т. д.IWebGLComponentEventsИнтерфейс абстрагирует события, которые должен связать компонент, такие как события перетаскивания, события выбора и т. д.WebGLComponentКласс, который инкапсулирует базовую структуру компонентов WebGL, например, описание иерархии компонентов.childrenа такжеparentсвойство для добавления компонента на холстappendToLayer()методы и т. д., и реализоватьIWebGLComponentProps()Интерфейс, который определяет свойства компонента WebGL, реализующегоIWebGLComponentEventsИнтерфейс, который определяет события, которые должен прослушивать компонент. Каждый компонент наследуетWebGLComponentРодительский класс для реализации, напримерWebGLRectДобрый,WebGLTextДобрый.

путем определенияWebGLComponentРодительский класс реализует общую логику компонента.Основой компонента являетсяgroupа такжеtransformer, которые представляют собой группу фигур и свободно трансформируемые опорные точки, отображаемые на холсте WebGL.

1. Нарисуйте компоненты пользовательского интерфейсаКомпонент пользовательского интерфейса состоит из нескольких графических элементов Konva, таких как компонент кнопки, состоящий из прямоугольника (Konva.Rect) и текст (Konva.Text)сочинение. ЧерезgroupДобавьте несколько фигур, чтобы нарисовать компонент.image-20200706123205939

2. Удалите компонентыЧтобы удалить компонент, вам нужно всего лишь последовательно удалить три части, то есть удалить текущий компонент из родительского компонента и удалить компонент из холста.group, который удаляет компонентtransformer

removeFromLayer() {  
  this.parent?.removeChild(this.getId());
  this.getGroup().remove();
  this.getTransformer().remove();
}

3. Добавьте дочерние компоненты к родительским компонентамДля добавления компонента к другому требуется толькоgroupа такжеtransformerПросто переместите его в родительский компонент и используйте в дочернем компоненте.parentОтносится к родительскому компоненту, который используется в родительском компоненте.childrenСохраняет ссылки на все дочерние компоненты.image-20200706123505999

Поэтому при добавлении дочерних компонентов необходимо установить иерархическую связь между родительскими и дочерними компонентами.

appendComponent(component: WebGLComponent) {
  if (!this.isRawComponent) {
    const group = component.getGroup();
    const transformer = component.getTransformer();
    group.moveTo(this.getGroup()); // 移动到父组件中
    transformer.moveTo(this.getGroup()); // 移动到父组件中

    if (component.parent) { // 移除子组件原来的父组件
      component.parent.removeChild(component.getId());
    }
    component.parent = this; // 重新指向父组件
    this.appendChild(component);
  }
}

Взаимное преобразование между страницей пользовательского интерфейса на стороне клиента и JSON

Сервер использует MongoDB для хранения отредактированной страницы пользовательского интерфейса, поэтому ему необходимо реализовать преобразование страницы пользовательского интерфейса в JSON и преобразование объекта JSON в страницу пользовательского интерфейса.

1. Преобразование страницы пользовательского интерфейса и объекта JSONПерейдите от корневого компонента, извлеките свойства, такие как тип, имя, подкомпонент, стиль и т. д., а затем рекурсивно проанализируйте подкомпонент.

export function webGLComponentToJsonObject(component: WebGLComponent): TRawComponent {
  return {
    id: component.getId(),
    type: component.getType(),
    name: component.getName(),
    props: getComponentProps(component),
    children: component.getChildren().size ?
      [...component.getChildren().values()].map(value => {
        return webGLComponentToJsonObject(value);
      }) : []
  };
}

2. Преобразование JSON в страницу пользовательского интерфейсаИспользуйте поиск в ширину для обхода объектов JSON, создания экземпляров родительских компонентов и соответствующих дочерних компонентов по очереди, установки свойств компонентов, добавления дочерних компонентов к родительским компонентам, записи корневого узла, определения того, сгенерирован ли он в форме вставки, и добавления его к середине холста

export function drawComponentFromJsonObject(jsonObject: TRawComponent, renderer: CanvasEditorRenderer, isPaste = false): WebGLComponent {
  let root: WebGLComponent | null = null; // 记录根节点
  const queue = [jsonObject]; // 广度优先搜索队列
  const map = new Map<string, WebGLComponent>(); // 记录当前组件是否实例化

  while (queue.length) { // 广度优先搜索
    const front = queue.shift() as TRawComponent; // 记录父节点
    let parent;
    if (map.has(front.id)) { // 如果父组件实例过,则直接拿到实例化的引用
      parent = map.get(front.id);
    } else { // 未实例化,则对组件进行实例化,并记录到map中
      parent = new (ComponentMap as any)[front.type][front.name](
        front.props.position
      ) as WebGLComponent;
      setComponentProps(parent, front.props); // 设置属性
      map.set(front.id, parent);
    }

    if (root === null) { // 获取根节点
      root = parent as WebGLComponent;
      renderer.addRootComponent(root as WebGLComponent); // 将根节点绘制到UI画布中
    }

    for (let v of front.children) { // 遍历子组件
      queue.push(v);
      const child = new (ComponentMap as any)[v.type][v.name](v.props.position, v.props.size) as WebGLComponent;
      setComponentProps(child, v.props);
      renderer.addComponentForParent(parent as WebGLComponent, child); // 绘制到父组件中
      map.set(v.id, child);
    }
  }
  const component = root as WebGLComponent;
  // 是否以粘贴的形式
  isPaste && component.setPosition({
    x: component.getPosition().x + 10,
    y: component.getPosition().y + 10
  });
  renderer.getComponentManager().showCurrentComponentTransformer(
    root?.getId() as string
  );
  renderer.render(); // 重新渲染UI画布
  return component;
}

Реализация функции редактирования компонента пользовательского интерфейса на стороне клиента

Связь между React и WebGL основана на дереве состояний Redux путем вызова на стороне WebGL.dispatch()Чтобы уведомить React о необходимости рендеринга, используйте хуки useEffect для связи при рендеринге компонентов React Editor.

Для реализации функции редактирования необходимо записать состояние редактирования в дереве штата Redux.state, в формате{id, editType }idпредставляет идентификатор компонента,editTypeУказывает тип редактирования.

Вызывается при нажатии операции редактированияdispatch()Функция Send Edit Component ID и Edit Type, компоненты React Editor используют Useeffect Hooks для получения изменений и использованияCanvasEditorRendererМетод компонента редактирования, предоставляемый классом, реализует функцию редактирования компонента.

const editToolsDeps = [editToolsState.id, editToolsState.editType];
useEffect(() => {
  if (editToolsState.id) {
    const renderer = (webglEditor.current as CanvasEditorRenderer);
    switch (editToolsState.editType) {
      case 'delete': { // 删除组件
        // 移除画布中对应Id的组件
        const rmCpn = removeComponentFromWebGLEditor(editToolsState.id, renderer);
        EventEmitter.emit('auto-save', webGLPageState.pageId); // 自动保存
        // 新增编辑历史
        dispatch(addEditHistory(editToolsState.id, 'delete', {
          old: '',
          new: webGLComponentToJsonObject(rmCpn as WebGLComponent)
        }));
        return;
      }
      case 'paste': { // 粘贴组件
        const newCpn = pasteComponentToWebGLEditor(editToolsState.id, renderer);
        // 新增编辑历史
        dispatch(addEditHistory(editToolsState.id, 'paste', { old: '', new: newCpn?.getId() }));
        EventEmitter.emit('auto-save', webGLPageState.pageId);
        return;
      }
      case 'save': { // 保存
        savePage(webGLPageState.pageId, renderer.toJsonObject() as object).then((v: any) => {
          if (!v.err) {
            toast('save!');
            dispatch(resetComponent());
          }
        });
        return;
      }
      case 'undo': { // 重做
        dispatch(removeEditHistory());
        dispatch(resetComponent());
        return;
      }
      default: {
        return;
      }
    }
  }
}, editToolsDeps);

1. Удалить компонент с холстаКогда компонент на холсте пользовательского интерфейса выбран, выбранный идентификатор компонента будет сохранен в дереве состояний Redux.Через идентификатор компонента вызовитеCanvasEditorRendererКласс удаляет метод, соответствующий идентификатору, и его внутренняя реализация выглядит следующим образом.

const cpn = this.componentsManager.getComponentById(id);
this.componentsManager.removeComponentById(id);
this.render();
this.dispatch(resetComponent());
return cpn;

2. Скопируйте и вставьте компонентПри копировании компонента запишите идентификатор компонента, а при вставке найдите компонент, соответствующий идентификатору, преобразуйте его в объект JSON, а затем восстановите компонент пользовательского интерфейса из объекта JSON и добавьте его на холст пользовательского интерфейса для реализации вставки. логика

if (this.webGLComponentCollection.has(id)) {
  const cpn = this.webGLComponentCollection.get(id) as WebGLComponent;
  const json = webGLComponentToJsonObject(cpn); // 转化到JSON对象
  return drawComponentFromJsonObject(json, renderer, true); // 再由JSON对象生成新的组件
}
return null;

3. Повторить компонентыЛогика компонента Redo реализована путем записи истории редактирования. История редактирования хранится в массиве При наличии операции редактирования операция сохраняется в массиве Формат хранения{id, operator, data},idпредставляет идентификатор компонента,operatorПредставляет имя операции, а данные представляют данные, необходимые для операции, обратной операции оператора. При выполнении команды повтора выньте последний элемент массива и выполните операцию, обратную соответствующей операции, для достижения эффекта повтора.

Возьмите операцию вставки компонента в качестве примера, вставьте компонент и добавьте операцию вставки в массив.

dispatch(addEditHistory(editToolsState.id, 'paste', { old: '', new: newCpn?.getId() }));

Обратная операция операции вставки компонента заключается в удалении компонента, поэтому получите идентификатор вставленного компонента в данных и удалите его из редактора пользовательского интерфейса, чтобы добиться эффекта повторного выполнения.

const { id, data } = editHistory.current;
renderer.removeComponent(data.new);

Реализация функции модификации свойств UI компонентов на стороне клиента

Абстрагируя свойства стиля компонентов WebGL, абстрагируются пять типов свойств: свойство фона, свойство границы, свойство тени, свойство текста и свойство изображения. При изменении свойств компонента сначала определите тип измененных свойств, а затем измените и отобразите свойства этого типа на холсте пользовательского интерфейса. При изменении свойств панель свойствPropspanelкомпоненты проходятdispatch()Измените состояние дерева состояний Redux, затем перерисуйте пользовательский интерфейс. Компонент UIEditor прослушивает изменения состояния с помощью побочного эффекта useEffect и вызываетCanvasEditorRendererКатегорияmodifyComponentPropsМетод реализует модификацию свойств компонента.image-20200706125031944

Суммировать

Этот проект является моим дипломным проектом. С платформой Кубика Рубика я познакомился во время стажировки в ByteDance. Реализация редактора пользовательского интерфейса платформы Кубика Рубика основана на технологии DOM. По сравнению с редактором пользовательского интерфейса, реализованным программой проектирования Figma используя WebGL, проект также пытался использовать WebGL для реализации редактора пользовательского интерфейса и создания его как приложения.

недостатки
  • Реализовать компоненты пользовательского интерфейса в WebGL сложно, в настоящее время реализовано не так много доступных компонентов пользовательского интерфейса, поэтому невозможно редактировать какой-либо пользовательский интерфейс.

  • Код DSL компилирует целевой код, а целевой код имеет плохую читабельность.

  • Упакованный пакет приложения слишком велик и т. д.

план на будущее
  • Он будет изучать, как использовать компьютерное зрение, алгоритмы машинного обучения и т. Д., Чтобы идентифицировать проекты дизайна пользовательского интерфейса и преобразовывать их в представление DSL системы, чтобы компилировать в целевой код.

  • Узнайте, как анализировать файл PSD и преобразовывать PSD в представление DSL для компиляции в объектный код.

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

Время летит незаметно, и четырехлетняя университетская жизнь подходит к концу. За четыре года обучения в колледже некоторые люди предпочитают чувствовать себя комфортно, другие предпочитают сдаться, но я предпочитаю продолжать усердно учиться, не оставляя ни о чем сожалений. За четыре года непрерывного обучения я превратился из компьютерного новичка в технического эксперта, поступил на стажировку в ByteDance и рекомендовал аспирантов с отличными оценками. "Бог вознаграждает усердие", учиться совсем не просто. За четыре года учебы в университете слишком много удач и слез, и я очень благодарен себе за упорство. «Изучая широкий спектр знаний и учась на них, накапливая много и получая небольшую прибыль», накопление знаний за четыре года учебы позволило мне достичь сегодняшних результатов. В жизни еще далеко, выпускной – это не конец, и нужно еще продолжать усердно работать в аспирантуре или работать в будущем.Поздравляю с выпуском! ! !

Ссылаться на

Практика разработки Electron в Taro IDE

Поделитесь полугодовым опытом разработки и оптимизации приложений Electron

Konvajs.Konva Tutorials

Адрес проекта на гитхабе: GitHub.com/солнечные часы-электрические…