Next.js v14 的模板(template.js)到底有啥用?

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

前言

Next.js v13 推出了基于 React Server Component 的 App Router 作为新的路由解决方案,在初学 App Router 的时候,布局和模板的使用可能会让大家感到困惑。倒不是不理解其用法,而是不明白有什么作用。

本篇就为大家详细介绍 Next.js 的模板,并举一些例子帮助大家理解应用。让我们开始吧!

布局 VS 模板

布局和模板用法基本类似,最大的区别在于状态的保持。让我们直接写个示例代码,在实际项目中体会。

使用官方脚手架,创建一个 Next.js 项目:

bash 复制代码
npx create-next-app@latest

运行效果如下:

为了样式美观,我们会用到 Tailwind CSS,所以注意勾选 Tailwind CSS,其他随意。

新建以下目录和文件:

bash 复制代码
app               
├─ (form)         
│  ├─ about       
│  │  └─ page.js  
│  ├─ settings    
│  │  └─ page.js  
│  └─ layout.js     

其中,app/(form)/layout.js代码如下:

javascript 复制代码
'use client'

import Link from "next/link";
import { useState } from "react";

export default function RootLayout({ children }) {
  const [text, setText] = useState('');

  return (
    <div className="p-5">
      <nav className="flex items-center justify-center gap-10 text-blue-600">
        <Link href="/about">About</Link>
        <Link href="/settings">Settings</Link>
      </nav>
      <label htmlFor="text" className="block text-sm font-medium leading-6 text-gray-900">
        在这里随意输入一些内容:
      </label>
      <div className="mt-2">
        <input
          id="text"
          required
          className="block w-full rounded-md border-0 px-1.5 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
          value={text} onChange={e => setText(e.target.value)}
        />
      </div>
      {children}
    </div>
  );
}

app/(form)/about/page.js代码如下:

javascript 复制代码
export default function Page() {
  return <div className="h-60 mt-5 flex-1 rounded-xl bg-indigo-500 text-white flex items-center justify-center">Hello, About!</div>
}

app/(form)/about/settings.js代码如下:

javascript 复制代码
export default function Page() {
  return <div className="h-60 mt-5 flex-1 rounded-xl bg-indigo-500 text-white flex items-center justify-center">Hello, Settings!</div>
}

命令行运行 npm run dev开启开发模式,打开 localhost:3000页面,交互效果如下:

我们在输入框中随意输入一些文字,然后点击导航栏切换,此时你会发现,输入框中的文字没有变化。这就是布局的效果,在导航的时候,状态不会改变。

现在让我们把 layout.js更名为 template.js,重新查看交互效果:

我们依然在输入框中随意输入一些文字,然后点击导航栏切换,此时你会发现,输入的文字都会被重置掉。这就是模板的效果,在导航的时候,状态不会保持。

更具体的来说,模板会在导航的时候为每个子级创建一个新实例。这就意味着当用户在共享一个模板的路由间导航的时候(比如例子中的 /about/settings 就共享 app/(form)/template.js这个模板),将挂载组件的新实例,DOM 元素会重新创建,所以状态不会保留。

模板的用途

某些情况下,模板会比布局更适合:

  • 依赖于 useEffect 和 useState 的功能,比如记录页面访问数(维持状态就不会在路由切换时记录访问数了)、用户反馈表单(每次重新填写)等
  • 更改框架的默认行为,比如布局内的 Suspense 只会在布局首次加载的时候展示一次 fallback UI,当切换页面的时候不会再展示。但是使用模板,fallback UI 会在每次切换页面的时候展示

如果你在项目中用过 template.js,对这个描述自然是理解的。但如果你没有用过,对此的理解就容易模模糊糊,所以让我们举两个例子:

1. 依赖 useEffect 和 useState 的功能

依然沿用刚才的例子,让我们把 template.js再更名回 layout.js,修改app/(form)/layout.js代码如下:

javascript 复制代码
'use client'

import Link from "next/link";
import { useEffect, useState } from "react";

export default function RootLayout({ children }) {
  const [text, setText] = useState('');

  useEffect(() => {
    console.log('count page view')
  }, [])

  return (
    <div className="p-5">
      <nav className="flex items-center justify-center gap-10 text-blue-600">
        <Link href="/about">About</Link>
        <Link href="/settings">Settings</Link>
      </nav>
      <label htmlFor="text" className="block text-sm font-medium leading-6 text-gray-900">
        在这里随意输入一些内容:
      </label>
      <div className="mt-2">
        <input
          id="text"
          required
          className="block w-full rounded-md border-0 px-1.5 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
          value={text} onChange={e => setText(e.target.value)}
        />
      </div>
      {children}
    </div>
  );
}

运行 npm run build && npm run start,开启生产版本,交互效果如下:

从上图可以看出,页面加载的时候会打印一次 count page view,但是当发生导航的时候并没有再次打印。这就是常见的单页应用的问题,路由切换的时候没有重新统计 PV。

为了能够正确统计 PV,此时就可以使用模板,将 layout.js更名为 template.js便可以正确统计:

但其实也不用这么麻烦,因为 layout.js 和 template.js 可以一起使用。当一起使用时,它们的层级关系为:

Layout 会包裹 Template,所以修改 app/(form)/layout.js代码为:

javascript 复制代码
import Link from "next/link";

export default function RootLayout({ children }) {
  return (
    <div className="p-5">
      <nav className="flex items-center justify-center gap-10 text-blue-600">
        <Link href="/about">About</Link>
        <Link href="/settings">Settings</Link>
      </nav>
      {children}
    </div>
  );
}

新建 app/(form)/template.js,代码如下:

javascript 复制代码
'use client'

import { useState, useEffect } from "react";

export default function Template({ children }) {

  const [text, setText] = useState('');

  useEffect(() => {
    console.log('count page view')
  }, [])

  return (
    <>
      <label htmlFor="text" className="block text-sm font-medium leading-6 text-gray-900">
        在这里随意输入一些内容:
      </label>
      <div className="mt-2">
        <input
          id="text"
          required
          className="block w-full rounded-md border-0 px-1.5 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
          value={text} onChange={e => setText(e.target.value)}
        />
      </div>
      {children}
    </>
  )
}

运行 npm run build && npm run start,开启生产版本,交互效果如下:

2. 更改框架的默认行为,比如 Suspense

修改 app/(form)/layout.js代码为:

javascript 复制代码
import Link from "next/link";
import { Suspense } from "react";

export const dynamic = 'force-dynamic'

function Loading() {
  return <div className="h-10 mt-5 mb-2 flex-1 rounded-xl bg-sky-500 text-white flex items-center justify-center">Loading</div>
}

const sleep = ms => new Promise(r => setTimeout(r, ms));

async function CustomComponent() {
  await sleep(1000)
  return <div className="h-10 mt-5 mb-2 flex-1 rounded-xl bg-indigo-500 text-white flex items-center justify-center">Hello, Layout!</div>
}

export default function RootLayout({ children }) {
  return (
    <div className="p-5">
      <nav className="flex items-center justify-center gap-10 text-blue-600">
        <Link href="/about">About</Link>
        <Link href="/settings">Settings</Link>
      </nav>
      <Suspense fallback={<Loading />}>
        <CustomComponent />
      </Suspense>
      {children}
    </div>
  );
}

修改 app/(form)/template.js代码为:

javascript 复制代码
'use client'

import { Suspense } from "react";
import { useState, useEffect } from "react";


function Loading() {
  return <div className="h-10 mt-5 mb-2 flex-1 rounded-xl bg-sky-500 text-white flex items-center justify-center">Loading</div>
}

const sleep = ms => new Promise(r => setTimeout(r, ms));

async function CustomComponent() {
  await sleep(1000)
  return <div className="h-10 mt-5 mb-2 flex-1 rounded-xl bg-indigo-500 text-white flex items-center justify-center">Hello, Template!</div>
}

export default function Template({ children }) {

  const [text, setText] = useState('');

  useEffect(() => {
    console.log('count page view')
  }, [])

  return (
    <div>
      <Suspense fallback={<Loading />}>
        <CustomComponent />
      </Suspense>
      <label htmlFor="text" className="block text-sm font-medium leading-6 text-gray-900">
        在这里随意输入一些内容:
      </label>
      <div className="mt-2">
        <input
          id="text"
          required
          className="block w-full rounded-md border-0 px-1.5 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
          value={text} onChange={e => setText(e.target.value)}
        />
      </div>
      {children}
    </div>
  )
}

运行 npm run build && npm run start,开启生产版本,交互效果如下:

在布局中使用 Suspense,组件在导航的时候不会发生改变。而在模板中使用 Suspense,组件在导航的时候每次都会触发 Loading 效果。

除了使用 Suspense 的时候,比如导航的时候添加动画效果也是可以的。

修改 app/(form)/template.js代码为:

jsx 复制代码
'use client'

import { Suspense } from "react";
import { useState, useEffect } from "react";

function Loading() {
  return <div className="h-10 mt-5 mb-2 flex-1 rounded-xl bg-sky-500 text-white flex items-center justify-center">Loading</div>
}

const sleep = ms => new Promise(r => setTimeout(r, ms));

async function CustomComponent() {
  await sleep(1000)
  return <div className="h-10 mt-5 mb-2 flex-1 rounded-xl bg-indigo-500 text-white flex items-center justify-center">Hello, Template!</div>
}

export default function Template({ children }) {

  const [text, setText] = useState('');

  const [animation, setAnimation] = useState('fadeOut');

  useEffect(() => {
    console.log('count page view')
    setAnimation("fadeIn")
  }, [])

  return (
    <div>
      <Suspense fallback={<Loading />}>
        <CustomComponent />
      </Suspense>
      <label htmlFor="text" className="block text-sm font-medium leading-6 text-gray-900">
        在这里随意输入一些内容:
      </label>
      <div className="mt-2">
        <input
          id="text"
          required
          className="block w-full rounded-md border-0 px-1.5 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
          value={text} onChange={e => setText(e.target.value)}
          />
      </div>
      <div className={`section ${animation}`}>
        {children}
      </div>
    </div>
  )
}

做动画需要修改样式,打开 app/globals.css,添加如下代码:

css 复制代码
.section {
  transition: 2s;
}

.fadeIn {
  opacity: 1;
}

.fadeOut {
  opacity: 0;
}

运行 npm run build && npm run start,开启生产版本,交互效果如下:

查看代码和地址:CodeSandbox Template

总结

简单的来说,如果你需要在导航(路由切换)的时候做一些事情如发送统计代码、重新加载、添加动画效果等等,那就可以考虑使用 template.js。

相关推荐
崔庆才丨静觅3 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60614 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了4 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅4 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅4 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅5 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment5 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅5 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊5 小时前
jwt介绍
前端
爱敲代码的小鱼5 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax