写点什么

给我 5 分钟,保证教会你在 vue3 中动态加载远程组件

作者:EquatorCoco
  • 2024-08-07
    福建
  • 本文字数:4779 字

    阅读完需:约 16 分钟

前言


在一些特殊的场景中(比如低代码、减少小程序包体积、类似于 APP 的热更新),我们需要从服务端动态加载.vue文件,然后将动态加载的远程 vue 组件渲染到我们的项目中。今天这篇文章我将带你学会,在 vue3 中如何去动态加载远程组件。


defineAsyncComponent异步组件


想必聪明的你第一时间就想到了defineAsyncComponent方法。我们先来看看官方对defineAsyncComponent方法的解释:


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


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


我们先来简单看看使用defineAsyncComponent方法的例子,代码如下:

import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => { return new Promise((resolve, reject) => { // ...从服务器获取组件 resolve(/* 获取到的组件 */) })})// ... 像使用其他一般组件一样使用 `AsyncComp`
复制代码


defineAsyncComponent方法接收一个返回 Promise 的回调函数,在 Promise 中我们可以从服务端获取 vue 组件的 code 代码字符串。然后使用resolve(/* 获取到的组件 */)将拿到的组件传给defineAsyncComponent方法内部处理,最后和普通组件一样在 template 中使用AsyncComp组件。


从服务端获取远程组件


有了defineAsyncComponent方法后事情从表面上看着就很简单了,我们只需要写个方法从服务端拿到 vue 文件的 code 代码字符串,然后在defineAsyncComponent方法中使用resolve拿到的 vue 组件。


第一步就是本地起一个服务器,使用服务器返回我们的 vue 组件。这里我使用的是http-server,安装也很简单:

npm install http-server -g
复制代码


使用上面的命令就可以全局安装一个 http 服务器了。


接着我在项目的 public 目录下新建一个名为remote-component.vue的文件,这个 vue 文件就是我们想从服务端加载的远程组件。remote-component.vue文件中的代码如下:

<template>  <p>我是远程组件</p>  <p>    当前远程组件count值为:<span class="count">{{ count }}</span>  </p>  <button @click="count++">点击增加远程组件count</button></template>
<script setup>import { ref } from "vue";const count = ref(0);</script>
<style>.count { color: red;}</style>
复制代码


从上面的代码可以看到远程 vue 组件和我们平时写的 vue 代码没什么区别,有templateref响应式变量、style样式。


接着就是在终端执行http-server ./public --cors命令启动一个本地服务器,服务器默认端口为8080。但是由于我们本地起的 vite 项目默认端口为5173,所以为了避免跨域这里需要加--cors./public的意思是指定当前目录的public文件夹。


启动了一个本地服务器后,我们就可以使用 http://localhost:8080/remote-component.vue链接从服务端访问远程组件啦,如下图:



从上图中可以看到在浏览器中访问这个链接时触发了下载远程 vue 组件的操作。


defineAsyncComponent加载远程组件


const RemoteChild = defineAsyncComponent(async () => {  return new Promise(async (resolve) => {    const res = await fetch("http://localhost:8080/remote-component.vue");    const code = await res.text();    console.log("code", code);    resolve(code);  });});
复制代码


接下来我们就是在defineAsyncComponent方法接收的 Promise 的回调函数中使用 fetch 从服务端拿到远程组件的 code 代码字符串应该就行啦,代码如下:


同时使用console.log("code", code)打个日志看一下从服务端过来的 vue 代码。


上面的代码看着已经完美实现动态加载远程组件了,结果不出意外在浏览器中运行时报错了。如下图:



在上图中可以看到从服务端拿到的远程组件的代码和我们的remote-component.vue的源代码是一样的,但是为什么会报错呢?


这里的报错信息显示加载异步组件报错,还记得我们前面说过的defineAsyncComponent方法是在回调中resolve(/* 获取到的组件 */)。而我们这里拿到的code是一个组件吗?


我们这里拿到的code只是组件的源代码,也就是常见的单文件组件 SFC。而defineAsyncComponent中需要的是由源代码编译后拿的的 vue 组件对象,我们将组件源代码丢给defineAsyncComponent当然会报错了。


看到这里有的小伙伴有疑问了,我们平时在父组件中 import 子组件不是也一样在 template 就直接使用了吗?


子组件local-child.vue代码:

<template>  <p>我是本地组件</p>  <p>    当前本地组件count值为:<span class="count">{{ count }}</span>  </p>  <button @click="count++">点击增加本地组件count</button></template>
<script setup>import { ref } from "vue";const count = ref(0);</script>
<style>.count { color: red;}</style>
复制代码


父组件代码:

<template>  <LocalChild /></template>
<script setup lang="ts">import LocalChild from "./local-child.vue";console.log("LocalChild", LocalChild);</script>
复制代码


上面的 import 导入子组件的代码写了这么多年你不觉得怪怪的吗?


按照常理来说要 import 导入子组件,那么在子组件里面肯定要写 export 才可以,但是在子组件local-child.vue中我们没有写任何关于 export 的代码。


答案是在父组件 import 导入子组件触发了vue-loader或者@vitejs/plugin-vue插件的钩子函数,在钩子函数中会将我们的源代码单文件组件 SFC 编译成一个普通的 js 文件,在 js 文件中export default导出编译后的 vue 组件对象。


这里使用console.log("LocalChild", LocalChild)来看看经过编译后的 vue 组件对象是什么样的,如下图:



从上图可以看到经过编译后的 vue 组件是一个对象,对象中有rendersetup等方法。

defineAsyncComponent 方法接收的组件就是这样的 vue 组件对象,但是我们前面却是将 vue 组件源码丢给他,当然会报错了。


最终解决方案vue3-sfc-loader


从服务端拿到远程 vue 组件源码后,我们需要一个工具将拿到的 vue 组件源码编译成 vue 组件对象。幸运的是优秀的 vue 不光暴露出一些常见的 API,而且还将一些底层 API 给暴露了出来。比如在@vue/compiler-sfc包中就暴露出来了compileTemplatecompileScriptcompileStyleAsync等方法。


如果你看过我写的 vue3编译原理揭秘 开源电子书,你应该对这几个方法觉得很熟悉。


  • compileTemplate方法:用于处理单文件组件 SFC 中的 template 模块。

  • compileScript方法:用于处理单文件组件 SFC 中的 script 模块。

  • compileStyleAsync方法:用于处理单文件组件 SFC 中的 style 模块。


vue3-sfc-loader包的核心代码就是调用@vue/compiler-sfc包的这些方法,将我们的 vue 组件源码编译为想要的 vue 组件对象。下面这个是改为使用vue3-sfc-loader包后的代码,如下:

import * as Vue from "vue";import { loadModule } from "vue3-sfc-loader";
const options = { moduleCache: { vue: Vue, }, async getFile(url) { const res = await fetch(url); const code = await res.text(); return code; }, addStyle(textContent) { const style = Object.assign(document.createElement("style"), { textContent, }); const ref = document.head.getElementsByTagName("style")[0] || null; document.head.insertBefore(style, ref); },};
const RemoteChild = defineAsyncComponent(async () => { const res = await loadModule( "http://localhost:8080/remote-component.vue", options ); console.log("res", res); return res;});
复制代码


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


我们在终端来看看经过loadModule函数处理后拿到的 vue 组件对象是什么样的,如下图:



从上图中可以看到经过loadModule函数的处理后就拿到来 vue 组件对象啦,并且这个组件对象上面也有熟悉的render函数和setup函数。其中render函数是由远程组件的 template 模块编译而来的,setup函数是由远程组件的 script 模块编译而来的。


看到这里你可能有疑问,远程组件的 style 模块怎么没有在生成的 vue 组件对象上面有提现呢?


答案是 style 模块编译成的 css 不会塞到 vue 组件对象上面去,而是单独通过options上面的addStyle方法传回给我们了。addStyle方法接收的参数textContent的值就是 style 模块编译而来 css 字符串,在addStyle方法中我们是创建了一个 style 标签,然后将得到的 css 字符串插入到页面中。


完整父组件代码如下:

<template>  <LocalChild />  <div class="divider" />  <button @click="showRemoteChild = true">加载远程组件</button>  <RemoteChild v-if="showRemoteChild" /></template>
<script setup lang="ts">import { defineAsyncComponent, ref, onMounted } from "vue";import * as Vue from "vue";import { loadModule } from "vue3-sfc-loader";import LocalChild from "./local-child.vue";
const showRemoteChild = ref(false);
const options = { moduleCache: { vue: Vue, }, async getFile(url) { const res = await fetch(url); const code = await res.text(); return code; }, addStyle(textContent) { const style = Object.assign(document.createElement("style"), { textContent, }); const ref = document.head.getElementsByTagName("style")[0] || null; document.head.insertBefore(style, ref); },};
const RemoteChild = defineAsyncComponent(async () => { const res = await loadModule( "http://localhost:8080/remote-component.vue", options ); console.log("res", res); return res;});</script>
<style scoped>.divider { background-color: red; width: 100vw; height: 1px; margin: 20px 0;}</style>
复制代码


在上面的完整例子中,首先渲染了本地组件LocalChild。然后当点击“加载远程组件”按钮后再去渲染远程组件RemoteChild。我们来看看执行效果,如下图:



从上面的 gif 图中可以看到,当我们点击“加载远程组件”按钮后,在 network 中才去加载了远程组件remote-component.vue。并且将远程组件渲染到了页面上后,通过按钮的点击事件可以看到远程组件的响应式依然有效。


vue3-sfc-loader同时也支持在远程组件中去引用子组件,你只需在options额外配置一个pathResolve就行啦。pathResolve方法配置如下:

const options = {  pathResolve({ refPath, relPath }, options) {    if (relPath === ".")      // self      return refPath;
// relPath is a module name ? if (relPath[0] !== "." && relPath[0] !== "/") return relPath;
return String( new URL(relPath, refPath === undefined ? window.location : refPath) ); }, // getFile方法 // addStyle方法}
复制代码


其实vue3-sfc-loader包的核心代码就 300 行左右,主要就是调用 vue 暴露出来的一些底层 API。如下图:



总结


这篇文章讲了在 vue3 中如何从服务端加载远程组件,首先我们需要使用defineAsyncComponent方法定义一个异步组件,这个异步组件是可以直接在 template 中像普通组件一样使用。


但是由于defineAsyncComponent接收的组件必须是编译后的 vue 组件对象,而我们从服务端拿到的远程组件就是一个普通的 vue 文件,所以这时我们引入了vue3-sfc-loader包。vue3-sfc-loader包的作用就是在运行时将一个 vue 文件编译成 vue 组件对象,这样我们就可以实现从服务端加载远程组件了。


文章转载自:前端欧阳

原文链接:https://www.cnblogs.com/heavenYJJ/p/18346051

体验地址:http://www.jnpfsoft.com/?from=infoq

用户头像

EquatorCoco

关注

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
给我5分钟,保证教会你在vue3中动态加载远程组件_Vue_EquatorCoco_InfoQ写作社区