遇到的问题:需要支持用户在线编写一段vue组件代码,并且显示出来,该组件在前台的特定地方会进行展示
解法之二是支持用户在线编写一段vue组件代码,并且能在前台展示出来
下面是一个实现思路(尝试使用vue3来实现vue2的一些代码,所以有些地方的api不是很合理,发现不合理的地方或更好的点子,欢迎指正)
为什么需要支持用户在线编写vue单组件代码片段
很多时候用户想要显式一个自定义的内容,这个内容它可能就是一个醒目的提示文字、一个获取数据之后自定义展示的样式、或者里面有一段js逻辑用来执行一个dom操作,并不会特别的复杂和庞大,代码数量在800行以内。对于这个数据的代码,基本是一个文件就可以容纳的下,也不需要工程化的管理,需要的是简单轻便,快速实时能看见效果
这个需求核心的点是:
- 用户可以在线编写vue单组件代码
- 可能需要使用容器提供的环境数据,比如上下文数据
思路
最终vue组件注册的api
对于单组件的注册使用在vue2里面有以下几种方式:
- 使用
Vue.component方法 - 使用
Vue.extend方法 - 使用new Vue方法
如果使用Vue.component方法?Vue.component方法是全局注册组件,我在固定的模版里面是不晓得当前的组件名称的,挂载的时候也不方便指定dom进行挂载
这里最终选择的是Vue.extend方法,因为它可以返回一个组件构造器,这个构造器可以在需要的时候进行实例化,从而实现动态组件的效果,通过mount方法挂载到页面上
用户输入的内容是什么?
用户输入的内容可以是任意的vue单组件代码片段,以下是一些示例:
一个vue模版文件,示例1:
<template>
<div>
<h1>{{ title }}</h1>
<p>{{ content }}</p>
</div>
</template>
<script>
export default {
data() {
return {
title: 'Hello World',
content: 'This is a content',
};
},
created() {
console.log('created');
};
</script>
一个JavaScript的描述对象,示例2:
export default {
template: `
<div>
<h1>{{ title }}</h1>
<p>{{ content }}</p>
</div>
`,
data() {
return {
title: 'Hello World',
content: 'This is a content',
};
},
created() {
console.log('created');
}
};
一个函数体的片段,示例3:
const tempObj = {
template: `
<div>
<h1>{{ title }}</h1>
<p>{{ content }}</p>
</div>
`,
data() {
return {
title: 'Hello World',
content: 'This is a content',
};
},
created() {
console.log('created');
},
}
return tempObj;
分析前我们先确认第二个需求点: 需要使用容器提供的环境数据,比如上下文数据
也就是说,我的组件里面可能获取以下数据:
- 如果该组件在列表里面展示,那么可能会有一个formListData的数据
- 如果该组件在编辑页面/详情页展示,那么可能会有一个formData的数据
- 如果该组件在流程中使用,那么可能会有一个flowData的数据
- 如果该组件在事件流中使用,那么可能会有一个eventData的数据
- ......
不同场景下需要的数据可能不一致,所以我们需要一个上下文数据,这个上下文数据是一个对象,里面包含了所有的数据,这个数据是容器提供的,我们可以在组件里面使用
分析如下:
示例1: vue模版方式的最符合用户的开发习惯,并且我们是vue2的技术。但是如何给到用户一个上下文数据呢?
可以通过props的方式传递,但是这样的话也是能获取对应的上下文参数的。有个问题用户在后台编写的时候写的是vue的模版代码,那么就是需要编译成js才能使用的,这个编译过程需要在线上完成,如何做是一个问题。也有一些支持在线编译vue文件的库比如vue3-sfc-loader
这篇文章里提到了juejin.cn/post/737553…,可以参考一下
示例2: JavaScript的描述对象,这个是最符合我们的需求的,因为我们可以直接使用这个对象,然后通过Vue.extend方法进行注册,这个对象里面可以直接使用上下文数据。但是这都是个对象了,再包装一层成为一个函数体,这样可操作的内容更多,并且函数式的思维好理解
示例3: 函数体的片段,这个也是可以的,但是这个函数体的片段需要返回一个对象,这个对象里面包含了组件的所有内容,这个函数体的片段可以直接使用上下文数据,模拟一个函数的执行环境获得最终的对象
最终的选择是示例3:
示例2和3 是一致的;示例1开发起来比较符合vue的开发习惯,但是业务上需要使用的场景足够简单,能用就行,加上vue3-sfc-loader的大小也比较的大且重
最终选的是:函数体的片段
示例demo
流程如下:
主要的逻辑代码如下(vue2 翻译过来的vue3示例代码):
DynamicLoader.vue:
<template>
<div class="dynamic-component" ref="container"></div>
</template>
<script setup>
// 引入必要的 Vue API
import {
ref,
onMounted,
getCurrentInstance,
defineProps,
reactive,
computed,
watch,
watchEffect,
nextTick,
provide,
inject,
onBeforeMount,
onUpdated,
onUnmounted,
onBeforeUnmount,
toRef,
toRefs,
// defineEmits,
// defineExpose,
useSlots,
useAttrs,
} from "vue";
import { createApp } from "vue";
import CodeEngine from "./CodeEngine";
const props = defineProps({
scriptPath: {
type: String,
required: true,
},
});
const container = ref(null);
const instance = ref(null);
const renderComponent = async () => {
if (!container.value) return;
try {
const response = await fetch(props.scriptPath);
if (!response.ok) {
throw new Error(`获取脚本失败: ${response.status}`);
}
const code = await response.text();
const apiContext = {
ref,
reactive,
computed,
watch,
watchEffect,
onMounted,
onBeforeMount,
onUpdated,
onUnmounted,
onBeforeUnmount,
nextTick,
provide,
inject,
toRef,
toRefs,
// defineEmits,
// defineExpose,
useSlots,
useAttrs,
// 可选的加入window对象
window,
};
// 执行前可先进行code校验检查...等操作
// 获取vueOption对象
const RemoteComponentOptions = CodeEngine.getInstance().executeTemplateFunc(
code,
apiContext
);
// 下面的操作也可封装到CodeEngine中,便于复用
if (instance.value) {
instance.value.unmount();
container.value.innerHTML = "";
}
const appContext = getCurrentInstance()?.appContext?.app;
const childApp = createApp(RemoteComponentOptions);
if (appContext?.config?.globalProperties) {
childApp.config.globalProperties = appContext.config.globalProperties;
}
instance.value = childApp.mount(container.value);
} catch (err) {
console.error("加载远程组件出错:", err);
}
};
onMounted(() => {
renderComponent();
});
</script>
app.vue:
<template>
<div id="app">
<h1>本地宿主应用</h1>
<DynamicLoader scriptPath="/my-remote-component.js" />
</div>
</template>
<script>
import DynamicLoader from "./DynamicLoader.vue";
export default {
name: "App",
components: {
DynamicLoader,
},
};
</script>
模版文件public/my-remote-component.js:
const vueComponentOptions = {
setup() {
const currentTime = ref(new Date().toLocaleString());
const updateTime = () => {
currentTime.value = new Date().toLocaleString();
};
return {
currentTime,
updateTime,
};
},
template: `
<div style="border: 2px solid blue; padding: 20px; margin: 20px;">
<h2>我是远程加载的组件</h2>
<p>当前时间: {{ currentTime }}</p>
<button @click="updateTime">更新时间</button>
</div>
`,
};
return vueComponentOptions;
结语
上面是一个可行示例,有很多可以拓展的地方,比如:
- 可以加入对代码的校验
- 组件实例化的逻辑抽离
- 支持特定种类的组件,比如用户常用的弹框提醒组件、表单组件等
- 上下文数据逻辑的优化
- 性能优化:同模版数据的缓存
- 引擎的优化:单独抽离出执行器
- 执行环境的安全问题
- css的处理加载
- ......
需要依据业务需求进行调整,使用的地方多的话,可以抽离为一个单独的依赖包,向外暴露CodeEngine方便在其他项目中使用
源码地址:
参考文章:
并且感谢公司前辈设计的代码逻辑