摘要:網(wǎng)頁(yè)中文本朗讀功能開發(fā)實(shí)現(xiàn)分享文本首發(fā)我的博客前幾天完成了一個(gè)需求,在網(wǎng)頁(yè)中完成鼠標(biāo)指向哪里,就用語(yǔ)音讀出所指的文本。獲取完整朗讀文本要處理的朗讀文本這樣就可以獲取到一個(gè)標(biāo)簽的功能提醒和內(nèi)容的全部帶朗讀文本了。
網(wǎng)頁(yè)中文本朗讀功能開發(fā)實(shí)現(xiàn)分享
文本首發(fā)我的博客 - https://blog.cdswyda.com/post/2017120914
前幾天完成了一個(gè)需求,在網(wǎng)頁(yè)中完成鼠標(biāo)指向哪里,就用語(yǔ)音讀出所指的文本。如果是按鈕、鏈接、文本輸入框,則還還要給出是什么的提醒。同時(shí)針對(duì)大段的文本,不能整段的去讀,要按照標(biāo)點(diǎn)符號(hào)進(jìn)行斷句處理。
重點(diǎn)當(dāng)然就是先獲取到當(dāng)前標(biāo)簽上的文本,再把文本轉(zhuǎn)化成語(yǔ)音即可。
標(biāo)簽朗讀這個(gè)很簡(jiǎn)單了,只用根據(jù)當(dāng)前是什么標(biāo)簽,給出提示即可。
// 標(biāo)簽朗讀文本 var tagTextConfig = { "a": "鏈接", "input[text]": "文本輸入框", "input[password]": "密碼輸入框", "button": "按鈕", "img": "圖片" };
還有需要朗讀的標(biāo)簽,繼續(xù)再添加即可。
然后根據(jù)標(biāo)簽,返回前綴文本即可。
/** * 獲取標(biāo)簽朗讀文本 * @param {HTMLElement} el 要處理的HTMLElement * @returns {String} 朗讀文本 */ function getTagText(el) { if (!el) return ""; var tagName = el.tagName.toLowerCase(); // 處理input等多屬性元素 switch (tagName) { case "input": tagName += "[" + el.type + "]"; break; default: break; } // 標(biāo)簽的功能提醒和作用應(yīng)該有間隔,因此在最后加入一個(gè)空格 return (tagTextConfig[tagName] || "") + " "; }
獲取完整的朗讀文本就更簡(jiǎn)單了,先取標(biāo)簽的功能提醒,再取標(biāo)簽的文本即可。
文本內(nèi)容優(yōu)先取 title 其次 alt 最后 innerText。
/** * 獲取完整朗讀文本 * @param {HTMLElement} el 要處理的HTMLElement * @returns {String} 朗讀文本 */ function getText(el) { if (!el) return ""; return getTagText(el) + (el.title || el.alt || el.innerText || ""); }
這樣就可以獲取到一個(gè)標(biāo)簽的功能提醒和內(nèi)容的全部帶朗讀文本了。
正文分隔接下來(lái)要處理的就是正文分隔了,在這個(gè)過(guò)程中,踩了不少坑,走了不少?gòu)澛罚煤糜涗浺幌隆?/p>
首先準(zhǔn)備了正文分隔的配置:
// 正文拆分配置 var splitConfig = { // 內(nèi)容分段標(biāo)簽名稱 unitTag: "p", // 正文中分隔正則表達(dá)式 splitReg: /[,;,;。]/g, // 包裹標(biāo)簽名 wrapTag: "label", // 包裹標(biāo)簽類名 wrapCls: "speak-lable", // 高亮樣式名和樣式 hightlightCls: "speak-help-hightlight", hightStyle: "background: #000!important; color: #fff!important" };
最開始想的就是直接按照正文中的分隔標(biāo)點(diǎn)符號(hào)進(jìn)行分隔就好了呀。
想法如下:
獲取段落全部文本
使用 split(分隔正則表達(dá)式) 方法將正文按照標(biāo)點(diǎn)符號(hào)分隔成小段
每個(gè)小段用標(biāo)簽包裹放回去即可
然而理想很豐滿,現(xiàn)實(shí)很骨感。
兩個(gè)大坑如下:
split 方法進(jìn)行分隔,分隔后分隔字符就丟了,也就是說(shuō)把原文的一些標(biāo)點(diǎn)符號(hào)給弄丟了。
如果段落內(nèi)還存在其他標(biāo)簽,而這個(gè)標(biāo)簽內(nèi)部也正好存在待分隔的標(biāo)點(diǎn)符號(hào),那包裹分段標(biāo)簽時(shí)直接破換了原標(biāo)簽的完整性。
關(guān)于第一個(gè)問(wèn)題,丟失標(biāo)點(diǎn)的符號(hào),考慮過(guò)逐個(gè)標(biāo)點(diǎn)來(lái)進(jìn)行和替換 split 分隔方法為逐個(gè)字符循環(huán)來(lái)做。
前者問(wèn)題是原本一次完成的工作分成了多次,效率太低。第二種感覺效率更低了,分隔本來(lái)是很稀疏的,但是卻要變成逐個(gè)字符出判斷處理,更關(guān)鍵的是,分隔標(biāo)點(diǎn)的位置要插入包裹標(biāo)簽,會(huì)導(dǎo)致字符串長(zhǎng)度變化,還要處理下標(biāo)索引。代碼是機(jī)器跑的,或許不會(huì)覺得煩,但是我真的覺得好煩。如果這么干,或許以后哪個(gè)AI或者同事看到這樣的代碼,說(shuō)不定會(huì)說(shuō)“這真是個(gè)傻xxxx”。
第二個(gè)問(wèn)題想過(guò)很多辦法來(lái)補(bǔ)救,如先使用正則匹配捕獲內(nèi)容中成對(duì)的標(biāo)簽,對(duì)標(biāo)簽內(nèi)部的分隔先處理一遍,然后再處理整個(gè)的。
想不明白問(wèn)題二的,可參考一下待分隔的段落:
這是一段測(cè)試文本,這里有個(gè)鏈接。您好,可以點(diǎn)擊此處進(jìn)行跳轉(zhuǎn)還有其他內(nèi)容其他內(nèi)容容其他內(nèi)容容其他內(nèi)容,容其他內(nèi)容。
如先使用/<((w+?)>)(.+?)2(?=>)/g 正則,依次捕獲段落內(nèi)被標(biāo)簽包裹的內(nèi)容,對(duì)標(biāo)簽內(nèi)部的內(nèi)容先處理。
但是問(wèn)題又來(lái)了,這么處理的都是字符串,在js中都是基本類型,這些操作進(jìn)行的時(shí)候都是在復(fù)制的基礎(chǔ)上進(jìn)行的,要修改到原字符串里去,還得記錄下原本的開始結(jié)束位置,再將新的插進(jìn)去。繁,還是繁,但是已經(jīng)比之前逐個(gè)字符去遍歷的好,正則捕獲中本來(lái)就有了匹配的索引,直接用即可,還能接受。
但是這只是處理了段落內(nèi)部標(biāo)簽的問(wèn)題,段落內(nèi)肯定還有很多文本是沒有處理呢,怎么辦?
正則匹配到了只是段落內(nèi)標(biāo)簽的結(jié)果啊,外面的沒有啊。哦,對(duì),有匹配到的索引,上次匹配到的位置加上上次處理的長(zhǎng)度,就是一段直接文本的開始。下一次匹配到的索引-1就是這段直接文本的結(jié)束。這只是匹配過(guò)程中的,還有首尾要多帶帶處理。又回到煩的老路上去了。。。
這么煩,一個(gè)段落分隔能這么繁瑣,我不信!
突然想到了,有文本節(jié)點(diǎn)這么個(gè)東西,刪繁就簡(jiǎn)嘛,正則先到邊上去,直接處理段落的所有節(jié)點(diǎn)不就行了。
文本節(jié)點(diǎn)則分隔直接包裹,標(biāo)簽節(jié)點(diǎn)則對(duì)內(nèi)容進(jìn)行包裹,這種情況下處理的直接是dom,更省事。
文本節(jié)點(diǎn)里放標(biāo)簽?這是在開玩笑么,是也不是。文本節(jié)點(diǎn)里確實(shí)只能放文本,但是我把標(biāo)簽直接放進(jìn)去,它會(huì)自動(dòng)轉(zhuǎn)義,那最后再替換出來(lái)不就行了。
好了,方案終于有了,而且這個(gè)方案邏輯多簡(jiǎn)單,代碼邏輯自然也不會(huì)煩。
/** * 正文內(nèi)容分段處理 * @param {jQueryObject/HTMLElement/String} $content 要處理的正文jQ對(duì)象或HTMLElement或其對(duì)應(yīng)選擇器 */ function splitConent($content) { $content = $($content); $content.find(splitConfig.unitTag).each(function (index, item) { var $item = $(item), text = $.trim($item.text()); if (!text) return; var nodes = $item[0].childNodes; $.each(nodes, function (i, node) { switch (node.nodeType) { case 3: // text 節(jié)點(diǎn) // 由于是文本節(jié)點(diǎn),標(biāo)簽被轉(zhuǎn)義了,后續(xù)再轉(zhuǎn)回來(lái) node.data = "<" + splitConfig.wrapTag + ">" + node.data.replace(splitConfig.splitReg, "" + splitConfig.wrapTag + ">$&<" + splitConfig.wrapTag + ">") + "" + splitConfig.wrapTag + ">"; break; case 1: // 元素節(jié)點(diǎn) var innerHtml = node.innerHTML, start = "", end = ""; // 如果內(nèi)部還有直接標(biāo)簽,先去掉 var startResult = /^/.exec(innerHtml); if (startResult) { start = startResult[0]; innerHtml = innerHtml.substr(start.length); } var endResult = / $/.exec(innerHtml); if (endResult) { end = endResult[0]; innerHtml = innerHtml.substring(0, endResult.index); } // 更新內(nèi)部?jī)?nèi)容 node.innerHTML = start + "<" + splitConfig.wrapTag + ">" + innerHtml.replace(splitConfig.splitReg, "" + splitConfig.wrapTag + ">$&<" + splitConfig.wrapTag + ">") + "" + splitConfig.wrapTag + ">" + end; break; default: break; } }); // 處理文本節(jié)點(diǎn)中被轉(zhuǎn)義的html標(biāo)簽 $item[0].innerHTML = $item[0].innerHTML .replace(new RegExp("<" + splitConfig.wrapTag + ">", "g"), "<" + splitConfig.wrapTag + ">") .replace(new RegExp("" + splitConfig.wrapTag + ">", "g"), "" + splitConfig.wrapTag + ">"); $item.find(splitConfig.wrapTag).addClass(splitConfig.wrapCls); }); }
上面代碼中最后對(duì)文本節(jié)點(diǎn)中被轉(zhuǎn)義的包裹標(biāo)簽替換似乎有點(diǎn)麻煩,但是沒辦法,ES5之前JavaScript并不支持正則的后行斷言(也就是正則表達(dá)式中“后顧”)。所以沒辦法對(duì)包裹標(biāo)簽前后的 < 和 > 進(jìn)行精準(zhǔn)替換,只能連同標(biāo)簽名一起替換。
事件處理在上面完成了文本獲取和段落分隔,下面要做的就是鼠標(biāo)移動(dòng)上去時(shí)獲取文本觸發(fā)朗讀即可,移開時(shí)停止朗讀即可。
鼠標(biāo)移動(dòng),只讀一次,基于這兩點(diǎn)原因,使用 mouseenter 和 mouseleave 事件來(lái)完成。
原因:
不冒泡,不會(huì)觸發(fā)父元素的再次朗讀
不重復(fù)觸發(fā),一個(gè)元素內(nèi)移動(dòng)時(shí)不會(huì)重復(fù)觸發(fā)。
/** * 在頁(yè)面上寫入高亮樣式 */ function createStyle() { if (document.getElementById("speak-light-style")) return; var style = document.createElement("style"); style.id = "speak-light-style"; style.innerText = "." + splitConfig.hightlightCls + "{" + splitConfig.hightStyle + "}"; document.getElementsByTagName("head")[0].appendChild(style); } // 非正文需要朗讀的標(biāo)簽 逗號(hào)分隔 var speakTags = "a, p, span, h1, h2, h3, h4, h5, h6, img, input, button"; $(document).on("mouseenter.speak-help", speakTags, function (e) { var $target = $(e.target); // 排除段落內(nèi)的 if ($target.parents("." + splitConfig.wrapCls).length || $target.find("." + splitConfig.wrapCls).length) { return; } // 圖片樣式多帶帶處理 其他樣式統(tǒng)一處理 if (e.target.nodeName.toLowerCase() === "img") { $target.css({ border: "2px solid #000" }); } else { $target.addClass(splitConfig.hightlightCls); } // 開始朗讀 speakText(getText(e.target)); }).on("mouseleave.speak-help", speakTags, function (e) { var $target = $(e.target); if ($target.find("." + splitConfig.wrapCls).length) { return; } // 圖片樣式 if (e.target.nodeName.toLowerCase() === "img") { $target.css({ border: "none" }); } else { $target.removeClass(splitConfig.hightlightCls); } // 停止語(yǔ)音 stopSpeak(); }); // 段落內(nèi)文本朗讀 $(document).on("mouseenter.speak-help", "." + splitConfig.wrapCls, function (e) { $(this).addClass(splitConfig.hightlightCls); // 開始朗讀 speakText(getText(this)); }).on("mouseleave.speak-help", "." + splitConfig.wrapCls, function (e) { $(this).removeClass(splitConfig.hightlightCls); // 停止語(yǔ)音 stopSpeak(); });
注意要把針對(duì)段落的語(yǔ)音處理和其他地方的分開。為什么? 因?yàn)槎温涫莻€(gè)塊級(jí)元素,鼠標(biāo)移入段落中的空白時(shí),如:段落前后空白、首行縮進(jìn)、末行剩余空白等,是不應(yīng)該觸發(fā)朗讀的,如果不阻止掉,進(jìn)行這些區(qū)域?qū)⒅苯佑|發(fā)整段文字的朗讀,失去了我們對(duì)段落文本內(nèi)分隔的意義,而且,無(wú)論什么方式轉(zhuǎn)化語(yǔ)音都是要時(shí)間的,大段內(nèi)容可能需要較長(zhǎng)時(shí)間,影響語(yǔ)音輸出的體驗(yàn)。
文本合成語(yǔ)音上面我們是直接使用了 speakText(text) 和 stopSpeak() 兩個(gè)方法來(lái)觸發(fā)語(yǔ)音的朗讀和停止。
我們來(lái)看下如何實(shí)現(xiàn)這個(gè)兩個(gè)功能。
其實(shí)現(xiàn)代瀏覽器默認(rèn)已經(jīng)提供了上面功能:
var speechSU = new window.SpeechSynthesisUtterance(); speechSU.text = "你好,世界!"; window.speechSynthesis.speak(speechSU);
復(fù)制到瀏覽器控制臺(tái)看看能不能聽到聲音呢?(需要Chrome 33+、Firefox 49+ 或 IE-Edge)
利用一下兩個(gè)API即可:
SpeechSynthesisUtterance 用于語(yǔ)音合成
lang : 語(yǔ)言 Gets and sets the language of the utterance.
pitch : 音高 Gets and sets the pitch at which the utterance will be spoken at.
rate : 語(yǔ)速 Gets and sets the speed at which the utterance will be spoken at.
text : 文本 Gets and sets the text that will be synthesised when the utterance is spoken.
voice : 聲音 Gets and sets the voice that will be used to speak the utterance.
volume : 音量 Gets and sets the volume that the utterance will be spoken at.
onboundary : 單詞或句子邊界觸發(fā),即分隔處觸發(fā) Fired when the spoken utterance reaches a word or sentence boundary.
onend : 結(jié)束時(shí)觸發(fā) Fired when the utterance has finished being spoken.
onerror : 錯(cuò)誤時(shí)觸發(fā) Fired when an error occurs that prevents the utterance from being succesfully spoken.
onmark : Fired when the spoken utterance reaches a named SSML "mark" tag.
onpause : 暫停時(shí)觸發(fā) Fired when the utterance is paused part way through.
onresume : 重新播放時(shí)觸發(fā) Fired when a paused utterance is resumed.
onstart : 開始時(shí)觸發(fā) Fired when the utterance has begun to be spoken.
SpeechSynthesis : 用于朗讀
paused : Read only 是否暫停 A Boolean that returns true if the SpeechSynthesis object is in a paused state.
pending : Read only 是否處理中 A Boolean that returns true if the utterance queue contains as-yet-unspoken utterances.
speaking : Read only 是否朗讀中 A Boolean that returns true if an utterance is currently in the process of being spoken — even if SpeechSynthesis is in a paused state.
onvoiceschanged : 聲音變化時(shí)觸發(fā)
cancel() : 情況待朗讀隊(duì)列 Removes all utterances from the utterance queue.
getVoices() : 獲取瀏覽器支持的語(yǔ)音包列表 Returns a list of SpeechSynthesisVoice objects representing all the available voices on the current device.
pause() : 暫停 Puts the SpeechSynthesis object into a paused state.
resume() : 重新開始 Puts the SpeechSynthesis object into a non-paused state: resumes it if it was already paused.
speak() : 讀合成的語(yǔ)音,參數(shù)必須為SpeechSynthesisUtterance的實(shí)例 Adds an utterance to the utterance queue; it will be spoken when any other utterances queued before it have been spoken.
詳細(xì)api和說(shuō)明可參考:
MDN - SpeechSynthesisUtterance
MDN - SpeechSynthesis
那么上面的兩個(gè)方法可以寫為:
var speaker = new window.SpeechSynthesisUtterance(); var speakTimer, stopTimer; // 開始朗讀 function speakText(text) { clearTimeout(speakTimer); window.speechSynthesis.cancel(); speakTimer = setTimeout(function () { speaker.text = text; window.speechSynthesis.speak(speaker); }, 200); } // 停止朗讀 function stopSpeak() { clearTimeout(stopTimer); clearTimeout(speakTimer); stopTimer = setTimeout(function () { window.speechSynthesis.cancel(); }, 20); }
因?yàn)檎Z(yǔ)音合成本來(lái)是個(gè)異步的操作,因此在過(guò)程中進(jìn)行以上處理。
現(xiàn)代瀏覽器已經(jīng)內(nèi)置了這個(gè)功能,兩個(gè)API接口兼容性如下:
Feature | Chrome | Edge | Firefox (Gecko) | Internet Explorer | Opera | Safari |
---|---|---|---|---|---|---|
(WebKit) Basic | support 33 | (Yes) | 49 (49) | No support | ? | 7 |
如果要兼容其他瀏覽器或者需要一種完美兼容的解決方案,可能就需要服務(wù)端完成了,根據(jù)給定文本,返回相應(yīng)語(yǔ)音即可,百度語(yǔ)音 http://yuyin.baidu.com/docs就提供這樣的服務(wù)。
cdswyda - 網(wǎng)頁(yè)文本朗讀實(shí)現(xiàn) - github
cdswyda - 網(wǎng)頁(yè)文本朗讀實(shí)現(xiàn) - demo
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/92382.html
摘要:用戶代理瀏覽器給視障用戶朗讀網(wǎng)頁(yè)的屏幕閱讀器,以及搜索引擎放出的爬蟲都是用戶代理,它們需要顯示朗讀和分析網(wǎng)頁(yè)。小知識(shí)屬性中的文本會(huì)在圖片因故未能加載時(shí)顯示,或者由屏幕閱讀器朗讀出來(lái)。 前言 代碼網(wǎng)址:http://www.stylinwithcss.com/ 第一章 HTML標(biāo)記與文檔結(jié)構(gòu) 1.html的含義 HTML 標(biāo)記內(nèi)容的目的是為了賦予網(wǎng)頁(yè)語(yǔ)義(semantic)。就是要給你的...
摘要:作為一名資深碼農(nóng),結(jié)合身邊一群民工的真實(shí)體驗(yàn),小編有那么一點(diǎn)權(quán)威給各位推薦幾款程序員必備常用的擴(kuò)展插件。插件是一款為谷歌瀏覽器定制的非常強(qiáng)大的一款管理插件。 作為一名資深碼農(nóng),結(jié)合身邊一群IT民工的真實(shí)體驗(yàn),小編有那么一點(diǎn)權(quán)威給各位推薦幾款程序員必備、常用的chrome擴(kuò)展插件。1.Click&Clean下載地址:http://www.cnplugins.com/offi...Clic...
閱讀 1209·2021-11-17 09:33
閱讀 3617·2021-09-28 09:42
閱讀 3345·2021-09-13 10:35
閱讀 2504·2021-09-06 15:00
閱讀 2450·2021-08-27 13:12
閱讀 3617·2021-07-26 23:38
閱讀 1856·2019-08-30 15:55
閱讀 546·2019-08-30 15:53