4. NextJs 内置组件 Link、Image
上一节我们用了许多的例子去创建路由、渲染页面,我们在路由跳转的时候使用 NextJs 内置的一个组件,Link,接下来我们详细地看看它的使用介绍
4.1 Link组件
Link组件是 NextJs 基于我们的 HTML 中的a标签拓展的一个组件,它支持页面预加载以及在客户端路由之间的导航。如果我们项目中需要使用到路由切换,尽可能地使用该组件,毕竟还可以提高 SEO ~
我们来看看该组件接受的属性以及具体的介绍
href跳转到的位置replace如果设置为 true,那只会修改 URL 但是不会新增一个路由栈scrolltrue会跳转到页面顶部,false不会跳转位置prefetchtrue会在该组件出现在用户浏览器窗口时,去预加载链接内的内容
其余a标签的属性,我们可以传给Link,它会默认透出传给a标签
4.1.1 href
该属性可以传一个 URL 字符串或者传一个对象,例如
<Link href="/blog">博客</Link>
当我们需要导航到动态路由或者路由上携带参数,我们可以用一个对象的形式,例如
// 导航到 /blog/1
<Link href={{
pathname: '/blog/[blogId]',
query: { blogId: '1' }
}}>
博客详情
</Link>
// 导航到 /blog?search=NextJs
<Link href={{
pathname: '/blog',
query: { search: 'NextJs' }
}}>
搜索博客
</Link>
但是我实际使用中将参数设置为动态的值,会报错
所以我们还是用动态字符串拼接的形式去跳转动态路由吧~
4.1.2 scroll 属性
这个属性我觉得是很有用的,NextJs 默认将它设置为 true,意味着我们前往一个新的路由,会跳转到页面的顶部。但是我们什么情况会用到scroll=false呢?
我来举个例子,当我的博客列表数据太大需要分页时,这个时候我们新增一个加载更多按钮去请求数据,我要通过路由跳转的方式去请求数据,如果scroll=true,那么我们请求数据就会跳到顶部去,交互太差啦~
我们来实现这个用例改造一下/(daily)/blog/page.tsx路由内容
import Link from "next/link";
const BLOGS = [...new Array(100)].map((_, index) => index);
const MORE_BLOGS = [...new Array(200)].map((_, index) => index);
const Blog = ({ searchParams }: { searchParams: { more: true } }) => {
const blogs = searchParams.more ? MORE_BLOGS : BLOGS;
return (
<div className="p-4">
<h1>我的博客界面</h1>
<ul>
{blogs.map((b) => (
<li key={b}>
<Link href={`/blog/${b}`}>博客{b}</Link>
</li>
))}
</ul>
<Link href={`/blog?more=true`} scroll={false}>
<button>加载更多</button>
</Link>
</div>
);
};
export default Blog;
这里我们新增了一个加载更多 Link 按钮,并且页面props里面我接受了searchParams.more这样的一个参数,当路由带有这个参数,我就加载200 条数据,否则我就加载100 条数据。初始化当我们只有 100 条数据的时候,我们滚动到页面底部,点击加载更多按钮,将剩余的 100 条也加载出来了,但是我们浏览的窗口位置没有改变
由此,我们就简单的实现了一个加载更多的功能,接下来我们来看看一个更重要,更复杂的组件Image
4.2 Image组件
在前端领域,图片如何显示是一个非常重要的话题,我们是应该直接显示?还是懒加载?还是预加载?还有怎么处理图片的适配、体积等等都是老生常谈。
在 NextJs 中,内置了Image组件为我们做了非常多的处理优化。接下来,我们来看看怎么去使用它。
4.2.1 Props
我们先来看看该组件的接受的属性
| 属性 | 作用 | 是否必传 |
|---|---|---|
src | 图片路径 | true |
width | 图片宽度 | true |
height | 图片高度 | true |
alt | 图片描述信息 | true |
loader | 图片渲染前接受的回调函数,处理图片路径等 | - |
sizes | 根据屏幕处理不同图片尺寸 | - |
quality | 图片质量 | - |
placeholder | 在图片加载完成前展位 | - |
onLoad | 加载完成回调 | - |
onError | 加载出错回调 | - |
loading | 加载模式 | - |
blurDataURL | 配合placeholder使用 | - |
overrideSrc | 覆盖src属性 | - |
我们先来看看必传的属性
4.2.1.1 src
该组件的src属性我们有两种传入方法
- 静态引入
比如:
import icon from './icon.png'- 好处:Next 在打包时,会给图片生成一个极小体积的图片占位,等原图片加载完成后,再进行替换
- 坏处:增加打包时间,包体积也会变大
我们用个例子来实现,在首页,我们在网上找一个 banner 图片,在
src目录下新建一个assets文件夹存放资源文件,放入我们的 banner 图片,然后我们在/(home)/page.tsx中静态引入图片
import BannerImg from "@/assets/image/banner.jpg";
import Image from "next/image";
export default function Home() {
return (
<main>
<Image src={BannerImg} alt="首页背景图" placeholder="blur" />
</main>
);
}
然后我们打开控制台,将浏览器disabled cache或者在浏览器刷新按钮处右键选择强制刷新,我们可以看到,他会很快地显示一个模糊的图片占位,等真正高清的图片加载完成后,再显示原本的图片
- 使用 URL 字符串的形式引入图片,有两种形式
- 将资源文件存储到
public目录下,然后引入图片,NextJs 默认会去优化这种访问形式 - 将资源文件上传到 OSS 上,用外部链接的形式去访问,当以这种形式访问图片有两点要注意
- 需要将 OSS 的域名配置到
next.config.js里面的image.remotePatterns里面,否则部署上线后无法显示 - 需要配置
loader属性,否则打包后会出现图片无法访问的情况
- 需要将 OSS 的域名配置到
- 将资源文件存储到
我们首先来试试使用public目录存储图片的形式去访问图片,我们找一个 Logo 去显示到我们的首页顶部
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "@/app/globals.css";
import React from "react";
import Link from "next/link";
import Image from "next/image";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
favorite,
daily,
auth,
}: Readonly<{
children: React.ReactNode;
favorite: React.ReactNode;
daily: React.ReactNode;
auth: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<nav className="w-full h-30 flex justify-between items-center box-border p-8">
<div>其他</div>
<div className="h-14">
<Image
src="/logo.png"
width={400}
height={160}
alt="LOGO"
style={{ objectFit: "contain", height: "100%" }}
/>
</div>
<Link href="/login">点击登录</Link>
</nav>
我是Home Root布局:
{children}
{daily}
{favorite}
{auth}
</body>
</html>
);
}
我们来看看刷新浏览器效果(我们这里默认禁用缓存)
我们发现,确实 logo 和 banner 图片的效果渲染不太一样,因为这里的图片默认是懒加载的,会闪一下,我们来加一个新的属性priority,这个属性设置后,图片默认的懒加载就会失效,并且页面刷新,会优先加载该图片
再看看效果,我们发现不会闪一下了。效果很不错。
我们再来看看怎么怎么使用外部链接来渲染图片。其实外部的链接的图片的使用,我觉得一般是会经常变化的图片,例如,我们这里的博客里面的图片。我们在组件里新增一个图片组件:
"use client";
import Img, { ImageProps, ImageLoaderProps } from "next/image";
const imageLoader = ({ src, width, quality }: ImageLoaderProps) => {
return `${src}?w=${width}&q=${quality || 75}`;
};
export default function Image(props: ImageProps) {
return <Img {...props} loader={imageLoader} />;
}
然后我们找一张博客图片存放到自己的 OSS 上面,用这个 URL https://imgs-1257212764.cos.ap-chengdu.myqcloud.com/nextjs-app-router-template/doc-images/6294.png,我们直接进入/(daily)/blog/[blogId]/page.tsx页面修改
import React, { memo } from "react";
import { Image } from "@/components";
const BlogDetail = (props: { params: { blogId: string } }) => {
return (
<div>
博客ID: {props.params.blogId}
<div className="relative w-full h-80">
<Image
src="https://imgs-1257212764.cos.ap-chengdu.myqcloud.com/nextjs-app-router-template/doc-images/6294.png"
fill
alt="博客图片"
style={{ objectFit: "contain" }}
/>
</div>
</div>
);
};
export default BlogDetail;
我们来看看效果
4.2.1.2 width & height
当我们不使用静态引入文件时(打包时会自动生成)或者使用fill属性时,我们必须传入宽高,Nextjs 会默认使用你传入宽高去占位置,避免页面发生偏移,影响视觉体验。
4.2.1.3 alt
alt 这个属性也是必须传入的,为了避免图片加载失败的情况,没有显示的内容,当然它也有利于搜索引擎去见检索到该页面。
接下来我们看看非必传的属性
4.2.1.4 loader
我们刚刚在引入外部图片的时候使用到了 loader 属性,再来看看
const imageLoader = ({ src, width, quality }: ImageLoaderProps) => {
return `${src}?w=${width}&q=${quality || 75}`;
};
这个函数接受三个属性src, width, quality,配置它的目的就是使用第三方的 CDN 进行图片的加速。
当然我们也可以直接在next.config.js里面直接配置,作用于全局的图片,例如
module.exports = {
images: {
loader: "custom",
loaderFile: "./my/image/loader.js",
},
};
4.2.1.5 fill
当我们的图片大小不确定时,这个时候我们可以使用fill属性,但是一定记得在父节点加上fixed,relative,absolute属性,因为Image组件默认会有absolute属性,如果不加,它就会一直往上找具备这三个属性的节点,没有找到就以浏览器窗口作为父节点。
如果我们在给图片加上style={{ objectFit: "contain" }},他将会按照你父节点的高度,去等比缩放图片,如果窗口比图片窄,就会默认减少图片的高度。也是等比缩放。有点意思~
如果我们在给图片加上style={{ objectFit: "cover" }},那么它将会铺满,然后将图片裁切掉。我们直接在首页的图片进行改造:
import BannerImg from "@/assets/image/banner.jpg";
import Image from "next/image";
export default function Home() {
return (
<main>
<div className="relative h-[calc(100vh-120px)]">
<Image
src={BannerImg}
alt="首页背景图"
placeholder="blur"
fill
style={{ objectFit: "cover" }}
/>
</div>
</main>
);
}
来看看效果,此时 Banner 已经被裁掉了:
4.2.1.6 sizes
我们直接看看官方的例子:
import Image from "next/image";
export default function Page() {
return (
<div className="grid-element">
<Image
fill
src="/example.png"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
</div>
);
}
sizes属性的目的其实就是,根据不同的设备尺寸,去用不同尺寸的图片。比如,当我的设备宽度大于 1200px,这个时候我一行其实只用显示 3 张图(每一张宽度大概 400px),我就没有必要使用宽度大于 1200px 的图片,这样图片将是合理尺寸的 9 倍大,会严重影响性能。
4.2.1.7 quality
图片质量。值为 1-100,越大表示图片体积越大,也越清晰。
4.2.1.8 priority
当设置为 true,该图片会被视作高优先级并且会优先预加载
4.2.1.9 placeholder
该属性一共有 3 个值,empty,blur,data:image/...
默认为empty也就是没有占位。
如果设置为blur,那么需要blurDataURL属性设置为一个占位 Base64 值设置成功后会有一个模糊的效果
当然也可以直接使用 Base64 的值,官方提供了一个工具(链接)可以将图片转化为体积极小的 Base64 的值
4.2.1.10 overrideSrc
当我们提供一个src给Image组件,他会自动生src和srcset,
<img
srcset="
/_next/image?url=%2Fme.jpg&w=640&q=75 1x,
/_next/image?url=%2Fme.jpg&w=828&q=75 2x
"
src="/_next/image?url=%2Fme.jpg&w=828&q=75"
/>
但是有时候我们是不需要src给我们重新生成的,这里我们使用overrideSrc给生成的src给覆盖掉。
<Image src="/me.jpg" overrideSrc="/override.jpg" />
这样就可以了
<img
srcset="
/_next/image?url=%2Fme.jpg&w=640&q=75 1x,
/_next/image?url=%2Fme.jpg&w=828&q=75 2x
"
src="/override.jpg"
/>
4.2.2 Config
4.2.2.1 deviceSizes
根据用户不同的设备大小,来提供不同尺寸的图片,需要和sizes搭配使用。比如,我们给首页的 Banner 图片添加,sizes="100vw",这个时候我们改变我们的的浏览器窗口,你会发现,它在根据浏览器窗口的宽度获取不同的尺寸,来确保该图片适应用户的设备。
NextJs 帮我们内置了deviceSizes的断点,我们一般不需要过多配置,如下
module.exports = {
images: {
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
},
};
4.2.2.2 imageSizes
imageSizes是配合deviceSizes生成srcset的,它的最大值是deviceSizes的最小值,NextJs 帮我们内置为
module.exports = {
images: {
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
};
4.2.2.3 formats
formats允许我们定义不同的图片格式,当浏览器发起请求时,会在Accept携带浏览器信息,NextJs 会判断formats里面的格式是否在浏览器中支持,如果支持就会修改该图片的格式显示。如果没有匹配到,则会用图片原始的格式显示。
NextJs 内置配置为:
module.exports = {
images: {
formats: ["image/webp"],
},
};
⚠️ 注意注意:
如果是使用自己的 OSS 存储图片,则需要去Proxy里面配置Accept头
4.2.3 缓存策略
Image使用的缓存策略,只针对于内置的loader,如果使用其他的loader,需要开发者自己去看 OSS 文档配置。
所有使用了默认的loader的图片在第一次请求时,会被优化处理并且缓存到.next/cache/images下面
在后续的请求中,如果缓存的图片没有过期,则会使用缓存的图片进行显示。过期后,会重新请求并且缓存。缓存图片的状态,我们可以从相应头里面看到,有一个属性 x-nextjs-cache,它有以下的值
MISS图片未被缓存(大概率是第一次访问)STALE图片已经被缓存,但是已过期,需要在后台重新更新。HIT图片从缓存里面返回。并且在有效期内。 图片缓存的有效期,我们可以使用minimumCacheTTL配置或使用Cache-Control里的max-age,会默认取两者之间相对大的那个。如果s-maxage和max-age同时存在,优先使用s-maxage
我们还可以做以下的优化:
- 配置
minimumCacheTTL增加缓存的时间 - 配置
deviceSizes和imageSizes来减少产生图片的数量 - 配置
formats属性减少多个格式化的处理
例如下面这个例子,一张图产生了各种不同的尺寸:
4.2.3.1 minimumCacheTTL 缓存图片过期时间
我们可以通过在next.config.js配置minimumCacheTTL用于控制图片缓存的时间
module.exports = {
images: {
minimumCacheTTL: 60,
},
};
如果我们的图片是静态引入的,它将图片内容作为哈希并且永久缓存图片。
我们没有办法手动让缓存失效,如果项目需要,就只有将过期时间调小点,否则需要自己去删除缓存文件或者修改图片的src来调整缓存。
4.2.3.2 disableStaticImages 禁用静态引入
如果项目中需要禁止静态引入图片,可以配置
module.exports = {
images: {
disableStaticImages: true,
},
};
4.2.3.3 dangerouslyAllowSVG 允许 SVG 作为图片
Image默认不会优化svg,svg是一个向量可以随意改变大小;其次他和 HTML/CSS 有着很多相同的特性,容易被外部攻击。所以我们推荐使用unoptimized属性设置true。
当然,如果我们需要使用,我们可以设置dangerouslyAllowSVG
module.exports = {
images: {
dangerouslyAllowSVG: true,
contentDispositionType: "attachment",
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
},
};
设置contentDispositionType避免浏览器下载图片,设置contentSecurityPolicy避免脚本被植入。
4.2.4 图片响应式
接下来我们讲讲三种图片的响应式显示
4.2.4.1 静态引入图片
如果是静态引入图片,我们可以直接设置sizes='100vw'就 OK 了,它会按图片等比撑全屏
import Image from "next/image";
import me from "./me.jpg";
export default function Author() {
return (
<Image
src={me}
alt="Picture of the author"
sizes="100vw"
style={{
width: "100%",
height: "auto",
}}
/>
);
}
4.2.4.2 带宽高比的响应式图片
如果图片是动态的或者是一个远程的链接,我们可以通过传入宽高,来设置图片正确的比列
import Image from "next/image";
export default function Page({ photoUrl }) {
return (
<Image
src={photoUrl}
alt="Picture of the author"
sizes="100vw"
style={{
width: "100%",
height: "auto",
}}
width={500}
height={300}
/>
);
}
4.2.4.3 使用fill属性来实现响应式图片
如果我们不知道图片的宽高比,我们可以使用fill属性来使图片响应式
import Image from "next/image";
export default function Page({ photoUrl }) {
return (
<div className="relative h-[300px] w-[500px]">
<Image
src={photoUrl}
alt="Picture of the author"
sizes="300px"
fill
style={{
objectFit: "contain",
}}
/>
</div>
);
}
到这里,我们就完成了Link和Image的介绍啦~