<font id="zqva1"></font>
<rt id="zqva1"></rt>
  • <tt id="zqva1"></tt>
    <cite id="zqva1"></cite>

    <cite id="zqva1"><noscript id="zqva1"></noscript></cite>
      <rp id="zqva1"><meter id="zqva1"></meter></rp>

        <cite id="zqva1"></cite>
          <b id="zqva1"></b>
          <rp id="zqva1"></rp>
          <cite id="zqva1"></cite>

          <rt id="zqva1"></rt>

        1. <rp id="zqva1"></rp>

          網頁中文本朗讀功能開發實現分享

          時間:?2017-12-11閱讀:?824標簽:?網頁
          本文由 依韻 ,于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'
          };

          最開始想的就是直接按照正文中的分隔標點符號進行分隔就好了呀。

          想法如下:

          1. 獲取段落全部文本
          2. 使用 split(分隔正則表達式) 方法將正文按照標點符號分隔成小段
          3. 每個小段用標簽包裹放回去即可

          然而理想很豐滿,現實很骨感。

          兩個大坑如下:

          1. split 方法進行分隔,分隔后分隔字符就丟了,也就是說把原文的一些標點符號給弄丟了。
          2. 如果段落內還存在其他標簽,而這個標簽內部也正好存在待分隔的標點符號,那包裹分段標簽時直接破換了原標簽的完整性。

          關于第一個問題,丟失標點的符號,考慮過逐個標點來進行和替換 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('&lt;' + splitConfig.wrapTag + '&gt;', 'g'), '<' + splitConfig.wrapTag + '>')
                      .replace(new RegExp('&lt;/' + splitConfig.wrapTag + '&gt;', 'g'), '</' + splitConfig.wrapTag + '>');
                  $item.find(splitConfig.wrapTag).addClass(splitConfig.wrapCls);
              });
          }

          上面代碼中最后對文本節點中被轉義的包裹標簽替換似乎有點麻煩,但是沒辦法,ES5之前JavaScript并不支持正則的后行斷言(也就是正則表達式中“后顧”)。所以沒辦法對包裹標簽前后的 &lt; 和 &gt; 進行精準替換,只能連同標簽名一起替換。


          事件處理

          在上面完成了文本獲取和段落分隔,下面要做的就是鼠標移動上去時獲取文本觸發朗讀即可,移開時停止朗讀即可。

          鼠標移動,只讀一次,基于這兩點原因,使用 mouseenter 和 mouseleave 事件來完成。

          原因:

          1. 不冒泡,不會觸發父元素的再次朗讀
          2. 不重復觸發,一個元素內移動時不會重復觸發。
          /**
           * 在頁面上寫入高亮樣式
           */
          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 用于語音合成

            • 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 : 單詞或句子邊界觸發,即分隔處觸發 Fired when the spoken utterance reaches a word or sentence boundary.
            • onend : 結束時觸發 Fired when the utterance has finished being spoken.
            • onerror : 錯誤時觸發 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 : 暫停時觸發 Fired when the utterance is paused part way through.
            • onresume : 重新播放時觸發 Fired when a paused utterance is resumed.
            • onstart : 開始時觸發 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 : 聲音變化時觸發
            • cancel() : 情況待朗讀隊列 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() : 讀合成的語音,參數必須為SpeechSynthesisUtterance的實例 Adds an utterance to the utterance queue; it will be spoken when any other utterances queued before it have been spoken.

          詳細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接口兼容性如下:

          FeatureChromeEdgeFirefox (Gecko)Internet ExplorerOperaSafari
          (WebKit) Basicsupport 33(Yes)49 (49)No support?7

          如果要兼容其他瀏覽器或者需要一種完美兼容的解決方案,可能就需要服務端完成了,根據給定文本,返回響應語音即可,百度語音 http://yuyin.baidu.com/docs就提供這樣的服務。

          站長推薦

          1.阿里云: 本站目前使用的是阿里云主機,安全/可靠/穩定。點擊領取2000元代金券、了解最新阿里云產品的各種優惠活動點擊進入

          2.騰訊云: 提供云服務器、云數據庫、云存儲、視頻與CDN、域名等服務。騰訊云各類產品的最新活動,優惠券領取點擊進入

          3.廣告聯盟: 整理了目前主流的廣告聯盟平臺,如果你有流量,可以作為參考選擇適合你的平臺點擊進入

          鏈接: http://www.modern-decoration.com.cn/article/detial/239

          什么是DOCTYPE聲明?對網頁起何作用?

          要建立符合標準的網頁,DOCTYPE聲明是必不可少的關鍵組成部分。那么什么是DOCTYPE聲明?對網頁起何作用?DOCTYPE是document type(文檔類型)的簡寫;!DOCTYPE聲明是一種指令

          javascript如何獲取網頁的標題(title)?

          網頁的標題(title),一般是由HTML文件的<title>標簽決定的。如果想要獲取網頁的標題(title),其實就是獲取<title>標簽中的內容。下面本篇文章就來給大家介紹一下獲取方法,希望對大家有所幫助。

          網頁tab鍵的實現

          前端常用tab鍵的實現,用到的原理是當點擊一個元素時,通過javascript操作css的display屬性,達到控制另一個元素的顯示(display: block)與不顯示(display: none),需要注意的是,由于使用的是onclick事件

          如何給網頁劃分結構?

          學習前端第一步:劃分網頁結構,網頁的結構的劃分應該遵循哪些原則,如何去劃分網頁的結構呢?對于一個前端初學者,第一步就是要學會如何劃分一個網頁的結構。當設計師給到你一張設計圖,你需要根據這張圖做出一個符合標準的頁面

          網頁屏蔽(鎖左、右鍵)的非JS方法

          但是這種屏蔽方法的破解方法也是眾所周知的。那就是連續單擊鼠標左鍵和右鍵便又可以看到右鍵功能表了。但是,我見過一種很好的屏蔽右鍵的方法。它的原理和上面所說的不同。它并不是用JS來編寫的腳本,而是利用定義網頁屬性來起到限制的作用

          網頁設計十大流行趨勢

          最近看到越來越多的網絡設計嘗試個性化的風格,其中比較突出的一點是個性化字體的增多:用自己獨特設計的字體代替標準印刷體,讓設計更加獨特,配色改變反應了人們審美需求的改變。同色系網站設計將成為主流

          靜態和動態網頁的區別?

          在靜態web程序中,客戶端使用web瀏覽器經過網絡連接到服務器上,使用HTTP協議發起一個請求(Request),告訴服務區我現在需要得到哪個頁面,所有的請求交給web服務器

          FreeMarker網頁靜態化

          網頁靜態化解決方案在實際開發中運用比較多,例如新聞網站,門戶網站中的新聞頻道或者是文章類的頻道。網頁靜態化技術和緩存技術的共同點都是為了減輕數據庫的訪問壓力,但是具體的應用場景不同

          使用 CSS 追蹤用戶

          除了使用 JS 追蹤用戶,現在有人提出了還可以使用 CSS 進行網頁追蹤和分析,譯者認為,這種方式更為 優雅,更為 簡潔,且 不好屏蔽,值得嘗試一波

          h5網頁水印SDK的實現代碼示例

          在網站瀏覽中,常常需要網頁水印,以便防止用戶截圖或錄屏暴露敏感信息后,追蹤用戶來源。如我們常用的釘釘軟件,聊天背景就會有你的名字。那么如何實現網頁水印效果呢?

          內容以共享、參考、研究為目的,不存在任何商業目的。其版權屬原作者所有,如有侵權或違規,請與小編聯系!情況屬實本人將予以刪除!

          文章投稿關于web前端網站點搜索站長推薦網站地圖站長QQ:522607023

          小程序專欄: 土味情話心理測試腦筋急轉彎幽默笑話段子句子語錄成語大全運營推廣

          国产精品高清视频免费 - 视频 - 在线观看 - 影视资讯 - 唯爱网