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

打印 上一主题 下一主题

主题 1627|帖子 1627|积分 4881

前言

在一些特殊的场景中(比如低代码、减少小程序包体积、雷同于APP的热更新),我们需要从服务端动态加载.vue文件,然后将动态加载的长途vue组件渲染到我们的项目中。今天这篇文章我将带你学会,在vue3中如何去动态加载长途组件。
加入欧阳的高质量vue源码交流群、欧阳平常写文章参考的多本vue源码电子书
defineAsyncComponent异步组件

想必智慧的你第一时间就想到了defineAsyncComponent方法。我们先来看看官方对defineAsyncComponent方法的解释:
   定义一个异步组件,它在运行时是懒加载的。参数可以是一个异步加载函数,或是对加载行为进行更具体定制的一个选项对象。
  defineAsyncComponent方法的返回值是一个异步组件,我们可以像普通组件一样直接在template中使用。和普通组件的区别是,只有当渲染到异步组件时才会调用加载内部现实组件的函数。
我们先来简单看看使用defineAsyncComponent方法的例子,代码如下:
  1. import { defineAsyncComponent } from 'vue'
  2. const AsyncComp = defineAsyncComponent(() => {
  3.   return new Promise((resolve, reject) => {
  4.     // ...从服务器获取组件
  5.     resolve(/* 获取到的组件 */)
  6.   })
  7. })
  8. // ... 像使用其他一般组件一样使用 `AsyncComp`
复制代码
defineAsyncComponent方法接收一个返回 Promise 的回调函数,在Promise中我们可以从服务端获取vue组件的code代码字符串。然后使用resolve(/* 获取到的组件 */)将拿到的组件传给defineAsyncComponent方法内部处置惩罚,最后和普通组件一样在template中使用AsyncComp组件。
从服务端获取长途组件

有了defineAsyncComponent方法后事变从外貌上看着就很简单了,我们只需要写个方法从服务端拿到vue文件的code代码字符串,然后在defineAsyncComponent方法中使用resolve拿到的vue组件。
第一步就是当地起一个服务器,使用服务器返回我们的vue组件。这里我使用的是http-server,安装也很简单:
  1. npm install http-server -g
复制代码
使用上面的下令就可以全局安装一个http服务器了。
接着我在项目的public目录下新建一个名为remote-component.vue的文件,这个vue文件就是我们想从服务端加载的长途组件。remote-component.vue文件中的代码如下:
  1. <template>
  2.   <p>我是远程组件</p>
  3.   <p>
  4.     当前远程组件count值为:<span class="count">{{ count }}</span>
  5.   </p>
  6.   <button @click="count++">点击增加远程组件count</button>
  7. </template>
  8. <script setup>
  9. import { ref } from "vue";
  10. const count = ref(0);
  11. </script>
  12. <style>
  13. .count {
  14.   color: red;
  15. }
  16. </style>
复制代码
从上面的代码可以看到长途vue组件和我们平常写的vue代码没什么区别,有template、ref响应式变量、style样式。
接着就是在终端执行http-server ./public --cors下令启动一个当地服务器,服务器默认端口为8080。但是由于我们当地起的vite项目默认端口为5173,所以为了避免跨域这里需要加--cors。 ./public的意思是指定当前目录的public文件夹。
启动了一个当地服务器后,我们就可以使用 http://localhost:8080/remote-component.vue链接从服务端访问长途组件啦,如下图:

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

  1. const RemoteChild = defineAsyncComponent(async () => {
  2.   return new Promise(async (resolve) => {
  3.     const res = await fetch("http://localhost:8080/remote-component.vue");
  4.     const code = await res.text();
  5.     console.log("code", code);
  6.     resolve(code);
  7.   });
  8. });
复制代码
接下来我们就是在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代码:
  1. <template>
  2.   <p>我是本地组件</p>
  3.   <p>
  4.     当前本地组件count值为:<span class="count">{{ count }}</span>
  5.   </p>
  6.   <button @click="count++">点击增加本地组件count</button>
  7. </template>
  8. <script setup>
  9. import { ref } from "vue";
  10. const count = ref(0);
  11. </script>
  12. <style>
  13. .count {
  14.   color: red;
  15. }
  16. </style>
复制代码
父组件代码:
  1. <template>
  2.   <LocalChild />
  3. </template>
  4. <script setup lang="ts">
  5. import LocalChild from "./local-child.vue";
  6. console.log("LocalChild", LocalChild);
  7. </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组件是一个对象,对象中有render、setup等方法。defineAsyncComponent方法接收的组件就是这样的vue组件对象,但是我们前面却是将vue组件源码丢给他,固然会报错了。
终极办理方案vue3-sfc-loader

