前几天完成一个需求。鼠标指向网页上的任何地方,所指的文字就会被语音朗读出来。如果它是一个按钮、链接或文本输入框,也应该给出它是什么的提示。同时,对于大段文字,无法通读整段,需要根据标点符号进行分句。当然,重点是先获取当前标签上的文字,然后将文字转化为语音。标签读取非常简单,根据当前标签给出提示即可。//标签读取文本vartagTextConfig={'a':'link','input[text]':'文本输入框','input[password]':'密码输入框','button':'button','img':'图片'};还有标签需要朗读,继续添加即可。然后根据标签,返回前缀文本。/***获取标签读取文本*@param{HTMLElement}el待处理的HTMLElement*@returns{String}读取文本*/functiongetTagText(el){if(!el)return'';vartagName=el.tagName.toLowerCase();//处理input等多属性元素switch(tagName){case'input':tagName+='['+el.type+']';break;default:break;}//函数提醒和函数标签的应该是有空隙的,所以加个空格return(tagTextConfig[tagName]||'')+'';}得到完整的文字朗读。先获取标签的功能提醒,再获取标签的文字比较容易。文本内容先取title,再取alt***innerText。/***获取完整的阅读文本*@param{HTMLElement}el待处理的HTMLElement*@returns{String}阅读文本*/functiongetText(el){if(!el)return'';returngetTagText(el)+(el.title||el.alt||el.innerText||'');}这样就可以得到功能提醒和一个带有朗读文本的标签的所有内容。文本分离接下来要处理的是文本分离。在这个过程中踩了很多坑,走了很多弯路,所以我会好好记录一下。首先准备文本分割的配置://文本分割配置varsplitConfig={//内容分割标签名unitTag:'p',//文本分割正则表达式splitReg:/[,;,;。]/g,//wrap标签名wrapTag:'label',//wrap标签类名wrapCls:'speak-lable',//高亮样式名称和样式hightlightCls:'speak-help-hightlight',hightStyle:'background:#000!重要;颜色:#fff!重要'};我首先想到的是直接根据文中的标点符号将文分开。思路如下:获取一段的所有文字使用split(分隔正则表达式)方法将文字按照标点符号分割成小段。每个小段都可以用标签包裹并放回去。然而,理想很丰满,现实很骨感。两个大坑如下:split方法进行分离,分离后分离字符丢失,也就是原文中的部分标点符号丢失。如果段落中还有其他标签,而这个标签内部有标点符号需要分隔,那么在对段标签进行换行时,会直接破坏原标签的完整性。关于第一个问题,标点符号丢失了,我考虑过一个一个做,用一个字符一个字符循环代替split分隔法。前者的问题是,原本一次性完成的工作被分成了多次,效率太低。第二感觉效率低。分离本来就很稀疏,却要逐字判断。更重要的是,分隔标点的位置需要插入到包装标签中,这会导致字符串的长度发生变化。处理下标索引。代码是机器跑的,也许不会很烦,但我真的觉得很烦。如果你这样做了,也许以后任何一个AI或同事看到这样的代码都会说“这真是愚蠢的xxxx”。第二个问题想了很多办法补救,比如用正则匹配来抓取内容中成对的标签,先处理标签内部的分离,再处理整体。如果第二题不明白,可以参考要分割的段落:
这是一篇测试文,这里是链接。您好,可以点这里跳转还有其他内容,其他内容,其他内容,其他内容,其他内容。
如果先使用/<((\w+?)>)(.+?)<\/\2(?=>)/g规则,段落中被标签包裹的内容会被抓取到转,先处理标签里面的Content。但是问题又来了。所有的字符串都是这样处理的,在js中都是基本类型。这些操作都是在复制的基础上进行的。如果要修改成原来的字符串,就得记录下来。原来的开始和结束位置,然后插入新的。复杂,还是复杂,但总比之前一个字一个字遍历好。正则抓包中已经有匹配索引,直接使用即可。但这只是处理段落内部标签的问题。段落中肯定有很多文字没有经过处理。我应该怎么办?正则匹配只是段落内标签的结果,不包含外部标签。哦,对了,有匹配索引,最后匹配的位置加上最后处理的长度就是一段直接文本的开头。下一个匹配项的索引-1是此直接文本的结尾。这只是在匹配过程中,开头和结尾要分开处理。又回到了无聊的老路上。..好烦啊,不相信一个段落分隔可以这么麻烦!突然想到,如果有文本节点这种东西,很容易删除复杂,正则化先到边缘,直接处理段落的所有节点是不够的。文本节点分离直接包裹,标签节点用于包裹内容。这样的话,直接处理DOM,比较方便。文本节点中的标签?这是一个笑话,是或不是。的确,文本节点只能放文本,但是如果我直接把标签放进去,它会自动转义,所以直接替换就好了。好了,解决方案终于有了,而且解决方案的逻辑这么简单,代码逻辑自然不会烦人。/***文本内容分割处理*@param{jQueryObject/HTMLElement/String}$content待处理的文本jQ对象或HTMLElement或其对应的选择器*/functionsplitConent($content){$content=$($content);$content.find(splitConfig.unitTag).each(函数(index,item){var$item=$(item),text=$.trim($item.text());if(!text)return;varnodes=$item[0].childNodes;$.each(nodes,function(i,node){switch(node.nodeType){case3://textnode//因为是文本节点,标签转义了,跟-up然后切换回来')+''+splitConfig.wrapTag+'>';break;case1://elementnodevarinnerHtml=node.innerHTML,start='',end='';//如果里面有直接标签,先去掉varstartResult=/^<\w+?>/.exec(innerHtml);if(startResult){start=startResult[0];innerHtml=innerHtml.substr(start.length);}varendResult=/<\/\w+?>$/.exec(innerHtml);if(endResult){end=endResult[0];innerHtml=innerHtml.substring(0,endResult.index);}//更新内部内容node.innerHTML=start+'<'+splitConfig.wrapTag+'>'+innerHtml.replace(splitConfig.splitReg,''+splitConfig.wrapTag+'>$&<'+splitConfig.wrapTag+'>')+''+splitConfig.wrapTag+'>'+end;break;default:break;}});//处理文本节点中转义的html标签$item[0].innerHTML=$item[0].innerHTML.replace(newRegExp('<'+splitConfig.wrapTag+'>;','g'),'<'+splitConfig.wrapTag+'>').replace(newRegExp(''+splitConfig.wrapTag+'>','g'),''+splitConfig.wrapTag+'>');$ite??m.find(splitConfig.wrapTag).addClass(splitConfig.wrapCls);});}在上面的代码中,在文本节点中替换转义的wrap标签似乎有点麻烦,但没有解决方案,ES5之前的JavaScript不支持正则look-behindassertions(即正则表达式中的“look-behind”),所以没有办法准确替换<事件处理完成了上面的文本获取和段落分离。接下来要做的就是当鼠标向上移动时获取文本并触发阅读,当鼠标移开时停止阅读。由于这两个原因,鼠标移动(只读一次)是使用mouseenter和mouseleave事件完成的。原因:不冒泡,不会触发重读父元素不重复触发,元素内移动不重复触发。/***在页面上写高亮样式*/functioncreateStyle(){if(document.getElementById('speak-light-style'))return;varstyle=document.createElement('style');style.id='speak-light-style';style.innerText='.'+splitConfig.hightlightCls+'{'+splitConfig.hightStyle+'}';document.getElementsByTagName('head')[0].appendChild(style);}//非-需要读取的文本标签用逗号分隔varspeakTags='a,p,span,h1,h2,h3,h4,h5,h6,img,input,button';$(document).on('mouseenter.speak-help',speakTags,function(e){var$target=$(e.target);//排除if($target.parents('.'+splitConfig.wrapCls).length||$target.find('.'+splitConfig.wrapCls).length){return;}//图片样式单独处理,其他样式统一处理if(e.target.nodeName.toLowerCase()==='img'){$target.css({border:'2pxsolid#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();});//朗读段落中的文字$(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();});注意段落等的语音处理为什么要分地方?因为段落是块级元素,当鼠标移入段落中的空白处时,如:段落前后的空白、第一行的缩进、最后一行剩余的空白等,不应触发阅读。如果不被挡住,这些区域就会被直接触发。通读全文,失去了段落文本内部分隔的意义。而且,不管用什么方法转换语音,都需要时间。大段内容可能会耗时较长,影响语音输出体验。文本合成语音上面我们直接使用了speakText(text)和stopSpeak()方法来触发语音的朗读和停止。让我们看看如何实现这两个功能。事实上,现代浏览器已经默认提供了以上功能:varspeechSU=newwindow.SpeechSynthesisUtterance();speechSU.text='Hello,world!';window.speechSynthesis.speak(speechSU);复制到浏览器控制台看看能不能听到声音?(需要Chrome33+、Firefox49+或IE-Edge)使用以下两个API:SpeechSynthesisUtterance用于语音合成lang:language获取和设置话语的语言。pitch:pitch获取和设置语音的音高。rate:SpeechSpeed获取并设置说话的速度。text:Text获取和设置在说出话语时将合成的文本。voice:Voice获取并设置将用于说出话语的声音。volume:Volume获取和设置话语的音量。onboundary:当说出的话语到达单词或句子边界时触发。onend:当话语说完时触发。onerror:当发生阻止话语被成功说出的错误时触发。onmark:当口头表达到达命名的SSML“标记”标签时触发。onpause:当话语中途暂停时触发。onresume:恢复暂停的话语时触发。onstart:开始说出话语时触发。SpeechSynthesis:用于说话paused:只读是否暂停如果SpeechSynthesis对象处于暂停状态,则返回true的布尔值。pending:只读是否处理一个布尔值,如果话语队列包含尚未说出的话语,则返回true。speaking:Readonly是否大声朗读一个布尔值,如果ut则返回trueterance目前正在被说出——即使SpeechSynthesis处于暂停状态。onvoiceschanged:语音变化时触发cancel():待发声队列的情况从发声队列中移除所有发声。getVoices():获取浏览器支持的语音包列表返回代表当前设备上所有可用语音的SpeechSynthesisVoice对象列表。pause():Pause将SpeechSynthesis对象置于暂停状态。resume():Restart将SpeechSynthesis对象置于非暂停状态,如果它已经暂停则恢复它。speak():读取合成语音,参数必须是SpeechSynthesisUtterance的一个实例,向话语队列中添加一个话语;当任何其他话语在它被说出之前排队时,它就会被说出。详细的api和使用说明参考:MDN-SpeechSynthesisUtteranceMDN-SpeechSynthesis那么以上两个方法可以写成:varspeaker=newwindow.SpeechSynthesisUtterance();varspeakTimer,stopTimer;//开始朗读函数speaker);},200);}//停止朗读函数stopSpeak(){clearTimeout(stopTimer);clearTimeout(speakTimer);stopTimer=setTimeout(function(){window.speechSynthesis.cancel();},20);}因为语音合成本来就是一个异步操作,所以上面的处理是在过程中进行的现代浏览器已经内置此功能,两种API接口的兼容性如下:功能ChromeEdgeFirefox(Gecko)InternetExplorerOperaSafari(WebKit)基本支持33(Yes)49(49)不支持?7如果你想兼容其他浏览器如果你需要一个完全兼容的解决方案,你可能需要服务器来完成它。根据给定的文字,可以返回相应的语音。百度语音http://yuyin.baidu.com/docs提供了这样的服务。cdswyda-网页文本阅读的实现-githubcdswyda-网页文本阅读的实现-demo