Y.K.'s Sanctuary
  • 主頁
  • 部落格
  • 永續
  • 鄉土
  • 小遊戲
  • 主頁
  • 部落格
  • 永續
  • 鄉土
  • 小遊戲

30 天架起一個前後端分離網站 - Day 3

連載區索引

30 天架起一個前後端分離網站

要完成一個小朋友下樓梯遊戲,我們需要幾個模組。首先,我們需要有一個靜態頁面,這個頁面是整個遊戲的入口,未來如果網站有其他頁面的話,它也會作為其中一個分頁;接下來,會需要一個處理 RWD 的模組,因為大家會使用各種各樣的設備上網,而我們的遊戲需要用鍵盤來控制,所以會需要一個根據使用者螢幕大小來決定呈現什麼內容的模組;接下來,才是遊戲本體。 在 Day 3,我們將專注處理靜態頁面以及這個 RWD 模組,同時,藉著這個機會更多了解 Next.js 怎麼建立起一個網站吧!

目錄

  • Hydration Warning
  • 程式入口
  • RWD 模組
  • 淺談 use client, use server

Hydration Warning

首先,我們需要先處理 Text content does not match server-rendered HTML 問題。這個錯誤發生於同一個 html 元件在 pre-render 的時候,與後續的 render 不一致。根據 Next.js 官方文件的建議,在這裡我們可以很簡單的採用第三種方法,加入 suppressHydrationWarning attribute。

frontend/down-game/src/app/layout.js

import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({ children }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
        suppressHydrationWarning
      >
        {children}
      </body>
    </html>
  );
}

程式入口

frontend/down-game/src/app/page.js

import CompatibilityChecker from "@/components/down-stairs-game/compatibilityChecker";
import Head from "next/head";

export default function Home() {
  return (
    <div className="flex items-center justify-center h-screen">
      <Head>
        <title>小朋友下樓梯</title>
        <meta
          name="description"
          content="小朋友下樓梯遊戲 - Next.js & TypeScript 版本"
        />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <div className="container mx-auto py-8 px-4">
        <h1 className="text-4xl font-bold text-center mb-8 text-white">
          小朋友下樓梯
        </h1>
        <CompatibilityChecker />
        <div className="max-w-2xl mx-auto bg-white rounded-xl shadow-md overflow-hidden">
          <div className="bg-gray-50 p-6 border-t border-gray-200">
            <h2 className="text-xl font-semibold mb-3 text-gray-700">
              遊戲說明
            </h2>
            <ul className="space-y-2 text-gray-600">
              <li className="flex items-center">
                <svg
                  className="w-5 h-5 mr-2 text-green-500"
                  fill="none"
                  stroke="currentColor"
                  viewBox="0 0 24 24"
                  xmlns="http://www.w3.org/2000/svg"
                >
                  <path
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    strokeWidth="2"
                    d="M9 5l7 7-7 7"
                  ></path>
                </svg>
                使用鍵盤的左右箭頭鍵控制角色移動
              </li>
              <li className="flex items-center">
                <svg
                  className="w-5 h-5 mr-2 text-green-500"
                  fill="none"
                  stroke="currentColor"
                  viewBox="0 0 24 24"
                  xmlns="http://www.w3.org/2000/svg"
                >
                  <path
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    strokeWidth="2"
                    d="M9 5l7 7-7 7"
                  ></path>
                </svg>
                避免碰到畫面上方邊界
              </li>
              <li className="flex items-center">
                <svg
                  className="w-5 h-5 mr-2 text-green-500"
                  fill="none"
                  stroke="currentColor"
                  viewBox="0 0 24 24"
                  xmlns="http://www.w3.org/2000/svg"
                >
                  <path
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    strokeWidth="2"
                    d="M9 5l7 7-7 7"
                  ></path>
                </svg>
                在階梯上行走,注意不要掉落
              </li>
              <li className="flex items-center">
                <svg
                  className="w-5 h-5 mr-2 text-green-500"
                  fill="none"
                  stroke="currentColor"
                  viewBox="0 0 24 24"
                  xmlns="http://www.w3.org/2000/svg"
                >
                  <path
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    strokeWidth="2"
                    d="M9 5l7 7-7 7"
                  ></path>
                </svg>
                遊戲速度會隨著分數增加而加快
              </li>
            </ul>
          </div>
        </div>
      </div>
    </div>
  );
}

