2025-04-24 20:14:09
最初是在 reddit 上看到有人在寻找可以解压 zip 文件的 Firefox 插件 [1],好奇为什么还有这种需求,发现作者使用的是环境受限的电脑,无法自由的安装本地程序。于是吾辈便去检查了现有的在线解压工具,结果却发现排名前 5 的解压工具都没有完全支持下面两点
下面的视频展示了当前一些在线工具的表现
实际上,只有 ezyZip 有点接近,但它也不支持解压 ZIP 中的特定目录。
在简单思考之后,吾辈考虑尝试使用时下流行的 Vibe Coding 来创建一个 Web 工具来满足这个需求。首先检查 zip 相关的 npm 包,吾辈之前经常使用的是 jszip,但这次检查时发现它的不能处理大型 ZIP 文件 [2]。所以找到了更快的 fflate,但遗憾的是,它完全不支持加密解密功能,但作者在 issue 中推荐了 zip.js [3]。
官网给出的例子非常简单,也非常简洁明了。如果是解压文件并触发下载,只需要结合使用 BlobWriter/file-saver 即可。
1 |
|
这段代码出现了一个有趣之处:BlobWriter
,它是如何保存解压后的超大型文件的?毕竟数据总要在某个地方,blob 似乎都在内存中,而且也只允许流式读取而不能流式写入。检查一下 GitHub 上的源代码 [4]。
1 |
|
是的,这里的关键在于 Response
,它允许接受某种 ReadableStream [5] 类型的参数,而 ReadableStream 并不保存数据到内存,它只是一个可以不断拉取数据的流。
例如下面手动创建了一个 ReadableStream,它生成一个从零开始自增的无限流,但如果没有消费,它只会产生第一条数据。
1 |
|
如果消费 100 次,它就会生成 100 个值。
1 |
|
而在 zip.js 解压时,通过 firstEntry.getData(blobWriter)
将解压单个文件产生的二进制流写入到了 Response 并转换为 Blob 了。但是,难道 await new Response().blob()
不会将数据全部加载到内存中吗?
是的,一般认为 Blob 保存的数据都在内存中,但当 Blob 过大时,它会透明的转移到磁盘中 [6],至少在 Chromium 官方文档中是如此声称的,JavaScript 规范并未明确指定浏览器要如何实现。有人在 Stack Overflow 上提到 Blob 只是指向数据的指针,并不保存真实的数据 [7],这句话确实非常正确,而且有趣。顺便一提,可以访问 chrome://blob-internals/ 查看浏览器中所有的 Blob 对象。
解压目录主要麻烦的是一次写入多个目录和文件到本地,而这需要利用浏览器中较新的 File System API [8],目前为止,它在浏览器中的兼容性还不错 [9],所以这里利用它来解压 ZIP 中的目录并写入本地。无论如何,只要做好降级处理,使用这个新 API 是可行的。
首先,可以通过拖拽 API 或者 input File 来获取一个目录的 FileSystemDirectoryHandle 句柄。一旦拿到它,就可以访问这个目录下所有的文件,并且可以创建子目录和写入文件(支持流式写入)。假设我们有一个要写入的文件列表,可以轻松使用下面的方法写入到选择的目录中。
1 |
|
尽管 File System API 已经可以胜任普通的文件操作,但它仍然有一些局限性,包括
*.cfg
或者以 ~
结尾的文件,同样被认为有风险 [11]
这是一个很早之前就有人做过的事情,但直到现在仍然可以发现一些有趣的东西。尤其是 Blob 的部分,之前从未真正去了解过它的存储方式。
基于本文探讨的技术,吾辈最终实现了一个名为 MyUnzip 的在线解压工具,欢迎访问 https://myunzip.com 试用并提出反馈。
2025-04-03 15:56:35
最近在做一些服务端相关的事情,使用了 Cloudflare Workers + D1 数据库,在此过程中,遇到了一些数据库相关的问题,而对于前端而言数据库是一件相当不同的事情,所以在此记录一下。
下图是最近 30 天的请求记录,可以看到数据库查询变化之剧烈。
解决问题的前提是发现问题,有几个方法可以更容易留意到相关问题。
c.env.DB.prepare('<sql>').run()).meta
并检查返回的 meta,它包含这个 sql 实际读取/写入的行数首先明确一点,Workers 和 D1 虽然同为 Cloudflare 的服务,但同时使用它们并不会让 D1 变得更快。拿下面这个简单的查询举例,它的平均响应时间(在 Workers 上发起查询到在 Workers 上得到结果)超过了 200ms。
1 |
|
所以在一个接口中包含大量的数据库操作时,应该尽量使用 d1 batch 来批量完成,尤其是对于写入操作,由于没有只读副本,它只会比查询更慢。例如
1 |
|
应该更换为
1 |
|
这样只会向 d1 发出一次 rest 请求即可完成多个数据库写入操作。
ps1: prisma 不支持 d1 batch,吾辈因此换到了 drizzle 中,参考 记录一次从 Prisma 到 Drizzle 的迁移。
ps2: 使用 batch 进行批量查询时需要小心,尤其是多表有同名的列时,参考 https://github.com/drizzle-team/drizzle-orm/issues/555
在 update 时应该排除 id(即使实际上没有修改)。例如下面的代码,将外部传入的 user 传入并更新,看起来没问题?
1 |
|
实际执行的 SQL 语句
1 |
|
然而,一旦这个 id 被其他表通过外键引用了。它就会导致大量的 rows read 操作。例如另一张名为 tweet 的表有一个 userId 引用了这个字段,并且有 1000 行数据。
1 |
|
然后进行一次 update 操作并检查实际操作影响的行数
1 |
|
可以看到 rows read 突然增高到了 2005,而预期应该是 1,考虑一下关联的表可能有数百万行数据,这是一场噩梦。而如果确实排除了 id 字段,则可以看到 rows read/rows written 确实是预期的 1,无论它关联了多少数据。
1 |
|
可以说这是个典型的愚蠢错误,但前端确实对数据库问题不够敏锐。
吾辈在 D1 仪表盘中看到了下面这个 SQL 语句在 rows read 中名列前矛。像是下面这样
1 |
|
可能会在仪表盘看到 rows read 的暴增。
这导致了吾辈在实现分页时直接选择了基于 cursor 而非 offset,而且永远不会给出总数,因为即便 id 有索引,统计数量也会扫描所有行。这也是一个已知问题:https://community.cloudflare.com/t/full-scan-for-simple-count-query/682625
起因是吾辈注意到下面这条 sql 导致了数十万的 rows read。
1 |
|
下面是对应的 ts 代码
1 |
|
可以看到这里连接了 4 张表查询,这种愚蠢的操作吾辈不知道当时是怎么写出来的,也许是 LLM 告诉吾辈的 😂。而吾辈并未意识到这种操作可能会导致所谓的“笛卡尔积爆炸”[1],必须进行一些拆分。
“笛卡尔积爆炸”是什么?在这个场景下就吾辈的理解而言,如果使用 leftJoin 外连多张表,并且外联的字段相同,那么就是多张表查询到的数据之和。例如下面这条查询,如果 modListUser/modListRule 都有 100 条数据,那么查询的结果则有 100 * 100 条结果,这并不符合预期。
1 |
|
而如果正确的拆分查询并将数据分组和转换放到逻辑层,数据库的操作就会大大减少。
1 |
|
如果 rows written 数量不多,或者没有批处理的需求,那这可能只是过早优化。
这是在优化写入性能时尝试的一个小技巧,可以提升批量写入的性能。考虑下面这个批量插入的代码
1 |
|
嗯,这只是个愚蠢的例子,当然要使用 batch 操作,就像上面说的那样。
1 |
|
但是否忘记了什么?是的,数据库允许在一行中写入多条数据,例如:
1 |
|
不幸的是,sqlite 允许绑定的参数数量有限,D1 进一步限制了它 [2],每次参数绑定最多只有 100 个。也就是说,如果我们有 10 列,我们最多在一条 SQL 中插入 10 行,如果批处理数量很多,仍然需要放在 batch 中处理。
幸运的是,实现一个通用的自动分页器并不麻烦,参考 https://github.com/drizzle-team/drizzle-orm/issues/2479#issuecomment-2746057769
1 |
|
那么,我们实际获得性能收益是多少?
就上面举的 3 个例子进行了测试,每个例子分别插入 5000 条数据,它们在数据库执行花费的时间是
78ms => 37ms => 14ms
吾辈认为这个优化还是值得做的,封装之后它对代码几乎是无侵入的。
服务端的问题与客户端相当不同,在客户端,即便某个功能出现了错误,也只是影响使用者。而服务端的错误可能直接影响月底账单,而且需要一段时间才能看出来,因此需要小心,添加足够的单元测试。解决数据库查询相关的问题时,吾辈认为遵循 发现 => 调查 => 尝试解决 => 跟进 => 再次尝试 => 跟进 => 完成 的步骤会有帮助,第一次解决并不一定能够成功,甚至有可能变的更糟,但持续的跟进将使得及时发现和修复问题变得非常重要。
2025-03-13 08:33:23
这两天吾辈开始尝试将一个 Chrome 扩展发布到 Safari,这是一件一直想做的事情,但由于 Xcode 极其糟糕的开发体验,一直没有提起兴趣完成。这两天又重新燃起了一丝想法,来来回回,真正想做的事情总是会完成。所以于此记录一篇,如何做到以及踩过的坑。下面转换的扩展 Redirector 实际上已经发布到 Chrome/Firefox/Edge,将作为吾辈第一个发布到 App Store 的 Safari 扩展。
首先,在 WXT 的官方文档中提到了如何发布 Safari 版本 [1],提到了一个命令行工具 xcrun [2],它允许将一个 Chrome 扩展转换为 Safari 扩展。
WXT 提供的命令
1 |
|
由于吾辈使用了 Manifest V3,第二条命令必须修改为
1 |
|
不幸的是,立刻就可以发现一个错误,默认的 App Bundle Identifier 不正确,需要手动指定 --bundle-identifier
,由于需要运行多次这个命令,所以还应该指定 –force 允许覆盖现有 Output。
1 |
|
现在可以在 Redirector 目录下看到一个 Xcode 项目,并且会自动使用 Xcode 打开该项目。
接下来切换到 Xcode 开始 build 并运行这个扩展。
然而,打开 Safari 之后默认不会看到刚刚 build 的扩展,因为 Safari 默认不允许运行未签名的扩展 [3]。
需要设置 Safari
此时,如果你像吾辈一样之前安装然后卸载过这个扩展的话,需要手动使用 --project-location
来指定另一个路径重新转换,然后在 Xcode 中构建,这是一个已知的 issue [4]。
好的,完全退出 Xcode/Safari,然后重新运行新的转换命令,指定一个其他目录(这里是用了日期后缀)作为转换 Xcode 项目目录。
1 |
|
在 Safari 扩展故障排除中可以有这样一条命令,可以检查已经识别安装的扩展 [5]。当然,实际上即使识别出来了,也有可能在 Safari 中看不到,必要不充分条件,转换之前最好检查 /Users/username/Library/Developer/Xcode/DerivedData 目录并清理构建的临时扩展。
1 |
|
无论如何,如果一切正常,就可以在 Extensions 中查看并启用临时扩展了。
启用它,然后就可以在 Safari Toolbar 中看到扩展图标并进行测试了。
如果你发现 Mac 生态下的开发很痛苦,经常没有任何错误但也没有正常工作,那可能是正常的。
好吧,如果幸运的话,扩展就可以正常在 Safari 中工作了对吧,Safari 支持 Chrome 扩展对吧?oh sweet summer child,当然不可能这么简单,Safari 有一些不兼容的 API,它不会出错,但也确实不工作。你可以在官方兼容性文档 [6] 中检查一些不兼容的 API,并采用其他方法绕过。
例如 webRequest 相关的 API,Manifest v3 中 webRequest blocking API 被删除是已知的,但根据这个 App Developer Forums 上的 issue 可知,对于 Safari 而言,Manifest v3 中 webRequest API 整个功能都不生效,仅在 Manifest v2 persistent background pages 中生效,有趣的是,iOS 不支持 persistent background pages。所以之前使用的 webRequest API 需要转换,幸运的是,对于 Redirector 而言,只需要转换为一个。
browser.webRequest.onBeforeRequest.addListener
=> browser.webNavigation.onCommitted.addListener
可以参考官方文档 [7] 调试 background script,它不方便,但它是唯一的方法。
现在,扩展可以正常工作了。
现在,让我们开始构建并发布到 App Store 或在其他地方发布扩展,无论哪种方式,都需要正确配置签名证书。
在发布之前,还需要手动指定版本,因为它并不跟随 manifest 中指定的版本,而是单独的,建议在转换之后就指定。
该配置也在 <project name>/<project name>.xcodeproj/project.pbxproj
文件中,可以搜索并替换 MARKETING_VERSION = 1.0;
然后从 Xcode 工具栏选择 Product > Archive,就可以构建一个 bundle 并等待分发了。
首先点击 Validate App 确保 bundle 没有什么配置错误。这里遇到了一个错误,提示吾辈的扩展名(不是扩展 id)已存在,需要使用一个其他的名字。
好的,让我们修改 manifest 中的名字并重复以上转换和构建流程,重新验证,没有发现错误。
接下来就可以分发 App 了,点击 Distribute App,然后选择 App Store Connect 在 App Store 上架或 Direct Distribute 公证插件。
吾辈发现这个视频很有帮助 https://youtu.be/s0HtHvgf1EQ?si=rbzc88E1Y_6nZY6k
最后,还需要前往 https://appstoreconnect.apple.com/apps 完善发布信息,包括截图、隐私政策和定价等等。吾辈没有意识到 Apple 使用网页来管理 App 发布,这与 Apple 万物皆可 App(App Developer/TestFlight) 的风格似乎不太相像,因此白白苦等了 2 周。
吾辈还发现 App 描述中禁止使用 emoji 字符,否则会提示
The message contains invalid characters.
Mac/iOS 开发是非常封闭的平台,开发工具与体验与 Web 大有不同,但考虑到 Safari 是 Mac 上默认的浏览器,而在 iOS 上更是无法修改的事实上的标准,可能还是值得投入一些精力去支持它,尽管它甚至比 Firefox 更加糟糕。
2025-03-04 22:05:04
svelte5 在去年 10 月发布,据说是 svelte 发布以来最好的版本。其中,他们主要为之自豪的是 runes,这是一个基于 proxy 实现的一个反应式状态系统。但经过 vue3 composition api 和 solidjs signals,吾辈并未感到兴奋。相反,这篇博客将主要针对吾辈在实际项目中遇到的 svelte5 问题进行说明,如果你非常喜欢 svelte5,现在就可以关闭页面了。
例如,当吾辈像 react/vue 一样用 runes 编写一个 hook,例如 useCounter
1 |
|
但这个函数不能放在普通的 .ts 文件,必须使用 .svelte.ts 后缀并让 @sveltejs/vite-plugin-svelte 编译这个文件中的 runes 代码,否则你会得到 $state is not defined
。这也包括 unit test,如果想要使用 runes(通常是测试 hooks/svelte component),文件名也必须以 .svelte.test.ts 结尾,真是糟糕的代码感染。
看到上面的 useCounter 中返回的值被放在 get value 里面了吗?那是因为必须这样做,例如如果尝试编写一个 hooks 并直接返回 runes 的状态,不管是 $state
还是 $derived
,都必须用函数包裹传递来“保持 reaction”,否则你会得到一个错误指向 https://svelte.dev/docs/svelte/compiler-warnings#state_referenced_locally。当然这也包括函数参数,看到其中的讽刺了吗?
1 |
|
当然,你不能直接返回 { now }
而必须使用 get/set 包裹,svelte5 喜欢让人写更多模版代码。
1 |
|
哦,当吾辈说必须使用函数包裹 runes 状态时,显然 svelte 团队为自己留了逃生舱口,那就是 class。检查下面这段代码,它直接返回了 class 的实例,而且正常工作!如果你去查看 sveltekit 的官方代码,他们甚至将 class 的声明和创建放在了一起:https://github.com/sveltejs/kit/blob/3bab7e3eea4dda6ec485d671803709b70852f28b/packages/kit/src/runtime/client/state.svelte.js#L31-L40
1 |
|
显然,它不能应用于普通的 js object 上,不需要等到运行,在编译阶段就会爆炸。
1 |
|
最后,让我们看看 $state 是否可以将整个 class 变成响应式的?
1 |
|
当然不行,像 mobx 一样检测字段 class field 并将其变成响应式的显然太难了。然而,有趣的是,在这里你可以使用普通的 js 对象了。当然,当然。。。
1 |
|
印象中这几种写法在 vue3 中都可以正常工作,看起来怪癖更少一点。
就像 svelte 官方文档中说明的一样,在测试中无法使用 bindable props,因为它是一个模版的专用功能,无法在 js 中使用,必须通过额外的组件将 bindable props 转换为 svelte/store writable props,因为它可以在 svelte 组件测试中使用。
1 |
|
当想要测试这个包含 bindable props 的组件时,必须编写一个包装组件,类似这样。
1 |
|
单元测试代码
1 |
|
是说,有办法像是 vue3 一样动态绑定多个 bindable props 吗?
1 |
|
不,它甚至没有提供逃生舱口,所以无法实现某种通用的 Bindable2Writable 高阶组件来将 bindable props 自动转换为 writable props,这是非常愚蠢的,尤其是 vue3 已经珠玉在前的前提下,svelte5 的实现如此糟糕简直难以理解。
对于下面这样一个组件,它只是一个双向绑定的 checkbox,很简单。
1 |
|
那么,如果去掉 bind 呢?单向绑定?不,它只是设置了初始值,然后就由 input 的内部状态控制了,而不是预期中的不再改变。观看 3s 演示
https://x.com/rxliuli/status/1896856626050855298/video/3
当然,这不是 svelte 的问题,除了 react 之外,其他 web 框架的单向数据流似乎在遇到表单时都会出现例外。
这点是所有新框架都避免不了的,但在 svelte 却特别严重,包括
每当有人抱怨 svelte5 变得更复杂时,社区总有人说你是用 svelte5 编写 hello world 的新手、或者说你可以继续锚定到 svelte4。首先,第一点,像吾辈这样曾经使用过 react/vue 的人而言,svelte4 看起来很简单,吾辈已经用 svelte4 构建了一些程序,它们并不是 hello world,事实上,可能有 10k+ 行的纯代码。其次,锚定到旧版本对于个人是不可能的,当你开始一个新项目的时候,几乎没有办法锚定到旧版本,因为生态系统中的一切都在追求新版本,旧版本的资源很难找到。
就在吾辈发布完这篇博客之后,立刻有人以 “Svelte’s reactivity doesn’t exist at runtime” 进行辩护,而在 svelte5 中,这甚至不是一个站得住脚的论点。当然,他获得了 5 个 👍,而吾辈得到了一个 👎。
https://www.reddit.com/r/sveltejs/comments/1j6ayaf/comment/mgnctgm/
svelte5 变得更好了吗?显然,runes 让它与 react/vue 看起来更像了一点,但目前仍然有非常多的边界情况和怪癖,下个项目可能会考虑认真使用 solidjs,吾辈已经使用 react/vue 太久了,还是想要找点新的东西看看。
2025-02-23 11:40:21
最近使用 Cloudflare D1 作为服务端的数据库,ORM 选择了很多人推荐的 Prisma,但使用过程中遇到了一些问题,主要包括
首先说一下第一个问题,Cloudflare D1 本身并不支持事务,仅支持使用 batch 批处理,这是一种有限制的事务。https://developers.cloudflare.com/d1/worker-api/d1-database/#batch
例如
1 |
|
而如果你使用 Prisma 的 $transaction
函数,会得到一条警告。
1 |
|
这条警告指向了 cloudflare/workers-sdk,看起来是 cloudflare d1 的问题(当然,不支持事务确实是问题),但也转移了关注点,问题是为什么 prisma 内部不使用 d1 batch 函数呢?嗯,它目前不支持,仅此而已,检查 @prisma/adapter-d1 的事务实现。
例如下面这个统计查询,统计 + 去重,看起来很简单?
1 |
|
你在 prisma 中可能会想这样写。
1 |
|
不,prisma 不支持,检查已经开放了 4 年 的 issue#4228。
顺便说一句,drizzle 允许你这样做。
1 |
|
这一点没有真正分析过,只是体感上感觉服务端 API 请求很慢,平均时间甚至达到了 1s,而目前最大的一张表数据也只有 30+k,而大多数其他表还不到 1k,这听起来不正常。但事后来看,从 prisma 切换到 drizzle 之后,bundle 尺寸从 2776.05 KiB / gzip: 948.21 KiB
降低到了 487.87 KiB / gzip: 93.10 KiB
,gzip 之后甚至降低了 90%,这听起来并不那么不可思议了。
有人做了一些测试,似乎批量插入 1k 条的性能问题更糟糕,甚至超过了 30s。https://github.com/prisma/prisma/discussions/23646#discussioncomment-10965747
说完了 Prisma 的这么多问题,接下来说一下在迁移过程中踩到的坑。
在迁移时,首先使用 Grok 从 schema.prisma 自动生成了 drizzle 需要的 schema.ts。但发现了以下问题
原本的表结构
1 |
|
Grok 自动转换生成
1 |
|
这里的自动转换有几个问题
sql`uuid()`
在 prisma 中由应用抽象层填充,但 schema.ts 里使用 sql`uuid()`
,这里应该同样由应用抽象层填充sql`CURRENT_TIMESTAMP`
Invalid Date
实际上需要修改为
1 |
|
嗯,在 join 查询时 drizzle 并不会自动解决冲突的列名。假设有 User 和 ModList 两张表
id | screenName | name |
---|---|---|
user-1 | user-screen-name | user-name |
id | name | userId |
---|---|---|
modlist-1 | modlist-name | user-1 |
然后执行以下代码,非批量查询的结果将与批量查询的结果不同。
1 |
|
1 |
|
这里的 ModList 和 User 中有冲突的列名 id/name,在批量查询时后面的列将会覆盖掉前面的,参考相关的 issue。
https://github.com/cloudflare/workers-sdk/issues/3160
https://github.com/drizzle-team/drizzle-orm/issues/555
需要手动指定列的别名。
1 |
|
然后就会得到一致的结果。
1 |
|
甚至可以实现一个通用的别名生成器。
1 |
|
然后就不再需要手动设置别名,而且它还是类型安全的!
1 |
|
数据迁移时兼容性最重要,修改或优化 schema 必须放在迁移之后。整体上这次的迁移结果还是挺喜人的,后续开新坑数据库 ORM 可以直接用 drizzle 了。
2025-02-16 20:31:07
最近开发一个跨浏览器的扩展时,由于需要在 Content Script 中请求远端服务 API,在 Chrome 中没有遇到任何问题,但在 Firefox 中,它会在 Content Script 上应用网站的 CSP 规则。不幸的是,一些网站禁止在 Script 中请求它们不知道的 API 端点。
首先,这里出现了一个关键名词:CSP,又叫内容安全策略,主要是用来解决 XSS 的。基本上,它允许网站所有者通过服务端响应 Content-Security-Policy
Header 来控制网站页面可以请求的 API 端点。例如下面这个规则仅允许请求 https://onlinebanking.jumbobank.com,其他 API 端点的请求都将被浏览器拒绝。
1 |
|
也就是说,你可以打开 Twitter,并在网站 Devtools Console 中执行 fetch('https://placehold.co/200')
,然后就得到了一个 CSP 错误。
如果你将相同的代码放在扩展的 Content Script 中,然后在 Chrome 中测试扩展,一切正常。
而在 Firefox 中,嗯,你仍然会得到一个 CSP 错误。
如果你使用 Manifest V2,Firefox 则会正常放过,并且不会显示在 Network 中。吾辈甚至不想知道它做了什么。
经过一番调查,吾辈成功找到了相关的 issue,而它们均创建于 9 年前,最新的讨论甚至在 4 天前。检查下面两个 issue。
那么,问题就在这儿,看起来也无法在短期内解决,如果想要让自己的扩展支持 Firefox,现在应该怎么办?
好吧,基本思路有 2 个:
declarativeNetRequest/webRequestBlocking
API 来修改或删除它们呢?首先需要在 Background Script 中定义接口,准备接受参数并转发请求。
1 |
|
同时必须在 manifest 中 声明正确的 host_permissions
权限,添加你要请求的域名。
1 |
|
然后在 Content Script 中调用它。
1 |
|
可以看到现在可以正常得到结果了,但这种方式的主要问题是与原始的 fetch 接口并不相同,上面实现了 blob 类型的请求接口,但并未完整支持 fetch 的所有功能。嗯,考虑到 Request/Response 都不是完全可以序列化的,这会有点麻烦。
接下来,将介绍一种非侵入式的方法,允许在不修改 Content Script 解决该问题,首先是 declarativeNetRequest API,由于 WebRequestBlocking API 被广泛的应用于广告拦截器中,直接导致了 Chrome Manifest V3 正式将其废弃,并推出了静态的 declarativeNetRequest API 替代(尽管远远不能完全替代),但对于解决当下的这个问题很有用,而且很简单。
首先在 manifest 中声明权限,注意 host_permissions 需要包含你要处理的网站。
1 |
|
然后在 public 目录中添加一个 rules.json 文件,其中定义了要删除 x.com 上的 content-security-policy
response header。
1 |
|
可以看到网站的 CSP 已经不复存在,可以看到浏览器也不会拦截你的请求了。但是,这种方法的主要问题是网站安全性受损,就这点而言,这不是一个好方法。
一般而言,推荐使用 Background Script 转发请求,尽管它要编写更多的样板代码,吾辈也就此在 WXT 上问过框架作者,他似乎一般也会这样做。
参考: https://github.com/wxt-dev/wxt/discussions/1442#discussioncomment-12219769