这是 Next.js 构建博客的第四篇文章,上一篇文章介绍了 Next.js 如何打包成 SSG 文件,这篇文章重点介绍一下在开发中容易遇到的问题。
图片盗链
有一些网站会有图片防盗处理,例如掘金,为了减少网站的压力,在其他网站访问资源的时候会直接 403,判断原理是在 http 请求中会有 referer 和 host 参数,当参数不一致就认定为非法。

绕过这个的方式也很简单,就是对 referer 进行修改,默认值是 strict-origin-when-cross-origin。
对于同源的请求,发送来源、路径以及查询字符串。对于在相同安全级别的情况下(HTTPS→HTTPS)的跨源请求,仅发送来源。在目标的安全级别下降的情况下(HTTPS→HTTP)则不发送 Referer 标头。
那么直接修改 referer 为 no-referrer,整个 Referer 首部会被移除。访问来源信息不随着请求一起发送。
下面是一个具体的实现代码,对图片加载失败进行拦截处理。
            
            
              js
              
              
            
          
          "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 结合体,一般有三种使用场景
- 跳过 ssr
 
有一些场景不需要 ssr,例如我添加一个点击量的组件或者添加一个查看图片的功能,这种情况下 ssr 没有任何帮助,这个时候就可以使用 dynamic。
            
            
              js
              
              
            
          
          import dynamicNext from "next/dynamic";
const Statistics = dynamicNext(() => import("./statistics"), { ssr: false });
return (
  <>
    <Statistics></Statistics>
  </>
);
        - 延迟加载
 
通过延迟加载来减少初始渲染路线,来提高初始加载性能。例如延迟加载客户端组件或者库,在用户点击的时候才进行渲染。
page.tsx
            
            
              js
              
              
            
          
          "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
            
            
              js
              
              
            
          
          "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>
  );
}
        - 添加自定义加载组件
 
            
            
              js
              
              
            
          
          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 对需要使用客户端的组件进行剥离,下面是一个示例。
            
            
              js
              
              
            
          
          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 有相关讨论,但是还没解决。
解决方法:
- 切换低版本 antd
 - 换一个 loading 方案
 
最后
如果文章有书写错误地方欢迎指出。下一篇会介绍如何给博客添加点击量以及图片放大缩小等功能。