本文由 依韵 ,于2017年12月09日 Saturday 14:52发布。
本文地址:https://blog.cdswyda.com/post/2017120914
前几天完成了一个需求,在网页中完成鼠标指向哪里,就用语音读出所指的文本。如果是按钮、链接、文本输入框,则还还要给出是什么的提醒。同时针对大段的文本,不能整段的去读,要按照标点符号进行断句处理。
重点当然就是先获取到当前标签上的文本,再把文本转化成语音即可。
这个很简单了,只用根据当前是什么标签,给出提示即可。
// 标签朗读文本
var tagTextConfig = {
'a': '链接',
'input[text]': '文本输入框',
'input[password]': '密码输入框',
'button': '按钮',
'img': '图片'
};
还有需要朗读的标签,继续再添加即可。
然后根据标签,返回前缀文本即可。
/**
* 获取标签朗读文本
* @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;
}
// 标签的功能提醒和作用应该有间隔,因此在最后加入一个空格
return (tagTextConfig[tagName] || '') + ' ';
}
获取完整的朗读文本就更简单了,先取标签的功能提醒,再取标签的文本即可。
文本内容优先取 title 其次 alt 最后 innerText。
/**
* 获取完整朗读文本
* @param {HTMLElement} el 要处理的HTMLElement
* @returns {String} 朗读文本
*/
function getText(el) {
if (!el) return '';
return getTagText(el) + (el.title || el.alt || el.innerText || '');
}
这样就可以获取到一个标签的功能提醒和内容的全部带朗读文本了。
接下来要处理的就是正文分隔了,在这个过程中,踩了不少坑,走了不少弯路,好好记录一下。
首先准备了正文分隔的配置:
// 正文拆分配置
var splitConfig = {
// 内容分段标签名称
unitTag: 'p',
// 正文中分隔正则表达式
splitReg: /[,;,;。]/g,
// 包裹标签名
wrapTag: 'label',
// 包裹标签类名
wrapCls: 'speak-lable',
// 高亮样式名和样式
hightlightCls: 'speak-help-hightlight',
hightStyle: 'background: #000!important; color: #fff!important'
};
最开始想的就是直接按照正文中的分隔标点符号进行分隔就好了呀。
想法如下:
然而理想很丰满,现实很骨感。
两个大坑如下:
关于第一个问题,丢失标点的符号,考虑过逐个标点来进行和替换 split 分隔方法为逐个字符循环来做。
前者问题是原本一次完成的工作分成了多次,效率太低。第二种感觉效率更低了,分隔本来是很稀疏的,但是却要变成逐个字符出判断处理,更关键的是,分隔标点的位置要插入包裹标签,会导致字符串长度变化,还要处理下标索引。代码是机器跑的,或许不会觉得烦,但是我真的觉得好烦。如果这么干,或许以后哪个AI或者同事看到这样的代码,说不定会说“这真是个傻xxxx”。
第二个问题想过很多办法来补救,如先使用正则匹配捕获内容中成对的标签,对标签内部的分隔先处理一遍,然后再处理整个的。
想不明白问题二的,可参考一下待分隔的段落:
<p>这是一段测试文本,这里有个链接。<a>您好,可以点击此处进行跳转</a>还有其他内容其他内容容其他内容容其他内容,容其他内容。</p>
如先使用/<((\w+?)>)(.+?)<\/\2(?=>)/g 正则,依次捕获段落内被标签包裹的内容,对标签内部的内容先处理。
但是问题又来了,这么处理的都是字符串,在js中都是基本类型,这些操作进行的时候都是在复制的基础上进行的,要修改到原字符串里去,还得记录下原本的开始结束位置,再将新的插进去。繁,还是繁,但是已经比之前逐个字符去遍历的好,正则捕获中本来就有了匹配的索引,直接用即可,还能接受。
但是这只是处理了段落内部标签的问题,段落内肯定还有很多文本是没有处理呢,怎么办?
正则匹配到了只是段落内标签的结果啊,外面的没有啊。哦,对,有匹配到的索引,上次匹配到的位置加上上次处理的长度,就是一段直接文本的开始。下一次匹配到的索引-1就是这段直接文本的结束。这只是匹配过程中的,还有首尾要单独处理。又回到烦的老路上去了。。。
这么烦,一个段落分隔能这么繁琐,我不信!
突然想到了,有文本节点这么个东西,删繁就简嘛,正则先到边上去,直接处理段落的所有节点不就行了。
文本节点则分隔直接包裹,标签节点则对内容进行包裹,这种情况下处理的直接是dom,更省事。
文本节点里放标签?这是在开玩笑么,是也不是。文本节点里确实只能放文本,但是我把标签直接放进去,它会自动转义,那最后再替换出来不就行了。
好了,方案终于有了,而且这个方案逻辑多简单,代码逻辑自然也不会烦。
/**
* 正文内容分段处理
* @param {jqueryObject/HTMLElement/String} $content 要处理的正文jQ对象或HTMLElement或其对应选择器
*/
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 节点
// 由于是文本节点,标签被转义了,后续再转回来
node.data = '<' + splitConfig.wrapTag + '>' +
node.data.replace(splitConfig.splitReg, '</' + splitConfig.wrapTag + '>$&<' + splitConfig.wrapTag + '>') +
'</' + splitConfig.wrapTag + '>';
break;
case 1:
// 元素节点
var innerHtml = node.innerHTML,
start = '',
end = '';
// 如果内部还有直接标签,先去掉
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);
}
// 更新内部内容
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(new RegExp('<' + splitConfig.wrapTag + '>', 'g'), '<' + splitConfig.wrapTag + '>')
.replace(new RegExp('</' + splitConfig.wrapTag + '>', 'g'), '</' + splitConfig.wrapTag + '>');
$item.find(splitConfig.wrapTag).addClass(splitConfig.wrapCls);
});
}
上面代码中最后对文本节点中被转义的包裹标签替换似乎有点麻烦,但是没办法,ES5之前JavaScript并不支持正则的后行断言(也就是正则表达式中“后顾”)。所以没办法对包裹标签前后的 < 和 > 进行精准替换,只能连同标签名一起替换。
在上面完成了文本获取和段落分隔,下面要做的就是鼠标移动上去时获取文本触发朗读即可,移开时停止朗读即可。
鼠标移动,只读一次,基于这两点原因,使用 mouseenter 和 mouseleave 事件来完成。
原因:
/**
* 在页面上写入高亮样式
*/
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);
}
// 非正文需要朗读的标签 逗号分隔
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);
// 排除段落内的
if ($target.parents('.' + splitConfig.wrapCls).length || $target.find('.' + splitConfig.wrapCls).length) {
return;
}
// 图片样式单独处理 其他样式统一处理
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();
});
// 段落内文本朗读
$(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() 两个方法来触发语音的朗读和停止。
我们来看下如何实现这个两个功能。
其实现代浏览器默认已经提供了上面功能:
var speechSU = new window.SpeechSynthesisUtterance();
speechSU.text = '你好,世界!';
window.speechSynthesis.speak(speechSU);
复制到浏览器控制台看看能不能听到声音呢?(需要Chrome 33+、Firefox 49+ 或 IE-Edge)
利用一下两个api即可:
SpeechSynthesisUtterance 用于语音合成
SpeechSynthesis : 用于朗读
详细api和说明可参考:
那么上面的两个方法可以写为:
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);
}
因为语音合成本来是个异步的操作,因此在过程中进行以上处理。
现代浏览器已经内置了这个功能,两个API接口兼容性如下:
Feature | Chrome | Edge | Firefox (Gecko) | Internet Explorer | Opera | Safari |
---|---|---|---|---|---|---|
(WebKit) Basic | support 33 | (Yes) | 49 (49) | No support | ? | 7 |
如果要兼容其他浏览器或者需要一种完美兼容的解决方案,可能就需要服务端完成了,根据给定文本,返回响应语音即可,百度语音 http://yuyin.baidu.com/docs就提供这样的服务。
CSS以图换字的技术,很久都没人提起了。它是一种在h1标签内,使用图像替换文本元素的技术,使页面在设计和可访问性之间达到平衡。本文将详细介绍CSS以图换字的9种方法
在网页开发中,经常会遇到把一些通用内容的页面集中到一个页面中,需要使用这些页面只需要包含引入即可,这样有利于维护和修改,当通用页面修改时只需更改一个文件就可以了,不需要每个文件单独处理。
开发网站之前,你需要知道哪些事情呢?每个开发者的答案可能都不太相同,这里整理为6个方面:界面和用户体验、安全性、性能(Performance)、搜索引擎优化、技术(Technology)、解决bug
理解大型分布式网站你必须知道这些概念:1. I/O优化、2. Web前端调优、3.服务降级(自动优雅降级)、4.幂等性设计、5.失效转移、6.性能优化、7. 代码优化、8. 负载均衡、9.缓存等
每一个网页或者说是web页都有其固定的后缀名,不同的后缀名对应着不同的文件格式和不同的规则、协议、用法,最常见的web页的后缀名是.html和.htm,但这只是web页最基本的两种文件格式,今天我们来介绍一下web页的其它一些文件格式。
网页自动跳转,是指当用户访问某个网页时,被自动跳转到另一个网页中去。网页自动跳转的主要作用是,当域名变更后,或者网站里的一个或多个网页被删除后,可以使用这种方式将用户引导到其它正常的网页中去,从而留住用户。
锚链链接是一个非常重要的概念,在网页中增加恰当的锚链接,会让所在网页和所指向网页的重要程度有所提升,从而影响到关键词排名。锚链接对SEO的作用主要体现在以下几个方面
HTML几乎是平铺直叙的。CSS是一个伟大的进步,它清晰地区分了页面的结构和外观。在本教程中,您将了解在浏览器中看到的内容是如何实际呈现的,以及如何在必要时进行抓取。
VMware Workstation提供了两种虚拟机上网方式,一种bridge,一种NAT,bridge可以获得公网地址,而NAT只能是内网地址了。例1:在虚拟机内搭建http服务器,使用公网地址访问,例2: ssh端口映射
静态网页是标准的HTML文件,它的文件扩展名是.htm、.html,静态网页是网站建设的基础。从网站浏览者的角度来看,无论是动态网页还是静态网页,都可以展示基本的文字和图片信息,但从网站开发、管理、维护的角度来看就有很大的差别。
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!