有这样一个场景:接口返回一段文本,文本中有多个 [[]] 格式的内容,前端展示时,需要对[[]] 中的内容使用自定义组件渲染,其余内容当作字符串展示。
由于文本内容是接口动态返回的,所以,无法在 template 中定义展示的模板,而是需要编程式地动态生成虚拟 DOM 并渲染。
相关的 Vue 特性
component 标签和动态组件
component 标签有几种用法:
<component :is="xxx"></component>
is 可以是一个组件名的 string(前提是该组件已引入)
import Foo from "./Foo.vue";
export default {
components: { Foo },
data() {
return {
view: "Foo",
};
},
};
<component :is="view" />
也可以是一个 component 实例
import Foo from "./Foo.vue";
export default {
components: {
Foo,
},
data() {
return {
Foo,
};
},
<component :is="Foo"></component>
还可以是一个 jsx 实例
getJSX() {
return <el-button>get jsx</el-button>;
},
<component :is="getJSX()"></component>
方案设计
- 编写一个函数,传入接口返回的字符串,返回
jsx - 把
jsx传给component标签的is参数
函数体的处理逻辑
由于[[]]内的内容需要替换为自定义组件,所以可以对字符串根据[[ 和]]进行分割。对于每个[[和]]外的内容,返回一个文本标签;对于[[和]]内的内容,返回一个组件的 jsx。
如何根据 [[xxx]] 返回 jsx?
这属于业务逻辑,可以使用 js 的 String API 对 [[xxx]] 进行处理,得到业务需要的内容,如:
使用 match 进行字符串内容提取
使用 replace 进行字符串内容替换
或者另外调接口获取其他数据,然后一起展示
实现
根据上面的方案,写出第一版代码
import { ref, onMounted } from "vue";
import MyContent from "./MyContent.vue"; // 渲染[[]]的自定义组件
const dynamicContent = ref();
const initFn = async () => {
const str = await getContent(); // 调接口获取文本
const regex = /(\[\[)(.*?)(\]\])/g;
const array = splitFn(str); // 对文本内容按照 [[ ]] 进行切割
const result = array.map((item) => {
if (item.match(regex)) {
const current = item.replace(regex, (match, p1, p2) => {
return p2;
});
return <MyContent content={current}></MyContent>;
} else {
return <span>{item}</span>;
}
});
const newResult = repeatFn(result, 100);
dynamicContent.value = <div>{newResult}</div>;
};
onMounted(initFn);
<component :is="dynamicContent"></component>
存在的不足
上述代码可以实现动态组件的渲染,但是,有以下缺点:
- 一次性渲染出所有动态组件 动态组件内部可能需要单独调接口,如果一次性渲染出所有组件,就会并发调用很多接口,阻塞页面渲染
- 无法按需渲染 如果组件内部 DOM 比较复杂,渲染计算量过大,而且存在大量这样的组件,我们希望等到这个组件要出现在视口时,再进行渲染,以避免页面卡顿。
有什么优化的方案呢?
为了避免大量组件一次性渲染造成的接口并发量过大,我们可以在组件内部控制 DOM 展示,或者接口调用的时机:在组件将要出现在视口时,才展示 DOM 或者调用接口。
可以通过 IntersectionObserver 来实现。
API 文档见:developer.mozilla.org/zh-CN/docs/…
IntersectionObserver 有 2 个参数,callback 和 options
callback 参数如下
| 参数名 | 作用 | 类型 |
|---|---|---|
| entries | Array< IntersectionObserverEntry > | |
| observer | IntersectionObserver 实例 | IntersectionObserver |
IntersectionObserverEntry 的所有属性详见:developer.mozilla.org/zh-CN/docs/…
下面列举几个常用的属性
| 属性名 | 作用 | 类型 |
|---|---|---|
| isIntersecting | 目标元素和区域是否相交 | Boolean |
| intersectionRatio | 交叉比例 | Number |
options 参数如下
| 参数名 | 作用 | 类型 |
|---|---|---|
| root | 视口的 dom 元素 | DOM element |
| threshold | 阈值,默认为 0 |
threshold 的详细用法见:developer.mozilla.org/zh-CN/docs/…
如何使用 IntersectionObserver 来实现按需渲染呢?
在组件 mounted 时创建 IntersectionObserver 对象
io = new window.IntersectionObserver(intersectionHandler, {
rootMargin: "0px",
root: props.viewport,
threshold: [0, Number.MIN_VALUE, 0.01],
});
使用 observe 函数监听目标 DOM 元素
observe 函数的详细用法见:developer.mozilla.org/zh-CN/docs/…
io.observe(componentRef.value);
组件销毁时,取消监听
if (io) {
io.unobserve(componentRef.value);
}
优化后的代码如下
<div ref="componentRef">
<component :is="displayContent"></component>
</div>
let io = null;
const intersectionHandler = (entries) => {
if (
// 正在交叉
entries[0].isIntersecting || // 交叉率大于0
entries[0].intersectionRatio
) {
initFn(); // 业务逻辑
io.unobserve(componentRef.value);
}
};
const initIntersectionObserver = () => {
io = new window.IntersectionObserver(intersectionHandler, {
rootMargin: "0px",
root: props.viewport,
threshold: [0, Number.MIN_VALUE, 0.01],
});
io.observe(componentRef.value);
};
onMounted(initIntersectionObserver);
onBeforeUnmount(() => {
if (io) {
io.unobserve(componentRef.value);
}
});
优化前的运行效果
优化后的运行效果