Перейти к основному содержимому

Компоненты remote

Что такое remote-компонент?

Ссылка на заголовок

Remote-компонент — это Vue-компонент, который рендерится в sandboxed remote-runtime и синхронизируется с host через обновления канала.

Remote-компоненты описывают, что нужно отрисовать. Host-компоненты (зарегистрированные на стороне host) определяют, как это рендерится в реальном DOM.

Ответственность host и remote

Ссылка на заголовок
  • createProvider() существует только на стороне host.
  • На стороне remote шага регистрации provider нет.
  • Сторона remote строит деревья через:
    • defineRemoteComponent(...) для компонентов, экспортированных host;
    • native-теги (html, svg, mathml) для локальной структуры.

Где нужна сериализация

Ссылка на заголовок

Сам Vue-код на remote не ограничен только scalars. Внутри remote-компонентов можно использовать обычные паттерны Vue (ref, computed, локальные функции, сложные объекты).

Правила сериализации применяются только на границах, которые зеркалятся на host:

  1. defineRemoteComponent(...): props/attrs и payload событий, которые уходят в host-компоненты.
  2. Native html / svg / mathml узлы: attrs/props и children, которые синхронизируются в host DOM. На практике они ведут себя как встроенные мини host-компоненты.

Для этих границ используйте данные, дружественные к postMessage:

  • Scalars: string, number, boolean, null.
  • Массивы сериализуемых значений.
  • Plain objects (POJO) с сериализуемыми вложенными значениями.
  • Комбинации трех вариантов выше.

Если значение несериализуемое, нормализуйте его до передачи через границу.

Утилита defineRemoteComponent

Ссылка на заголовок

Используйте defineRemoteComponent, чтобы описать типизированный bridge к host-компонентам по имени.

import {
defineRemoteComponent,
defineRemoteMethod,
} from '@omnicajs/vue-remote/remote'
const VButton = defineRemoteComponent('VButton')
const VInput = defineRemoteComponent('VInput', [
'update:value',
] as unknown as {
'update:value': (value: string) => true;
})
const VDialog = defineRemoteComponent('VDialog', {
methods: {
open: defineRemoteMethod<[id: string], boolean>(),
close: defineRemoteMethod<[], void>(),
},
})

Object-form позволяет держать emits, именованные slots и host-methods рядом:

import {
defineRemoteComponent,
defineRemoteMethod,
} from '@omnicajs/vue-remote/remote'
import { ref } from 'vue'
const VInput = defineRemoteComponent('VInput', {
emits: {
'update:value': (value: string) => value.length >= 0,
},
slots: ['prefix', 'suffix'],
methods: {
focus: defineRemoteMethod<[], void>(),
setSelectionRange: defineRemoteMethod<[start: number, end: number], void>(),
},
})
const input = ref<InstanceType<typeof VInput> | null>(null)
await input.value?.focus()
await input.value?.setSelectionRange(0, 2)

methods поддерживает три режима:

  • string[] для простых делегатов () => Promise<void>.
  • объект валидаторов для typed tuple-аргументов и runtime-проверки до invoke.
  • defineRemoteMethod<Args, Result>(validator?) для typed-аргументов и typed async-результата.

Если type задан как SchemaType<...>, methods переходит в schema-aware режим:

import {
defineRemoteComponent,
defineRemoteMethod,
type SchemaType,
} from '@omnicajs/vue-remote/remote'
type VInputSchema = SchemaType<
'VInput',
{ modelValue: string },
{
focus: () => Promise<void>;
setSelectionRange: (start: number, end: number) => Promise<void>;
}
>
const VInputType = 'VInput' as VInputSchema
const VInput = defineRemoteComponent(VInputType, {
methods: {
focus: defineRemoteMethod<[], void>(),
setSelectionRange: defineRemoteMethod<[number, number], void>(),
// @ts-expect-error метода нет в schema
scrollToTop: defineRemoteMethod<[], void>(),
},
})

В schema-aware режиме:

  • имена методов ограничены схемой;
  • tuple-аргументы валидаторов должны совпадать со schema-методом;
  • несовместимые сигнатуры defineRemoteMethod<Args, Result> падают на этапе компиляции.

Legacy positional form по-прежнему работает:

const VCard = defineRemoteComponent('VCard', [], ['title'])

Migration note:

  • positional form оставляйте для полностью legacy-сценария;
  • на object-form переходите, когда нужны typed методы на ref;
  • SchemaType добавляйте только там, где нужна compile-time проверка допустимых имён и сигнатур методов.

