头图

在前端开发中,实现图片加载是一个常见且不可忽视的需求,图片作为一种高频使用的资源,正确且高效的加载图片能显著提高用户体验。

本文,我们将谈谈图片加载的实现方式以及各自实现的优缺点,然后我们基于new Image方式来谈谈单图片加载与多图片加载的实现以及优化。

图片加载的实现方式

在前端开发中,图片加载不仅仅是一个简单的 img 标签操作,尤其是在复杂的应用中,图片的加载往往涉及到异步操作、缓存策略、错误处理以及资源优化等多个方面,下面,我们将扩展讨论JavaScript中实现图片加载的几种常见方法,并探讨它们的应用场景和优缺点。

实现图片加载主要有如下几种方法:

  1. 使用 new Image() 创建图片对象。
  2. 使用 <img> 标签加载图片。
  3. 使用 fetch 和 Blob 对象加载图片。
  4. 使用 IntersectionObserver API和 setTimeout 加载图片(懒加载)。
  5. 使用 canvas 处理图片加载。

下面我们将谈谈每一种图片加载实现方法的优缺点和具体实现。

1. 使用 new Image() 创建图片对象

new Image() 是 JavaScript 中用于动态创建图片对象的标准方式,它相对简单,且能直接控制图片的加载过程。通过 onloadonerror 事件,我们可以实现图片加载的回调处理。它的优缺点分别如下:

优点

  • 代码实现简单直观,而且代码量也很少。
  • 可以直接控制图片加载的生命周期和处理加载失败等情况。

缺点:

  • 不能直接在HTML中使用。
  • 需要额外的JavaScript代码逻辑来显示图片(例如将图片插入到DOM中)。

我们来看一个具体的示例实现:

const loadImage = (src: string) =>
  new Promise<HTMLImageElement>((resolve, reject) => {
    const img = new Image();
    img.onload = () => resolve(img);
    img.onerror = () => reject(new Error(`Failed to load image at ${src}`));
    img.src = src;
});

使用方式如下:

loadImage('image.jpg')
  .then((img) => document.body.appendChild(img))
  .catch((err) => console.error(err));

2. 使用 <img> 标签加载图片

直接通过 HTML 中的 <img> 标签来加载图片是最传统和最常见的方式。通过 JavaScript,可以动态创建 <img> 元素,设置 src 属性,或者在页面中修改现有的 img 标签的 src 属性来加载新的图片。

优点:

  • 简单易用,适合直接在DOM中显示图片。
  • 浏览器会自动处理缓存、加载等细节。

缺点:

  • 没有内建的加载失败回调机制,需要通过事件处理来处理。
  • 控制不如 new Image() 灵活,无法在图像加载前进行复杂操作。

我们来看一个具体的示例实现:

const loadImage = (src: string) => {
  const img = document.createElement("img");
  img.onload = () => console.log("Image loaded");
  img.onerror = () => console.error("Failed to load image");
  img.src = src;
  document.body.appendChild(img);
  return img;
};

使用方式如下:

loadImage("image.jpg");

3. 使用 fetch 和 Blob 对象加载图片

使用 fetch API 加载图片资源是一种现代的方式,可以在后台获取图片内容,然后将其转换为 Blob 对象,再通过 URL.createObjectURL() 创建图片的临时URL,最终将其显示在页面上。这样可以更加灵活地处理图片的获取和显示,比如控制请求头、缓存策略,或者使用自定义的加载进度。

优点:

  • 通过 fetch 可灵活处理请求,例如添加自定义的请求头、控制缓存等。
  • 能通过 Blob 和 ObjectURL 实现离线加载、缓存和预加载等高级功能。
  • 可以和其他资源(如文件上传)结合处理。

缺点:

  • 需要更多的代码,涉及到异步请求的处理。
  • 浏览器支持问题,特别是在较老的浏览器中不完全支持。

我们来看一个具体的示例实现:

const loadImage = (src: string) =>
  fetch(src)
    .then((response) => response.blob())
    .then((blob) => {
      const imgURL = URL.createObjectURL(blob);
      const img = document.createElement("img");
      img.src = imgURL;
      document.body.appendChild(img);
    })
    .catch((err) => console.error("Failed to load image:", err));

使用方式如下:

loadImage("image.jpg");

4. 使用 IntersectionObserver API 和 setTimeout 加载图片(懒加载)

图片懒加载是一种优化用户体验和性能的技术,通过延迟加载页面外的图片,避免一次性加载所有图片,减少页面的初次加载时间。JavaScript 中的 setTimeoutIntersectionObserver 可以配合图片加载技术实现懒加载。

优点:

  • 提高页面性能,减少无关图片的请求。
  • 提升页面的加载速度,尤其是图片数量较多时。

缺点:

  • 实现复杂度相对较高。
  • 需要保证浏览器对懒加载技术的支持(如 IntersectionObserver)。

我们来看一个具体的示例实现:


const loadImage = (img: HTMLImageElement) => {
  const observer = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        const imgElement = entry.target as HTMLImageElement;
        imgElement.src = imgElement.dataset.src!;
        observer.unobserve(imgElement);
      }
    });
  });

  observer.observe(img);
}

使用方式如下:

<div class="container">
    <img data-src="image1.jpg" class="lazy-img" />
    <img data-src="image2.jpg" class="lazy-img" />
    <img data-src="image3.jpg" class="lazy-img" />
    <img data-src="image4.jpg" class="lazy-img" />
    <img data-src="image5.jpg" class="lazy-img" />
</div>
document
  .querySelectorAll<HTMLImageElement>(".container img.lazy-img")
  .forEach((img) => {
    loadImage(img);
  });

5. 使用 canvas 处理图片加载

<canvas> 元素不仅仅可以用来渲染图形,还可以用来处理图片。在 JavaScript 中加载图片到 canvas 上进行绘制,不仅能够显示图片,还可以进行进一步的图片处理,如裁剪、滤镜、图像合成等。

优点:

  • 强大的图片处理能力,能够执行诸如裁剪、缩放、滤镜等操作。
  • 可以通过 Canvas 将图片绘制成动态效果。

缺点:

  • 对性能要求较高,尤其是在进行复杂的图片处理时。
  • 需要掌握更多的 Canvas API 和图片处理技巧。

我们来看一个具体的示例实现:

const loadImage = (img: HTMLImageElement) => {
  img.onload = () => {
    const canvas = document.createElement("canvas");
    const ctx = canvas.getContext("2d")!;
    canvas.width = img.width;
    canvas.height = img.height;
    ctx.drawImage(img, 0, 0);
    document.body.appendChild(canvas);
  };
};

使用方式如下:

<div class="container">
    <img data-src="image1.jpg" class="img" />
</div>
const img = document.querySelector<HTMLImageElement>(".container img.img");
loadImage(img);

总结

JavaScript 实现图片加载的方式有很多种,每种方法都有其适用场景和优缺点。从最简单的 new Image() 到较为复杂的 fetchBlob,以及现代的懒加载和 canvas 操作,开发者可以根据项目需求选择合适的实现方式。

  • new Image() :适用于需要动态加载并控制图片加载生命周期的场景。
  • <img> 标签:适用于传统的静态加载,浏览器会自动处理很多细节。
  • fetch 与 Blob:适用于需要灵活控制请求、缓存策略或结合其他资源加载的场景。
  • 懒加载:提升页面性能,特别适用于图片数量较多、需要优化加载顺序的情况。
  • Canvas:适用于需要对图片进行更复杂操作(如裁剪、滤镜、合成等)的场景。

每种方法都可以在具体的应用中根据需求进行优化和调整,而结合现代前端框架的最佳实践,如异步加载、错误处理、并发控制等,能更好地提升用户体验与性能。

单图片加载与多图片加载的实现与优化

单图片加载的实现与注意事项

