远程加载vue组件文件并使用

941 阅读3分钟

远程加载vue组件是一个好玩的东西,在我们公司中有一些jsp项目和java web项目,如果开发一个功能那么我们前端只能将所有的功能都写在一个文件里面,如果想用组件化,那么不好意思:我!不!会!

如果能够加载一个远程的vue文件并当做vue组件的话就很好的处理了上面的问题

核心技术是vue3-sfc-loader

vue3

初始化

首先创建vue3项目npm init vue@latest,选择合适的依赖项并安装依赖

image-20240809152802060

然后启动项目并删除无用的文件

根据需要远程的vue文件安装依赖,因为需要将远程的vue文件当做组件,那么必须满足组件的依赖才行。一般vue3项目使用的组件库是element plus,这里安装element plus并全局使用

安装方式略过,请看官网

创建远程vue文件

既然我们需要加载远程的vue文件,那么我们需要先由远程的vue文件才行

直接拷贝一个项目中组件放到某个目录下面,安装http-server,然后在终端中打开目录,输入http-server --cors,如果没有安装http-server则需要先进行全局安装

npm install http-server -g
<script setup lang="ts">
  import { ref } from "vue";
  defineProps({
    modelValue: {
      type: [String, Number],
      default: ""
    },
    // label
    label: {
      type: String,
      default: "label"
    },
    // 是否显示冒号
    isColon: {
      type: Boolean,
      default: true
    },
    // label width
    labelWidth: {
      type: Number,
      default: 60
    },
    placeholder: {
      type: String,
      default: "请输入"
    },
    // 是否显示label
    isLabel: {
      type: Boolean,
      default: true
    }
  });
​
  let emit = defineEmits([
    "update:modelValue",
    "blur",
    "focus",
    "clear",
    "input"
  ]);
  // 绑定的数据
  let inpText = ref<any>(null);
​
  const changeData = val => {
    emit("update:modelValue", val);
    emit("input", val);
  };
  const blurFn = () => {
    emit("blur", inpText.value);
  };
  const focusFn = () => {
    emit("focus", inpText.value);
  };
  const clearFn = () => {
    emit("clear", inpText.value);
  };
​
  // 绑定的ref
  let inpRef = ref();
  // 组件获取失焦
  const blur = () => {
    inpRef.value.blur();
  };
  // 组件清除数据
  const clear = () => {
    inpRef.value.clear();
  };
  // 组件获取焦点
  const focus = () => {
    inpRef.value.focus();
  };
  // 选择组件的文本
  const select = () => {
    inpRef.value.select();
  };
  // 暴漏方法
  defineExpose({ blur, clear, focus, select });
</script>
​
<template>
  <div class="eInp">
    <div
      class="label"
      :style="{ width: labelWidth + 'px !important' }"
      v-if="isLabel"
    >
      {{ label }}{{ isColon ? ":" : "" }}
    </div>
    <el-input
      ref="inpRef"
      v-bind="$attrs"
      :model-value="modelValue"
      :placeholder
      @input="changeData"
      @blur="blurFn"
      @focus="focusFn"
      @clear="clearFn"
    />
  </div>
</template>
<style lang="scss" scoped>
  .eInp {
    display: flex;
    align-items: center;
    width: 100%;
    .inpRow {
      flex: 1;
    }
  }
</style>
​

获取组件并使用

安装依赖vue3-sfc-loader

npm i vue3-sfc-loader

引入loadModuleloadModule函数接收的第一个参数为远程组件的URL,第二个参数为options。在options中有个getFile方法,获取远程组件的code代码字符串就是在这里去实现的。

import { loadModule } from 'vue3-sfc-loader/dist/vue3-sfc-loader.esm.js'

因为Vue3不默认导出Vue了,所以这里直接导入所有

import * as Vue from 'vue'

接下来我们就需要获取远程的组件了,获取远程组件需要使用defineAsyncComponent函数

defineAsyncComponent:定义一个异步组件,它在运行时是懒加载的。参数可以是一个异步加载函数,或是对加载行为进行更具体定制的一个选项对象。

defineAsyncComponent方法的返回值是一个异步组件,我们可以像普通组件一样直接在template中使用。和普通组件的区别是,只有当渲染到异步组件时才会调用加载内部实际组件的函数。

defineAsyncComponent需要使用loadModule方法

  // 获取组件
  const eInp = defineAsyncComponent(() => {
​
    // 传递地址和参数
    return loadModule("http://172.16.4.219:8080/eInp.vue", eInpOps);
  });

eInpOps参数需要设置moduleCachegetFileaddStyle

  // 远程加载组件的选项
  let eInpOps = ref({
    // 固定的
    moduleCache: {
      vue: Vue
    },
    async getFile(url) {
      return await fetch(url).then(res => res.text());
    },
    // 添加样式
    addStyle(textContent) {
      // 组件中的样式
      const style = Object.assign(document.createElement("style"), {
        textContent
      });
      // 从文档的 <head> 部分获取第一个 <style> 元素
      const ref = document.head.getElementsByTagName("style")[0] || null;
      document.head.insertBefore(style, ref);
    }
  });

这样我们就将远程的vue文件变成了一个组件

使用和正差个组件一样

<eInp />

如果需要设置样式可以和之前一样设置也可以通过addStyle添加样式

通过addStyle添加样式实际上是设置到公共样式上面

至此vue3远程获取组件就完成了

完整代码

<script setup>
  import * as Vue from "vue";
  import { loadModule } from "vue3-sfc-loader/dist/vue3-sfc-loader.esm.js";
  import { ref, defineAsyncComponent } from "vue";
  let name = ref("张三");
  // 远程加载组件的选项
  let eInpOps = ref({
    // 固定的
    moduleCache: {
      vue: Vue
    },
    async getFile(url) {
      return await fetch(url).then(res => res.text());
    },
    // 添加样式
    addStyle(textContent) {
      // 组件中的样式
      const style = Object.assign(document.createElement("style"), {
        textContent
      });
      // 从文档的 <head> 部分获取第一个 <style> 元素
      const ref = document.head.getElementsByTagName("style")[0] || null;
      document.head.insertBefore(style, ref);
    }
  });
  // 获取组件
  const eInp = defineAsyncComponent(() => {
    // 传递地址和参数
    return loadModule("http://172.16.4.219:8080/eInp.vue", eInpOps.value);
  });
</script>

<template>
  <eInp v-model="name" class="aaa" />
</template>
<style lang="scss" scoped>
  .aaa {
    color: red;
  }
</style>

参考文章 juejin.cn/post/727782…

mp.weixin.qq.com/s/mkOnq6pu0…

vue2

初始化

创建vue2项目create vue2demo

image-20240809163053965

同样也是清除无用的代码,根据远程vue文件安装依赖。一般vue3项目使用的组件库是element ui,这里安装element ui并全局使用

安装方式略过,请看官网

接下来就是创建爱你vue2的远程组件,和vue3一样,不在复述

获取组件并使用

安装并引入vue3-sfc-loader

npm i vue3-sfc-loader
import { loadModule } from 'vue3-sfc-loader/dist/vue2-sfc-loader.js'

引入vue

import Vue from 'vue/dist/vue.common.js'

创建获取组件的函数,将组件设置到data数据中

// 获取组件
      async getSyncCom() {
        let url = "http://172.16.4.219:8081/eInp.vue";
        const com = await loadModule(url, {
          moduleCache: {
            vue: Vue
          },
          // 获取文件
          async getFile(url) {
            const res = await fetch(url);
            if (!res.ok) {
              throw Object.assign(new Error(`${res.statusText}  ${url}`), {
                res
              });
            }
            return {
              getContentData: asBinary =>
                asBinary ? res.arrayBuffer() : res.text()
            };
          },
          // 添加样式
          addStyle(textContent) {
            const style = Object.assign(document.createElement("style"), {
              textContent
            });
            const ref = document.head.getElementsByTagName("style")[0] || null;
            document.head.insertBefore(style, ref);
          }
        });
        this.eInp = com;
      }

mounted中调用获取组件的函数

使用的时候需要使用动态组件的方式进行使用

<component :is="eInp" />

完整代码

<template>
  <div class="home">
    <component :is="eInp" v-model="name" />
  </div>
</template>

<script>
  import Vue from "vue/dist/vue.common.js";
  import { loadModule } from "vue3-sfc-loader/dist/vue2-sfc-loader.js";
  export default {
    name: "Home",
    data() {
      return {
        eInp: null,
        name: "张三"
      };
    },

    mounted() {
      this.getSyncCom();
    },
    methods: {
      // 获取组件
      async getSyncCom() {
        let url = "http://172.16.4.219:8081/eInp.vue";
        const com = await loadModule(url, {
          moduleCache: {
            vue: Vue
          },
          // 获取文件
          async getFile(url) {
            const res = await fetch(url);
            if (!res.ok) {
              throw Object.assign(new Error(`${res.statusText}  ${url}`), {
                res
              });
            }
            return {
              getContentData: asBinary =>
                asBinary ? res.arrayBuffer() : res.text()
            };
          },
          // 添加样式
          addStyle(textContent) {
            const style = Object.assign(document.createElement("style"), {
              textContent
            });
            const ref = document.head.getElementsByTagName("style")[0] || null;
            document.head.insertBefore(style, ref);
          }
        });
        this.eInp = com;
      }
    }
  };
</script>

组件代码

<template>
  <div>
    <div class="eInp">
      <div
        class="label"
        :style="{ width: labelWidth + 'px !important' }"
        v-if="isLabel"
      >
        {{ label }}{{ isColon ? ":" : "" }}
      </div>
      <el-input
        ref="inpRef"
        v-bind="$attrs"
        :value="modelValue"
        :placeholder="placeholder"
        @input="changeData"
        @blur="blurFn"
        @focus="focusFn"
        @clear="clearFn"
        @change="changeFn"
        class="inpRow"
      />
    </div>
  </div>
</template>

<script>
  export default {
    name: "eInp",
    model: {
      prop: "modelValue",
      event: "update",
    },
    props: {
      modelValue: {
        type: [String, Number],
        default: "",
      },
      // label
      label: {
        type: String,
        default: "label",
      },
      // 是否显示冒号
      isColon: {
        type: Boolean,
        default: true,
      },
      // label width
      labelWidth: {
        type: Number,
        default: 60,
      },
      placeholder: {
        type: String,
        default: "请输入",
      },
      // 是否显示label
      isLabel: {
        type: Boolean,
        default: true,
      },
    },
    data() {
      return {
        inpText: this.modelValue,
      };
    },
    methods: {
      changeData(val) {
        console.log("file: index.vue:60 ~ val:", val);
        this.inpText = val;
        this.$emit("update", val);
      },
      blurFn() {
        this.$emit("blur", this.inpText);
      },
      focusFn() {
        this.$emit("focus", this.inpText);
      },
      clearFn() {
        this.$emit("clear", this.inpText);
      },
      changeFn() {
        this.$emit("change", this.inpText);
      },
      // 组件获取失焦
      blur() {
        this.$refs.inpRef.blur();
      },
      // 组件清除数据
      clear() {
        this.$refs.inpRef.clear();
      },
      // 组件获取焦点
      focus() {
        this.$refs.inpRef.focus();
      },
      // 选择组件的文本
      select() {
        this.$refs.inpRef.select();
      },
    },
  };
</script>

<style lang="scss" scoped>
  .eInp {
    display: flex;
    align-items: center;
    width: 100%;
    .inpRow {
      flex: 1;
    }
  }
