再谈图片懒加载中的宽高比盒子与布局抖动
我绝对不会告诉你这篇文章新建文件夹于 4 月 4 日;我绝对不会解释为什么半年不写更新博客。
早在两年前,我曾发过一篇 在 Hexo 中实现图片懒加载 的文章,借助 srcset
实现了更好的效果。但是在那篇文章里提到一个问题——加载原图时引发的布局抖动——也一直搁置着没有解决,毕竟想着我网站的图片总是东一份西一份,不是很好计算占位空间。
但其实也不是没有办法,例如 probe-image-size 等项目都很好的实现在不下载整张图片的前提下获取图片尺寸数据。
既然借口没了……
先说思路
分配占位空间以对抗布局抖动,我之前一只采用的是取巧的方案——直接取封面图的长宽比,这样至少主页等位置能够避免抖动了,但是访客浏览时间最长的页面——文章页——依然有存在抖动的可能。
过了两年多,终归还是让我耐不住开始考虑更优的解决方案了。
其实「网站的图片总是东一份西一份」并非「不是很好计算占位空间」的理由,毕竟早就有在线的图片尺寸获取方案。主要是在图片不算少的网站中,每次构建都重新获取一次图片尺寸未免太浪费时间。就算你不在乎,你的自动部署 CI 可能就要撑爆额度来向你抗议了。而我的博客全部用 Markdown 存储原始内容、由 Hexo 管理生成页面,暂且也没发现什么方便的缓存方案。
诶?既然使用 Markdown 存储原始内容,那为什么不直接修改 Markdown 原始文件呢?
再说实现
一开始,为了顺应 Hexo 管理,我试图将脚本写进 Hexo 的 Script 里,这样也能更方便的调用一系列 hexo-
组件。但是半天只有同步写法可以正常运行,一旦使用异步写法,就会在脚本尚未执行结束是 Hexo 便开始构建,导致不希望的结果。故放弃,把脚本写在最外面并用 npm 运行吧。
// package.json
"scripts": {
+ "size": "node ./imageSize/index.js",
- "build": "npx hexo cl && npx hexo g && node ./minify/minify.js"
+ "build": "npx hexo cl && node ./imageSize/index.js && npx hexo g && node ./minify/minify.js"
},
然后安装组件:
npm i probe-image-size
# yarn add probe-image-size
probe-image-size
就是上文提到的在线获取图片尺寸的工具。装 markdown-it
是因为 GitHub 上没看懂 Hexo 外如何调用 hexo-renderer-marked
。
通过 Hexo API - Rendering 可知,在 hexo.render
下可以通过 Hexo 调用渲染服务。因此不再需要额外安装诸如 markdown-it
之类的渲染包。
const Hexo = require('hexo')
const hexoMarkdown = async (str) => {
const hexo = new Hexo(process.cwd(), {
silent: true,
})
await hexo.init()
await hexo.load()
const renderer = hexo.render
const markdown_str = await renderer.render({ text: str, engine: 'markdown' })
return markdown_str
}
借助 hexo-fs
先扫描出 source
文件夹下所有 .md
文件:
//./imageSize/index.js
fs.listDir(path.join(__dirname + '/../source'), (err, list) => {
if (err) console.log(err);
for (dir of list) {
if (!dir.endsWith('.md')) continue;
handle(path.join(__dirname + '/../source/' + dir)).catch(err => console.log(err));
}
});
async function handle(dir) {}
然后是处理函数,里面自己读注释吧:
//./imageSize/index.js
async function handle(dir) {
let content = await fs.readFile(dir).catch((err) => console.log(err));
let content_md = markdown.render(content);
// 无图片,直接结束
if (!content_md.match(/<img(.*?)src="(.*?)"(.*?)>/gi)) return;
let list = [], sizes = {};
// 扫出所有 **没有设定尺寸** 的图片链接
content_md.match(/<img(.*?)src="(.*?)"(.*?)>/gi).forEach((item) => {
item.replace(/<img(.*?)src="(.*?)"(.*?)>/gi, (str, p1, p2, p3) => {
// 确定之前没有过设定尺寸,否则便不用再获取一次了
if (!p1.match(/style\=\"(.*?)width\:(.*?)\"/gi) && !p3.match(/style\=\"(.*?)width\:(.*?)\"/gi)) list.push(p2);
return '';
});
});
// 去重,确定有需要处理的图片,否则结束
list = Array.from(new Set(list));
if (!list) return;
// 用 probe-image-size 获取图片尺寸并用对象保存
// 这里不能用 `forEach`,否则不能 `await` 了
for (img of list) {
let size = await probe(img).catch((err) => console.log(img + '\n' + err));
if (size.width) sizes[img] = `style="max-width: ${size.width + size.wUnits}; aspect-ratio: ${size.width + ' / ' + size.height}"`;
}
// 替换 Markdown 原文件
content = content.replaceAll(/<img(.*?)src="(.*?)"(.*?)>/gi, (str, p1, p2, p3) => {
if (sizes[p2]) return str.replace(`src="${p2}"`, `src="${p2}" ${sizes[p2]}`);
return str;
});
content = content.replaceAll(/\!\[(.*?)\]\((.*?)\)/gi, str => {
let str_md = markdown.renderInline(str);
str_md = str_md.replace(/<img(.*?)src="(.*?)"(.*?)>/gi, (mth, m1, m2, m3) => {
if (sizes[m2]) return mth.replace(`src="${m2}"`, `src="${m2}" ${sizes[m2]}`);
return str;
});
return str_md;
});
// 写出,保存
await fs.writeFile(dir, content, (err) => {
if (err) console.log(err);
});
}
后
其实想过既然已经引入额外脚本,不如去实现些更厉害的功能,譬如懒加载前先生成的虚化缩略图。但是由于一开始思路限制在修改源 Markdown 文件,这番修改后可能会使源文件可读性很差,遂放弃。