单张图片的加载通常是最简单的用例,我们通过 new Image() 来创建一个新的图片对象,并通过 src 属性指定图片的地址进行加载,然而,在开发中,为了避免加载失败、缓存控制及一些个性化需求,我们需要对单图片加载进行进一步的封装和优化。

Promise包装

图片加载过程是异步的,因此使用 Promise 来包装图片加载过程,可以让其更符合现代异步编程的风格,通过这种包装,开发者可以方便地处理图片加载成功与失败的回调,或者实现链式调用。

const loadImage = (src: string) =>
  new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = () => resolve(img);
    img.onerror = () => reject(new Error(`Failed to load image at ${src}`));
    img.src = src;
  });

兜底操作与 src 解析

为了确保图片正确加载,我们需要对 src 进行解析,保证图片地址有效。假设用户传入的地址可能为相对路径,我们可以在加载前将其解析为绝对路径。

const parseImageSrc = (src: string) => {
  const baseURL = window.location.origin;
  // 如果是相对路径,则转换成绝对路径
  if (!/^http(s)?:\/\//.test(src)) {
    return baseURL + src;
  }
  return src;
};

const loadImage = (src: string) =>
  new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = () => resolve(img);
    img.onerror = () => reject(new Error(`Failed to load image at ${src}`));
    img.src = parseImageSrc(src);
  });

图片缓存

为了提高性能,我们可以利用浏览器的缓存机制。如果图片已经加载过一次,再次加载时可以直接使用缓存,避免重新下载。现代浏览器会自动缓存已经加载过的图片,但开发者可以通过合理设置 cache-controlETag 头部来控制缓存策略。

透传相关属性与定制化

很多情况下,开发者需要定制化图片加载的行为,例如透传 alttitle 或设置 loading="lazy" 等属性。这些都可以在封装时透传到图片元素上。

// React中可以直接使用 type HTMLImageElementAttributes = React.HtmlHTMLAttributes<React.ElementRef<'img'>>;
interface HTMLImageElementAttributes {
  alt?: string;
  src?: string;
  width?: number;
  height?: number;
  crossOrigin?: "anonymous" | "use-credentials" | "";
  loading?: "eager" | "lazy";
  decoding?: "sync" | "async" | "auto";
  srcset?: string;
  sizes?: string;
  referrerPolicy?: ReferrerPolicy;
  [k: string]: unknown;
}

const loadImage = (src: string, attributes: HTMLImageElementAttributes = {}) =>
  new Promise<HTMLImageElement>((resolve, reject) => {
    const img = new Image();
    // 透传属性
    Object.keys(attributes).forEach((key) => {
      img.setAttribute(key, `${attributes[key]}`);
    });

    img.onload = () => resolve(img);
    img.onerror = () => reject(new Error(`Failed to load image at ${src}`));
    img.src = parseImageSrc(src);
  });

多图片加载的实现与优化

在实际开发中,常常需要一次性加载多个图片,例如加载一组图片或者批量获取图片资源。这时,我们就需要解决并发加载、错误处理和展示优先级等问题。

1. 兜底操作与错误信息反馈

多图片加载时,某些图片可能由于路径错误或网络问题加载失败。在此情况下,我们需要提供错误信息,避免整个加载过程因为单张图片的失败而导致整体失败。

const loadImages = (srcList: string[]) => {
  const promises = srcList.map((src) =>
    loadImage(src).catch((err) => {
      console.error(`Image failed to load: ${src}`, err);
      return null; // 继续加载其他图片
    })
  );

  return Promise.all(promises);
};

2. 图片加载优先级与展示

为了提供更好的用户体验,我们可以通过优先展示已加载完成的图片,避免所有图片都加载完毕才显示。可以通过设置 then 回调动态添加图片到页面中。

const loadImages = (srcList: string[]) => {
  const promises = srcList.map((src) =>
    loadImage(src)
      .then((img) => {
        document.body.appendChild(img); // 动态加载完成的图片
      })
      .catch((err) => {
        console.error(`Image failed to load: ${src}`, err);
        return null; // 继续加载其他图片
      })
  );

  return Promise.all(promises);
};

