
公众号阅读增强插件是一款 Chrome 浏览器扩展,旨在提升用户阅读微信公众号文章的体验。通过自动生成文章的结构化目录,让您轻松了解文章结构、快速导航到感兴趣的部分,并在阅读长文时保持位置感知。
https://github.com/honwhy/WeChatReaderEnhancer
流光卡片的作者在 V2EX 上发帖,[开源分享/视频演示] 我开发的一款 Chrome/Edge 插件:公众号阅读增强器 ,介绍了这个插件,并且还给出了开源地址: https://github.com/someone1128/WeChatReaderEnhancer
原作者使用了 cursor 开发了这款插件,从效果上看功能完善、样式美观、注释清晰, 只可惜大部分是用 TypeScript + 操作 DOM 的方式实现的。 在我看来这种开发方式不利于代码维护以及后续添加新功能。
于是花了 2 天时间将项目工程用 WXT+Vue 做了重构,修复了阅读进度没有正确恢复的问题等等。同时,去掉不必要的 node_modules/dist.zip 等文件的提交,由于重构后与原项目代码结构差异比较大,因此无法 pr 回馈到原项目。
在 V2EX 上同时也收到网友们的建议,陆陆续续优化和改造完善这款插件。
一般刚开始接触浏览器插件开发的程序员,可能不知道,chrome.runtime.onMessage 和 chrome.runtime.sendMessage 是可以像 http request/response 方式编码的。往往写出非常异步 callback 的难受方式,
// content.js chrome.runtime.onMessage.addListener 接收 background 发回来的结果 chrome.runtime.sendMessage(params) // background.js chrome.runtime.onMessage.addListener(message, sender, sendRespOnse=> { chrome.tabs.query({active: true}).then(tab => { chrome.tabs.sendMessage(tab.id, xxx) }) }) 改用 WXT + webextension-polyfill 实现方式,可以做到 async/await 优雅方式,
// background.js brower.runtime.onMessage.addListener(message => { return somePromise() }) // content.js const resp = await brower.runtime.sendMessage(params) 是不是就顺眼多了,心智负担也降低了很多。
举例一个场景,在 popup 中修改了某些配置,然后在 content 中想立马应用上。
不了解 WXT 的程序员,可能会想到,在 popup 修改配置后 sendMessage 给到 background ,然后 background 再sendMessage 给 content 。
其实,WXT 有一个很好用的 storage watch 方案,刚好我这里把它做成 hooks 形式,
export function useSettings(handleSettingsChange: (settings: Settings) => void) { const settings = ref<Settings>({ ...defaultSettings }) const unwatch = storage.watch<Settings>(`sync:settings`, (newSettings) => { settings.value = newSettings || { ...defaultSettings } handleSettingsChange(newSettings!) }) function updateSettings(newSettings: Settings) { settings.value = newSettings storage.setItem(`sync:settings`, newSettings) } function resetSettings() { settings.value = { ...defaultSettings } storage.setItem(`sync:settings`, defaultSettings) } onMounted(async () => { console.log(`useSettings mounted`) const item = await storage.getItem<Settings>(`sync:settings`) console.log(`useSettings getItem`, item) if (item) { settings.value = item } }) onUnmounted(() => { unwatch() }) return { settings, updateSettings, resetSettings, } } 在 content 中,只要使用storage.watch 就可以实时监听到配置的变化了
const unwatch = storage.watch<Settings>(`sync:settings`, (newSettings) => { settings.value = newSettings || { ...defaultSettings } handleSettingsChange(newSettings!) }) 原来项目中使用了最简单的 content script 方式,注入到公众号文章宿主环境中,这种做法是有可能引入 css 样式污染宿主环境的,更建议的做法是使用 ShadowRoot 。
重构后新建了一个 ShadowRoot component wechat-toc ,效果见下图,同时可以看到样式文件也放到了 wechat-toc 里面了。
经过几天的 bug 修复,功能迭代,从页面上 可以看到这些增强的效果。
我猜原作者把是遗漏了这项功能,原来的代码中是有关于保存和获取阅读位置的方法,但是实现方式是通过来回 sendMessage 方式实现的有点繁琐,优化如下
/** * 获取用户上次阅读位置 * @param url 文章 URL * @returns Promise ,解析为上次阅读位置 */ export async function getReadingPosition(url: string) { const key = `reading_position_${hashString(url)}` const data = await storage.getItem<ReadingPosition>(`sync:${key}`) return data } 恢复 scrollTo 到原来位置,
// 获取上次阅读位置并滚动到对应位置 const lastPosition = await getReadingPosition(window.location.href) if (lastPosition?.position) { window.scrollTo({ top: lastPosition.position, behavior: `smooth` }) } 接收 v2 网友的建议,在页面的右上角增加了一个 二维码的功能。
import QRCode from 'qrcode' function createQrCode() { // 添加二维码悬浮框 const qrCodeCOntainer= createElement(`div`, { class: `wechat-toc-qrcode-container`, title: `扫描二维码在手机上阅读`, }) const targets = document.getElementsByTagName(`wechat-toc`) const body = targets[0]!.shadowRoot body!.appendChild(qrCodeContainer) // 生成二维码 const qrCodeCanvas = createElement(`canvas`) qrCodeContainer.appendChild(qrCodeCanvas) QRCode.toCanvas(qrCodeCanvas, window.location.href, { width: 150 }, (error: any) => { if (error) console.error(`二维码生成失败:`, error) }) } 参考了 doocs/md 关于模型配置的部分代码。
AI 总结的功能目前实现比较粗糙,
const template = ` 请用中文撰写一篇 100 字以内的文章摘要,需包含核心观点、主要论据和结论。要求语言精炼、逻辑清晰,重点突出文章的核心价值与创新点,确保信息完整且无遗漏。 优化说明: 结构化要求:明确要求包含核心观点/论据/结论三要素 质量标准:增加"逻辑清晰""重点突出"等质量维度 价值导向:强调"核心价值与创新点"的提炼 完整性要求:补充"确保信息完整"的约束条件 专业表达:使用"撰写"替代"总结"提升专业感 文章标题:%title% 文章内容: %content% ` export async function chat(body: { content: string, title: string }) { const settings = await storage.getItem<Settings>(`sync:settings`) if (!settings || !settings.endpoint || !settings.apiKey || !settings.modelName) { console.error(`请先设置模型 API 地址、密钥和名称`) return { choices: [ { message: { content: ``, }, }, ], } } const propmt = template.replace(`%title%`, body.title).replace(`%content%`, body.content) // bailian // https://dashscope.aliyuncs.com/compatible-mode/ // `qwen-plus` const respOnse= await ofetch(`${settings.endpoint}/chat/completions`, { method: `POST`, headers: { 'Content-Type': `application/json`, 'Authorization': `Bearer ${settings.apiKey}`, }, body: { model: settings.modelName, store: true, messages: [{ role: `user`, content: propmt }], }, }) console.log(`chatgpt 返回`, response) return response } 效果,
这部份比较简单,使用reading-time 这个库即可实现,注意要用 textContent 的内容去预估而不是整个 HTML ,另外这个库目前对 browser 支持不是很好,import 的时候要注意调整。
import readingTime from 'reading-time/lib/reading-time' async function addReadingTime() { const metaCOntent= document.querySelector(`#meta_content`) if (!metaContent) { console.warn(`未找到 meta_content`) return } const { minutes } = readingTime(document.body.textContent) const readingTimeCOntainer= createElement(`span`, { class: `rich_media_meta rich_media_meta_text wechat-toc-reading-time`, title: `预计阅读时间`, }) readingTimeContainer.textCOntent= `(阅读大约需 ${Number.parseInt(minutes)} 分钟)` metaContent.append(readingTimeContainer) } 1 korvin 199 天前 via Android ♂快把识别文章中 URL ,转成<a> |
2 Leoking222 199 天前 提个建议,这个目录显示在右侧栏会不会更好一些,或者可以做一个可定义的选项来。 |
4 Honwhy OP @Leoking222 v2.0.3 版本 支持配置调整目录位置 |
6 Honwhy OP |
8 350041264812 196 天前 @Honwhy 没关系的,你可以发,作者同意了 |
9 350041264812 196 天前 你可以发布到即刻 / 推特中,然后我顺便也给你转发一下帖子 |
10 Honwhy OP |
11 Leoking222 192 天前 @Honwhy #4 目前已经安装了 2.0.3 版本了,请问在哪里可以调节目录的位置呢 https://gaoziman.oss-cn-hangzhou.aliyuncs.com/uPic/2025-05-19-image-20250519140913576.png |
12 Honwhy OP |