最近想要用 Notion 记笔记,奈何 Notion 的标签分类功能确实不太好用…… 看了看其它文章中配置多级标签的繁杂流程之后,我觉得还是写一个插件比较靠谱……
本文主要介绍 Notion 简单的插件开发,编程语言使用 JavaScript,所以熟悉 JS 的同学应该很快就能上手。官网链接:https://developers.notion.com/docs/getting-started,英语好的同学自取。
1. Notion 数据库 Notion 的灵魂在于数据库,它是 Notion 中最强大也最复杂的功能。下面简单梳理一下 Notion 数据库会用到的知识,其它内容估计自己试试也能摸索出来。数据库官方文档:https://www.notion.so/zh-cn/help/category/databases。
1.1 数据库是什么 简单来说,数据库是一个包含了多个 Notion 页面的集合。数据库可以以多种视图动态展现数据库下的页面:
我们一般常用表格(Table)、看板(Board)、列表(List)三种视图,这里方便起见先用列表演示。列表中展示了数据库下所有的页面,初始化几个页面:
把它们在表格视图下展示就是这个样子。同样地,你也可以在表格中的“名称”一栏新建数据库页面。
1.2 页面属性 数据库中的页面与普通页面不一样,它具有属性(Property)。例如上图的标签、日期就都是属性。常用的属性类型有文本、单选、多选、状态等。
给页面添加属性可以在“表格”视图添加,也可以在页面内部添加:
(可能有些反直觉,但标签确实是用“多选”类型实现的)
那如何新建属性呢?点击表格视图下标题栏的“+”号,选择一个类型:
再根据提示操作即可。
简单布置一下,高级文章管理面板的感觉是不是出来了?
2. 创建你的第一个插件 终于进入正题了。这里将创建第一个 Notion 插件。全程只使用了一个 Notion API:数据库读取。
2.1 先决条件 一定的准备条件(虽然大部分同学可能已经拥有)
Node.js 及 NPM
一个 Notion 账号及默认工作空间
一个趁手的 IDE,这里以 VS Code 演示
2.2 在 Notion 中创建插件 (Notion 中,我们又将插件(Plug-in)称为集成(Integration))
打开集成页面 ,创建新集成;
填写基本信息(集成分为自用的内部集成和公共集成,免费版用户只能创建内部集成):
保存之后你会得到一个集成密钥。复制备用。
2.3 准备 Node.js 环境 用 VS Code 打开一个文件夹,输入以下内容初始化并安装依赖:
1 2 npm init npm install @notionhq/client
然后新建文件 index.js;
新建一个数据库。你可以自己添加内容,也可以复制官方模拟数据。然后在页面中复制 Database ID(如下图):
在数据库页面授权你的插件访问,不然请求会被拒绝:
然后就是 index.js 了~
打开 index.js,引入 Client:
1 const { Client } = require ("@notionhq/client" );
定义几个常量:
1 2 const NOTION_KEY = "xxx" ; const NOTION_DATABASE_ID = "xxx" ;
创建主函数(异步),并在结尾调用。创建新的 Client:
1 2 3 4 5 async function main ( ) { const notion = new Client ({ auth : NOTION_KEY }); } main ();
读取数据库:
1 const response = await notion.databases .retrieve ({ database_id : NOTION_DATABASE_ID }); console .log (response);
全貌:
1 2 3 4 5 6 7 8 9 10 11 12 const { Client } = require ("@notionhq/client" );const NOTION_KEY = "xxx" ; const NOTION_DATABASE_ID = "xxx" ; async function main ( ) { const notion = new Client ({ auth : NOTION_KEY }); const response = await notion.databases .retrieve ({ database_id : NOTION_DATABASE_ID }); console .log (response); } main ();
运行:
成功!
3. Notion 页面 API 插件与 Notion 的交互主要依靠 API(即前面提到的 Client)。这里简单介绍一下 Notion 的页面 API。
官方文档:https://developers.notion.com/reference/post-page
3.1 读取页面信息 做实验肯定需要原材料。所以打开数据库中的一个页面,随便写点什么:
刚刚的数据库有 Database ID,那么页面肯定有 Page ID。复制备用:
读取页面信息的 API 是 notion.pages.retrieve({page_id: string}),用上面的 Page ID 尝试一下:
1 2 3 4 5 6 7 8 9 10 11 12 const { Client } = require ("@notionhq/client" );const NOTION_KEY = "xxx" ;const NOTION_PAGE_ID = "xxx" ; async function main ( ) { const notion = new Client ({ auth : NOTION_KEY }); const response = await notion.pages .retrieve ({ page_id : NOTION_PAGE_ID }); console .log (response); } main ();
看看返回了什么:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 { object: 'page', id: 'your_page_id', created_time: '2024 -08 -14 T00: 13 : 00.000 Z', last_edited_time: '2024 -08 -14 T03: 00 : 00.000 Z', created_by: { object: 'user', id: '4 d41673d-b5c2-48 eb-b9bc-ab8171bb1aae' } , last_edited_by: { object: 'user', id: '4 d41673d-b5c2-48 eb-b9bc-ab8171bb1aae' } , cover: null , icon: null , parent: { type: 'database_id', database_id: 'xxx' } , archived: false , in_trash: false , properties: { '重要性': { id: 'YqII', type: 'select', select: [ Object] } , '状态': { id: 'eM%7 B%7 B', type: 'status', status: [ Object] } , '标签': { id: 'n%7 Dss', type: 'multi_select', multi_select: [ Array] } , '名称': { id: 'title', type: 'title', title: [ Array] } } , url: 'https: public_url: null , request_id: '25 d4c385-3100 -4 c19-bc34-27 aeebb6e5d8' }
再看看 Property,挑一个重要性来获取吧:
1 2 3 4 5 6 7 8 9 10 11 12 const { Client } = require ("@notionhq/client" );const NOTION_KEY = "xxx" ;const NOTION_PAGE_ID = "xxx" ;async function main ( ) { const notion = new Client ({ auth : NOTION_KEY }); const response = await notion.pages .retrieve ({ page_id : NOTION_PAGE_ID }); console .log (response.properties .重要性.select ); } main ();
返回正常:
1 { id: 'utS@', name: 'Very Important', color: 'red' }
3.2 读取页面信息 如果你们有看一些 Notion 的使用教学视频,就一定会听到一句话:在 Notion 中,万物皆为块。页面亦然。所以如果我们要读取页面内容,就要使用页面的另一层身份:块。
有关块信息的官方文档:https://developers.notion.com/reference/patch-block-children
块信息获取的 API:notion.blocks.retrieve({ block_id: string }),其中 Block ID 就是块的 ID。而页面的 Block ID 就是 Page ID。
尝试一下:
1 2 3 4 5 6 7 8 9 10 11 12 const { Client } = require ("@notionhq/client" );const NOTION_KEY = "xxx" ;const NOTION_PAGE_ID = "xxx" ;async function main ( ) { const notion = new Client ({ auth : NOTION_KEY }); const response = await notion.blocks .retrieve ({ block_id : NOTION_PAGE_ID }); console .log (response); } main ();
返回:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 { object: 'block', id: 'your_page_id', parent: { type: 'database_id', database_id: 'xxx' } , created_time: '2024 -08 -14 T00: 13 : 00.000 Z', last_edited_time: '2024 -08 -14 T03: 00 : 00.000 Z', created_by: { object: 'user', id: '4 d41673d-b5c2-48 eb-b9bc-ab8171bb1aae' } , last_edited_by: { object: 'user', id: '4 d41673d-b5c2-48 eb-b9bc-ab8171bb1aae' } , has_children: true , archived: false , in_trash: false , type: 'child_page', child_page: { title: 'Article 1 ' } , request_id: 'f3cd2746-ca65-4126 -9 b37-522 b653c9e26' }
页面内容呢?还有另一个 API:获取子块。notion.blocks.children.list({ block_id: string })
尝试一下:
1 2 3 4 5 6 7 8 9 10 11 12 const { Client } = require ("@notionhq/client" );const NOTION_KEY = "xxx" ;const NOTION_PAGE_ID = "xxx" ;async function main ( ) { const notion = new Client ({ auth : NOTION_KEY }); const response = await notion.blocks .children .list ({ block_id : NOTION_PAGE_ID }); console .log (response); } main ();
拿到子块的 Block ID 了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 { object: 'list', results: [ { object: 'block', id: '88625397 -c225-428 b-97 f4-fee545913d72', parent: [ Object] , created_time: '2024 -08 -14 T02: 59 : 00.000 Z', last_edited_time: '2024 -08 -14 T02: 59 : 00.000 Z', created_by: [ Object] , last_edited_by: [ Object] , has_children: false , archived: false , in_trash: false , type: 'paragraph', paragraph: [ Object] } , { object: 'block', id: '1 d83b0c4-4 a9f-4 fe9-b877-43 c5fd5810c5', parent: [ Object] , created_time: '2024 -08 -14 T02: 59 : 00.000 Z', last_edited_time: '2024 -08 -14 T03: 00 : 00.000 Z', created_by: [ Object] , last_edited_by: [ Object] , has_children: false , archived: false , in_trash: false , type: 'paragraph', paragraph: [ Object] } , { object: 'block', id: 'a47674ef-041 d-48 b5-a696-d23bc4a687b2', parent: [ Object] , created_time: '2024 -08 -14 T03: 00 : 00.000 Z', last_edited_time: '2024 -08 -14 T03: 00 : 00.000 Z', created_by: [ Object] , last_edited_by: [ Object] , has_children: false , archived: false , in_trash: false , type: 'paragraph', paragraph: [ Object] } , { object: 'block', id: 'cab3d9a8-40 cd-46 ed-9 ffa-25 b37ac3888e', parent: [ Object] , created_time: '2024 -08 -14 T03: 00 : 00.000 Z', last_edited_time: '2024 -08 -14 T03: 00 : 00.000 Z', created_by: [ Object] , last_edited_by: [ Object] , has_children: false , archived: false , in_trash: false , type: 'paragraph', paragraph: [ Object] } ] , next_cursor: null , has_more: false , type: 'block', block: { } , request_id: 'e51de75e-9696 -448 f-b71a-a86b4b31cabf' }
再获取一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const { Client } = require ("@notionhq/client" );const NOTION_KEY = "xxx" ;const NOTION_PAGE_ID = "xxx" ;async function main ( ) { const notion = new Client ({ auth : NOTION_KEY }); const response = await notion.blocks .children .list ({ block_id : NOTION_PAGE_ID , }); const ids = response.results .map ((e ) => e.id ); for (const i in ids) { const j = await notion.blocks .retrieve ({ block_id : ids[i] }); console .log (j.paragraph .rich_text ); } } main ();
Perfect~
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 [ { type: 'text', text: { content: 'Paragraph 1 ', link: null } , annotations: { bold: false , italic: false , strikethrough: false , underline: false , code: false , color: 'default' } , plain_text: 'Paragraph 1 ', href: null } ] [ { type: 'text', text: { content: 'Paragraph 2 ', link: null } , annotations: { bold: true , italic: false , strikethrough: false , underline: false , code: false , color: 'default' } , plain_text: 'Paragraph 2 ', href: null } ] [ { type: 'text', text: { content: 'Paragraph 3 ', link: null } , annotations: { bold: false , italic: true , strikethrough: false , underline: false , code: false , color: 'default' } , plain_text: 'Paragraph 3 ', href: null } ]
3.3 官方文档的阅读 本文不能列举出所有 API 的使用方法,具体详见前面给出的官方文档链接。
官方文档食用方法:以 Create a page 为例。(搭配英语好的大脑或浏览器翻译插件食用更佳)
左栏为说明及参数:
右栏为示例。上方为示例代码(JavaScript 要选上!)
下方为响应类型及响应内容(左边就是参数了)。
4. Notion 数据库 API 简单介绍一下 Notion 的数据库 API。
官方文档:https://developers.notion.com/reference/create-a-database
4.1 读取数据库信息 即上文提到过的第一个 API。
API:notion.databases.retrieve({ database_id: string });
示例就不用了,看上文就行。
4.2 查询数据库信息 API:notion.databases.query({ database_id: string, sort: array, filter: json })
database_id 大家都很熟悉,这里说一下其它两个。
首先是 sort。sort 是一个存储排序规则的数组。排序规则为形似 { property: ‘Name’, direction: ‘ascending’ } 的 JSON。property 为依据排序的属性,direction 为排序方向。ascending 为升序,descending 为降序。
详见 https://developers.notion.com/reference/post-database-query-sort
然后是 filter。filter 即过滤器,可以帮你过滤你不想要的页面。“and”为和,“or”为或。示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 { "and" : [ { "property" : "Done" , "checkbox" : { "equals" : true } } , { "or" : [ { "property" : "Tags" , "contains" : "A" } , { "property" : "Tags" , "contains" : "B" } ] } ] }
详见 https://developers.notion.com/reference/post-database-query-filter
5. Notion 富文本 Notion 中的文本基于富文本。在 Notion 中,无论是获取信息,还是插入信息,都离不开它。下面简单介绍它的使用。
官方文档:https://developers.notion.com/reference/block,https://developers.notion.com/reference/rich-text
Notion 块的显示取决于两个重要因素,一个是块类型,另一个就是内部富文本。主要的块类型详见官方文档:
如果没有特殊情况,一般的块类型为段落(Paragraph)。通常一个块的内容可以用如下方式定义:
1 2 3 4 5 6 7 { paragraph: { rich_text: [ ] } }
接着来说富文本。Notion 的富文本分为三类:文本、提及和等式。等式内容少用所以请自行查阅官方文档。
文本类的富文本主要结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 { type: "text" , text: { content: "some text" , link: null } , annotations: { bold: false , italic: false , strikethrough: false , underline: false , code: false , color: "default" } , plain_text: "some text" , href": null // 链接,少用 }
提及即 Notion 中近似于链接的内容,熟悉 Notion 的同学应该清楚。提及中最重要的部分是 ID。主要结构如下(以数据库提及为例):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 { type: "mention" , mention: { type: "database" , database: { id: "xxx" } } , annotations: { bold: false , italic: false , strikethrough: false , underline: false , code: false , color: "default" } , plain_text: "xxx" , href: "https://www.notion.so/xxx" }
下面是一个提及块的完整示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 { paragraph: { rich_text: [ { type: "mention" , mention: { type: "page" , page: { id: "xxx" , } , } , annotations: { bold: false , italic: false , strikethrough: false , underline: false , code: false , color: "default" , } , plain_text: "This is a test page" , href: "https://www.notion.so/" + id.replace("-" , "" ), } , ] , } ,
由于富文本很多内容可以复用,所以建议封装一下:
6. 实战 了解了一些 API 之后,我们就可以进入实战环节了。Notion 插件通常以浏览器插件 + Notion 集成的方式实现(或许也可以用油猴脚本?还没尝试过)。
我的 Notion 插件叫 TagPlus,顾名思义它用来增强 Notion 标签管理功能。它在本地运行得很不错:
最后,Enjoy it~
参考: Notion API 探索笔记(一) Notion Developers API Reference Notion Developers Guides