普通视图

发现新文章,点击刷新页面。
昨天 — 2025年12月30日首页

Next.js第十八章(静态导出SSG)

作者 小满zs
2025年12月29日 16:58

静态导出SSG

Next.js 支持静态站点生成(SSG,Static Site Generation),可以在构建时预先生成所有页面的静态 HTML 文件。这种方式特别适合内容相对固定的站点,如官网博客文档等,能够提供最佳的性能和 SEO 表现。

配置静态导出

需要在next.config.js文件中配置outputexport,表示导出静态站点。distDir表示导出目录,默认为out

import type { NextConfig } from "next";
const nextConfig: NextConfig = {
  /* config options here */
  output: "export", // 导出静态站点
  distDir: "dist", // 导出目录
};

export default nextConfig;

接着我们执行npm run build命令,构建静态站点。

构建完成之后,我们安装http-server来启动静态站点。

npm install http-server -g #安装http-server
cd dist #进入导出目录
http-server -p 3000 #启动静态站点

11.gif

启动完成之后发现点击a标签无法进行跳转,是因为打完包之后的页面叫about.html,而我们的跳转链接是/about,所以需要修改配置项。

build.png

修改配置项

需要在next.config.js文件中配置trailingSlashtrue,表示添加尾部斜杠,生成/about/index.html而不是/about.html

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  /* config options here */
  output: "export", // 导出静态站点
  distDir: "dist", // 导出目录
  trailingSlash: true, // 添加尾部斜杠,生成 /about/index.html 而不是 /about.html
};

export default nextConfig;

trailingSlash.png

此时重新点击a标签就可以进行跳转了。

动态路由处理

新建目录: src/app/posts/[id]/page.tsx

如果要使用动态路由,则需要使用generateStaticParams函数来生成有多少个动态路由,这个函数需要返回一个数组,数组中包含所有动态路由的参数,例如{ id: '1' }表示对应id为1的详情页。

export async function generateStaticParams() {
    //支持调用接口请求详情id列表 const res = await fetch('https://api.example.com/posts')
    return [
        { id: '1' }, //返回对应的详情id
        { id: '2' },
    ]
}

export default async function Post({ params }: { params: Promise<{ id: string }> }) {
    const { id } = await params
    return (
        <div>
            <h1>Post {id}</h1>
        </div>
    )
}

图片优化

如果使用Image组件优化图片,在开发模式会进行报错

⚠️ 警告

get-img-props.ts 442 Uncaught Error: Image Optimization using the default loader is not compatible with { output: 'export' }.

可能的解决方案:

  • 移除 { output: 'export' } 并运行 "next start" 以启用包含图片优化 API 的服务器模式。
  • next.config.js 中配置 { images: { unoptimized: true } } 来禁用图片优化 API。
  • 使用自定义loader实现

了解更多:nextjs.org/docs/messag…

import Image from "next/image"
import test from '@/public/1.png'
export default function About() {
    return (
        <div>
            <h1>About</h1>
            <Image  loading="eager" src={test} alt="logo" width={250 * 3} height={131 * 3} />
        </div>
    )
}

我们使用自定义loader来实现图片优化,要求我们通过一个图床托管图片。路过图床 是一个免费的图床,我们可以使用它来托管图片。

luguo.png

url.png

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  /* config options here */
  output: "export", // 导出静态站点
  distDir: "dist", // 导出目录
  trailingSlash: true, // 添加尾部斜杠,生成 /about/index.html 而不是 /about.html
  images: {
    loader: 'custom', // 自定义loader
    loaderFile: './image-loader.ts', // 自定义loader文件
  },
};

export default nextConfig;

根目录:/image-loader.ts

export default function imageLoader({ src, width, quality }: { src: string, width: number, quality: number }) {
    return `https://s41.ax1x.com${src}`
}

src/app/about/page.tsx

import Image from "next/image"

export default function About() {
    return (
        <div>
            <h1>About</h1>
            <Image loading="eager" src='/2025/12/29/pZYbW7t.jpg' alt="logo" width={250 * 3} height={131 * 3} />
        </div>
    )
}

img.png

注意事项

以下功能在SSG中不支持,请勿使用:

  • Dynamic Routes with dynamicParams: true
  • 动态路由没有使用generateStaticParams()
  • 路由处理器依赖于Request
  • Cookies
  • Rewrites重写
  • Redirects重定向
  • Headers头
  • Proxy代理
  • Incremental Static Regeneration增量静态再生
  • Image Optimization with the default loader默认加载器的图像优化
  • Draft Mode草稿模式
  • Server Actions服务器操作
  • Intercepting Routes拦截路由
昨天以前首页

动态配色方案:在 Next.js 中实现 Shadcn UI 主题色切换

2025年12月29日 09:28

前言

Hi,大家好,我是白雾茫茫丶!

你是否厌倦了千篇一律的网站配色?想让你的 Next.js 应用拥有像 Figma 那样灵活的主题切换能力?在当今追求个性化和用户体验的时代,单一的配色方案早已无法满足用户多样化的审美需求。无论是适配品牌形象、响应节日氛围,还是提供用户自定义选项,动态主题色切换已成为现代 Web 应用的重要特性。

本文将带你深入探索如何在 Next.js 应用中实现专业级的主题色切换系统。我们将利用 Shadcn UI 的设计系统架构,结合 CSS 自定义属性(CSS Variables)的强大能力,打造一个不仅支持多套预设配色方案,还能保持代码优雅和性能高效的主题切换方案。无论你是想为用户提供“蓝色商务”、“绿色生态”还是“紫色创意”等不同视觉主题,这篇文章都将为你提供完整的实现路径。

告别单调,迎接多彩——让我们一起构建让用户眼前一亮的动态主题系统!

开发思路

我的实现思路主要基于 CSS 自定义属性(CSS Variables)。每套主题配色对应一组预定义的变量值,以独立的类型(或类名)标识。在切换主题时,只需为 <html> 根元素动态添加对应的类型类名,即可通过 CSS 变量的作用域机制,全局应用相应的配色方案,从而高效、无缝地完成主题切换。

主题构建工具

当然,要高效地实现基于 CSS 变量的动态主题系统,离不开一个强大的主题构建工具来生成和管理不同配色方案。在这里,我强烈推荐一款专为 shadcn/ui 打造的主题编辑与生成工具:

tweakcn.com/

TweakCN 不仅界面简洁直观,更深度集成了 shadcn/ui 的设计规范,支持实时预览、一键导出 Tailwind CSS 配置及 CSS 变量定义。你可以自由调整主色、辅助色、语义色(如成功、警告、错误等),并自动生成适配深色/浅色模式的完整配色方案。更重要的是,它输出的代码可直接用于 Next.js 项目,配合 CSS 变量策略,轻松实现主题切换——无需手动计算颜色值或反复调试样式,极大提升了开发效率与设计一致性。对于希望快速定制品牌化 UI 风格的开发者来说,TweakCN 无疑是一个强大而贴心的助手。

定义多套配色方案

1、在主题编辑页面,TweakCN 默认提供了 43 套精心设计的配色方案。你可以逐一浏览并实时预览每种方案在实际 UI 组件中的呈现效果。从中挑选几套符合项目风格或个人审美的配色,也可以基于现有方案进一步微调主色、辅助色或语义色,打造完全属于你自己的定制化主题。

202512/w0sy4ok3lelxx4fjx2tafw52lk1kunjr.gif

2、在确认主题配色后,点击右上角的 {} Code 按钮,点击 Copy 复制样式:

202512/ucjfomwfkpjmbuz9n82kgyx1u9ia8eym.png

3、新建一个 theme.css 文件,用来保存不同的主题配色:

