V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
Honwhy
V2EX  ›  程序员

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

  •  
  •   Honwhy · 36 天前 · 1483 次点击
    这是一个创建于 36 天前的主题,其中的信息可能已经有所发展或是发生改变。

    公众号阅读增强插件

    公众号阅读增强插件是一款 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  
       36 天前 via Android
    🙋‍♂️快把识别文章中 URL ,转成<a>
    Leoking222
        2
    Leoking222  
       35 天前
    提个建议,这个目录显示在右侧栏会不会更好一些,或者可以做一个可定义的选项来。
    Honwhy
        3
    Honwhy  
    OP
       35 天前
    @korvin v 2.0.3 版本已经处理了
    Honwhy
        4
    Honwhy  
    OP
       35 天前
    @Leoking222 v2.0.3 版本 支持配置调整目录位置
    korvin
        5
    korvin  
       35 天前
    @Honwhy #3 你这个目前只能通过源码本地安装吗
    Honwhy
        6
    Honwhy  
    OP
       34 天前
    @korvin #5 我不好意思发 chrome 等商店,原作者已经发了一个的。 @350041264812

    从这里获取 https://wxreader.browserplus.store/
    korvin
        7
    korvin  
       34 天前
    @Honwhy #6 那可以在 github 放个 release 包
    350041264812
        8
    350041264812  
       33 天前
    @Honwhy 没关系的,你可以发,作者同意了
    350041264812
        9
    350041264812  
       33 天前
    你可以发布到即刻 / 推特中,然后我顺便也给你转发一下帖子
    Honwhy
        10
    Honwhy  
    OP
       33 天前
    Leoking222
        11
    Leoking222  
       29 天前
    @Honwhy #4 目前已经安装了 2.0.3 版本了,请问在哪里可以调节目录的位置呢
    https://gaoziman.oss-cn-hangzhou.aliyuncs.com/uPic/2025-05-19-image-20250519140913576.png
    Honwhy
        12
    Honwhy  
    OP
       28 天前
    @Leoking222 #11 我自己拿 github release 安装了,点击 icon 弹出窗口有目录位置配置选项的,
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5160 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 33ms · UTC 06:42 · PVG 14:42 · LAX 23:42 · JFK 02:42
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.