字
字节笔记本
2026年2月22日
Qwerty Learner useWordStats Hook 源码解析
本文介绍 Qwerty Learner 项目中 useWordStats Hook 的实现,这是一个用于统计分析用户打字练习数据的自定义 React Hook。该 Hook 从 IndexedDB 中读取练习记录,计算并返回多种维度的统计数据,包括练习次数、词数统计、WPM(每分钟字数)、正确率以及错误按键分布。
文件概述
useWordStats.ts 位于 src/pages/Analysis/hooks/ 目录下,是 Qwerty Learner 数据分析功能的核心 Hook。它接收时间范围参数,返回该时间段内的打字练习统计数据,用于生成热力图、趋势图等可视化图表。
核心接口定义
typescript
interface IWordStats {
isEmpty?: boolean
exerciseRecord: Activity[] // 每日练习次数(用于热力图)
wordRecord: Activity[] // 每日练习词数(用于热力图)
wpmRecord: [string, number][] // 每日 WPM 数据
accuracyRecord: [string, number][] // 每日正确率数据
wrongTimeRecord: { name: string; value: number }[] // 错误按键统计
}主要功能实现
1. 日期范围计算
typescript
function getDatesBetween(start: number, end: number) {
const dates = []
let curr = dayjs(start).startOf('day')
const last = dayjs(end).endOf('day')
while (curr.diff(last) < 0) {
dates.push(curr.clone().format('YYYY-MM-DD'))
curr = curr.add(1, 'day')
}
return dates
}使用 dayjs 计算两个时间戳之间的所有日期,为数据统计建立日期索引。
2. 活跃度等级计算
typescript
function getLevel(value: number) {
if (value === 0) return 0
else if (value < 4) return 1
else if (value < 8) return 2
else if (value < 12) return 3
else return 4
}将数值映射为 0-4 的等级,用于热力图的颜色深浅表示。
3. Hook 主函数
typescript
export function useWordStats(startTimeStamp: number, endTimeStamp: number) {
const [wordStats, setWordStats] = useState<IWordStats>({
exerciseRecord: [],
wordRecord: [],
wpmRecord: [],
accuracyRecord: [],
wrongTimeRecord: [],
})
useEffect(() => {
const fetchWordStats = async () => {
const stats = await getChapterStats(startTimeStamp, endTimeStamp)
setWordStats(stats)
}
fetchWordStats()
}, [startTimeStamp, endTimeStamp])
return wordStats
}标准的 React Hook 模式,接收起止时间戳,返回统计数据状态。
4. 数据统计核心逻辑
typescript
async function getChapterStats(startTimeStamp: number, endTimeStamp: number): Promise<IWordStats> {
// 从 IndexedDB 查询时间范围内的记录
const records: IWordRecord[] = await db.wordRecords
.where('timeStamp')
.between(startTimeStamp, endTimeStamp)
.toArray()
if (records.length === 0) {
return { isEmpty: true, ... }
}
// 初始化日期数据结构
let data: { [date: string]: {
exerciseTime: number // 练习次数
words: string[] // 练习词数组
totalTime: number // 总计用时
wrongCount: number // 错误次数
wrongKeys: string[] // 错误按键
} } = {}
// 聚合统计数据
for (let i = 0; i < records.length; i++) {
const date = dayjs(records[i].timeStamp * 1000).format('YYYY-MM-DD')
data[date].exerciseTime++
data[date].words.push(records[i].word)
data[date].totalTime += records[i].timing.reduce((acc, curr) => acc + curr, 0)
data[date].wrongCount += records[i].wrongCount
data[date].wrongKeys.push(...Object.values(records[i].mistakes).flat())
}
// 生成各类统计记录...
}5. 统计指标计算
练习次数统计:
typescript
const exerciseRecord = RecordArray.map(([date, { exerciseTime }]) => ({
date,
count: exerciseTime,
level: getLevel(exerciseTime),
}))练习词数统计(去重):
typescript
const wordRecord = RecordArray.map(([date, { words }]) => ({
date,
count: Array.from(new Set(words)).length,
level: getLevel(Array.from(new Set(words)).length),
}))WPM(每分钟字数)计算:
typescript
const wpmRecord = RecordArray.map<[string, number]>(([date, { words, totalTime }]) => [
date,
Math.round(words.length / (totalTime / 1000 / 60)),
]).filter((d) => d[1])正确率计算:
typescript
const accuracyRecord = RecordArray.map<[string, number]>(([date, { words, wrongCount }]) => [
date,
Math.round((words.join('').length / (words.join('').length + wrongCount)) * 100),
]).filter((d) => d[1])错误按键统计:
typescript
const wrongTimeRecord: { name: string; value: number }[] = []
const allWrongTime = RecordArray
.map(([, { wrongKeys }]) => wrongKeys)
.flat()
.map((key) => key.toUpperCase())
allWrongTime.forEach((key) => {
const index = wrongTimeRecord.findIndex((item) => item.name === key)
if (index === -1) {
wrongTimeRecord.push({ name: key, value: 1 })
} else {
wrongTimeRecord[index].value++
}
})技术亮点
- IndexedDB 数据查询:使用 Dexie.js 的链式 API 高效查询时间范围数据
- 日期数据处理:使用 dayjs 进行日期格式化和计算
- 数据聚合模式:通过 reduce 和 map 实现多维度统计
- 类型安全:完整的 TypeScript 类型定义
项目链接
分享: