前段时间把说说平台从 0.29.x 升到了 Memos 0.31.0,接口几乎全换了一遍。旧代码直接跑不通,最典型的就是列表接口的 parent 参数没了、附件字段从 resources 改名成 attachments。这篇就把升级后实际能用的调用方式逐个拆开讲,示例全部对着线上实例跑过,附接口路径、字段说明、返回结构和错误码,文末还有这次踩到的两个坑——其中一个是防火墙拦的,不是 Memos 的锅。

一、0.31.0 到底变了什么

先说结论:这版不是小修,是把 API 的资源定位方式整个换了一遍。 主要变化有这么几条。

1. 列表接口不再收 parent,改用 CEL filter

旧版查某人的说说,是把作者信息塞在路径里:

1
2
# 旧版(0.29.x):作者塞进路径
GET /api/v1/memos?parent=users/123

0.31.0 把这个参数拿掉了,换成一个 CEL 表达式风格的 filter 查询参数:

1
2
# 0.31.0:作者变成筛选条件
GET /api/v1/memos?filter=creator == "users/123"

这一点很关键——creator 的值是资源名 users/{username},不是数字 ID。这一点和旧版不一样,也是最容易踩的地方。我这边实例升级后实测,users/1 这种数字 ID 已经查不到了,直接返回 user not found。

2. 附件字段更名

Memo.resources 改成了 Memo.attachments,附件的资源名格式和文件路由也跟着变了:

项目旧版0.31.0
字段名resourcesattachments
附件资源名resources/{uid}attachments/{uid}
文件地址/api/v1/resources/{uid} 类/file/attachments/{uid}/{filename}

3. 头像地址移出 API

旧版走 /api/v1/users/{id}/avatar,这版统一挪到了文件路由:

1
2
旧:/api/v1/users/{id}/avatar     → 实测 404
新:/file/users/{username}/avatar → 实测 200

4. 几个版本级别的大改动

除了上面这些实测差异,官方 Release 里还提到几个影响较大的变更,简单列一下:

  • Shortcuts 改名 Saved Views:API 从 users/*/shortcuts/* 变成 users/*/views/*,老客户端必须跟着改
  • 评论可见性独立:CreateMemoComment 不再继承主 memo 的可见性,不传就默认 PRIVATE
  • Reaction 移除 contentId:只能从请求路径或资源名里拿 memo 标识
  • 管理员不再是隐式协作者:写操作全部走作者权限校验,管理员也改不了别人的 memo

二、调用前提:实例必须是公开访问

这一条得单独拎出来讲,因为它是所有后续问题的根源。

Memos 的实例访问策略和它的公开 URL 是两件事。实例权限如果是「公开访问」,匿名请求才能读到 PUBLIC 的 memo;如果是「私有访问」,不登录就是 authentication required。

我这边实测的状态码对照:

场景HTTP返回体
公开实例 + 匿名读 PUBLIC200正常列表
未认证访问写操作401{"code":16,"message":"authentication required"}
带无效 token 读公开 memo200正常返回(token 被忽略)
未认证读用户列表401{"code":16,...}

理解这条链路很重要:请求从浏览器出去,先过反向代理,再过防火墙,最后才到 Memos 服务。中间那层防火墙是有自己规则的,后面第五节会讲它怎么坑了我。

三、接口逐个说明

3.1 列出 memo —— 核心接口

这是用得最多的接口,也是这版改动最狠的地方。

请求

1
GET https://s.dusays.com/api/v1/memos?filter=creator == "users/penn"&pageSize=10
参数类型说明
filterstringCEL 表达式,筛选条件
pageSizeint每页条数,不传默认 50,非法值也回落成 50
pageTokenstring上一页返回的 nextPageToken

实测 pageSize 的行为:pageSize=0 返回 50 条,pageSize=1000 也能正常给 1000 条,没有上限保护,所以别拿它当无脑拉全量的工具——真有需要就循环 pageToken。

返回

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
{
"memos": [
{
"name": "memos/g48Yif2Bd66WvDfuFkeSwe",
"state": "NORMAL",
"creator": "users/penn",
"createTime": "2026-10-11T18:45:04Z",
"updateTime": "2026-10-11T18:45:04Z",
"content": "每日60秒简报。",
"visibility": "PUBLIC",
"tags": [],
"pinned": false,
"attachments": [],
"relations": [],
"reactions": [],
"property": {
"hasLink": false,
"hasTaskList": false,
"hasCode": false,
"hasIncompleteTasks": false,
"title": ""
},
"snippet": "每日60秒简报。"
}
],
"nextPageToken": "CAEQAQ=="
}

几个字段值得单独说:

  • name 是 memo 的资源名,格式 memos/{uid},后面查详情、改内容全靠它
  • creator 是资源名 users/{username},不是数字 ID
  • property 是这版新加的预解析字段,前端要显示「这条有没有链接 / 有没有待办 / 有没有代码块」,不用再自己解析一遍 Markdown,直接读它就行
  • snippet 是纯文本摘要,适合做列表预览

3.2 filter 能用什么,不能用什么

这是最容易踩坑的地方。filter 不是随便写都能过的,我把这版实测能用的表达式列一下。

可以正常用的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 按作者
filter=creator == "users/penn"

# 链式条件,注意是 && 不是 &
filter=creator == "users/penn" && visibility == "PUBLIC"

# 多值匹配
filter=creator == "users/penn" && visibility in ["PUBLIC","PROTECTED"]

# 标签过滤 —— 注意写法,见下文
filter=creator == "users/penn" && "memos" in tags

# 正文模糊匹配
filter=creator == "users/penn" && content.contains("简报")

# 标签数量判断
filter=creator == "users/penn" && size(tags) > 0

不能用的(会直接报错):

写法结果
create_time > "..."400 undeclared reference to 'create_time'
createTime > "..."400 undeclared reference to 'createTime'
state == "NORMAL"400 undeclared reference to 'state'
has(tags)400 invalid argument to has() macro
tags == tags400 field "tags" does not support comparison

注意几个字段名和返回体里的驼峰不一样:返回体是 createTime,但筛选表达式里根本没有时间字段可筛。state、createTime、visibility 之外的字段基本都不可筛。

还有个坑:tag 字段的 exists 宏在某些环境下会被防火墙拦掉(第五节细说),所以标签过滤要换种写法。

3.3 分页:nextPageToken

这版没有 offset 页码,只用游标。逻辑很简单:拿上一页返回的 nextPageToken 当请求参数传回去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let pageToken = '';
let all = [];

do {
const url = new URL('https://s.dusays.com/api/v1/memos');
url.searchParams.set('filter', 'creator == "users/penn"');
url.searchParams.set('pageSize', '100');
if (pageToken) url.searchParams.set('pageToken', pageToken);

const res = await fetch(url);
const data = await res.json();
if (data.code) { throw new Error(`Memos API ${data.code}`); }

all.push(...data.memos);
pageToken = data.nextPageToken || ''; // 空串就是最后一页
} while (pageToken);

两个注意点:

  1. nextPageToken 必须原样回传,不要解码也不要拼路径。它是 Base64 变体,里面带 ==,直接 encodeURIComponent 就行
  2. nextPageToken 是空字符串表示到底了,不是 null 也不是缺字段。判断结束条件要用 !pageToken

实测这个游标是稳定的,连着翻两页拿到的 name 不重复、不跳条。

3.4 查单条 memo

1
GET /api/v1/memos/{uid}
1
curl https://s.dusays.com/api/v1/memos/g48Yif2Bd66WvDfuFkeSwe

返回结构和列表里的一条完全一致。查不到的时候:

1
{"code":5,"message":"memo not found","details":[]}

HTTP 状态是 404,注意这版错误响应体统一带 code 字段,而且错误的时候 HTTP 状态码和 body 里的 code 是两套编号,别混:

情况HTTPbody.codebody.message
资源不存在4045memo not found
路由不存在4045Not Found
用户不存在4045user not found
filter 语法错4003invalid filter: ...
未认证40116authentication required

这版是 gRPC 风格的错误码(5 = NOT_FOUND、3 = INVALID_ARGUMENT、16 = UNAUTHENTICATED),不是 HTTP 码直接搬过来。

3.5 用户信息与头像

1
GET /api/v1/users/{username}
1
2
3
4
5
6
7
8
9
10
11
12
{
"name": "users/penn",
"role": "ADMIN",
"username": "penn",
"email": "",
"displayName": "Teacher Du",
"avatarUrl": "/file/users/penn/avatar",
"description": "杜老师说博主",
"state": "NORMAL",
"createTime": "2023-02-02T11:53:41Z",
"updateTime": "2024-10-23T08:06:27Z"
}

两个和旧版不一样的地方:

  • 用户名精确匹配,大小写敏感。实测 GET /api/v1/users/Penn 直接返回 404 user not found,只有 users/penn 能命中,这版用户名不再做大小写归一化了
  • avatarUrl 是相对路径 /file/users/penn/avatar,拼接时别忘了加站点域名,而且要写成 /file/... 而不是 /api/v1/...

另外,用户名走 users/{username},数字 ID 这版已经失效——GET /api/v1/users/1 返回 404。老代码里存的用户 ID 全得换成用户名。

想批量拿头像信息的话,GET /api/v1/users 这个用户列表接口是需要认证的,匿名会返回 401。

3.6 memo 的子资源

挂在单条 memo 下的子资源,都用 memo 的资源名做父级:

子资源路径返回
附件GET /api/v1/memos/{uid}/attachments{"attachments":[...],"nextPageToken":""}
关联GET /api/v1/memos/{uid}/relations{"relations":[...]}
表情回应GET /api/v1/memos/{uid}/reactions{"reactions":[...],"nextPageToken":""}
评论GET /api/v1/memos/{uid}/comments{"memos":[...],"nextPageToken":""}

注意评论接口的返回字段叫 memos 而不是 comments——评论本身也是 memo,接口是复用的。想取附件文件本体,地址是 /file/attachments/{uid}/{filename},别再用旧的 resources 路径。

四、页面嵌入代码:完整调用结构

前面讲的是接口本身,这节是把它套到页面上的实际代码。我这边说说广场用的就是这套,跑在 Hexo 页面的 <article class="md-text content"> 里。

4.1 完整结构

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
<!-- 1. 样式:四个 CSS,顺序有讲究 -->
<link href="https://jsd.dusays.com/npm/penndu@21.0.0/memos/css/style.css" rel="stylesheet" type="text/css">
<link href="https://jsd.dusays.com/npm/penndu@21.0.0/memos/css/APlayer.min.css" rel="stylesheet" type="text/css">
<link href="https://jsd.dusays.com/npm/penndu@21.0.0/memos/css/highlight.github.min.css" rel="stylesheet" type="text/css">
<link href="https://jsd.dusays.com/npm/penndu@21.0.0/memos/css/custom.css" rel="stylesheet" type="text/css">

<!-- 2. 容器:两个必备节点 -->
<section id="main" class="container">
<div class="total">总计 <span id="total">0</span> 条说说</div>
<div id="memos" class="memos"></div>
</section>

<!-- 3. 配置:必须在 main.js 之前定义 -->
<script type="text/javascript">
var memo = {
host: 'https://s.dusays.com/', // Memos 网址,末尾带 /
limit: '1', // 每页条数,默认 10
creatorId: 'penn', // Memos 用户名(v0.31 起 creator 资源名用用户名)
domId: '#memos',
username: 'penn', // 前端显示的 ID
name: 'Teacher Du', // 前端显示的全名
language: 'zh-CN',
total: true, // 是否显示总条数
doubanAPI: '' // 豆瓣 API,不填则只显示 URL
}
</script>

<!-- 4. 依赖:marked 必须在 main.js 之前 -->
<script type="text/javascript" src="https://jsd.dusays.com/npm/penndu@21.0.0/memos/js/marked.umd.min.js?v=14.0.0"></script>
<script type="text/javascript" src="https://jsd.dusays.com/npm/penndu@21.0.0/memos/js/view-image.min.js"></script>
<script type="text/javascript" src="https://jsd.dusays.com/npm/penndu@21.0.0/memos/js/APlayer.min.js"></script>
<script type="text/javascript" src="https://jsd.dusays.com/npm/penndu@21.0.0/memos/js/Meting.min.js"></script>
<script type="text/javascript" src="https://jsd.dusays.com/npm/penndu@21.0.0/memos/js/custom.js"></script>
<script type="text/javascript" src="https://jsd.dusays.com/npm/penndu@21.0.0/memos/js/main.js"></script>

加载顺序有硬约束:配置对象 memo 必须在 main.js 之前定义(main.js 顶层直接读 memo.host,配置没到位会抛 ReferenceError),marked.umd.min.js 也要在前面,否则正文 Markdown 渲染不出来。custom.js 和 custom.css 都是空文件占位(实测 custom.js 返回 0 字节),留给你放自己的覆盖逻辑。

4.2 配置项逐个说明

这里有个必须说的事:配置里十个字段,main.js 实际只读了三个。

字段是否被 main.js 使用说明
host✅Memos 实例地址,末尾 / 会被自动去掉(replace(/\/+$/, ''))
limit✅每页条数,会过 parseInt,NaN 或 < 1 回落成 10
creatorId✅Memos 用户名,拼成 users/{creatorId} 塞进 filter
domId❌容器选择器没被读,实际写死在代码里
username❌未被读取
name❌未被读取
language❌未被读取
total❌未被读取
doubanAPI❌未被读取

我 grep 过 main.js 全文(158 行),里面出现的配置引用只有 memo.host、memo.limit、memo.creatorId 三处。所以后面那六个字段目前是声明了但不生效——留着无害(将来版本可能用上),但别指望改它们能改变行为。

limit 的默认值行为要留意:limit: '1' 是字符串,会被 parseInt 成数字 1。传 '0'、'abc' 或负数都会回落到 10,不报错。

4.3 main.js 里的请求是怎么拼的

核心就一个 buildListUrl(),把配置和筛选条件拼成最终 URL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var limit = parseInt(memo.limit, 10);
if (isNaN(limit) || limit < 1) { limit = 10; }
var base = memo.host.replace(/\/+$/, '');
var creatorName = 'users/' + memo.creatorId;

function buildListUrl(pageToken, tagName) {
var filter = 'creator == "' + creatorName + '"';
if (tagName) {
filter += ' && tags.exists(t, t == "' + tagName + '")';
}
var url = base + '/api/v1/memos?filter=' + encodeURIComponent(filter) + '&pageSize=' + limit;
if (pageToken) { url += '&pageToken=' + encodeURIComponent(pageToken); }
return url;
}

分页逻辑是预取下一页:loadFirst() 拿到第一页后,如果 nextPageToken 非空就立刻调 preloadNext() 把第二页拉进 nextDom,用户点「加载更多」时直接渲染已缓存的 nextDom,感知不到延迟。终止条件有两个:返回条数少于 limit,或者 nextPageToken 为空串。

4.4 ⚠️ 这套代码当前有三个问题

写在这里不是要拆台,是这几个坑排查时踩过,别人复用大概率也会撞上。这三个都出在 CDN 上的 penndu@21.0.0,本地仓库没有源码。

一、标签过滤必然 403。 就是第五节坑一那个问题——buildListUrl() 里用的是 tags.exists(t, t == "..."),而 exists( 会被防火墙拦。改成 "' + tagName + '" in tags 即可,两种写法实测返回条数一致。

二、main.js 在 CDN 上是截断的。 实测该文件只有 5912 字节、158 行,且最后一行停在 } else { 后面:

1
2
} else {
re

文件到这儿就没了——renderMemos() 函数没有定义完。浏览器解析到这儿会抛 SyntaxError: Unexpected end of input,整个 main.js 都不会执行,页面容器 <div id="memos"> 会一直是空的。我抓了线上说说页的 HTML 确认:容器确实是 <div id=memos class=memos></div> 空标签,<span id="total"> 也还是初始值 0,说明渲染没跑起来。

三、memoDom 永远是 null。 全文只有 var memoDom = null; 这一处赋值,之后标签点击时却直接用 memoDom.innerHTML = '':

1
2
3
var memoDom = null;        // 全文唯一一次赋值
...
memoDom.innerHTML = ''; // 后面直接用 → TypeError

domId 配置项本来应该用来给 memoDom 赋值,但因为 main.js 被截断在 renderMemos() 内部,那段赋值代码没被上传,所以点标签会抛 TypeError。这三个问题里,第二个是根因——补齐 main.js 后,后两个大概率一并解决。

所以现在这份代码贴出来是「存档 + 说明」性质的,真要用建议先核对这几点。

五、两个实打实的坑

前面讲的接口细节都是「怎么用」,这节讲「哪些地方会莫名其妙失败」。

坑一:防火墙把 exists( 拦了

这个坑排查了挺久,起因是我这边标签过滤点了没反应,控制台报错才注意到——返回的根本不是 JSON,是 1Panel 的拦截页:

1
2
<div class="text">请求携带恶意参数 已被拦截</div>
<div class="footer">由 <a class="link" href="https://1panel.cn">1Panel</a> 提供防护</div>

HTTP 状态 403。我把规则缩小到了很具体的一步:

filter 内容结果
tags.exists(t)403 被拦
exists(t)403 被拦
foo.exists(t)403 被拦
tags.exists400 正常报错(没到 WAF)
"读书" in tags200 正常
size(tags) > 0200 正常
content.contains("简报")200 正常

规律很清楚:只要 exists 后面紧跟左括号 exists(,就被拦。 不带括号的 tags.exists 能过(虽然它本身语法也不对,会返回 400)。而且这层防护拦的是请求参数,跟路径无关——我试过 ?x=exists(t) 一样 403。

所以标签过滤要改成 in 写法:

1
2
3
4
5
// 会被 403
'creator == "users/penn" && tags.exists(t, t == "memos")'

// 正常返回
'creator == "users/penn" && "memos" in tags'

两种写法实测返回条数一致,语义也等价。如果你自己也是套了防护面板,自查一下有没有把 CEL 表达式里的关键字误伤。 好消息是 in 写法本身语法更短,也不挑环境,比 exists 更值得当默认写法。

这个不算 bug,算个语义陷阱。

1
2
3
4
5
6
7
8
# 基线:前 100 条里 hasLink 都是 false
filter=creator == "users/penn" → 100 条,全部 hasLink=false

# 筛「没有链接的」
filter=creator == "users/penn" && has_link == false → 0 条

# 筛「有链接的」
filter=creator == "users/penn" && has_link == true → 5 条

基线里明明全是 hasLink=false,筛 false 反而一条都不出。翻这 5 条命中的记录,property.hasLink 字段确实都是 true,内容也确实带链接。所以这个字段是能筛的,但 false 这个取值的行为跟直觉对不上,用之前先拿小样本验一下,别直接拿去做生产筛选。

顺带一个:页面大小没有上限

pageSize=1000 实测真的返回 1000 条,没有服务端截断。这个在自用场景无害,但如果你的调用方不可信,一个 pageSize=100000 就能把内存打满。自己在前面兜一层就行。

六、代码错误怎么兜

这一节把前面的错误码整理成可直接用的处理逻辑。核心原则:先判 HTTP 状态,再判 body 里的 code。

通用错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
async function callMemos(path, params = {}) {
const url = new URL('https://s.dusays.com' + path);
for (const [k, v] of Object.entries(params)) {
url.searchParams.set(k, v);
}

const res = await fetch(url);
const contentType = res.headers.get('content-type') || '';

// 关键:防护面板返回的是 HTML,不是 JSON
if (!contentType.includes('application/json')) {
if (res.status === 403) {
throw new Error('被防护面板拦截:检查 filter 里有没有会被误伤的表达式');
}
throw new Error(`非 JSON 响应(HTTP ${res.status})`);
}

const data = await res.json();

if (!res.ok || data.code) {
throw new Error(`Memos ${res.status}/${data.code}: ${data.message}`);
}
return data;
}

上面 !contentType.includes('application/json') 那个判断是实战里加的——不加的话,await res.json() 会直接抛一个语法错误异常,信息全丢了,根本看不出是被防火墙拦的。

按状态码分支

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
try {
const data = await callMemos('/api/v1/memos', {
filter: 'creator == "users/penn"',
pageSize: 20
});
render(data.memos);
} catch (err) {
switch (err.status) {
case 400:
// filter 语法错,最常见是 && 写成了 &、字段名拼错
console.warn('筛选表达式有问题', err.message);
break;
case 401:
// 实例是私有访问,或者写操作没带 token
console.warn('需要认证:检查实例访问模式');
break;
case 403:
// 防火墙误伤,或权限不足
console.warn('被拦截:检查表达式关键字');
break;
case 404:
// 资源名写错了,或者用了旧的 resources 路径
console.warn('资源不存在:确认资源名格式');
break;
}
}

关于 filter 语法错(400)

filter 报 400 时,错误消息里会带 CEL 编译器的原始报错,能直接看出错在哪。实测两种典型错法:

1
2
# 逻辑用错:单 & 不是「与」,是位与
filter=creator == "users/penn" & visibility == "PUBLIC"
1
2
3
4
{"code":3,"message":"invalid filter: failed to compile filter:
ERROR: <input>:1:23: Syntax error: token recognition error at: '& '
| creator == users/penn & visibility == PUBLIC
| ......................^
1
2
# 字段名用了返回体里的驼峰
filter=creator == "users/penn" && createTime > "2026-01-01T00:00:00Z"
1
{"code":3,"message":"invalid filter: ... undeclared reference to 'createTime'"}

注意看这个报错——它把整个 filter 表达式原样回显在消息里。如果你的 filter 里带了敏感信息,注意一下。

七、完整可用示例

把前面几节串起来,一个能直接跑的完整抓取脚本:

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
// 抓取某人全部公开 memo,支持标签筛选
async function fetchMemos({ host, creator, tag, pageSize = 100 }) {
const parts = [`creator == "${creator}"`]; // creator 必须是 users/{username}

if (tag) {
// 用 in 写法,不要用 tags.exists(),会被防护面板拦
parts.push(`"${tag}" in tags`);
}

const filter = parts.join(' && ');
const out = [];
let pageToken = '';

do {
const url = new URL(`${host}/api/v1/memos`);
url.searchParams.set('filter', filter);
url.searchParams.set('pageSize', String(pageSize));
if (pageToken) url.searchParams.set('pageToken', pageToken);

const res = await fetch(url);
const data = await res.json();
if (!res.ok || data.code) {
throw new Error(`[${res.status}] ${data.message}`);
}

out.push(...data.memos);
pageToken = data.nextPageToken || '';
} while (pageToken);

return out;
}

// 用法
const memos = await fetchMemos({
host: 'https://s.dusays.com',
creator: 'users/penn',
tag: 'memos' // 不传就是全量
});
console.log(`共 ${memos.length} 条`);

跑一下就知道条数对不对:

1
2
3
4
5
const list = await fetchMemos({
host: 'https://s.dusays.com',
creator: 'users/penn'
});
console.log(list.length); // 实测 2226 条

分页也顺手验一下,游标翻到底不重不漏:

1
// 实测:23 页 / 2226 条 / 去重后仍是 2226,无重复

八、迁移检查清单

从旧版升过来,按这个顺序过一遍基本不会漏:

  1. parent 全部换成 filter,作者写成 users/{username},别用数字 ID
  2. resources 全部换成 attachments,文件地址换成 /file/attachments/{uid}/{filename}
  3. 头像地址换成 /file/users/{username}/avatar
  4. 筛选表达式里的 exists( 换成 "标签" in tags,尤其自己套了防护面板的
  5. 用户名改成精确匹配,别再依赖大小写不敏感
  6. shortcuts 相关调用换成 views
  7. 评论请求显式指定可见性,别依赖继承
  8. 错误处理同时判 HTTP 状态和 body.code

九、写在最后

0.31.0 这版改动集中在一个点上:把所有「定位资源」的方式统一到资源名 + CEL filter 上了。 users/penn、memos/g48Yif2Bd66WvDfuFkeSwe、attachments/{uid} 这类资源名成了唯一入口,字段名也统一成了驼峰。

代价是迁移必须跟着改,好处是筛选能力上了一个台阶——过去只能拉全量再前端过滤,现在可以直接在服务端按作者、标签、可见性、正文内容筛。property 字段的预解析也省掉了前端一层 Markdown 解析。

前面第二节那个 pageSize 没上限、第五节那两个坑,都是实测撞出来的,不是文档里写的。0.31.0 的 API 文档还在跟着版本走,示例里 parent 这类老写法也能搜到不少,所以别完全照抄官方示例——跑一遍才知道合不合你的部署。

本文所有接口路径、字段名、返回结构、错误码和请求结果,都是对着线上实例逐个实测的(curl 直接打 s.dusays.com)。示例代码里的筛选表达式也都在真实环境下验证过可用性。

第四节那段嵌入代码是我自己说说广场在用的,贴出来时也顺手核了一遍——结果发现三个问题,其中 main.js 在 CDN 上是被截断的(5912 字节,renderMemos() 写了一半),这大概也解释了为什么线上说说页现在是空的。补齐这个文件之后,标签过滤和 memoDom 相关的两个问题应该能一起解决。


说说平台搬到了 Memos,接口细节和踩坑记录都写在这了,欢迎留言交流。

站内搜索

没有找到内容!