We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
在最近开发的过程中遇到了一个问题,在集成 Vue SFC Playground 的时候同时也使用了 monaco-editor,而 Vue SFC Playground 使用的默认编辑器就是 monaco-editor-core。
monaco-editor
monaco-editor-core
这就导致了一个问题,存在两个编辑器,但是它们都定义了全局变量 self.MonacoEnvironment 。导致虽然功能还能用,但是高亮以及智能联想全部没了。
self.MonacoEnvironment
// self.MonacoEnvironment通常情况下是这样的 import * as monaco from "monaco-editor"; import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker"; import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker"; import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker"; import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker"; import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker"; // @ts-ignore self.MonacoEnvironment = { getWorker(_: unknown, label: string) { if (label === "json") { return new jsonWorker(); } if (label === "css" || label === "scss" || label === "less") { return new cssWorker(); } if (label === "html" || label === "handlebars" || label === "razor") { return new htmlWorker(); } if (label === "typescript" || label === "javascript") { return new tsWorker(); } return new editorWorker(); }, }; monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true);
Vue SFC Playground 的定义是这样的
(self as any).MonacoEnvironment = { async getWorker(_: any, label: string) { if (label === "vue") { const worker = new vueWorker(); const init = new Promise<void>((resolve) => { worker.addEventListener("message", (data) => { if (data.data === "inited") { resolve(); } }); worker.postMessage({ event: "init", tsVersion: store.typescriptVersion, tsLocale: store.locale, } satisfies WorkerMessage); }); await init; return worker; } return new editorWorker(); }, };
还有没有办法愉快玩耍了,痛苦。
于是就在脑海中风暴,有没有解决方案呢?
最终还是决定使用方案 2,虽然会增加很多工作量,但是从长久来看是更有收益的。
通过 vue-route 注册一个特定的路由,它的作用就是嵌套 iframe 元素,通过这个路由来渲染 Vue SFC Playground 项目。
vue-route
之后所有跟编辑器相关的值和状态全部保留在 iframe 内,但是如果需要使用则通过 postMessage 来进行传递。
postMessage
具体来说,iframe 内部的变量全部都是都是自身独享的,如果其他模块需要调用,例如给 Vue SFC Playground 插入一个新文件,更改 importsMap 等操作,需要通过主页面的 postMessage 来更新 iframe。取值也是发送消息之后,从主页面接收 message 消息完成取值。
importsMap
message
等等,你不会以为我这篇文章只为说这个吧。
回到开发体验的角度来说这个事情,我要做的事情很简单,就是用 iframe 来隔离应用,防止出现全局变量冲突导致的一系列问题。
但是就诞生了怎么跟 iframe 交互的这个事情,正常的流程肯定走 message + postMessage 这套,但是这样就会有两套接收、发送。而且消息的取值也会收到限制。
message + postMessage
简单来说就是 postMessage 会使用结构化算法,其实也就是深拷贝原生实现的,但是它不是全能的,有一些限制。
这就导致我们通过 postMessage 传递消息其实是收到很多限制的,这个方案先放到一边。
从开发角度来说,更合理的是我使用响应式的对象,传递给子 iframe 这个对象,它修改,我的主页面也触发 watch 等操作。 这样就是无缝感知的了,下面是一个伪代码。
const store = ref({ // ... });
iframe
<script setup> const store = parent["xxxx"]; // 后续一系列使用 store.value.xx.xx == xxx; </script>
愿景很好,但是并不支持,我还特意去 vue issues 提了一个问题 watch Unable to Track Changes on window Object in Parent When Accessed from an iframe。
没法了,这个方案被噶了。
除此之外呢,还有什么方案呢?
分析一下 iframe 挂载过程,主页面先加载 => 然后触发子组件 => 子组件加载 iframe。
这个版本方案则是通过响应式来完成主窗口和子 iframe 的通讯。
具体来说就是在主窗口,取自身的 window 对象,而子 iframe 通过 parent[xxx] 来更新主窗口的值。
parent[xxx]
每一个 parent[xxx] 值变化的时候通知主窗口来进行值的修改,而主窗口的值更改通知 iframe 进行值的更新。
下面是一个伪代码的实现
<script setup> const mountValue = computed(() => { return { // ... }; }); watch( () => mountValue.value, (values) => { parent["11111"] = values; }, { deep: true, immediate: true } ); window.addEventListener("message", (e) => { // 接收到消息,更新mountValue const { key } = e.data; mountValue.value[key] = parent["11111"][key]; }); </script>
const [, { store }] = useSimulate<globalMountingAll>({ iframeId: id, }); // 操作 store.value = {};
这个就是一个简单版本的实现,不过有两个点感觉可以详细说一下。
我们都知道解构一个对象,会触发 proxy 的 get 方法,不过 get 方法如果第一次运行的时候因为子 iframe 没有值,所以会返回 undefined,但是但是,如果后续有值了,在触发更新就不是一个响应式对象了,因为你解构返回的值就不是一个对象。 所以这块一定要用 ref 来包裹返回,也就是说,初次的时候正常会 undefined,但是后续的值会在 iframe 加载之后更新正确。
proxy
get
undefined
ref
对于如果原来的值就是 ref,如果继续用 ref 包裹起来会出现问题,或者是一个 computed 的时候会出现 store.value.value 才能访问到,所以先需要调用一次 unref,来消除 ref。
computed
store.value.value
unref
上面就是方案二的实现细节了,虽然从代码角度来说工作量没有减少,但是一次编写之后后续使用不会再出现手动管理 message 和 postMessage 的事情发生。
这或许也是一种权衡吧。
下面是 ts 版本的一个代码实现
import { isRef, MaybeRef, onUnmounted, Ref, ref, toValue, unref, watch, WatchStopHandle, } from "vue"; import { COMMUNICATION_TYPE, useCommunicate, Props as UseCommunicateProps, } from "../useCommunicate"; import _ from "lodash-es"; import { useLoadingStatus } from "../useLoadingStatus"; import { useEventListener } from "@vueuse/core"; import type { globalMountingAll } from "../../components/iframeRepl/component.vue"; type Props = UseCommunicateProps; export type Unref<T> = T extends MaybeRef<infer U> ? U : T; export type Referencing<V> = Ref<Unref<V> | undefined>; export type ToPromise<T> = { [K in keyof T]: Referencing<T[K]>; }; /* * 对参数T必须进行约束,不然会出现值不存在的情况 */ export const useSimulate = <T extends globalMountingAll>(props: Props) => { const { postMessage } = useCommunicate(props); const { iframeId } = props; const { initLoading: loading } = useLoadingStatus(); const dependencyItems = new Map<string | symbol, Ref<any>>(); const unwatchAll = new Set<WatchStopHandle>(); const additionalParameters = new Proxy({} as ToPromise<T>, { get(_t, p) { /* * 收集依赖 */ const prop = p as keyof T; const value = _.get(window[toValue(iframeId) as any], prop); const v = ref(_.clone(unref(value))) as Ref< Unref<T[typeof prop]> | undefined >; /* * 如果v发生了变更,直接通知原属性进行更新值 */ const unwatch = watch( () => v.value, (v) => { // @ts-ignore const value = _.get(window[toValue(iframeId)], p); if (!isRef(value)) { return; } value.value = v; // @ts-ignore _.set(window[toValue(iframeId)], p, value); postMessage({ key: p, // // value: v, }); }, { deep: true } ); unwatchAll.add(unwatch); dependencyItems.set(p, v); return v; }, set() { return true; }, }); // 表示已经变更了,重新赋值 const setChange = () => { for (const [key, refItem] of dependencyItems) { // 更新 Ref 的值,而不是替换 Ref 对象 refItem.value = unref(_.get(window[toValue(iframeId) as any], key)); } }; useLoadingStatus(setChange); /* * 订阅变更 */ useEventListener(window, "message", (e) => { if (e.data?.type !== COMMUNICATION_TYPE || e.data?.data !== "change") { return; } setChange(); }); onUnmounted(() => { dependencyItems.clear(); unwatchAll.forEach((unwatch) => unwatch()); unwatchAll.clear(); }); return [loading, additionalParameters] as const; };
useLoadingStatus 的作用就是在 iframe 加载完成的时候通知。
useCommunicate 则是进行 postMessage 发送的封装,不过在这里不重要你可以自己实现。
const globalMounting = computed(() => { return { store, }; }); export type globalMountingAll = Unref<typeof globalMounting>; watch( () => globalMounting.value, (values) => { if (!id) { return; } // @ts-ignore parent[id] = values; parent.postMessage({ type: COMMUNICATION_TYPE, data: "change", }); }, { immediate: true } ); useEventListener(window, "message", (e) => { const key = e.data?.data?.key; if (e.data?.type !== COMMUNICATION_TYPE || !key) { return; } // @ts-ignore globalMounting.value[key] = parent[id]?.[key]; });
Unref 是我封装的一个 Type 方法,你可以理解成去掉.value 这块可以自行实现。
const [, { store }] = useSimulate<globalMountingAll>({ iframeId: id, });
虽然很想把所有的想法都说出来,不过肯定会有一些疏忽的地方,另外文章如果有错别字以及其他错误也欢迎指出。
The text was updated successfully, but these errors were encountered:
No branches or pull requests
在最近开发的过程中遇到了一个问题,在集成 Vue SFC Playground 的时候同时也使用了
monaco-editor
,而 Vue SFC Playground 使用的默认编辑器就是monaco-editor-core
。这就导致了一个问题,存在两个编辑器,但是它们都定义了全局变量
self.MonacoEnvironment
。导致虽然功能还能用,但是高亮以及智能联想全部没了。点击展开具体 `MonacoEnvironment` 代码
Vue SFC Playground 的定义是这样的
还有没有办法愉快玩耍了,痛苦。
解决方案
于是就在脑海中风暴,有没有解决方案呢?
最终还是决定使用方案 2,虽然会增加很多工作量,但是从长久来看是更有收益的。
iframe 实现思路
通过
vue-route
注册一个特定的路由,它的作用就是嵌套 iframe 元素,通过这个路由来渲染 Vue SFC Playground 项目。之后所有跟编辑器相关的值和状态全部保留在 iframe 内,但是如果需要使用则通过
postMessage
来进行传递。具体来说,iframe 内部的变量全部都是都是自身独享的,如果其他模块需要调用,例如给 Vue SFC Playground 插入一个新文件,更改
importsMap
等操作,需要通过主页面的postMessage
来更新 iframe。取值也是发送消息之后,从主页面接收message
消息完成取值。等等,你不会以为我这篇文章只为说这个吧。
回到开发体验的角度来说这个事情,我要做的事情很简单,就是用 iframe 来隔离应用,防止出现全局变量冲突导致的一系列问题。
但是就诞生了怎么跟 iframe 交互的这个事情,正常的流程肯定走
message + postMessage
这套,但是这样就会有两套接收、发送。而且消息的取值也会收到限制。简单来说就是 postMessage 会使用结构化算法,其实也就是深拷贝原生实现的,但是它不是全能的,有一些限制。
这就导致我们通过 postMessage 传递消息其实是收到很多限制的,这个方案先放到一边。
方案一
从开发角度来说,更合理的是我使用响应式的对象,传递给子 iframe 这个对象,它修改,我的主页面也触发 watch 等操作。
这样就是无缝感知的了,下面是一个伪代码。
iframe
愿景很好,但是并不支持,我还特意去 vue issues 提了一个问题 watch Unable to Track Changes on window Object in Parent When Accessed from an iframe。
没法了,这个方案被噶了。
方案二
除此之外呢,还有什么方案呢?
分析一下 iframe 挂载过程,主页面先加载 => 然后触发子组件 => 子组件加载 iframe。
这个版本方案则是通过响应式来完成主窗口和子 iframe 的通讯。
具体来说就是在主窗口,取自身的 window 对象,而子 iframe 通过
parent[xxx]
来更新主窗口的值。每一个
parent[xxx]
值变化的时候通知主窗口来进行值的修改,而主窗口的值更改通知 iframe 进行值的更新。下面是一个伪代码的实现
这个就是一个简单版本的实现,不过有两个点感觉可以详细说一下。
我们都知道解构一个对象,会触发
proxy
的get
方法,不过 get 方法如果第一次运行的时候因为子 iframe 没有值,所以会返回undefined
,但是但是,如果后续有值了,在触发更新就不是一个响应式对象了,因为你解构返回的值就不是一个对象。所以这块一定要用
ref
来包裹返回,也就是说,初次的时候正常会undefined
,但是后续的值会在 iframe 加载之后更新正确。对于如果原来的值就是
ref
,如果继续用ref
包裹起来会出现问题,或者是一个computed
的时候会出现store.value.value
才能访问到,所以先需要调用一次unref
,来消除ref
。上面就是方案二的实现细节了,虽然从代码角度来说工作量没有减少,但是一次编写之后后续使用不会再出现手动管理
message
和postMessage
的事情发生。这或许也是一种权衡吧。
具体实现代码
下面是 ts 版本的一个代码实现
useSimulate
useLoadingStatus 的作用就是在 iframe 加载完成的时候通知。
useCommunicate 则是进行 postMessage 发送的封装,不过在这里不重要你可以自己实现。
iframe 实现
Unref 是我封装的一个 Type 方法,你可以理解成去掉.value 这块可以自行实现。
使用
最后
虽然很想把所有的想法都说出来,不过肯定会有一些疏忽的地方,另外文章如果有错别字以及其他错误也欢迎指出。
The text was updated successfully, but these errors were encountered: