中文字幕日韩精品一区二区免费_精品一区二区三区国产精品无卡在_国精品无码专区一区二区三区_国产αv三级中文在线

網(wǎng)頁中文本朗讀功能開發(fā)和實(shí)現(xiàn)

前幾天完成了一個(gè)需求,在網(wǎng)頁中完成鼠標(biāo)指向哪里,就用語音讀出所指的文本。如果是按鈕、鏈接、文本輸入框,則還還要給出是什么的提醒。同時(shí)針對(duì)大段的文本,不能整段的去讀,要按照標(biāo)點(diǎn)符號(hào)進(jìn)行斷句處理。

創(chuàng)新互聯(lián)公司長(zhǎng)期為上千家客戶提供的網(wǎng)站建設(shè)服務(wù),團(tuán)隊(duì)從業(yè)經(jīng)驗(yàn)10年,關(guān)注不同地域、不同群體,并針對(duì)不同對(duì)象提供差異化的產(chǎn)品和服務(wù);打造開放共贏平臺(tái),與合作伙伴共同營(yíng)造健康的互聯(lián)網(wǎng)生態(tài)環(huán)境。為萬榮企業(yè)提供專業(yè)的做網(wǎng)站、成都網(wǎng)站建設(shè),萬榮網(wǎng)站改版等技術(shù)服務(wù)。擁有十年豐富建站經(jīng)驗(yàn)和眾多成功案例,為您定制開發(fā)。

重點(diǎn)當(dāng)然就是先獲取到當(dāng)前標(biāo)簽上的文本,再把文本轉(zhuǎn)化成語音即可。

標(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)容的全部帶朗讀文本了。

正文分隔

接下來要處理的就是正文分隔了,在這個(gè)過程中,踩了不少坑,走了不少彎路,好好記錄一下。

首先準(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)行分隔,分隔后分隔字符就丟了,也就是說把原文的一些標(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è)問題,丟失標(biāo)點(diǎn)的符號(hào),考慮過逐個(gè)標(biāo)點(diǎn)來進(jìn)行和替換 split 分隔方法為逐個(gè)字符循環(huán)來做。

前者問題是原本一次完成的工作分成了多次,效率太低。第二種感覺效率更低了,分隔本來是很稀疏的,但是卻要變成逐個(gè)字符出判斷處理,更關(guān)鍵的是,分隔標(biāo)點(diǎn)的位置要插入包裹標(biāo)簽,會(huì)導(dǎo)致字符串長(zhǎng)度變化,還要處理下標(biāo)索引。代碼是機(jī)器跑的,或許不會(huì)覺得煩,但是我真的覺得好煩。如果這么干,或許以后哪個(gè)AI或者同事看到這樣的代碼,說不定會(huì)說“這真是個(gè)傻xxxx”。

第二個(gè)問題想過很多辦法來補(bǔ)救,如先使用正則匹配捕獲內(nèi)容中成對(duì)的標(biāo)簽,對(duì)標(biāo)簽內(nèi)部的分隔先處理一遍,然后再處理整個(gè)的。

想不明白問題二的,可參考一下待分隔的段落:

這是一段測(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)容先處理。

但是問題又來了,這么處理的都是字符串,在js中都是基本類型,這些操作進(jìn)行的時(shí)候都是在復(fù)制的基礎(chǔ)上進(jìn)行的,要修改到原字符串里去,還得記錄下原本的開始結(jié)束位置,再將新的插進(jìn)去。繁,還是繁,但是已經(jīng)比之前逐個(gè)字符去遍歷的好,正則捕獲中本來就有了匹配的索引,直接用即可,還能接受。

但是這只是處理了段落內(nèi)部標(biāo)簽的問題,段落內(nèi)肯定還有很多文本是沒有處理呢,怎么辦?

