Skip to content

第5.6章:通过 Blade + Fetch 显示数据

学习时间: 50分钟


1. 混合方法:两全其美

我们可以通过两种方式构建页面:

  1. 完整服务器端渲染 (SSR): Laravel 生成所有 HTML,包括行星列表。对于任何更新(删除、添加),页面都会完全重新加载。
  2. 完整客户端渲染 (CSR): Laravel 返回一个空的 HTML“外壳”,然后 JavaScript 从 API 请求所有数据并在客户端渲染它们。(这是单页应用程序 - SPA 的方法)。

我们的选择是混合方法:

  • 首次加载 (SSR): Laravel 立即返回带有预先准备好的行星列表的页面。这速度快,对 SEO 友好。用户立即看到内容。
  • 后续操作 (CSR): JavaScript 捕获用户操作(按钮点击)并与 API 交互,仅更新页面所需部分,而无需完全重新加载。

💡 宇宙类比:

当你进入舰桥时,指挥中心(SSR)立即给你一张主导航图。它已经在你手中,无需等待。但随后你会在你的平板电脑上激活“实时模式”(CSR),它将开始从卫星接收实时更新,重新绘制你地图上的物体。


2. 步骤 1:准备页面

我们将使用我们的行星列表页面 resources/views/planets/index.blade.php。它已经能够显示从控制器传递的数据。现在我们将在其上添加通过 JS 工作控件。

添加“刷新列表”按钮和通知容器:

    <div class="controls">
        <h2>所有已知行星列表</h2>
        <button id="refresh-btn">通过 API 刷新</button>
    </div>
    <div id="notification-area" class="notification"></div>
    <hr>
    {{-- 这个 div 将作为我们动态更新的容器 --}}
    <div id="planet-list-container" class="planet-list">
        {{-- 包含“子”视图,它渲染初始列表 --}}
        @include('planets.partials.list', ['planets' => $planets])
    </div>

请注意 @include('planets.partials.list', ...)。我们将列表显示逻辑提取到一个单独的可重用文件中。


步骤 2:创建可重用“局部”视图 (Partial)

将重复的部分提取到单独的文件中是一个好习惯。

创建文件 resources/views/planets/partials/list.blade.php

@forelse($planets as $planet)
    <div class="planet-card" id="planet-card-{{ $planet->id }}">
        <h3>{{ $planet->name }}</h3>
        <p>太阳系:{{ $planet->solar_system }}</p>
        <p>直径:{{ number_format($planet->size_km, 0, '.', ' ') }} 公里</p>
        <a href="{{ route('planets.show', $planet) }}">了解更多 &rarr;</a>
        <button class="delete-btn" data-id="{{ $planet->id }}" data-url="{{ route('api.planets.destroy', $planet) }}">
            退役
        </button>
    </div>
@empty
    <p>数据库中没有行星。</p>
@endforelse
  • 重要: 请注意,删除按钮的 URL 现在是为 API 路由生成的:route('api.planets.destroy', $planet)。为此,请确保在 routes/api.php 中您有一个命名资源:Route::apiResource('planets', ...)->name('api.planets');

步骤 3:编写用于动态更新的 JavaScript

现在是最有趣的部分。我们将创建一个 JavaScript,它将通过按钮向 API 请求最新的行星列表并重新渲染它。

创建文件 public/js/planet-manager.js 并将其引入到 layouts/app.blade.php 中。

document.addEventListener('DOMContentLoaded', () => {
    const refreshBtn = document.getElementById('refresh-btn');
    const planetListContainer = document.getElementById('planet-list-container');
    const notificationArea = document.getElementById('notification-area');

    // 用于显示通知的函数
    function showNotification(message, isError = false) {
        notificationArea.textContent = message;
        notificationArea.className = isError ? 'notification error' : 'notification success';
        setTimeout(() => {
            notificationArea.textContent = '';
            notificationArea.className = 'notification';
        }, 3000);
    }

    // 用于渲染单个行星卡的函数
    function createPlanetCardHtml(planet) {
        // 重要:我们生成与我们的 partial 视图相同的 HTML
        return `
            <div class="planet-card" id="planet-card-${planet.id}">
                <h3>${planet.name}</h3>
                <p>太阳系:${planet.solar_system}</p>
                <p>直径:${new Intl.NumberFormat().format(planet.size_km)} 公里</p>
                <a href="/planets/${planet.id}">了解更多 &rarr;</a>
                <button class="delete-btn" data-id="${planet.id}" data-url="/api/planets/${planet.id}">
                    退役 (JS)
                </button>
            </div>
        `;
    }

    // 用于请求和重新渲染行星列表的函数
    async function fetchAndRenderPlanets() {
        showNotification('正在从轨道卫星请求最新数据...');
        try {
            const response = await fetch('/api/planets', {
                headers: { 'Accept': 'application/json' }
            });

            if (!response.ok) {
                throw new Error('获取数据时发生网络错误。');
            }

            const planets = await response.json(); // Laravel 默认会为分页资源返回 { data: [...] }

            planetListContainer.innerHTML = ''; // 清除旧列表

            if (planets.data.length === 0) {
                planetListContainer.innerHTML = '<p>数据库中没有行星。</p>';
            } else {
                planets.data.forEach(planet => {
                    const cardHtml = createPlanetCardHtml(planet);
                    planetListContainer.innerHTML += cardHtml;
                });
            }
            showNotification('数据已成功更新!', false);
        } catch (error) {
            console.error('更新行星列表时出错:', error);
            showNotification(error.message, true);
        }
    }

    // 为按钮添加事件监听器
    if (refreshBtn) {
        refreshBtn.addEventListener('click', fetchAndRenderPlanets);
    }

    // 可以将上一章的删除逻辑移到这里,
    // 以便所有 JS 都集中在一个地方。
});

3. 最终检查

  1. 启动服务器php artisan serve 或确保 Herd 正在运行)。
  2. 如果需要,重新创建数据库php artisan migrate:fresh --seed
  3. 在浏览器中打开 /planets 页面
    • 您应该立即看到服务器生成的行星列表。
  4. 点击“通过 API 刷新”按钮
    • 您将看到加载通知。
    • 列表应该会短暂消失并重新出现,但这次它将是基于从 API 获取的数据由 JavaScript 生成的。

您已成功实现混合模型!


巩固知识小测验

1. 什么是混合渲染方法(SSR + CSR)?

2. 初始服务器端渲染 (SSR) 的主要优点是什么?

3. 示例中为什么使用 `@include('planets.partials.list')`?

4. 在 JavaScript 代码中,我们复制了卡片的 HTML 结构。有什么更高级的方法可以避免这种情况?

5. 为什么 API (`/api/planets`) 和 JavaScript 代码 (`createPlanetCardHtml`) 生成一致的数据/HTML 很重要?


🚀 恭喜您完成第5章!

您已经从 Blade 基础知识学到了创建交互式混合页面的巨大进步。您学会了:

  • 创建和使用 Blade 模板和布局。
  • 组织用于 CRUD 操作的 Web 路由和控制器。
  • 使用 CSRF 令牌保护 Web 表单和 AJAX 请求。
  • 集成 JavaScript 以在不重新加载页面的情况下与 API 进行动态交互。

您的飞行控制中心已完全功能化、安全且交互性强。 您已准备好进入下一个重要阶段 — 将此方法与其他框架进行比较,并学习生产环境的最佳实践。