在这个信息爆炸的时代,我们每天都要面对大量的文字阅读。无论是学习、工作还是个人成长,阅读都扮演着至关重要的角色。然而,在快节奏的生活中,我们往往难以找到足够的安静时间专注于阅读。本文用 HTML + JavaScript 实现了一个基于Web的语音文章朗读器,为您带来全新的阅读体验。
效果演示
项目核心
本项目主要包含以下核心功能:
- 语音合成(Text-to-Speech)功能
- 控制播放、暂停、继续和停止操作
- 语音选择功能
- 阅读进度保存与恢复
- 句子级高亮显示
- 点击任意句子直接跳转并朗读
页面结构
控制区域
包含所有操作按钮(开始、暂停、继续、停止、重置)和语音选择下拉框。
<div class="controls">
<button id="playBtn">开始朗读</button>
<button id="pauseBtn" disabled>暂停</button>
<button id="resumeBtn" disabled>继续</button>
<button id="stopBtn" disabled>停止</button>
<select id="voiceSelect" class="voice-select"></select>
<button id="resetBtn">重置进度</button>
</div>
文章区域
包含多个段落,每个段落由多个可交互的句子组成。
<div class="article" id="article">
<p class="paragraph">
<span class="sentence">在编程的世界里,学习是一个永无止境的过程。</span>
<span class="sentence">随着技术的不断发展,我们需要不断更新自己的知识和技能。</span>
<span class="sentence">HTML、CSS和JavaScript是构建现代网页的三大基石。</span>
</p>
<p class="paragraph">
<span class="sentence">掌握这些基础技术后,你可以进一步学习各种前端框架和工具。</span>
<span class="sentence">React、Vue和Angular是目前最流行的前端框架。</span>
<span class="sentence">它们都采用了组件化的开发模式,提高了代码的可维护性和复用性。</span>
</p>
<p class="paragraph">
<span class="sentence">除了前端技术,后端开发也是全栈工程师必须掌握的技能。</span>
<span class="sentence">Node.js让JavaScript可以用于服务器端编程,大大扩展了JavaScript的应用范围。</span>
<span class="sentence">数据库技术也是开发中的重要组成部分。</span>
</p>
</div>
进度信息
显示当前阅读进度。
<div class="progress-info">
当前进度: <span id="progressText">0/0</span>
<div class="progress-bar-container">
<div class="progress-bar"></div>
</div>
</div>
核心功能实现
定义基础变量
获取DOM元素
const sentences = document.querySelectorAll('.sentence');
const playBtn = document.getElementById('playBtn');
const pauseBtn = document.getElementById('pauseBtn');
const resumeBtn = document.getElementById('resumeBtn');
const stopBtn = document.getElementById('stopBtn');
const resetBtn = document.getElementById('resetBtn');
const voiceSelect = document.getElementById('voiceSelect');
const progressText = document.getElementById('progressText');
const progressBar = document.querySelector('.progress-bar');
定义语音合成相关变量
let speechSynthesis = window.speechSynthesis;
let voices = [];
let currentUtterance = null;
let currentSentenceIndex = 0;
let isPaused = false;
语音合成初始化
通过 window.speechSynthesis API 获取系统支持的语音列表,并填充到下拉选择框中。
function initSpeechSynthesis() {
// 获取可用的语音列表
voices = speechSynthesis.getVoices();
// 填充语音选择下拉框
voiceSelect.innerHTML = '';
voices.forEach((voice, index) => {
const option = document.createElement('option');
option.value = index;
option.textContent = `${voice.name} (${voice.lang})`;
voiceSelect.appendChild(option);
});
// 尝试选择中文语音
const chineseVoice = voices.find(voice =>{
voice.lang.includes('zh') || voice.lang.includes('cmn')
});
if (chineseVoice) {
const voiceIndex = voices.indexOf(chineseVoice);
voiceSelect.value = voiceIndex;
}
}
句子朗读功能
function speakSentence(index) {
if (index >= sentences.length || index < 0) return;
// 停止当前朗读
if (currentUtterance) {
speechSynthesis.cancel();
}
// 更新当前句子高亮
updateHighlight(index);
// 创建新的语音合成实例
const selectedVoiceIndex = voiceSelect.value;
const utterance = new SpeechSynthesisUtterance(sentences[index].textContent);
if (voices[selectedVoiceIndex]) {
utterance.voice = voices[selectedVoiceIndex];
}
utterance.rate = 0.9; // 稍微慢一点的语速
// 朗读开始时的处理
utterance.onstart = function() {
sentences[index].classList.add('reading');
playBtn.disabled = true;
pauseBtn.disabled = false;
resumeBtn.disabled = true;
stopBtn.disabled = false;
};
// 朗读结束时的处理
utterance.onend = function() {
sentences[index].classList.remove('reading');
if (!isPaused) {
if (currentSentenceIndex >= sentences.length - 1) {
// 朗读完成
playBtn.disabled = false;
pauseBtn.disabled = true;
resumeBtn.disabled = true;
stopBtn.disabled = true;
updateProgressText();
return;
}
currentSentenceIndex++;
saveProgress();
speakSentence(currentSentenceIndex);
}
};
// 开始朗读
currentUtterance = utterance;
speechSynthesis.speak(utterance);
updateProgressText();
}
句子高亮功能
function updateHighlight(index) {
sentences.forEach((sentence, i) => {
sentence.classList.remove('current');
if (i === index) {
sentence.classList.add('current');
// 滚动到当前句子
sentence.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
});
}
更新进度文本
function updateProgressText() {
progressText.textContent = `${currentSentenceIndex + 1}/${sentences.length}`;
const percentage = (currentSentenceIndex + 1) / sentences.length * 100;
progressBar.style.width = `${percentage}%`;
}
进度保存与恢复
保存进度到本地存储
function saveProgress() {
localStorage.setItem('readingProgress', currentSentenceIndex);
localStorage.setItem('articleId', 'demoArticle');
updateProgressText();
}
从本地存储加载进度
function loadProgress() {
const savedArticleId = localStorage.getItem('articleId');
if (savedArticleId === 'demoArticle') {
const savedProgress = localStorage.getItem('readingProgress');
if (savedProgress !== null) {
currentSentenceIndex = parseInt(savedProgress);
if (currentSentenceIndex >= sentences.length) {
currentSentenceIndex = 0;
}
updateHighlight(currentSentenceIndex);
updateProgressText();
}
}
}
点击句子朗读跳转功能
sentences.forEach((sentence, index) => {
sentence.addEventListener('click', function() {
currentSentenceIndex = index;
speakSentence(currentSentenceIndex);
});
});
扩展建议
- 语速调节:增加语速调节滑块,让用户自定义朗读速
- 多语言支持:自动检测文本语言并选择合适的语音引擎
- 断句优化:改进自然语言处理逻辑,使朗读更符合口语习惯
- 多文章支持:扩展文章管理系统,允许用户选择不同文章进行朗读
完整代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文章逐句高亮朗读</title>
<style>
body {
font-family: 'Microsoft YaHei', sans-serif;
line-height: 1.6;
max-width: 800px;
margin: 0 auto;
padding: 40px 20px;
color: #333;
height: 100vh;
box-sizing: border-box;
background: linear-gradient(to bottom right, #f8f9fa, #e9ecef);
}
h1 {
text-align: center;
color: #2c3e50;
margin-bottom: 40px;
font-size: 2.5em;
letter-spacing: 2px;
position: relative;
animation: fadeInDown 1s ease-out forwards;
}
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
h1::after {
content: '';
display: block;
width: 100px;
height: 4px;
background: linear-gradient(to right, #3498db, #2980b9);
margin: 15px auto 0;
border-radius: 2px;
animation: growLine 1s ease-out forwards;
}
@keyframes growLine {
from {
width: 0;
}
to {
width: 100px;
}
}
.controls {
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
border-radius: 10px;
padding: 20px;
background-color: #ffffffcc;
display: flex;
flex-direction: column;
gap: 15px;
margin-bottom: 30px;
}
.controls > div {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 15px;
}
button {
padding: 10px 20px;
background: linear-gradient(135deg, #3498db, #2980b9);
color: white;
border: none;
border-radius: 25px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s ease-in-out;
box-shadow: 0 4px 6px rgba(52, 152, 219, 0.3);
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(52, 152, 219, 0.4);
}
button:disabled {
background: linear-gradient(135deg, #95a5a6, #7f8c8d);
box-shadow: none;
transform: none;
}
.article {
font-size: 18px;
line-height: 1.8;
background-color: #ffffffee;
border-radius: 10px;
padding: 25px;
margin-top: 30px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.05);
margin-bottom: 30px;
position: relative;
z-index: 0
}
.article::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(circle at top left, rgba(52, 152, 219, 0.05) 0%, transparent 100%);
z-index: -1;
border-radius: 10px;
}
.paragraph {
margin-bottom: 20px;
}
.sentence {
border-radius: 3px;
transition: all 0.3s ease-in-out;
cursor: pointer;
position: relative;
z-index: 1;
}
.sentence:hover {
background-color: #f0f0f0;
}
.sentence::after {
content: '';
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.3);
opacity: 0;
z-index: -1;
transition: opacity 0.3s ease-in-out;
}
.sentence:hover::after {
opacity: 1;
}
.current {
background-color: #fffde7 !important;
font-weight: bold;
transform: scale(1.05);
box-shadow: 0 2px 8px rgba(255, 221, 0, 0.3);
}
.progress-info {
text-align: center;
margin-top: 20px;
font-size: 14px;
color: #7f8c8d;
}
select {
padding: 8px;
border-radius: 4px;
border: 1px solid #bdc3c7;
font-size: 16px;
}
.voice-select {
min-width: 220px;
padding: 10px 12px;
border-radius: 25px;
border: 1px solid #bdc3c7;
font-size: 16px;
background-color: #f8f9fa;
transition: all 0.3s ease-in-out;
appearance: none;
background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24'%3E%3Cpath fill='%23555' d='M7 10l5 5 5-5z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 15px center;
background-size: 12px;
display: block;
margin: 0 auto;
}
.voice-select:focus {
outline: none;
border-color: #3498db;
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
}
.progress-info {
text-align: center;
margin-top: 30px;
font-size: 14px;
color: #7f8c8d;
position: relative;
height: 30px;
}
.progress-bar-container {
width: 100%;
height: 6px;
background-color: #ecf0f1;
border-radius: 3px;
overflow: hidden;
margin: 10px 0;
}
.progress-bar {
height: 100%;
width: 0;
background: linear-gradient(to right, #3498db, #2980b9);
transition: width 0.3s ease-in-out;
}
</style>
</head>
<body>
<h1>文章逐句高亮朗读</h1>
<div class="controls">
<div>
<button id="playBtn">开始朗读</button>
<button id="pauseBtn" disabled>暂停</button>
<button id="resumeBtn" disabled>继续</button>
<button id="stopBtn" disabled>停止</button>
<button id="resetBtn">重置进度</button>
</div>
<select id="voiceSelect" class="voice-select"></select>
</div>
<div class="article" id="article">
<p class="paragraph">
<span class="sentence">在编程的世界里,学习是一个永无止境的过程。</span>
<span class="sentence">随着技术的不断发展,我们需要不断更新自己的知识和技能。</span>
<span class="sentence">HTML、CSS和JavaScript是构建现代网页的三大基石。</span>
</p>
<p class="paragraph">
<span class="sentence">掌握这些基础技术后,你可以进一步学习各种前端框架和工具。</span>
<span class="sentence">React、Vue和Angular是目前最流行的前端框架。</span>
<span class="sentence">它们都采用了组件化的开发模式,提高了代码的可维护性和复用性。</span>
</p>
<p class="paragraph">
<span class="sentence">除了前端技术,后端开发也是全栈工程师必须掌握的技能。</span>
<span class="sentence">Node.js让JavaScript可以用于服务器端编程,大大扩展了JavaScript的应用范围。</span>
<span class="sentence">数据库技术也是开发中的重要组成部分。</span>
</p>
</div>
<div class="progress-info">
当前进度: <span id="progressText">0/0</span>
<div class="progress-bar-container">
<div class="progress-bar"></div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 获取DOM元素
const sentences = document.querySelectorAll('.sentence');
const playBtn = document.getElementById('playBtn');
const pauseBtn = document.getElementById('pauseBtn');
const resumeBtn = document.getElementById('resumeBtn');
const stopBtn = document.getElementById('stopBtn');
const resetBtn = document.getElementById('resetBtn');
const voiceSelect = document.getElementById('voiceSelect');
const progressText = document.getElementById('progressText');
const progressBar = document.querySelector('.progress-bar');
// 语音合成相关变量
let speechSynthesis = window.speechSynthesis;
let voices = [];
let currentUtterance = null;
let currentSentenceIndex = 0;
let isPaused = false;
// 从本地存储加载进度
loadProgress();
// 初始化语音合成
function initSpeechSynthesis() {
// 获取可用的语音列表
voices = speechSynthesis.getVoices();
// 填充语音选择下拉框
voiceSelect.innerHTML = '';
voices.forEach((voice, index) => {
const option = document.createElement('option');
option.value = index;
option.textContent = `${voice.name} (${voice.lang})`;
voiceSelect.appendChild(option);
});
// 尝试选择中文语音
const chineseVoice = voices.find(voice =>{
voice.lang.includes('zh') || voice.lang.includes('cmn')
});
if (chineseVoice) {
const voiceIndex = voices.indexOf(chineseVoice);
voiceSelect.value = voiceIndex;
}
}
// 语音列表加载可能需要时间
speechSynthesis.onvoiceschanged = initSpeechSynthesis;
initSpeechSynthesis();
// 朗读指定句子
function speakSentence(index) {
if (index >= sentences.length || index < 0) return;
// 停止当前朗读
if (currentUtterance) {
speechSynthesis.cancel();
}
// 更新当前句子高亮
updateHighlight(index);
// 创建新的语音合成实例
const selectedVoiceIndex = voiceSelect.value;
const utterance = new SpeechSynthesisUtterance(sentences[index].textContent);
if (voices[selectedVoiceIndex]) {
utterance.voice = voices[selectedVoiceIndex];
}
utterance.rate = 0.9; // 稍微慢一点的语速
// 朗读开始时的处理
utterance.onstart = function() {
sentences[index].classList.add('reading');
playBtn.disabled = true;
pauseBtn.disabled = false;
resumeBtn.disabled = true;
stopBtn.disabled = false;
};
// 朗读结束时的处理
utterance.onend = function() {
sentences[index].classList.remove('reading');
if (!isPaused) {
if (currentSentenceIndex >= sentences.length - 1) {
// 朗读完成
playBtn.disabled = false;
pauseBtn.disabled = true;
resumeBtn.disabled = true;
stopBtn.disabled = true;
updateProgressText();
return;
}
currentSentenceIndex++;
saveProgress();
speakSentence(currentSentenceIndex);
}
};
// 开始朗读
currentUtterance = utterance;
speechSynthesis.speak(utterance);
updateProgressText();
}
// 更新句子高亮
function updateHighlight(index) {
sentences.forEach((sentence, i) => {
sentence.classList.remove('current');
if (i === index) {
sentence.classList.add('current');
// 滚动到当前句子
sentence.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
});
}
// 更新进度文本
function updateProgressText() {
progressText.textContent = `${currentSentenceIndex + 1}/${sentences.length}`;
const percentage = (currentSentenceIndex + 1) / sentences.length * 100;
progressBar.style.width = `${percentage}%`;
}
// 保存进度到本地存储
function saveProgress() {
localStorage.setItem('readingProgress', currentSentenceIndex);
localStorage.setItem('articleId', 'demoArticle'); // 在实际应用中可以使用文章ID
updateProgressText();
}
// 从本地存储加载进度
function loadProgress() {
const savedArticleId = localStorage.getItem('articleId');
if (savedArticleId === 'demoArticle') {
const savedProgress = localStorage.getItem('readingProgress');
if (savedProgress !== null) {
currentSentenceIndex = parseInt(savedProgress);
if (currentSentenceIndex >= sentences.length) {
currentSentenceIndex = 0;
}
updateHighlight(currentSentenceIndex);
updateProgressText();
}
}
}
// 事件监听器
playBtn.addEventListener('click', function() {
currentSentenceIndex = 0;
speakSentence(currentSentenceIndex);
});
pauseBtn.addEventListener('click', function() {
if (speechSynthesis.speaking && !isPaused) {
speechSynthesis.pause();
isPaused = true;
pauseBtn.disabled = true;
resumeBtn.disabled = false;
}
});
resumeBtn.addEventListener('click', function() {
if (isPaused) {
speechSynthesis.resume();
isPaused = false;
pauseBtn.disabled = false;
resumeBtn.disabled = true;
}
});
stopBtn.addEventListener('click', function() {
speechSynthesis.cancel();
isPaused = false;
playBtn.disabled = false;
pauseBtn.disabled = true;
resumeBtn.disabled = true;
stopBtn.disabled = true;
// 移除所有朗读样式
sentences.forEach(sentence => {
sentence.classList.remove('reading');
});
});
resetBtn.addEventListener('click', function() {
localStorage.removeItem('readingProgress');
localStorage.removeItem('articleId');
currentSentenceIndex = 0;
updateHighlight(currentSentenceIndex);
updateProgressText();
});
// 点击句子跳转到该句子并朗读
sentences.forEach((sentence, index) => {
sentence.addEventListener('click', function() {
currentSentenceIndex = index;
speakSentence(currentSentenceIndex);
});
});
});
</script>
</body>
</html>