1. 项目背景与核心价值
去年在准备算法面试时,我发现牛客网的题目资源虽然丰富,但缺乏系统性的进度追踪工具。每次刷题后都要手动记录进度,既浪费时间又容易遗漏。于是决定开发一个轻量级的浏览器插件,专门用于追踪牛客网的刷题进度,并整合每日一题功能。
这个工具主要解决三个痛点:
- 自动记录已完成的题目及通过状态
- 可视化展示刷题进度和薄弱环节
- 一键跳转每日推荐题目
经过三个月的迭代,目前插件已稳定运行半年,帮助我和200+位用户累计追踪了超过1.5万道题目。下面分享具体实现方案和关键细节。
2. 技术架构设计
2.1 整体方案选型
采用浏览器插件形式而非独立网站,主要基于以下考量:
- 数据获取便利性:插件可以直接读取牛客页面DOM,避免复杂的API逆向
- 用户体验连贯:用户无需离开刷题页面即可查看进度
- 开发成本低:相比全栈应用,插件只需前端技术栈
技术栈组合:
- Manifest V3规范(兼容Chrome/Edge)
- Vue 3 + Composition API(前端框架)
- IndexedDB(本地数据存储)
- GitHub Actions(自动构建发布)
2.2 核心模块分解
mermaid复制graph TD
A[牛客页面注入] --> B[题目识别模块]
A --> C[提交记录抓取]
B --> D[题目分类标记]
C --> E[通过状态判断]
D --> F[本地数据库存储]
E --> F
F --> G[数据可视化面板]
(注:实际实现时应避免使用mermaid图表,改用文字描述)
3. 关键实现细节
3.1 题目自动识别方案
牛客网的题目页面主要有两种形式:
- 题库列表页:
https://www.nowcoder.com/exam/oj - 题目详情页:
https://www.nowcoder.com/practice/{questionId}
通过分析DOM结构,发现可用特征:
html复制<!-- 列表页 -->
<div class="question-item" data-question-id="12345">
<span class="question-name">两数之和</span>
</div>
<!-- 详情页 -->
<div class="subject-question">
<h1>1. 两数之和</h1>
</div>
实现代码示例:
javascript复制// content-script.js
function getCurrentQuestion() {
if (location.pathname.includes('/practice/')) {
return {
id: location.pathname.split('/').pop(),
title: document.querySelector('.subject-question h1')?.textContent
}
}
return null
}
3.2 提交记录监控
通过MutationObserver监听提交结果区域变化:
javascript复制const observer = new MutationObserver((mutations) => {
const result = document.querySelector('.result-subject')
if (result?.textContent.includes('通过')) {
chrome.runtime.sendMessage({
type: 'SUBMIT_RESULT',
status: 'ACCEPTED'
})
}
})
observer.observe(document.body, {
subtree: true,
childList: true
})
4. 数据存储设计
4.1 数据结构
采用分层存储方案:
typescript复制interface QuestionRecord {
id: string
title: string
lastSubmit: Date
status: 'UNATTEMPTED' | 'FAILED' | 'ACCEPTED'
difficulty: number
tags: string[]
}
interface UserData {
version: 1
questions: Record<string, QuestionRecord>
dailyStreak: number
}
4.2 性能优化
针对大规模数据场景(>5000题):
- 按专题分库存储
- 添加复合索引:
javascript复制db.createIndex('questions', ['status', 'difficulty'])
- 定期压缩历史数据
5. 可视化面板实现
5.1 进度日历热图
仿GitHub贡献图样式,使用Canvas绘制:
javascript复制function drawHeatmap(ctx, data) {
const cellSize = 15
const gap = 2
const colors = ['#ebedf0', '#9be9a8', '#40c463', '#30a14e', '#216e39']
data.forEach((week, i) => {
week.forEach((day, j) => {
ctx.fillStyle = colors[Math.min(day.count, 4)]
ctx.fillRect(
i * (cellSize + gap),
j * (cellSize + gap),
cellSize,
cellSize
)
})
})
}
5.2 题目分类统计
使用D3.js生成环形图:
javascript复制const pie = d3.pie()
.value(d => d.count)
.sort(null)
const arcs = pie(data)
const arc = d3.arc()
.innerRadius(60)
.outerRadius(100)
6. 每日一题系统
6.1 推荐算法
基于以下因素计算推荐权重:
- 最近错误率
- 距离上次练习时间
- 题目难度系数
- 同类题目掌握程度
计算公式:
code复制weight =
(0.4 * errorRate) +
(0.3 * daysSinceLastTry) +
(0.2 * (1 - mastery)) +
(0.1 * normalizedDifficulty)
6.2 推送机制
实现两种提醒方式:
- 浏览器通知API
javascript复制new Notification('今日题目已更新', {
body: '动态规划:最长递增子序列',
icon: 'icon.png'
})
- 新标签页徽章计数
javascript复制chrome.action.setBadgeText({ text: '1' })
7. 实战踩坑记录
7.1 牛客网反爬策略
遇到的防护措施及解决方案:
- DOM结构突变:每周小更新,每月大更新
- 解决方案:配置多套选择器,添加自动降级机制
- 请求频率限制:
- 添加随机延迟(200-800ms)
- 错误自动重试(最多3次)
7.2 浏览器兼容问题
- Firefox的IndexedDB实现差异:
- 需要显式处理版本变更
- 添加事务超时保护
- Safari的WebKit限制:
- 禁用部分ES2020特性
- 使用polyfill填充API
8. 用户反馈迭代
收集到的典型需求与实现方案:
| 需求类型 | 实现方案 | 开发周期 |
|---|---|---|
| 题目收藏功能 | 添加star标记字段 | 2天 |
| 企业真题筛选 | 爬取题目元数据 | 1周 |
| 竞赛模式计时器 | 集成performance.now() | 3天 |
| 移动端适配 | 响应式面板布局 | 5天 |
9. 性能优化成果
优化前后对比(测试数据集:5000题记录):
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 加载时间 | 1200ms | 380ms |
| 搜索延迟 | 450ms | 90ms |
| 内存占用 | 85MB | 32MB |
| 存储体积 | 12MB | 4.7MB |
关键优化手段:
- 数据分片懒加载
- 二进制压缩存储
- Web Worker处理计算任务
10. 安全防护措施
实现的安全机制:
- 数据沙箱隔离
- 内容脚本运行在独立环境
- 通过
chrome.runtime通信
- 敏感操作确认
- 导出数据需密码验证
- 删除操作二次确认
- 加密备份
- 使用AES-256加密云同步数据
11. 扩展开发技巧
11.1 调试技巧
- 独立调试面板:
json复制// manifest.json
{
"devtools_page": "devtools.html"
}
- 跨上下文调试:
javascript复制chrome.debugger.attach({ tabId }, '1.2', () => {
chrome.debugger.sendCommand({ tabId }, 'Network.enable')
})
11.2 发布流程
自动化发布流水线:
yaml复制# .github/workflows/release.yml
steps:
- uses: actions/checkout@v3
- run: npm run build
- uses: chrome-extension-upload@v1
with:
client-id: ${{ secrets.CLIENT_ID }}
client-secret: ${{ secrets.CLIENT_SECRET }}
extension-id: ${{ secrets.EXTENSION_ID }}
refresh-token: ${{ secrets.REFRESH_TOKEN }}
file: dist/package.zip
12. 用户增长数据
上线半年后的关键指标:
| 时间段 | 新增用户 | 日活 | 平均使用时长 |
|---|---|---|---|
| 第1月 | 87 | 23 | 18min |
| 第3月 | 156 | 41 | 27min |
| 第6月 | 233 | 67 | 35min |
典型用户画像:
- 计算机专业应届生(62%)
- 转行学习算法的开发者(28%)
- 竞赛选手(10%)
13. 未来演进方向
- 智能错题本系统
- 自动生成错题分析报告
- 相似题目推荐
- 多人协作模式
- 组队刷题进度同步
- 题目讨论即时通讯
- AI辅助解题
- 代码错误智能诊断
- 最优解对比分析
14. 项目开源计划
核心模块开源策略:
| 模块 | 开源状态 | 仓库地址 |
|---|---|---|
| 牛客题目解析器 | MIT | github.com/dd-explorer/parser |
| 数据可视化组件 | Apache-2.0 | github.com/dd-explorer/charts |
| 浏览器插件框架 | 保留版权 | 私有仓库 |
社区贡献指南:
- Issue模板规范
- PR审核流程
- 代码风格检查
15. 开发环境配置
推荐工具链配置:
json复制{
"editorconfig": {
"charset": "utf-8",
"indent_style": "space",
"indent_size": 2
},
"vscode": {
"extensions": [
"Vue.volar",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}
}
16. 测试方案设计
自动化测试覆盖策略:
| 测试类型 | 工具 | 覆盖率目标 |
|---|---|---|
| 单元测试 | Jest | 80%+ |
| 集成测试 | Cypress | 主要流程 |
| 性能测试 | Lighthouse | 全部页面 |
| 安全测试 | OWASP ZAP | 关键路径 |
CI流水线配置示例:
yaml复制jobs:
test:
matrix:
os: [ubuntu-latest, windows-latest]
browser: [chrome, firefox]
steps:
- run: npm test
- run: npm run e2e -- --browser ${{ matrix.browser }}
17. 错误监控体系
实现的监控维度:
- 前端错误追踪
javascript复制window.addEventListener('error', (e) => {
navigator.sendBeacon('/log', {
msg: e.message,
stack: e.error.stack,
timestamp: Date.now()
})
})
- 性能指标上报
javascript复制const perfData = {
dcl: performance.timing.domContentLoadedEventEnd,
load: performance.timing.loadEventEnd
}
- 用户行为分析
javascript复制chrome.runtime.onMessage.addListener((req) => {
if (req.type === 'PAGE_VIEW') {
analytics.log(req.payload)
}
})
18. 数据迁移方案
版本升级时的数据迁移策略:
- 增量迁移(<1000条)
javascript复制function migrateV1ToV2(oldDb) {
return new Promise((resolve) => {
const tx = oldDb.transaction('questions')
tx.oncomplete = () => resolve()
})
}
- 批量迁移(>1000条)
javascript复制async function batchMigrate() {
const CHUNK_SIZE = 500
let cursor = await db.transaction('questions')
.store.openCursor()
while (cursor) {
const chunk = []
for (let i = 0; i < CHUNK_SIZE && cursor; i++) {
chunk.push(transform(cursor.value))
cursor = await cursor.continue()
}
await newDb.transaction('questions', 'readwrite')
.store.add(chunk)
}
}
19. 插件权限管理
声明的必要权限:
json复制{
"permissions": [
"storage",
"activeTab",
"scripting",
"notifications"
],
"host_permissions": [
"*://*.nowcoder.com/*"
]
}
权限使用规范:
- 按需请求额外权限
javascript复制chrome.permissions.request({
origins: ['*://*.nowcoder.com/*']
}, (granted) => {
if (granted) {
// 初始化高级功能
}
})
- 提供权限回收入口
javascript复制chrome.permissions.remove({
origins: ['*://*.nowcoder.com/*']
})
20. 国际化支持
多语言实现方案:
- 资源文件结构
code复制/locales
/en
messages.json
/zh-CN
messages.json
- 动态加载逻辑
javascript复制function loadLocale(lang) {
return import(`../locales/${lang}/messages.json`)
.then(msgs => i18n.setLocaleMessage(lang, msgs))
}
- 自动语言检测
javascript复制const userLang = navigator.language.startsWith('zh')
? 'zh-CN'
: 'en'