从服务端拿到长途vue组件源码后,我们需要一个工具将拿到的vue组件源码编译成vue组件对象。幸运的是优秀的vue不光袒露出一些常见的API,而且还将一些底层API给袒露了出来。比如在@vue/compiler-sfc包中就袒露出来了compileTemplate、compileScript、compileStyleAsync等方法。
假如你看过我写的 vue3编译原理揭秘 开源电子书,你应该对这几个方法以为很熟悉。


  • compileTemplate方法:用于处置惩罚单文件组件SFC中的template模块。
  • compileScript方法:用于处置惩罚单文件组件SFC中的script模块。
  • compileStyleAsync方法:用于处置惩罚单文件组件SFC中的style模块。
而vue3-sfc-loader包的焦点代码就是调用@vue/compiler-sfc包的这些方法,将我们的vue组件源码编译为想要的vue组件对象。
下面这个是改为使用vue3-sfc-loader包后的代码,如下:
  1. import * as Vue from "vue";
  2. import { loadModule } from "vue3-sfc-loader";
  3. const options = {
  4.   moduleCache: {
  5.     vue: Vue,
  6.   },
  7.   async getFile(url) {
  8.     const res = await fetch(url);
  9.     const code = await res.text();
  10.     return code;
  11.   },
  12.   addStyle(textContent) {
  13.     const style = Object.assign(document.createElement("style"), {
  14.       textContent,
  15.     });
  16.     const ref = document.head.getElementsByTagName("style")[0] || null;
  17.     document.head.insertBefore(style, ref);
  18.   },
  19. };
  20. const RemoteChild = defineAsyncComponent(async () => {
  21.   const res = await loadModule(
  22.     "http://localhost:8080/remote-component.vue",
  23.     options
  24.   );
  25.   console.log("res", res);
  26.   return res;
  27. });
复制代码
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字符串插入到页面中。
完整父组件代码如下:
  1. <template>
  2.   <LocalChild />
  3.   <div class="divider" />
  4.   <button @click="showRemoteChild = true">加载远程组件</button>
  5.   <RemoteChild v-if="showRemoteChild" />
  6. </template>
  7. <script setup lang="ts">
  8. import { defineAsyncComponent, ref, onMounted } from "vue";
  9. import * as Vue from "vue";
  10. import { loadModule } from "vue3-sfc-loader";
  11. import LocalChild from "./local-child.vue";
  12. const showRemoteChild = ref(false);
  13. const options = {
  14.   moduleCache: {
  15.     vue: Vue,
  16.   },
  17.   async getFile(url) {
  18.     const res = await fetch(url);
  19.     const code = await res.text();
  20.     return code;
  21.   },
  22.   addStyle(textContent) {
  23.     const style = Object.assign(document.createElement("style"), {
  24.       textContent,
  25.     });
  26.     const ref = document.head.getElementsByTagName("style")[0] || null;
  27.     document.head.insertBefore(style, ref);
  28.   },
  29. };
  30. const RemoteChild = defineAsyncComponent(async () => {
  31.   const res = await loadModule(
  32.     "http://localhost:8080/remote-component.vue",
  33.     options
  34.   );
  35.   console.log("res", res);
  36.   return res;
  37. });
  38. </script>
  39. <style scoped>
  40. .divider {
  41.   background-color: red;
  42.   width: 100vw;
  43.   height: 1px;
  44.   margin: 20px 0;
  45. }
  46. </style>
复制代码
在上面的完整例子中,起首渲染了当地组件LocalChild。然后当点击“加载长途组件”按钮后再去渲染长途组件RemoteChild。我们来看看执行效果,如下图:

从上面的gif图中可以看到,当我们点击“加载长途组件”按钮后,在network中才去加载了长途组件remote-component.vue。并且将长途组件渲染到了页面上后,通过按钮的点击事件可以看到长途组件的响应式依然有效。
vue3-sfc-loader同时也支持在长途组件中去引用子组件,你只需在options额外配置一个pathResolve就行啦。pathResolve方法配置如下:
  1. const options = {
  2.   pathResolve({ refPath, relPath }, options) {
  3.     if (relPath === ".")
  4.       // self
  5.       return refPath;
  6.     // relPath is a module name ?
  7.     if (relPath[0] !== "." && relPath[0] !== "/") return relPath;
  8.     return String(
  9.       new URL(relPath, refPath === undefined ? window.location : refPath)
  10.     );
  11.   },
  12.   // getFile方法
  13.   // addStyle方法
  14. }
复制代码
其实vue3-sfc-loader包的焦点代码就300行左右,主要就是调用vue袒露出来的一些底层API。如下图:

总结

这篇文章讲了在vue3中如何从服务端加载长途组件,起首我们需要使用defineAsyncComponent方法定义一个异步组件,这个异步组件是可以直接在template中像普通组件一样使用。
但是由于defineAsyncComponent接收的组件必须是编译后的vue组件对象,而我们从服务端拿到的长途组件就是一个普通的vue文件,所以这时我们引入了vue3-sfc-loader包。vue3-sfc-loader包的作用就是在运行时将一个vue文件编译成vue组件对象,这样我们就可以实现从服务端加载长途组件了。
最后推荐一下欧阳自己写的开源电子书vue3编译原理揭秘,这本书初中级前端能看懂。完全免费,只求一个star。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

张国伟

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表