使用 Tailwind 讓我們可以省去管理 css 檔案的問題,當專案規模越來越大以後,css 的管理問題會越來越使人頭痛。雖然 Tailwind 也有一些問題,Tailwind 語法與原生的 css 有一些出入,導致使用者必須學習另一套新的語法,不過整體而言,套用 Tailwind,可以幫助我們更簡單的管理頁面的風格設計。Tailwind官網有很詳細的使用文件可以參考,不過在 AI 時代裡,我們其實可以選擇直接交給 AI 幫忙處理就好,這種死板板的語法,可以放心的交給 AI 生成,出錯的機率很低。

RWD 模組

frontend/down-game/src/components/down-stairs-game/compatibilityChecker.jsx

"use client";

import { useEffect, useState } from "react";

export default function CompatibilityChecker() {
  const [isCompatible, setIsCompatible] = useState(true);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    // 檢查螢幕寬度和鍵盤支援
    const checkCompatibility = () => {
      const hasWideScreen = window.innerWidth >= 1024;

      // 檢測是否可能是觸控裝置(通常沒有實體鍵盤)
      const isTouchDevice =
        "ontouchstart" in window ||
        navigator.maxTouchPoints > 0 ||
        !!navigator.msMaxTouchPoints;

      setIsCompatible(hasWideScreen && !isTouchDevice);
      setIsLoading(false);
    };

    // 初始檢查
    checkCompatibility();

    // 監聽視窗大小變化
    window.addEventListener("resize", checkCompatibility);

    // 清理函數
    return () => {
      window.removeEventListener("resize", checkCompatibility);
    };
  }, []);

  if (isLoading) {
    return (
      <div className="p-16 text-center">
        <div className="inline-block animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-gray-900 mb-4"></div>
        <p>載入中...</p>
      </div>
    );
  }

  if (!isCompatible) {
    return (
      <div className="p-8 text-center">
        <div className="bg-yellow-50 border-l-4 border-yellow-400 p-4 mb-6">
          <div className="flex">
            <div className="flex-shrink-0">
              <svg
                className="h-5 w-5 text-yellow-400"
                xmlns="http://www.w3.org/2000/svg"
                viewBox="0 0 20 20"
                fill="currentColor"
              >
                <path
                  fillRule="evenodd"
                  d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
                  clipRule="evenodd"
                />
              </svg>
            </div>
            <div className="ml-3">
              <p className="text-sm text-yellow-700">
                此遊戲最佳體驗需要電腦和鍵盤!
              </p>
            </div>
          </div>
        </div>

        <div className="text-6xl mb-4 opacity-70">💻</div>

        <h3 className="text-xl font-bold mb-2 text-gray-800">請使用電腦遊玩</h3>
        <p className="text-gray-600 mb-6">
          本遊戲需要使用鍵盤操作,且建議在更大的螢幕上遊玩以獲得最佳體驗。
          請使用筆記型電腦或桌上型電腦訪問此頁面。
        </p>
      </div>
    );
  }
}

完成後,當我們把瀏覽器視窗縮窄時,會看到這樣子的畫面: 而把寬度拉長後,則會看到這樣的畫面:到這裡,就暫時把工作告一段落吧!

淺談 use client, use server

這哥倆好是 Next.js 13 以後引入的模組。如果對 Flutter 有一些了解的朋友,可以試著用 StatelessWidget 和 StatefulWidget 的邏輯去理解它們。原則上,use client、use server 被設計出來的目的,就是要明確靜態頁面,以及各種動態渲染邏輯之間的分工。一面這會讓整個畫面渲染的效率提升,一面也對程式開發的架構有很大幫助。

簡單而言,use server 是一個靜態頁面:各種切版或是靜態的網頁內容,可以被放在這裡。同時,各種 "use client"的模組,也會在這裡被引用。use client 則是一個動態頁面,React 當中最重要的各種 Hook,比如 useState, useEffect,都只能在這類模組下被調用。use server 和 use client 組合在一起後,就可以形成一個有複雜操作與功能的頁面。

Next.js 在 13 版後導入這個功能,我認為雖然有很大的好處,可是同時也增加了整個 Framework 的複雜度,學習曲線愈來愈高。這個問題不只是對原本熟悉 Next.js 的人而言很痛苦,也對新手非常的不友善。但是歸根究底,這個問題可能還是跟 React 本身的設計有關係,而不是 Next.js 的問題。我認為 use client 與 use server 的導入,可以幫助 MVVM 模式的開發效率提升,但是 React 的整個框架哲學與 MVVM 的概念是不太能相容的。我想這可能是導致 Next.js 導入這個模式以後,使用起來反而有一些困擾的原因。

Thank you for reading. If you enjoyed this post, consider sharing it with others.