实现前端资源加载器 (AssetLoader)
问题描述
请实现一个 AssetLoader
类,用于异步向页面中加载 JavaScript 和 CSS 资源。加载器需要满足以下要求:
- 并发加载:所有资源应同时开始加载,以提高效率。
- 类型识别:能自动根据文件扩展名 (
.js
或.css
) 创建正确的 HTML 标签(<script>
或<link>
)。 - 状态通知:
- 当单个资源加载失败时,需要在控制台打印出具体的失败提示。
- 当所有 JavaScript 资源都加载完成后(无论成功或失败),需要在控制台打印一条“All JS resources have finished loading.”的提示。
- 当所有 CSS 资源都加载完成后(无论成功或失败),需要在控制台打印一条“All CSS resources have finished loading.”的提示。
- 完成定义:对于分组提示而言,单个资源的加载失败也视为“完成”。
使用示例
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
来处理并发任务的完成状态。
Class 结构: 创建一个
AssetLoader
类,并定义一个load
方法来接收 URL 数组。资源分类: 在
load
方法内部,首先遍历输入的 URL 数组,根据文件扩展名将它们分类成jsUrls
和cssUrls
两个独立的数组。创建单个加载任务:
- 定义一个私有辅助方法,例如
_loadAsset(url)
,它负责加载单个资源。 - 这个方法的核心是返回一个
new Promise()
。 - 在 Promise 内部,根据 URL 是
.js
还是.css
来创建对应的 DOM 元素(<script>
或<link rel="stylesheet">
)。 - 为创建的元素绑定
onload
和onerror
事件监听器。onload
时,调用Promise
的resolve()
方法,表示加载成功。onerror
时,调用Promise
的reject()
方法,表示加载失败。
- 最后,将元素添加到文档的
<head>
中以触发浏览器的加载行为。
- 定义一个私有辅助方法,例如
并发处理与完成回调:
- 使用
map
方法遍历jsUrls
和cssUrls
数组,为每个 URL 调用_loadAsset
方法,从而得到两个 Promise 数组:jsPromises
和cssPromises
。 Promise.allSettled()
是本题的关键。与Promise.all
(一旦有失败就立即拒绝)不同,Promise.allSettled
会等待所有传入的 Promise 都完成(无论是resolved
还是rejected
),然后返回一个包含所有结果状态的对象数组。这完美符合“失败也算完成”的要求。- 分别对
jsPromises
和cssPromises
调用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',
]);