Пример: remote-app с host-компонентами

Ссылка на заголовок
import { createRemoteRenderer, createRemoteRoot, defineRemoteComponent } from '@omnicajs/vue-remote/remote'
import { defineComponent, h, ref } from 'vue'
const VButton = defineRemoteComponent('VButton')
const VInput = defineRemoteComponent('VInput', [
'update:value',
] as unknown as {
'update:value': (value: string) => true;
})
export async function mountRemote(channel: Parameters<typeof createRemoteRoot>[0]) {
const root = createRemoteRoot(channel, {
components: ['VButton', 'VInput'],
})
await root.mount()
const app = createRemoteRenderer(root).createApp(defineComponent({
setup () {
const text = ref('')
return () => h('section', { class: 'remote-form' }, [
h('h2', 'Remote form'),
h(VInput, {
value: text.value,
placeholder: 'Type here',
'onUpdate:value': (value: string) => text.value = value,
}),
h(VButton, { onClick: () => text.value = '' }, 'Clear'),
])
},
}))
app.mount(root)
}

Пример: SVG + MathML + host-компонент в одном дереве

Ссылка на заголовок
import { defineComponent, h } from 'vue'
import { defineRemoteComponent } from '@omnicajs/vue-remote/remote'
const VCard = defineRemoteComponent('VCard', [], ['title'])
export default defineComponent({
setup () {
return () => h('article', { class: 'metrics' }, [
h('svg', { viewBox: '0 0 120 24', width: '100%', height: '24' }, [
h('path', { d: 'M0 18 L20 12 L40 14 L60 6 L80 10 L100 4 L120 8', stroke: 'currentColor', fill: 'none' }),
]),
h('math', { display: 'block' }, [
h('mrow', [h('mi', 'x'), h('mo', '='), h('mn', '42')]),
]),
h(VCard, {}, {
title: () => 'Summary',
default: () => 'Rendered by host, composed by remote.',
}),
])
},
})

SFC с host-компонентами

Ссылка на заголовок
<script setup lang="ts">
import { ref } from 'vue'
import { defineRemoteComponent } from '@omnicajs/vue-remote/remote'
const VButton = defineRemoteComponent('VButton')
const VInput = defineRemoteComponent('VInput', [
'update:value',
] as unknown as {
'update:value': (value: string) => true;
})
const text = ref('')
</script>
<template>
<section class="remote-form">
<h2>Remote form</h2>
<VInput
:value="text"
placeholder="Type here"
@update:value="text = $event"
/>
<VButton @click="text = ''">Clear</VButton>
</section>
</template>

SFC с native HTML/SVG/MathML + host-компонентами

Ссылка на заголовок
<script setup lang="ts">
import { defineRemoteComponent } from '@omnicajs/vue-remote/remote'
const VCard = defineRemoteComponent('VCard', [], ['title'])
</script>
<template>
<article class="metrics">
<svg viewBox="0 0 120 24" width="100%" height="24">
<path d="M0 18 L20 12 L40 14 L60 6 L80 10 L100 4 L120 8" stroke="currentColor" fill="none" />
</svg>
<math display="block">
<mrow>
<mi>x</mi>
<mo>=</mo>
<mn>42</mn>
</mrow>
</math>
<VCard>
<template #title>Summary</template>
Rendered by host, composed by remote.
</VCard>
</article>
</template>

Ограничение ref на стороне remote

Ссылка на заголовок

Для native html / svg / mathml элементов в remote-шаблонах refs не указывают на реальные browser DOM nodes. Они указывают на узлы remote-дерева (proxy-объекты вроде RemoteComponent и связанных node-types).

<script setup lang="ts">
import { onMounted, ref } from 'vue'
const el = ref<unknown>(null)
onMounted(() => {
// Not HTMLElement/SVGElement/MathMLElement.
// This is a remote tree node proxy.
console.log(el.value)
})
</script>
<template>
<div ref="el" />
</template>

Из-за этого многие привычные DOM APIs недоступны в remote-refs (getBoundingClientRect, classList, прямой querySelector и т.д.).

DOM-библиотеки в remote-runtime

Ссылка на заголовок

Большинство библиотек, которым нужен прямой доступ к DOM, не будут работать на стороне remote по той же причине: remote-runtime управляет proxy-узлами дерева, а не реальными browser-элементами.

Если вам нужна такая библиотека:

  • интегрируйте ее на стороне host (внутри host-компонента),
  • отдайте наружу узкий сериализуемый API через props/events,
  • держите сторону remote сфокусированной на декларативном состоянии и intent.