前言
Next.js 13 在 13.2 版本中引入了一种新的元数据(metadata)API,目的是为了使用它来替代 next/head
的功能,并扩展新的能力,本篇文章将介绍 metadata
的运用及原理,以及为何要使用它来取代 next/head
。
回顾一下 head 标签
在运用之前,我们再来回顾一下 HTML head 标签内有哪些标签,以及这些标签的含义:
<title>
定义了页面的标题<base>
为页面上的所有的相对链接规定默认 URL 和默认跳转目标的方式。<link>
定义了一个文档和外部资源之间的关系(比如样式、preload script/font/css/…、prefetch script/font/css/… 等)<meta>
定义了HTML文档中的元数据(比如)<script>
定义了客户端的脚本文件<style>
定义了HTML文档的样式文件
其中可以加载外部资源的就 link
和 script
,扩展性最强的是 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>
,且如果要 script
和 link
引入外部的 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就会出现两次
metadata API
从字面上,感觉 metadata
和 <meta>
关系很大,但 metadata
把除了 script
和 style
标签以外的其他标签的所有功能都集成进来了,可以让开发者写的时候更加方便和快捷,定制了一套全新的规范。
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
数据方式
我们可以在 Layout
或 Page
组件的 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
的参数在官方文档上并没有直接说明是什么,但比较好推测,也可以去源码进行验证:
params
就是路由参数,路由为 /app/**/[slug]/page.js
中的路由参数就是 { slug: 'xxx' }
,具体可以去官方文档看看。
searchParams
就是 location.search
解析成的键值对,http://localtion:3000?id=123
中searchParams
为 { 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>
主要的属性就是 rel
和 href
,因此,metadata 中的link类属性 key 一般表示 rel
,值表示href
,但不一定和标准完全一样,metadataBase
这个就不属于规范,其他的除了 alternates
和 icons
基本都可以对应。
<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
定义 faviconicon.(ico|jpg|png|svg)
定义 faviconsitemap.(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 在生产环境中会自动为这些文件的文件名进行哈希编译,以便于缓存。
sitemap
、 robots
、 manifest
的配置还可以是动态的,因为在某些情况下您可能需要动态创建文件,比如动态路由的时候,这时候就可以使用 (.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 中的标签,都可以使用这种方式进行配置,截取了官方文档的一张图:
具体可以点击到官方文档看看具体描述。
最后
本篇文章比较全面的对比了一下 metadata API 和 旧版 Head 组件,metadata API 功能覆盖了 Head 组件的功能,且功能更加强大,也让 Next.js 的应用能力上了一个台阶,可以更方便的应对更加复杂的项目。
参考文章:
欢迎👏大家关注➕点赞👍➕收藏✨支持一下,有问题欢迎评论区提出,感谢纠错!
本文正在参加「金石计划」
原文链接:https://juejin.cn/post/7222179242956095543 作者:𝑯𝒖𝒕𝒂𝒐