要完成一個小朋友下樓梯遊戲,我們需要幾個模組。首先,我們需要有一個靜態頁面,這個頁面是整個遊戲的入口,未來如果網站有其他頁面的話,它也會作為其中一個分頁;接下來,會需要一個處理 RWD 的模組,因為大家會使用各種各樣的設備上網,而我們的遊戲需要用鍵盤來控制,所以會需要一個根據使用者螢幕大小來決定呈現什麼內容的模組;接下來,才是遊戲本體。 在 Day 3,我們將專注處理靜態頁面以及這個 RWD 模組,同時,藉著這個機會更多了解 Next.js 怎麼建立起一個網站吧!
首先,我們需要先處理 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 生成,出錯的機率很低。
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>
);
}
}
完成後,當我們把瀏覽器視窗縮窄時,會看到這樣子的畫面: 而把寬度拉長後,則會看到這樣的畫面:
到這裡,就暫時把工作告一段落吧!
這哥倆好是 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.