likes
comments
collection
share

Next.js 为什么使用metadata取代了Head组件?

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

前言

Next.js 13 在 13.2 版本中引入了一种新的元数据(metadata)API,目的是为了使用它来替代 next/head 的功能,并扩展新的能力,本篇文章将介绍 metadata 的运用及原理,以及为何要使用它来取代 next/head

对 Next.js app 模式不了解的可以去看一下我写的前一篇文章:Next.js 13 的 app 目录模式功能梳理

回顾一下 head 标签

在运用之前,我们再来回顾一下 HTML head 标签内有哪些标签,以及这些标签的含义:

  • <title> 定义了页面的标题
  • <base> 为页面上的所有的相对链接规定默认 URL 和默认跳转目标的方式。
  • <link> 定义了一个文档和外部资源之间的关系(比如样式、preload script/font/css/...、prefetch script/font/css/... 等)
  • <meta> 定义了HTML文档中的元数据(比如)
  • <script> 定义了客户端的脚本文件
  • <style> 定义了HTML文档的样式文件

其中可以加载外部资源的就 linkscript,扩展性最强的是 meta,这也是 metadata 的重点。

Head组件的问题

next/head 本身就导出了一个 Head 组件,使用方式和 HTML head 标签类似,使用方式如下:

import Head from "next/head";

const Page = () => {
  return (
    <>
      <Head>
        <meta charSet="utf-8" />
        <title>页面标题</title>
        <meta name="description" content="next.js" />
        <link rel="icon" href="favicon.ico" type="image/x-icon" />
        <base href="http://xxxx.com/" target="_blank" />
      </Head>
      <div>page-head</div>
    </>
  )
}

export default Page

上面的例子属于页面级,如果要写应用级(全局)的 head 标签,那么需要在 _app.js 中使用 <Head>,且如果要 scriptlink 引入外部的 js 或者 css,那么还必须在 _docuemnt.js 中去使用 next/document 导出的 Head 标签才行。

import { Html, Head, Main, NextScript } from "next/document";

function Document() {
  return (
    <Html>
      <Head>
        <link rel="stylesheet" href="http://xxxx.com/xxx.css" />
        <script src="http://xxxx.com/xxx.js"></script>
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

export default Document;

这样会造成如果需要引入远程的css或者js文件,就需要重写 _docuemnt.js

还有一个问题就是,Next.js 本身具有服务端渲染的能力,Head 组件会在客户端和服务端都进行渲染,导致Head组件内的代码会重复渲染(这个更主要的是因为 Next.js pages 目录的渲染方式导致的问题)。

next/head 的 Head 组件可以强制使用 link 和 script,但使用时,next.js 没有去处理,会导致服务的渲染后,客户端会重新渲染一次,link 和 script就会出现两次

Next.js 为什么使用metadata取代了Head组件?

metadata API

从字面上,感觉 metadata<meta> 关系很大,但 metadata 把除了 scriptstyle 标签以外的其他标签的所有功能都集成进来了,可以让开发者写的时候更加方便和快捷,定制了一套全新的规范。

metadata API 主要分为三个方向来处理 head 中的内容定义:

  • 导出 metadata 数据方式
  • 基于文件的元数据
  • 支持直接写 head 内部标签,而不使用 Head 组件

另外 不管是metadata API 还是 next/head,它们都会默认给 head 添加两个 meta 元素:

<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width" /> // Head 组件
<meta name="viewport" content="width=device-width, initial-scale=1" /> // metadata API

导出 metadata 数据方式

我们可以在 LayoutPage 组件的 js 文件中导出 metadata 对象来定义元数据:

// layout.tsx 或者 page.tsx
import { Metadata } from "next";

export const metadata: Metadata = {
  title: "Next App",
}

// 生成的html:
// <head>
//   <title>Next App</title>
// </head>

也可以异步生成 metadata :

export async function generateMetadata({ params, searchParams }): Metadata {
  const data = await getDetail(params.slug);
  return { title: data.title };
}
// 生成的html:
// <head>
//   <title>xxx</title>
// </head>

generateMetadata 的参数在官方文档上并没有直接说明是什么,但比较好推测,也可以去源码进行验证:

Next.js 为什么使用metadata取代了Head组件?

params 就是路由参数,路由为 /app/**/[slug]/page.js 中的路由参数就是 { slug: 'xxx' } ,具体可以去官方文档看看。

Next.js 为什么使用metadata取代了Head组件?

searchParams 就是 location.search 解析成的键值对,http://localtion:3000?id=123searchParams{ id: 123 }

metadata 中的数据说明官方文档有详细解释,ts 类型中也有详细的注释说明,我这里再进行一个简单的分类和说明:

{
  // <title>
  title?: null | string | TemplateString // TemplateString 表示支持
  
  // 与 <link> 相关字段
  metadataBase: null | URL // 不是 <base> 标签,但图片等本地资源的域名会替换为 metadataBase,类似 next 旧模式 next.config.js 中的 `assetPrefix` ,且应用更全面
  alternates?: null | AlternateURLs // rel="alternate"
  icons?: null | IconURL | Array<Icon> | Icons // rel="icon"
  manifest?: null | string | URL // rel="manifest"
  archives?: null | string | Array<string> // rel="archives"
  assets?: null | string | Array<string> // rel="assets"
  bookmarks?: null | string | Array<string> // rel="bookmarks"
  
  // 与 <meta> 相关的通用字段
  description: "Generated by create next app"
  viewport: "width=device-width, initial-scale=1" // 会自动添加
  applicationName: null | string
  generator: null | string
  keywords?: null | string | Array<string>
  referrer?: null | ReferrerEnum
  themeColor?: null | string | ThemeColorDescriptor | ThemeColorDescriptor[]
  colorScheme?: null | ColorSchemeEnum
  creator?: null | string
  publisher?: null | string
  robots?: null | string | Robots
  formatDetection?: null | FormatDetection
  itunes?: null | ItunesApp
  abstract?: null | string
  appLinks?: null | AppLinks
  category?: null | string
  classification?: null | string
  
  // <meta> 文档的通用验证标记
  verification?: Verification
  
  // <meta> 与app抓取信息相关
  openGraph?: null | OpenGraph // <meta property={`og:${key}`} content={value} />
  twitter?: null | Twitter // <meta property={`twitter:${key}`} content={value} />
  appleWebApp?: null | boolean | AppleWebApp // <meta name={`apple-mobile-web-app-${key}`} content={value} />
  
  // <meta> 扩展
  other: {}
  
  // <link> 和 <meta> 的组合相关字段
  authors: null | Author | Array<Author>,
}

<link> 主要的属性就是 relhref,因此,metadata 中的link类属性 key 一般表示 rel,值表示href,但不一定和标准完全一样,metadataBase 这个就不属于规范,其他的除了 alternatesicons 基本都可以对应。

<meta><link> 规则类似,且都是 <meta name="xxx" content="xxx" /> 这样的格式,因为其本身可以扩展 name ,因此这里新增了 other 字段,可以根据应用的需要进行自定义扩展 name,比如QQ应用中的强制竖屏:<meta name=”x5-orientation” content=”portrait”>

这部分就说到这里,真正需要用到的时候还是依赖类型提示,或者去翻阅文档。

基于文件的元数据

除了基于配置的 metadata 外,metadata API 现在还支持新的文件命名约定,让开发者可以更方便地定义页面上的内容,也可以让这些资源去改进 SEO 和在网络上共享,这些约定的文件如下:

  • opengraph-image.(jpg|png|svg) 定义 <meta name="og:image" />
  • twitter-image.(jpg|png|svg) 定义 <meta name="twitter:image" />
  • favicon.ico 定义 favicon
  • icon.(ico|jpg|png|svg) 定义 favicon
  • sitemap.(xml|js|jsx|ts|tsx) SEO 配置文件,只有这个不支持 metadata 直接配置的方式。
  • robots.(txt|js|jsx|ts|tsx) 定义 <meta name="robots" />
  • manifest.(json|js|jsx|ts|tsx) 定义 <link ref="manifest" />

这些文件和 layout.js 组件一样,父页面的可以被子页面复用。

比如,应用目录如下:

./app
├── features
│   ├── metadata
│   │   └── page.tsx
│   ├── opengraph-image.png
│   └── template.tsx
├── layout.tsx
└── page.tsx

打开 / 页面就只有 / 目录下的约定文件:

<link rel="icon" href="/icon.png?fbfe4cb2512858df" type="image/png" sizes="48x48">

打开 /features/metadata head 标签内会继承 /features/ 目录下的约定文件:

<link rel="icon" href="/icon.png?fbfe4cb2512858df" type="image/png" sizes="48x48">
<meta property="og:image:type" content="image/png"/>
<meta property="og:image:width" content="300"/>
<meta property="og:image:height" content="168"/>
<meta property="og:image" content="http://localhost:3000/features/opengraph-image.png?bf99532d5e3da57e"/>

Next.js 在生产环境中会自动为这些文件的文件名进行哈希编译,以便于缓存。

sitemaprobotsmanifest 的配置还可以是动态的,因为在某些情况下您可能需要动态创建文件,比如动态路由的时候,这时候就可以使用 (.js|.jsx|.ts|.tsx) 编写代码来生成文件。

例如,虽然可以添加静态 sitemap.xml 文件,但大多数网站都有一些页面是使用外部数据源动态生成的。这时候就可以加一个 sitemap.js 来对动态路由的每个路由页面返回对应的 sitemap 文件。

// app/sitemap.js
// 这部分主要参考 [next.js 13.3](https://nextjs.org/blog/next-13-3) 中的案例

export default async function sitemap() {
  // 远程获取博客列表
  const res = await fetch('https://.../posts');
  const allPosts = await res.json();
  
  // 转换为博客的 sitemap
  const posts = allPosts.map((post) => ({
    url: `https://xxx.com/blog/${post.id}`,
    lastModified: post.publishedAt,
  }));
  
  // 加入本地的其他路由页面
  const routes = ['', '/about', '/blog'].map((route) => ({
    url: `https://xxx.com${route}`,
    lastModified: new Date().toISOString(),
  }));

  // 组合成最终的全面的 sitemap
  return [...routes, ...posts];
}

支持直接写 head 内部标签

generateMetadata 只支持返回 metadata 数据,generateMetadata 中请求的接口还有页面需要显示的数据,那不是得再请求一次?

这时候可以直接在 Page 组件中写 Html head 中的那些标签:

import Script from 'next/script'

const getPageInfo = async function getPageInfo() {
  const { data } = await fetch(`/api/xxx`).then(res => res.json());
  return data;
}

async function Page() {
  const data = await getPageInfo();
  return (
    <>
      <title>{data?.title}</title>
      <meta name='description' content={data?.desc} />
      <link  href="https://cdn.bootcdn.net/ajax/libs/animate.css/4.1.1/animate.min.css"></link>
      <Script src="https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.21/lodash.core.js" />
      <div>{data?.content}</div>
    </>
  );
}

export default Page;

这样看起来是不是方便了很多,不需要引入 Head 组件,且还可以直接使用 link 来加载远端的样式文件。

这里几个注意事项:

  • 全局定义了 title,这里再这样使用,title标签可能会出现两次,但是对页面上的显示是在预期中的(显示页面级别的 title)。
  • meta/base 标签会被提取到 head 中
  • link 会在 head 中进行 link preload 加载,但实际使用还是在页面定义的地方。
  • Script 会在 head 中进行 link preload 加载,但实际使用会在 body 底部进行加载。

metadata API 不支持一些 head 中的标签,都可以使用这种方式进行配置,截取了官方文档的一张图:

Next.js 为什么使用metadata取代了Head组件?

具体可以点击到官方文档看看具体描述。

最后

本篇文章比较全面的对比了一下 metadata API 和 旧版 Head 组件,metadata API 功能覆盖了 Head 组件的功能,且功能更加强大,也让 Next.js 的应用能力上了一个台阶,可以更方便的应对更加复杂的项目。

参考文章:

欢迎👏大家关注➕点赞👍➕收藏✨支持一下,有问题欢迎评论区提出,感谢纠错!

本文正在参加「金石计划」