之前一篇 在 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 markdown-it
-
- # yarn add probe-image-size markdown-it
probe-image-size 就是上文提到的在线获取图片尺寸的工具。装 markdown-it 是因为 GitHub 上没看懂 Hexo 外如何调用 hexo-renderer-marked。
借助 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="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 文件,这番修改后可能会使源文件可读性很差,遂放弃。