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 搭建博客的第一章,整个系列会详细介绍如何结合 GitHub 和 Next.js 搭建自己的博客。

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

在正式开始之前,使用坚果云画了一份流程图,方便后续的理解。

image-3.png

整个流程都高度依赖 issues 和 labels,所以在正式讲解 Next.js 之前还需要思考怎么把当前仓库的所有 issues 和 labels 爬取下来,这里 GitHub 官方已经给出了相关的 api 文档,只需要参考调用即可。 不过这里额外补充一下,为什么需要把整体 issues 和 labels 拉取下来再进行 Next.js 拉取呢,主要有三个原因:

  1. GitHub api 并没有给出总页数多少,我们需要重复调用才知道是否结束,不会像开发项目中知道第几页从当前页数拉取就行;
  2. 每天调用的 api 也是有额度限制的,但是在开发环境调用频率很高会导致不能使用就太糟糕了;
  3. 可以对拉下来的数据进行拓展;

项目初始化

整体项目会最终采用一个 MonoRepo 的设计,采用的技术是 pnpm + workspace 形式,下面详细讲解下步骤。

  1. 新建 package.json 文件
pnpm init -y
  1. 创建 pnpm-workspace.yaml 文件,调整文件内容为
packages:
  # 所有在 packages/  子目录下的 package
  - "packages/**"
  # 不包括在 test 文件夹下的 package
  - "!**/test/**"
  1. 在 packages 下创建 sideEffect 文件夹,在 sideEffect 下创建 package.json
cd packages/sideEffect
pnpm init -y

这个 sideEffect 文件最终就是我们加载各种副作用的一个文件夹,拉取 issues 的操作也在这里完成。

经过上面一些步骤,目前项目的大概雏形已经有了,下面安装一些必备的依赖项方便后续的操作

pnpm install axios dayjs dotenv fs-extra

之后进入settings/tokens设置个人令牌,在开发环境传递给 GitHub api 接口使用,否则会受到限制每小时只能请求 60 次

image-2.png

这里贴一下官方的文档地址 issues,下一步就是把当前仓库所有信息拉取下来。

拉取 issues and labels

创建一个新的 api/index.ts 文件,我们所有相关的跟 GitHub api 都通过这个完成。

上面在 settings/tokens 创建一个新的 token 保存下来,在 sideEffect 下新建一个.env 文件,将 token 保存成下面键值对形式。

AUTHORIZATION=xxx
# GITHUB_REPOSITORY是你的用户名+仓库名组成,根据你自己的仓库调整
GITHUB_REPOSITORY=bosens-China/blog

之后新建一个 utils/request.ts 文件,这个文件就是封装一下 axios 方便使用。

import axios from "axios";

export const instance = axios.create({
  baseURL: "https://api.github.com/",
  timeout: 10000,
  headers: {
    Accept: "application/vnd.github+json",
    Authorization: `Bearer ${process.env.AUTHORIZATION}`,
    "X-GitHub-Api-Version": "2022-11-28",
  },
});

之后返回到 api/index.ts 文件

import { instance } from "../utils/request";
const { GITHUB_REPOSITORY } = process.env;

export const issues = async (page = 1) => {
  const { data } = await instance.get<IssuesDaum[]>(
    `/repos/${GITHUB_REPOSITORY}/issues`,
    {
      params: {
        filter: "created",
        state: "open",
        sort: "updated",
        per_page: 100,
        page,
      },
    }
  );
  return data;
};

export const labels = async (page = 1) => {
  const { data } = await instance.get<Label[]>(
    `/repos/${GITHUB_REPOSITORY}/labels`,
    {
      params: {
        per_page: 100,
        page,
      },
    }
  );
  return data;
};

IssuesDaum 和 Label 是详细的类型定义文件这里忽略掉,如果需要相关类型文件可以点击访问

之后新建 implement.ts 文件,这个文件就是调用 issues 和 labels 接口,然后把信息保存下来。

上面有说到根据 GitHub 的文档可以看到 labels 和 issues 都是返回一个数组,但是我们并不知道有没有拉取完,所以这边的思路就是创建一个新的文件,让他调用自身直到返回空数组为止。

const continued = async <T extends (page?: number) => Promise<unknown[]>>(
  fn: T,
  page = 1
) => {
  const result = (await fn(page)) as ReturnType<T>;
  if (Array.isArray(result) && result.length) {
    const arr = await continued(fn, page + 1);
    result.push(...arr);
  }
  return result;
};

最初的时候创建了一个.env 文件,这个文件是保存开发环境的一些信息,不过根据 esm 加载顺序我们必须要保证在调用其他模块的时候 dotenv 信息已经正确加载,所以这边思路如下。

创建一个立即执行函数,把需要执行的代码放里面执行即可,或者使用顶层 await 也可以,下面是完整代码

import dotenv from "dotenv";
import fs from "fs-extra";
import path from "path";

dotenv.config();

const { GITHUB_REPOSITORY } = process.env;

(async () => {
  console.time(`Start crawling the required data...`);
  const { labels, issues } = await import("./api");
  try {
    const [labelsData, issuesData] = await Promise.all([
      continued(labels),
      continued(issues),
    ]);
    // 考虑到后续可能别人直接拷贝这个项目使用,对label一次插入
    let other = labelsData.find((f) => f.name === "其他")!;
    if (!other) {
      other = {
        id: 1000000000,
        node_id: "MDU6TGFiZWwxMzcxNjg2NjEx",
        url: `https://api.github.com/repos/${GITHUB_REPOSITORY}/labels/其他`,
        name: "其他",
        color: "f6ecbf",
        default: false,
        description: "未找到分类,暂定的文章分类",
      };
      labelsData.push(other);
    }
    const map: Map<string, typeof issuesData> = new Map();
    issuesData.forEach((item) => {
      if (!item.labels.length) {
        item.labels.push(other);
      }
      item.labels.forEach((label) => {
        const id = `${label.id}`;
        if (!map.has(id)) {
          map.set(id, []);
        }
        map.get(id)?.push(item);
      });
    });

    await fs.writeJson(
      path.join(__dirname, "./data.json"),
      {
        label: labelsData,
        issuesData: issuesData,
        labelsMap: [...map],
      },
      { spaces: 2 }
    );
  } catch (e) {
    console.log(e instanceof Error ? e.message : e);
  }
  console.timeEnd(`Start crawling the required data...`);
})();

提供资产

上面的代码都是 TypeScript,不能直接运行,这里安装 tsx

pnpm add tsx

它的作用就是调用 TypeScript 代码,相比 ts-node 它不会进行类型检查,速度很快。

之后在 package.json 下的 scripts 下创建命令,方便快速调用

scripts: {
  "crawlingResource": "tsx ./src/implement.ts",
}

之后执行 pnpm run crawlingResource,就可以看到在 src 下生成了一个 data.json 的文件。

image.png

这里再新建一个 index.ts 文件,方便对 data.json 进行一些封装查询操作。

import data from "./data.json";

// 对数据进行封装,方便调用
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const classification = new Map(data.labelsMap as any) as Map<
  string,
  typeof data.issuesData
>;

const map = new Map<string, (typeof data.label)[number] | undefined>();

export const getLabel = (id: string) => {
  if (map.has(id)) {
    return map.get(id);
  }
  const result = data.label.find((f) => f.id === +id);
  map.set(id, result);
  return result;
};

export default data;

到这里就把拉取资源的相关写完了,不过还需要在 package.json 暴露出口,让其他模块安装之后可以进行调用

"main": "./src/index.ts",

最后

最后记得在当前目录创建一个新的.gitignore 文件,将.env 文件忽略。

第一节内容就讲完了,下一节会介绍 Next.js 构建博客之博客搭建,如果有书写错误欢迎指出。