WebToApp 模块市场投稿指南:从 module.json 到审核上线的完整开发实战
【免费下载链接】web-to-appThe most full featured web-to-app toolkit on Android, a complete APK workshop that runs entirely on your phone项目地址: https://gitcode.com/GitHub_Trending/web/web-to-app
本篇指南以 WebToApp 仓库中modules/目录(即 WebToApp 官方模块市场)为对象,系统讲解市场的工作原理、模块清单(manifest)与索引(registry)的字段规则、main.js在 WebView 中的注入合约、本地校验工具以及维护者审核清单。读完本文,你将掌握在 WebToApp 模块市场上提交、审查并维护一个 JS/CSS 模块的完整流程,并能结合仓库源码理解安装与注入的底层实现。
模块市场是什么:一个由 Git 仓库直接驱动的应用内市场
modules/目录就是WebToApp 的模块市场。App 内置的市场页面中展示的所有模块,都是直接从这个文件夹通过raw.githubusercontent.com拉取的,并以cdn.jsdelivr.net/gh/作为 CDN 兜底。整个市场没有其他后端——一个 PR 一旦合并到main分支,下一刻就会上线,不存在发布窗口或人工上线操作。
本目录下的 modules/README.md 是模块市场投稿规则的唯一主文档,仓库根目录README.md与.github/CONTRIBUTING.md只保留高层摘要,真正的字段规则与提交流程均以它为准。
目录结构
modules/ ├── registry.json ← App 首次下载的索引文件 ├── submissions.json ← CI 生成:PR / 贡献者元数据 ├── README.md ← 投稿规则主文档(即本文所依据的文档) └── <module-path>/ ← 每个模块一个目录 ├── module.json ← 模块清单(必需) ├── main.js ← 模块源码(必需) ├── style.css ← 可选 CSS,加载时自动注入 └── icon.png ← 可选图标,最大 256 KB(也支持 .svg/.webp/.jpg)市场的读取与安装链路
当用户打开应用内市场时,App 会同时拉取registry.json和submissions.json,只渲染两边都出现的条目——这是"只展示已合并 PR"这一承诺的实现机制:注册表负责描述"有哪些模块",提交文件负责证明"哪些模块真的合并进了main"。
点击Install时,App 才会下载对应模块的module.json和main.js(若hasCss为true则连同style.css),并把结果交给本地扩展管理器完成安装。registry.json会被缓存一小时,市场页面上的刷新按钮可以绕过缓存强制更新。
图标的两套体系
registry.json和module.json中都存在icon字段,它接受一个 Material Icons 名称字符串(如"auto_awesome"、"dark_mode"),主要用于本地内置模块。若想使用真实的品牌图片,需要在注册表层级设置iconUrl:
- 相对路径(
"icon.png"):必须指向模块目录内与main.js同级的icon.png/icon.svg/icon.webp/icon.jpg/icon.jpeg之一,且文件小于 256 KB,否则会直接导致 CI 校验失败; - 绝对
https://URL:指向仓库外的图片托管地址; - 两个字段都不设置时,App 会用模块名的首字母自动生成一个圆形头像。
提交一个模块的完整流程
- Fork
shiaho777/web-to-app仓库。 - 在
modules/下新建一个唯一且为 kebab-case的目录,例如modules/dark-reader-lite/。这个文件夹名就是registry.json条目中的path。 - 目录内至少包含两个文件:
module.json— 模块清单,字段规则见下文 module.json schema;main.js— 在 WebView 中执行的模块代码。
- 在 modules/registry.json 中追加一条对应记录,并保证
id、name、version、runAt、permissions在两个文件之间保持一致(registry 是列表展示面,manifest 才是实际被安装的内容)。 - 提交 Pull Request。维护者按 审核 Checklist 审查后合并;CI 会自动校验整个
modules/目录,建议先运行 本地校验 自查。 - 合并后,所有客户端在下次刷新(默认 1 小时缓存)即可看到新模块。
整个提交流程没有独立的开发者账号、API Key 或投稿门户——Fork 改代码提 PR 即是全部。
module.json schema
module.json是模块的完整清单,App 安装时以它为真实依据。完整的示例:
{ "id": "globally-unique-id", "name": "Display Name", "description": "Paragraph shown on the install page.", "icon": "material-icon-name", "category": "OTHER", "tags": ["tag1", "tag2"], "version": { "code": 1, "name": "1.0.0", "changelog": "Initial release" }, "author": { "name": "Your Name", "url": "https://github.com/your-handle", "email": "optional@example.com" }, "runAt": "DOCUMENT_END", "urlMatches": [ { "pattern": "*", "isRegex": false, "exclude": false } ], "permissions": ["DOM_ACCESS"], "configItems": [ { "key": "greeting", "name": "Greeting text", "description": "Shown in the floating banner.", "type": "TEXT", "defaultValue": "Hello, WebToApp!", "required": false } ] }注意:module.json中的version是一个对象,包含code(单调递增的整数)、name(semver 字符串)与changelog;而在registry.json中对应的字段只是 semver 字符串——同一个版本号,两种形态,数值必须保持一致。
仓库中 modules/hello-world/module.json 是一个真实的最小示例,其configItems声明了greeting(TEXT)与durationMs(NUMBER)两个可配置项,App 端会据此自动生成设置表单。
允许的 category 取值
CONTENT_FILTER、CONTENT_ENHANCE、STYLE_MODIFIER、THEME、FUNCTION_ENHANCE、AUTOMATION、NAVIGATION、DATA_EXTRACT、DATA_SAVE、INTERACTION、ACCESSIBILITY、MEDIA、VIDEO、IMAGE、AUDIO、SECURITY、ANTI_TRACKING、SOCIAL、SHOPPING、READING、TRANSLATE、DEVELOPER、OTHER。
未知值不会破坏安装,而是回落为OTHER——代价仅仅是该模块不会出现在市场首页的分类筛选芯片中。从源码看,ExtensionModule.kt 中ModuleCategory枚举为每个分类都映射了 Material 图标与本地化显示名(如READING("book")、VIDEO("videocam")),App 分类面板即由此驱动。
允许的 runAt 取值
DOCUMENT_START、DOCUMENT_END、DOCUMENT_IDLE、CONTEXT_MENU、BEFORE_UNLOAD。省略时默认为DOCUMENT_END。源码中ModuleRunTime将每个取值映射到对应的 DOM 事件(如DOCUMENT_END→DOMContentLoaded,BEFORE_UNLOAD→beforeunload),这决定了模块脚本在页面生命周期的哪个时刻被注入执行。
允许的 permissions 取值
DOM_ACCESS、DOM_OBSERVE、CSS_INJECT、STORAGE、COOKIE、INDEXED_DB、CACHE、NETWORK、WEBSOCKET、FETCH_INTERCEPT、CLIPBOARD、NOTIFICATION、ALERT、KEYBOARD、MOUSE、TOUCH、LOCATION、CAMERA、MICROPHONE、DEVICE_INFO、MEDIA、FULLSCREEN、PICTURE_IN_PICTURE、SCREEN_CAPTURE、DOWNLOAD、FILE_ACCESS、EVAL、IFRAME、WINDOW_OPEN、HISTORY、NAVIGATION。
权限列表在安装页面上只起展示与提示作用,运行时并不会按它做沙箱隔离——审查者用它来识别危险能力。其中危险项(COOKIE、INDEXED_DB、NETWORK、WEBSOCKET、FETCH_INTERCEPT、CLIPBOARD、LOCATION、CAMERA、MICROPHONE、SCREEN_CAPTURE、FILE_ACCESS、EVAL、IFRAME)在评审时会受到额外关注。这与 ExtensionModule.kt 中ModulePermission枚举的dangerous标记一一对应。
允许的 configItems[].type 取值
TEXT、TEXTAREA、NUMBER、BOOLEAN、SELECT、MULTI_SELECT、RADIO、CHECKBOX、COLOR、URL、EMAIL、PASSWORD、REGEX、CSS_SELECTOR、JAVASCRIPT、JSON、RANGE、DATE、TIME、DATETIME、FILE、IMAGE。
每个类型在 App 设置页都有对应的输入控件与本地化说明(见ConfigItemType枚举及Strings.configType*文案)。SELECT类型配合options数组使用——modules/reading-mode/module.json 中的theme(light/sepia/dark)与fontFamily(serif/sans/mono)就是典型用法。
urlMatches 匹配模式
支持两种模式:
isRegex: false(推荐):Chrome 扩展风格的 glob 匹配:*匹配任意数量的字符;*://...会展开为(https?|ftp|file)://;<all_urls>和*都表示"匹配所有 URL"。
isRegex: true:Java 风格正则,每次 URL 匹配有 200ms 超时,超时按不匹配处理。
exclude: true让该规则从匹配集合中做减法;若某模块只写了 exclude 规则,则匹配除此之外的所有 URL。App 端的实现位于 ExtensionModule.kt 的matchesUrl/matchRule:glob 会被编译为正则(*→.*、*://→ 协议组、其余字符转义),编译结果放入有界 LRU 缓存(容量 64)避免每次页面加载重复编译;正则分支则由单线程执行器提交并在 200ms 内取结果,超时返回false。从源码结构看,这套匹配逻辑同时被 App 端用于判断面板按钮的 Active/Inactive 状态。
registry.json 索引条目 schema
registry.json是 App 先拉取用来渲染列表的索引,每个条目镜像模块清单并额外携带path(文件夹名)与hasCss标志:
{ "id": "globally-unique-id", "path": "folder-name-under-modules", "name": "Display Name", "description": "One-line summary", "icon": "material-icon-name", "category": "OTHER", "tags": ["tag1", "tag2"], "version": "1.0.0", "minAppVersion": 33, "author": { "name": "Your Name", "url": "https://github.com/your-handle" }, "runAt": "DOCUMENT_END", "permissions": ["DOM_ACCESS", "CSS_INJECT"], "urlMatches": [ { "pattern": "*", "isRegex": false, "exclude": false } ], "hasCss": false, "iconUrl": "icon.png" }minAppVersion:允许你发布一个依赖特定新 API 的模块——App 端versionCode低于该值的用户将看不到这条目。当前仓库的versionCode为66(v2.6.4);只有当你确实依赖更新版本时才设置它。目前注册表中的条目均使用minAppVersion: 33。hasCss:与模块目录中是否存在style.css严格对应,true时 App 会在安装时一并拉取 CSS。iconUrl:可选。相对路径("icon.png"/"icon.svg"/"icon.webp"/"icon.jpg"/"icon.jpeg")指向模块目录内文件(必须小于 256 KB),或绝对https://URL。缺省时显示首字母圆形头像。
当前 modules/registry.json 中收录了 8 个模块,覆盖OTHER、STYLE_MODIFIER、READING、INTERACTION、NAVIGATION、ACCESSIBILITY、VIDEO等分类,runAt在DOCUMENT_START与DOCUMENT_END之间按需选择——例如web-tint(视觉滤镜)与tv-dpad-cursor(TV 遥控光标)需要在页面早期介入而使用DOCUMENT_START,reading-mode、auto-scroll等则等待 DOM 就绪。
submissions.json:决定谁出现在市场
modules/submissions.json 由Module Market Publish工作流(.github/workflows/modules-publish.yml)在每次向main推送时自动生成:每个条目对应一个真正落地到main的模块,包含 PR 编号、合并时间、PR 作者的 GitHub 身份,以及该模块目录下所有其他提交者的 GitHub 身份(contributors列表)。App 端市场将原始提交者与这些贡献者以叠加头像展示,并聚合出一个贡献者榜单。
App 端只展示出现在submissions.json里的模块——这就是"只展示已合并 PR"的全部机制:客户端没有内置白名单,也没有可绕过的过滤;模块若不在该文件中,用户就完全看不到它。目前文件中的条目既包含经由 PR 合并的第三方模块(如wta-auto-open-video-player,PR #134),也包含维护者直推的官方模块(标记"direct": true)。
发布工作流的执行逻辑:
- 向
main推送且改动触及modules/时,遍历所有模块目录。 - 对每个目录,用
git log --diff-filter=A找出引入该目录的提交。 - 调用
GET /repos/{owner}/{repo}/commits/{sha}/pulls判断该提交是否属于某个已合并的 PR;若是,则记录 PR 编号、URL、merged_at与 PR 作者的 GitHub login + 头像。 - 否则按直接推送处理,记录提交作者解析出的 GitHub login。任何真实作者都会被记录(模块只能通过评审或维护者推送进入
main),只有 bot 身份被排除。 - 对每个模块遍历其完整提交历史,用 commit API 解析出每位作者的 GitHub login,把除原始提交者(及 bot)之外的人全部记入
contributors。 - 将重新生成的文件提交回
main。
作为贡献者,你无需做任何事——PR 合并几秒后工作流自动跑完,下一次有人打开市场就能看到你的模块。
main.js 注入合约与运行时环境
模块代码在注入前会被包进一个IIFE中执行,并提供以下全局变量:
| 全局 | 值 |
|---|---|
__MODULE_INFO__ | { id, name, icon, version, uiConfig, runMode } |
__MODULE_CONFIG__ | 用户已保存配置的{ key: value }对象 |
__MODULE_UI_CONFIG__ | 面板 UI 配置(与__MODULE_INFO__.uiConfig一致) |
__MODULE_RUN_MODE__ | 'INTERACTIVE'或'AUTO' |
getConfig(key, defaultValue) | 读取__MODULE_CONFIG__的便捷访问器 |
代码外层还有一层try/catch:未捕获的异常会以模块名作为前缀写入console.error,不会中断页面上的其他注入。因此你无需自己再包一层try/catch,但必须意识到错误是静默吞掉的——失败的模块不会打扰用户,但也可能悄悄失效。
若你携带了style.css(并在registry.json中把hasCss设为true),它会被注入为id="ext-module-<模块id>"的<style>标签,且先于你的代码执行。
一个最小的 hello-world 模块:
(function () { var greeting = getConfig('greeting', 'Hello!'); var banner = document.createElement('div'); banner.textContent = greeting; banner.style.cssText = 'position:fixed;top:24px;left:50%;' + 'transform:translateX(-50%);padding:10px 16px;background:#111;' + 'color:#fff;border-radius:12px;z-index:2147483647;'; document.body.appendChild(banner); setTimeout(function () { banner.remove(); }, 3000); })();仓库中 modules/hello-world/main.js 是完整版:它通过getConfig('greeting', ...)与getConfig('durationMs', ...)读取两个可配置项,实现了带淡入淡出动画的浮动横幅。更多参考见 modules/reading-mode/main.js。
运行时包装的源码级还原
App 端这一注入合约的生成逻辑位于 ExtensionModule.kt 的generateExecutableCode():它把configValues、UI 配置、runMode、URL 匹配规则序列化后注入包装代码,随后依次声明getConfig函数、注入 CSS、把用户代码放进try/catch,最后执行面板自动注册逻辑。也就是说,README 中描述的"IIFE + 全局变量 + try/catch"并非约定俗成,而是由 App 代码强制生成的执行环境。
可选:注册一个面板按钮
如果模块带有交互式 UI,可通过__WTA_MODULE_UI__.register注册到浮动面板,让用户能从面板呼出它:
__WTA_MODULE_UI__.register({ id: __MODULE_INFO__.id, name: __MODULE_INFO__.name, icon: __MODULE_INFO__.icon, uiConfig: __MODULE_UI_CONFIG__, runMode: __MODULE_RUN_MODE__, onClick: function () { // open your UI } });如果模块声明了configItems却没有调用register,运行时会自动注册一个默认入口,至少保证用户能打开模块设置页。reading-mode的 main.js 展示了标准用法:将onClick: toggle交给运行时,由运行时负责按钮渲染。
版本管理与用户配置保留
发布更新时,必须同时提升module.json中的version.code/version.name与registry.json中的version。客户端会用 semver 与本地已安装版本比较,并提供一键升级。
升级保留用户配置:新清单configItems中仍然存在的 key,其值会被保留;被移除的 key 会被自动清理。因此重命名 config key 等于重置它——请在version.changelog中记录这类破坏性变更。仓库内reading-mode的版本演进(1.0.0 → 1.1.0)即为佐证:changelog明确记录了"改进文章提取(广告/侧栏惩罚)、阅读时长估算、自定义字号"等变更。
本地校验
仓库自带一个 Python 校验器,它模拟 App 安装时的检查,并叠加 CI 用来卡 PR 的若干正确性规则:
python3 .github/scripts/ci/validate_modules.py脚本位于 .github/scripts/ci/validate_modules.py,仅使用标准库,无需pip install。同一个脚本也会在 GitHub Actions(.github/workflows/modules-check.yml)中运行——任何改动modules/的 PR 都必须通过 CI 才能被维护者合并。
校验器会捕获:
registry.json或任一module.json的 JSON 解析错误;- 必填字段缺失(
id、name、version); category/runAt/permissions/configItems[].type的非法枚举值;registry.json与module.json之间的字段不一致(id、name、version、runAt、漏报的权限);- 文件夹名不是 kebab-case;
- modules 目录与 registry 条目不对齐(孤立目录 / 幽灵条目);
- 重复的
id或path; - 缺少必需文件(
module.json、main.js); - 多余文件(未被
iconUrl引用的图标、额外子目录等给出 warning); hasCss标志与磁盘上style.css是否存在不匹配;iconUrl引用了不存在或超过 256 KB 的图片;main.js顶层return(在 IIFE 包装内会成为语法错误);getConfig(...)调用但configItems未声明对应字段。
审核 Checklist
以下是维护者在合并前逐项核查的内容,投稿者亦可对照自查:
- 目录名和
id唯一且为 kebab-case module.json与registry.json的id/name/version/runAt/permissions/urlMatches一致main.js可读;除非 PR 中附源码链接,否则不接受混淆/压缩代码- 没有无条件调用第三方网络端点
- 未在
permissions中声明就读取document.cookie或鉴权 token urlMatches范围合理(侵入性强的模块不应无脑写*)- 代码能在 IIFE 包装下正常运行(没有顶层
return) runAt与代码预期一致(DOCUMENT_START模块不得假设document.body已存在)version.name变更时version.code已随之 +1hasCss当且仅当目录中存在style.css时才为true- 若设置
iconUrl,文件存在、小于 256 KB、扩展名为png/svg/webp/jpg/jpeg之一 - 没有未被
iconUrl引用的图标文件或其他运行时无法触达的多余文件
从示例模块到生产模块
- hello-world(main.js / module.json):最小可运行模板,展示
getConfig读取 TEXT 与 NUMBER 两类配置项,适合作为新模块的起点。 - reading-mode(main.js / module.json):在 200 行内演示了完整模块面——多类型
configItems(SELECT/NUMBER/BOOLEAN)、基于文本密度与标签权重的文章提取启发式、带安全回滚的 DOM 重构、__WTA_MODULE_UI__.register面板注册,以及用sessionStorage在 SPA 导航间保持阅读状态。注释明确建议生产环境替换为 Mozilla 的@mozilla/readability级提取库。 - 内置模块的对照:App 端 BuiltInModules.kt 以代码形式定义了媒体下载器、视频增强、网页分析、页内查找、高级深色模式等 8 个内置模块(
builtIn = true),它们与市场模块共享同一套ExtensionModule数据模型与注入管线——理解内置模块的实现有助于写出契合 App 交互范式的市场模块。
综上,modules/目录本身即是一个"以 Git 为后端的微型应用商店":registry.json负责发现,submissions.json负责可信度,module.json负责安装语义,main.js/style.css负责执行,而 CI 校验器与审核清单共同守住了目录的格式与安全边界。投稿者只需遵循本文的字段规则与流程,即可让自己的模块在合并后的下一次刷新进入全球用户的手机。
【免费下载链接】web-to-appThe most full featured web-to-app toolkit on Android, a complete APK workshop that runs entirely on your phone项目地址: https://gitcode.com/GitHub_Trending/web/web-to-app
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考