正則匹配到了只是段落內(nèi)標(biāo)簽的結(jié)果啊,外面的沒有啊。哦,對(duì),有匹配到的索引,上次匹配到的位置加上上次處理的長(zhǎng)度,就是一段直接文本的開始。下一次匹配到的索引-1就是這段直接文本的結(jié)束。這只是匹配過程中的,還有首尾要單獨(dú)處理。又回到煩的老路上去了。。。

這么煩,一個(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)義,那最后再替換出來不就行了。

好了,方案終于有了,而且這個(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)回來

node.data = \'<\' + splitConfig.wrapTag + \'>\' +

node.data.replace(splitConfig.splitReg, \'$&<\' + splitConfig.wrapTag + \'>\') +

\'\';

break;

case 1:

// 元素節(jié)點(diǎn)

var innerHtml = node.innerHTML,

start = \'\',

end = \'\';

// 如果內(nèi)部還有直接標(biāo)簽,先去掉

var startResult = /^<w+?>/.exec(innerHtml);

if (startResult) {

start = startResult[0];

innerHtml = innerHtml.substr(start.length);

}

var endResult = /</w+?>$/.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 + \'>\') +

\'\' +

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\'), \'\');

$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 事件來完成。

原因:

不冒泡,不會(huì)觸發(fā)父元素的再次朗讀

不重復(fù)觸發(fā),一個(gè)元素內(nèi)移動(dòng)時(shí)不會(huì)重復(fù)觸發(fā)。

/**

* 在頁面上寫入高亮樣式

*/

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;

}

// 圖片樣式單獨(dú)處理 其他樣式統(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);

}

// 停止語音

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);

// 停止語音

stopSpeak();

});

注意要把針對(duì)段落的語音處理和其他地方的分開。為什么? 因?yàn)槎温涫莻€(gè)塊級(jí)元素,鼠標(biāo)移入段落中的空白時(shí),如:段落前后空白、首行縮進(jìn)、末行剩余空白等,是不應(yīng)該觸發(fā)朗讀的,如果不阻止掉,進(jìn)行這些區(qū)域?qū)⒅苯佑|發(fā)整段文字的朗讀,失去了我們對(duì)段落文本內(nèi)分隔的意義,而且,無論什么方式轉(zhuǎn)化語音都是要時(shí)間的,大段內(nèi)容可能需要較長(zhǎng)時(shí)間,影響語音輸出的體驗(yàn)。

文本合成語音

上面我們是直接使用了 speakText(text) 和 stopSpeak() 兩個(gè)方法來觸發(fā)語音的朗讀和停止。

我們來看下如何實(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 用于語音合成

lang : 語言 Gets and sets the language of the utterance.

pitch : 音高 Gets and sets the pitch at which the utterance will be spoken at.

rate : 語速 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() : 獲取瀏覽器支持的語音包列表 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() : 讀合成的語音,參數(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和說明可參考:

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音合成本來是個(gè)異步的操作,因此在過程中進(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)語音即可。

當(dāng)前標(biāo)題:網(wǎng)頁中文本朗讀功能開發(fā)和實(shí)現(xiàn)
標(biāo)題URL:http://www.rwnh.cn/article12/cjopdc.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供標(biāo)簽優(yōu)化、網(wǎng)站設(shè)計(jì)公司、網(wǎng)站營(yíng)銷、網(wǎng)站收錄全網(wǎng)營(yíng)銷推廣、響應(yīng)式網(wǎng)站

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)

商城網(wǎng)站建設(shè)
兖州市| 徐闻县| 虎林市| 独山县| 炉霍县| 邮箱| 石狮市| 琼海市| 昌邑市| 益阳市| 金平| 渝北区| 都兰县| 自治县| 岳普湖县| 固原市| 涿鹿县| 湖州市| 东明县| 博爱县| 通州市| 子长县| 秭归县| 永吉县| 将乐县| 宁国市| 柘荣县| 乡宁县| 丰镇市| 红原县| 凤山县| 开封市| 滦南县| 阜宁县| 大名县| 秭归县| 军事| 禹城市| 衢州市| 白城市| 天全县|