天天看點

探索在原生網頁中使用自定義資料屬性

先說說我為什麼有這種“奇怪”的想法。

它基于這樣一個場景:我最近閑來無事完善了一個小demo:音樂播放器。在裡面有一個功能 —— 點選清單某一項彈出音樂播放彈框。我原先一直是“為每一項單獨加一個click事件監聽”。這很糟糕!

<div id="videob"></div>

//js代碼
videob.onclick=function(){
  let video=document.createElement('video')
  video.setAttribute('src','xxx')
  video.setAttribute('controls','controls')
  video.style.cssText="width:90%;height:300px"
  document.querySelector('body').appendChild(video)
}      

後來我優化時改用了 事件代理 ,不得不承認它非常優秀,但是我不得不改變結構,使得js中出現了一堆“MP4資料”(一個數組),在觸發事件時判斷目前是哪一個,然後從數組中拿到指派給 ​

​audio​

​​ 的 ​

​src​

​ 。

事實上,在開發 Web 應用時,經常會用 JavaScript 擷取文檔之外的資訊

幸運的是,HTML5引入了自定義資料屬性:所有的自定義資料屬性都以 data-字首開頭,HTML5 文檔的驗證器會在驗證時忽略它。開發人員可以在任意元素中加入自定義資料屬性,屬性值可以是照片的中繼資料、經緯度坐标或者彈出視窗的尺寸。

最棒的是,幾乎在所有浏覽器中,你都能夠使用自定義資料屬性,因為我們可以輕易地使用 JavaScript 來擷取它們。

為什麼設定onclick不好

在開始操作之前,我們必須先“重複”一個概念:0級DOM事件 。通俗來說就是各種on字首的事件名稱(比如:onclick、onmouseon、onkeydown),他們可以存在于 ​

​<script>​

​ 或者HTML标簽中,且沒有事件捕獲和冒泡機制!

有這樣一個場景:Web 應用的開發人員尤其是初學者常常需要依賴于彈出視窗,以便向使用者顯示線上幫助資訊、附加選項或者其他重要的使用者界面功能。

我們經常能在一個頁面中看到如下代碼:

<a href="#" onclick="window.open('xxx',null,'width=300,height=300');">go to xxx</a>      

就是這段沒怎麼起眼的代碼存在着非常緻命的問題:

  1. 連結的目标位址沒有設定!如果 JavaScript 被禁用了,那麼連結将無法引導使用者進入相應頁面。這很一個非常嚴重,以至于我們需要立即解決。我建議開發者永遠不要省略 href屬性、任何情況下都不要為 href 屬性賦“#”。
  2. 注意保持行為與内容分離,正如用連結樣式表保持樣式資訊分離一樣。開始的時候,使用 onclick 會帶來便利,但是想象一下頁面上有 50 個連結的情況,那時你會看到 onclick 方法失控的場面。你将隻能一遍又一遍地編寫重複的 JavaScript 代碼。如果是通過伺服器端代碼來生成浏覽器端代碼,那麼你就是在增加 JavaScript 事件。

為了解決上面的問題,我們為每個a連結設定了href,而且增加了不同的class!以增加“相同類型元素”的可識别性。

<a href="xxx" class="pop">go to xxx</a>

//js代碼
var link=document.querySelector('a.pop')
link.onclick=function(e){
  e.preventDefault();
  window.open(this.getAttribute('href'));
}      

我們還沒有為​

​open​

​設定長寬!

現在,我們又回到了和文章開頭一樣的問題!

使用自定義資料屬性

當建立應用了 JavaScript 的 Web 應用時,剛剛提到的情況比較常見。如你所見,在代碼中存儲視窗的期望高度和寬度是可取的,但是 onclick 方法有諸多弊端。我們可以改換在元素上嵌入屬性的方式加以實作。現在要做的是将連結改造成下面這種形式:

<a href="xxx" data-width="600" data-height="400" class="pop">go to xxx</a>      
var link=document.querySelector('a.pop')
link.onclick=function(e){
  e.preventDefault();
  window.open(this.getAttribute('href'),null,"width="+this.getAttribute('data-width')+",height="+this.getAttribute('data-height'));
}      

好吧,這個例子似乎沒有什麼說服力。

但是為了讓你能夠真正認識到“問題的嚴重性”,我将帶你來優化文章開頭的示例:

<div id="mxc">
  <div class="videob" data-uri="./img/1.mp4"></div>
  <div class="videob" data-uri="./img/2.mp4"></div>
</div>      
mxc.addEventListener('click',function(e){
  let target=e.target || e.srcElement;
  let video=document.createElement('video')
  video.setAttribute('src',target.getAttribute('data-uri'))
  video.setAttribute('controls','controls')
  video.style.cssText="width:90%;height:300px;margin:0 auto"
  document.querySelector('body').appendChild(video)
},false)      

這樣才“真正”展現了“代理的價值”!

2020-09-09更新

發現一個沒怎麼注意的事情:父元素事件中的 ​​

​e.target​

​ 是直接指向了“目前觸發事件的子元素的最底層子元素”,其上面的“父元素”都将以parentNode的形式被展示出來(原型上)。

發現這個是因為想要換一種思路寫一個“類原生相冊”,我的思路是:開始時總的寬度是100vw,子元素(相冊圖檔)以flex排列;點選某一個圖檔預覽時動态給父元素添加一個類名,這個類的作用是:将父元素下(單張)圖檔的最大寬度設為100vw(子元素從始至終都不設寬)。然後在js中根據子元素的長度計算其“真正寬度”(​

​.style.width​

​)。并根據目前點選的是第幾個子元素計算應該transform偏移多少距離?

其核心代碼:

//計算目前點選元素是父元素中的第幾個子元素 == jQuery:index()
var child = document.getElementById(xxx);
var index = Array.prototype.indexOf.call(child.parentNode,child);      

其他用到“自定義資料屬性”最多的地方應該就是【圖檔懶加載】裡面設定替換圖了吧:

<body>
<div class="imgs">
    <div>
        <img src="占位圖" data-src="真正圖檔" alt="圖檔說明">
    </div>
    ...
</div>
<script>
    window.onload=function () {
        var imgs=document.querySelectorAll('img');
        function offset(el){
            let top=el.offsetTop
            while(el.offsetParent){
                el=el.offsetParent
                top+=el.offsetTop   //scrollTop:設定或擷取位于對象最頂端和視窗中可見内容的最頂端之間的距離
            }
            return { top }
        }
        function lazyload(imgs) {
            var h=window.innerHeight;   //innerheight   傳回視窗的文檔顯示區的高度
            var s=document.documentElement.scrollTop || document.body.scrollTop;   //document.body.scrollTop 網頁被卷去的高
            for(var i=0;i<imgs.length;i++){
                if((h+s)>offset(imgs[i]).top){   //視窗文檔顯示去高度+向上滾動的高度,判斷是否到達了圖檔的位置,如果到了,才去加載,并在加載中 用一個動圖去“占位”
                    (function (i) {
                        setTimeout(function () {
                            var tmp=new Image();
                            tmp.src=imgs[i].getAttribute("data-src");   //   直接擷取
                            tmp.onload=function () {
                                imgs[i].src=imgs[i].getAttribute("data-src");   //   如果直接擷取不了(還沒加載完),就等加載完前面的再去擷取
                            }
                        },1300)
                    })(i)
                }
            }
        }
        lazyload(imgs);
        window.onscroll=function () {
            lazyload(imgs);
        }
    }
</script>
</body>      

繼續閱讀