前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >图片懒加载固定占位

图片懒加载固定占位

作者头像
Innei
发布2021-12-28 11:21:30
7550
发布2021-12-28 11:21:30
举报
文章被收录于专栏:静之森

如今图片懒加载已是家常便饭,然而一般的图片懒加载的占位往往不跟随原图片的大小,导致图片加载完成后,总体的位置会改变,体验很是不好。相信大家都看过知乎或者 Medium 之类的图片加载方式,从开始加载到完成加载,占位始终在一个地方,再加上平滑的过度,视觉上很舒适。

于是我今天尝试将图片懒加载也做成这样,但是有一个问题,我所有文章中的图都不在本地,而是分布于各个图床,甚至有些还失效了。我开始尝试在前端处理,图片在开始加载的时候是能在加载完成之前获取到图片头部的相关信息的。但是有个问题,这样的话还是做不到平滑过度。

于是,放弃了一个下午的研究成果,开始重做后端。在后端模型中,我加入了一个新的字段,用于记录图片的各种信息。每篇文章可能不止一个图,所以应该是一个数组。

之后,把所有文章中的图片链接提取出来,然后去请求数据,在分析图片并记录到数据库,这样的话,只需要一次操作就行了,之后文章更新额时候在触发一下钩子。

后端

下面代码以 NestJS, Typegoose 为例

提取 markdown 中的图片链接

ts

代码语言:javascript
复制
1export const pickImagesFromMarkdown = (text: string) => {
2  const reg = /(?<=\!\[.*\]\()(.+)(?=\))/g
3  const images = [] as string[]
4  for (const r of text.matchAll(reg)) {
5    images.push(r[0])
6  }
7  return images
8}

COPY

获取图片并分析, 用到了 NestJS 的 http 模块和 image-size

ts

代码语言:javascript
复制
1import { imageSize } from 'image-size'
2import { HttpService } from '@nestjs/common'
3export const getOnlineImageSize = async (http: HttpService, image: string) => {
4  const { data } = await http
5    .get(image, {
6      responseType: 'arraybuffer',
7    })
8    .toPromise()
9  const buffer = Buffer.from(data)
10  const size = imageSize(buffer)
11  return size
12}

COPY

存到数据库

ts

代码语言:javascript
复制
1// base.service.ts
2// class WriteBaseService
3
4async RecordImageDimensions(id: string, socket?: SocketIO.Socket) {
5    const text = (await this.__model.findById(id).lean()).text
6    const images = pickImagesFromMarkdown(text)
7    const result = [] as ISizeCalculationResult[]
8    for await (const image of images) {
9      try {
10        this.logger.log('Get --> ' + image)
11        const size = await getOnlineImageSize(this.__http, image)
12        if (socket) {
13          socket.send(
14            gatewayMessageFormat(
15              EventTypes.IMAGE_FETCH,
16              'Get --> ' + image + JSON.stringify(size),
17            ),
18          )
19        }
20        result.push(size)
21      } catch (e) {
22        this.logger.error(e.message)
23        if (socket) {
24          socket.send(gatewayMessageFormat(EventTypes.IMAGE_FETCH, e.message))
25        }
26        result.push({
27          width: undefined,
28          height: undefined,
29          type: undefined,
30        })
31      }
32    }
33
34    await this.__model.updateOne(
35      { _id: id as any },
36      // @ts-ignore
37      { $set: { images: result } },
38    )
39  }

COPY

因为有些图片可能 404 了,或者网不好(国内直连 https://raw.githubusercontent.com/),所以如果报错也要 push 一下,全为空就行了,前端到时候在处理一下。

ts

代码语言:javascript
复制
1;[this.postService, this.noteService, this.pageService].forEach(
2      async (s) => {
3        s.refreshImageSize(socket)
4      },
5    )
6 }

COPY

最后把所有 model 都执行一下这个方法。

前端

前端部分以 React 为例。

处理完之后,后端返回的数据中多了一个 images 字段,如。

https://cdn.jsdelivr.net/gh/innei/img-bed@master/20200602211735.png
https://cdn.jsdelivr.net/gh/innei/img-bed@master/20200602211735.png

前端在渲染图片之前先要根据实际大小计算出渲染到页面中的尺寸,然后定死 placeholder 的大小,在图片加载完成之后移除或者隐藏 placeholder。

计算尺寸可以参考,如下

ts

代码语言:javascript
复制
1const calculateDimensions = (width?: number, height?: number) => {
2 if (!width || !height) {
3   return { height: 300, width: undefined }
4 }
5 const MAX = {
6   width: document.getElementById('write')?.offsetWidth || 500, // 容器的宽度
7   height: Infinity, // 可选最大高度
8 }
9 const dimensions = { width, height }
10 if (width > height && width > MAX.width) {
11   dimensions.width = MAX.width
12   dimensions.height = (MAX.width / width) * height
13 } else if (height === width) {
14   if (width <= MAX.width) {
15     dimensions.height = dimensions.width = height
16   } else {
17     dimensions.height = MAX.width
18     dimensions.width = dimensions.height
19   }
20 }
21 return dimensions
22}

COPY

因为 Markdown 渲染的结构比较复杂,我所以我使用了 Context 进行传值,我使用的渲染库是 react-markdown,可以对每个 tag 进行自定义渲染。

tsx

代码语言:javascript
复制
1const RenderImage: FC<{ src: string; alt?: string }> = ({ src, alt }) => {
2  const images = useContext(imageSizesContext)
3  const [cal, setCal] = useState({} as { height?: number; width?: number })
4  useEffect(() => {
5    const size = images.shift()
6    const cal = calculateDimensions(size?.width, size?.height)
7
8    setCal(cal)
9  }, [images])
10  if (typeof document === 'undefined') {
11    return null
12  }
13
14  return (
15    <ImageLazyWithPopup
16      src={src}
17      alt={alt}
18      height={cal.height}
19      width={cal.width}
20    />
21  )
22}

COPY

完整的 Image 组件可到

mx-web

查看。包含了图片的过度动画。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020-05-31,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 后端
  • 前端
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档