分类 Web 下的文章

公司的Web项目,处理图片延迟加载时,遇到一个图片预留占位的显示问题。记录一下相关解决方案。

1. 需求

本来要解决的问题是,图片“延迟加载”。然后图片未加载之前,需要预留占位,避免加载后撑大页面(主要是为了更好看吧)。

2. 解决方案

图片延迟加载,使用<img>loading="lazy"即可。现代浏览器,当前对其支持还不错。另外,最好不用使用CSS的background加载图片,因为没有很好(或者说简单)的延迟加载解决方案。

对于图片预留占位,主要根据界面设计的布局,选择不同的处理方法。比较麻烦的是,需要自适应浏览器窗口大小的情况。如下:

  • 已知图片宽高。可以随便整。
  • 图片框固定大小。使用CSS的object-fit获得最佳显示效果。
  • 固定列数的图片框自适应浏览器窗口。按浏览器窗口自动计算图片框的宽度。
  • 图片框宽度自适应。使用动态的正方形图片框。这是最终采用的方案。

2.1. 已知图片宽高

可以设置图片按比例缩放,或者按最长边等比例缩放。这个不用说了。

2.2. 图片框固定大小

例如图片框固定,宽为300px,高为300px。图片保持比例,并完整显示,使用CSS的object-fit:contain;。代码如下:

<div style="width:300px; height:300px;">
  <img src="..." style="width:100%; height:100%; object-fit:contain;" />
</div>

2.3. 固定列数的图片框自适应浏览器窗口

一般是列表中带图片的情况,可以使用CSS的单位vwvh

  • vw,浏览器窗口宽度的1%
  • vh,浏览器窗口高度的1%

关于CSS的长度单位,详见: - CSS:层叠样式表 | MDN

缺点:

  • 如果图片的宽高,不能根据屏幕的宽高计算出来,此方案不适用。
  • 如果图片宽高不能适应图片框的比例,也是使用object-fit:contain;按比例缩放。

以下示例,按4列显示,图片框是宽高都为窗口宽度24%的正方形:

<style>
body{margin:0; padding:0;}
.ItemList{width:100%; margin:0; padding: 0; list-style: none; display: flex; flex-wrap: wrap; justify-content: space-evenly;}
.ItemWrap{width:24%; margin:1vw 0 0; padding:0; display: block;}
.ItemWrap img{width:24vw; height:24vw; object-fit: contain;}
</style>
<ul class="ItemList">
  <li class="ItemWrap"><img src="..." loading="lazy" /></li>
  <li class="ItemWrap"><img src="..." loading="lazy" /></li>
  <li class="ItemWrap"><img src="..." loading="lazy" /></li>
  <li class="ItemWrap"><img src="..." loading="lazy" /></li>
  <li class="ItemWrap"><img src="..." loading="lazy" /></li>
  <li class="ItemWrap"><img src="..." loading="lazy" /></li>
  <li class="ItemWrap"><img src="..." loading="lazy" /></li>
  <li class="ItemWrap"><img src="..." loading="lazy" /></li>
</ul>

2.4. 图片框宽度自适应

动态宽高的正方形图片框,是比较折中和灵活的方案。可以不用知道图片宽高。图片框宽度动态计算,高度设为跟高度一致。示例如下:

<style>
body{margin:0; padding:0;}
.ItemList{width:100%; margin:0; padding:0; list-style:none; display:flex; flex-wrap:wrap; justify-content:space-evenly;}
.ItemWrap{width:24%; margin:1vw 0 0; padding:0; display:block; background-color:#c5a29c;}
.ImgWrap{width:100%; height:0; padding-bottom:100%; overflow:hidden; display:block; position:relative;}
.ImgWrap img{width:100%; height:100%; object-fit:contain; position: absolute;}
</style>
<ul class="ItemList">
  <li class="ItemWrap"><a class="ImgWrap"><img src="" loading="lazy"></a></li>
  <li class="ItemWrap"><a class="ImgWrap"><img src="" loading="lazy"></a></li>
  <li class="ItemWrap"><a class="ImgWrap"><img src="" loading="lazy"></a></li>
  <li class="ItemWrap"><a class="ImgWrap"><img src="" loading="lazy"></a></li>
  <li class="ItemWrap"><a class="ImgWrap"><img src="" loading="lazy"></a></li>
  <li class="ItemWrap"><a class="ImgWrap"><img src="" loading="lazy"></a></li>
  <li class="ItemWrap"><a class="ImgWrap"><img src="" loading="lazy"></a></li>
  <li class="ItemWrap"><a class="ImgWrap"><img src="" loading="lazy"></a></li>
</ul>

解析:

  • ul标签,使用灵活的flex样式显示列表。
  • li标签,决定列数。
  • a标签,作为图片框,限制图片显示的宽高。
  • a标签的height:0; padding-bottom:100%;,通过padding-bottom设置高度与宽度一致,是这个方案最巧妙的地方
  • a标签的overflow:hidden;,避免内嵌的图片溢出。
  • a标签的position:relative;,把宽高传给子级。
  • img标签,通过position: absolute;悬浮显示在图片框上面,其宽高获取了图片框(即父级)的宽高。

3. 后续问题

图片加载时,小图最好加载对应的缩略图。体积小,加载快,体验好。但是要考虑图片框大小,避免小图被拉大而导致模糊,降低用户观感。

上个月,发现“cloudflare.com”被解析为“127.0.0.1”,于是研究了一下DNS污染。

1. 检测

可以直接使用相关网站,检查各个DNS针对指定域名的解析是否正确。例如:

也可以使用命令或工具,查询使用指定DNS解析指定域名的结果。nslookup命令的示例如下:

# nslookup 域名 DNS地址
nslookup cloudflare.com 223.5.5.5

2. 解决方案

一般设置DNS为可靠的共用DNS即可。由于是网络运营商的DNS发现的污染,所以不推荐使用三大运营商的DNS。暂时改为使用“阿里公共DNS”。

最简单的是,修改网络出口设备(例如路由器)的DNS,所有网络设备(例如手机、电脑)都使用其默认DNS。比较麻烦的是,各个网络设备各自设置DNS。

国内外的免费公共DNS,可参考:

几个比较有名的DNS如下:

3. 扩展内容

3.1. 关于nslookup命令

一般各大系统都有nslookup命令。对于Debian 11和Ubuntu 22.04,可能没有默认安装nslookup,需要手动安装。

# Debian或Ubuntu,安装nslookup命令
sudo apt install bind9-dnsutils

# 查看域名的DNS A记录解析
nslookup -type=A cloudflare.com 223.5.5.5

该命令的详细说明,可以参考man nslookup或Debian官方文档:nslookup(1) — bind9-dnsutils — Debian bookworm — Debian Manpages

3.2. 如何修改DNS配置

3.2.1. 总结

  • 路由器,进入其管理后台,修改DNS配置。
  • 网络终端设备(电脑,手机等),需要明确配置DNS的范围,一般是:全局、指定网络接口、指定代理服务。
  • 全局DNS,需确定管理DNS服务的程序,再修改其配置文件。
  • 指定网络接口的DNS,一般修改其网卡设置。
  • 代理服务的DNS,一般不走本地设置,需要参考该代理服务的配置说明。

3.2.2. 关于配置全局DNS

对于Linux(例如Debian 12),一般查看/etc/resolv.conf文件,可以了解当前使用什么DNS。直接修改该文件,可以更改当前全局DNS,例如:

# 设置DNS为阿里DNS
nameserver 223.5.5.5

但是,如果有其它程序接管了/etc/resolv.conf文件,比如systemd-resolved服务,系统重启后会该文件被重置,导致设置无效。注意各个系统的情况不同,比如:

  • Debian 11/12,默认没有安装systemd-resolved
  • Ubuntu 22.04,默认安装并启用systemd-resolved

3.2.3. 关于配置网络接口DNS

  • 图形界面,通过网络配置,修改相应的DNS。

    • 例如Ubuntu 22.04,使用NetworkManager管理。网络接口的配置文件在/etc/NetworkManager/system-connections/
  • 命令界面,一般修改配置文件/etc/network/interfaces

    • 例如Debian 11/12,其示例配置如下:
# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).

source /etc/network/interfaces.d/*

# The loopback network interface
auto lo
iface lo inet loopback

# The primary network interface
allow-hotplug enp3s0
#iface enp3s0 inet dhcp
iface enp3s0 inet static
    address 192.168.0.100
    netmask 255.255.255.0
    gateway 192.168.0.1
    dns-nameservers 223.5.5.5 192.168.0.1

3.3. 管理DNS缓存

3.3.1. Linux的DNS缓存

  1. 使用systemd-resolve,适合Ubuntu 20.04及以下、Debian 11/12等。
# 清除DNS缓存
sudo systemd-resolve --flush-caches

# 查看DNS缓存情况
sudo systemd-resolve --statistics

对于Debian 11/12,需要启用systemd-resolve服务。

sudo systemctl enable systemd-resolved.service
  1. 使用resolvectl,适合Ubuntu 20.04以上。
# 清除DNS缓存
sudo resolvectl flush-caches

# 查看DNS缓存情况
sudo resolvectl statistics
  1. 重启网络服务,清除DNS缓存,适合一般Linux。
# 基于Init.d的系统
sudo /etc/init.d/networking restart

# 基于SystemD的系统
sudo service networking restart

3.3.2. Windows的DNS缓存

rem 清除DNS缓存
ipconfig /flushdns

3.3.3. Chrome浏览器的DNS缓存

Chrome本身建立了自己的DNS缓存,并提供简单的管理功能。打开链接:chrome://net-internals/#dns 即可。

利用Nginx部署了图片服务,为了实现请求图片时进行鉴权,使用了auth_request的配置。

参考官网文档:Authentication Based on Subrequest Result | NGINX Documentation

整理后的示例代码,如下:

server {
    ...... # 略过一些配置
    underscores_in_headers on; # 可选。允许请求header名称带下划线(_)

    location ^~ /images/ {
        # 配置鉴权规则
        auth_request /auth;

        # 生成鉴权URL,并添加请求参数。
        set $auth_request_uri $uri; # 创建变量,传递鉴权URL
        if ($uri ~ ^/images/(.*)$) {
            # 利用if实现对URL进行字符串替换,生成想要的鉴权URL,并带上请求参数
            # 由于静态文件,尤其是图片,一般使用GET方法,所以URL的请求参数可以作为鉴权参数
            set $auth_request_uri /auth/$1?$args;
        }

        # 将401、403状态码映射为404。即鉴权失败时,让浏览器以为文件不存在。
        # 由于“/404.html”不存在,Nginx会返回默认的404错误页面。
        error_page 401 403 =404 /404.html;

        # 设置图片存放目录,/images/有/结尾,这里也要有/结尾
        alias /opt/sites/pub_img/;
    }

    # 这里配置鉴权后端的请求,即鉴权URL
    location = /auth {
        internal; # 只能内部访问
        proxy_pass http://127.0.0.1:8080$auth_request_uri; # 设置完整的鉴权URL
        proxy_pass_request_headers on; # 默认值。转发所有原始的请求header到鉴权URL
        proxy_pass_request_body off; # 不向鉴权URL接发送原始的请求体
        proxy_set_header Content-Length ""; # 不向鉴权URL发送原始的请求体
        proxy_set_header X-Original-URI $request_uri; # 传递原始的请求URL
    }
}

说明和总结

  1. 此方式能轻松实现静态文件进行灵活的鉴权,甚至是无权限服务添加访问权限。

    • Nginx支持的鉴权配置,有4种:
  2. 使用鉴权URL时,基本目前基于请求头和URL参数的鉴权方式都支持,非常灵活。

    • proxy_pass_request_headers默认为on,会把原始请求Header转发到鉴权URL
    • 生成鉴权URL时,会把原始URL参数传过去。
  3. 鉴权URL的处理,最好跟原始URL无关,并把原始URL作为鉴权参数传过去。本示例代码做了URL字符串替换,对后面扩展不够灵活。
  4. 不建议使用Nginx实现复杂的功能。其配置文件不是代码,不是按顺序执行,也不方便调试。

最近玩了下Cloudflare,其丰富的免费功能,令我印象深刻。虽然其提供了一键开启的免费SSL服务,但是开发测试环境还是需要用到SSL证书。这里记录一下获取SSL证书的配置。

1. 准备

需要先准备好以下三个方面:

  • 域名

    • 可直接购买,一般按年收费。
    • eu.org提供免费域名,但申请域名的审核时长不定。
  • DNS

    • Cloudflare提供免费DNS服务,需要先免费注册账户。
    • Cloudflare设置域名解析。
  • 证书机构。推荐一直在使用“Let's Encrypt”。其具有以下三个优点:

    • 基于DNS服务验证域名所有权。通过在DNS添加指定的TXT记录实现验证。
    • 支持通配符证书。
    • 客户端程序Certbot实现证书申请和续签。

参考:

2. Cloudflare API Token

  • 登录Cloudflare并进入Dashboard:https://dash.cloudflare.com/
  • 点击右上角的用户头像,再点“My profile”。
  • 点左边菜单的“API Tokens”,再点右边的“Create Token”按钮。
  • 找到“Edit zone DNS”的行,点击右边的“Use template”按钮。
  • “Token name”填写Token名称,“Zone Resources”选“Include”、“Specific zone”、指定的域名,再点下面的“Continue to summary”按钮。
  • 最后点“Create Token”,就显示所申请的API Token了。由于只显示一次,要保存好。

3. Certbot

这里以Debian 12为例,安装、配置和使用Certbot。

Certbot官方网站:Certbot

3.1 安装

Certbot官方安装教程是使用snapd,但Debian 12的软件仓库已提供Certbot,以及cloudflare的插件。

sudo apt update
sudo apt install certbot python3-certbot-dns-cloudflare

3.2 配置

编辑文件/etc/letsencrypt/certbot-dns-cloudflare-credentials.ini,填写以下内容。其中“_cloudflare_api_token_”需要替换为第2步申请到的Cloudflare API Token。

# Cloudflare API token, get SSL for foxail.eu.org
dns_cloudflare_api_token=_cloudflare_api_token_

保存后,设置该文件的权限:

sudo chmod 600 /etc/letsencrypt/certbot-dns-cloudflare-credentials.ini

3.3 申请SSL证书

执行以下命令申请SSL证书。其中:

  • example.com需要改为实际的域名。
  • 选项--key-type ecdsa使用ECDSA密钥。ECDSA使用更短的密钥就可实现和RSA相同的安全级别,意味着更高的效率。
sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/certbot-dns-cloudflare-credentials.ini \
  --key-type ecdsa \
  --domain "example.com,*.example.com"

第一次执行会提示域名所有权验证失败,按所给提示,在DNS添加相关的TXT记录即可。即添加DNS记录,类型为“TXT”,名称为“_acme-challenge”,内容为提示所给的。添加成功后,再执行一次刚才的命令。

执行命令后会提示一些问题:

  • 要求输入邮箱,可以跳过。输入邮箱的话,可以接收到证书过期或安全问题的通知。
  • 同意服务条款,输入Y并按回车。
  • 是否订阅电子前沿基金会的邮件,看需要选Y或N。

成功后会提示证书的存储位置。

3.4 更新SSL证书

第一次成功执行Certbot后,会自动创建定时任务/etc/cron.d/certbot,实现自动更新证书,该文件的内容如下:

# /etc/cron.d/certbot: crontab entries for the certbot package
#
# Upstream recommends attempting renewal twice a day
#
# Eventually, this will be an opportunity to validate certificates
# haven't been revoked, etc.  Renewal will only occur if expiration
# is within 30 days.
#
# Important Note!  This cronjob will NOT be executed if you are
# running systemd as your init system.  If you are running systemd,
# the cronjob.timer function takes precedence over this cronjob.  For
# more details, see the systemd.timer manpage, or use systemctl show
# certbot.timer.
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

0 */12 * * * root test -x /usr/bin/certbot -a \! -d /run/systemd/system && perl -e 'sleep int(rand(43200))' && certbot -q renew

3.5 删除域名

无效的域名会导致所有证书更新失败。执行以下Shell脚本可以删除指定域名(以www.example.com为例)的相关配置:

sudo rm -rf /etc/letsencrypt/live/www.example.com/
sudo rm -rf /etc/letsencrypt/archive/www.example.com/
sudo rm /etc/letsencrypt/renewal/www.example.com.conf

注:升级Chrome 114.0.5735.90后,问题已解决。

Lunbuntu 22.04.2,昨晚升级安装系统升级包后,Chrome浏览器渲染页面不正常了。具体是,页面背景色正常,布局正常,也能显示一些布局样式,但完全不显示相关文字,甚至有些图片也不能显示。

尝试Shell里直接运行/opt/google/chrome/chrome,会弹出以下信息:

MESA-INTEL: warning: Haswell Vulkan support is incomplete

应该是显卡与其驱动,不支持Chrome的新特性吧。

于是找到这个文章:Linux: Chrome Starts Very Slowly After Enable Nvidia Driver

根据文章的相关内容,尝试出三个可以正常渲染页面的Chrome浏览器的启动参数(如下)。

启动参数说明效果
--disable-gpu禁用GPU硬件加速。正常
--disable-gpu-driver-bug-workarounds禁用各种GPU驱动程序错误的解决方法。正常
--enable-features=Vulkan开启Vulkan特性可以渲染页面,但有些图片显示为黑块

最后的解决方案:

  1. 选用启动参数--disable-gpu-driver-bug-workarounds
  2. 修改/usr/bin/google-chrome文件,在最后一行exec -a "$0" "$HERE/chrome" "$@"中添加启动参数。即该文件的最后一行修改为:
#exec -a "$0" "$HERE/chrome" "$@"
exec -a "$0" "$HERE/chrome" --disable-gpu-driver-bug-workarounds "$@"

另外,关于Chrome浏览器的各个启动参数,有个文章整理得很详细:List of Chromium Command Line Switches

前几天打开Twitter,发现Chrome(版本80)的地址栏右边出现了十字图标,并提示“Install”。点击安装后,桌面和Chrome Apps都新增了Twitter图标。再双击该图标,就会以窗口形式打开Twitter。窗口像本地应用,但细看,只是没有了地址栏的Chrome窗口。好奇之下,发现这是PWA的最新形态!

Progressive Web Apps,简PWA,就是把网页应用化,或者是利用网页技术开发的应用。这家伙的好处是,对于绝大部分的网站来说,加几个文件就在原来网页的基础上,完成了客户端的开发。第一次看到这个技术提案时,非常激动,简直就是我们网站的救星,不用考虑如何开发iOS和Android的应用了。但其困难的地方是,统一标准和普及的问题。虽然得到越来越多浏览器的支持,但是Safari不太积极。

关于PWA相关的知识和教程,都可以在这里找到:
Progressive Web Apps
https://web.dev/progressive-web-apps/

比较齐全的PWA资源:
awesome-pwa
https://github.com/hemanth/awesome-pwa

收集了一堆PWA的网站:
https://pwa.rocks/

值得推荐的是,对PWA技术很积极的Twitter:
https://twitter.com/

注意的是,要使PWA被浏览器识别为可安装,需要一个正方形的图标。这个在做入门实例时折腾了很久才发现。

后面还想试试结合WASM,看看能否做出更好玩的东西~