使用 Notion 数据库进行 Next.js 应用全栈开发

816次阅读  |  发布于1年以前

前言

在上一篇中,我们使用了 strapi 和 Next.js 开发了一个简易微博,但是我没有部署上线,因为我知道这个小应用只能个人体验,若是我们的个人项目想要部署上线,难道还得花钱买服务器吗,“任何不能令人满意的东西,不值得我们屈尊“,今天我就得带大家来白嫖一下 Notion 数据库,让我们的个人应用轻松上线。

本文涉及代码都在 Github 仓库中

Notion 是什么?

Notion 是一款极其出色的个人笔记软件,它将“万物皆对象”的思维运用到笔记中,让使用者可以天马行空地去创造、拖拽、链接。Notion 适合各种场景,无论是生活、工作还是学习,各种东西都可以在这里记录;它可以帮助用户记录日程表、每日计划、待办事项、日记等,到了时间系统会自动进行提醒。

但对我们程序员来说,Notion 除了是笔记软件,还可以是数据库。

在开始之前,我强烈建议不要在生产环境中使用 Notion 数据库。

需求分析

我先来介绍一下我的需求,之前写过一篇《【实战】Next.js + 云函数开发一个面试刷题网站》,采用了"腾讯云云开发"中的云数据库和 Vercel 部署了一个面试刷题网站,但现在"腾讯云云开发"目前收费了,价格是 19.9 元/月,所以我打算将数据库从腾讯云迁移到 Notion,并且使用 Next.js 服务端渲染,最后部署同样使用 Vercel。这样做的优点是:

创建 Notion 数据库

如果没有账户的同学,请大家自行注册前往 https://www.notion.so/ 注册账号,Notion 目前没有中文版本,大家可以装这个油猴插件[1],汉化一下。

首先我们在 Notion 中,创建一个 full page table 页面,来作为我们的数据库保存数据。

1 . 打开 Notion,添加一个 page,输入名称,并在模板中选择 DATABASE 下的 Table。

新建页面创建 DATABASE

2 . 点击 properties,添加我们题目表所需要的一些数据项:

Notion 添加 properties 字段

题目表

根据之前的数据结构,添加字段,这是题目表的数据 ts 类型接口


export interface Question {
    _id: string;
    category: string; // 分类
    title: string; // 标题
    desc: string; // 简介
    options: string; // 选项,JSON转成字符串
    explanation: string; //  解析
    level: number; //  难度
    tagId: number; //  标签
}

Notion properties 字段类型

可以看到 Notion 数据字段也分为文本数字下拉多选文件等等,我们在右侧选择字段类型,所有添加完成后,我们再手动录入一条数据,以便于后续测试数据。

标签表


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 项目

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

2 . 安装 TailwindCSS


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

3 . 编辑 tailwind.config.js 配置文件

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

4 . 修改 styles/global.css 为 tailwindcss 的初始化指令

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

5 . 设置 Node 环境变量,新建一个 .env.local 文件


NOTION_ACCESS_TOKEN=
NOTION_DATABASE_QUESTION_ID=
NOTION_DATABASE_TAG_ID=

TOKEN 为刚才复制的 TOKEN,数据库 ID 为 NOTION 页面 URL 上的 ID

复制 Notion 数据库ID

链接数据库

我们需要安装 Notion Javascript 客户端,来获取表数据信息。

yarn add @notionhq/client

查询列表

新建一个 lib/NotionServer.ts, 将数据库请求的方法封装在这个 class 中

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<Question[]> {
    const response = await this.client.databases.query({
      database_id: database,
    });

    return response.results;
  }
}

新建pages/api/question.ts, 与约定式路由一样,任何在pages/api 目录下的文件都可以作为/api/*接口访问,以下代码就直接调用了刚才创建的 NotionServer:


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<Data>
) {
  const data = await notionServer.query();
  res.status(200).json(data);
}

此时我们访问 http://localhost:3000/api/question ,会看到如下数据

Notion api 返回数据结构

这显然不是我们想要的数据,在 Notion 中,表中的每一个单元格数据,都有类型和针对这个类型的描述字段。 我们想要的格式是这样的,比如:

{
  "title":"输出什么?"
  "desc":"题目描述"
}

title 字段叫标题,而且肯定是单行文本类型,是明确的,所以我们需要简化一下字段,在 NotionServer.ts 中加入一个transformer 方法,用于数据转换:


class NotionService {
  ...

  async query(): Promise<Question[]> {
    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 查询数据详情可以使用如下代码

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

  return NotionService.transformer(response);
}

同样查询结果需要转换一下,将 Notion 复杂结果转换为我们需要的简单数据结构。

添加/修改数据

添加和修改数据,我们可以使用下面这个 2 个方法

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

其中 paramters 的值,我们可以通过 ts 类型看,其实跟查询返回的数据一致,我只需要拷贝查询结果的其中一条数据,删除掉字段中的 id,替换掉里面的内容为真实数据即可。

notionhq/client 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 用于创建题目的接口。

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<Data>
) {
  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 中


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,查询未归档的数据。

下面是假删除数据的代码:

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 中直接获取数据,用于服务端渲染。

以下是首页中服务端获取数据的部分实现代码

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

export default function Interview({
  data,
  tags,
  q,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
  // 使用 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 需要拆分成数组存储,读取的时候再将数组拼接成一个字符串。

至于如何拆分,大家可以思考下,也直接看我的代码仓库[2]

2 . 传统数据分页传 pagepage_size 2 个字段,但在 Notion 中采用了 start_cursor 指针的方式,前端需要使用滚动翻页的方式。

小结

本文我们利用 Next.js 和 Notion API 编写了一个前端刷题网站,整个流程是:

使用 Notion 作为数据库有利有弊

最大的优点: 免费,打通 Notion,让笔记和网站得以同步;

缺点:Notion API 的结构比较复杂,接口固定,不能实现定义的功能,所以这里适合做一些尝试性的项目。

思考:若是我们的网站也需要实现 CMS 自定义设计表单的功能,我相信 Notion 的数据结构值得我们参考。

好了,以上就是利用 Notion API 和 Next.js 进行应用全栈开发的过程,你学会了吗?若对你有帮助,记得帮我点赞。

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8