使用 Notion 數據庫進行 Next.js 應用全棧開發

語言: CN / TW / HK

highlight: monokai-sublime theme: vuepress


文章為稀土掘金技術社區首發簽約文章,14 天內禁止轉載,14 天后未獲授權禁止轉載,侵權必究!

前言

在上一篇中,我們使用了 strapi 和 Next.js 開發了一個簡易微博,但是我沒有部署上線,因為我知道這個小應用只能個人體驗,若是我們的個人項目想要部署上線,難道還得花錢買服務器嗎,“任何不能令人滿意的東西,不值得我們屈尊“,今天我就得帶大家來白嫖一下 Notion 數據庫,讓我們的個人應用輕鬆上線。

本文涉及代碼都在 Github 倉庫

Notion 是什麼?

Notion 是一款極其出色的個人筆記軟件,它將“萬物皆對象”的思維運用到筆記中,讓使用者可以天馬行空地去創造、拖拽、鏈接。Notion 適合各種場景,無論是生活、工作還是學習,各種東西都可以在這裏記錄;它可以幫助用户記錄日程表、每日計劃、待辦事項、日記等,到了時間系統會自動進行提醒。

但對我們程序員來説,Notion 除了是筆記軟件,還可以是數據庫。

在開始之前,我強烈建議不要在生產環境中使用 Notion 數據庫。

需求分析

我先來介紹一下我的需求,之前寫過一篇《【實戰】Next.js + 雲函數開發一個面試刷題網站》,採用了"騰訊云云開發"中的雲數據庫和 Vercel 部署了一個面試刷題網站,但現在"騰訊云云開發"目前收費了,價格是 19.9 元/月,所以我打算將數據庫從騰訊雲遷移到 Notion,並且使用 Next.js 服務端渲染,最後部署同樣使用 Vercel。這樣做的優點是:

  • 整個上線網站我都不用花錢
  • SSR 渲染,搜索引擎可以收錄網站中更多的頁面。

創建 Notion 數據庫

如果沒有賬户的同學,請大家自行註冊前往 https://www.notion.so/ 註冊賬號,Notion 目前沒有中文版本,大家可以裝這個油猴插件,漢化一下。

首先我們在 Notion 中,創建一個 full page table 頁面,來作為我們的數據庫保存數據。

  1. 打開 Notion,添加一個 page,輸入名稱,並在模板中選擇 DATABASE 下的 Table。

新建頁面創建 DATABASE

  1. 點擊 properties,添加我們題目表所需要的一些數據項:

Notion 添加 properties 字段

題目表

根據之前的數據結構,添加字段,這是題目表的數據 ts 類型接口

ts export interface Question { _id: string; category: string; // 分類 title: string; // 標題 desc: string; // 簡介 options: string; // 選項,JSON轉成字符串 explanation: string; // 解析 level: number; // 難度 tagId: number; // 標籤 }

Notion properties 字段類型

可以看到 Notion 數據字段也分為文本數字下拉多選文件等等,我們在右側選擇字段類型,所有添加完成後,我們再手動錄入一條數據,以便於後續測試數據。

標籤表

ts export interface Tag { id: number; tagName: string; image: string; }

Notion 創建標籤表

我們使用相同步驟,建立標籤表,並且添加數據到表中。

建立表關聯

題目表和標籤表是多對一的關係,一個標籤下有多道題目,一個題目只有一個標籤

Notion 建立表關聯

在題目表添加屬性tag,選擇 Relation,讓後選擇“標籤”表

Notion 建立表關聯

這樣題目表和標籤表就建立了關係

創建 Notion 集成

在使用 Notion API 之前,我們需要創建一個 Notion 的應用集成,獲取 API Key。 打開 https://www.notion.so/my-integrations,打開 Notion 集成頁面,登錄自己的賬號,點擊 New integration 創建一個新的應用:

創建 Notion 集成

名稱可以自己起,上傳一個 LOGO,然後關聯一個 Notion 的工作空間:

創建 Notion 集成,填寫信息

點擊提交,這個應用就創建好了,在跳轉的新頁面裏,把Internal Integration Token複製下來,不要泄露,否則拿到這個 key 的人都能操作你的筆記啦。

複製 Notion Token

接下來在 Notion Page 頁面,點擊更多,創建 Connections,連接到我們剛才創建的應用

Notion 頁面關聯集成應用

這樣就可以使用 API 來操作筆記啦。

初始化項目

  1. 創建一個 next 項目

bash npx create-next-app fe-app --typescript cd fe-app

  1. 安裝 TailwindCSS

bash yarn add -D tailwindcss postcss autoprefixer @tailwindcss/typography npx tailwindcss init -p

  1. 編輯 tailwind.config.js 配置文件

js /** @type {import('tailwindcss').Config} */ module.exports = { content: [ "./src/**/*.{js,ts,jsx,tsx}", ], theme: { extend: {}, }, plugins: [ require('@tailwindcss/typography') ], }

  1. 修改 styles/global.css 為 tailwindcss 的初始化指令

css @tailwind base; @tailwind components; @tailwind utilities;

  1. 設置 Node 環境變量,新建一個 .env.local 文件

bash NOTION_ACCESS_TOKEN= NOTION_DATABASE_QUESTION_ID= NOTION_DATABASE_TAG_ID=

TOKEN 為剛才複製的 TOKEN,數據庫 ID 為 NOTION 頁面 URL 上的 ID

複製 Notion 數據庫ID

鏈接數據庫

我們需要安裝 Notion Javascript 客户端,來獲取表數據信息。

bash yarn add @notionhq/client

查詢列表

新建一個 lib/NotionServer.ts, 將數據庫請求的方法封裝在這個 class 中

```ts import { Client } from "@notionhq/client";

const auth = process.env.NOTION_ACCESS_TOKEN;

const database = process.env.NOTION_DATABASE_QUESTION_ID ?? "";

type Question = any;

export default class NotionService { client: Client;

constructor() { this.client = new Client({ auth }); }

async query(): Promise { const response = await this.client.databases.query({ database_id: database, });

return response.results;

} } ```

新建pages/api/question.ts, 與約定式路由一樣,任何在pages/api 目錄下的文件都可以作為/api/*接口訪問,以下代碼就直接調用了剛才創建的 NotionServer:

```ts import type { NextApiRequest, NextApiResponse } from "next"; import NotionServer from "../../lib/NotionServer";

type Data = any;

const notionServer = new NotionServer();

export default async function handler( req: NextApiRequest, res: NextApiResponse ) { const data = await notionServer.query(); res.status(200).json(data); } ```

此時我們訪問 http://localhost:3000/api/question ,會看到如下數據

Notion api 返回數據結構

這顯然不是我們想要的數據,在 Notion 中,表中的每一個單元格數據,都有類型和針對這個類型的描述字段。 我們想要的格式是這樣的,比如:

json { "title":"輸出什麼?" "desc":"題目描述" }

title 字段叫標題,而且肯定是單行文本類型,是明確的,所以我們需要簡化一下字段,在 NotionServer.ts 中加入一個transformer 方法,用於數據轉換:

```ts class NotionService { ...

async query(): Promise { const response = await this.client.databases.query({ database_id: database, });

return response.results.map((item) => NotionService.transformer(item));

}

private static transformer(page: any): Question { let data: any = {};

for (const key in page.properties) {
  switch (page.properties[key].type) {
    case "relation":
      data[key] = page.properties[key].relation[0].id;
      break;

    case "title":
    case "rich_text":
      data[key] =
        page.properties[key][page.properties[key].type][0].text.content;
      break;

    default:
      data[key] = page.properties[key];
      break;
  }
}

return data;

} } ```

轉換後,刷新頁面,便得到了我們想要的數據。

轉換後的 json 數據

查詢詳情

根據 page_id 查詢數據詳情可以使用如下代碼

```ts async detail(id: string): Promise { const response = await this.client.pages.retrieve({ page_id: id, });

return NotionService.transformer(response); } ```

同樣查詢結果需要轉換一下,將 Notion 複雜結果轉換為我們需要的簡單數據結構。

添加/修改數據

添加和修改數據,我們可以使用下面這個 2 個方法

ts this.client.pages.create(paramters) this.client.pages.update(paramters)

其中 paramters 的值,我們可以通過 ts 類型看,其實跟查詢返回的數據一致,我只需要拷貝查詢結果的其中一條數據,刪除掉字段中的 id,替換掉裏面的內容為真實數據即可。

notionhq/client TS 類型提示

下面是添加題目函數的代碼:

ts async create(question: Question): Promise<any> { const response = await this.client.pages.create({ parent: { database_id: database, }, properties: { desc: { type: "rich_text", rich_text: [ { type: "text", text: { content: question.desc, }, }, ], }, options: { type: "rich_text", rich_text: [ { type: "text", text: { content: question.options, }, }, ], }, explanation: { type: "rich_text", rich_text: [ { type: "text", text: { content: question.explanation, }, }, ], }, tag: { type: "relation", relation: [ { id: question.tag, }, ], }, title: { type: "title", title: [ { type: "text", text: { content: question.title, }, }, ], }, }, }); return response; }

編輯數據與添加數據方法相似,只需要拷貝查詢結果的數據,將需要修改的部分修改掉即可,使用 Notion 作為數據庫的還有一個好處,就是可以將 Notion 作為 CMS 系統來用,有時添加和修改不是必須的功能,我們可以直接在 Notion 後台進行數據管理。

數據庫遷移

因此我們可以從雲開發數據庫中將數據導出,通過 api 導入到 Notion 數據庫中。

首先在新建一個 pages/api/create.ts 用於創建題目的接口。

```ts import type { NextApiRequest, NextApiResponse } from "next"; import NotionServer from "../../lib/NotionServer";

type Data = any;

const notionServer = new NotionServer();

export default async function handler( req: NextApiRequest, res: NextApiResponse ) { if (req.method !== "POST") { res.status(405).send({ message: "Only POST requests allowed" }); return; }

const data = await notionServer.create(req.body);

res.status(200).json(data); } ```

接下來就可以將導出的 json,通過執行本地 node fetch 的方式將數據全部導入到 Notion 中

```ts const fs = require("fs"); const path = require("path"); const fetch = require("node-fetch");

(async () => { const content = fs.readFileSync( path.resolve(__dirname, "./fe.json"), "utf8" );

const tags = [ { _id: "6b428f57-0831-4280-ac1c-8d016c8d038b", id: 17, image: "/static/logo/fun.svg", tagName: "趣味題", }, ... ];

const data = content.split("\n");

for (let index = 0; index < data.length; index++) { if (data[index].trim() === "") continue; let item = JSON.parse(data[index]); item.tag = tags.find((tag) => tag.id === item.tagId)?._id;

const res = await fetch("http://localhost:3000/api/create", {
  method: "post",
  headers: {
    "Content-Type": "application/json; charset=utf-8",
  },
  body: JSON.stringify(item),
}).then((res) => res.json());

console.log("第" + index + "題" + item.title + res.id);

} console.log("end"); })(); ```

上面代碼中 tags 我是選創建了一個 tag 查詢接口,因為 tags 數據不多,所以我直接將數據複製了下來,用 nodejs 執行一下以上代碼,我們便可以在控制枱中看到數據導入的進度。

數據遷移到 Notion 完成

執行完成,總共 968 題, 我們來 Notion 中看看導入的效果。

Notion 查看數據表

當然我也將全部題目公開在 Notion 上面了,大家可以通過這個鏈接查看。

刪除數據

在 Notion 中,不能使用 api 的方式刪除數據,所以我們需要換一種思維來解決,在表中創建一個字段In stock 來標識,用於假刪除數據,也就是將“刪除數據邏輯”修改為“修改數據邏輯”,將這條數據修改為歸檔類型,所以之前查詢列表接口需要加上 In stock 值為 false,查詢未歸檔的數據。

下面是假刪除數據的代碼:

ts async remove(pageId: string) { const response = await this.client.pages.update({ page_id: pageId, properties: { "In stock": { checkbox: true, }, }, }); return response; }

Next.js SSR

到此,我們完成了數據表的建設,以及數據庫的遷移。完成了對 NotionService 的封裝,有了 NotionService ,我們不但可以完成接口,而且還可以在 Next.js 中直接獲取數據,用於服務端渲染。

以下是首頁中服務端獲取數據的部分實現代碼

```ts import React from "react"; import { GetServerSidePropsContext,InferGetServerSidePropsType } from "next"; import NotionServer from "@/lib/NotionServer";

export default function Interview({ data, tags, q, }: InferGetServerSidePropsType) { // 使用 props 數據,之間進行 render return (...) } // 在Nodejs 環境中執行 export async function getServerSideProps(context: GetServerSidePropsContext) { const { q = "", tagid, cursor } = context.query; const notionServer = new NotionServer();

const tags = await notionServer.queryTags();

const { data, has_more } = await notionServer.query({ title: q as string, tagid: tagid as string, }); // 獲取到的數據,傳遞給組件props return { props: { data, tags, has_more, q, }, }; } ```

這部分主要跟之前文章中的內容大致相同,所以我就不過多介紹了,我們直接來看下效果。

Notion 和 Next.js 開發現效果

大家可以使用 https://notion.runjs.cool/ 訪問體驗,也可以與之前的騰訊雲開發做對比 https://runjs.cool/

Notion 和騰訊雲接口對比

Notion 小數據量接口平均 800ms,列表接口包含了答案詳情,所以在 1s 以上,騰訊雲每個接口都在 300ms 以下。

踩坑

雖然全文下來,大致上沒什麼難度,但是我在事件過程中也遇到了一些坑。

  1. Notion 中,文本字段是一個數組,上面transformer代碼中我們取值都是取了[0].text.content,但 content 的長度現在是 2000,所以我們之前的一些 Text 字段,超過 2000 需要拆分成數組存儲,讀取的時候在將數組拼接成一個字符串。

至於如何拆分,大家可以思考下,也直接看我們的代碼倉庫

  1. 傳統數據分頁才 pagepage_size 2 個字段,但在 Notion 中採用了 start_cursor 指針的方式,前端需要使用滾動翻頁的方式。

小結

本文我們利用 Next.js 和 Notion API 編寫了一個前端刷題網站,整個流程是:

  • 創建數據庫頁面、配置屬性、獲取數據庫 id。
  • 從 Notion 官網創建應用,獲取 API KEY。
  • 使用 @notionhq/client 對 Notion 數據進行操作,編寫接口
  • 在 Page 中,使用 getServerSideProps 進行數據獲取,讓後使用 react 進行渲染頁面。

使用 Notion 作為數據庫有利有弊

最大的優點: 免費,打通 Notion,讓筆記和網站得以同步;

缺點:Notion API 的結構比較複雜,接口固定,不能實現定義的功能,所以這裏適合做一些嘗試性的項目。

思考:若是我們的網站也需要實現 CMS 自定義設計表單的功能,我相信 Notion 的數據結構值得我們參考。

好了,以上就是利用 Notion API 和 Next.js 進行應用全棧開發的過程,你學會了嗎?若對你有幫助,記得幫我點贊。

後續

接下來我將繼續分享 Next.js 相關的實戰文章,歡迎各位關注我的《Next.js 全棧開發實戰》 專欄。

  • 使用 Prisma 和 PostgreSQL 進行 Next.js 應用全棧開發
  • 使用 NextAuth 實現 Next.js 應用的鑑權與認證
  • 使用 React query 給 Next.js 應用全局狀態管理
  • 使用 i18next 實現 Next.js 應用國際化
  • 使用 Playwright 進行 Next.js 應用的端到端測試
  • 使用 Github actions 給 Next.js 應用創建 CI/CD
  • 使用 Docker 部署 Next.js 應用
  • 將 Next.js 應用部署到騰訊雲 serverless

你對哪塊內容比較感興趣呢?歡迎在評論區留言,感謝您的閲讀。