思考: 如何在 Markdown 博客中实现图片懒加载 ?
通常我们可以使用 Vue-Lazyload自定义指令来实现,但是通常这些库都需要template有真实的 img 标签,例如:
<template>
<img v-lazy="imgUrl" />
</template>
而我的博客是,基于 Markdown 的博客,后台管理系统通过提交Markdown文本提交到数据库,前端博客展示时用marked库解析为
html,用v-html直接把解析好的html插入到页面中
这种情况,template中没有真实的img标签,没办法使用上面的自定义指令.
不过这种情况,实现图片懒加载也非常简单
实现图片懒加载一般有两种方案:
-
监听
scroll事件配合getBoundingClientRect等方法判断图片是否出现在页面可视区域 -
采用
Intersection Observerapi
IntersectionObserver 兼容性
由于项目是vue3的,vue3本身就不支持IE11,所以项目对于兼容性要求没那么高了,再加上这里我个人的项目,不需要太考虑兼容性,所以我选择用IntersectionObserver的解决方案
实现图片懒加载
自定义 marked 的 renderer
为了防止img直接加载,我们还需要把它的src的属性改为data-src,如何实现呢?
我们可以自定义marked的renderer
import marked from "marked";
const renderer = new marked.Renderer();
// 自定义渲染器,renderer.image的意思是,在渲染img时,采用自定义的渲染方式
renderer.image = function (href, title, alt) {
return `<img data-src="${href}" alt="${alt}">`;
};
export default renderer;
使用时只需要把我们实现好的renderer通过配置项传入即可,它内部会和默认的渲染器进行一个合并
通过 nextTick 获取到渲染完的真实 dom
解析完markdown 为 html , 把html插入页面后,调用 nextTick把文章内容的根节点传入 lazyImage函数中,最后实现lazyImage 函数
marked(markdown, {
renderer,
});
<div v-html="contentHTML" class="markdown-body" ref="contentEl"></div>
// 1.通过api 获取到markdown内容通过marked解析
// 2.解析完后赋值给 contentHTML
// 之后调用 nextTick 可以获取到渲染后的dom节点
nextTick(() => {
if (contentEl.value) {
// 懒加载图片
lazyImage(contentEl.value);
}
});
实现 lazyImage 函数
lazyImage 通过接收一个 dom 节点来获取它所有的子img节点,然后对 img 进行懒加载
const callback: IntersectionObserverCallback = (entries) => {
entries.forEach((item) => {
// isIntersecting是一个Boolean值,判断目标元素当前是否可见
if (item.isIntersecting) {
// 到了可见区域,需要img节点的data-src改为src,并且停止监听这个img节点
const src = item.target.getAttribute("data-src") || "";
item.target.setAttribute("src", src);
item.target.removeAttribute("data-src");
//停止监听特定目标元素
intersectionObserver.unobserve(item.target);
}
});
};
const intersectionObserver = new IntersectionObserver(callback);
export default function lazyImage(parentEl: HTMLElement): void {
// 获取到父节点下的所有带data-src的img标签
const imgs = parentEl.querySelectorAll("img[data-src]");
imgs.forEach((item) => {
//开始监听一个目标元素
intersectionObserver.observe(item);
});
}
这样就实现好了图片懒加载
解决图片抖动问题
虽然上面已经实现好了图片懒加载,但是还不够完美,这也是我在ipad上刷掘金的经常遇到的问题.
因为学校图书馆 WiFi 不稳定,有时候网速特别慢.当网络慢的时候,在掘金打开一篇文章,往下滚动页面,阅读文章的时候,有可能因为网络原因,上面的一张图片刚开始没有加载出来,等我看了一会文章后,上面的图片又突然加载好了,造成页面突然的抖动.
为了更加直观的理解上面的内容,我特意的录了 gif
在控制台把图片
分析原因
造成这样的原因就是图片刚开始没有高度,等图片加载出来后又有了高度,造成页面的抖动.
我们通常的解决方案是在img标签外包裹一个img-wrap,给img-wrap设置宽高,img的宽高都设置为100%,但是问题是 markdown 中我们不知道图片的高度,或者说图片的比例.如果说我们有了图片的大小信息,就可以先给图片一个高度.这样页面就不会出现抖动的问题了.
经过查阅资料,我了解到css中有一个aspect-ratio的属性
aspect-ratio
aspect ratio 翻译为中文就是宽高比(也称:纵横比)即 x:y。
首先定宽高比,在网页中开发中时很常见的,而且面试也偶有出现。我之前使用的 padding-bottom 来实现的。不过,今天的 CSS 提供了更加方便的解决方案:aspect-ratio。aspect-ratio 翻译为中文就是纵横比的意思。
因为博客的图片宽度我都是设置的85%,所以理论上只需要得到图片的纵横比,就可以配合aspect ratio属性,给img初始的高度,例如图片的大小是1887 * 2831,则需要设置style="aspect-ratio:1887 / 2831;"图片就有了个默认高度.
但是问题有来了我们怎么能得到图片的真实宽高呢?
我想到两个方案:
-
后端给图片的
url添加图片宽高信息但是这需要图片链接本身必须是自己的服务器才行,如果是外链的图片就不行了,解决方案就是在提交 markdown 的时候把外链的图片都换成的自己服务器的
-
我们可以在
图片加载完后获取图片的naturalWidth和naturalHeight属性naturalWidth 和 naturalHeight 是 html5 新增的属性,它们可以直接获取图片的原始宽高。
图片加载完后 ? 图片加载完成后还有屁用啊!
别慌,我们可以在后台管理系统中获取到图片的
naturalWidth和naturalHeight属性,把这些信息添加到图片的alt中例如这样子:
如果给图片添加大小信息 ? 我采用的是正则的方案,在提交到服务器前,先判断图片有没有大小信息,如果没有给图片添加图片的大小信息.
// 获取所有图片的正则
const reg = /!\[(.*?)\]\((.*?)\)/g;
// 判断有没有图片大小信息的正则
const hasSizeReg = /\d+\*\d+$/;
interface ImagesInfo {
imageText: string;
title: string;
src: string;
naturalWidth?: number;
naturalHeight?: number;
}
//
export default async function markdownImageAddSize(markdown: string) {
const images: ImagesInfo[] = [];
markdown.replace(reg, replaceHelper);
return await handleImageAll(images, markdown);
function replaceHelper(imageText: string, title: string, src: string): string {
images.push({ imageText, title, src });
return imageText;
}
}
async function handleImageAll(images: ImagesInfo[], markdown: string) {
let res: string = markdown;
for (const img of images) {
const { title, src, imageText } = img;
try {
const { naturalWidth, naturalHeight } = await getImageNaturalInfo(src);
let sizeInfo = "";
//如果没有sizeInfo,则加sizeInfo
if (!hasSizeReg.test(title)) {
sizeInfo = `-${naturalWidth}*${naturalHeight}`;
}
const newImageText = ``;
res = res.replaceAll(imageText, newImageText);
} catch (e) {
console.warn(`image ${img.src} error`);
}
}
return res;
}
// 通过图片的src,获取所有图片的大小信息
function getImageNaturalInfo(src: string) {
return new Promise<{
naturalWidth: number;
naturalHeight: number;
}>((resolve, reject) => {
// 通过 new Image 创建一个 Image , 并设置src
const img = new Image();
img.src = src;
// 因为在markdown有预览,所以这些src的图片都加载好了,onload会很快
img.onload = function () {
//
const naturalWidth = img.naturalWidth;
const naturalHeight = img.naturalHeight;
resolve({
naturalWidth,
naturalHeight,
});
};
img.onerror = function (e) {
reject(e);
};
});
}
这样处理后,markdown 文本在传入数据库前都会给图片添加图片大小信息
博客中就可以用这个信息来设置aspect-ratio 属性
改造 markedRenderer
import marked from "marked";
// 获取图片的size
const getSizeReg = /(\d+)\*(\d+)$/;
const renderer = new marked.Renderer();
renderer.image = function (href, title, alt) {
const res = getSizeReg.exec(alt);
// 如果有size信息,则添加style aspect-ratio
if (res) {
const [, naturalWidth, naturalHeight] = res;
return `<img data-src="${href}" style="aspect-ratio:${naturalWidth} / ${naturalHeight};" alt="${alt}">`;
} else {
return `<img data-src="${href}" alt="${alt}">`;
}
};
export default renderer;
添加 loading 效果
@keyframes skeleton-loading {
0% {
background-position: 100% 50%;
}
100% {
background-position: 0 50%;
}
}
.markdown-body :deep(img) {
width: 85%;
background: linear-gradient(90deg, #f2f2f2 25%, #e6e6e6 37%, #f2f2f2 63%);
background-size: 400% 100%;
padding: 0;
animation: skeleton-loading 1s ease infinite;
}
最终效果图
对比
优化前
优化后
想去尝试的小伙伴可以访问我的个人博客: coderly的个人博客