:root {
  --background: oklch(1 0 0);
  --foreground: oklch(0.1450 0 0);
  --card: oklch(1 0 0);
  --card-foreground: oklch(0.1450 0 0);
  --popover: oklch(1 0 0);
  --popover-foreground: oklch(0.1450 0 0);
  --primary: oklch(0.2050 0 0);
  --primary-foreground: oklch(0.9850 0 0);
  --secondary: oklch(0.9700 0 0);
  --secondary-foreground: oklch(0.2050 0 0);
  --muted: oklch(0.9700 0 0);
  --muted-foreground: oklch(0.5560 0 0);
  --accent: oklch(0.9700 0 0);
  --accent-foreground: oklch(0.2050 0 0);
  --destructive: oklch(0.5770 0.2450 27.3250);
  --destructive-foreground: oklch(1 0 0);
  --border: oklch(0.9220 0 0);
  --input: oklch(0.9220 0 0);
  --ring: oklch(0.7080 0 0);
  --chart-1: oklch(0.646 0.222 41.116);
  --chart-2: oklch(0.6 0.118 184.704);
  --chart-3: oklch(0.398 0.07 227.392);
  --chart-4: oklch(0.828 0.189 84.429);
  --chart-5: oklch(0.769 0.188 70.08);
  --sidebar: oklch(0.9850 0 0);
  --sidebar-foreground: oklch(0.1450 0 0);
  --sidebar-primary: oklch(0.2050 0 0);
  --sidebar-primary-foreground: oklch(0.9850 0 0);
  --sidebar-accent: oklch(92.2% 0 0);
  --sidebar-accent-foreground: oklch(0.2050 0 0);
  --sidebar-border: oklch(0.9220 0 0);
  --sidebar-ring: oklch(0.7080 0 0);
  --font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
  --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
  --radius: 0.625rem;
  --shadow-x: 0;
  --shadow-y: 1px;
  --shadow-blur: 3px;
  --shadow-spread: 0px;
  --shadow-opacity: 0.1;
  --shadow-color: oklch(0 0 0);
  --shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
  --shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
  --shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
  --shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
  --shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
  --shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
  --shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
  --shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
  --tracking-normal: 0em;
  --spacing: 0.25rem;
}

.dark {
  --background: oklch(0.1450 0 0);
  --foreground: oklch(0.9850 0 0);
  --card: oklch(0.2050 0 0);
  --card-foreground: oklch(0.9850 0 0);
  --popover: oklch(0.2690 0 0);
  --popover-foreground: oklch(0.9850 0 0);
  --primary: oklch(0.9220 0 0);
  --primary-foreground: oklch(0.2050 0 0);
  --secondary: oklch(0.2690 0 0);
  --secondary-foreground: oklch(0.9850 0 0);
  --muted: oklch(0.2690 0 0);
  --muted-foreground: oklch(0.7080 0 0);
  --accent: oklch(0.3710 0 0);
  --accent-foreground: oklch(0.9850 0 0);
  --destructive: oklch(0.7040 0.1910 22.2160);
  --destructive-foreground: oklch(0.9850 0 0);
  --border: oklch(0.2750 0 0);
  --input: oklch(0.3250 0 0);
  --ring: oklch(0.5560 0 0);
  --chart-1: oklch(0.488 0.243 264.376);
  --chart-2: oklch(0.696 0.17 162.48);
  --chart-3: oklch(0.769 0.188 70.08);
  --chart-4: oklch(0.627 0.265 303.9);
  --chart-5: oklch(0.645 0.246 16.439);
  --sidebar: oklch(0.2050 0 0);
  --sidebar-foreground: oklch(0.9850 0 0);
  --sidebar-primary: oklch(0.4880 0.2430 264.3760);
  --sidebar-primary-foreground: oklch(0.9850 0 0);
  --sidebar-accent: oklch(0.2690 0 0);
  --sidebar-accent-foreground: oklch(0.9850 0 0);
  --sidebar-border: oklch(0.2750 0 0);
  --sidebar-ring: oklch(0.4390 0 0);
  --font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
  --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
  --radius: 0.625rem;
  --shadow-x: 0;
  --shadow-y: 1px;
  --shadow-blur: 3px;
  --shadow-spread: 0px;
  --shadow-opacity: 0.1;
  --shadow-color: oklch(0 0 0);
  --shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
  --shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
  --shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
  --shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
  --shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
  --shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
  --shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
  --shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
}

4、这样我们就默认一个主题,如果是多套配色,我们可以加上主题类名区分,例如:

/* Amber Minimal */
:root.theme-amber-minimal{
}
.dark.theme-amber-minimal{
}

/* Amethyst Haze */
:root.theme-amethyst-haze{
}
.dark.theme-amethyst-haze {
}

5、然后把 theme.css 导入到全局样式中,Next.js 项目一般是 global.css

@import "./themes.css";

到这里,我们的准备工作就算完成了,接下来我们就完成主题色的切换逻辑!

具体实现

1、这里我们需要用到 zustand 来保存主题色的状态:

pnpm add zustand

