跳到主要内容位置

Express.js + Notion API 保存数据

Notion 是最近一段时间比较火的笔记软件,使用了数据库的方式管理笔记,笔记之间可以建立链接,与 RDBMS,关系型数据库的设计非常类似。Notion 的笔记集合相当于数据库中的表,每个笔记就是表中的一行,笔记的属性则为列,这种方式作为数据库来存储数据再适合不过了,而最近 Notion 又上线了 Beta 版的 API,可以让外部应用来操作笔记了,那么这一期我们就利用 Notion API 和 Express.js,为我们的留言板应用添加上后端接口。

Notion 应用集成

在使用 Notion API 之前,我们需要创建一个 Notion 的应用集成,获取 API Key。 打开 https://www.notion.so/my-integrations,打开 Notion 集成页面,登录自己的账号,点击 New integration 创建一个新的应用:

img

名称可以自己起,logo 也可以选择性上传,然后关联一个 Notion 的工作空间:

img

点击提交,这个应用就创建好了,在跳转的新页面里,把 internal integration token 复制下来,保存到一个私密的地方,不要泄露,否则拿到这个 key 的人都能操作你的笔记啦~:

img

剩下的就保持默认就好。现在 Notion 的应用就创建好了。

创建数据库

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

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

img

  1. 点击 properties,添加我们留言板所需要的一些数据项:

img

这里,标题属性是固定的,因为每个页面必须要有标题,但是对于留言来说,标题没什么用,我这里就改成了序号,后面的几个属性的类型分别是:

  1. user - Text,富文本类型,存放用户昵称。
  2. avatar - URL,url 类型,存放用户头像 url。
  3. content - Text,富文本类型,存放留言内容。
  4. replyTo & replies - Relation,关系类型,表示该条留言是回复哪条留言的,这里选择 relation 的时候,Notion 会提示我们选择关联的数据库,因为留言和回复都是留言,所以选择我们创建的数据库本身,再在下一步页面中,选择 Create a new property,这样,既可以通过留言获取它的所有回复,也可以通过回复,获取它所回复的留言,点击 Create relation,它会自动再创建一个返向的关联关系,我们把它改名为 replies,这样 replies 里是回复列表,replyTo 是回复的上级留言。根据 replyTo 是否有值,我们把留言分为顶级留言和回复,顶级留言就是通过页面上方留言框发表的留言,回复则是点回复按钮发表的留言回复。
  5. time - Created time,创建时间类型,这个属性不用我们自己填,Notion 会在创建页面的时候,自动加上创建时间。
  6. 现在,我们的数据库就创建好了,我们可以添加几条示例数据,然后把其中的几个,通过 replyTo 或 replies,设置为回复。 img
  7. 把我们之前创建的 Notion 应用添加进来,这样 API 才有权限访问这个数据库,点击右上角的 share,在输入框中点一下,选择刚创建的应用。
  8. 我们还需要保存数据库 id,用于 API 中访问这个特定的数据库,在页面右上角点击菜单,选择 copy link,在浏览器粘贴,把 url 最后一段,?之前的一串数字复制并保存下来,他就是 database id。
https://www.notion.so/myworkspace/945273b86fa24e2XXXXXXXXXXXXXXXXX?v=...
|--------- Database ID --------|

获取当前用户 id

因为我们回复留言的时候,默认取得是当前用户的用户名和头像,那么我们可以利用 Notion 的登录用户,来获取这些信息。我们可以利用 Postman 或者其它你熟悉的 HTTP 请求工具,获取 Notion 登录的 user id,后面用它来查询用户具体信息。

  1. 打开 Postman,使用 https://api.notion.com/v1/users 这个 api,在 Authorization 中,选择 bearer token,把 Notion 的 API key 传递进去。
  2. 在 Headers 选项中,添加 Notion-Version 请求头,值为 2021-05-13,Notion API 要求我们传递 API 版本号,来保持兼容性,因为 Notion API 现在还是 Beta 阶段,API 结构变化很大。
  3. 请求方式为 GET
  4. 点击 send,在返回的数据中,找到 name 为自己 Notion 用户名的那一条,把 id(8a05119e-XXXX-XXXX- XXXXXX)复制下来,注意不是名字为咱们创建的 Notion 应用的名字那条。

