Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion designer-demo/src/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ initPreview({
id: 'engine.root',
metas: [HttpService, GenerateCodeService]
},
config: { id: 'engine.config', theme: 'light' },
config: { id: 'engine.config', theme: 'light', previewHotReload: false },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify if hot reload should be disabled by default

Adding previewHotReload: false explicitly disables hot reload for the preview in the demo environment. Consider whether this is the desired default behavior for demonstration purposes.


🏁 Script executed:

#!/bin/bash
# Check if there are any comments or documentation explaining the default hot reload behavior
rg -i "previewHotReload" --type js --type ts --type vue -C 3

# Check if other demo files use different settings
rg -i "previewHotReload.*true" --type js --type ts --type vue

Length of output: 180


Let’s search without restricting file types to see if this setting appears elsewhere:


🏁 Script executed:

#!/bin/bash
# Search for any occurrences of previewHotReload
rg -i previewHotReload -C 3 .

# Search for generic hot reload flags
rg -i hotReload -C 3 .

Length of output: 3124


Clarify previewHotReload default behavior

The previewHotReload option defaults to live‐update (true) when not specified. In the demo you’ve explicitly turned it off:

// designer-demo/src/preview.js:28
config: { id: 'engine.config', theme: 'light', previewHotReload: false },

Please decide which you intend for the demo:

  • Remove the previewHotReload: false override so the preview hot‐reload remains enabled by default.
  • Or keep it disabled but add a comment explaining why live updates are not desired in this environment.

toolbars: [Breadcrumb, Media, Lang]
},
lifeCycles: {
Expand Down
301 changes: 271 additions & 30 deletions packages/common/js/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,53 +10,294 @@
*
*/

import { constants } from '@opentiny/tiny-engine-utils'
import { useThrottleFn } from '@vueuse/core'
import {
useMaterial,
useResource,
useMessage,
useCanvas,
usePage,
useBlock,
getMetaApi,
META_SERVICE,
getMergeMeta
} from '@opentiny/tiny-engine-meta-register'
import { utils } from '@opentiny/tiny-engine-utils'
import { isDevelopEnv } from './environments'
import { useMaterial, useResource } from '@opentiny/tiny-engine-meta-register'
// prefer old unicode hacks for backward compatibility

const { COMPONENT_NAME } = constants
const { deepClone } = utils

export const utoa = (string) => btoa(unescape(encodeURIComponent(string)))

export const atou = (base64) => decodeURIComponent(escape(atob(base64)))

const open = (params = {}) => {
const paramsMap = new URLSearchParams(location.search)
params.app = paramsMap.get('id')
params.tenant = paramsMap.get('tenant')
// 保存预览窗口引用
let previewWindow = null

const getScriptAndStyleDeps = () => {
const { scripts, styles } = useMaterial().getCanvasDeps()
const utilsDeps = useResource().getUtilsDeps()

params.scripts = [...scripts, ...utilsDeps].reduce((res, item) => {
const scriptsDeps = [...scripts, ...utilsDeps].reduce((res, item) => {
res[item.package] = item.script

return res
}, {})
params.styles = [...styles]
const stylesDeps = [...styles]

return {
scripts: scriptsDeps,
styles: stylesDeps
}
}

const getSchemaParams = async () => {
const { isBlock, getPageSchema, getCurrentPage, getSchema } = useCanvas()
const isBlockPreview = isBlock()
const { scripts, styles } = getScriptAndStyleDeps()

if (isBlockPreview) {
const { getCurrentBlock } = useBlock()
const block = getCurrentBlock()

const latestPage = {
...block,
page_content: getSchema()
}

return deepClone({
currentPage: latestPage,
ancestors: [],
scripts,
styles
})
}

const pageSchema = getPageSchema()
const currentPage = getCurrentPage()
const { getFamily } = usePage()
const latestPage = {
...currentPage,
page_content: pageSchema
}

const ancestors = await getFamily(latestPage)

return deepClone({
currentPage: latestPage,
ancestors,
scripts,
styles
})
}

// 当 schema 变化时发送更新
const sendSchemaUpdate = (data) => {
previewWindow.postMessage(
{
source: 'designer',
type: 'schema',
data
},
previewWindow.origin || window.location.origin
)
}

let hasSchemaChangeListener = false

const cleanupSchemaChangeListener = () => {
const { unsubscribe } = useMessage()
unsubscribe({
topic: 'schemaChange',
subscriber: 'preview-communication'
})
unsubscribe({
topic: 'schemaImport',
subscriber: 'preview-communication'
})
unsubscribe({
topic: 'pageOrBlockInit',
subscriber: 'preview-communication'
})
hasSchemaChangeListener = false
}

const handleSchemaChange = async () => {
// 如果预览窗口不存在或已关闭,则取消订阅
if (!previewWindow || previewWindow.closed) {
cleanupSchemaChangeListener()
return
}

const params = await getSchemaParams()
sendSchemaUpdate(params)
}

// 设置监听 schemaChange 事件,自动发送更新到预览页面
export const setupSchemaChangeListener = () => {
// 如果已经存在监听,则取消之前的监听
if (hasSchemaChangeListener) {
return
}

const { subscribe } = useMessage()

subscribe({
topic: 'schemaChange',
subscriber: 'preview-communication',
// 防抖更新,防止因为属性变化频繁触发
callback: useThrottleFn(handleSchemaChange, 1000, true)
})

subscribe({
topic: 'schemaImport',
subscriber: 'preview-communication',
callback: useThrottleFn(handleSchemaChange, 1000, true)
})

subscribe({
topic: 'pageOrBlockInit',
subscriber: 'preview-communication',
callback: handleSchemaChange
})

hasSchemaChangeListener = true
}

// 监听来自预览页面的消息
const setupMessageListener = () => {
window.addEventListener('message', async (event) => {
const parsedOrigin = new URL(event.origin)
const parsedHost = new URL(window.location.href)
// 确保消息来源安全
if (parsedOrigin.origin === parsedHost.origin || parsedOrigin.host === parsedHost.host) {
const { event: eventType, source } = event.data || {}
// 通过 heartbeat 消息来重新建立连接,避免刷新页面后 previewWindow 为 null
if (source === 'preview' && eventType === 'connect' && !previewWindow) {
Comment on lines +165 to +172
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

event.origin === "null" again crashes URL parsing

Same issue described for usePreviewCommunication.ts: parsing "null" with new URL() fails. Add an early guard:

-    const parsedOrigin = new URL(event.origin)
-    const parsedHost = new URL(window.location.href)
+    if (event.origin === 'null') return
+    const parsedOrigin = new URL(event.origin)
+    const parsedHost = new URL(window.location.href)

Prevents silent failures when the designer is served from file:// or similar contexts.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
window.addEventListener('message', async (event) => {
const parsedOrigin = new URL(event.origin)
const parsedHost = new URL(window.location.href)
// 确保消息来源安全
if (parsedOrigin.origin === parsedHost.origin || parsedOrigin.host === parsedHost.host) {
const { event: eventType, source } = event.data || {}
// 通过 heartbeat 消息来重新建立连接,避免刷新页面后 previewWindow 为 null
if (source === 'preview' && eventType === 'connect' && !previewWindow) {
window.addEventListener('message', async (event) => {
+ if (event.origin === 'null') return
const parsedOrigin = new URL(event.origin)
const parsedHost = new URL(window.location.href)
// 确保消息来源安全
if (parsedOrigin.origin === parsedHost.origin || parsedOrigin.host === parsedHost.host) {
const { event: eventType, source } = event.data || {}
// 通过 heartbeat 消息来重新建立连接,避免刷新页面后 previewWindow 为 null
if (source === 'preview' && eventType === 'connect' && !previewWindow) {
// ...
}
}
})

previewWindow = event.source
setupSchemaChangeListener()
}

if (source === 'preview' && eventType === 'onMounted' && previewWindow) {
const params = await getSchemaParams()
sendSchemaUpdate(params)
}
}
})

// 创建 BroadcastChannel 实例用于通信
const previewChannel = new BroadcastChannel('tiny-engine-preview-channel')

// 可能是刷新,需要重新建立连接
previewChannel.postMessage({
event: 'connect',
source: 'designer'
})

previewChannel.close()
}

// 初始化消息监听
setupMessageListener()

const handleHistoryPreview = (params, url) => {
let historyPreviewWindow = null
const handlePreviewReady = (event) => {
if (event.origin === window.location.origin || event.origin.includes(window.location.hostname)) {
const { event: eventType, source } = event.data || {}
if (source === 'preview' && eventType === 'onMounted' && historyPreviewWindow) {
const { scripts, styles, ancestors = [], ...rest } = params

historyPreviewWindow.postMessage(
{
source: 'designer',
type: 'schema',
data: deepClone({
currentPage: rest,
ancestors,
scripts,
styles
})
},
previewWindow.origin || window.location.origin
)

// 历史页面不需要实时更新预览,发送完消息后移除监听
window.removeEventListener('message', handlePreviewReady)
}
}
}

window.addEventListener('message', handlePreviewReady)

historyPreviewWindow = window.open(url, '_blank')
}

const getQueryParams = (params = {}, isHistory = false) => {
const paramsMap = new URLSearchParams(location.search)
const tenant = paramsMap.get('tenant') || ''
const pageId = paramsMap.get('pageid')
const blockId = paramsMap.get('blockid')
const theme = getMetaApi(META_SERVICE.ThemeSwitch)?.getThemeState()?.theme
const framework = getMergeMeta('engine.config')?.dslMode
const platform = getMergeMeta('engine.config')?.platformId
const { scripts, styles } = getScriptAndStyleDeps()

let query = `tenant=${tenant}&id=${paramsMap.get('id')}&theme=${theme}&framework=${framework}`

query += `&platform=${platform}&scripts=${JSON.stringify(scripts)}&styles=${JSON.stringify(styles)}`

if (pageId) {
query += `&pageid=${pageId}`
}

if (blockId) {
query += `&blockid=${blockId}`
}

if (isHistory) {
query += `&history=${params.history}`
}

return query
}

const open = (params = {}, isHistory = false) => {
const href = window.location.href.split('?')[0] || './'
const tenant = new URLSearchParams(location.search).get('tenant') || ''
const { scripts, styles } = getScriptAndStyleDeps()
const query = getQueryParams(params, isHistory)

let openUrl = ''
const hashString = utoa(JSON.stringify(params))

openUrl = isDevelopEnv
? `./preview.html?tenant=${tenant}#${hashString}`
: `${href.endsWith('/') ? href : `${href}/`}preview?tenant=${tenant}#${hashString}`
// 从预览组件配置获取自定义URL
const customPreviewUrl = getMergeMeta('engine.toolbars.preview')?.options?.previewUrl
const defaultPreviewUrl = isDevelopEnv ? `./preview.html` : `${href.endsWith('/') ? href : `${href}/`}preview`

const aTag = document.createElement('a')
aTag.href = openUrl
aTag.target = '_blank'
aTag.click()
}
if (customPreviewUrl) {
// 如果配置了自定义预览URL,则使用自定义URL
openUrl =
typeof customPreviewUrl === 'function'
? customPreviewUrl(defaultPreviewUrl, query)
: `${customPreviewUrl}?${query}`
} else {
// 否则使用默认生成的URL
openUrl = `${defaultPreviewUrl}?${query}`
}

if (isHistory) {
handleHistoryPreview({ ...params, scripts, styles }, openUrl)
return
}

if (previewWindow && !previewWindow.closed) {
// 如果预览窗口存在,则聚焦预览窗口
previewWindow.focus()
return
}

// 打开新窗口并保存引用
previewWindow = window.open(openUrl, '_blank')

Comment on lines +288 to 296
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Existing preview window is focused but not refreshed

When the preview window already exists, open() simply focuses it and returns. Query-string changes (tenant, theme, history, etc.) or a full schema refresh will not be reflected until the next schema mutation occurs, leading to stale previews.

Minimal fix – trigger an immediate sync before early-return:

-  if (previewWindow && !previewWindow.closed) {
-    // 如果预览窗口存在,则聚焦预览窗口
-    previewWindow.focus()
-    return
-  }
+  if (previewWindow && !previewWindow.closed) {
+    previewWindow.focus()
+    // force immediate schema push so that deps / params stay in sync
+    getSchemaParams().then(sendSchemaUpdate).catch(console.error)
+    return
+  }

Ensures the user sees the latest state without needing an extra schema-changing action.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (previewWindow && !previewWindow.closed) {
// 如果预览窗口存在,则聚焦预览窗口
previewWindow.focus()
return
}
// 打开新窗口并保存引用
previewWindow = window.open(openUrl, '_blank')
if (previewWindow && !previewWindow.closed) {
previewWindow.focus()
// force immediate schema push so that deps / params stay in sync
getSchemaParams().then(sendSchemaUpdate).catch(console.error)
return
}
// 打开新窗口并保存引用
previewWindow = window.open(openUrl, '_blank')

export const previewPage = (params = {}) => {
params.type = COMPONENT_NAME.Page
open(params)
// 设置 schemaChange 事件监听
setupSchemaChangeListener()
}

export const previewBlock = (params = {}) => {
params.type = COMPONENT_NAME.Block
open(params)
export const previewPage = (params = {}, isHistory = false) => {
open(params, isHistory)
}
2 changes: 1 addition & 1 deletion packages/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@opentiny/tiny-engine-meta-register": "workspace:*",
"@opentiny/tiny-engine-utils": "workspace:*",
"@vue/shared": "^3.3.4",
"@vueuse/core": "^9.6.0",
"axios": "~0.28.0",
"css-tree": "^2.3.1",
"eslint-linter-browserify": "8.57.0",
Expand All @@ -50,7 +51,6 @@
"@opentiny/tiny-engine-vite-plugin-meta-comments": "workspace:*",
"@vitejs/plugin-vue": "^5.1.2",
"@vitejs/plugin-vue-jsx": "^4.0.1",
"@vueuse/core": "^9.6.0",
"glob": "^10.3.4",
"vite": "^5.4.2"
},
Expand Down
1 change: 1 addition & 0 deletions packages/design-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
"devDependencies": {
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@types/babel__core": "^7.20.5",
"@types/node": "^18.0.0",
"@vitejs/plugin-vue": "^5.1.2",
"@vitejs/plugin-vue-jsx": "^4.0.1",
Expand Down
18 changes: 14 additions & 4 deletions packages/design-core/src/preview/src/Toolbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@
</template>

<script lang="jsx">
import { watch } from 'vue'
import { useBreadcrumb, getMergeRegistry, getMergeMeta } from '@opentiny/tiny-engine-meta-register'
import { Switch as TinySwitch } from '@opentiny/vue'
import { getSearchParams } from './preview/http'
import { constants } from '@opentiny/tiny-engine-utils'
import { BROADCAST_CHANNEL } from '../src/preview/srcFiles/constant'
import { injectDebugSwitch } from './preview/debugSwitch'
import { previewState } from './preview/usePreviewData'

export default {
components: {
Expand All @@ -30,10 +32,18 @@ export default {
const ChangeLang = getMergeRegistry('toolbars', 'engine.toolbars.lang')?.entry
const langOptions = getMergeMeta('engine.toolbars.lang').options
const ToolbarMedia = null // TODO: Media plugin rely on layout/canvas. Further processing is required.
const { setBreadcrumbPage, setBreadcrumbBlock } = useBreadcrumb()

const { setBreadcrumbPage } = useBreadcrumb()
const { pageInfo } = getSearchParams()
setBreadcrumbPage([pageInfo?.name])
watch(
() => previewState.currentPage,
(newVal) => {
if (newVal?.page_content?.componentName === constants.COMPONENT_NAME.Block) {
setBreadcrumbBlock([newVal?.name_cn || newVal?.page_content?.fileName])
} else {
setBreadcrumbPage([newVal?.name])
}
}
)

Comment on lines +35 to 47
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Ensure breadcrumb is initialized on first render

watch will not run until previewState.currentPage changes, so if currentPage is already populated when the component mounts the breadcrumb stays empty.
Add { immediate: true } to invoke the callback once during setup.

-    watch(
-      () => previewState.currentPage,
-      (newVal) => {
+    watch(
+      () => previewState.currentPage,
+      (newVal) => {
         if (newVal?.page_content?.componentName === constants.COMPONENT_NAME.Block) {
           setBreadcrumbBlock([newVal?.name_cn || newVal?.page_content?.fileName])
         } else {
           setBreadcrumbPage([newVal?.name])
         }
-      }
+      },
+      { immediate: true }
     )

🏁 Script executed:

#!/bin/bash
# Locate Toolbar.vue to confirm import and signature of watch
fd --extension vue Toolbar.vue

# Inspect the top of the file to see how watch is imported
sed -n '1,20p' packages/design-core/src/preview/src/Toolbar.vue

Length of output: 959


Add immediate option to breadcrumb watcher
To ensure the breadcrumb initializes on mount, invoke the watcher immediately by adding { immediate: true }:

File: packages/design-core/src/preview/src/Toolbar.vue
Lines: ~35

 watch(
   () => previewState.currentPage,
   (newVal) => {
     if (newVal?.page_content?.componentName === constants.COMPONENT_NAME.Block) {
       setBreadcrumbBlock([newVal?.name_cn || newVal?.page_content?.fileName])
     } else {
       setBreadcrumbPage([newVal?.name])
     }
-  }
+  }, 
+  { immediate: true }
 )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { setBreadcrumbPage, setBreadcrumbBlock } = useBreadcrumb()
const { setBreadcrumbPage } = useBreadcrumb()
const { pageInfo } = getSearchParams()
setBreadcrumbPage([pageInfo?.name])
watch(
() => previewState.currentPage,
(newVal) => {
if (newVal?.page_content?.componentName === constants.COMPONENT_NAME.Block) {
setBreadcrumbBlock([newVal?.name_cn || newVal?.page_content?.fileName])
} else {
setBreadcrumbPage([newVal?.name])
}
}
)
const { setBreadcrumbPage, setBreadcrumbBlock } = useBreadcrumb()
watch(
() => previewState.currentPage,
(newVal) => {
if (newVal?.page_content?.componentName === constants.COMPONENT_NAME.Block) {
setBreadcrumbBlock([newVal?.name_cn || newVal?.page_content?.fileName])
} else {
setBreadcrumbPage([newVal?.name])
}
},
{ immediate: true }
)

const setViewPort = (item) => {
const iframe = document.getElementsByClassName('iframe-container')[0]
Expand Down
Loading