2、创建主题配色枚举:

    /**
     * @description: 主题色
     */
    export const THEME_PRIMARY_COLOR = Enum({
      DEFAULT: { value: 'default', label: 'Default', color: 'oklch(0.205 0 0)' },
      AMBER_MINIMAL: { value: 'amber-minimal', label: 'Amber', color: 'oklch(0.7686 0.1647 70.0804)' },
      AMETHYST_HAZE: { value: 'amethyst-haze', label: 'Amethyst', color: 'oklch(0.6104 0.0767 299.7335)' },
      CANDYLAND: { value: 'candyland', label: 'Candyland', color: 'oklch(0.8677 0.0735 7.0855)' },
      DARKMATTER: { value: 'darkmatter', label: 'Darkmatter', color: 'oklch(0.6716 0.1368 48.5130)' },
      ELEGANT_LUXURY: { value: 'elegant-luxury', label: 'Elegant', color: 'oklch(0.4650 0.1470 24.9381)' },
      SAGE_GARDEN: { value: 'sage-garden', label: 'Garden', color: 'oklch(0.6333 0.0309 154.9039)' },
      SUPABASE: { value: 'supabase', label: 'Supabase', color: 'oklch(0.8348 0.1302 160.9080)' },
      TWITTER: { value: 'twitter', label: 'Twitter', color: 'oklch(0.6723 0.1606 244.9955)' },
    });

3、新建 store/useAppStore.ts 文件:

    'use client'
    import { create } from 'zustand'
    import { createJSONStorage, persist } from 'zustand/middleware'

    import { THEME_PRIMARY_COLOR } from '@/enums';
    import { initializePrimaryColor } from '@/lib/utils';

    type AppState = {
      primaryColor: typeof THEME_PRIMARY_COLOR.valueType; // 主题色
      setPrimaryColor: (color: typeof THEME_PRIMARY_COLOR.valueType) => void; // 设置主题色
    }

    export const useAppStore = create(
      persist<AppState>(
        (set) => ({
          primaryColor: THEME_PRIMARY_COLOR.DEFAULT, // 默认主题色
          setPrimaryColor: (color) => {
            set({ primaryColor: color })
            initializePrimaryColor(color);
          }
        }),
        {
          name: 'app-theme', // 用于存储在 localStorage 中的键名
          storage: createJSONStorage(() => localStorage)// 指定使用 localStorage 存储
        }))

4、创建主题色初始化函数:

    /**
     * @description: 初始化主题色
     * @param {typeof} color
     */
    export const initializePrimaryColor = (color: typeof THEME_PRIMARY_COLOR.valueType) => {
      if (typeof document !== 'undefined') {
        // 清空 theme- 开头的类名
        const html = document.documentElement;
        Array.from(html.classList)
          .filter((className) => className.startsWith("theme-"))
          .forEach((className) => {
            html.classList.remove(className)
          })
        // 如果不是默认主题色,则添加对应的类名
        if (color !== THEME_PRIMARY_COLOR.DEFAULT) {
          html.classList.add(`theme-${color}`);
        }
      }
    }

5、创建主题切换按钮:

    import { type FC, useCallback } from "react";

    import { getClipKeyframes } from '@/components/animate-ui/primitives/effects/theme-toggler';
    import { Button } from '@/components/ui';
    import { THEME_PRIMARY_COLOR } from '@/enums';
    import { useAppStore } from '@/store/useAppStore';

    const PrimaryColorPicker: FC = () => {
      const primaryColor = useAppStore((s) => s.primaryColor);
      const setPrimaryColor = useAppStore((s) => s.setPrimaryColor);
      const themeModeDirection = useAppStore((s) => s.themeModeDirection);

      const [fromClip, toClip] = getClipKeyframes(themeModeDirection);

      // 点击颜色切换
      const onChangeColor = useCallback(async (color: typeof THEME_PRIMARY_COLOR.valueType) => {
        if (primaryColor === color) {
          return;
        }
        if ((!document.startViewTransition)) {
          setPrimaryColor(color);
          return;
        }
        await document.startViewTransition(async () => {
          setPrimaryColor(color);
        }).ready;
        document.documentElement
          .animate(
            { clipPath: [fromClip, toClip] },
            {
              duration: 700,
              easing: 'ease-in-out',
              pseudoElement: '::view-transition-new(root)',
            },
          )
      }, [primaryColor, setPrimaryColor, fromClip, toClip])
      return (
        <>
          <div className="grid grid-cols-3 gap-2">
            {THEME_PRIMARY_COLOR.items.map(({ value, label, raw }) => (
              <Button
                size="sm"
                aria-label="PrimaryColorPicker"
                variant={primaryColor === value ? "secondary" : "outline"}
                key={value}
                className="text-xs justify-start"
                onClick={() => onChangeColor(value)}
              >
                <span className="inline-block size-2 rounded-full"
                  style={{ backgroundColor: raw.color }} />
                {label}
              </Button>
            ))}
          </div>
          <style>{`::view-transition-old(root), ::view-transition-new(root){animation:none;mix-blend-mode:normal;}`}</style>
        </>
      )
    }
    export default PrimaryColorPicker;

