ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

打造e621/e926客户端:API调用、限流与收藏同步实战

2026/9/29 1:22:59 拓冰建站 浏览量
打造e621/e926客户端:API调用、限流与收藏同步实战 简介一款基于谷歌Flutter框架、使用Dart语言编写的移动客户端工程源码用于浏览和交互e621与e926两个在线图站适合移动端开发者、Flutter进阶学员以及希望自建图站客户端的爱好者。项目功能覆盖较全包括帖子和图池的浏览搜索、帖子修改与评论、图片上传下载、收藏夹与热门内容访问、标签Wiki查询、本地黑名单、DText富文本解析、视频播放、自动更新检查以及多主题切换基本覆盖此类工具型App的主要模块。压缩包共185个文件大小约29.59MB主体为108个Dart源码文件并配有PNG/JPG图片资源、Gradle及XML构建配置、iOS工程所需的Plist和Storyboard文件以及若干GIF、Swift、Kotlin、Shell等辅助脚本Android与iOS双端工程结构一目了然。目前已有3124人学习下载。通过研读该项目可以熟悉真实App中的页面路由、网络层封装、状态管理、富文本解析、主题定制与跨端打包等工程实现也可直接修改复用为个人定制客户端或作为Flutter综合实战项目用于技术分享与求职展示。1. e1547 是什么把 e621 与 e926 装进一个不扎手的手机壳e1547 是我对一个移动应用的项目代号它要干的事一句话就能说清让用户在手机上有一个专门为 e621 和 e926 两个站点服务的客户端而不是被迫在浏览器里放大缩小、反复回退和忍受无限的重新加载。这类图片站的网页端本身就是为桌面设计的手机上打开之后标签栏挤成一团、点开大图还要再等一次全屏渲染。移动应用把帖子流、标签搜索、收藏、评分过滤这些高频动作变成原生控件同时用服务器开放的 JSON 接口做数据源。适合谁适合那些经常按标签多条件组合搜图、需要把喜欢的帖子存到本地、或者单纯想用一个干净界面替代网页的人。2. 先定传输层e621 与 e926 的 API 调用和域名切换怎么做2.1 e621 和 e926 的关系不是两个系统是同一套数据的两种过滤视图开始写代码之前必须先把两个站点的关系理清。我一般会把它们理解成同一套 API 协议下的两个视图而不是两个完全独立的网站。e621 是完整内容的入口e926 则在域名层面直接过滤掉高评级内容只保留安全内容。对移动应用来说这意味着你在做域名切换时不能只是把 baseUrl 从https://e621.net换成https://e926.net还要考虑搜索词、缓存、评分过滤逻辑跟着一起切换。如果两个站点共用登录凭证那么收藏和投票这类账号操作也必须在同一套认证体系下工作。常见做法是保存同一份用户名与 API 密钥在请求头里用 Basic Auth 做身份声明。只读浏览不需要登录但一旦用户要收藏帖子、投票或者发表评论就必须带着认证信息去请求。开发阶段最省事的做法是先做一个站点开关把当前域名、当前检索词、当前评分下限都放到同一个状态里管理。我的路由设计是这样应用启动时默认走 e621 域名用户切换安全模式时域名换成 e926两个域名共用一个请求封装函数只是传入的 baseUrl 不同。这样后续所有请求都走同一个入口出问题时只需要看一层日志。2.2 最小可用的网络层User-Agent、超时和分页一个都不能少e621 这类站点对请求头非常敏感尤其是 User-Agent。空 UA 的请求会在入口层被直接拒绝根本到不了业务逻辑。我习惯为移动端请求封装一个统一的apiGet方法把 UA、超时和域名切换都收敛在一个文件里后续调试也只需要改这一个位置。// 移动端 API 请求核心站点开关 强制 User-Agent const String _e621Base https://e621.net; const String _e926Base https://e926.net; String currentBase _e621Base; Futurehttp.Response apiGet(String path, MapString, String params) async { final uri Uri.parse(currentBase path).replace(queryParameters: params); return http .get(uri, headers: { User-Agent: e1547/0.1.0 (Android; contact: devexample.com), Accept: application/json, }) .timeout(const Duration(seconds: 12)); }上面这段逻辑并不复杂但值得说的有三个地方。第一是 UA 串必须是“应用名/版本号 平台 联系方式”的组合联系方式可以放邮箱官方索引页面看到陌生 UA 时会先读这段信息。第二是超时设成了 12 秒移动网络下网络抖动很常见但如果超过 12 秒还没拿到响应继续等下去只会拖垮用户体验不如直接抛错让上层走重试或提示。第三是currentBase用全局状态保存站点切换时只需要重新赋值不需要改调用方。这个封装的缺点也很明显没有自动重试也没有把鉴权信息统一注入。如果后面要加登录态需要在这里继续扩展比如在 headers 里追加 Basic Auth。不过作为最小可用网络层它已经足够支撑第一版开发。2.3 每分钟 50 次的配额为什么必须在客户端做令牌桶e621 的 API 对请求频率限制得很死。我个人的经验是每 3 秒一个请求比较稳妥短时间突发的并发请求很容易触发 501 或者 429。移动端用户不会像爬虫一样疯狂请求但应用内部可能因为图片预加载、自动补全、分页预取同时发出多个请求这就会把自己挤到限流线上去。解决思路是在客户端做令牌桶意思是不管业务层想发多少请求网络层最多按固定速率放行。每次请求前先消费一个令牌令牌不足就直接等待。// 令牌桶限流让客户端保持慢速而不是依赖服务器最后兜底 class BooruLimiter { final int maxTokens; final Duration refillInterval; int _tokens; DateTime _lastRefill; BooruLimiter({this.maxTokens 10, this.refillInterval const Duration(seconds: 3)}) : _tokens 10, _lastRefill DateTime.now(); bool tryAcquire() { final now DateTime.now(); final elapsed now.difference(_lastRefill).inSeconds; if (elapsed 1) { _tokens (maxTokens elapsed).clamp(0, maxTokens); _lastRefill now; } if (_tokens 0) { _tokens - 1; return true; } return false; } }参数可以按你的实际使用习惯调整。maxTokens 10表示短时最多连续放行 10 次refillInterval 3 秒表示每 3 秒补充一个令牌。这个参数组合对移动端搜索场景够用翻页时连续加载十几页会出现轻微限流但用户体验上不是不可接受。如果想要更平滑可以把maxTokens加到 20但我不建议更大因为服务器端的阈值不会无限放宽。令牌桶代码本身没有依赖第三方库直接放在网络层里每次apiGet调用前先检查一次。如果tryAcquire()返回 false就把请求往后推迟而不是硬发出去。3. 把站点数据搬进手机帖子流、标签搜索与收藏3.1 帖子流与翻页用 page 游标做手机上拉加载e621 的帖子列表接口返回的是一个帖子数组移动端做无限滚动时最常见的设计是用 page 参数做分页。每次请求新的 page把返回的帖子追加到列表末尾。这里有一个容易出错的地方并发请求。用户快速上拉时界面可能会同时触发第 2 页和第 3 页的加载数据到达顺序不确定就会重复插入。我一般会用一个isLoading标志位拦截只有上一次请求完成之后才能发起下一次。代码结构大致是这样// 帖子列表加载每次只允许一个分页请求在跑 FutureListPost fetchPostPage(int page, ListString tags) async { if (_isLoading) return []; _isLoading true; try { final params { tags: tags.join( ), page: page.toString(), limit: 40, }; final res await apiGet(/posts.json, params); final data jsonDecode(utf8.decode(res.bodyBytes)); return data[posts] .mapPost((e) Post.fromJson(e)) .toList(); } finally { _isLoading false; } }这段代码有两个关键点_isLoading是实例变量它保证同一时间只有一个分页请求limit: 40是单页数量移动端一次加载 40 条比较平衡加载太少会频繁翻页太多则首屏时间变长。返回结构里每个帖子对象都包含文件 URL、预览图 URL、评分、标签列表、发布时间。移动端渲染的时候优先展示预览图点开大图时再到详情页加载原图。不要把原图 URL 直接塞进列表页的 Image widget否则移动网络下会卡顿到没法用手机流量也会被瞬间吃完。3.2 标签自动补全搜索框的手感和服务器字典联动图片站用户最常做的事情就是按标签搜索比如同时搜species:canine和rating:safe。手动输入标签容易打错所以搜索框一定要做自动补全。e621 提供了标签自动补全接口常见路径是/tags/autocomplete.json参数名在不同版本里略有差异有的是search有的是search[name_matches]开发时要对着文档确认一次。客户端不要每次键盘输入都发请求那会把限流额度快速消耗光。我会用 300 毫秒防抖用户停顿下来才开始请求并且把长度不足 2 个字符的输入直接忽略。// 标签自动补全防抖 最小字符限制 Timer? _debounce; void onTagQueryChanged(String raw) { _debounce?.cancel(); final query raw.trim(); if (query.length 2) return; _debounce Timer(const Duration(milliseconds: 300), () async { final res await apiGet(/tags/autocomplete.json, {search: query}); final data jsonDecode(res.body) as List; tagSuggestions.value data.take(10).toList(); }); }逻辑上用的是Timer做防抖取消上一次未执行的请求后再发起新的请求。为什么取前 10 条移动端键盘弹起后屏幕空间有限下拉框展示 10 条刚好一屏再多就要滚动交互太重。300 毫秒是输入停顿的普遍阈值再长会觉得补全迟钝再短就会频繁打断输入。3.3 收藏管理远端收藏与本地数据库的双向映射收藏是这个移动应用最核心的闭环。用户浏览帖子时点收藏应用要立刻在 UI 上更新状态同时还需要定期和远端同步因为用户可能在网页端也收藏了同一张图。常见做法是拉取收藏时读取远端收藏列表拿到帖子 ID 后写入本地 SQLite 表下次启动时先读本地缓存再后台刷新远端。这样避免了每次打开应用都要等网络请求。-- 本地收藏表以帖子 ID 为主键站点字段用于区分 e621 与 e926 的同一帖子 CREATE TABLE IF NOT EXISTS favorite ( post_id INTEGER NOT NULL, site TEXT NOT NULL DEFAULT e621, created_at TEXT, PRIMARY KEY (post_id, site) );这张表里最容易被忽略的是site字段。e621 和 e926 同一张帖子可能共享同一个 ID但两个站点对文件的分辨率、过滤和可见性处理不同如果不加站点区分用户可能在 e926 收藏了一张图切到 e621 时同一 ID 显示成另一张图很容易引发困惑。互补的做法是每次收藏时把来源站点写死这样切换域名不会影响收藏列表。4. e1547 避坑记录开发 e621 e926 客户端时我踩过的 4 个真实坑4.1 没有 User-Agent 的请求一上来就是 HTTP 403现象客户端第一次连服务器请求帖子列表时很快就遇到 403。控制台打印响应体没有业务错误信息只有一行提示服务端拒绝了这次请求。原因e621 对 User-Agent 极其严格。没有 UA、UA 为空串、或者长度太短都会被挡在入口层。很多移动开发框架自带的 HTTP 客户端会默认发送一段通用的 UA但那段 UA 并不满足图片站的要求。解决统一在网络层强制设置 UA格式必须包含应用名、版本号、平台信息和联系方式比如e1547/0.1.0 (Android; contact: devexample.com)。同时注意不要在业务层覆盖这个 Header避免调试时又被自己人改回去。4.2 页面滚得快一点就返回 501 限流别把它当服务器故障现象列表页快速上拉时前几分钟一切正常翻了十几页之后接口开始返回 501。按照经验判断是频率超标但明明每次请求间隔都超过 2 秒。原因请求间隔的“单请求”视角只是表象。翻页时应用自动触发了原图预加载图片资源和 JSON 请求同时发出令牌桶里积攒的配额很快被一次突发请求耗尽。多请求并发导致实际请求数大于预估。解决把预加载和分页请求做统一限流或者干脆把并发下载队列拆开。我一般会给图片下载单独设一个并发阈值为 2 的队列而 JSON 请求走另一条路径。两条通路互不争抢限流判断也更有底气。如果服务器依然返回 501客户端要退避等待至少 3 秒再重试不要立即重发。4.3 文件名正确图片在下次打开时黑屏现象收藏过的帖子文件下载到本地时文件名是正确的但过几天再从收藏夹打开图片区域只显示黑色块。重新下载又可以显示过几天又黑屏。原因缓存主键设计漏了站点维度。e621 和 e926 对同一个帖子可能提供不同的文件地址我使用帖子 ID 做缓存文件名结果在 e926 下载的文件被 e621 的帖子 ID 映射覆盖。文件内容格式出问题后显示端直接渲染失败。解决缓存和收藏一样都要把文件名与“站点 帖子 ID”绑定。我改成了${site}_${postId}.jpg的方式同时把文件实际格式信息写到 meta 文件里显示端先读 meta 再按格式加载。这个改动看起来小但避免了大量黑屏投诉。4.4 切换到 e926 后搜索结果“空了一截”不是 Bug是过滤在起作用现象用户从 e621 切到 e926 之后同一个标签搜索词返回的帖子明显变少有的搜索词甚至一页都没有。原因e926 在服务端做了评分过滤安全评分以下的帖子直接不返回。这不是 Bug而是两个站点的机制差异。但用户并没有心理准备容易以为应用坏了。解决客户端要感知当前站点的评分基线。当搜索结果数量极少或为空时在页面底部显示一条提示说明“当前为 e926 安全站点已过滤高评分内容”。不要硬把空列表渲染成数据为零要给用户一个明确的行动暗示比如切换到 e621 继续查看。这个提示文案不需要复杂但一定要存在。4.5 登录态失效收藏、投票时冷不丁弹出重新认证现象用户收藏第一张图时正常连续收藏五六张后突然收到 401。旧版应用直接报错没有引导用户重新登录。原因服务端对 API Key 的校验会随着权限更新或账号状态变化而失效不是做一次登录就能一劳永逸的。移动端没有网页端 Cookie 那么长的生命周期需要主动管理登录凭证。解决在认证失败时拦截 401 响应弹出一次性页面引导用户重新进入账号设置页输入用户名和新的 API Key。不要把旧 Key 继续存着反复试浪费请求次数。同时可以本地保留一份上次成功同步的收藏快照在用户重新登录之前阅读体验不受影响。5. 进阶可复用的小技巧为 e1547 做一个“按热度掉落”的推荐流5.1 用 order:score 和时间窗组合出每日推荐只依赖用户手动搜索会让应用显得很被动。我习惯在应用里加一个“热门”入口实现起来不需要单独做一个推荐系统而是用搜索参数堆出来的。核心是order:score配合时间窗限定当天上传的帖子按评分排序。// 每日热门当天上传、评分超过 50 的帖子按热度排序 String hotQuery() { final now DateTime.now(); final startOfDay DateTime(now.year, now.month, now.day); return order:score score:50 uploaded:${startOfDay.millisecondsSinceEpoch ~/ 1000}; }这段查询逻辑的关键是把uploaded设为当天零点的时间戳再配合score:50过滤掉冷门内容。实际效果不是真正的个性化推荐但比完整推荐系统简单得多而且不会额外消耗接口配额。移动应用开发里这种“用参数组合替代算法”的做法很常见维护成本低效果也直观。5.2 离线收藏与下载队列给二次打开一颗后悔药我喜欢在实现收藏的同时做一个批量缓存队列。用户收藏帖子后应用自动把原图加入下载队列但不要一下子全塞进去否则限流和手机电量都会出问题。控制并发在 2 个任务左右等一个完成再拉下一个。这给用户带来一个很实在的好处没有网络时也能打开收藏看缓存。最后的收尾习惯是每次发布前我会在真机上把网络切到 4G 环境完整走一遍搜索、翻页、收藏、切换站点再切回来的流程。这个流程能暴露很多模拟器里发现不了的问题比如 UA 头被系统组件改写、分页并发触发限流、缓存文件因站点切换导致黑屏。把这些问题全部打掉之后这个移动应用才算真正能交到用户手里。希望帮到你。本文还有配套的精品资源点击获取