From 7b0e48cc79598c13a8da3fdcee92f315b4cc80c8 Mon Sep 17 00:00:00 2001 From: chilingling Date: Fri, 28 Mar 2025 16:44:36 +0800 Subject: [PATCH 1/9] feat: preview support auto update --- packages/common/js/preview.js | 264 ++++++++++-- packages/common/package.json | 2 +- packages/design-core/package.json | 1 + .../design-core/src/preview/src/Toolbar.vue | 18 +- .../src/preview/src/preview/Preview.vue | 182 +------- .../src/preview/src/preview/generate.js | 14 +- .../src/preview/src/preview/http.js | 18 +- .../src/preview/src/preview/importMap.js | 5 +- .../src/preview/usePreviewCommunication.ts | 88 ++++ .../src/preview/src/preview/usePreviewData.ts | 396 ++++++++++++++++++ packages/plugins/block/src/BlockSetting.vue | 32 +- packages/plugins/page/src/PageHistory.vue | 14 +- .../plugins/page/src/composable/usePage.ts | 30 +- packages/toolbars/preview/src/Main.vue | 41 +- 14 files changed, 815 insertions(+), 290 deletions(-) create mode 100644 packages/design-core/src/preview/src/preview/usePreviewCommunication.ts create mode 100644 packages/design-core/src/preview/src/preview/usePreviewData.ts diff --git a/packages/common/js/preview.js b/packages/common/js/preview.js index 9b04a081d..2e14ac1eb 100644 --- a/packages/common/js/preview.js +++ b/packages/common/js/preview.js @@ -10,53 +10,257 @@ * */ -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 + }, + '*' + ) +} + +// 监听来自预览页面的消息 +const setupMessageListener = () => { + window.addEventListener('message', async (event) => { + // 确保消息来源安全 + if (event.origin === window.location.origin || event.origin.includes(window.location.hostname)) { + const { event: eventType, source } = event.data || {} + // 通过 heartbeat 消息来重新建立连接,避免刷新页面后 previewWindow 为 null + if (source === 'preview' && eventType === 'heartbeat' && !previewWindow) { + previewWindow = event.source + } + + if (source === 'preview' && eventType === 'onMounted' && previewWindow) { + const params = await getSchemaParams() + sendSchemaUpdate(params) + } + } + }) +} + +// 初始化消息监听 +setupMessageListener() + +let schemaChangeListener = null +const handleSchemaChange = async () => { + const { unsubscribe } = useMessage() + // 如果预览窗口不存在或已关闭,则取消订阅 + if (!previewWindow || previewWindow.closed) { + unsubscribe({ + topic: 'schemaChange', + subscriber: 'preview-communication' + }) + unsubscribe({ + topic: 'schemaImport', + subscriber: 'preview-communication' + }) + unsubscribe({ + topic: 'pageOrBlockInit', + subscriber: 'preview-communication' + }) + schemaChangeListener = null + return + } + + const params = await getSchemaParams() + sendSchemaUpdate(params) +} + +// 设置监听 schemaChange 事件,自动发送更新到预览页面 +export const setupSchemaChangeListener = () => { + // 如果已经存在监听,则取消之前的监听 + if (schemaChangeListener) { + return + } + const { subscribe } = useMessage() + + schemaChangeListener = 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 + }) +} + +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 + }) + }, + '*' + ) + } + } + } + + 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}` + openUrl = isDevelopEnv ? `./preview.html?${query}` : `${href.endsWith('/') ? href : `${href}/`}preview?${query}` - const aTag = document.createElement('a') - aTag.href = openUrl - aTag.target = '_blank' - aTag.click() -} + if (isHistory) { + handleHistoryPreview({ ...params, scripts, styles }, openUrl) + return + } + + if (previewWindow && !previewWindow.closed) { + // 如果预览窗口存在,则聚焦预览窗口 + previewWindow.focus() + 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) } diff --git a/packages/common/package.json b/packages/common/package.json index 6985ae98e..c8b795e9b 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -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", @@ -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" }, diff --git a/packages/design-core/package.json b/packages/design-core/package.json index f8ca8ee27..98e296d95 100644 --- a/packages/design-core/package.json +++ b/packages/design-core/package.json @@ -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", diff --git a/packages/design-core/src/preview/src/Toolbar.vue b/packages/design-core/src/preview/src/Toolbar.vue index 3c5c7c105..b6d1e95c9 100644 --- a/packages/design-core/src/preview/src/Toolbar.vue +++ b/packages/design-core/src/preview/src/Toolbar.vue @@ -14,11 +14,13 @@ ' + } + + const transformedScript = transformSync(p1, { + babelrc: false, + plugins: [[vueJsx, { pragma: 'h' }]], + sourceMaps: false, + configFile: false + }) + + const res = `' + + return `${res}${endTag}` + }) + + newFiles[panelName] = newPanelValue + } + + // 根据新的参数更新预览 + const updatePreview = async (params: { currentPage: IPage; ancestors: IPage[] }) => { + const { appData, metaData, importMapData } = await getBasicData(basicFiles) + + previewState.currentPage = params.currentPage + previewState.ancestors = params.ancestors + + store.setImportMap(importMapData) + + const blockSet = new Set() + + let blocks = [] + // @ts-ignore + const { getAllNestedBlocksSchema, generatePageCode } = getMetaApi('engine.service.generateCode') + + if (params.ancestors?.length) { + const promises = params.ancestors.map((item) => + getAllNestedBlocksSchema(item.page_content, fetchBlockSchema, blockSet) + ) + blocks = (await Promise.all(promises)).flat() + } + + const currentPageBlocks = await getAllNestedBlocksSchema( + params.currentPage?.page_content || {}, + fetchBlockSchema, + blockSet + ) + blocks = blocks.concat(currentPageBlocks) + + const pageCode = [ + ...getPageAncestryFiles(appData, params), + ...(blocks || []).map((blockSchema) => { + return { + panelName: `${blockSchema.fileName}.vue`, + panelValue: generatePageCode(blockSchema, appData?.componentsMap || [], { blockRelativePath: './' }) || '', + panelType: 'vue' + } + }) + ] + + const newFiles = store.getFiles() + const searchParams = new URLSearchParams(location.search) + const appJsCode = processAppJsCode(newFiles['app.js'], JSON.parse(searchParams.get('styles') || '{}')) + + newFiles['app.js'] = appJsCode + + pageCode.map(fixScriptLang).forEach((item) => assignFiles(item, newFiles)) + + const metaFiles = generateMetaFiles(metaData) + Object.assign(newFiles, metaFiles) + + setFiles(newFiles) + } + + const loadInitialData = async () => { + const { currentPage, ancestors } = await getPageOrBlockByApi() + previewState.currentPage = currentPage + previewState.ancestors = ancestors + + if (currentPage) { + updatePreview({ currentPage, ancestors }) + } + } + + return { + loadInitialData, + updateUrl, + updatePreview + } +} diff --git a/packages/plugins/block/src/BlockSetting.vue b/packages/plugins/block/src/BlockSetting.vue index c8dc7d382..2a022ecbe 100644 --- a/packages/plugins/block/src/BlockSetting.vue +++ b/packages/plugins/block/src/BlockSetting.vue @@ -87,16 +87,9 @@