这里我加了切换过渡动画,不需要的可以自行去掉!

6、页面刷新的时候需要同步,在 Provider.tsx 中初始化:

    import { initializePrimaryColor } from '@/lib/utils';
    const primaryColor = useAppStore((s) => s.primaryColor);

    // 初始化主题色
    useEffect(() => {
      if (primaryColor) {
        initializePrimaryColor(primaryColor);
      }
    }, [primaryColor])

效果预览

202512/fzqxxw5cxexwp2kqeklcevk6o2dwulqi.gif

总结

实现动态主题配色的方式多种多样——从 CSS-in-JS、Tailwind 的 class 切换,到运行时注入样式表等,各有优劣。本文分享的是基于 CSS 自定义属性(CSS Variables)HTML 根元素类名切换 的轻量级方案,配合 TweakCN 这样的可视化工具,能够快速构建出结构清晰、易于维护的主题系统。当然,这仅是我个人在项目中的一种实践思路,如果你有更优雅、更高效的实现方式,欢迎在评论区留言交流!技术因分享而进步,期待看到你的创意方案 🌈。

线上预览:next.baiwumm.com

Github 地址:github.com/baiwumm/nex…

Next.js第十七章(Script脚本)

作者 小满zs
2025年12月27日 06:01

Script组件

Next.js允许我们使用Script组件去加载js脚本(外部/本地脚本),并且他还对Script组件进行优化。

基本使用

局部引入

src/app/home/page.tsx

在home路由引入一个远程的js脚本,他只会在切换到home路由时才会加载,并且只会加载一次,然后纳入缓存。

import Script from 'next/script' //引入Script组件
export default function HomePage() {
    return (
        <div>
            <Script src="https://unpkg.com/vue@3/dist/vue.global.js" />
        </div>
    )
}

他的底层原理会把这个Script组件转换成<script>标签,然后插入到<head>标签中。

head.png

全局引入

src/app/layout.tsx

全局引入直接在app/layout.tsx中引入,他会自动在所有页面中引入,并且只会加载一次,然后纳入缓存。

import Script from 'next/script'
export default function RootLayout({ children }: { children: React.ReactNode }) {
    return (
        <html>
            <head>
                <Script src="https://unpkg.com/vue@3/dist/vue.global.js" />
            </head>
            <body>
                {children}
            </body>
        </html>
    )
}
加载策略

Next.js允许我们通过strategy属性来控制Script组件的加载策略。

  • beforeInteractive: 在代码和页面之前加载会阻塞页面渲染
  • afterInteractive(默认值): 在页面渲染到客户端之后加载。
  • lazyOnload: 在浏览器空闲时稍后加载脚本。
  • worker(实验性特性): 暂时不建议使用。
<Script id="VGUBHJMK1" strategy="beforeInteractive" src="https://unpkg.com/vue@3/dist/vue.global.js" />
<Script id="VGUBHJMK2" strategy="afterInteractive" src="https://unpkg.com/vue@3/dist/vue.global.js" />
<Script id="VGUBHJMK3" strategy="lazyOnload" src="https://unpkg.com/vue@3/dist/vue.global.js" />
<Script id="VGUBHJMK4" strategy="worker" src="https://unpkg.com/vue@3/dist/vue.global.js" />

webWorker模式 尚不稳定,谨慎使用,小提示可以给Script组件添加id,Next.js会追踪优化。

内联脚本

即使不从外部文件载入脚本,Next.js也支持我们通过{}直接在Script组件编写代码。

import Script from "next/script";
export default function RootLayout({
  children
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <head>
        <Script id="VGUBHJMK5" strategy="beforeInteractive" src="https://unpkg.com/vue@3/dist/vue.global.js"></Script>
      </head>
      <body>
        {children}
        <div id="app"></div>
        <Script id="VGUBHJMK6"
         strategy="afterInteractive">
        {
           `
            const {createApp} = Vue
            createApp({
              template: '<h1>{{ message }}</h1>',
              setup() {
                return {
                  message: 'Next.js + Vue.js'
                }
              }
            }).mount('#app')
          `
        }
        </Script>
      </body>
    </html>
  );
}

