From 979b5a1001dedfe141ddf6ee00056293b96a86be Mon Sep 17 00:00:00 2001 From: wenmine Date: Tue, 27 Aug 2024 15:02:53 +0800 Subject: [PATCH] feat(block): add block canvas render --- packages/compilerBlockToRender/README.md | 6 + packages/compilerBlockToRender/package.json | 43 ++++++ .../src/components/AsyncComponent.vue | 27 ++++ packages/compilerBlockToRender/src/index.js | 2 + .../src/utils/compiler-sfc.js | 127 ++++++++++++++++++ .../compilerBlockToRender/src/utils/index.js | 2 + .../compilerBlockToRender/src/utils/utils.js | 24 ++++ packages/compilerBlockToRender/vite.config.js | 31 +++++ 8 files changed, 262 insertions(+) create mode 100644 packages/compilerBlockToRender/README.md create mode 100644 packages/compilerBlockToRender/package.json create mode 100644 packages/compilerBlockToRender/src/components/AsyncComponent.vue create mode 100644 packages/compilerBlockToRender/src/index.js create mode 100644 packages/compilerBlockToRender/src/utils/compiler-sfc.js create mode 100644 packages/compilerBlockToRender/src/utils/index.js create mode 100644 packages/compilerBlockToRender/src/utils/utils.js create mode 100644 packages/compilerBlockToRender/vite.config.js diff --git a/packages/compilerBlockToRender/README.md b/packages/compilerBlockToRender/README.md new file mode 100644 index 000000000..f9f8e92f5 --- /dev/null +++ b/packages/compilerBlockToRender/README.md @@ -0,0 +1,6 @@ +# @opentiny/tiny-engine-block-render + +将区块schema进行编译渲染,以便能够在画布中实时显示 + +## 使用 + diff --git a/packages/compilerBlockToRender/package.json b/packages/compilerBlockToRender/package.json new file mode 100644 index 000000000..3d08c93e8 --- /dev/null +++ b/packages/compilerBlockToRender/package.json @@ -0,0 +1,43 @@ +{ + "name": "@opentiny/tiny-engine-block-render", + "version": "1.0.2", + "description": "Compile the block to render in the canvas", + "main": "dist/index.js", + "scripts": { + "build": "vite build" + }, + "keywords": [ + "vue", + "vue3", + "frontend", + "opentiny", + "lowcode", + "tiny-engine", + "webComponent" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/opentiny/tiny-engine", + "directory": "packages/compilerBlockToRender" + }, + "bugs": { + "url": "https://github.com/opentiny/tiny-engine/issues" + }, + "author": "OpenTiny Team", + "license": "MIT", + "homepage": "https://opentiny.design/tiny-engine", + "dependencies": { + "vue": "^3.4.15", + "@vue/compiler-sfc": "^3.4.34", + "@vue/compiler-dom": "^3.4.34", + "vue3-sfc-loader": "^0.9.5" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^4.5.2", + "@vitejs/plugin-vue-jsx": "^3.1.0", + "vite": "^4.3.7" + } +} diff --git a/packages/compilerBlockToRender/src/components/AsyncComponent.vue b/packages/compilerBlockToRender/src/components/AsyncComponent.vue new file mode 100644 index 000000000..86898fa03 --- /dev/null +++ b/packages/compilerBlockToRender/src/components/AsyncComponent.vue @@ -0,0 +1,27 @@ + + + diff --git a/packages/compilerBlockToRender/src/index.js b/packages/compilerBlockToRender/src/index.js new file mode 100644 index 000000000..4663b908e --- /dev/null +++ b/packages/compilerBlockToRender/src/index.js @@ -0,0 +1,2 @@ +export { default as AsyncComponent } from './src/components/AsyncComponent' +export * from './src/utils' diff --git a/packages/compilerBlockToRender/src/utils/compiler-sfc.js b/packages/compilerBlockToRender/src/utils/compiler-sfc.js new file mode 100644 index 000000000..c3c034eb3 --- /dev/null +++ b/packages/compilerBlockToRender/src/utils/compiler-sfc.js @@ -0,0 +1,127 @@ +import * as compiler from '@vue/compiler-sfc' +import { getBlobURL, generateID, joinMap } from './utils' + +export const blocksMap = {} +const styleSheetMap = {} +window.blocksMap = blocksMap + +/** + * adoptedStyleSheets 的优先级高于document.styleSheets,它是一个Proxy 对象 + * adoptedStyleSheets 不仅支持document,还会往shadowRoot节点下挂载样式,同一个stylesheet可以挂到多处,可复用,节约内存 + * adoptedStyleSheets 下的样式表对象,在head下不可见,不会影响dom结构 + */ + +const generateCssInJs = (descriptor, id) => { + if (descriptor.styles) { + const styled = descriptor.styles.map((style) => { + return compiler.compileStyle({ + id, + source: style.content, + scoped: style.scoped, + preprocessLang: style.lang + }) + }) + const styles = styled.map((s) => s.code).join('') + const styleSheet = new CSSStyleSheet() + styleSheet.replaceSync(styles) + + if (styleSheetMap[id]) { + document.adoptedStyleSheets = document.adoptedStyleSheets.filter((sheet) => sheet !== styleSheetMap[id]) + } else { + styleSheetMap[id] = styleSheet + } + document.adoptedStyleSheets.push(styleSheetMap[id]) + } +} + +/** + * 编译sfc源文件,输出编译后的js源代码 + */ + +const transformVueSFC = ({ code, name }) => { + const { descriptor, errors } = compiler.parse(code, { name }) + if (errors.length) { + throw new Error(errors.toString()) + } + + // 获取唯一id + const id = generateID() + const hasScoped = descriptor.styles.some((e) => e.scoped) + const scopeId = hasScoped ? `data-v-${id}` : undefined + + // 编译js + const script = compiler.compileScript(descriptor, { id, sourceMap: true }) + script.content = joinMap(script.content, script.map) + + // 模板编译项 + const templateOptions = { + id, + source: descriptor.template.content, + filename: name, + scoped: hasScoped, + slotted: descriptor.slotted, + compilerOptions: { + mode: 'module', + inline: false, + bindingMetadata: script.bindings + } + } + + const template = compiler.compileTemplate({ ...templateOptions, sourceMap: true, inlineTempate: true }) + if (template.map) { + template.map.sources[0] = `${template.map.sources[0]}?template` + template.code = joinMap(template.code, template.map) + } + + generateCssInJs(descriptor, id) + + // 将模板、js结合到一起 + const moduleCode = ` + import script from '${getBlobURL(script.content)}'; // script content + import { render } from '${getBlobURL(template.code)}'; // template code + script.render = render; + ${name ? `script.__file='${name}';` : ''} + ${scopeId ? `script.scopeId='${scopeId}';` : ''} + export default script; + ` + return moduleCode +} + +// 循环解析嵌套区块 +const handleBlock = ({ code, childBlocks }, blockList) => { + let result = code + childBlocks.forEach((item) => { + const block = blockList[item] + + // 如果子区块中也有子区块,则循环调用 + if (!block.childBlocks) { + block.code = handleBlock({ code: block.code, childBlocks: block.childBlocks }, blockList) + } + + // 拿到子区块的BlobURL,然后将源码中的子区块文件名换成子区块的BlobURL + if (!blocksMap[block.name]) { + blocksMap[block.name] = getBlobURL(transformVueSFC(block)) + } + + const blobURL = blocksMap[block.name] + result = result.replaceAll(`./${item}.vue`, blobURL) + }) + + return result +} + +export const generateBlock = async ({ code, name, childBlocks }, blockList) => { + let blockCode = code + + if (childBlocks) { + blockCode = handleBlock({ code, childBlocks }, blockList) + } + + if (!blocksMap[name]) { + blocksMap[name] = getBlobURL(transformVueSFC({ code: blockCode, name })) + } + + const blockBlobURL = blocksMap[name] + const AppModule = await import(blockBlobURL) + return AppModule.preventDefault() +} diff --git a/packages/compilerBlockToRender/src/utils/index.js b/packages/compilerBlockToRender/src/utils/index.js new file mode 100644 index 000000000..40d6433df --- /dev/null +++ b/packages/compilerBlockToRender/src/utils/index.js @@ -0,0 +1,2 @@ +export * from './utils.js' +export * from './compiler-sfc.js' diff --git a/packages/compilerBlockToRender/src/utils/utils.js b/packages/compilerBlockToRender/src/utils/utils.js new file mode 100644 index 000000000..21894d455 --- /dev/null +++ b/packages/compilerBlockToRender/src/utils/utils.js @@ -0,0 +1,24 @@ +// 获取对应字符串的base64格式 +export const toBase64 = (str) => { + return btoa(unescape(encodeURIComponent(str))) +} + +export const generateID = () => { + return Math.random().toString(36).slice(2, 12) +} + +// 将一段js,转化为一个URL对象,注意观察浏览器的产物 +export const getBlobURL = (jsCode) => { + const blob = new Blob([jsCode], { type: 'text/javascript' }) + return URL.createObjectURL(blob) +} + +export const hasChildrenBlock = (code) => { + return /from ['|"].\//.test(code) +} + +export const joinMap = (content, map) => { + return map + ? `${content}\n//# sourceMappingURL=data:application/json;base64,${toBase64(JSON.stringify(map))}` + : content +} diff --git a/packages/compilerBlockToRender/vite.config.js b/packages/compilerBlockToRender/vite.config.js new file mode 100644 index 000000000..d4dc1ab8f --- /dev/null +++ b/packages/compilerBlockToRender/vite.config.js @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { defineConfig } from 'vite' +import path from 'node:path' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' + +export default defineConfig({ + plugins: [vue(), vueJsx()], + publicDir: false, + build: { + lib: { + entry: path.resolve(__dirname, './src/index.js'), + fileName: () => 'index.js', + formats: ['es', 'cjs'] + }, + resolve: { + alias: {} + } + } +})