likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 9

前言

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

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

布局 VS 模板

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

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

npx create-next-app@latest

运行效果如下:

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

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

新建以下目录和文件:

app               
├─ (form)         
│  ├─ about       
│  │  └─ page.js  
│  ├─ settings    
│  │  └─ page.js  
│  └─ layout.js     

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

'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代码如下:

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代码如下:

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页面,交互效果如下:

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

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

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

Next.js v14 的模板(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代码如下:

'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,开启生产版本,交互效果如下:

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

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

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

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

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

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

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

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,代码如下:

'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,开启生产版本,交互效果如下:

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

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

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

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代码为:

'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,开启生产版本,交互效果如下:

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

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

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

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

'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,添加如下代码:

.section {
  transition: 2s;
}

.fadeIn {
  opacity: 1;
}

.fadeOut {
  opacity: 0;
}

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

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

查看代码和地址:CodeSandbox Template

总结

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