第二种写法使用 dangerouslySetInnerHTML 属性来设置内联脚本。

<Script dangerouslySetInnerHTML={{__html: `
    const {createApp} = Vue
    createApp({
        template: '<h1>{{ message }}</h1>',
        setup() {
        return {
            message: 'Next.js + Vue.js'
        }
        }
    }).mount('#app')
    ` }} strategy="afterInteractive">
</Script>
事件监听
  • onload: 脚本加载完成时触发。
  • onReady: 脚本加载完成后,且组件每次挂载的时候都会触发。
  • onError: 脚本加载失败时触发。

Script组件只有在导入客户端的时候才会生效,所以需要使用'use client'声明这是一个客户端组件。

'use client'
 
import Script from 'next/script'
 
export default function Page() {
  return (
    <>
      <Script
        src="https://example.com/script.js"
        onLoad={() => {
          console.log('Script has loaded')
        }}
      />
    </>
  )
}

Next.js第十六章(font字体)

作者 小满zs
2025年12月27日 05:54

font字体

next/font模块,内置了字体优化功能,其目的是防止CLS布局偏移。font模块主要分为两部分,一部分是内置的Google Fonts字体,另一部分是本地字体。

基本用法

Goggle字体

在使用google字体的时候,Google字体和css文件会在构建的时候下载到本地,可以与静态资源一起托管到服务器,所以不会向Google发送请求。

  1. 基本使用
import { BBH_Sans_Hegarty } from 'next/font/google' //引入字体库
const bbhSansHegarty = BBH_Sans_Hegarty({
  weight: '400', //字体粗细
  display: 'swap', //字体显示方式
})
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
  return (
    <html lang="en">
      <body className={bbhSansHegarty.className}> {/** bbhSansHegarty会返回一个类名,用于加载字体 */}
        {children}
        sdsadasdjsalkdjasl
        你好
      </body>
    </html>
  );
}

google.png

  1. 可变字体

可变字体是一种可以适应不同字重和样式的字体,它可以在不同的设备上自动调整字体大小和样式,以适应不同的屏幕大小和分辨率。

import { Roboto } from 'next/font/google'
const roboto = Roboto({
  weight: ['400', '700'], //字体粗细 (不是所有字体都支持可变字体)
  style: ['normal', 'italic'], //字体样式   
  subsets: ['latin'],
  display: 'swap',
})

如何选择其他字体?可以参考Google Fonts

web.png

import { Inter,BBH_Sans_Bartle,Roboto_Slab,Rubik,Montserrat } from 'next/font/google' //引入其他字体库

API 参考

配置选项

属性 Google 本地 类型 必填 说明
src String/Array 字体文件路径
weight String/Array 可选 字体粗细,如 '400'
style String/Array - 字体样式,如 'normal'
subsets Array - 字符子集
axes Array - 可变字体轴
display String - 显示策略
preload Boolean - 是否预加载
fallback Array - 备用字体
adjustFontFallback Boolean/String - 调整备用字体
variable String - CSS 变量
declarations Array - 自定义声明
style

字体样式,如 'normal' 'italic(斜体)' 'oblique(倾斜)' 等。

weight

字体粗细,如 '400' '700' '900' 等。

display

auto:浏览器默认(通常为 block)

block:空白 3s → 备用字体 → 自定义字体

swap:备用字体 → 自定义字体

fallback:空白 100ms → 备用字体,3s 内加载完成则切换

optional:空白 100ms,100ms 内加载完成则使用,否则用备用字体

本地字体

字体下载地址:免费可商用字体

本地字体需要通过src属性指定字体文件路径,字体文件路径可以是单个文件,也可以是多个文件。

import localFont from 'next/font/local'
const local = localFont({
  src:'./font/zydtt.ttf', //本地字体文件路径
  display: 'swap', //字体显示方式
})
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
  return (
    <html lang="en">
      <body className={local.className}>
        {children}
        sdsadasdjsalkdjasl
        你好
      </body>
    </html>
  );
}

local.png

不只是作品集:用 Next.js 打造我的数字作品库

2025年12月23日 09:31

前言

Hi,大家好,我是白雾茫茫丶!

