vue组件之远程组件

vue组件之远程组件

手里有一个多团队共同研发的项目,A团队需要提供组件给B团队,因为组件更新频率太快,且两个团队又各自有自己的发版频率,最终导致两个团队都不堪其扰。于是我想到了远程组件,从线上实时加载组件,这样就不需要A团队的组件每次更新都去麻烦B团队,毕竟就算有CI/CD每一次打包都挺麻烦的。

异步组件-defineAsyncComponent

在看官方文档的时候,在异步组件基本用法里出现以下内容:

从服务器获取组件......

那就先试试

  1. 将组件放置到服务器,获取到可访问的URL地址。

先简单写一个自增组件:

html 复制代码
// 远程组件
<template>
  <div class="child-component">
    <span @click="increase">自增+1</span> <span>{{count}}</span>
  </div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)

const increase = () => {
  count.value ++
}
</script>

这里我使用http-server配置服务器,步骤比较简单:

安装npm包

cmd 复制代码
npm i -g http-server

找到远程组件的文件夹,执行命令。

cmd 复制代码
http-server -p 8080 ./ --cors

这里我通过-p设置了端口号8080,设置服务器根目录为当前文件夹./,最后通过-cors设置允许跨域,不然会无法访问。

最终得到远程组件的地址:http://localhost:8080/remoteComponent.vue

  1. 通过defineAsyncComponent方法,获取到组件代码,编译成组件进行使用。

代码如下:

html 复制代码
// 主应用
<template>
  <h1>Hello World</h1>
  <remoteComponent />
</template>

<script setup>
import { defineAsyncComponent } from 'vue'

const remoteComponent = defineAsyncComponent(() => {
  return new Promise((resolve) => {
    resolve(import ('http://localhost:8080/remoteComponent.vue'))
  })
})
</script>

太简单了,结果:

bash 复制代码
Failed to load module script: Expected a JavaScript-or-Wasm module script but the server responded with a MIME type of "application/octet-stream". Strict MIME type checking is enforced for module scripts per HTML spec.

Uncaught (in promise) TypeError: Failed to fetch dynamically imported module: http://localhost:8080/remoteComponent.vue

以上两个错误表示,无法加载非JavaScripthtml文件格式以外的文件,即不能加载vue文件,加载异步模块失败。

既然如此,改一改代码

html 复制代码
// 主应用
<template>
  <h1>Hello World</h1>
  <remoteComponent />
</template>

<script setup>
import { defineAsyncComponent } from 'vue'

const remoteComponent = defineAsyncComponent(() => {
  return new Promise((resolve)  => {
    fetch('http://localhost:8080/remoteComponent.vue')
    .then(res => {
      resolve(res.text())
    })
  })
})
</script>

又报错

bash 复制代码
Unhandled error during execution of async component loader 

异步组件加载过程中出现错误。但是远程组件的代码已经加载出来了,恰好也是这里出了问题。因为加载的仅仅是代码,或者说是一串字符串而已,而并不是一个组件。

思路打开,解决方案有俩个,一个是先打包再加载,另一个是先加载再打包成组件。

先打包再加载

将远程组件打包成js模块(umd)

js 复制代码
// 远程组件
export default {
  template: `
    <div class="child-component">
      <span @click="increase">自增+1</span>
      <span>{{ count }}</span>
    </div>
  `,
  data() {
    return {
      count: 0
    };
  },
  methods: {
    increase() {
      this.count++;
    }
  }
};
html 复制代码
// 主应用
<template>
  <h1>Hello World</h1>
  <component v-if="remoteComponent" :is="remoteComponent" />
</template>

<script setup>
import { defineAsyncComponent, onMounted, ref } from 'vue'

const remoteComponent = ref(null)

onMounted(async() => {
  const module  = await import('http://localhost:8080/remoteComponent.js')
  remoteComponent.value = defineAsyncComponent(() => Promise.resolve(module.default))
})
</script>

还是两个报错,解读一下:

  1. 组件被错误地设置为响应式,这里可以使用markRaw解除响应式
  2. 这是 Vue 运行时构建版本不支持模板编译 导致的错误,可以配置 Vite 别名使用带编译器的 Vue 构建版本。 这里只需要再对主应用进行两处微调:
html 复制代码
// 主应用
<template>
  <h1>Hello World</h1>
  <component v-if="remoteComponent" :is="remoteComponent" /> // 需要等待模块加载完成,所以使用v-if
</template>

<script setup>
import { defineAsyncComponent, onMounted, ref, markRaw } from 'vue'

const remoteComponent = ref(null)

onMounted(async() => {
  const module  = await import('http://localhost:8080/remoteComponent.js') // 异步加载组件地址
  const comp = defineAsyncComponent(() => Promise.resolve(module.default)) //  定义异步组件
  remoteComponent.value = markRaw(comp) // 解除响应式
})
</script>
js 复制代码
// 主应用 vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      vue: 'vue/dist/vue.esm-bundler.js' // Vue 运行时构建版本不支持模板编译,配置 Vite 别名使用带编译器的 Vue 构建版本
    }
  }
})

最终结果:

先加载再打包

首先看了一下vue.complie(),如果是作为主应用来说,使用起来太麻烦了。拿到源码之后,需要先对源码进行切割,分成templatescriptstyle,再用vue解析器分别解析和插入。另外script部分还需要判断是否为setup,再进行改写成普通script。最后才是组装成组件,过程麻烦且繁杂。

不过,有一个插件vue3-sfc-loader,可以将以上过程简单处理。

html 复制代码
<template>
  <h1>Hello World</h1>
  <remoteComponent />
</template>

<script setup>
import * as Vue from 'vue'
import { loadModule } from 'vue3-sfc-loader'
import { defineAsyncComponent } from 'vue'

// loadModule 配置项
const options = {
  moduleCache: { //  缓存模块
    vue: Vue
  },
  // 获取文件内容的方法,接收一个 URL,返回该 URL 的文本内容
  async getFile(url) {
    const response = await fetch(url)
    if (response.status === 200) {
      return response.text()
    } else {
      throw new Error(response.statusText)
    }
  },
  // 将组件中提取的样式插入到页面 head 中
  addStyle(textContent) {
    const style = Object.assign(document.createElement("style"), { textContent })
    const refs = document.head.getElementsByTagName("style")[0] || null
    document.head.insertBefore(style, refs)
  }
}

// 使用 defineAsyncComponent 创建一个异步组件
const remoteComponent = defineAsyncComponent(async() => {
  return new Promise((resolve) => {
     // 使用 vue3-sfc-loader 加载远程 .vue 文件
    loadModule('http://localhost:8080/remoteComponent.vue', options).then((res) => {
      resolve(res)
    })
  })
})
</script>

模块联邦-vite-plugin-federation

以上,不管是先加载还是先打包(umd),其实都只是简单场景下的单个文件组件。真实场景其实可能是工程级别的大型组件,甚至引入很多第三方组件。所以就不太适用。

恰好在之前的微前端技术选型时,就了解过模块联邦

模块联邦(Module Federation)是Webpack 5引入的一项革命性功能,允许不同JavaScript应用在运行时动态共享代码和依赖,是实现微前端架构的核心技术之一。

其基本架构为:

  • Host(主应用):作为主容器,动态加载远程模块的应用。‌‌‌‌
  • Remote(远程应用):提供可共享模块的独立应用,通过暴露接口供Host消费。‌‌‌‌
  • 双向主机(Bidirectional-hosts):兼具Host和Remote角色的应用,实现模块互调。‌‌

在vite中需要实现这个功能,需要借助vite-plugin-federation

先起一个正常的npm publish的组件,具体代码就不展示了。

再安装vite-plugin-federationvite-plugin-top-level-await

bash 复制代码
npm i @originjs/vite-plugin-federation -D
npm i vite-plugin-top-level-await -S
js 复制代码
// vite.config.js
// 正常组件引入
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path, {resolve } from 'path'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import vueJsx from '@vitejs/plugin-vue-jsx'
// 模块联邦引入
import federation from "@originjs/vite-plugin-federation" // 模块联邦
import topLevelAwait from 'vite-plugin-top-level-await' // 顶层支持引入await

export default defineConfig({
  plugins: [
    vue(),
    vueJsx(),
    AutoImport({
      imports: ['vue', 'vue-router'],
      resolvers: [ElementPlusResolver()]
    }),
    Components({ resolvers: [ElementPlusResolver()] }),
    // 模块联邦
    federation({
      name: 'remote_app', // 模块名称
      filename: 'remote-app.js', // 打包后主组件名
      exposes: {
        './RemoteApp': './src/components/RemoteApp.vue', // **导出包名**:导出包主入口
      },
      shared: ['vue', 'element-plus', 'axios', 'vue-router', 'dayjs', 'echarts'] // 共享第三方库
    }),
    topLevelAwait({
      promiseExportName: "__tla",
      promiseImportName: i => `__tla_${i}`
    })
  ],
  resolve: {
    alias: [
      {
        find: '@',
        replacement: resolve(__dirname, 'src')
      }
    ]
  },
  // 为了提高远程请求组件请求速度,可以在打包配置里做一些处理
  build: {
    assetsInlineLimit: 40960, // 小于 40KB 的静态资源将被内联为 Base64,减少请求次数
    minify: true, // 开启 JS/CSS 压缩,减小体积
    cssCodeSplit: false, // 关闭 CSS 分割,CSS 内联到 HTML,减少请求次数
    sourcemap: false, // 不生成源码映射文件,减少包体积
    rollupOptions: {
      output: {
        minifyInternalExports: false // 保留内部模块导出变量名,便于调试(后期可关闭)
      }
    }
  }
})

