Реестр

Гайд по использованию реестра и передаче типизированных сущностей из проектов в shared

Для чего нужен?

Некоторые компоненты shared-front требуют зависимости, которые зависят от проекта, к примеру api вызовы или ссылки на страницы. Решением этой проблемы служит Реестр.

Принцип работы

Для работы с реестром необходимы несколько шагов

Шаг 0 (Установка в проект)

Nuxt(2/3)

// nuxt.config.js
export default {
  modules: [
    'shared-front/nuxt/registry',
  ],
};

Vue(2/3)

// main.ts
import { Plugin } from 'shared-front/lib/modules/registry';

appOrVue.use(Plugin);

Шаг 1 (Описание зависимости)

На данном шаге происходит описание типа зависимости и создание уникального ключа для реестра

Описание зависимости происходит на уровне shared-front в папке src/registry

// src/registry/example.ts
import type { RegistryKey, Registry } from '@/modules/registry';
import { useRegistryWrapper, useRegistryItem } from '@/modules/registry';

export interface Example {
  foo: string;

  bar: number;
}

export const exampleRegistryKey = Symbol('example registry-key') as RegistryKey<Example>;

export default useRegistryWrapper(exampleRegistryKey);
// Идентично
export default (registry?: Registry) =>
  useRegistryItem(exampleRegistryKey, registry);

Шаг 1.2 (Классовое описание)

Если зависимость имеет схожую структуру в разных проектах, имеет смысл создать js class и использовать далее его же или наследовать и модифицировать

// src/registry/example.ts
import type { RegistryKey } from '@/modules/registry';

export class Example {
  foo = 'foo';

  bar = 123;
}

export const exampleRegistryKey = Symbol('example registry-key') as RegistryKey<Example>;

export default useRegistryWrapper(exampleRegistryKey);

Из-за бага vite#9251 использовать символы невозможно и приходится использовать строки

export const exampleRegistryKey = 'example registry-key' as unknown as RegistryKey<Example>;

Шаг 1.5 (Описание pinia store)

Для работы с pinia добавлены более удобные хелперы с рассчетом на расширение

// src/registry/stores/example.ts
import {
  type ExtractId,
  type ExtractState,
  type ExtractGetters,
  type ExtractActions,
  type StoreRegistryKey,
  defineDefaultStore,
  useStoreWrapper,
  setActiveRegistry,
} from '@/modules/registry';


// Хелпер сугубо для правильной типизации
export const defaultStore = defineDefaultStore({
  id: 'example',

  state: () => ({
    foo: 'foo',
    bar: 123,
  }),

  getters: {
    fooBar(state) {
      return state.foo + state.bar;
    },
  },
});


// Шаблонный код для дальнейшего расширения типов
export type Id = ExtractId<typeof defaultStore>;
export interface State extends ExtractState<typeof defaultStore> {}
export interface Getters extends ExtractGetters<typeof defaultStore> {}
export interface Actions extends ExtractActions<typeof defaultStore> {}


// StoreRegistryKey легкий алиас для RegistryKey<Store<...>>
// "${defaultStore.id}" вместо "example" сугубо для более простого копирования в разные файлы
export const injectionKey = `store:${defaultStore.id}` as unknown as StoreRegistryKey<Id, State, Getters, Actions>;


export default useStoreWrapper<Id, State, Getters, Actions>(injectionKey, defaultStore);
// Идентично
const useStore = defineStore(defaultStore);
export default (registry?: Registry) =>
  useRegistryItem(injectionKey, registry, ({ $pinia }, registry) => {
    setActiveRegistry(registry);

    return useStore($pinia);
  });

Указание типов для useStoreWrapper<> важно, иначе не будут работать новые типы после расширения

Шаг 2 (Реализация зависимости)

Данный шаг подразумевает конкретную реализацию зависимости

Реализация происходит на уровне проектов в папках registry

// registry/example.ts
import { defineRegistryItem } from 'shared-front/lib/modules/registry';
import { exampleRegistryKey } from 'shared-front/lib/registry/example';

export default defineRegistryItem(exampleRegistryKey, (context) => ({
  foo: 'foo',
  bar: 123,
}));

Шаг 2.2 (Классовая реализация)

Дополнение к предыдущему классовому шагу

