实现前端资源加载器 (AssetLoader)

问题描述

请实现一个 AssetLoader 类,用于异步向页面中加载 JavaScript 和 CSS 资源。加载器需要满足以下要求:

  1. 并发加载:所有资源应同时开始加载,以提高效率。
  2. 类型识别:能自动根据文件扩展名 (.js.css) 创建正确的 HTML 标签(<script><link>)。
  3. 状态通知
    • 单个资源加载失败时,需要在控制台打印出具体的失败提示。
    • 所有 JavaScript 资源都加载完成后(无论成功或失败),需要在控制台打印一条“All JS resources have finished loading.”的提示。
    • 所有 CSS 资源都加载完成后(无论成功或失败),需要在控制台打印一条“All CSS resources have finished loading.”的提示。
  4. 完成定义:对于分组提示而言,单个资源的加载失败也视为“完成”。

使用示例

const loader = new AssetLoader();

loader.load([
  // 成功的 JS
  '[https://cdn.bootcdn.net/ajax/libs/react/17.0.2/umd/react.production.min.js](https://cdn.bootcdn.net/ajax/libs/react/17.0.2/umd/react.production.min.js)',
  // 成功的 CSS
  '[https://cdn.bootcdn.net/ajax/libs/antd/4.16.13/antd.min.css](https://cdn.bootcdn.net/ajax/libs/antd/4.16.13/antd.min.css)',
  // 故意写错的 JS,用于测试失败场景
  'https://invalid-host/path/to/nonexistent-script.js',
  // 成功的 JS
  '[https://cdn.bootcdn.net/ajax/libs/moment.js/2.29.1/moment.min.js](https://cdn.bootcdn.net/ajax/libs/moment.js/2.29.1/moment.min.js)',
  // 故意写错的 CSS
  'https://another-invalid-host/styles/nonexistent-style.css',
]);

// 预期的控制台输出顺序可能不同,但内容应包含:
// Error: Failed to load: https://invalid-host/path/to/nonexistent-script.js
// Error: Failed to load: https://another-invalid-host/styles/nonexistent-style.css
// All JS resources have finished loading.
// All CSS resources have finished loading.

解题思路

要构建这个加载器,核心在于将 DOM 事件(onload, onerror)与 Promise 结合起来,并利用 Promise.allSettled 来处理并发任务的完成状态。

  1. Class 结构: 创建一个 AssetLoader 类,并定义一个 load 方法来接收 URL 数组。

  2. 资源分类: 在 load 方法内部,首先遍历输入的 URL 数组,根据文件扩展名将它们分类成 jsUrlscssUrls 两个独立的数组。

  3. 创建单个加载任务:

    • 定义一个私有辅助方法,例如 _loadAsset(url),它负责加载单个资源。
    • 这个方法的核心是返回一个 new Promise()
    • 在 Promise 内部,根据 URL 是 .js 还是 .css 来创建对应的 DOM 元素(<script><link rel="stylesheet">)。
    • 为创建的元素绑定 onloadonerror 事件监听器。
      • onload 时,调用 Promiseresolve() 方法,表示加载成功。
      • onerror 时,调用 Promisereject() 方法,表示加载失败。
    • 最后,将元素添加到文档的 <head> 中以触发浏览器的加载行为。
  4. 并发处理与完成回调:

    • 使用 map 方法遍历 jsUrlscssUrls 数组,为每个 URL 调用 _loadAsset 方法,从而得到两个 Promise 数组:jsPromisescssPromises
    • Promise.allSettled() 是本题的关键。与 Promise.all(一旦有失败就立即拒绝)不同,Promise.allSettled 会等待所有传入的 Promise 都完成(无论是 resolved 还是 rejected),然后返回一个包含所有结果状态的对象数组。这完美符合“失败也算完成”的要求。
    • 分别对 jsPromisescssPromises 调用 Promise.allSettled()
    • .then() 回调中,遍历 allSettled 返回的结果数组。如果某个结果的 status'rejected',就打印其失败原因。
    • 在遍历完所有结果后,打印对应资源组(JS 或 CSS)的“全部加载完成”的消息。