现在该获取的信息都有了。

使用 Express.js 创建 API

接下来,使用 Express.js 创建我们的后端 API。为什么不直接使用前端应用直接访问 Notion API 呢?因为 Notion API 跟自己的账号有关,需要使用 API KEY 去访问,这个 API KEY 一旦泄露,任何人都能修改自己账号的 Notion 笔记,前端应用的代码都是暴露在客服端的,在使用 HTTP 请求传递 API KEY 时,可以直接从 Chrome 的网络请求中看到。所以这里我们用 Express.js 来转发请求,把 API KEY 保存在私密的地方。 接下来给我们的应用添加 Express,我们这里只需要编写一些很简单的代码,所以就把它放到我们的 vue 项目中了。我们还会利用 Notion 官方的 Notion Client 来发送请求,使用 Dotenv 来访问我们的 API KEY 环境变量。

  1. 使用 yarn 安装 Express、Notion Client 和 dotenv:
yarn add express @notionhq/client dotenv
  1. 在项目根目录下,新建一个.env 文件,里边保存我们的 Notion API key 和数据库 id:
NOTION_KEY=secret_V8fQDhKOXXXXXXXXXXXXXXXXXXXX
NOTION_DB_ID=945273b86fa24XXXXXXXXXXXXXXXXXXXXXXXXXX
NOTION_CURR_USER_ID=8a05119e-958XXXXXXXXXXXXXXXXXXXXX
  1. 同样在项目根目录下,创建一个 server.js 文件,用于编写 express 服务端代码。
  2. 打开 server.js 文件,先编写一些 express 模板代码,用来测试:
    • 这里监听 3001 端口,并处理 / 根路由,返回 Hello World。
const express = require("express");
const app = express();
const port = 3001;

app.get("/", (req, res) => {
res.send("Hello World!");
});

app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});
  1. 使用 node server.js 运行 Express,在浏览器中访问 http://localhost:3001,能看到 Hello World 字样就算成功了。
  2. 我们导入 Notion Client 和 Dotenv:
    • require("dotenv").config(); 会直接读取 .env 文件,并把里边的键值对放到环境变量中。
const { Client } = require("@notionhq/client");
require("dotenv").config();
  1. 从环境变量中获取 Notion API Key 、数据库 id 和 当前用户 id:
const NOTION_KEY = process.env.NOTION_KEY;
const NOTION_DB_ID = process.env.NOTION_DB_ID;
const NOTION_CURR_USER_ID = process.env.NOTION_CURR_USER_ID;
  1. 初始化 Notion client,把 notion api key 传递给它的配置对象的 auth 属性:
const notion = new Client({ auth: NOTION_KEY });
  1. 给 express 加上 json() 插件,来把接收到的 HTTP 请求 BODY,从 JSON 转换为 JS 对象:
const app = express();
app.use(express.json());

现在 Express 的基本配置就完成了。重启一下服务,来让改动生效。

获取留言列表

接下来看一下获取留言列表数据,在编写代码之前,我们先用 Postman 看一下 Notion 返回笔记列表时的数据结构。

  1. 打开 Postman,新建一个 POST 请求,使用 Notion 查询数据库的 API 路径 [https://api.notion.com/v1/databases/1413433XXXXXXXX/query](https://api.notion.com/v1/databases/14134332bdc94b7ebe6ab1721977b17b/query),把后面那段数据库的 id 换成自己的。
  2. Authorization 和 headers 的配置和我们之前获取用户列表时的一样,分别设置 bearer token 和 notion-version。
  3. 点击发送,过一会可以看到它返回的 JSON 结构,可以说是比较的复杂,但是我们只关注我们所需要的。
  4. results 数组是返回的数据库里的页面列表,每个页面我们关注的是它的 id 和 properties 属性。
  5. properties 属性和我们在 Notion 里定义的属性类型保持一致,以属性名为 key,属性的信息为 value,属性的信息包括 type 类型,属性值等,每个类型都有自己的结构,我们分别看一下。
    1. replies 是关联类型,它的 relation 数组中保存着它的回复的 id,我们需要根据 id 再去查询回复的详情。
    2. user 为 rich_text 类型,因为 Notion 的文本 Text 属性类型实际上是富文本,富文本的内容是一个数组,它会按不同文本样式分割为多个文本块,每个文本块有内容 text 和样式标注 annotation。在我们这个应用里,我们只需要把它作为普通文本,所以取数组第一个元素就可以了,然会通过它的 text 对象中的 content 属性来获取它的值。
    3. time 是 created time,创建时间类型,可以通过 created_time 属性获取值。
    4. content 也是富文本类型,我们这里简单的把它当作普通文本,和 user 用户名一样,通过 rich_text 数组的第一个元素中,text 对象的 content 属性获取留言内容。
    5. replyTo 是关联类型,和 replies 一样,不过它指向的是上级留言的 id。
    6. avatar 是 url 类型,通过它的 url 属性来获取头像地址。
    7. no 是 title 类型,也就是我们的页面标题,这里不需要,所以不关心。

分析完数据库的结构之后,我们可以根据它,转换为较为简单的结构来让前端访问,现在来编写获取留言列表的 API。为了简单起见,我把所有代码都写到 server.js 中了。

  1. 先定义获取留言列表的函数 getAllComments:
async function getAllComments() {
const result = await notion.databases.query({ database_id: NOTION_DB_ID });
const comments = new Map();
// 原始评论数据
result?.results?.forEach((page) => {
comments.set(page.id, {
id: page.id,
user: page.properties.user.rich_text[0].text.content,
time: getRelativeTimeDesc(page.properties.time.created_time),
content: page.properties.content.rich_text[0].text.content,
avatar: page.properties.avatar.url,
replies: page.properties.replies.relation,
replyTo: page.properties.replyTo?.relation[0]?.id,
});
});

// 组装回复,把关系 id 替换为实际评论
let commentsPopulated = [...comments.values()].reduce((acc, curr) => {
if (!curr.replyTo) {
curr.replies = curr.replies.map((reply) => comments.get(reply.id));
acc.push(curr);
}
return acc;
}, []);

return commentsPopulated;
}

在这里边,我们:

  • 使用 Notion client 封装好的获取数据库的 API,notion.databases.query 来查询数据库,把数据库 id 通过参数对象传递进去。
  • 在返回数据后,获取它的 results 字段,里边保存着所有页面数据,遍历它,根据它的数据结构,把它扁平化,分别把 id、user、time、content、avatar、replies 和 replyTo 属性拿出来,并把这些留言保存到 Map 中,map 的 key 为留言 id,值为留言对象,用这种结构,可以方便的通过 id 找到对应的留言。
  • 这里,time,因为要显示为 1 秒前、1 分钟前等字样,这里定义了一个 getRelativeTimeDesc() 函数,来获取这种相对的时间,稍后再看它的代码。
  • replies 回复列表因为保存的只是 id,我们需要在返回的列表中找到对应的留言对象。因为我们的数据比较少,我直接在返回的结果数组中进行查找了,如果数据量大,并且有分页,那么需要通过 Notion API 继续查询单个留言的详情。在获取回复列表时,我们遍历所有留言列表,把 map 的 values() 拿出来,并使用 reduce 方法,只把顶级留言放到结果中,把对应的回复,放到它们的 replies 属性中。那么在 reduce 中,我们判断,如果一个留言对象没有 replyTo 数据,也就是它不是一条回复,那么就用 map 遍历它的 replies 属性,它里边的 id,从 comments map 中找到对应的回复对象,这样就把 replies 替换成有真实留言对象的数组了。
  • 然后把这个顶级留言添加到结果数组中,重复这个流程,直到所有留言的回复都替换成真实的对象了。
  • 最后返回结果。
  1. getRelativeTimeDesc() 函数的代码是这样的:
function getRelativeTimeDesc(time) {
const currentInMs = new Date().getTime();
const timeInMs = new Date(time).getTime();

const minuteInMs = 60 * 1000;
const hourInMs = 60 * minuteInMs;
const dayInMs = 24 * hourInMs;
const monthInMs = 30 * dayInMs;
const yearInMs = 365 * dayInMs;

const relativeTime = currentInMs - timeInMs;
if (relativeTime < minuteInMs) {
return `${Math.ceil(relativeTime / 1000)} 秒前`;
} else if (relativeTime < hourInMs) {
return `${Math.ceil(relativeTime / minuteInMs)} 分钟前`;
} else if (relativeTime < dayInMs) {
return `${Math.ceil(relativeTime / hourInMs)} 小时前`;
} else if (relativeTime < monthInMs) {
return `${Math.ceil(relativeTime / dayInMs)} 天前`;
} else if (relativeTime < yearInMs) {
return `${Math.ceil(relativeTime / monthInMs)} 月前`;
} else {
return `${Math.ceil(relativeTime / yearInMs)} 年前`;
}
}

它的作用是:

  • 获取传递进来的时间,和当前时间的时间戳。
  • 定义了一些秒、分钟、小时、月份、年份所对应的毫秒数。
  • 通过计算当前时间和传递进来的的时间的时间戳差值,与上边定义的毫秒数对比。
  • 落在哪个区间,就使用对应的描述,例如差值大于等于月的毫秒数,小于等于年的毫秒数,就计算它有几个月,显示几个月前。
  1. 好了,现在获取了回复列表,我们可以处理用户请求了,使用 app.get 创建一个处理 get 请求的函数,处理 /comments 路径,在里边调用 getAllComments() 获取评论列表,然后使用 res.json() 把它发送给客户端,再利用 try catch 处理错误信息,如果有问题,就返回 500 错误码给客户端。
app.get("/comments", async (req, res) => {
try {
const comments = await getAllComments();
res.json(comments);
} catch (error) {
console.log(error);
res.sendStatus(500);
}
});
  1. 我们可以运行 node server.js 启动一下 Express,在浏览器或 Postman 中访问 http://localhost:3001/comments,如果看到返回了留言列表数据,就是成功了。

发表留言和回复

发表留言和回复的处理逻辑是一样的,唯一的区别就是有没有 replyTo 这个属性,并保存了所回复的留言的 id。在发布留言的时候,我们只需要一个留言内容参数就可以了,用户名和头像可以从当前登录的 Notion User 中获取,发表时间也会自动生成。 我们来定义个处理发表留言的函数,addComment:

async function addComment({ content, replyTo = "" }) {
let no =
(await notion.databases.query({ database_id: NOTION_DB_ID })).results
.length + 1;
let { avatar_url, name } = await notion.users.retrieve({
user_id: NOTION_CURR_USER_ID,
});

notion.request({
method: "POST",
path: "pages",
body: {
parent: { database_id: NOTION_DB_ID },
properties: {
no: {
title: [
{
text: {
content: no.toString(),
},
},
],
},
user: {
rich_text: [
{
text: {
content: name,
},
},
],
},
avatar: {
url: avatar_url,
},
content: {
rich_text: [
{
text: {
content,
},
},
],
},
// 如果有 replyTO 参数传递进来的,再添加到请求 body 中
...(replyTo && {
replyTo: {
relation: [
{
id: replyTo,
},
],
},
}),
},
},
});
}

在这个函数里:

  • 它接收留言对象作为参数,把 content 和 replyTo 属性解构出来,给 replyTo 默认值设置为空。
  • 获取序号,我们在 Notion 的 database 中,把标题改成了序号,这里简单起见,就用 Notion Client 获取了现有的留言数量,在此基础上加 1。
  • 通过 notion client 的 user api,获取用户名和头像,这里把环境变量中,保存的当前用户 ID 传递进去,把返回结果中的 avatar_url 和 name 属性解构出来。
  • 接着使用 notion client,发送添加 page 的请求,这里没什么难的,只是这里 body 的数据结构,跟之前查询数据库返回的数据结构是一样的,要组装这个结构比较麻烦。
  • 在 notion.request() 方法里,设置 method 请求方式为 POST,路径为 pages,body 中传递数据。
    • parent 设置数据库的 id,指定在哪个数据库中添加页面。
    • properties 设置属性,no 为页面标题,它是 title 类型,本质上也是个富文本,给它传递一个数组,数组的第一个元素为 text 对象,content 属性值为之前获取到的序号。
    • user 是富文本类型,用同样的方式,设置属性值为当前用户的用户名。
    • avatar 是 url 类型,设置它的 url 属性为当前用户的头像 url。
    • content 也是富文本类型,设置留言内容。
    • 如果有 replyTo 的话,就设置 replyTo 属性,它是 relation 类型,relation 保存了一个数组,里边是关联的页面的 id。这里用到了一个小技巧,利用扩展运算符,可以在有 replyTo 属性的时候,把它后面的对象解构出来,放到父对象中,如果没有就不会解构出任何东西来,适合在创建对象字面值的时候,根据条件添加属性。

现在发布留言的函数就创建完了,接下来,使用 app.post() 创建一个处理 POST 请求的函数,路径为 /comments,在里边调用 addComment(),如果成功返回 201 状态码,有问题返回 500 状态码:

app.post("/comments", async (req, res) => {
try {
await addComment(req.body);
res.sendStatus(201);
} catch (error) {
console.log(error);
res.sendStatus(500);
}
});

重启一下 Express 服务,用 Postman 测试一下是否可以成功发表留言,我们先看一个发表顶级留言的例子,新建一个 Post 请求,使用http://localhost:3001/comments API 路径,在 body 里,选择 raw,json,根据代码的数据结构,写上示例的数据:

{
"content": "测试回复"
}

点击发送,如果返回了 201,就成功了,这个时候可以去 Notion 里看一下,它就有了新添加的页面,也就是留言。这里没有看到,应该是后端服务有问题了,看一下提示,Notion 返回的 API 调用结果显示没有 no 这个属性,那我们看一下 Notion 页面,这里 No 的 n 大写了,把它改成小写,再试一次。 再测试有关联关系的,也就是发表一条回复,用同样的配置,把数据改一下,加上 replyTo,里边的 id 随机设置一个现有的顶级留言的 id:

{
"content": "测试回复2",
"replyTo": "937f71ad-f9b4-48c9-9dac-273a702de088"
}

点击发送,在返回 201 之后,回到 Notion 看一下,新添加的留言如果有 replyTo 字段,并且对应的顶级留言的 replies 中,也包含了回复的引用,那就说明成功了。

小结

这期视频我们利用 Epxress 和 Notion API 编写了我们留言板项目的后台接口,整个流程是:

  1. 从 Notion 官网创建应用,获取 API KEY。
  2. 创建数据库页面、配置属性、获取数据库 id。
  3. 获取当前登录用户的 id。
  4. 安装 express、notion client、dotenv 依赖。
  5. 编写 express 代码,实现获取留言列表和发布留言 API。

Notion API 的结构比较复杂,所以这里适合做一些尝试性的项目,后期随着 Notion API 的改进和稳定性的提高,说不定就能作为一个 CMS 系统,给前端应用提供数据了。

好了,这个就是利用 Notion API 和 Express.js 编写留言板项目接口的过程,你学会了吗?如果有帮助请三连,想学更多有用的前端开发知识,请关注峰华前端工程师,感谢观看!

提示

一系列的课程让你成为高级前端工程师。课程覆盖工作中所有常用的知识点和背后的使用逻辑,示例全部都为工作项目简化而来,学完即可直接上手开发!

即使你已经是高级前端工程师,在课程里也可能会发现新的知识点和技巧,让你的工作更加轻松!

《React 完全指南》课程,连载中现只需 48 元(领取优惠券)点击查看详情。

《Vue 3.x 全家桶完全指南与实战》课程,包括 Vue 3.x、TypeScript、Vue Router 4.x、Vuex 4.x 所有初级到高级的语法特性详解,让你完全胜任 Vue 前端开发的工作。点击查看详情。

《React即时通信UI实战》课程,利用 Storybook、Styled-components、React-Spring 打造属于自己的组件库。

《JavaScript 基础语法详解》本人所著图书,包含 JavaScript 全面的语法知识和新特性, 可在京东、当当、淘宝等各大电商购买