给我5分钟,保证教会你在vue3中动态加载远程组件
前言
在一些特殊的场景中(比如低代码、减少小程序包体积、类似于APP的热更新),我们需要从服务端动态加载.vue
文件,然后将动态加载的远程vue组件渲染到我们的项目中。今天这篇文章我将带你学会,在vue3中如何去动态加载远程组件。
欧阳写了一本开源电子书vue3编译原理揭秘,这本书初中级前端能看懂。完全免费,只求一个star。
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代码没什么区别,有template
、ref
响应式变量、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组件是一个对象,对象中有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
包后的代码,如下:
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组件对象,这样我们就可以实现从服务端加载远程组件了。
关注公众号:【前端欧阳】,给自己一个进阶vue的机会
另外欧阳写了一本开源电子书vue3编译原理揭秘,这本书初中级前端能看懂。完全免费,只求一个star。
给我5分钟,保证教会你在vue3中动态加载远程组件的更多相关文章
- Module加载的详细说明-保证你有所收获
模块 HTML 网页中,浏览器通过<script>标签加载 JavaScript 脚本. <!-- 页面内嵌的脚本 --> <script type="appl ...
- 史上最简单的vi教程,10分钟包教会
从第一次接触vi/vim到现在已经十几年了,在这个过程中,来来回回,反反复复,学习vi很多次了. 虽然关于vi的使用,我还远未达到"专家"的水平,但对于vi的使用,我有话说. 1. ...
- ios同步线程(dispatch_sync)保证代码在主线程中执行
- (BOOL)transitionToNextPhase { // 保证代码在主线程 if (![[NSThread currentThread] isMainThread]) { dispatch ...
- 一分钟带你了解下MyBatis的动态SQL!
MyBatis的强大特性之一便是它的动态SQL,以前拼接的时候需要注意的空格.列表最后的逗号等,现在都可以不用手动处理了,MyBatis采用功能强大的基于OGNL的表达式来实现,下面主要介绍下. 一. ...
- 使用SVG图像作为loading加载 以保证图像高清不模糊
使用教程 接下来设计达人网小编为大家讲解这个使用方法,其实是相当简单的. STEP 1: 复制你想要的SVG加载动画代码到<body>里面,小编随意复制一个代码如下:<svg ver ...
- 自己写的保证js顺序加载的方法
var arr =["test1.js","test2.js","test3.js"] loadScripts:function(arr){ ...
- 安卓开发笔记——关于开源组件PullToRefresh实现下拉刷新和上拉加载(一分钟搞定,超级简单)
前言 以前在实现ListView下拉刷新和上拉加载数据的时候都是去继承原生的ListView重写它的一些方法,实现起来非常繁杂,需要我们自己去给ListView定制下拉刷新和上拉加载的布局文件,然后添 ...
- angularjs 与 UEditor开发,添加directive,保证加载顺序正常
'use strict'; angular.module('app.core').directive('ueditor', [function () { return { restrict: 'A', ...
- 10分钟了解 代理模式与java中的动态代理
前言 代理模式又分为静态代理与动态代理,其中动态代理是Java各大框架中运用的最为广泛的一种模式之一,下面就用简单的例子来说明静态代理与动态代理. 场景 李雷是一个唱片公司的大老板,很忙, ...
- 三分钟学会Redis在.NET Core中做缓存中间件
大家好,今天给大家说明如何在.NET Core中使用Redis,我们在想要辩论程序的好与坏,都想需要一个可视化工具,我经常使用的是一位国内大牛开发的免费工具,其Github地址为: https://g ...
随机推荐
- python _XMLParser.__init__()初始化失败,提示“takes 1 positional argument but 4 were given”
问题: 在一个新的环境下,执行openpyxl相关的操作,初始化时,逐步执行,需要调到 ElementTree.py _XMLParser.__init__(self, html, target, e ...
- 京东云上centos8.2 安装 consul1.11.1
做个笔记下 -- 前言 部分内容有参考网友的,但是地址不记得了! 安装内容基本参考官网的和上一个网友的 官网地址: https://www.consul.io/downloads 以下是使用root方 ...
- [WPF]用HtmlTextBlock实现消息对话框的内容高亮和跳转
动手写一个简单的消息对话框一文介绍了如何实现满足常见应用场景的消息对话框.但是内容区域的文字仅仅起到信息展示作用,对于需要部分关键字高亮,或者部分内容有交互性的场景(例如下图提示信息中的"w ...
- 【资料分享】Xilinx Zynq-7010/7020工业评估板规格书(双核ARM Cortex-A9 + FPGA,主频766MHz)
1 评估板简介 创龙科技TLZ7x-EasyEVM是一款基于Xilinx Zynq-7000系列XC7Z010/XC7Z020高性能低功耗处理器设计的异构多核SoC评估板,处理器集成PS端双核ARM ...
- 全国产!瑞芯微RK3568J/RK3568B2工业核心板规格书
核心板简介 创龙科技SOM-TL3568是一款基于瑞芯微RK3568J/RK3568B2处理器设计的四核ARM Cortex-A55全国产工业核心板,每核主频高达1.8GHz/2.0GHz.核心板CP ...
- 某手创作服务 __NS_sig3 sig3 | js 逆向
拿获取作品列表为例 https://cp.kuaishou.com/rest/cp/works/v2/video/pc/photo/list?__NS_sig3=xxxxxxxxxxx 搜索__NS_ ...
- 劫持TLS绕过canary && 堆和栈的灵活转换
引入:什么是TLScanary? TLScanary 是一种在 Pwn(主要是二进制漏洞利用)中常见的技术,专门用于处理 TLS 保护的二进制文件.在安全竞赛(例如 CTF)和漏洞利用场景中,攻击者需 ...
- 经典面试题函数柯里化: add(1)(2)(3) = 6
function currying() { const args = Array.prototype.slice.call(arguments); const inner = function () ...
- [oeasy]python0129_unicode_中文字符序号_十三道大辙_字符编码解码_eval_火星文
unicode 中文字符分类 回忆上次内容 字符集 从博多码 到 ascii 再到 iso-8859 系列 各自割据 如何把世界上各种字符统进行编码 unicode顺势而生不断进化 不过字符总量超 ...
- Day 9 - 线段树
线段树 引入 线段树是算法竞赛中常用的用来维护 区间信息 的数据结构. 线段树可以在 \(O(\log N)\) 的时间复杂度内实现单点修改.区间修改.区间查询(区间求和,求区间最大值,求区间最小值) ...