- Next.js 构建博客之资源抓取
- Next.js 构建博客之博客搭建
- Next.js 构建博客之打包 SSG
- Next.js 构建博客之常见问题处理
- Next.js 构建博客之功能拓展
- Next.js 构建博客之自动构建
这是 Next.js 搭建博客的第一章,整个系列会详细介绍如何结合 GitHub 和 Next.js 搭建自己的博客。
如果你想看已经部署博客的地址可以点击查看,代码仓库地址点击查看。
在正式开始之前,使用坚果云画了一份流程图,方便后续的理解。
整个流程都高度依赖 issues 和 labels,所以在正式讲解 Next.js 之前还需要思考怎么把当前仓库的所有 issues 和 labels 爬取下来,这里 GitHub 官方已经给出了相关的 api 文档,只需要参考调用即可。 不过这里额外补充一下,为什么需要把整体 issues 和 labels 拉取下来再进行 Next.js 拉取呢,主要有三个原因:
- GitHub api 并没有给出总页数多少,我们需要重复调用才知道是否结束,不会像开发项目中知道第几页从当前页数拉取就行;
- 每天调用的 api 也是有额度限制的,但是在开发环境调用频率很高会导致不能使用就太糟糕了;
- 可以对拉下来的数据进行拓展;
项目初始化
整体项目会最终采用一个 MonoRepo 的设计,采用的技术是 pnpm + workspace 形式,下面详细讲解下步骤。
- 新建 package.json 文件
pnpm init -y
- 创建 pnpm-workspace.yaml 文件,调整文件内容为
packages:
# 所有在 packages/ 子目录下的 package
- "packages/**"
# 不包括在 test 文件夹下的 package
- "!**/test/**"
- 在 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 次。
这里贴一下官方的文档地址 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
的文件。
这里再新建一个 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 构建博客之博客搭建,如果有书写错误欢迎指出。