基于JavaScript+HTML实现文章逐句高亮朗读功能
作者:技术小丁
在这个信息爆炸的时代,我们每天都要面对大量的文字阅读,无论是学习、工作还是个人成长,阅读都扮演着至关重要的角色,然而,在快节奏的生活中,我们往往难以找到足够的安静时间专注于阅读,本文用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>
到此这篇关于基于JavaScript+HTML实现文章逐句高亮朗读功能的文章就介绍到这了,更多相关JavaScript HTML文章高亮朗读内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!