通过这种方式,我们可以优雅地处理并发、异步和错误,实现一个功能完善的资源加载器。

代码实现

class AssetLoader {
  /**
   * 加载单个资源,并返回一个 Promise
   * @private
   * @param {string} url - 资源的 URL
   * @returns {Promise<string>} - 成功时 resolve,失败时 reject
   */
  _loadAsset(url) {
    return new Promise((resolve, reject) => {
      let element;

      // 根据文件扩展名创建 script 或 link 标签
      if (url.endsWith('.js')) {
        element = document.createElement('script');
        element.src = url;
      } else if (url.endsWith('.css')) {
        element = document.createElement('link');
        element.href = url;
        element.rel = 'stylesheet';
        element.type = 'text/css';
      } else {
        // 如果文件类型不支持,则直接拒绝
        const errMsg = `Unsupported asset type: ${url}`;
        return reject(new Error(errMsg));
      }

      // 监听加载成功事件
      element.onload = () => resolve(`Successfully loaded: ${url}`);
      // 监听加载失败事件
      element.onerror = () => reject(new Error(`Failed to load: ${url}`));

      // 将元素添加到 head 中以触发加载
      document.head.appendChild(element);
    });
  }

  /**
   * 加载一个或多个 JS/CSS 资源
   * @param {string[]} urls - 包含资源 URL 的数组
   */
  load(urls) {
    if (!urls || !Array.isArray(urls) || urls.length === 0) {
      console.log('No assets to load.');
      return;
    }

    // 1. 将 URL 按类型分类
    const jsUrls = urls.filter(
      (url) => typeof url === 'string' && url.endsWith('.js')
    );
    const cssUrls = urls.filter(
      (url) => typeof url === 'string' && url.endsWith('.css')
    );

    // 2. 为每种类型的资源创建加载 Promise
    const jsPromises = jsUrls.map((url) => this._loadAsset(url));
    const cssPromises = cssUrls.map((url) => this._loadAsset(url));

    // 3. 使用 Promise.allSettled 处理 JS 资源组
    if (jsPromises.length > 0) {
      Promise.allSettled(jsPromises).then((results) => {
        results.forEach((result) => {
          // 当单个资源加载失败时给出提示
          if (result.status === 'rejected') {
            console.error(result.reason.message);
          }
        });
        // 所有 JS 资源加载完成后给出提示
        console.log('All JS resources have finished loading.');
      });
    }

    // 4. 使用 Promise.allSettled 处理 CSS 资源组
    if (cssPromises.length > 0) {
      Promise.allSettled(cssPromises).then((results) => {
        results.forEach((result) => {
          // 当单个资源加载失败时给出提示
          if (result.status === 'rejected') {
            console.error(result.reason.message);
          }
        });
        // 所有 CSS 资源加载完成后给出提示
        console.log('All CSS resources have finished loading.');
      });
    }
  }
}

// --- 使用示例 ---
console.log('Initializing asset loader...');
const loader = new AssetLoader();

loader.load([
  '[https://cdn.bootcdn.net/ajax/libs/react/17.0.2/umd/react.production.min.js](https://cdn.bootcdn.net/ajax/libs/react/17.0.2/umd/react.production.min.js)',
  '[https://cdn.bootcdn.net/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js](https://cdn.bootcdn.net/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js)',
  '[https://cdn.bootcdn.net/ajax/libs/antd/4.16.13/antd.min.css](https://cdn.bootcdn.net/ajax/libs/antd/4.16.13/antd.min.css)',
  // 故意写错的 URL 来测试失败场景
  'https://invalid-host/path/to/nonexistent-script.js',
  'https://another-invalid-host/styles/nonexistent-style.css',
]);
Copyright © Jun 2025 all right reserved,powered by Gitbook该文件修订时间: 2025-07-03 17:35:08

results matching ""

    No results matching ""

    results matching ""

      No results matching ""