ChrAlpha's Blog

再谈图片懒加载中的宽高比盒子与布局抖动

2022-06-20·便签格

我绝对不会告诉你这篇文章新建文件夹于 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 文件,这番修改后可能会使源文件可读性很差,遂放弃。

再谈图片懒加载中的宽高比盒子与布局抖动
本文作者
ChrAlpha
发布日期
2022-06-20
更新日期
2023-08-23
转载或引用本文时请遵守 CC BY-NC-SA 4.0 许可协议,注明出处、不得用于商业用途!
CC BY-NC-SA 4.0