</style>

参考文章:article.juejin.cn/post/723695…

html单页面

初始化(vue2)

在html中使用需要先引入vue才可以,同样也需要引入组件库的cdn

<script src="https://cdn.jsdelivr.net/npm/vue@2.6.12"></script>
<!-- 引入样式 -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<!-- 引入组件库 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>

核心还是vue3-sfc-loader,同样引入对应的cdn,github.com/FranckFreib…

 <!-- vue3-sfc-loader -->
<script src="https://cdn.jsdelivr.net/npm/vue3-sfc-loader/dist/vue2-sfc-loader.js"></script>

引入之后会在window上挂载vue2-sfc-loader函数

image-20240809172201698

开始初始化vue

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>vue2</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12"></script>
    <!-- 引入样式 -->
    <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
    <!-- 引入组件库 -->
    <script src="https://unpkg.com/element-ui/lib/index.js"></script>
    <!-- vue3-sfc-loader -->
    <script src="https://cdn.jsdelivr.net/npm/vue3-sfc-loader/dist/vue2-sfc-loader.js"></script>
</head>

<body>
    <div id="app">
        <div>{{name}}</div>
    </div>
</body>
<script>
    new Vue({
        el: '#app',
        data: {
            name: "张三"
        },
    })
</script>

</html>

image-20240809171423894

页面能够正常显示

使用vue组件(vue2)

使用方法和vue2一样,不同点在于在获取组件的函数中需要先引入loadModule,其他代码逻辑一样

完整代码如下

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>vue2</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12"></script>
    <!-- 引入样式 -->
    <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
    <!-- 引入组件库 -->
    <script src="https://unpkg.com/element-ui/lib/index.js"></script>
    <!-- vue3-sfc-loader -->
    <script src="https://cdn.jsdelivr.net/npm/vue3-sfc-loader/dist/vue2-sfc-loader.js"></script>
</head>

<body>
    <div id="app">
        <div>{{name}}</div>
        <component :is="eInp" v-model="name" />
    </div>
</body>
<script>
    new Vue({
        el: '#app',
        data: {
            name: "张三",
            eInp: null
        },
        mounted() {
            this.getSyncCom();
        },
        methods: {
            // 获取组件
            async getSyncCom() {
                const { loadModule } = window['vue2-sfc-loader'];
                let url = "http://172.16.4.219:8081/eInp.vue";
                const com = await loadModule(url, {
                    moduleCache: {
                        vue: Vue
                    },
                    // 获取文件
                    async getFile(url) {
                        const res = await fetch(url);
                        if (!res.ok) {
                            throw Object.assign(new Error(`${res.statusText}  ${url}`), {
                                res
                            });
                        }
                        return {
                            getContentData: asBinary =>
                                asBinary ? res.arrayBuffer() : res.text()
                        };
                    },
                    // 添加样式
                    addStyle(textContent) {
                        const style = Object.assign(document.createElement("style"), {
                            textContent
                        });
                        const ref = document.head.getElementsByTagName("style")[0] || null;
                        document.head.insertBefore(style, ref);
                    }
                });
                this.eInp = com;
            }
        }
    })
</script>

</html>

初始化(vue3)

在html中使用需要先引入vue才可以,同样也需要引入组件库的cdn

<script src="https://cdn.jsdelivr.net/npm/vue@2.6.12"></script>
<!-- 引入样式 -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<!-- 引入组件库 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>

核心还是vue3-sfc-loader,同样引入对应的cdn,github.com/FranckFreib…

 <!-- vue3-sfc-loader -->
<script src="https://cdn.jsdelivr.net/npm/vue3-sfc-loader/dist/vue2-sfc-loader.js"></script>

引入之后会在window上挂载vue2-sfc-loader函数

image-20240812084352943

