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

116 天前
 Honwhy

公众号阅读增强插件

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

✨ 新特性

[v2.0.1] - 2025-05-08

✨ 新特性

[v2.0.0] - 2025-05-07

✨ 新特性

1708 次点击
所在节点    程序员
12 条回复
korvin
116 天前
🙋‍♂️快把识别文章中 URL ,转成<a>
Leoking222
115 天前
提个建议,这个目录显示在右侧栏会不会更好一些,或者可以做一个可定义的选项来。
Honwhy
115 天前
@korvin v 2.0.3 版本已经处理了
Honwhy
115 天前
@Leoking222 v2.0.3 版本 支持配置调整目录位置
korvin
115 天前
@Honwhy #3 你这个目前只能通过源码本地安装吗
Honwhy
114 天前
@korvin #5 我不好意思发 chrome 等商店,原作者已经发了一个的。 @350041264812

从这里获取 https://wxreader.browserplus.store/
korvin
114 天前
@Honwhy #6 那可以在 github 放个 release 包
350041264812
113 天前
@Honwhy 没关系的,你可以发,作者同意了
350041264812
113 天前
你可以发布到即刻 / 推特中,然后我顺便也给你转发一下帖子
Honwhy
113 天前
Leoking222
108 天前
@Honwhy #4 目前已经安装了 2.0.3 版本了,请问在哪里可以调节目录的位置呢
https://gaoziman.oss-cn-hangzhou.aliyuncs.com/uPic/2025-05-19-image-20250519140913576.png
Honwhy
108 天前
@Leoking222 #11 我自己拿 github release 安装了,点击 icon 弹出窗口有目录位置配置选项的,

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1131002

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX