【前端从0到1实战】第9篇:异步加载(Async)与骨架屏(Skeleton)

【前端从0到1实战】第9篇:异步加载(Async)与骨架屏(Skeleton)

在前面四篇中,我们掌握了“静态”组件的构建,即所有内容都在 HTML 中预先定义好。今天,我们将迈出从“网页”到“Web 应用”最关键的一步:异步数据加载。

在真实世界中,数据几乎都来自服务器。当用户点击按钮时,我们需要:

向服务器请求数据 (使用 fetch API)。

在等待数据返回时,给用户一个即时反馈(加载中...)。

数据返回后,将其动态渲染到页面上。

为了提供最佳用户体验 (UX),我们将摒弃简单的“旋转菊花图”,转而实现一个现代 Web 应用的标配——“骨架屏 (Skeleton)”加载动画。

第一部分:HTML 结构搭建 (状态容器)

与之前的组件不同,这一次的 HTML 扮演的是一个“状态机”的角色。它需要预先定义好所有可能的“状态”:

Idle (空闲):初始状态,提示用户操作。

Loading (加载中):骨架屏状态,默认隐藏。

Error (错误):请求失败状态,默认隐藏。

Success (成功):数据将被 JS 动态插入到这里。

<!-- 1. 控制区 -->
<div class="async-header">
    <h2>用户列表</h2>
    <p>点击下方按钮从服务器加载用户数据。</p>
    <button id="btn-fetch-data" class="btn btn-primary">
        加载数据
    </button>
</div>

<!-- 
  2. 内容区 (状态容器)
  id="user-list-container" 是我们将要操作的主区域
-->
<div id="user-list-container" class="user-list">

    <!-- 
      状态 1: Idle (空闲)
      id="idle-message" (默认显示)
    -->
    <div id="idle-message" class="state-message">
        <p>尚未加载数据。请点击按钮。</p>
    </div>

    <!-- 
      状态 2: Loading (加载中) 
      id="loading-skeleton" (默认隐藏)
      这里是“骨架屏”的核心结构。
    -->
    <div id="loading-skeleton" class="skeleton-wrapper hidden">
        <!-- 我们可以复制多个骨架卡片以模拟列表 -->
        <!-- 骨架卡片 1 -->
        <div class="skeleton-card">
            <div class="skeleton skeleton-avatar"></div>
            <div class="skeleton-text-group">
                <div class="skeleton skeleton-line-title"></div>
                <div class="skeleton skeleton-line-text"></div>
            </div>
        </div>
        <!-- 骨架卡片 2 -->
        <div class="skeleton-card">
            <div class="skeleton skeleton-avatar"></div>
            <div class="skeleton-text-group">
                <div class="skeleton skeleton-line-title"></div>
                <div class="skeleton skeleton-line-text"></div>
            </div>
        </div>
    </div>

    <!-- 
      状态 3: Error (错误) 
      id="error-message" (默认隐藏)
    -->
    <div id="error-message" class="state-message error hidden">
        <p>数据加载失败!请检查网络并重试。</p>
    </div>
    
    <!-- 
      状态 4: Success (成功)
      (这个状态没有 HTML 占位符,因为 JS 会动态创建 .user-card
       并将其插入到 #user-list-container 中)
    -->
    
</div>

第二部分:CSS 样式 (骨架屏动画)

CSS 的核心是实现“骨架屏”的微光闪烁 (Shimmer)动画。

我们定义一个 .skeleton 基础类,设置它的背景色为浅灰色。

我们使用 @keyframes 定义一个“微光”动画,它本质上是一个从左到右移动的“亮色渐变”。

我们将这个动画应用到 .skeleton 元素上。

/* --- 基础容器样式 --- /
.async-container {
width: 600px;
max-width: 100%;
margin: 40px auto;
border: 1px solid #dfe4ea;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
background-color: #fff;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
overflow: hidden; /
保证内部元素不溢出圆角 */
}

