Next的核心特点
我们常用的Vue、React等严格来说不能叫做前端框架(Framework),最多只能叫做库(Library)。基于库的方案集成的项目,才能被称之为框架。当下,React最流行的框架是Next.js,Vue对标的则是Nuxt.js。
Next.js 的几个特点
- 多种渲染方式
- 高效路由模式
- 扩展性
- 部署友好
上述几个特点是我认为Next最核心的优势,这些特点也帮助它成为当下前端最流行的框架,甚至没有之一。
渲染方式
服务端渲染(SSR)、静态站点生成(SSG)、增量静态生成(ISR)这三种渲染方式基本上能应对前端开发的大部分情况了。SSR是在服务器生成HTML内容,帮助提升SEO效果;SSG构建时预渲染HTML,适合博客、个人网站的开发;ISR能够动态的更新静态内容,而不会重新构建整个项目,对性能有比较好的优化
路由模式
比较值得说的是文件系统路由,自动读取/pages下的路径作为路由,无需额外配置路由文件。这个在开发上很好用。以及动态路由生成,通过URL参数来生成动态页面,比如pages/post/[id].js
扩展性
对Tailwindcss的集成度友好、同时less、scss方案也没问题;自带i18n,也无需额外安装插件。
部署
这个相当于核心卖点,Next.js的开发商Vercel提供了云部署,更是重量级,可以几乎以白嫖的方式部署你的应用。当然这个模式也有缺点,后续再讲。
总结
结合上述特点,Next很适合做多语言的项目,由于对SEO的友好,也很适合做电商等应用,多种渲染策略,使得它灵活性很高。这次我打算使用Next结合Canvas做一个轮盘小游戏,感兴趣的往下看吧。
初始化
初始化就使用官方标准三板斧:
npx create-next-app@latest --next-name

解释一下安装设置:
- TypeScript 选择Yes,我打算使用类型约束
- 启用ESLint,选No,TypeScript本身就完成了规范约束,使用TS做单一约束系统
- Tailwindcss,Yes,这一套样式语法,对开发效率来说有提升
- code 路径,默认src,选择Yes,符合一贯开发习惯
- AppRouter,应用路由系统,也是Next的特色,选择Yes
- Turbopack,运行环境,Turbopack是对标Vite的工具,也是Next官方建议的,该工具是用Rust编写,自带热更新,性能很强,我们也选择Yes
- 是否自定义倒入别名,默认为@,选择No
接下来就等待安装完毕即可。

