低代码平台支持用户在线编写vue单组件代码片段

433 阅读6分钟

遇到的问题:需要支持用户在线编写一段vue组件代码,并且显示出来,该组件在前台的特定地方会进行展示

解法之二是支持用户在线编写一段vue组件代码,并且能在前台展示出来

下面是一个实现思路(尝试使用vue3来实现vue2的一些代码,所以有些地方的api不是很合理,发现不合理的地方或更好的点子,欢迎指正)

为什么需要支持用户在线编写vue单组件代码片段

很多时候用户想要显式一个自定义的内容,这个内容它可能就是一个醒目的提示文字、一个获取数据之后自定义展示的样式、或者里面有一段js逻辑用来执行一个dom操作,并不会特别的复杂和庞大,代码数量在800行以内。对于这个数据的代码,基本是一个文件就可以容纳的下,也不需要工程化的管理,需要的是简单轻便,快速实时能看见效果

这个需求核心的点是:

  1. 用户可以在线编写vue单组件代码
  2. 可能需要使用容器提供的环境数据,比如上下文数据

思路

最终vue组件注册的api

对于单组件的注册使用在vue2里面有以下几种方式:

  1. 使用Vue.component方法
  2. 使用Vue.extend方法
  3. 使用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;

分析前我们先确认第二个需求点: 需要使用容器提供的环境数据,比如上下文数据

也就是说,我的组件里面可能获取以下数据:

  1. 如果该组件在列表里面展示,那么可能会有一个formListData的数据
  2. 如果该组件在编辑页面/详情页展示,那么可能会有一个formData的数据
  3. 如果该组件在流程中使用,那么可能会有一个flowData的数据
  4. 如果该组件在事件流中使用,那么可能会有一个eventData的数据
  5. ......

不同场景下需要的数据可能不一致,所以我们需要一个上下文数据,这个上下文数据是一个对象,里面包含了所有的数据,这个数据是容器提供的,我们可以在组件里面使用

分析如下:

示例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

流程如下:

img_2025_02_22_21_43_26.png

主要的逻辑代码如下(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;

img_2025_02_23_21_44_26.png

结语

上面是一个可行示例,有很多可以拓展的地方,比如:

  • 可以加入对代码的校验
  • 组件实例化的逻辑抽离
  • 支持特定种类的组件,比如用户常用的弹框提醒组件、表单组件等
  • 上下文数据逻辑的优化
  • 性能优化:同模版数据的缓存
  • 引擎的优化:单独抽离出执行器
  • 执行环境的安全问题
  • css的处理加载
  • ......

需要依据业务需求进行调整,使用的地方多的话,可以抽离为一个单独的依赖包,向外暴露CodeEngine方便在其他项目中使用

源码地址:

github.com/Aoyia/low-c…

参考文章:

juejin.cn/post/716810…

juejin.cn/post/737553…

并且感谢公司前辈设计的代码逻辑