.async-header {
padding: 25px;
text-align: center;
background-color: #f8f9fa;
border-bottom: 1px solid #dfe4ea;
}
.async-header h2 { margin: 0 0 10px; }
.btn-primary {
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
}
.btn-primary:disabled {
background-color: #e9ecef;
cursor: not-allowed;
}

.user-list {
padding: 25px;
min-height: 200px;
}

.state-message {
text-align: center;
padding: 40px 0;
color: #57606f;
}
.state-message.error {
color: #dc3545;
font-weight: 500;
}

/* 工具类,用于 JS 切换状态 */
.hidden {
display: none !important;
}

/* --- 核心:骨架屏 (Skeleton) 样式 --- */

/* 1. 定义微光动画 /
@keyframes shimmer {
0% {
/
动画开始前,微光在左侧 /
background-position: -468px 0;
}
100% {
/
动画结束后,微光在右侧 */
background-position: 468px 0;
}
}

/* 2. 定义基础骨架元素 */
.skeleton {
animation-duration: 1.5s;
animation-fill-mode: forwards;
animation-iteration-count: infinite;
animation-name: shimmer;
animation-timing-function: linear;

/* 核心:创建“微光”渐变
   从浅灰 -> 亮白 -> 浅灰
*/
background: linear-gradient(
    to right,
    #f6f7f8 8%,  /* 基础色 */
    #e9ecef 38%, /* 亮色 */
    #f6f7f8 54%  /* 基础色 */
);
background-size: 1000px 64px;
border-radius: 4px;

}

/* 3. 布局骨架卡片 /
.skeleton-wrapper {
/
(这个 .hidden 类会被 JS 移除) /
}
.skeleton-card {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.skeleton-avatar {
width: 50px;
height: 50px;
border-radius: 50%; /
圆形头像 /
flex-shrink: 0; /
防止头像被压缩 */
}
.skeleton-text-group {
flex-grow: 1;
margin-left: 15px;
}
.skeleton-line-title {
width: 40%;
height: 18px;
margin-bottom: 8px;
}
.skeleton-line-text {
width: 80%;
height: 14px;
}

/* --- 状态 4:成功加载后的卡片样式 (用于对比) --- /
.user-card {
display: flex;
align-items: center;
padding: 15px;
margin-bottom: 10px;
border: 1px solid #dfe4ea;
border-radius: 8px;
animation: fadeIn 0.3s ease-out;
}
.user-avatar {
width: 50px;
height: 50px;
border-radius: 50%;
margin-right: 15px;
background-color: #dfe4ea; /
头像占位符 */
}
.user-info h4 {
margin: 0 0 5px;
color: #2f3542;
}
.user-info p {
margin: 0;
color: #57606f;
}

/* 淡入动画 */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}

第三部分:JS 交互逻辑 (async/await)

这是本系列的“毕业大戏”。我们将使用现代 JavaScript 的 async/await 语法来处理 fetch 请求,这使得异步代码像同步代码一样易于阅读。

