昨天看了职棒大联盟的全垒打背景音乐才知道,比利时兄弟的《The Hum》原来是洛杉矶道奇队 2022 年的全垒打配乐😂。
前言
本文其实主要目的并不是想说怎么用 node.js 实现爬虫,而更想说为什么说 next.js 是一个全栈框架(严格上来说,并不是前端框架)。
假如现在你去问 ChatGPT,“next.js 是前端还是全栈框架?” ChatGPT 会给出很多证明 next.js 是全栈框架的例子,比如:
next.js有自己的路由系统,可以实现服务端渲染(SSR);next.js支持 API 路由;next.js支持部署服务端函数(serverless functions)。
在本文中,Jim 将会用一个简单的爬虫例子来证明 next.js 是一个全栈框架。
cheerio.js 爬虫
cheerio.js 是一个 node.js 的库,可以用来解析 HTML,它的 API 和 jQuery 很像,所以很容易上手。
安装
通过 npm 安装即可。
$ npm install cheerio
使用
cheerio.js 的使用非常简单,只需要引入 cheerio,然后调用 load 方法,传入 HTML 字符串即可。
// ES6
import { CheerioAPI, load } from 'cheerio';
const resp = await fetch("http://www.baidu.com");
const html = await resp.text();
const $: CheerioAPI = load(html);
然后就和 jQuery 一样了,可以通过 $('selector') 来获取元素,然后通过 .text() 来获取元素的文本内容。
console.log($("title").text());
// 百度一下,你就知道
next.js API 路由
next.js 的 API 路由是一个非常好用的功能,它可以让我们在 next.js 项目中直接创建 API 接口,而不需要额外的配置。
// pages/api/example.ts
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handler(req: NextApiRequest, res: NextApiResponse) => {
  res.status(200).json({ name: "Jim" });
};
在上面的例子中,我们创建了一个 example 的 API 接口,它的路径是 /api/example。这个接口的功能就是返回一个 JSON 对象。
我们可以进一步尝试将 cheerio.js 和 next.js 的 API 路由结合起来,来实现一个简单的爬虫再渲染:
// pages/api/scrape_baidu.ts
import { CheerioAPI, load } from 'cheerio';
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handler(req: NextApiRequest, res: NextApiResponse) => {
  const resp = await fetch("http://www.baidu.com");
  const html = await resp.text();
  const $: CheerioAPI = load(html);
  res.status(200).json({ title: $("title").text() });
};
这个接口的功能就是爬取百度首页的标题,并返回给前端。接下来让前端渲染一下:
// app/page.tsx
export default function Home() {
  const [title, setTitle] = useState<string>("");
  useEffect(() => {
    fetch("/api/scrape_baidu")
      .then((res) => res.json())
      .then((data) => setTitle(data.title));
  }, []);
  return <div>{title}</div>;
};
上述方法是一种常见的方法,但是有一非常非常致命的 bug——会造成无限循环。这也是 next.js 在继承 react.js 的 useEffect 时的一个糟糕特性,详见这两篇文章:How to Solve the Infinite Loop of React.useEffect()
 和 Github Issue - Infinite render loop with server components when running next dev。
大神 Jack Herrington 给出了一种解决方案:
// app/page.tsx
function makeQueryClient() {
  const fetchMap = new Map<string, Promise<any>>();
  return function queryClient<QueryResult>(
    name: string,
    query: () => Promise<QueryResult>
  ): Promise<QueryResult> {
    if (!fetchMap.has(name)) {
      fetchMap.set(name, query());
    }
    return fetchMap.get(name)!;
  };
}
const queryClient = makeQueryClient();
export default function Home() {
  const baidu = use(queryClient("baidu", () =>
    fetch("/api/scrape_baidu").then((res) => res.json())
  ));
  return (
    <div>{baidu.title}</div>
  );
};
好的,本文简要地介绍了如何使用 next.js 的 API 路由来实现一个简单的爬虫。但是,这个爬虫的效率并不高,因为每次请求都会重新爬取一次百度首页,这样的话,如果有 100 个用户同时访问这个页面,那么就会有 100 个请求同时爬取百度首页,这样的话,百度就会封掉我们的 IP 了。
所以,我们需要将爬虫的结果缓存起来,这样就可以避免重复爬取了。