Skip to content

Commit e8ea500

Browse files
feat(kit): support devtools in iframe (#886)
1 parent 07a637a commit e8ea500

File tree

10 files changed

+174
-2
lines changed

10 files changed

+174
-2
lines changed

packages/applet/src/modules/components/index.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,10 +220,11 @@ const devtoolsState = useDevToolsState()
220220
const appRecords = computed(() => devtoolsState.appRecords.value.map(app => ({
221221
label: app.name + (app.version ? ` (${app.version})` : ''),
222222
value: app.id,
223+
iframe: app.iframe,
223224
})))
224225
225226
const normalizedAppRecords = computed(() => appRecords.value.map(app => ({
226-
label: app.label,
227+
label: app.label + (app.iframe ? ` (iframe: ${app.iframe})` : ''),
227228
id: app.value,
228229
})))
229230

packages/core/src/rpc/global.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ function getDevToolsState() {
3030
name: item.name,
3131
version: item.version,
3232
routerId: item.routerId,
33+
iframe: item.iframe,
3334
})),
3435
activeAppRecordId: state.activeAppRecordId,
3536
timelineLayersState: state.timelineLayersState,

packages/devtools-kit/src/core/app/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { target } from '@vue/devtools-shared'
1+
import { isBrowser, target } from '@vue/devtools-shared'
22
import slug from 'speakingurl'
33
import { AppRecord, VueAppInstance } from '../../types'
4+
import { getRootElementsFromComponentInstance } from '../component/tree/el'
45

