编写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
主要使用:页面按需加载插件、图片。
- 返回的展示页面,正常使用插件及引入图片
- 父组件中,需要将(接口返回的) 子组件中引入的插件等同等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>