Ed Web 是一个静态网页:直接从 Ed 的接口抓一门课的 Ed Discussion 论坛,存进浏览器的 IndexedDB,离线也能看、筛选和搜索。没有后端,token 存在 sessionStorage 里,只发给 edstem.org。
它是我自己服务器上一个私人学习工具的公开版。10 月 25 日我给 Ed Web 改了版,把私人工具里能在浏览器里做的部分搬了过来(#6)。这篇讲这几部分。
只刷新变了的帖子
旧版每次刷新都把所有帖子和回复重抓一遍。大的课有几百个帖子,Ed 又限流,刷新要等很久,有时还会遇到 429。
Ed 的帖子列表很便宜,100 个帖子一个请求。抓一个帖子的回复则是一个帖子一个请求。所以现在刷新先读列表,给每个帖子算一个指纹,只有指纹变了才重抓整个帖子:
/* 跟私人工具的 content_stamp 一样:updated_at 之外再带几个计数和状态位,
* 这样只有内容真变了才重抓正文。 */
function contentStamp(t) {
return [t.updated_at, t.reply_count, t.unresolved_count, t.vote_count, t.is_staff_answered,
t.is_answered, t.is_locked, t.title].map((x) => String(x ?? '')).join('|');
}
只看 updated_at 不够。新回复或者老师回答不一定会改它,但一定会改某个计数或状态位。
Ed 真的返回 429 时,浏览器能读到 Retry-After 就按它等,读不到就依次等 1.5、3、4.5 秒。
“新”是什么意思
“新”标记只有在表示“上次看过之后才出现的”时才有用。现在每个帖子和回复都存一个 first_seen,也就是这个浏览器第一次见到它的时间。每次刷新记下开始的时刻,在那之后第一次见到的就是新的:
function isFresh(firstSeen) {
return !!STATE.prevSyncedAt && !!firstSeen && firstSeen >= STATE.syncedAt;
}
第一次抓的时候没有可比的,就退回“48 小时内发的”。以前就有的帖子,显示有几条新回复。
安全地渲染 Ed 的正文
Ed 用自己的 XML 存帖子:<document><paragraph>...,里面有 <link>、<snippet>、<image>、<file>、列表、表格和提示框。旧版把它全部转成纯文本,代码的格式乱了,附件也没了。
现在 docToHTML 用 DOMParser 解析,再遍历整棵树。有两条规则保证安全:
- 每一段文字写进页面之前都经过
esc,帖子里的任何标签都不会原样输出。 - 链接、图片、附件和视频,只有地址以
http://或https://开头才做成链接。其他的,比如javascript:,只显示成文字。
const safeHref = (u) => (/^https?:\/\//i.test(String(u || '').trim()) ? String(u).trim() : null);
XML 解析失败的话,退回纯文本,而不是显示一张空卡片。
一句话结论取对的人
每张帖子卡片在标题下显示一句话,以前是帖子的开头。对提问帖来说,有用的通常是回答,所以现在取最后一条老师的回复,因为后面的老师回复经常会更正前面的。没有老师回复的提问帖,显示同学的回答,并标明是同学答的。其他帖子显示帖子本身。
API 令牌
Ed 有两种 token。网页版存在 localStorage 里的是 JWT,14 天过期。在 Ed 设置里生成的 API 令牌不会过期,可以在那里撤销。两种用的请求头不一样:
function authHeaders(token) {
return tokenKind(token) === 'jwt' ? { 'x-token': token } : { authorization: 'Bearer ' + token };
}
页面会从 JWT 的 exp 字段读出还剩几天。API 令牌不会过期,适合每周都要打开的页面。这和私人工具里的处理一样,我自己用的就是 API 令牌。
其他小改动
- 搜索要求每个词都出现,不是只要有一个。
- 可以按收藏、关注、我发的筛选,也可以筛出 Ed 记录里你还没看过回复的帖子。
- 被删回复下面的子回复保留,因为 Ed 网页上也还显示。
- 课程内容会存阅读页的正文和测验题,也能搜索。
没有后端的页面怎么测
抓取和渲染的逻辑都在一个 app.js 里,没有构建步骤。tests/sync.test.mjs 按标记字符串把需要的函数从 app.js 里切出来,在 Node 里执行,不连网。README 里的截图是对着一个假的 Ed 接口拍的,课程、人和帖子都是编的,仓库里没有任何真实的课程数据。
小结
- 一个便宜的列表,加上每一项的指纹,就能把全量刷新变成小刷新。
- “新”应该从上次刷新算起,而不是从发帖时间算。
- 渲染别人写的标记时,所有文字都转义,只放行你想要的链接协议。
代码:app.js、ARCHITECTURE.zh-CN.md、sync.test.mjs。使用指南:docs.secureview.tech/zh-cn/ed-web。