@omnicajs/vue-remote
v0.2.23
Published
Proxy renderer for Vue.js based on @remote-ui
Maintainers
Readme
@omnicajs/vue-remote
Proxy renderer for Vue.js v3 based on @remote-ui/rpc and designed to provide necessary tools
for embedding remote applications into your main application.
Documentation
- Published docs: https://omnicajs.github.io/vue-remote/
- Development guide:
docs/DEV.md
Installation
Using yarn:
yarn add @omnicajs/vue-remoteor, using npm:
npm install @omnicajs/vue-remote --saveDescription
Vue-remote lets you take tree-like structures created in a sandboxed JavaScript environment, and render them to the DOM in a different JavaScript environment. This allows you to isolate potentially-untrusted code off the main thread, but still allow that code to render a controlled set of UI elements to the main page.
The easiest way to use vue-remote is to synchronize elements between a hidden iframe and the top-level page.
To use vue-remote, you’ll need a web project that is able to run two JavaScript environments: the “host” environment, which runs on the main HTML page and renders actual UI elements, and the “remote” environment, which is sandboxed and renders an invisible version of tree-like structures that will be mirrored by the host.
Next, on the “host” HTML page, you will need to create a “receiver”. This object will be responsible for receiving the updates from the remote environment, and mapping them to actual DOM elements.
Vue-remote use postMessage events from the iframe, in order to pass changes in the remote tree to receiver
See more about remote rendering:
Usage
Remote template ref tooling
For remote SFCs you can enable remote-native template ref inference in Vue tooling.
If you are consuming the published package from another project, use the package subpath instead:
{
"vueCompilerOptions": {
"plugins": [
"@omnicajs/vue-remote/tooling"
]
}
}With the plugin enabled, refs like <div ref="panel" /> inside *.remote.vue or <script setup remote> SFCs are inferred as remote element proxies from @omnicajs/vue-remote/remote, not as real DOM elements.
Remote event modifier bundler setup
If your remote SFCs are bundled with webpack, add the package loader after vue-loader for compiled Vue modules and remote entry scripts so Vue rewrites withModifiers(...) and withKeys(...) to the remote-aware helpers:
module.exports = {
module: {
rules: [
{
resourceQuery: /vue.*type=(?:template|script|scriptSetup)/,
loader: '@omnicajs/vue-remote/webpack-loader',
},
{
test: /\.remote\.(?:[cm]?[jt]sx?|vue)$/,
loader: '@omnicajs/vue-remote/webpack-loader',
},
],
},
}Vite users can keep using the built-in plugin integration from this repository; the event helper rewrite logic is shared between both bundlers.
IDE notes
- VS Code: use the
Vue - Officialextension from the Vue language tools project. - PhpStorm / WebStorm: use
Vue Language Server (Volar)and prefer theEnable service-powered type engineoption for more accurate inference. - Other IDEs and editors: if they are backed by
@vue/language-server/ Volar-compatible Vue tooling, they should pick upvueCompilerOptions.pluginsas well.
Basic example
Host application:
import type { PropType } from 'vue'
import type { Channel } from '@omnicejs/vue-remote/host'
import type { Endpoint } from '@remote-ui/rpc'
import {
defineComponent,
h,
onBeforeUnmount,
onMounted,
ref,
} from 'vue'
import {
createEndpoint,
fromIframe,
} from '@remote-ui/rpc'
import {
HostedTree,
createProvider,
createReceiver,
} from '@omnicajs/vue-remote/host'
// Here we are defining Vue components provided by a host
const provider = createProvider({
VButton: defineComponent({
props: {
appearance: {
type: String as PropType<'elevated' | 'outline' | 'text' | 'tonal'>,
default: 'elevated',
},
},
setup (props, { attrs, slots }) {
return () => h('button', {
...attrs,
class: [{
['v-button']: true,
['v-button' + props.appearance]: true,
}, attrs.class],
}, slots)
},
}),
VInput: defineComponent({
props: {
type: {
type: HTMLInputElement['type'],
default: 'text',
},
value: {
type: String,
default: '',
},
},
emits: ['update:value'],
setup (props, { attrs, emit }) {
return () => h('input', {
...attrs,
...props,
onInput: (event) => emit('update:value', (event.target as HTMLInputElement).value),
})
},
}),
})
type EndpointApi = {
// starts a remote application
run (channel: Channel, api: {
doSomethingOnHost (): void;
}): Promise<void>;
// useful to tell a remote application that it is time to quit
release (): void;
}
const hostApp = defineComponent({
props: {
src: {
type: String,
required: true,
},
},
setup () {
const iframe = ref<HTMLIFrameElement | null>(null)
const receiver = createReceiver()
let endpoint: Endpoint<EndpointApi> | null = null
onMounted(() => {
endpoint = createEndpoint<EndpointApi>(fromIframe(iframe.value as HTMLIFrameElement, {
terminate: false,
}))
})
onBeforeUnmount(() => endpoint?.call.release())
return () => [
h(HostedTree, { provider, receiver }),
h('iframe', {
ref: iframe,
src: props.src,
style: { display: 'none' } as CSSStyleDeclaration,
onLoad: () => {
endpoint?.call?.run(receiver.receive, {
doSomethingOnHost (text: string) {
// some logic to interact with host application
},
})
},
}),
]
},
})
// src - remoteApp url
const app = createApp(hostApp, {src: 'localhost/remote'})
app.mount('#host')Remote application:
import {
defineComponent,
h,
ref,
} from 'vue'
import {
createEndpoint,
fromInsideIframe,
release,
retain,
} from '@remote-ui/rpc'
import {
createRemoteRenderer,
createRemoteRoot,
defineRemoteComponent,
} from '@omnicajs/vue-remote'
const createApp = async (channel, component, props) => {
const root = createRemoteRoot(channel, {
components: [
'VButton',
'VInput',
],
})
await root.mount()
const app = createRemoteRenderer(root).createApp(component, props)
app.mount(root)
return app
}
let onRelease = () => {}
// In order to proxy function properties and methods between environments,
// we need a library that can serialize functions over `postMessage`.
const endpoint = createEndpoint(fromInsideIframe())
const VButton = defineRemoteComponent('VButton')
const VInput = defineRemoteComponent('VInput', [
'update:value',
] as unknown as {
'update:value': (value: string) => true,
})
endpoint.expose({
// This `run()` method will kick off the process of synchronizing
// changes between environments. It will be called on the host.
async run (channel, api) {
retain(channel)
retain(api)
const app = await createApp(channel, defineComponent({
setup () {
const text = ref('')
return () => [
h(VInput, { 'onUpdate:value': (value: string) => text.value = value }),
h(VButton, { onClick: () => api.doSomethingOnHost(text.value) }, 'Do'),
]
},
}), {
api,
})
onRelease = () => {
release(channel)
release(api)
app.unmount()
}
},
release () {
onRelease()
},
})Host environment
HostedTree
This component is used to interpret the instructions given from remote applications and transfer them into virtual dom, that is processed by vue on the host into a real DOM.
Consumes:
- provider – instance of
Provider; used to determine what component should be used to render, if the given instruction doesn't belong to native DOM elements or vue slots; - receiver – a channel to communicate with remote application.
createProvider(keyValuePairs)
Creates provider consumed by HostedTree. The only argument contains key-value pairs, where key is a component name
and value is the component constructor. You can call createProvider without that argument, if your remote app doesn't
rely on any host's component.
Remote environment
createRemoteRenderer()
This method creates proxy renderer for Vue.js v3 that outputs instructions
to a @omnicajs/vue-remote/remote RemoteRoot object.
The key feature of the library that provides a possibility to inject 3d-party logic through an isolated sandbox (iframe
for example, but not limited to).
To run a Vue application, you should call this method supplying a remote root (RemoteRoot).
createReceiver
Creates a Receiver object. This object can accept the instructions from the remote application and reconstruct them into a virtual dom on the host.
The virtual dom can then be used by Vue to render a real DOM in the host.
createRemoteRoot()
Creates a RemoteRoot object consumed by the createRemoteRenderer() method.
This function is used to create a RemoteRoot. It takes a Channel and an options object as arguments.
The options object can include a components array and a strict boolean.
The components array is used when creating a RemoteRoot in the remote environment. This array should contain the names
of the components that the remote environment is allowed to render. These components are defined in the host environment
and are provided to the remote environment through the Provider object.
The purpose of this array is to control what components the remote environment can use. This is important for security and control over what the remote environment can do. By specifying the components in this array, you ensure that the remote environment can only render the components that you have explicitly allowed.
Here's an example of how you might use it:
const root = createRemoteRoot(channel, {
components: ['Button', 'Input', 'List'], // These are the components that the remote environment can render
strict: true,
});In this example, the remote environment is only allowed to render the Button, Input, and List components.
These components would be defined in the host environment and provided to the remote environment through the Provider object.
defineRemoteComponent()
The way of defining Vue components that represent remote components provided by a host. We used this method in the
example above to define VButton & VInput components.
Also, you can specify the remote component’s prop types, which become the prop types of the generated Vue component:
import {
defineRemoteComponent,
defineRemoteMethod,
} from '@omnicajs/vue-remote/remote'
export default defineRemoteComponent<'VButton', {
appearance?: 'elevated' | 'outline' | 'text' | 'tonal'
}>('VButton', [
'click',
] as unknown as {
'click': () => true,
})
export const VDialog = defineRemoteComponent('VDialog', {
methods: {
open: defineRemoteMethod<[id: string], boolean>(),
close: defineRemoteMethod<[], void>(),
},
})The Vue-like object form is useful when you want to keep emits, named slots, and host methods together:
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 support three modes:
string[]: generates() => Promise<void>delegates.- validator object: uses validator argument tuples and rejects before
invokewhen validation fails. defineRemoteMethod<Args, Result>(validator?): keeps runtime validation and adds an explicitPromise<Result>return type.
When the component type is provided as SchemaType<...>, methods become 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 method is not declared in schema
scrollToTop: defineRemoteMethod<[], void>(),
},
})In this mode:
- method keys are limited to
keyof MethodsOf<Type>; - validator argument tuples must match the schema method;
defineRemoteMethod<Args, Result>must stay compatible with the schema signature.
Legacy positional form still works:
const VCard = defineRemoteComponent('VCard', [], ['title'])nextTick()
Remote nextTick() is available from the same public entrypoint:
import {
defineRemoteComponent,
nextTick,
} from '@omnicajs/vue-remote/remote'
const VDialog = defineRemoteComponent('VDialog')
await nextTick()Unlike Vue's local-only scheduler boundary, this nextTick() resolves only after the relevant remote updates have crossed the boundary and the host renderer has completed its commit cycle. If the host session is torn down before that commit happens, the promise rejects with a lifecycle error instead of waiting forever.
Migration note:
- keep using
defineRemoteComponent(type, emits?, slots?)if you only need legacy behavior; - switch to
defineRemoteComponent(type, { emits, slots, methods })when you want typed host method delegates on refs; - use
SchemaTypeonly when you want compile-time validation of allowed method names and signatures.
