公众号阅读增强插件是一款 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 36 天前 via Android
🙋♂️快把识别文章中 URL ,转成<a>
|
![]() |
2
Leoking222 35 天前
提个建议,这个目录显示在右侧栏会不会更好一些,或者可以做一个可定义的选项来。
|
![]() |
4
Honwhy OP @Leoking222 v2.0.3 版本 支持配置调整目录位置
|
![]() |
6
Honwhy OP |
![]() |
8
350041264812 33 天前
@Honwhy 没关系的,你可以发,作者同意了
|
![]() |
9
350041264812 33 天前
你可以发布到即刻 / 推特中,然后我顺便也给你转发一下帖子
|
![]() |
10
Honwhy OP |
![]() |
11
Leoking222 29 天前
@Honwhy #4 目前已经安装了 2.0.3 版本了,请问在哪里可以调节目录的位置呢
https://gaoziman.oss-cn-hangzhou.aliyuncs.com/uPic/2025-05-19-image-20250519140913576.png |
![]() |
12
Honwhy OP |