开始初始化vue

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://cdn.jsdelivr.net/npm/browser-scss@1.0.3/dist/browser-scss.min.js"></script>
    <!-- vue3 -->
    <script src="https://unpkg.com/vue@3.4.37/dist/vue.global.js"></script>
    <!-- element plus -->
    <script src="https://unpkg.com/element-plus"></script>
    <link rel="stylesheet" href="//cdn.jsdelivr.net/npm/element-plus/dist/index.css" />
    <!-- vue3-sfc-loader -->
    <script src="https://cdn.jsdelivr.net/npm/vue3-sfc-loader"></script>
</head>

<body>
    <div id="app">
        <div>{{name}}</div>
        <el-button>Default</el-button>
        <el-button type="primary">Primary</el-button>
        <el-button type="success">Success</el-button>
        <el-button type="info">Info</el-button>
        <el-button type="warning">Warning</el-button>
        <el-button type="danger">Danger</el-button>
    </div>
</body>
<script>

    const { ref, defineAsyncComponent, onMounted } = Vue;

    const { loadModule } = window['vue3-sfc-loader']
    const app = Vue.createApp({
        setup() {
            let name = ref("hello world")
            

            

            return {
                name,
            }
        }
    });
    app.mount("#app");
</script>

</html>

image-20240812084533463

发现vue生效了,但是element plus没有生效,是因为在新版中需要使用一下才可以,修改代码

//  app.mount("#app");
app.use(ElementPlus).mount("#app");

这样就生效了

使用组件

使用方法基本上和vue3版本一样,需要注意的是引入loadModule函数和html(vue2)版本一样

完整代码

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://cdn.jsdelivr.net/npm/browser-scss@1.0.3/dist/browser-scss.min.js"></script>
    <!-- vue3 -->
    <script src="https://unpkg.com/vue@3.4.37/dist/vue.global.js"></script>
    <!-- element plus -->
    <script src="https://unpkg.com/element-plus"></script>
    <link rel="stylesheet" href="//cdn.jsdelivr.net/npm/element-plus/dist/index.css" />
    <!-- vue3-sfc-loader -->
    <script src="https://cdn.jsdelivr.net/npm/vue3-sfc-loader"></script>
</head>

<body>
    <div id="app">
        <div>{{name}}</div>
        <component :is="eInp" v-model="name"></component>
        <el-button>Default</el-button>
        <el-button type="primary">Primary</el-button>
        <el-button type="success">Success</el-button>
        <el-button type="info">Info</el-button>
        <el-button type="warning">Warning</el-button>
        <el-button type="danger">Danger</el-button>
    </div>
</body>
<script>

    const { ref, defineAsyncComponent, onMounted } = Vue;

    const { loadModule } = window['vue3-sfc-loader']
    const app = Vue.createApp({
        setup() {
            let name = ref("hello world")
            // 远程加载组件的选项
            let eInpOps = {
                // 固定的
                moduleCache: {
                    vue: Vue
                },
                async getFile(url) {
                    return await fetch(url).then(res => {
                        return res.text()
                    });
                },
                // 添加样式
                addStyle(textContent) {
                    // 组件中的样式
                    const style = Object.assign(document.createElement("style"), {
                        textContent
                    });
                    // 从文档的 <head> 部分获取第一个 <style> 元素
                    const ref = document.head.getElementsByTagName("style")[0] || null;
                    document.head.insertBefore(style, ref);
                }
            };

            // 获取组件
            const eInp = defineAsyncComponent(() => {

                // 传递地址和参数
                return loadModule("http://172.16.4.219:8080/eInp.vue", eInpOps);
            });

            return {
                name,
                eInp
            }
        }
    });
    app.use(ElementPlus).mount("#app");
</script>

</html>

image-20240812085005992

总结

通过以上验证,我们可以创建远程的.vue文件在jspjava web中使用vue组件,通过这种方式能够用帮助我们快速开发需求,但是组件是远程获取的,会存在一定的安全隐患。