安装完之后的目录如下,tailwind.config.js需要额外新增,我们创建这个文件并编辑,它的作用是告诉Next该项目的tailwind基础样式和配置以该文件为准:
module.exports = {
darkMode: "class",
content: [
"./src/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
colors:{
background: "var(--background)",
foreground: "var(--foreground)",
},
fontFamily: {
sans: ["var(--font-geist-sans)"],
mono: ["var(--font-geist-mono)"],
},
},
},
plugins: [],
};
点开next.config.js,增加如下编辑内容
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
/** 开关开发者UI的选项 */
devIndicators:false
};
export default nextConfig;
接下来,我们设置一下目录。
/src
/app
/picker-wheel
/components
/lib
/style
index.md
page.tsx
/setting
/share
/achieve
我们会在picker-wheel下制作轮盘,首先我们把根路径下的page清空,并导入picker-wheel。代码如下
/**根page**/
import Image from "next/image";
import Link from "next/link";
import PickerWheel from "./picker-wheel/page";
export default function Home() {
return (
<div className="
grid grid-rows-[20px_1fr_20px] justify-items-center items-center min-h-screen p-8 pb-20 gap-16 sm:p-20
">
<header>header</header>
<main>
<div>
<PickerWheel/>
</div>
</main>
<footer>footer</footer>
</div>
);
}
接着我们来做轮盘功能,代码如下
import WheelCanvas from "./components/wheelCanvas";
export default function PickerWheel() {
return (
<div className="container mx-auto p-4">
<h1>yes or no wheel</h1>
<WheelCanvas />
</div>
);
}
我使用的动画方案是Cavans的方式,我没有采用Anime.js+Div+css的方式,我发现在Next的环境下好像动画不生效,有懂的大佬可以在评论区指点一下。
接下来是WheelCanvas的具体实现
"use client";
import { useEffect, useRef, useState } from "react";
export default function WheelCanvas() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [isSpinning, setIsSpinning] = useState(false);
const rotationAngle = useRef(0);
// 绘制转盘(修复尺寸问题)
const drawWheel = (ctx: CanvasRenderingContext2D, angle: number = 0) => {
const prizes = ["Yes", "No"];
const centerX = ctx.canvas.width / 2;
const centerY = ctx.canvas.height / 2;
const displaySize = Math.min(ctx.canvas.width, ctx.canvas.height); // 统一尺寸基准
const radius = displaySize / 2 - 10; // 保留10px边距
// 清除画布
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
// 应用旋转
ctx.save();
ctx.translate(centerX, centerY);
ctx.rotate((angle * Math.PI) / 180);
ctx.translate(-centerX, -centerY);
// 绘制扇形
prizes.forEach((text, i) => {
const startAngle = (i * Math.PI * 2) / prizes.length;
const endAngle = ((i + 1) * Math.PI * 2) / prizes.length;
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.arc(centerX, centerY, radius, startAngle, endAngle);
ctx.fillStyle = i === 0 ? "#FF5252" : "#4CAF50";
ctx.fill();
// 绘制文字
ctx.save();
ctx.translate(centerX, centerY);
ctx.rotate(startAngle + Math.PI / prizes.length);
ctx.textAlign = "center";
ctx.fillStyle = "white";
ctx.font = `bold ${radius * 0.15}px Arial`; // 动态字体大小
ctx.fillText(text, radius * 0.5, radius * 0.05);
ctx.restore();
});
ctx.restore();
};
const handleSpin = () => {
if (isSpinning || !canvasRef.current) return;
const ctx = canvasRef.current.getContext("2d");
if (!ctx) return;
setIsSpinning(true);
const startAngle = rotationAngle.current;
const targetAngle = startAngle + 1800 + Math.random() * 360;
let startTime: number | null = null;
const duration = 3000;
// 手动动画
const animateFrame = (timestamp: number) => {
if (!startTime) startTime = timestamp;
const elapsed = timestamp - startTime;
const progress = Math.min(elapsed / duration, 1);
// 弹性缓动
const elasticProgress = progress < 0.5
? 0.5 * Math.pow(2, 20 * progress - 10)
: 0.5 * (2 - Math.pow(2, -20 * progress + 10));
rotationAngle.current = startAngle + (targetAngle - startAngle) * elasticProgress;
drawWheel(ctx, rotationAngle.current);
if (progress < 1) {
requestAnimationFrame(animateFrame);
} else {
// 转盘结束
setIsSpinning(false);
const prizeIndex = Math.floor(((rotationAngle.current % 360) + 90) % 360 / 180);
alert(`Result: ${["Yes", "No"][prizeIndex]}`);
}
};
requestAnimationFrame(animateFrame);
};
// 初始化Canvas(关键修复)
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
// 逻辑尺寸(保持300x300的工作坐标)
const logicalSize = 300;
canvas.width = logicalSize;
canvas.height = logicalSize;
// 显示尺寸(通过CSS控制)
canvas.style.width = `${logicalSize}px`;
canvas.style.height = `${logicalSize}px`;
const ctx = canvas.getContext("2d");
if (!ctx) return;
// 初始化绘制
drawWheel(ctx);
}, []);
return (
<div className="text-center">
<canvas
ref={canvasRef}
className="mx-auto border rounded-full shadow-lg"
/>
<button
onClick={handleSpin}
disabled={isSpinning}
className="mt-6 px-6 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
>
{isSpinning ? "spinning..." : "start spinning"}
</button>
</div>
);
}
效果呈现
最后我们可以来看一下效果。
点击旋转就会开始转。并且带有过度动画,和速度衰减,使得转盘更加真实。
后续我们还会增加额外的功能,音效的同步、抽奖结果的设置等等。