Быстрый старт фронтенда

Фреймворк VST utils использует экосистему Vue для отображения фронтенда. Руководство по быстрому старту проведет вас через наиболее важные шаги по настройке функций фронтенда. Установка приложения и настройка описаны в - разделе Backend этой документации.

Есть несколько этапов в приложении VST utils:

  1. Перед запуском приложения:

    • checkCacheVersions() проверяет, была ли изменена версия приложения с последнего посещения и очищает все кэшированные данные, если это так;

    • загрузка схемы OpenAPI с бэкенда. Генерирует сигнал „openapi.loaded“;

    • загрузка всех статических файлов из SPA_STATIC в setting.py;

    • устанавливает AppConfiguration из схемы OpenAPI;

  2. Приложение запущено:

    • если в settings.py есть centrifugoClient, подключается к нему. Дополнительные сведения о конфигурации centrifugo можно найти в разделе «Настройки клиента Centrifugo»;

    • загрузка списка доступных языков и переводов;

    • api.loadUser() возвращает данные пользователя;

    • ModelsResolver создает модели из схемы, генерирует сигнал models[${modelName}].created для каждой созданной модели и allModels.created, когда все модели созданы;

    • ViewConstructor.generateViews() инициализирует View fieldClasses и modelClasses;

    • QuerySetsResolver находит соответствующий queryset по имени модели и пути представления;

    • global_components.registerAll() регистрирует Vue global_components;

    • prepare() генерирует сигнал app.beforeInit с { app: this };

    • инициализация модели с LocalSettings. Узнайте больше об этом в разделе Локальные настройки;

    • создание routerConstructor из this.views, генерация сигнала „app.beforeInitRouter“ с { routerConstructor } и получение нового VueRouter({this.routes});

    • инициализация приложения Vue() из schema.info, хранилища pinia и генерация сигнала „app.afterInit“ с {app: this};

  3. Приложение смонтировано.

Есть схема, представляющая процесс инициализации приложения (названия сигналов красным шрифтом):

graph TD
  Cached("checkCachedVersion()")--New App Version-->Clean("cleanAllCache()");
  Cached--Same App Version-->Schema(Load Schema);
  Clean-->Cached
  Schema--'openapi.loaded'-->AppConfiguration
  AppConfiguration--Has Centrifugo options-->Centrifugo(Connect Centrifugo)
  AppConfiguration--No Centrifugo-->Translation
  Centrifugo-->Translation(Load translation, <br/> load languages)
  Translation-->LoadUser("api.LoadUser()")-->ModelsResolver
  subgraph Models generation
  ModelsResolver--All Models Created-->B('allModels.created'):::classSignal
  ModelsResolver--Not All Models Created-->Create(Create Model)
  Create(Create Model)-->SignalBeforeInit("'models#91;modelName#93;.fields.beforeInit'"):::classSignal-->Fields(Create Fields)
  Fields(Create Fields)-->SignalAfterInit("'models#91;modelName#93;.fields.afterInit'"):::classSignal-->modelsmodelName("'models#91;modelName#93;.created'"):::classSignal
  modelsmodelName-->ModelsResolver
  end
  ViewConstructor("ViewConstructor.generateViews()")--'allViewsCreated'-->QuerySetResolver
  QuerySetResolver--finds approppriate querySet-->registerAll("global_components.registerAll()")
  registerAll--registers Vue global_components-->prepare
  prepare--'app.beforeInit'-->RouterConstuctor
  RouterConstuctor--'app.beforeInitRouter'-->D("new VueRouter()")
  D-->E(Vue Internationalization plugin - i18n)
  E-->F(Create new Vue Instance)

linkStyle 3 stroke:red,color:red
linkStyle 11 stroke:red,color:red
linkStyle 12 stroke:red,color:red
linkStyle 14 stroke:red,color:red
linkStyle 16 stroke:red,color:red
linkStyle 19 stroke:red,color:red
linkStyle 20 stroke:red,color:red

classDef classSignal stroke:#333,color:#f00;

Настройка поля

Чтобы добавить собственный скрипт в проект, укажите его имя в settings.py

SPA_STATIC += [
    {'priority': 101, 'type': 'js', 'name': 'main.js', 'source': 'project_lib'},
]

и поместите скрипт (main.js) в каталог {appName}/static/.

  1. В main.js создайте новое поле, расширив его от BaseField (или любого другого соответствующего поля)

    Например, создадим поле, которое отображает элемент HTML h1 с текстом „Привет, мир!“

class CustomField extends spa.fields.base.BaseField {
    static get mixins() {
        return super.mixins.concat({
            render(createElement) {
                return createElement('h1', {}, 'Hello World!');
            },
        });
    }
}

Или отобразите имя человека с некоторым префиксом

class CustomField extends spa.fields.base.BaseField {
  static get mixins() {
    return super.mixins.concat({
      render(h) {
        return h("h1", {}, `Mr ${this.$props.data.name}`);
      },
    });
  }
}
  1. Зарегистрируйте это поле в app.fieldsResolver, чтобы предоставить соответствующий формат и тип поля для нового поля

