【编写Node接口;接口动态获取VUE文件并异步加载, 并渲染impoort插件使用】

编写Node接口;接口动态获取VUE文件并异步加载, 并渲染impoort插件使用;

本文使用vue3-sfc-loader在运行时从Node.js服务器动态加载Vue3和Vue2的单文件组件,无需依赖Node.js构建过程,同时展示了在Express应用中设置接口以及在Vue项目中使用loader的详细步骤。
vue3-sfc-loader ( https://github.com/FranckFreiburger/vue3-sfc-loader),它是Vue3/Vue2 单文件组件加载器。

在运行时从 html/js 动态加载 .vue 文件。无需 Node.js 环境,无需 (webpack) 构建步骤。

vue3-sfc-loader

*以下来源博主:https://blog.csdn.net/SongZhengxing_/article/details/137627777
本文添加补充页面按需加载插件、图片

主要特征:
  • 支持 Vue 3 和 Vue 2(参见dist/)
  • 仅需要 Vue 仅运行时构建
  • 提供esm和umd捆绑包(示例)
  • 嵌入式ES6模块支持(含import())
  • TypeScript 支持、JSX 支持
  • 自定义 CSS、HTML 和脚本语言支持,请参阅pug和stylus示例
  • SFC 自定义块支持
  • 通过日志回调正确报告模板、样式或脚本错误
  • 专注于组件编译。网络、样式注入和缓存由您决定(参见下面的示例)
  • 轻松构建您自己的版本并自定义您需要支持的浏览器

编写Node接口:

编写Node接口提供服务,用于返回vue文件

项目初始化和安装

bash 复制代码
mkdir nodeServe
cd nodeServe
npm iniy -y
npm install express cors

项目完整结构

bash 复制代码
nodeServer
├── index.js
├── loaderVue2.vue
├── loaderVue3.vue
├── package-lock.json
└── package.json

添加 index.js

javascript 复制代码
// express 基于Node.js平台,快速、开放、极简的 Web 开发框架 https://www.expressjs.com.cn/
const express = require("express")
const app = express()
const cors = require("cors")
const fs = require('fs');

// 配置cors中间件,允许跨域
app.use(cors())

app.get("/getVue2Str", (req, res) => {
    // 服务端读取文件,并变成字符串。传递给前端
    const data = fs.readFileSync('./loaderVue2.vue', 'utf8');

    res.send({
        code:200,
        fileStr:data,
        fileName:"loaderVue2.vue"
    });
})

app.get("/getVue3Str", (req, res) => {
    // 服务端读取文件,并变成字符串。传递给前端
    const data = fs.readFileSync('./loaderVue3.vue', 'utf8');

    res.send({
        code:200,
        fileStr:data,
        fileName:"loaderVue2.vue"
    });
})

app.listen(3000, () => {
    console.log("服务启动成功:http://localhost:3000")
})

这里用到的两个vue文件代码如下:
loaderVue2.vue

javascript 复制代码
<template>
  <div>
    <h1>我是远程加载的组件</h1>
    <input :value="value" @input="changeName" />
    <button @click="patchParentEvent">触发父组件方法</button>
  </div>
</template>
<script>
export default {
  props: ["value"],
  methods: {
    changeName(e) {
      this.$emit("input", e.target.value);
    },
    patchParentEvent() {
      this.$emit("parentEvent");
    },
  },
};
</script>

<style scoped>
h1 {
  color: red;
}
</style>

loaderVue3.vue

javascript 复制代码
<template>
  <div>
    <h1>测试远程页</h1>
    <div>引入图片:<img :src="resourceVersion" alt="" /></div>
    <input v-model="input" placeholder="placeholder" @input="changeValue" />
    <button @click="emitParentFun">点击emit</button>
  </div>
</template>

<script setup>
import { defineProps, defineEmits, ref, onMounted } from "vue";
import resourceVersion from "@/assets/images/resource_version.png";
import { ElMessage } from "element-plus";

const props = defineProps(["modelValue"]);
// 更新model绑定的值固定写法: update:modelValue
const emit = defineEmits(["update:modelValue", "childClick"]);

let input = ref("");

onMounted(() => {
  input.value = props.modelValue;
  // window环境指向的是接收方的window环境
  // console.log(window.testName);
});

const changeValue = (e) => {
  // 修改父组件的值
  emit("update:modelValue", e.target.value);
};

const emitParentFun = () => {
  ElMessage({
    message: "引入组件测试",
    type: "success",
  });
  // console.log("调用父组件的方法3", input.value);
  emit("childClick", input.value);
};
</script>

<style scope>
.text-red {
  color: red;
}
</style>

运行:

bash 复制代码
node index.js

接口返回的格式如下:

http://localhost:3000/getVue2Str

bash 复制代码
{
    "code": 200,
    "fileStr": "<template>\r\n  <div>\r\n    <h1>我是远程加载的组件</h1>\r\n    <input :value=\"value\" @input=\"changeName\" />\r\n    <button @click=\"patchParentEvent\">触发父组件方法</button>\r\n  </div>\r\n</template>\r\n<script>\r\nexport default {\r\n  props: [\"value\"],\r\n  methods: {\r\n    changeName(e) {\r\n      this.$emit(\"input\", e.target.value);\r\n    },\r\n    patchParentEvent() {\r\n      this.$emit(\"parentEvent\");\r\n    },\r\n  },\r\n};\r\n</script>\r\n\r\n<style scoped>\r\nh1 {\r\n  color: red;\r\n}\r\n</style>\r\n",
    "fileName": "loaderVue2.vue"
}

Vue2项目使用:

安装 vue3-sfc-loader

bash 复制代码
npm install vue3-sfc-loader

使用

注意:

vue2要从dist/vue2-sfc-loader这个目录下引入loadModule使用

vue2要从dist/vue3-sfc-loader这个目录下引入loadModule使用

javascript 复制代码
<template>
  <div>
    <component :is="remote" v-bind="$attrs" v-if="remote" v-model="name" @parentEvent="parentEvent"></component>
  </div>
</template>

<script>
import * as Vue from "vue"
import {loadModule} from "vue3-sfc-loader/dist/vue2-sfc-loader"

export default {
  name: 'App',
  data() {
    return {
      name: "李四",
      remote: null,
      url: "http://localhost:3000/getVue2Str",
    }
  },
  mounted() {
    this.load(this.url)
  },
  watch: {
    name(newName) {
      console.log(newName, "监听到变化")
    }
  },
  methods: {
    // 加载
    async load(url) {
      let res = await fetch(url).then(res => res.json());

      const options = {
        moduleCache: {
          vue: Vue
        },
        async getFile() {
          return res.fileStr
        },
        addStyle(textContent) {
          const style = Object.assign(document.createElement('style'), {textContent})
          const ref = document.head.getElementsByTagName('style')[0] || null
          document.head.insertBefore(style, ref)
        },
      };

      // 加载远程组件
      this.remote = await loadModule(res.fileName || "loader.vue", options)
    },
    // 子组件调用
    parentEvent() {
      console.log("父组件事件触发")
    }
  }
}
</script>

效果显示:

Vue3项目使用:(页面按需加载插件、图片等)

安装:

javascript 复制代码
npm install vue3-sfc-loader

主要使用:页面按需加载插件、图片。

  1. 返回的展示页面,正常使用插件及引入图片
  2. 父组件中,需要将(接口返回的) 子组件中引入的插件等同等import
javascript 复制代码
<template>
	<div>
		<component
	        v-if="componentName"
	        :is="componentName"
	        ref="stepComponent"
	        :listData="testList"
	        :handleNodeClick="handleNodeClick"
	        @childClick="childClick"
	      />
	</div>
</template>

<script setup>
import { ref, onMounted, computed, defineAsyncComponent, defineComponent, h, shallowRef } from "vue";
import { loadModule } from "vue3-sfc-loader/dist/vue3-sfc-loader";
import * as Vue from "vue";
import * as ElementPlus from "element-plus";
import testPic from "@/assets/images/resource_version.png";

//加载页面组件
const resResult = ref();
const componentName = computed(() => {
	const options = {
		  // 模块缓存
	      moduleCache: {
	        vue: Vue,
	        "element-plus": ElementPlus, // 组件按需引入
	        "@/assets/images/resource_version.png": testPic, // 图片引入
	      },
	      async getFile() {
	        return resResult.value.fileStr;
	      },
	      addStyle(textContent) {
	        const style = Object.assign(document.createElement("style"), {
	          textContent,
	        });
	        const ref = document.head.getElementsByTagName("style")[0] || null;
	        document.head.insertBefore(style, ref);
	      },
	};
	return defineAsyncComponent(() =>
	      loadModule(resResult.value?.fileName || "loader.vue", options)
	);
})

// 接口请求------------获取的后端页面数据
onMounted(async () => {
  let url = "http://localhost:3000/getVue3Str";
  resResult.value = await fetch(url ).then(
    (res) => res.json()
  );
  console.log("resResult------", resResult.value); 
});
</script>

<style scoped>
</style>

扩展------页面下一步异步加载动态组件:详细代码及组件引用扩展

javascript 复制代码
<template>
  <div class="step-content" style="height: 100%; min-height: 380px">
    <div class="step-left">
      <el-steps direction="vertical" :active="active">
        <el-step title="创建工程" />
        <el-step title="基础配置" />
        <el-step title="模型选取" />
        <el-step title="模型配置" />
        <el-step title="生成" />
        <el-step title="下载" />
      </el-steps>
    </div>
    <div class="step-right">
      <div class="step-bd">
        <component
          v-if="componentName"
          :is="componentName"
          ref="stepComponent"
          :listData="true"
          :handleNodeClick="false"
          @childClick="childClick"
        />

        <el-empty v-else description="空" />
      </div>
      <div class="next-btn-bd">
        <el-button style="margin-top: 12px" @click="cancelClick"
          >取消</el-button
        >
        <el-button style="margin-top: 12px" @click="previousClick"
          >上一步</el-button
        >
        <el-button style="margin-top: 12px" @click="nextClick"
          >下一步</el-button
        >
      </div>
    </div>
  </div>
</template>

<script setup>
import {
  ref,
  onMounted,
  computed,
  defineAsyncComponent,
  defineComponent,
  h,
  shallowRef,
} from "vue";
import { resetRelation, resetMeta, backendUpdate } from "@/api/system";
import { ElMessage } from "element-plus";
import step1Component from "./basicComponent.vue";
import { useAuthStore } from "@/store/authStore";
import { compileScript, parse } from "@vue/compiler-sfc";
import { loadModule } from "vue3-sfc-loader/dist/vue3-sfc-loader";
import * as Vue from "vue";
import * as ElementPlus from "element-plus";
import testPic from "@/assets/images/resource_version.png";
const authStore = useAuthStore();
const token = authStore.getToken();

const stepComponent = ref(null);
// 下一步
const active = ref(0);
// const asyncComponent = ref(null);
const AsyncComponent = shallowRef(null);

const previousClick = () => {
  if (active.value-- < 1) active.value = 0;
};
const nextClick = async () => {
  // import("./basicComponent2.vue")
  //   .then((module) => {
  //     asyncComponent.value = defineAsyncComponent(() =>
  //       Promise.resolve(module.default)
  //     );
  //     // 或者更简单地,直接使用加载的模块(如果不需要额外的异步处理逻辑)
  //     // asyncComponent.value = module.default;
  //   })
  //   .catch((err) => {
  //     console.error("Failed to load component:", err);
  //   });
  // return;
  // if (stepComponent.value) {
  //   if (active.value === 1) {
  //     // await stepComponent.value?.childNextMethod();
  //   } else if (active.value === 2) {
  //     // await stepComponent.value?.childNextMethod();
  //   } else if (active.value === 3) {
  //     // await stepComponent.value?.childNextMethod();
  //   } else if (active.value === 4) {
  //     // await stepComponent.value?.childNextMethod();
  //   } else if (active.value === 5) {
  //     // await stepComponent.value?.childNextMethod();
  //   } else if (active.value === 6) {
  //     // await stepComponent.value?.childNextMethod();
  //   }
  // }
  if (active.value++ > 5) active.value = 0;
};
const cancelClick = () => {
  active.value = 0;
};

const resResult = ref();
const componentName = computed(() => {
  if (active.value === 1) {
    const options = {
      moduleCache: {
        vue: Vue,
        "element-plus": ElementPlus,
        "@/assets/images/resource_version.png": testPic,
      },
      async getFile() {
        return resResult.value.fileStr;
      },
      addStyle(textContent) {
        const style = Object.assign(document.createElement("style"), {
          textContent,
        });
        const ref = document.head.getElementsByTagName("style")[0] || null;
        document.head.insertBefore(style, ref);
      },
    };
    // defineAsyncComponent(() =>
    //   loadModule(res.fileName || "loader.vue", options)
    // );
    // const loadDynamicComponent = () =>
    //   loadModule(resResult.value..fileName || "loader.vue", options);
    // 加载远程组件
    return defineAsyncComponent(() =>
      loadModule(resResult.value?.fileName || "loader.vue", options)
    );
  } else if (active.value === 4) {
    // return defineAsyncComponent(() => import("./basicComponent2.vue"));
    return defineAsyncComponent({
      loader: () => import("./basicComponent2.vue"),
      loadingComponent: {
        template: "<div>Loading...</div>",
      },
      errorComponent: {
        template: "<div>Error loading component</div>",
      },
      delay: 200, // 显示加载组件的延迟时间(毫秒)
      timeout: 3000, // 超时时间(毫秒)
    });
  } else if (active.value === 3) {
    // 模拟异步加载组件
    const loadDynamicComponent = () => {
      return new Promise((resolve) => {
        setTimeout(() => {
          // 动态创建组件
          const AsyncDynamicComponent = defineComponent({
            name: "AsyncDynamicComponent",
            setup() {
              // 使用 ref 定义响应式数据
              const title = ref("Dynamic Title");
              const description = ref(
                "This is a dynamically loaded component."
              );

              // 返回渲染函数
              return () =>
                h(
                  "div", // 根元素
                  {}, // 属性
                  [
                    h("h1", {}, title.value), // 动态标题
                    h("p", {}, description.value), // 动态描述
                  ]
                );
            },
          });

          // 解析 Promise,返回动态组件
          resolve(AsyncDynamicComponent);
        }, 0);
      });
    };
    // 使用 defineAsyncComponent 加载这个动态组件
    const AsyncLoadedComponent = defineAsyncComponent(loadDynamicComponent);
    return defineAsyncComponent(loadDynamicComponent);
  } else if (active.value === 2) {
    const loadDynamicComponent4 = () => {
      return new Promise((resolve) => {
        setTimeout(() => {
          // 动态模板字符串
          const templateString = `
            <div>
              <h1>{{ title }}</h1>
              <p>{{ description }}</p>
            </div>
          `;
          // 动态创建组件
          const AsyncDynamicComponent4 = defineComponent({
            name: "AsyncDynamicComponent4",
            setup() {
              const count = ref(0);
              const increment = () => {
                count.value++;
              };
              // 返回渲染函数
              return () =>
                h(
                  "div", // 根元素
                  {}, // 属性
                  [
                    h("p", {}, `Count: ${count.value}`),
                    h("button", { onClick: increment }, "Increment"),
                  ]
                );
            },
          });

          // 解析 Promise,返回动态组件
          resolve(AsyncDynamicComponent4);
        }, 0);
      });
    };
    // 使用 defineAsyncComponent 加载这个动态组件
    const AsyncLoadedComponent = defineAsyncComponent(loadDynamicComponent4);
    return defineAsyncComponent(loadDynamicComponent4);
  } else if (active.value === 5) {
    // -------------
    const dynamicLoader = {
      loadedComponents: new Map(),

      async loadComponent({ name, js, css }) {
        if (this.loadedComponents.has(name)) {
          return this.loadedComponents.get(name);
        }

        // 加载CSS
        if (css) {
          const link = document.createElement("link");
          link.rel = "stylesheet";
          link.href = css; //css的URL路径
          document.head.appendChild(link);
        }

        // 加载JS
        // const script = await fetch(js).then((r) => r.text());
        const script = `import { openBlock as r, createElementBlock as s } from "vue";
const _ = (c, t) => {
  const e = c.__vccOpts || c;
  for (const [o, n] of t)
    e[o] = n;
  return e;
}, a = {}, f = { class: "cs1" };
function l(c, t) {
  return r(), s("div", f, "测试");
}
const i = /* @__PURE__ */ _(a, [["render", l]]);
export {
  i as default
};
`;

        // 创建沙盒环境
        // 如果JS模块依赖外部库或Vue本身,可能会因为沙盒环境无法访问外部变量而报错。
        // 例如,如果JS代码里有import Vue from 'vue',在沙盒中执行时会找不到Vue,导致错误。
        // 用户可能需要确保JS代码是自包含的,或者正确传递依赖。
        const component = new Function(`
          "use strict";
          const exports = {};
          const module = { exports };
          ${script}
          return module.exports.__esModule ? module.exports.default : module.exports;
        `)();

        // 缓存组件
        this.loadedComponents.set(name, component);

        return component;
      },
    };

    const jsUrl = `const _sfc_main = {
  __name: 'MyComponent',
  setup(__props) {
    const count = ref(0);
    return (_ctx, _cache) => {
      return _createVNode("button", {
        onClick: () => count.value++
      }, "Clicked: " + count.value)
    }
  }
};
export default _sfc_main;`;

    const cssUrl = ``;

    // Vue 3 示例====
    const AsyncComponent = defineAsyncComponent({
      loader: () =>
        dynamicLoader.loadComponent({
          name: `componentName`,
          js: "", // jsUrl
          css: "", // cssUrl
        }),
      loadingComponent: {
        template: "<div>Loading...</div>",
      },
      errorComponent: {
        template: "<div>Error loading component</div>",
      },
      timeout: 3000,
    });

    return AsyncComponent;

    // -------------
  } else if (active.value === 6) {
    async function loadVueComponent(url = "") {
      // 1. 请求获取 .vue 文件内容
      //     fetch('/api/get-vue-file-content')
      // .then(response => response.text())
      // .then(vueFileContent => {
      //   console.log(vueFileContent);
      //   // 在这里你可以将 vueFileContent 显示在页面上
      // })
      // .catch(error => console.error('Error fetching Vue file content:', error));
      // const response = await fetch(url);
      // const vueContent = await response.text();
      const vueContent = ``;
      const vueFileContent = `<template>  <div>    <h1>{{ title }}</h1>  </div></template>
<script1>
export default {
  data() {
    return {
      title: 'Hello Vue!'
    };
  }
};
</script1>
<style scoped>
h1 {
  color: blue;
}
</style>
`;

      // 2. 使用 compiler-sfc 解析内容
      const { descriptor } = parse(vueContent);

      // 3. 提取各部分内容
      const template = descriptor.template?.content;
      const scriptContent = descriptor.script?.content || "export default {}";
      const styles = descriptor.styles.map((style) => style.content).join("\n");

      // 4. 动态编译 script 部分
      const { content: scriptCode } = compileScript(descriptor, {
        id: `dynamic-component-${Date.now()}`,
      });

      // 5. 将 script 转换为组件选项
      const scriptModule = { exports: {} };
      const script = new Function("exports", "module", scriptCode);
      script(scriptModule.exports, scriptModule);
      const componentOptions = scriptModule.exports.default || {};

      // 6. 处理模板
      if (template) {
        componentOptions.template = template;
      }

      // 7. 处理样式(可选:动态添加样式到页面)
      if (styles) {
        const styleEl = document.createElement("style");
        styleEl.textContent = styles;
        document.head.appendChild(styleEl);
      }

      // 8. 返回 Vue 组件
      return defineComponent(componentOptions);
    }
    loadVueComponent();
  }
});

// ============
// const AsyncComponent = shallowRef(null);
// const props = ref({});

// // 动态加载组件
// async function loadComponent() {
//   try {
//     const config = await fetchComponentConfig();
//     AsyncComponent.value = await dynamicLoader.loadComponent(config);
//   } catch (err) {
//     handleError(err);
//   }
// }
// ==============================

// uid
// const generateUID = () => {
//   const time = Date.now().toString(36);
//   const random = Math.random().toString(36).substr(2, 5);
//   return `${time}_${random}`;
// };

const childClick = (newVal) => {
  console.log("获取子组事件和值", newVal);
};

onMounted(async () => {
  resResult.value = await fetch("http://localhost:3000/getVue3Str").then(
    (res) => res.json()
  );
  console.log("resResult------", resResult.value);
});
</script>

<style scoped>
.step-content {
  display: flex;
  .step-left {
    min-width: 200px;
  }
  .step-right {
    flex: 1;
    display: flex;
    flex-direction: column;
    .step-bd {
      border: 3px solid var(--resource-relation-border);
      padding: 10px;
      flex: 1;
    }
    .next-btn-bd {
      text-align: right;
      /* height: 30px; */
    }
  }
}
</style>

完整源码:

https://gitee.com/szxio/load-remote-vue-components

相关推荐
不简说1 小时前
sv-print可视化打印组件不完全指南⑤
前端·javascript·前端框架
OpenTiny社区2 小时前
TinyPro 1.2.0 正式发布:增加综合搜索,解决数据筛选难题,后端单测覆盖率再提升!
前端·vue.js·github
冬枳2 小时前
AntD X Vue流式对话实战:实现Markdown渲染聊天机器人
前端·前端框架
DUOKE七七2 小时前
一文读懂!线上线下陪玩系统小程序源码的神奇力量
vue.js·后端
無名路人2 小时前
写了一个书签管理扩展,不要服务器,也不需要webdav,数据还能跨端管理。
前端·vue.js·开源
勘察加熊人2 小时前
vue实现二维码生成器和解码器
前端·javascript·vue.js
涔溪2 小时前
VUE的node包缓存很严重,问题及解决办法
前端·vue.js·缓存
拖孩4 小时前
【Nova UI】七、SASS 全局变量体系:组件库样式开发的坚固基石
前端·javascript·vue.js
胚芽鞘6814 小时前
添加登录和注册功能
javascript·vue.js·elementui
Dignity_呱4 小时前
大厂在用的css+js实现不等高瀑布流布局
前端·vue.js·面试