yliu

时来天地皆同力,运去英雄不自由

Next.js 构建博客之常见问题处理


image.png

  1. Next.js 构建博客之资源抓取
  2. Next.js 构建博客之博客搭建
  3. Next.js 构建博客之打包 SSG
  4. Next.js 构建博客之常见问题处理
  5. Next.js 构建博客之功能拓展
  6. Next.js 构建博客之自动构建

这是 Next.js 构建博客的第四篇文章,上一篇文章 Next.js 构建博客之打包 SSG 介绍了 Next.js 如何打包成 SSG 文件,这篇文章重点介绍一下在开发中容易遇到的问题。

如果你想看已经部署博客的地址可以点击查看,代码仓库地址点击查看

图片盗链

有一些网站会有图片防盗处理,例如掘金,为了减少网站的压力,在其他网站访问资源的时候会直接 403,判断原理是在 http 请求中会有 referer 和 host 参数,当参数不一致就认定为非法。

example

绕过这个的方式也很简单,就是对 referer 进行修改,默认值是 strict-origin-when-cross-origin。

对于同源的请求,发送来源、路径以及查询字符串。对于在相同安全级别的情况下(HTTPS→HTTPS)的跨源请求,仅发送来源。在目标的安全级别下降的情况下(HTTPS→HTTP)则不发送 Referer 标头。

那么直接修改 referer 为 no-referrer,整个 Referer 首部会被移除。访问来源信息不随着请求一起发送。

下面是一个具体的实现代码,对图片加载失败进行拦截处理。

"use client";

import { useEffect } from "react";

const map = new WeakMap();

// 拦截图片错误,并且正确加载
export default function AssetsWatch() {
  const replace = (dom: HTMLImageElement) => {
    if (map.get(dom)) {
      return;
    }
    const src = dom.src;
    map.set(dom, 1);
    dom.src = `${process.env.NEXT_PUBLIC_BASE_PATH}/error.svg`;
    fetch(src, {
      mode: "cors",
      referrerPolicy: "no-referrer",
    })
      .then((response) => {
        if (response.ok) {
          return response.blob();
        }

        throw new Error("Image request failed");
      })
      .then((blob) => {
        const imageUrlObject = URL.createObjectURL(blob);
        dom.src = imageUrlObject;
      })
      .catch((error) => {
        dom.alt = `图片加载失败`;
        dom.title = `图片加载失败,已回滚到默认图片`;
        dom.setAttribute("data-src", src);
        console.error("Error:", error.message);
      });
  };

  useEffect(() => {
    // 初始遍历一遍,因为插入时间已经很晚了
    const forEach = () => {
      Array.from(document.images).forEach((img) => {
        const dom = new Image();
        dom.src = img.src;
        dom.onerror = () => {
          replace(img);
        };
      });
    };
    const callback = (e: ErrorEvent) => {
      const dom = e.target as HTMLElement;
      if (!dom || !/img/i.test(dom.nodeName)) {
        return;
      }
      replace(dom as HTMLImageElement);
    };

    window.addEventListener("error", callback, true);
    forEach();
    return () => {
      window.removeEventListener("error", callback);
    };
  }, []);

  return null;
}

如果不太明白,可以参考我这篇文章阅读 如何优雅处理图片异常

dynamic 和 Suspense 使用场景

dynamic

dynamic 是 React.lazy 和 Suspense 结合体,一般有三种使用场景

  1. 跳过 ssr

有一些场景不需要 ssr,例如我添加一个点击量的组件或者添加一个查看图片的功能,这种情况下 ssr 没有任何帮助,这个时候就可以使用 dynamic。

import dynamicNext from "next/dynamic";

const Statistics = dynamicNext(() => import("./statistics"), { ssr: false });

return (
  <>
    <Statistics></Statistics>
  </>
);
  1. 延迟加载

通过延迟加载来减少初始渲染路线,来提高初始加载性能。例如延迟加载客户端组件或者库,在用户点击的时候才进行渲染。

page.tsx

"use client";

import { useState } from "react";
import dynamic from "next/dynamic";
const ComponentA = dynamic(() => import("../components/A"));

export default function () {
  return (
    <>
      <ComponentA />
    </>
  );
}

components/A.tsx

"use client";

import { useState } from "react";

const names = ["Tim", "Joe", "Bel", "Lee"];

export default function Page() {
  const [results, setResults] = useState();

  return (
    <div>
      <input
        type="text"
        placeholder="Search"
        onChange={async (e) => {
          const { value } = e.currentTarget;
          // Dynamically load fuse.js
          const Fuse = (await import("fuse.js")).default;
          const fuse = new Fuse(names);

          setResults(fuse.search(value));
        }}
      />
      <pre>Results: {JSON.stringify(results, null, 2)}</pre>
    </div>
  );
}
  1. 添加自定义加载组件
import dynamic from "next/dynamic";

const WithCustomLoading = dynamic(
  () => import("../components/WithCustomLoading"),
  {
    loading: () => <p>Loading...</p>,
  }
);

export default function Page() {
  return (
    <div>
      <WithCustomLoading />
    </div>
  );
}

Suspense

在一些组件中难免会使用到客户端组件,例如添加点击事件,或者使用 useState 等,这个时候就不是服务器组件了,一般要么把整个页面都变成客户端组件,但是这个会导致失去 seo 功能,另外一种则是使用 Suspense 对需要使用客户端的组件进行剥离,下面是一个示例。

import { Suspense } from "react";
import SearchBar from "./search-bar";

function SearchBarFallback() {
  return <>placeholder</>;
}

export default function Page() {
  return (
    <>
      <nav>
        <Suspense fallback={<SearchBarFallback />}>
          <SearchBar />
        </Suspense>
      </nav>
      <h1>Dashboard</h1>
    </>
  );
}

初始情况下 html 会加载 fallback 组件内容,之后水合过程将使用 SearchBar 组件。

不要使用重定向

因为博客的首页和 pages 页面其实是一个东西,所以想着 / 直接重定向到 page/1 就行,但是发现在使用过程中会有很明显白屏现象,就是 page 页面下的 loading 没有生效。

所以建议还是不要在首屏使用重定向这个方式。

不要使用 style 样式

博客的 UI 框架部分使用了 antd,在页面加载的过程中会有一个骨架屏,不过因为 antd5 的版本使用 style 来重构样式,在组件运行的时候注入 <style /> 方便定制和切换主题,导致 Next.js 使用的时候资源不会被缓存且导致骨架屏最初样式没有被加载出来。

目前 issues 有相关讨论,但是还没解决。

解决方法:

  1. 切换低版本 antd
  2. 换一个 loading 方案

最后

如果文章有书写错误地方欢迎指出。下一篇 Next.js 构建博客之功能拓展 会介绍如何给博客添加点击量以及图片放大缩小等功能。