Skip to content
New issue

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

feat: materials new protocol #940

Open
wants to merge 10 commits into
base: refactor/develop
Choose a base branch
from
233 changes: 49 additions & 184 deletions designer-demo/public/mock/bundle.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion mockServer/src/mock/get/app-center/v1/apps/schema/918.json
Original file line number Diff line number Diff line change
Expand Up @@ -2092,7 +2092,8 @@
"value": "",
"package": "axios",
"destructuring": false,
"exportName": "axios"
"exportName": "axios",
"cdnLink": "https://unpkg.com/browse/[email protected]/dist/esm/axios.min.js"
}
},
{
Expand Down
18 changes: 13 additions & 5 deletions packages/canvas/DesignCanvas/src/DesignCanvas.vue
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,20 @@ export default {
const canvasRef = ref(null)
let showModal = false // 弹窗标识
const { canvasSrc = '' } = getOptions(meta.id) || {}
let canvasSrcDoc = ''
const canvasSrcDoc = ref('')

if (!canvasSrc) {
const { importMap, importStyles } = getImportMapData(getMergeMeta('engine.config')?.importMapVersion)
canvasSrcDoc = initCanvas(importMap, importStyles).html
}
useMessage().subscribe({
topic: 'init_canvas_deps',
callback: (deps) => {
if (canvasSrc) {
return
}

const { importMap, importStyles } = getImportMapData(getMergeMeta('engine.config')?.importMapVersion, deps)

canvasSrcDoc.value = initCanvas(importMap, importStyles).html
}
})
Comment on lines +64 to +75
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

Add error handling for dependency initialization

While the message subscription implementation looks good, it should handle potential errors during import map generation.

Consider applying this improvement:

 useMessage().subscribe({
   topic: 'init_canvas_deps',
   callback: (deps) => {
     if (canvasSrc) {
       return
     }
+    try {
       const { importMap, importStyles } = getImportMapData(getMergeMeta('engine.config')?.importMapVersion, deps)
       canvasSrcDoc.value = initCanvas(importMap, importStyles).html
+    } catch (error) {
+      console.error('Failed to initialize canvas dependencies:', error)
+      // Consider showing a user-friendly error message
+    }
   }
 })
📝 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
useMessage().subscribe({
topic: 'init_canvas_deps',
callback: (deps) => {
if (canvasSrc) {
return
}
const { importMap, importStyles } = getImportMapData(getMergeMeta('engine.config')?.importMapVersion, deps)
canvasSrcDoc.value = initCanvas(importMap, importStyles).html
}
})
useMessage().subscribe({
topic: 'init_canvas_deps',
callback: (deps) => {
if (canvasSrc) {
return
}
try {
const { importMap, importStyles } = getImportMapData(getMergeMeta('engine.config')?.importMapVersion, deps)
canvasSrcDoc.value = initCanvas(importMap, importStyles).html
} catch (error) {
console.error('Failed to initialize canvas dependencies:', error)
// Consider showing a user-friendly error message
}
}
})


const removeNode = (node) => {
const { pageState } = useCanvas()
Expand Down
13 changes: 10 additions & 3 deletions packages/canvas/DesignCanvas/src/importMap.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { VITE_CDN_DOMAIN } from '@opentiny/tiny-engine-common/js/environments'

export function getImportMapData(overrideVersions = {}) {
export function getImportMapData(overrideVersions = {}, canvasDeps = { scripts: [], styles: [] }) {
const importMapVersions = Object.assign(
{
vue: '3.4.23',
Expand Down Expand Up @@ -32,16 +32,23 @@ export function getImportMapData(overrideVersions = {}) {
}
}

const materialRequire = canvasDeps.scripts.reduce((imports, { package: pkg, script }) => {
imports[pkg] = script

return imports
}, {})

const importMap = {
imports: {
vue: `${VITE_CDN_DOMAIN}/vue@${importMapVersions.vue}/dist/vue.runtime.esm-browser.prod.js`,
'vue-i18n': `${VITE_CDN_DOMAIN}/vue-i18n@${importMapVersions.vueI18n}/dist/vue-i18n.esm-browser.js`,
...blockRequire.imports,
...tinyVueRequire.imports
...tinyVueRequire.imports,
...materialRequire
}
}

const importStyles = [...blockRequire.importStyles]
const importStyles = [...blockRequire.importStyles, ...canvasDeps.styles]

return {
importMap,
Expand Down
67 changes: 55 additions & 12 deletions packages/canvas/common/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,23 +56,66 @@ export const copyObject = (node) => {
}

/**
* 动态导入组件,缓存组件对象
* @param {object} param0 组件的依赖: { package: 包名,script:js文件cdn, components:组件id和导出组件名的映射关系}
* 从页面importmap中获取模块的名称
* @returns importmap中模块的名称集合
*/
const getImportMapKeys = () => {
try {
const importMapElement = document.querySelector('script[type="importmap"]')

if (!importMapElement) {
return []
}

const importMaps = importMapElement.textContent
const importMapObject = JSON.parse(importMaps)

return Object.keys(importMapObject.imports)
} catch (error) {
return []
}
}

/**
* 动态导入获取组件库模块
* @param {*} pkg 模块名称
* @param {*} script 模块的cdn地址
* @returns
*/
export const dynamicImportComponents = async ({ package: pkg, script, components }) => {
if (!script) return
const scriptUrl = script.startsWith('.') ? new URL(script, location.href).href : script
const dynamicImportComponentLib = async ({ pkg, script }) => {
if (window.TinyComponentLibs[pkg]) {
return window.TinyComponentLibs[pkg]
}

if (!window.TinyComponentLibs[pkg]) {
const modules = await import(/* @vite-ignore */ scriptUrl)
let modules = {}

window.TinyComponentLibs[pkg] = modules
try {
// 优先从importmap导入,兼容npm.script字段定义的cdn地址
if (getImportMapKeys().includes(pkg)) {
Copy link
Member

Choose a reason for hiding this comment

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

这里建议做一下缓存,不然每次调用 getImportMapKeys 都要重新读取一次。

modules = await import(/* @vite-ignore */ pkg)
} else if (script) {
modules = await import(/* @vite-ignore */ script)
}
} catch (error) {
modules = {}
}

Object.entries(components).forEach(([componentId, exportName]) => {
const modules = window.TinyComponentLibs[pkg]
window.TinyComponentLibs[pkg] = modules

return modules
}
Comment on lines +85 to +106
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

Improve error handling and input validation.

The current implementation has several areas for improvement:

  1. Silent error handling might hide critical issues
  2. Missing input parameter validation
  3. No timeout handling for dynamic imports

Consider applying these improvements:

-const dynamicImportComponentLib = async ({ pkg, script }) => {
+const dynamicImportComponentLib = async ({ pkg, script } = {}) => {
+  if (!pkg) {
+    throw new Error('Package name is required')
+  }
+
   if (window.TinyComponentLibs[pkg]) {
     return window.TinyComponentLibs[pkg]
   }

   let modules = {}

+  const importWithTimeout = (url) => {
+    const timeout = 30000 // 30 seconds
+    return Promise.race([
+      import(/* @vite-ignore */ url),
+      new Promise((_, reject) =>
+        setTimeout(() => reject(new Error(`Import timeout: ${url}`)), timeout)
+      )
+    ])
+  }

   try {
     if (getImportMapKeys().includes(pkg)) {
-      modules = await import(/* @vite-ignore */ pkg)
+      modules = await importWithTimeout(pkg)
     } else if (script) {
-      modules = await import(/* @vite-ignore */ script)
+      modules = await importWithTimeout(script)
     }
   } catch (error) {
-    modules = {}
+    console.error(`Failed to load component library ${pkg}:`, error)
+    throw error
   }

   window.TinyComponentLibs[pkg] = modules
   return modules
 }
📝 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 dynamicImportComponentLib = async ({ pkg, script }) => {
if (window.TinyComponentLibs[pkg]) {
return window.TinyComponentLibs[pkg]
}
if (!window.TinyComponentLibs[pkg]) {
const modules = await import(/* @vite-ignore */ scriptUrl)
let modules = {}
window.TinyComponentLibs[pkg] = modules
try {
// 优先从importmap导入,兼容npm.script字段定义的cdn地址
if (getImportMapKeys().includes(pkg)) {
modules = await import(/* @vite-ignore */ pkg)
} else if (script) {
modules = await import(/* @vite-ignore */ script)
}
} catch (error) {
modules = {}
}
Object.entries(components).forEach(([componentId, exportName]) => {
const modules = window.TinyComponentLibs[pkg]
window.TinyComponentLibs[pkg] = modules
return modules
}
const dynamicImportComponentLib = async ({ pkg, script } = {}) => {
if (!pkg) {
throw new Error('Package name is required')
}
if (window.TinyComponentLibs[pkg]) {
return window.TinyComponentLibs[pkg]
}
let modules = {}
const importWithTimeout = (url) => {
const timeout = 30000 // 30 seconds
return Promise.race([
import(/* @vite-ignore */ url),
new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Import timeout: ${url}`)), timeout)
)
])
}
try {
if (getImportMapKeys().includes(pkg)) {
modules = await importWithTimeout(pkg)
} else if (script) {
modules = await importWithTimeout(script)
}
} catch (error) {
console.error(`Failed to load component library ${pkg}:`, error)
throw error
}
window.TinyComponentLibs[pkg] = modules
return modules
}


/**
* 获取组件对象并缓存,组件渲染时使用
* @param {object} param0 组件的依赖: { package: 包名,script:js文件cdn, components:组件id和导出组件名的映射关系}
* @returns
*/
export const getComponents = async ({ package: pkg, script, components }) => {
if (!pkg) return

const modules = await dynamicImportComponentLib({ pkg, script })

Object.entries(components).forEach(([componentId, exportName]) => {
if (!window.TinyLowcodeComponent[componentId]) {
window.TinyLowcodeComponent[componentId] = modules[exportName]
}
Expand All @@ -85,12 +128,12 @@ export const dynamicImportComponents = async ({ package: pkg, script, components
*/
export const updateDependencies = ({ detail }) => {
const { scripts = [], styles = [] } = detail || {}
const { styles: canvasStyles } = window.thirdPartyDeps
const { styles: canvasStyles } = window.componentsDepsMap
const newStyles = [...styles].filter((item) => !canvasStyles.has(item))

newStyles.forEach((item) => canvasStyles.add(item))

const promises = [...newStyles].map((src) => addStyle(src)).concat(scripts.map(dynamicImportComponents))
const promises = [...newStyles].map((src) => addStyle(src)).concat(scripts.map(getComponents))

Promise.allSettled(promises)
}
Comment on lines +131 to 139
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

Improve error handling for failed dependencies.

The function should handle and report failed dependency loads from Promise.allSettled.

Consider this improvement:

 export const updateDependencies = ({ detail }) => {
   const { scripts = [], styles = [] } = detail || {}
   const { styles: canvasStyles } = window.componentsDepsMap
   const newStyles = [...styles].filter((item) => !canvasStyles.has(item))
 
   newStyles.forEach((item) => canvasStyles.add(item))
 
   const promises = [...newStyles].map((src) => addStyle(src)).concat(scripts.map(getComponents))
 
-  Promise.allSettled(promises)
+  Promise.allSettled(promises).then((results) => {
+    const failures = results
+      .filter(({ status }) => status === 'rejected')
+      .map(({ reason }) => reason)
+    if (failures.length > 0) {
+      console.error('Failed to load some dependencies:', failures)
+    }
+  })
 }
📝 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 { styles: canvasStyles } = window.componentsDepsMap
const newStyles = [...styles].filter((item) => !canvasStyles.has(item))
newStyles.forEach((item) => canvasStyles.add(item))
const promises = [...newStyles].map((src) => addStyle(src)).concat(scripts.map(dynamicImportComponents))
const promises = [...newStyles].map((src) => addStyle(src)).concat(scripts.map(getComponents))
Promise.allSettled(promises)
}
const { styles: canvasStyles } = window.componentsDepsMap
const newStyles = [...styles].filter((item) => !canvasStyles.has(item))
newStyles.forEach((item) => canvasStyles.add(item))
const promises = [...newStyles].map((src) => addStyle(src)).concat(scripts.map(getComponents))
Promise.allSettled(promises).then((results) => {
const failures = results
.filter(({ status }) => status === 'rejected')
.map(({ reason }) => reason)
if (failures.length > 0) {
console.error('Failed to load some dependencies:', failures)
}
})
}

4 changes: 2 additions & 2 deletions packages/canvas/container/src/CanvasContainer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
<script>
import { onMounted, ref, computed, onUnmounted, watch, watchEffect } from 'vue'
import { iframeMonitoring } from '@opentiny/tiny-engine-common/js/monitor'
import { useTranslate, useCanvas, useMaterial, useMessage, useResource } from '@opentiny/tiny-engine-meta-register'
import { useTranslate, useCanvas, useMessage, useResource } from '@opentiny/tiny-engine-meta-register'
import { NODE_UID, NODE_LOOP, DESIGN_MODE } from '../../common'
import { registerHostkeyEvent, removeHostkeyEvent } from './keyboard'
import CanvasMenu, { closeMenu, openMenu } from './components/CanvasMenu.vue'
Expand Down Expand Up @@ -113,7 +113,7 @@ export default {
const beforeCanvasReady = () => {
if (iframe.value) {
const win = iframe.value.contentWindow
win.thirdPartyDeps = useMaterial().materialState.thirdPartyDeps
win.componentsDeps = useResource().resState.canvasDeps.scripts.filter((item) => item.components)
Copy link
Member

Choose a reason for hiding this comment

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

冲突要解一下, resState 变成 appSchemaState


const { subscribe, unsubscribe } = useMessage()
const { getSchemaDiff, patchLatestSchema, getSchema, getNode } = useCanvas()
Expand Down
1 change: 0 additions & 1 deletion packages/canvas/container/src/container.js
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,5 @@ export const initCanvas = ({ renderer, iframe, emit, controller }) => {
}

setConfigure(useMaterial().getConfigureMap())
canvasDispatch('updateDependencies', { detail: useMaterial().materialState.thirdPartyDeps })
canvasState.loading = false
}
40 changes: 23 additions & 17 deletions packages/canvas/render/src/RenderMain.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@

import { h, provide, inject, nextTick, shallowReactive, reactive, ref, watch, watchEffect, onUnmounted } from 'vue'
import { I18nInjectionKey } from 'vue-i18n'
import TinyVue from '@opentiny/vue'
import * as TinyVueIcon from '@opentiny/vue-icon'
import { useBroadcastChannel, useThrottleFn } from '@vueuse/core'
import { constants, utils as commonUtils } from '@opentiny/tiny-engine-utils'
import renderer, {
Expand Down Expand Up @@ -70,7 +68,7 @@ const getDeletedKeys = (objA, objB) => {

const getUtils = () => utils

const setUtils = (data) => {
const setUtils = async (data) => {
if (!Array.isArray(data)) {
return
}
Expand All @@ -85,25 +83,33 @@ const setUtils = (data) => {
}

const utilsCollection = {}
// 目前画布还不具备远程加载utils工具类的功能,目前只能加载TinyVue组件库中的组件工具
data?.forEach((item) => {
const util = TinyVue[item.content.exportName]
if (util) {
utilsCollection[item.name] = util
}

// 此处需要把工具类中的icon图标也加入utils上下文环境
const utilIcon = TinyVueIcon[item.content.exportName]
if (utilIcon) {
utilsCollection[item.name] = utilIcon
}

// 解析函数式的工具类
if (item.type === 'function') {
data
.filter((item) => item.type === 'function')
.forEach((item) => {
const defaultFn = () => {}
utilsCollection[item.name] = generateFunction(item.content.value, context) || defaultFn
})

const npmUtils = data.filter((item) => item.type === 'npm' && item.content.cdnLink)
const results = await Promise.allSettled(npmUtils.map((item) => import(/* @vite-ignore */ item.content.package)))
Copy link
Member

Choose a reason for hiding this comment

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

这里要区分情况,一种情况是:在 importMap 已经存在,一种是不存在,不存在的情况下,我理解是 import 是需要读取 cdnLink 的


results.forEach((res, index) => {
if (res.status !== 'fulfilled') {
globalNotify({
type: 'error',
message: `工具类 ${npmUtils[index].name} 加载失败,请检查CDN地址是否配置正确`
})
return
}

const module = res.value
const { name, content } = npmUtils[index]
const { exportName, destructuring } = content

utilsCollection[name] = destructuring ? module[exportName] : module.default
})

Object.assign(utils, utilsCollection)

// 因为工具类并不具有响应式行为,所以需要通过修改key来强制刷新画布
Expand Down
7 changes: 3 additions & 4 deletions packages/canvas/render/src/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
*/

import { createApp } from 'vue'
import { addScript, addStyle, dynamicImportComponents, updateDependencies } from '../../common'
import { addScript, addStyle, getComponents, updateDependencies } from '../../common'
import TinyI18nHost, { I18nInjectionKey } from '@opentiny/tiny-engine-common/js/i18n'
import Main, { api } from './RenderMain'
import lowcode from './lowcode'
Expand Down Expand Up @@ -79,10 +79,9 @@ export const createRender = (config) => {
initRenderContext()

const { styles = [], scripts = [] } = config.canvasDependencies
const { styles: thirdStyles = [], scripts: thirdScripts = [] } = window.thirdPartyDeps || {}

Promise.all([
...thirdScripts.map(dynamicImportComponents),
...scripts.map((src) => addScript(src)).concat([...thirdStyles, ...styles].map((src) => addStyle(src)))
...window.componentsDeps.map(getComponents),
...scripts.map((src) => addScript(src)).concat(styles.map((src) => addStyle(src)))
]).finally(() => create(config))
}
6 changes: 4 additions & 2 deletions packages/common/js/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

import { constants } from '@opentiny/tiny-engine-utils'
import { isDevelopEnv } from './environments'
import { useMaterial } from '@opentiny/tiny-engine-meta-register'
import { useResource } from '@opentiny/tiny-engine-meta-register'
// prefer old unicode hacks for backward compatibility

const { COMPONENT_NAME } = constants
Expand All @@ -26,13 +26,15 @@ const open = (params = {}) => {
params.app = paramsMap.get('id')
params.tenant = paramsMap.get('tenant')

const { scripts, styles } = useMaterial().materialState.thirdPartyDeps
const { scripts, styles } = useResource().resState.canvasDeps
params.scripts = {}

scripts
.filter((item) => item.script)
.forEach((item) => {
params.scripts[item.package] = item.script
})

params.styles = [...styles]

const href = window.location.href.split('?')[0] || './'
Expand Down
13 changes: 1 addition & 12 deletions packages/design-core/src/preview/src/preview/Preview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -59,17 +59,6 @@ export default {
store['initTsConfig']() // 触发获取组件d.ts方便调试
}

const addUtilsImportMap = (importMap, utils = []) => {
const utilsImportMaps = {}
utils.forEach(({ type, content: { package: packageName, cdnLink } }) => {
if (type === 'npm' && cdnLink) {
utilsImportMaps[packageName] = cdnLink
}
})
const newImportMap = { imports: { ...importMap.imports, ...utilsImportMaps } }
store.setImportMap(newImportMap)
}

const queryParams = getSearchParams()
const getImportMap = async () => {
if (import.meta.env.VITE_LOCAL_BUNDLE_DEPS === 'true') {
Expand All @@ -91,7 +80,7 @@ export default {
getImportMap()
]
Promise.all(promiseList).then(async ([appData, metaData, _void, importMapData]) => {
addUtilsImportMap(importMapData, metaData.utils || [])
store.setImportMap(importMapData)

const { getAllNestedBlocksSchema, generatePageCode } = getMetaApi('engine.service.generateCode')

Expand Down
Loading
Loading