// registry/example.ts
import { defineRegistryItem } from 'shared-front/lib/modules/registry';
import { Example, exampleRegistryKey } from 'shared-front/lib/registry/example';

class MyExample extends Example {
  foo = 'baz';
}

export default defineRegistryItem(exampleRegistryKey, (context) => new MyExample());

Шаг 2.5 (Реализация pinia store)

Более подробно про работу с pinia stores

// registry/stores/example.ts
import { injectionKey, defaultStore } from '@/registry/stores/foo';
import {
  defineRegistryStore,
  extendStore,
  setActiveRegistry,
  type ExtractState,
  type ExtractGetters,
  type ExtractActions,
} from '@/modules/registry';


// Расширение defaultStore
const extendedStore = extendStore(defaultStore, {
  state: () => ({
    baz: 'baz',
  }),

  getters: {
    barBaz(state) {
      return state.bar + state.baz;
    },
  },

  actions: {
    someActionInAnotherStore() {
      const anotherStore = useAnotherRegistryStore(this.$registry);
    },
  },
});


// Очередной шаблонный код
declare module 'shared-front/lib/registry/stores/example' {
  interface State extends ExtractState<typeof extendedStore> {}
  interface Getters extends ExtractGetters<typeof extendedStore> {}
  interface Actions extends ExtractActions<typeof extendedStore> {}
}

export default defineRegistryStore(injectionKey, extendStore);
// Идентично
const useStore = defineStore(extendedStore);
export default defineRegistryItem(injectionKey, ({ $pinia }, registry) => {
  setActiveRegistry(registry);

  return useStore($pinia);
});

Шаг 3 (Provide зависимости)

Перед использованием зависимости, необходимо поместить её в реестр

Provide зависимости происходит на уровне проектов, в компоненте родителе компонента, в котором эта зависимость нужна

Если у зависимости есть дефолтное значение и оно не изменяется в проекте, этот шаг можно пропустить

// pages/example/index.ts setup
import useExample from '@/registry/example';

const example = useExample();

Глобально сделать provide зависимости в предыдущем шаге нельзя при использовании ssr, т.к. нужен конкретный инстанс реестра

Для доступа к элементам реестра из nuxt route middleware, provide необходимо проводить или в предыдущем middleware или в плагине

Шаг 3.3 (Упрощенный provide)

Если зависимость не нужно переиспользовать или она нужна лишь для теста, можно совместить 2-ой и 3-ий шаги

// pages/example/index.ts setup
import { provideRegistryItem } from 'shared-front/lib/modules/registry';
import { exampleRegistryKey } from 'shared-front/lib/registry/example';

provideRegistryItem(exampleRegistryKey, {
  foo: 'foo',
  bar: 123,
});

Шаг 3.4 (Provide вне setup)

// pages/example/index.ts
import useExample from '@/registry/example';

export default defineComponent({
  created() {
    const example = useExample(this.$registry);
  },
});

Шаг 4 (Использование зависимости)

Финальный шаг при использовании реестра

Использование может происходить как на shared-front так и на проектах, актуальнее всего конечно в shared-front

// src/components/example/index.ts setup
import { useRegistryItem } from '@/modules/registry';
import useExample, { exampleRegistryKey } from '@/registry/example';

const example = useRegistryItem(exampleRegistryKey);
// Идентично, но в случае использование загрузчиков, приоритетно
const example = useExample();

Шаг 4.4 (Использование вне setup)

// src/components/example/index.ts
import { useRegistryItem } from '@/modules/registry';
import { exampleRegistryKey } from '@/registry/example';

export default defineComponent({
  computed: {
    example() {
      return useRegistryItem(exampleRegistryKey, this.$registry);
    },
  },
});

Контексты использования

Контексты использования реестра можно разбить на категории

  1. Ssr + Spa + Setup
  2. Ssr + Spa
  3. Spa + Setup
  4. Spa

Для всех кейсов кроме второго, все будет штатно работать без доп аргументов, для случая 2, необходимо добавлять registry к вызовам

Argument registry

Т.к. в среде Ssr единый инстанс реестра расшаривается на все запросы, какая-то приватная информация юзеров могла бы утечь. Чтобы подобное было невозможно, при использовании на сервере и вне setup, необходимо явно задавать реестр с которым производится манипуляция (аналогично pinia).

На странице api можно более подробно ознакомиться с аргументами каждой функции