这是 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 方案
最后
如果文章有书写错误地方欢迎指出。下一篇会介绍如何给博客添加点击量以及图片放大缩小等功能。