5

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

 2 years ago
source link: https://blog.ichr.me/post/lazyload-ratiobox/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

我绝对不会告诉你这篇文章新建文件夹于 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 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(/\!\[(.*?)\]\((.*?)\)/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;
    });
    content = content.replaceAll(/<img(.*?)src="(.*?)"(.*?)>/gi, (str, p1, p2, p3) => {
        if (sizes[p2]) return str.replace(`src="${p2}"`, `src="${p2}" ${sizes[p2]}`);
        return str;
    });

    // 写出,保存
    await fs.writeFile(dir, content, (err) => {
        if (err) console.log(err);
    });
}

其实想过既然已经引入额外脚本,不如去实现些更厉害的功能,譬如懒加载前先生成的虚化缩略图。但是由于一开始思路限制在修改源 Markdown 文件,这番修改后可能会使源文件可读性很差,遂放弃。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK