公众号阅读增强插件重构过程记录 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
Honwhy
V2EX    程序员

公众号阅读增强插件重构过程记录

  •  a href="Javascript:" Onclick="downVoteTopic(1131002);" class="vote">
  •   Honwhy
    honwhy 200 天前 1893 次点击
    这是一个创建于 200 天前的主题,其中的信息可能已经有所发展或是发生改变。

    公众号阅读增强插件

    公众号阅读增强插件是一款 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!) }) 

    ShadowRoot

    原来项目中使用了最简单的 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) }) } 

    AI 总结

    参考了 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) } 

    release notes

    [v2.0.2] - 2025-05-11

    新特性

    • 文章摘要总结:接入 AI 大语言总结文章内容并在顶部展示。
    • 阅读时间预估:展示文章字数及阅读文章预估的时间。

    [v2.0.1] - 2025-05-08

    新特性

    • 保存阅读进度:保存阅读进度,重新打开文章时自动定位到上次阅读的位置。
    • 进度条优化:阅读进度条调整到文章顶部位置。
    • 展示文章二维码:在侧边悬浮展示当前页面网址二维码,方便手机扫码阅读。

    [v2.0.0] - 2025-05-07

    新特性

    • 框架重构优化:使用 WXT+Vue 重构,支持 Chrome/Edge/Firefox 浏览器。
    • 项目工程优化:使用 @antfu/eslint-config 优化代码格式问题, 使用 simple-git-hooks 改善代码提交。
    • 优化存储的使用: 使用 Hook 优化浏览器插件 Sync 存储的保存、更新及变化监听功能。
    • 移除多余的文件: 不再提交 node_modules 和 dist.zip 文件。
    12 条回复    2025-05-19 21:59:32 +08:00
    korvin
        1
    korvin  
       199 天前 via Android
    ♂快把识别文章中 URL ,转成<a>
    Leoking222
        2
    Leoking222  
       199 天前
    提个建议,这个目录显示在右侧栏会不会更好一些,或者可以做一个可定义的选项来。
    Honwhy
        3
    Honwhy  
    OP
       198 天前
    @korvin v 2.0.3 版本已经处理了
    Honwhy
        4
    Honwhy  
    OP
       198 天前
    @Leoking222 v2.0.3 版本 支持配置调整目录位置
    korvin
        5
    korvin  
       198 天前
    @Honwhy #3 你这个目前只能通过源码本地安装吗
    Honwhy
        6
    Honwhy  
    OP
       197 天前
    @korvin #5 我不好意思发 chrome 等商店,原作者已经发了一个的。 @350041264812

    从这里获取 https://wxreader.browserplus.store/
    korvin
        7
    korvin  
       197 天前
    @Honwhy #6 那可以在 github 放个 release 包
    350041264812
        8
    350041264812  
       196 天前
    @Honwhy 没关系的,你可以发,作者同意了
    350041264812
        9
    350041264812  
       196 天前
    你可以发布到即刻 / 推特中,然后我顺便也给你转发一下帖子
    Honwhy
        10
    Honwhy  
    OP
       196 天前
    Leoking222
        11
    Leoking222  
       192 天前
    @Honwhy #4 目前已经安装了 2.0.3 版本了,请问在哪里可以调节目录的位置呢
    https://gaoziman.oss-cn-hangzhou.aliyuncs.com/uPic/2025-05-19-image-20250519140913576.png
    Honwhy
        12
    Honwhy  
    OP
       191 天前
    @Leoking222 #11 我自己拿 github release 安装了,点击 icon 弹出窗口有目录位置配置选项的,
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     5107 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 28ms UTC 09:30 PVG 17:30 LAX 01:30 JFK 04:30
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86