很久没和大家见面了,说来惭愧,自从AI成了“全能助手”,我这个“码字工”的笔就有点锈了——感觉很多技术问题,还没等我写完文章,AI三言两语就解释清楚了。少了点输出深度内容的动力,手也就慢慢懒了。

不过最近,我又找到了一点不一样的感觉。工作上接触不到,我就自己动手——这段时间一直在折腾 Next.js + Shadcn UI 这套组合。不得不说,确实很火,用起来也很顺手,从头搭一个项目的过程,反而让我找回了那种边踩坑边学习的踏实感。

今天想和大家分享的,是我在这个过程中做出来的一个小成果:一个个人作品信息展示的模板。我觉得它设计得挺干净,结构也清晰,不管是拿来即用,还是当作学习参考,应该都还不错。如果你也在找类似的灵感或模版,不妨看看,希望能帮到你。

灵感来源

在我探索 Shadcn UI 的过程中,偶然发现了一个设计非常出色的模板:

magicui.design/docs/templa…

最初我只是把它集成在自己正在捣鼓的项目里试试水,但很快发现,这个页面本身就足够完整和优雅——即使独立出来,也完全能作为一个专业的作品展示站点。

于是,我决定把它单独抽离出来,搭建成了一个专注的作品展示项目。在保留其原设计精髓的基础上,我根据自己的偏好做了一些调整,加入了一些个性化的交互细节和动画效果,让整个界面在简洁之中多了一点灵动的气息。

最终呈现出来的,就是现在这个版本——整体保持干净利落,又不失细节处的巧思。如果你也在寻找一个轻量、现代且易于定制的作品集模板,或许这个实现能给你带来一些灵感。

为什么每个开发者都需要一个作品站点?

作为开作为开发者,我们每天都在创造价值:在 GitHub 提交代码、在掘金写技术文章、在开源社区贡献方案……

但这些成果往往散落在不同的平台,像一座座信息孤岛。面试时,我们需要反复解释这些分散的内容;求职时,简历上的短短几行描述,难以承载我们真实的技术思考与项目深度。

构建一个统一的技术身份,将分散的项目、文章、数据可视化整合在一个专业、可访问的空间。它不仅仅是一个作品集,更是:

面试的隐形加分项——当面试官通过一个精心设计的站点看到你的完整技术路径、真实的项目思考过程,这种体验远胜过千篇一律的简历

技术能力的系统证明——可视化你的 GitHub 贡献、技能雷达图、项目迭代历史,让抽象的能力变得具体可见

技术栈

- 框架:Next.js 16、React 19、TypeScript 5
- 样式:Tailwind CSS v4、tw-animate-css
- 可视化:Recharts
- 其他:ahooks、enum-plus、lucide-react

特性

- 基于 `Next.js App Router` 的现代架构
- 使用 `Tailwind CSS v4` 与自定义主题变量,支持暗色模式
- GitHub 仓库与贡献统计 API
- Halo 文章列表聚合 API
- Recharts 数据图表可视化
- 完整 SEO 文件:`robots``sitemap``manifest`
- 集成 Umami、Microsoft Clarity、Google Analytics(生产环境自动启用)

环境变量

在项目根目录创建 .env,示例:

# 站点信息
NEXT_PUBLIC_NAME="你的名字"
NEXT_PUBLIC_APP_NAME="Portfolio"
NEXT_PUBLIC_DESC="一句话简介/站点描述"
NEXT_PUBLIC_APP_DOMAIN="https://your-domain.com"
NEXT_PUBLIC_THEME="light" # 可选:light | dark | system

# 分析统计(生产环境生效)
NEXT_PUBLIC_UMAMI_ID=""
NEXT_PUBLIC_CLARITY_ID=""
NEXT_PUBLIC_GA_ID=""

# GitHub API
GITHUB_TOKEN="" # 只读 Token
NEXT_PUBLIC_GITHUB_USERNAME="your-github-username"

# Halo API
HALO_TOKEN="" # 只读 Token

效果预览

PixPin_2025-12-23_09-27-56.png

总结

这个基于 Next.js + Shadcn UI 构建的个人作品展示模板,是我在探索现代前端技术栈过程中的一次实践与沉淀。

技术本身是工具,但如何用它更好地表达自己、呈现价值,才是更有意义的探索。

如果你也在构建个人项目、整理作品集,或单纯想学习 Next.js 全栈实践,这个项目或许能给你一些参考。

在线预览:portfolio.baiwumm.com

Github:github.com/baiwumm/por…

❌
❌