Picture loading fixed space occupation

Nowadays, lazy loading of pictures is common. However, the space occupation of general lazy loading of pictures often does not follow the size of the original picture. As a result, the overall position will change after the picture is loaded, and the experience is very bad. I believe everyone has seen the image loading methods such as Zhihu or Medium. From the beginning of loading to the completion of loading, the space occupation is always in one place. Coupled with the smooth transition, it is visually comfortable.

So I tried to do the same with lazy loading of pictures today, but there is a problem. The pictures in all my articles are not local, but distributed in various picture beds, and some even fail. I began to try to process at the front end. When the picture starts loading, I can get the relevant information of the picture header before the loading is completed. However, there is a problem. In this case, there is still no smoothing.

So he gave up the research results of an afternoon and began to redo the back end. In the back-end model, I added a new field to record various information of the picture. Each article may have more than one graph, so it should be an array.

After that, extract the picture links in all articles, then request data, analyze the pictures and record them in the database. In this way, it only needs one operation. Then, when the article updates the amount, trigger the hook.

back-end

The following code takes nestjs and typegoose as examples

Extract picture links in markdown

ts

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

Get the picture and analyze it, using the http module and image size library of NestJS

ts

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

Save to database

ts

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

Because some pictures may be 404, or the network is not good (domestic direct connection) https://raw.githubusercontent.com/ )Therefore, if an error is reported, you should also push it. Just leave it blank. The front end will handle it at that time.

ts

1;[this.postService, this.noteService, this.pageService].forEach(
2      async (s) => {
3        s.refreshImageSize(socket)
4      },
5    )
6 }

COPY

Finally, execute this method on all model s.

front end

The front part takes React as an example.

After processing, an images field is added to the data returned by the backend, such as.

Before rendering the picture, the front end first calculates the size to be rendered to the page according to the actual size, then determines the size of the placeholder, and removes or hides the placeholder after the picture is loaded.

The calculated dimensions can be referred to as follows:

ts

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, // Width of container
7   height: Infinity, // Optional maximum height
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

Because the structure of Markdown rendering is complex, I use Context to transfer values. The rendering library I use is react Markdown, which can render each tag customized.

tsx

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

The complete Image component can be found in

mx-web

see. Contains excessive animation of the picture.

Added by fansa on Tue, 04 Jan 2022 02:47:28 +0200