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

🎯 Что такое без кода?

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

По сути, в No-Code мы можем разделить два слоя:

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

В этом уроке мы сосредоточимся на рабочих процессах. Рабочий процесс — это широкое понятие, но в данном случае мы хотим использовать его для определения нашего приложения без кода. Мы хотим дать конечному пользователю возможность создать какой-то логический поток, например прочитать какое-то значение, сделать что-то, если значение равно 1 и т. д. Нам нужен простой графический пользовательский интерфейс, чтобы определить такой поток. Для этого воспользуемся пакетом Sequential Workflow Designer.

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

Вернемся к концепции рабочего процесса. Рабочий процесс содержит определение потока. Здесь важно одно: поток не должен быть линейным. Он может содержать некоторые условные операторы или циклы. Наименьшую часть здесь мы назвали ступенькой. Шаг представляет собой одну инструкцию или одну задачу для выполнения. Конечно, условия или циклы тоже являются шагами.

Здесь мы подготовим два простых шага и создадим простой исполнитель рабочего процесса.

  • readUserInput — чтение пользовательского ввода в буфер.
  • sendEmail — отправляет электронное письмо, если буфер равен некоторому значению.

👓 Дизайнер последовательного рабочего процесса

Чтобы добавить пакет в свой проект, вы можете добавить приведенный ниже код в свой раздел <head>.

<link href="https://cdn.jsdelivr.net/npm/[email protected]/css/designer.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/[email protected]/css/designer-light.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/[email protected]/css/designer-dark.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/designer.js"></script>

Если вы используете npm в своем проекте, вы можете добавить пакет с помощью npm:

npm i sequential-workflow-designer

🐾 Шаги

Шаги в дизайнере задаются в формате JSON. Почему JSON? Потому что его легко хранить где угодно (база данных, файл и т.д.).

В этом уроке мы будем использовать только task шагов. Но конструктор поддерживает больше типов (container, switch). Компонент задачи отображается в виде одного прямоугольника. Он не содержит детей. У него есть имя, иконка, один вход и один выход.

Посмотрите на приведенный ниже JSON, это определение шага задачи.

{
    "id": "unique_id",
    "componentType": "task",
    "type": "my_type",
    "name": "my_name",
    "properties": {
        "setting1": "value1",
        "setting2": "value2"
    }
}
  • id — уникальный идентификатор шага. Этот идентификатор относится к размещенным шагам в дизайнере. Если у нас есть 2 шага, у нас есть два уникальных идентификатора.
  • type — тип задачи, в этом уроке у нас есть два типа: readUserInput и sendEmail,
  • название — отображается в прямоугольнике,
  • свойства — здесь находятся все настройки шага, этот список зависит от типа шага.

Как я упоминал выше, мы хотим создать 2 простых шага.

Первый шаг — readUserInput. Мы хотим, чтобы пользователь мог изменить сообщение вопроса. Итак, мы определим одно свойство question.

{
    "componentType": "task",
    "type": "readUserInput",
    "name": "Read User Input",
    "properties": {
        "question": "Ask me"
    }
}

Следующий шаг — sendEmail. Здесь мы хотим позволить пользователю определить email и добавить условие ifReqisterEquals, которое включает доставку по электронной почте, если пользователь на последнем шаге readUserInput ввел определенное значение. По сути, я ввожу здесь простое условие.

{
    "componentType": "task",
    "type": "sendEmail",
    "name": "Send E-mail",
    "properties": {
        "ifReqisterEquals": "1",
        "email": "[email protected]"
    }
}

🔨 Набор инструментов

Панель инструментов — это элемент дизайнера, который позволяет перетаскивать шаги в процесс проектирования. Теперь мы готовим определение, которое инструментарий будет использовать для рендеринга шагов.

const toolboxConfig = {
    groups: [
        {
            name: 'Steps',
            steps: [
                {
                    componentType: 'task',
                    type: 'readUserInput',
                    name: 'Read User Input',
                    properties: {
                        question: 'Ask me'
                    }
                },
                {
                    componentType: 'task',
                    type: 'sendEmail',
                    name: 'Send E-mail',
                    properties: {
                        ifReqisterEquals: '1',
                        email: '[email protected]'
                    }
                }
            ]
        }
    ]
};

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

🐝 Иконки

Мы можем настроить значок для определенных componentType и type шага. Для этого нам нужно создать конфигурацию.

const stepsConfig = {
    iconUrlProvider: (componentType, type) => {
        return `./assets/icon-task.svg`
    },

    validator: (step) => true
};

В этом уроке мы используем только одну иконку для всех типов шагов. Но вы можете использовать аргументы componentType и type для предоставления определенных значков.

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

📇 Редакторы

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

const editorConfig = {
    globalEditorProvider: (definition) => {
        const editor = document.createElement('div');
        editor.innerText = 'Select a step.';
        return editor;
    },
    stepEditorProvider: (step) => {
        // create a step's editor here and return it
    }
}

Основная проблема здесь в том, что у нас может быть много типов шагов (в этом уроке их 2). Это означает, что для каждого шага требуется собственный редактор, потому что каждый шаг содержит разные настройки.

Здесь у нас есть базовый редактор шага readUserInput.

function createEditorForReadUserInput(step) {
    const editor = document.createElement('div');
    const label = document.createElement('label');
    label.innerText = 'Question';
    const input = document.createElement('input');
    input.setAttribute('type', 'text');
    input.value = step.properties['question'];
    input.addEventListener('input', () => {
        step.properties['question'] = input.value;
    });

    editor.appendChild(label);
    editor.appendChild(input);
    return editor;
}

А вот редактор шага sendEmail.

function createEditorForSendEmail(step) {
    const editor = document.createElement('div');
    const propNames = ['ifReqisterEquals', 'email'];
    for (let propName of propNames) {
        const label = document.createElement('label');
        label.innerText = propName;
        const input = document.createElement('input');
        input.setAttribute('type', 'text');
        input.value = step.properties[propName];
        input.addEventListener('input', () => {
            step.properties[propName] = input.value;
        });

        editor.appendChild(label);
        editor.appendChild(input);
    }
    return editor;
}

Как видите, эти редакторы обновляют step.properties после изменения пользователем.

У нас есть два генератора, но нам нужно объединить их в свойство stepEditorProvider.

stepEditorProvider: (step) => {
  if (step.type === 'readUserInput')
    return createEditorForReadUserInput(step);
  if (step.type === 'sendEmail')
    return createEditorForSendEmail(step);
  throw new Error('Not supported');
}

📐 Создать конструктор

Теперь мы можем создать конструктор.

const config = {
    toolbox: toolboxConfig,
    steps: stepsConfig,
    editors: editorConfig
};

Мы можем предоставить начальное определение рабочего процесса. Но в этом уроке мы этого делать не будем.

const startDefinition = {
    properties: {},
    sequence: []
};

Вот и все. Создадим конструктор.

const placeholder = document.getElementById('designer');
const designer = sequentialWorkflowDesigner.create(
    placeholder, 
    startDefinition, 
    config);

Теперь мы должны увидеть конструктор на экране.

🚀 Исполнение

Последняя часть — казнь. Здесь мы хотим выполнить спроектированный поток.

Нам нужно подготовить код для шага readUserInput. Этот шаг очень прост. Спрашиваем пользователя и сохраняем ответ. Вопрос определяется в свойстве question.

let register = null;

if (step.type === 'readUserInput') {
   register = prompt(step.properties['question']);
}

Шаг sendEmail немного сложнее. Здесь мы отправляем электронное письмо, если ранее выбранное значение от пользователя равно некоторому значению. Значение определяется в свойстве ifReqisterEquals.

if (step.type === 'sendEmail') {
  if (step.properties['ifReqisterEquals'] === register) {
     alert(`E-mail sent to ${step.properties['email']}`);
  }
}

Но как мы можем прочитать определение рабочего процесса? Это просто.

const definition = designer.getDefinition();

Вы можете проверить конечное состояние здесь.

function runWorkflow() {
    const definition = designer.getDefinition();    
    let register = null;
    
    for (let step of definition.sequence) {
        if (step.type === 'readUserInput') {
            register = prompt(step['question']);
        }
        else if (step.type === 'sendEmail') {
            if (step.properties['ifReqisterEquals'] === register) {
                alert(`E-mail sent to ${step.properties['email']}`);
            }
        }
    }
}

Теперь нам нужно вызвать функцию runWorkflow.

📦 Примеры

Готовый проект можно протестировать здесь.

Кстати, вы можете проверить приведенные ниже онлайн-демонстрации компонента Sequential Workflow Designer.