document.addEventListener('DOMContentLoaded', () => {

// --- 1. DOM 元素获取 (状态容器) ---
const fetchDataBtn = document.getElementById('btn-fetch-data');

const userListContainer = document.getElementById('user-list-container');
const idleMessage = document.getElementById('idle-message');
const loadingSkeleton = document.getElementById('loading-skeleton');
const errorMessage = document.getElementById('error-message');

// 我们将使用一个“假”的 API (JSONPlaceholder) 来模拟
const API_URL = '[https://jsonplaceholder.typicode.com/users?_limit=3](https://jsonplaceholder.typicode.com/users?_limit=3)'; // 只取3个用户

// --- 2. 状态切换函数 ---

// 切换到“加载中”状态
const showLoadingState = () => {
    idleMessage.classList.add('hidden');
    errorMessage.classList.add('hidden');
    loadingSkeleton.classList.remove('hidden');
    fetchDataBtn.disabled = true;
    fetchDataBtn.textContent = '加载中...';
};

// 切换到“错误”状态
const showErrorState = () => {
    loadingSkeleton.classList.add('hidden');
    idleMessage.classList.add('hidden');
    errorMessage.classList.remove('hidden');
    fetchDataBtn.disabled = false;
    fetchDataBtn.textContent = '重试';
};

// 切换到“空闲”状态 (用于清空)
const showIdleState = () => {
    errorMessage.classList.add('hidden');
    loadingSkeleton.classList.add('hidden');
    idleMessage.classList.remove('hidden');
    userListContainer.innerHTML = ''; // 清空动态内容
    userListContainer.appendChild(idleMessage); // 把空闲消息放回去
};

// --- 3. 核心:数据渲染函数 ---

// (A) 渲染成功
const renderSuccess = (users) => {
    // 隐藏所有状态指示器
    loadingSkeleton.classList.add('hidden');
    errorMessage.classList.add('hidden');
    idleMessage.classList.add('hidden');
    
    // 清空容器,准备填充新数据
    userListContainer.innerHTML = ''; 

    // 遍历 API 返回的 users 数组
    users.forEach(user => {
        // 动态创建 .user-card
        const card = document.createElement('div');
        card.className = 'user-card';

        // (为简单起见,我们使用 innerHTML 来构建卡片内部)
        // (在真实项目中,请注意防范 XSS 攻击)
        card.innerHTML = `
            <img src="[https://i.pravatar.cc/50?u=$](https://i.pravatar.cc/50?u=$){user.id}" alt="${user.name}" class="user-avatar">
            <div class="user-info">
                <h4>${user.name}</h4>
                <p>${user.email}</p>
            </div>
        `;
        
        // 将新创建的卡片添加到容器中
        userListContainer.appendChild(card);
    });
    
    fetchDataBtn.disabled = false;
    fetchDataBtn.textContent = '重新加载';
};


// --- 4. 事件绑定与 async/await ---
fetchDataBtn.addEventListener('click', async () => {
    // (A) 进入加载状态
    showLoadingState();

    // (B) 尝试执行异步操作
    try {
        // 模拟2秒的网络延迟,让我们能看清骨架屏
        await new Promise(resolve => setTimeout(resolve, 1500)); 

        // (1) 发送网络请求,并“等待”响应
        const response = await fetch(API_URL);

        // (2) 检查响应是否成功
        if (!response.ok) {
            // 如果 HTTP 状态码不是 2xx,则抛出一个错误
            throw new Error(`HTTP 错误! 状态: ${response.status}`);
        }

        // (3) 将响应体解析为 JSON,并“等待”
        const users = await response.json();

        // (4) 成功!调用渲染函数
        renderSuccess(users);

    } catch (error) {
        // (C) 如果 try 块中的任何“await”失败,就会进入 catch 块
        console.error('数据获取失败:', error);
        showErrorState();
    }
});

});

总结

恭喜您完成了本系列的全部课程!我们从一个简单的 Tab 选项卡开始,最终实现了一个使用 async/await 和“骨架屏”的动态数据组件。

我们学到了:

HTML: 如何将 HTML 作为一个“状态机”容器,预先定义 Idle、Loading 和 Error 状态。

CSS:

掌握了使用 @keyframes 和 linear-gradient 来创建专业级的“骨架屏微光”动画。

如何使用 .hidden 工具类来配合 JS 管理 UI 状态。

JS (核心):

如何使用 fetch 和 async/await 语法来处理异步 API 请求。

如何使用 try...catch 块来优雅地处理网络错误。

如何动态创建 DOM (document.createElement) 并将其渲染到页面上,这是 Web 应用的灵魂。

至此,您已经掌握了从0到1构建一个完整、专业、动态的前端组件所需的全部核心技能。

posted @ 2025-11-17 15:52  GreenBoos2025  阅读(4)  评论(0)    收藏  举报