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。

相关推荐
初心w50t2几秒前
Vue 前端开发性能优化攻略
前端·javascript·vue.js
{⌐■_■}几秒前
【软件工程】tob和toc含义理解
前端·数据库·mysql·golang·软件工程·tidb
码农捻旧39 分钟前
前端性能优化:从之理论到实践的破局道
前端·性能优化
3Katrina40 分钟前
前端面试之防抖节流(一)
前端·javascript·面试
kk_stoper40 分钟前
使用Ruby接入实时行情API教程
java·开发语言·javascript·数据结构·后端·python·ruby
浏览器API调用工程师_Taylor1 小时前
自动化重复任务:从手动操作到效率飞跃
前端·javascript·爬虫
赵润凤1 小时前
Vue 高级视频播放器实现指南
前端
FogLetter1 小时前
从原生JS事件到React事件机制:深入理解前端事件处理
前端·javascript·react.js
轻语呢喃1 小时前
js事件机制:监听、捕获、冒泡与委托
javascript
小公主1 小时前
如何利用闭包封装私有变量?掌握防抖、节流与 this 问题的巧妙解决方案
前端