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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy