Skip to content

Commit

Permalink
feat(block): add block canvas render
Browse files Browse the repository at this point in the history
  • Loading branch information
wenmine committed Aug 27, 2024
1 parent 3b20b8b commit 979b5a1
Show file tree
Hide file tree
Showing 8 changed files with 262 additions and 0 deletions.
6 changes: 6 additions & 0 deletions packages/compilerBlockToRender/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# @opentiny/tiny-engine-block-render

将区块schema进行编译渲染,以便能够在画布中实时显示

## 使用

43 changes: 43 additions & 0 deletions packages/compilerBlockToRender/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
27 changes: 27 additions & 0 deletions packages/compilerBlockToRender/src/components/AsyncComponent.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<template>
<div>
<Suspense>
<component :is="BlockComponent"></component>
<template #fallback>正在加载...</template>
</Suspense>
</div>
</template>

<script setup>
import { defineProps, defineAsyncComponent } from 'vue'
import { generateBlock } from '../utils'
const props = defineProps({
blockConfig: {
type: Object,
default: () => ({
code: `<template>
<h4>请传递对应区块源码</h4>
</template>`
})
}
})
const BlockComponent = defineAsyncComponent(async () => {
return await generateBlock(props.blockConfig)
})
</script>
2 changes: 2 additions & 0 deletions packages/compilerBlockToRender/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as AsyncComponent } from './src/components/AsyncComponent'
export * from './src/utils'
127 changes: 127 additions & 0 deletions packages/compilerBlockToRender/src/utils/compiler-sfc.js
Original file line number Diff line number Diff line change
@@ -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<Array> 对象
* 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()
}
2 changes: 2 additions & 0 deletions packages/compilerBlockToRender/src/utils/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './utils.js'
export * from './compiler-sfc.js'
24 changes: 24 additions & 0 deletions packages/compilerBlockToRender/src/utils/utils.js
Original file line number Diff line number Diff line change
@@ -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
}
31 changes: 31 additions & 0 deletions packages/compilerBlockToRender/vite.config.js
Original file line number Diff line number Diff line change
@@ -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: {}
}
}
})

0 comments on commit 979b5a1

Please sign in to comment.