正常build之后产生的文件如下:

直接将整个dist文件丢到服务器,并配置好端口允许跨域让主应用可以访问remote-app.js。这里做演示,直接还是http-server启动,得到http://localhost:8080/assets/remote-app.js

然后开始对主应用进行改造。

首先需要对远程应用的share配置中的共享包进行安装,如'vue', 'element-plus', 'axios', 'vue-router', 'dayjs', 'echarts'。其次,安装@originjs/vite-plugin-federation。这里就不展开了。

vite.config.js的配置:

js 复制代码
// vite.config.js
// 正常引入
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
// 模块联邦引入
import federation from "@originjs/vite-plugin-federation"

export default defineConfig(({mode}) =>{
  const env = loadEnv(mode, process.cwd(), '')
  return {
    plugins: [
    vue(),
    federation({
      name: 'host-app', // 主应用名称
      filename: 'remote-app.js',
      remotes: {
        remote_app: { // **模块名称**
          // 请求远程组件主入口,这里使用了环境变量,主要是为了开发和生成模式的切换方便
          external: `Promise.resolve('${env.VITE_API_BASE_URL}/assets/remote-app.js')`,
          // 使用异步加载方式,防止请求时间过长,这也是为什么远程引用需要用vite-plugin-top-level-await的原因
          externalType: "promise"
        }
      },
      // 远程组件使用的第三方包,需要原原本本在这里再复制一份,不需要其他处理。这里只是简单配置,也可以查看官方文档进行深入优化。
      shared: ['vue', 'element-plus', 'axios', 'vue-router', 'dayjs', 'echarts']
    })
  ],}
})

页面使用时,只需要简单引入即可:

html 复制代码
<template>
  <h1>Hello World</h1>
  <RemoteApp></RemoteApp>
</template>

<script setup>
// 正常通过import引入,注意
// RemoteApp是对应远程组件里配置的federation=》exposes里的导出包名
// remote_app是主应用里federation=》remotes配置的模块名称
import RemoteApp from 'remote_app/RemoteApp'
</script>

一般到这里就差不多了,不过需要注意的还有一些地方。如静态资源,比如第三方引入一张比较大的图片(小的图片通过vite.config.js里的build.assetsInlineLimit:40960配置将小于40kb的处理成了base64)会因为域的问题,导致图片无法加载。

这里其实有几种处理方式,随便说几种实用的。

  1. 图片少的,直接在代码里改。
html 复制代码
<template>
  <img :src="imgUrl" />
</template>

<script setup>
const imgUrl = new URL('@/assets/images/test.png', import.meta.url).href
</script>
// or
<script setup>
import { computed } from 'vue'

const baseUrl = import.meta.env.VITE_API_BASE_URL
const imgUrl = computed(() => `${baseUrl}assets/images/test.png`)
</script>
  1. 图片多的,上base。
js 复制代码
// vite.config.js
export default defineConfig({
  base: 'https://yourdomain.com/'
})
相关推荐
你的人类朋友11 分钟前
🤔Token 存储方案有哪些
前端·javascript·后端
烛阴12 分钟前
从零开始:使用Node.js和Cheerio进行轻量级网页数据提取
前端·javascript·后端
liuyang___28 分钟前
日期的数据格式转换
前端·后端·学习·node.js·node
西哥写代码30 分钟前
基于cornerstone3D的dicom影像浏览器 第三十一章 从PACS服务加载图像
javascript·pacs·dicom
贩卖纯净水.1 小时前
webpack其余配置
前端·webpack·node.js
码上奶茶2 小时前
HTML 列表、表格、表单
前端·html·表格·标签·列表·文本·表单
抹茶san2 小时前
和 Trae 一起开发可视化拖拽编辑项目(1) :迈出第一步
前端·trae
风吹头皮凉2 小时前
vue实现气泡词云图
前端·javascript·vue.js
南玖i2 小时前
vue3 + ant 实现 tree默认展开,筛选对应数据打开,简单~直接cv
开发语言·前端·javascript