NextJS学习 - 第四节 内置组件 Link & Image

1,003 阅读9分钟

4. NextJs 内置组件 Link、Image

上一节我们用了许多的例子去创建路由、渲染页面,我们在路由跳转的时候使用 NextJs 内置的一个组件,Link,接下来我们详细地看看它的使用介绍

4.1 Link组件

Link组件是 NextJs 基于我们的 HTML 中的a标签拓展的一个组件,它支持页面预加载以及在客户端路由之间的导航。如果我们项目中需要使用到路由切换,尽可能地使用该组件,毕竟还可以提高 SEO ~

我们来看看该组件接受的属性以及具体的介绍

  • href 跳转到的位置
  • replace 如果设置为 true,那只会修改 URL 但是不会新增一个路由栈
  • scroll true 会跳转到页面顶部,false 不会跳转位置
  • prefetch true 会在该组件出现在用户浏览器窗口时,去预加载链接内的内容

其余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>

但是我实际使用中将参数设置为动态的值,会报错 image

所以我们还是用动态字符串拼接的形式去跳转动态路由吧~

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

image

由此,我们就简单的实现了一个加载更多的功能,接下来我们来看看一个更重要,更复杂的组件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或者在浏览器刷新按钮处右键选择强制刷新,我们可以看到,他会很快地显示一个模糊的图片占位,等真正高清的图片加载完成后,再显示原本的图片

image

  • 使用 URL 字符串的形式引入图片,有两种形式
    • 将资源文件存储到public目录下,然后引入图片,NextJs 默认会去优化这种访问形式
    • 将资源文件上传到 OSS 上,用外部链接的形式去访问,当以这种形式访问图片有两点要注意
      • 需要将 OSS 的域名配置到next.config.js里面的image.remotePatterns里面,否则部署上线后无法显示
      • 需要配置loader属性,否则打包后会出现图片无法访问的情况

我们首先来试试使用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>
  );
}

我们来看看刷新浏览器效果(我们这里默认禁用缓存)

image

我们发现,确实 logo 和 banner 图片的效果渲染不太一样,因为这里的图片默认是懒加载的,会闪一下,我们来加一个新的属性priority,这个属性设置后,图片默认的懒加载就会失效,并且页面刷新,会优先加载该图片

再看看效果,我们发现不会闪一下了。效果很不错。

image

我们再来看看怎么怎么使用外部链接来渲染图片。其实外部的链接的图片的使用,我觉得一般是会经常变化的图片,例如,我们这里的博客里面的图片。我们在组件里新增一个图片组件:

"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;

我们来看看效果

image

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属性,如果不加,它就会一直往上找具备这三个属性的节点,没有找到就以浏览器窗口作为父节点。

image

如果我们在给图片加上style={{ objectFit: "contain" }},他将会按照你父节点的高度,去等比缩放图片,如果窗口比图片窄,就会默认减少图片的高度。也是等比缩放。有点意思~

image

如果我们在给图片加上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 已经被裁掉了:

image

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 个值,emptyblurdata:image/...

默认为empty也就是没有占位。

如果设置为blur,那么需要blurDataURL属性设置为一个占位 Base64 值设置成功后会有一个模糊的效果

当然也可以直接使用 Base64 的值,官方提供了一个工具(链接)可以将图片转化为体积极小的 Base64 的值

4.2.1.10 overrideSrc

当我们提供一个srcImage组件,他会自动生srcsrcset

<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",这个时候我们改变我们的的浏览器窗口,你会发现,它在根据浏览器窗口的宽度获取不同的尺寸,来确保该图片适应用户的设备。

image

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下面

image

在后续的请求中,如果缓存的图片没有过期,则会使用缓存的图片进行显示。过期后,会重新请求并且缓存。缓存图片的状态,我们可以从相应头里面看到,有一个属性 x-nextjs-cache,它有以下的值

  • MISS 图片未被缓存(大概率是第一次访问)
  • STALE 图片已经被缓存,但是已过期,需要在后台重新更新。
  • HIT 图片从缓存里面返回。并且在有效期内。 图片缓存的有效期,我们可以使用minimumCacheTTL配置或使用Cache-Control里的max-age,会默认取两者之间相对大的那个。如果s-maxagemax-age同时存在,优先使用s-maxage

我们还可以做以下的优化:

  • 配置minimumCacheTTL增加缓存的时间
  • 配置deviceSizesimageSizes来减少产生图片的数量
  • 配置formats属性减少多个格式化的处理

例如下面这个例子,一张图产生了各种不同的尺寸: alt text

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默认不会优化svgsvg是一个向量可以随意改变大小;其次他和 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>
  );
}

到这里,我们就完成了LinkImage的介绍啦~