3. 并发任务控制

加载大量图片时,直接并发加载可能会导致浏览器请求过多资源,影响性能。此时可以通过并发控制来限制同时加载的图片数量,避免过载。

const loadImagesWithLimit = (srcList: string[], limit = 3) => {
  let index = 0;
  const results: HTMLImageElement[] = [];

  const loadNextBatch = () => {
    if (index >= srcList.length) return Promise.resolve(results);

    const batch: Promise<number>[] = [];
    for (let i = 0; i < limit && index < srcList.length; i++) {
      const src = srcList[index++];
      const res = loadImage(src).then((img) => results.push(img));
      batch.push(res);
    }

    return Promise.all(batch).then(loadNextBatch);
  }

  return loadNextBatch();
};

在复杂的前端架构项目中如何保证图片功能的稳定

在一个庞大的前端架构项目中,图片加载可能是一个看似不起眼但又至关重要的功能。如果更改了图片加载的实现方式,如何确保整体项目架构不崩溃呢?这就涉及到架构的可维护性、可扩展性以及接口设计。

1. 解耦与模块化

为了避免一个小改动导致整个系统崩溃,图片加载相关的功能应该尽可能做到解耦。将图片加载封装成一个独立的模块,提供统一的接口,其他模块只需要调用这个接口,无需关心具体实现。这能确保修改或优化图片加载的实现时,其他模块不会受到影响。

2. 统一接口与版本控制

如果需要更改图片加载的方式(比如改用懒加载或添加缓存策略),我们可以提供不同版本的接口。例如,可以通过不同的配置项或者环境变量来控制是否启用新的图片加载方式。

const loadImageWithVersion = (src, options = { lazy: false }) => {
  if (options.lazy) {
    // 实现懒加载
  } else {
    return loadImage(src);
  }
}

3. 单元测试与回归测试

在进行图片加载功能的改动时,确保修改后的功能能够通过单元测试和回归测试。特别是在并发控制、错误处理和图片优先展示等方面,需要确保新实现不会破坏原有的功能。

describe('Image Loading', () => {
  it('should load image successfully', async () => {
    const img = await loadImage('valid-image.jpg');
    expect(img).not.toBeNull();
  });

  it('should handle image load failure', async () => {
    await expect(loadImage('invalid-image.jpg')).rejects.toThrow();
  });
});

4. 监控与日志

在大规模应用中,图片加载功能也需要通过日志进行监控。如果某张图片加载失败或加载异常,应该能够及时发现并反馈给开发人员,避免影响整个系统的稳定性。

总结

无论是单图片加载还是多图片加载,都涉及到异步操作、缓存控制和错误处理等复杂的逻辑。通过 Promise 包装图片加载过程,开发者能够更方便地管理图片加载的成功与失败,提升开发效率。在实现过程中,我们需要注意图片加载的兜底操作、并发控制、优先展示等问题。对于大型架构体系中图片加载功能的修改,应当通过解耦、版本控制、单元测试等方式,确保新功能的引入不会导致系统的不稳定。

总结

通过本文,我们了解了实现图片加载的几种方式,每一种实现方式我们都讲解了优点与缺点,并给出了具体的示例,然后我们了解了单图片加载以及多图片加载的实现和优化,涉及到了异步操作,错误处理,缓存控制等等,我们选择使用Promise 包装图片加载过程,使得开发者能够更方便地管理图片加载的成功与失败,提升开发效率。在实现过程当中,我们需要注意图片加载的兜底操作、并发控制、优先展示等问题。对于大型架构体系中图片加载功能的修改,应当通过解耦、版本控制、单元测试等方式,确保新功能的引入不会导致系统的不稳定。

以上就是本文所有内容,感谢阅读,如有帮助,望不吝啬点赞收藏。


夕水
5.3k 声望5.8k 粉丝

问之以是非而观其志,穷之以辞辩而观其变,资之以计谋而观其识,告知以祸难而观其勇,醉之以酒而观其性,临之以利而观其廉,期之以事而观其信。