56
const appRecordInfo = target.__VUE_DEVTOOLS_NEXT_APP_RECORD_INFO__ ??= {
67
id: 0,
@@ -52,6 +53,7 @@ export function createAppRecord(app: VueAppInstance['appContext']['app'], types:
5253
appRecordInfo.id++
5354
const name = getAppRecordName(app, appRecordInfo.id.toString())
5455
const id = getAppRecordId(app, slug(name))
56+
const [el] = getRootElementsFromComponentInstance(rootInstance) as /* type-compatible, this is returning VNode[] */ unknown as HTMLElement[]
5557

5658
const record: AppRecord = {
5759
id,
@@ -60,6 +62,7 @@ export function createAppRecord(app: VueAppInstance['appContext']['app'], types:
6062
instanceMap: new Map(),
6163
perfGroupIds: new Map(),
6264
rootInstance,
65+
iframe: isBrowser && document !== el?.ownerDocument ? el?.ownerDocument?.location?.pathname : undefined,
6366
}
6467

6568
app.__VUE_DEVTOOLS_NEXT_APP_RECORD__ = record
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
export function detectIframeApp(target: Window | typeof globalThis, inIframe = false) {
2+
if (inIframe) {
3+
function sendEventToParent(cb) {
4+
try {
5+
// @ts-expect-error skip type check
6+
const hook = window.parent.__VUE_DEVTOOLS_GLOBAL_HOOK__
7+
if (hook) {
8+
cb(hook)
9+
}
10+
}
11+
catch (e) {
12+
// Ignore
13+
}
14+
}
15+
16+
const hook = {
17+
id: 'vue-devtools-next',
18+
devtoolsVersion: '7.0',
19+
on: (event, cb) => {
20+
sendEventToParent((hook) => {
21+
hook.on(event, cb)
22+
})
23+
},
24+
once: (event, cb) => {
25+
sendEventToParent((hook) => {
26+
hook.once(event, cb)
27+
})
28+
},
29+
off: (event, cb) => {
30+
sendEventToParent((hook) => {
31+
hook.off(event, cb)
32+
})
33+
},
34+
emit: (event, ...payload) => {
35+
sendEventToParent((hook) => {
36+
hook.emit(event, ...payload)
37+
})
38+
},
39+
}
40+
41+
Object.defineProperty(target, '__VUE_DEVTOOLS_GLOBAL_HOOK__', {
42+
get() {
43+
return hook
44+
},
45+
configurable: true,
46+
})
47+
}
48+
49+
function injectVueHookToIframe(iframe) {
50+
if (iframe.__vdevtools__injected) {
51+
return
52+
}
53+
try {
54+
iframe.__vdevtools__injected = true
55+
const inject = () => {
56+
console.log('inject', iframe)
57+
try {
58+
iframe.contentWindow.__VUE_DEVTOOLS_IFRAME__ = iframe
59+
const script = iframe.contentDocument.createElement('script')
60+
script.textContent = `;(${detectIframeApp.toString()})(window, true)`
61+
iframe.contentDocument.documentElement.appendChild(script)
62+
script.parentNode.removeChild(script)
63+
}
64+
catch (e) {
65+
// Ignore
66+
}
67+
}
68+
inject()
69+
iframe.addEventListener('load', () => inject())
70+
}
71+
catch (e) {
72+
// Ignore
73+
}
74+
}
75+
76+
// detect iframe app to inject vue hook
77+
function injectVueHookToIframes() {
78+
if (typeof window === 'undefined') {
79+
return
80+
}
81+
82+
const iframes = Array.from(document.querySelectorAll<HTMLIFrameElement>('iframe:not([data-vue-devtools-ignore])'))
83+
for (const iframe of iframes) {
84+
injectVueHookToIframe(iframe)
85+
}
86+
}
87+
88+
injectVueHookToIframes()
89+
90+
let iframeAppChecks = 0
91+
const iframeAppCheckTimer = setInterval(() => {
92+
injectVueHookToIframes()
93+
iframeAppChecks++
94+
if (iframeAppChecks >= 5) {
95+
clearInterval(iframeAppCheckTimer)
96+
}
97+
}, 1000)
98+
}

packages/devtools-kit/src/core/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,14 @@ import {
1818
import { createDevToolsHook, hook, subscribeDevToolsHook } from '../hook'
1919
import { DevToolsHooks } from '../types'
2020
import { createAppRecord, removeAppRecordId } from './app'
21+
import { detectIframeApp } from './iframe'
2122
import { callDevToolsPluginSetupFn, createComponentsDevToolsPlugin, registerDevToolsPlugin, removeRegisteredPluginApp, setupDevToolsPlugin } from './plugin'
2223
import { initPluginSettings } from './plugin/plugin-settings'
2324
import { normalizeRouterInfo } from './router'
2425

2526
export function initDevTools() {
27+
detectIframeApp(target)
28+
2629
updateDevToolsState({
2730
vitePluginDetected: getDevToolsEnv().vitePluginDetected,
2831
})
@@ -136,6 +139,7 @@ export function initDevTools() {
136139
get() {
137140
return _devtoolsHook
138141
},
142+
configurable: true,
139143
})
140144
}
141145
else {

packages/devtools-kit/src/types/app.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,5 @@ export interface AppRecord {
5353
perfGroupIds: Map<string, { groupId: number, time: number }>
5454
rootInstance: VueAppInstance
5555
routerId?: string
56+
iframe?: string
5657
}

packages/playground/multi-app/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
<body>
1010
<div id="app"></div>
1111
<div id="app2"></div>
12+
<div id="app3"></div>
1213
<script type="module" src="/src/main.ts"></script>
1314
</body>
1415
</html>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<script setup lang="ts">
2+
import Srcdoc from './srcdoc.html?raw'
3+
</script>
4+
5+
<template>
6+
<div class="m-auto mt-5 h-30 w-60 flex flex-col items-center justify-center rounded bg-[#363636]">
7+
<iframe
8+
sandbox="allow-forms allow-modals allow-pointer-lock allow-popups allow-same-origin allow-scripts allow-top-navigation-by-user-activation"
9+
:srcdoc="Srcdoc"
10+
/>
11+
</div>
12+
</template>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<html>
2+
<head>
3+
<script type="importmap">
4+
{
5+
"imports": {
6+
"vue": "https://play.vuejs.org/vue.runtime.esm-browser.js"
7+
}
8+
}
9+
</script>
10+
</head>
11+
<body>
12+
<div id="app"></div>
13+
<script type="module">
14+
import { createApp, h, ref } from 'vue'
15+
16+
const app = createApp({
17+
setup() {
18+
const counter = ref(0)
19+
20+
return {
21+
counter,
22+
}
23+
},
24+
25+
render(ctx) {
26+
return h(
27+
'div',
28+
{
29+
style: {
30+
color: 'black',
31+
display: 'flex',
32+
'flex-wrap': 'wrap',
33+
'justify-content': 'center',
34+
'align-items': 'center',
35+
height: '100vh',
36+
},
37+
},
38+
h('h1', { style: { width: '100%' } }, 'App3 in iframe'),
39+
h('count', `${ctx.counter}`),
40+
h('div', h('button', { onClick: () => ctx.counter++ }, '++')),
41+
)
42+
},
43+
}).mount('#app')
44+
</script>
45+
</body>
46+
</html>

packages/playground/multi-app/src/main.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createApp } from 'vue'
22

33
import App2 from './App2.vue'
44
import App from './App.vue'
5+
import Iframe from './components/Iframe/index.vue'
56

67
import './style.css'
78
import 'uno.css'
@@ -10,6 +11,10 @@ const app = createApp(App)
1011

1112
const app2 = createApp(App2)
1213

14+
const app3 = createApp(Iframe)
15+
1316
app.mount('#app')
1417

1518
app2.mount('#app2')
19+
20+
app3.mount('#app3')

0 commit comments

Comments
 (0)