const customFieldFormat = 'customField';
app.fieldsResolver.registerField('string', customFieldFormat, CustomField);
  1. Слушайте подходящий сигнал models[ModelWithFieldToChange].fields.beforeInit для изменения формата поля

spa.signals.connect(`models[ModelWithFieldToChange].fields.beforeInit`, (fields) => {
    fields.fieldToChange.format = customFieldFormat;
});

Список моделей и их полей доступен во время выполнения в консоли по адресу app.modelsClasses

Чтобы изменить поведение поля, создайте новый класс поля с необходимой логикой. Допустим, вам нужно отправить API количество миллисекунд, но пользователь хочет вводить количество секунд. Решением будет переопределить методы toInner и toRepresent поля.

class MilliSecondsField extends spa.fields.numbers.integer.IntegerField {
  toInner(data) {
    return super.toInner(data) * 1000;
  }
  toRepresent(data) {
    return super.toRepresent(data)/1000;
  }
}

const milliSecondsFieldFormat = 'milliSeconds'
app.fieldsResolver.registerField('integer', milliSecondsFieldFormat, MilliSecondsField);
spa.signals.connect(`models[OneAllFields].fields.beforeInit`, (fields) => {
  fields.integer.format = milliSecondsFieldFormat;
});

Теперь у вас есть поле, которое отображает секунды, но сохраняет/получает данные в миллисекундах в виде детализированного представления модели AllFieldsModel.

Примечание

Если вам нужно показать какое-то предупреждение или ошибку в консоли разработчика, вы можете использовать методы warn и error поля. Вы можете передать сообщение, и оно будет выведено с типом поля, именем модели и именем поля.

Изменение пути к полю FkField

Иногда вам может потребоваться запросить другой набор объектов для FkField. Например, чтобы выбрать только известных авторов, создайте конечную точку famous_author на бэкенде и установите путь запроса FkField в famous_author. Слушайте сигнал app.beforeInit.

spa.signals.connect('app.beforeInit', ({ app }) => {
  app.modelsResolver.get('OnePost').fields.get('author').querysets.get('/post/new/')[0].url = '/famous_author/'
});

Теперь, когда мы создаем новый пост на конечной точке /post/, Author FkField выполняет GET-запрос к /famous_author/ вместо /author/. Это полезно для получения другого набора авторов (которые могли быть ранее отфильтрованы на бэкенде).

Стилизация CSS

  1. Как и скрипты, файлы CSS можно добавить в SPA_STATIC в setting.py

SPA_STATIC += [

    {'priority': 101, 'type': 'css', 'name': 'style.css', 'source': 'project_lib'},
]

Давайте проанализируем страницу и найдем класс CSS для нашего пользовательского поля. Это column-format-customField и создается с использованием шаблона column-format-{Field.format}.

  1. Используйте обычные стили CSS для изменения внешнего вида поля.

.column-format-customField:hover {
    background-color: orangered;
    color: white;
}

Другие элементы страницы также доступны для стилизации: например, чтобы скрыть определенный столбец, установите соответствующее поле в значение none.

.column-format-customField {
    display: none;
}

Показать столбец первичных ключей в списке

Каждый столбец первичного ключа имеет класс CSS pk-column и по умолчанию скрыт (с использованием display: none;).

Например, этот стиль покажет столбец первичных ключей во всех представлениях списка модели Order.

.list-Order .pk-column {
    display: table-cell;
}

Настройка представления

Слушайте сигнал «allViews.created» и добавьте новый пользовательский миксин в представление.

В следующем фрагменте кода показано отображение нового представления вместо представления по умолчанию.

spa.signals.once('allViews.created', ({ views }) => {
    const AuthorListView = views.get('/author/');
    AuthorListView.mixins.push({
        render(h) {
            return h('h1', {}, `Custom view`);
        },
    });
});

Узнайте больше о функции render() Vue в документации Vue.

Также можно настроить представление, переопределив вычисляемые свойства и методы по умолчанию существующих миксинов. Например, переопределите вычисляемое свойство breadcrumbs для отключения хлебных крошек в представлении списка авторов.

import { ref } from 'vue';

spa.signals.once("allViews.created", ({ views }) => {
    const AuthorListView = views.get("/author/");
    AuthorListView.extendStore((store) => {
        return {
            ...store,
            breadcrumbs: ref([]),
        };
    });
});

Иногда вам может потребоваться скрыть страницу с деталями по какой-то причине, но при этом сохранить доступ ко всем действиям и подссылкам с страницы списка. Чтобы сделать это, также следует слушать сигнал «allViews.created» и изменить параметр hidden с значения false по умолчанию на true, например:

spa.signals.once('allViews.created', ({ views }) => {
    const authorView = views.get('/author/{id}/');
    authorView.hidden = true;
});

Изменение заголовка представления

Чтобы изменить заголовок и строку, отображаемую в хлебных крошках, измените свойство title представления или метод getTitle для более сложной логики.

spa.signals.once('allViews.created', ({ views }) => {
    const usersList = views.get('/user/');
    usersList.title = 'Users list';

    const userDetail = views.get('/user/{id}/');
    userDetail.getTitle = (state) => (state?.instance ? `User: ${state.instance.id}` : 'User');
});

Базовая конфигурация Webpack

Чтобы использовать webpack в вашем проекте, переименуйте webpack.config.js.default в webpack.config.js. Каждый проект, основанный на vst-utils, содержит index.js в каталоге /frontend_src/app/. Этот файл предназначен для вашего кода. Запустите команду yarn для установки всех зависимостей. Затем выполните yarn devBuild из корневого каталога вашего проекта для сборки статических файлов. Последним шагом является добавление собранного файла в SPA_STATIC в settings.py.

SPA_STATIC += [
    {'priority': 101, 'type': 'js', 'name': '{AppName}/bundle/app.js', 'source': 'project_lib'},
]

Файл конфигурации Webpack позволяет добавлять дополнительные статические файлы. В webpack.config.js добавьте дополнительные записи

const config = {
  mode: setMode(),
  entry: {
    'app': entrypoints_dir + "/app/index.js" // default,
    'myapp': entrypoints_dir + "/app/myapp.js" // just added
  },

Выходные файлы будут собраны в каталоге frontend_src/{AppName}/static/{AppName}/bundle. Имя выходного файла соответствует имени записи в config. В приведенном выше примере выходные файлы будут называться app.js и myapp.js. Добавьте все эти файлы в STATIC_SPA в settings.py. Во время установки vstutils через pip фронтенд-код собирается автоматически, поэтому вам может потребоваться добавить каталог bundle в gitignore.

Хранилище страницы

У каждой страницы есть хранилище, которое можно получить глобально app.store.page или из компонента страницы с использованием this.store.

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

import { computed } from 'vue';

spa.signals.once('allViews.created', ({ views }) => {
    views.get('/user/{id}/').extendStore((store) => {
        // Override title of current page using computed value
        const title = computed(() => `Current page has ${store.instances.hength} instances`);

        async function fetchData() {
            await store.fetchData();  // Call original fetchData
            await callSomeExternalApi(store.instances.value);
        }

        return {
            ...store,
            title,
            fetchData,
        };
    });
});

Переопределение корневого компонента

Корневой компонент приложения можно переопределить с использованием сигнала app.beforeInit. Это может быть полезно, например, для изменения классов CSS макета, поведения кнопки назад или основных компонентов макета.

Пример настройки компонента боковой панели:

const CustomAppRoot = {
    components: { Sidebar: CustomSidebar },
    mixins: [spa.AppRoot],
};
spa.signals.once('app.beforeInit', ({ app }) => {
    app.appRootComponent = CustomAppRoot;
});

Перевод значений полей

Значения, отображаемые с использованием FKField или ChoicesField, могут быть переведены с использованием стандартных файлов перевода.

Ключ перевода должен быть определен как :model:<ModelName>:<fieldName>:<value>. Например:

TRANSLATION = {
    ':model:Category:name:Category 1': 'Категория 1',
}

Перевод значений может быть трудоемким, поскольку каждая модель на бэкенде обычно генерирует более одной модели на фронтенде. Для избежания этого добавьте атрибут _translate_model = „Category“ к модели на бэкенде. Это сокращает

':model:Category:name:Category 1': 'Категория 1',
':model:OneCategory:name:Category 1': 'Категория 1',
':model:CategoryCreate:name:Category 1': 'Категория 1',

в

':model:Category:name:Category 1': 'Категория 1',

Для FKField используется имя связанной модели. И fieldName должно быть равно viewField.

Локальные настройки

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

spa.signals.once('models[_LocalSettings].fields.beforeInit', (fields) => {
        const cameraField = new spa.fields.base.BaseField({ name: 'camera' });
        // You can add some logic here
        fields.camera = cameraField;
})

Хранилище

Есть три способа сохранения данных:

  • userSettingsStore - сохраняет данные на сервере. По умолчанию есть варианты изменения языка и кнопка включения/выключения темного режима. Данные для userSettingsStore поступают из схемы.

  • localSettingsStore - сохраняет данные в локальном хранилище браузера. Здесь вы можете хранить свои собственные поля, как описано в Локальные настройки.

  • store - хранит данные текущей страницы.

Чтобы использовать любое из этих хранилищ, вам нужно выполнить следующую команду: app.[storeName], например: app.userSettingsStore.

Примечание

Если вы обращаетесь к userSettingsStore изнутри компонента, тогда вам нужно использовать this.$app вместо app.

Из app.store вам может потребоваться:

  • vewsItems и viewItemsMap - хранят информацию о родительских представлениях для этой страницы. Они используются, например, в хлебных крошках. Различие между ними заключается только в способе хранения информации: viewItems - это массив объектов, а viewItemsMap - это карта.

  • page - сохраняет всю информацию о текущей странице.

  • title - заголовок текущей страницы.