本文轉載自【微信公眾號:趣談前端,ID:beautifulFront】經微信公眾號授權轉載,如需轉載與原文作者聯繫
作為一名前端愛好者, 筆者利用空餘時間研究了幾個國外網站的源碼,發現不管是庫,還是業務代碼,都會用到了一些比較有意思的API,雖然平時在工作中部分接觸過,但是經過這次的研究,覺得很有必要總結一下,畢竟已經2020年了,是時候更新一下技術儲備了,本文主要通過實際案例來帶大家快速了解以下幾個知識點:
Observer 原生觀察者script標籤事件深入 - 移除script標籤後事件仍然能執行的原因Proxy/Reflect自定義事件fileReader APIFullscreen 網頁全屏URL API的使用Geolocation 地理位置API的使用Notifications 瀏覽器原生消息通知Battery Status 設備電量情況我會對部分API做一些比較有意思的案例,那麼開始我們的學習吧~
1. Observer API
Observer是瀏覽器自帶的觀察者,它主要提供了Intersection, Mutation, Resize, Performance這四類觀察者, 這裡筆者重點介紹Intersection Observer.
1.1 Intersection Observer
IntersectionObserver提供了一種異步觀察目標元素與其祖先元素交叉狀態的方法。當一個IntersectionObserver對象被創建時,其被配置為監聽根中一段給定比例的可見區域,並且無法更改其配置,所以一個給定的觀察者對象只能用來監聽可見區域的特定變化值;然而,我們可以在同一個觀察者對象中配置監聽多個目標元素。
說簡單點就是該api可以異步監聽目標元素在根元素裡的位置變動,並觸發響應事件.我們可以利用它來實現更為高效的圖片懶加載, 無限滾動以及內容埋點上報等.接下來我們通過一個例子來說明一下它的使用步驟.
// 1.定義觀察者及觀察回調const intersectionObserver = new IntersectionObserver((entries, observer) => {entries.forEach(entry => { console.log(entry) // ...一些操作 }); }, { root: document.querySelector('#root'), rootMargin: '0px', threshold: 0.5 })// 2. 定義要觀察的目標對象const target = document.querySelector(「.target」);intersectionObserver.observe(target);
複製代碼
以上代碼就實現了一個基本的Intersection Observer,雖然已有代碼中還體現不出什麼實質性功能. 接下來介紹一下代碼中使用到的參數的含義:
callback IntersectionObserver實例的第一個參數, 當目標元素與根元素通過閾值時就會觸發該回調.回調中第一個參數是被觀察對象列表,一旦被觀察對象發生突變就會被移入該列表, 列表中每一項都保留有觀察者的位置信息;第二個參數為observer,觀察者本身.如下圖控制臺列印:
其中rootBounds表示根元素的位置信息, boundingClientRect表示目標元素的位置信息,intersectionRect表示叉部分的位置信息, intersectionRatio表示目標元素的可見比例.配置屬性 IntersectionObserver實例的第二個參數,用來配置監聽屬性,具體有以下三個屬性:root 所監聽對象的具體祖先元素(element)。如果未傳入值或值為null,則默認使用頂級文檔的視窗。rootMargin 計算交叉時添加到根(root)邊界盒bounding box的矩形偏移量, 可以有效的縮小或擴大根的判定範圍從而滿足計算需要thresholds 一個包含閾值的列表, 按升序排列, 列表中的每個閾值都是監聽對象的交叉區域與邊界區域的比率。當監聽對象的任何閾值被越過時,都會生成一個通知(Notification)。如果構造器未傳入值, 則默認值為0。以上屬性介紹字面上可能很難理解,筆者花幾個草圖來讓大家有個直觀的認知:
當我們設置rootMargin為10px時,我們的root會增大影響範圍,但目標元素移動到淡紅色區域就會被監聽到,當然我們還可以設置rootMargin為負值來減少影響區域.其支持的值為百分比和px,如下:
rootMargin: '10px'rootMargin: '10%'rootMargin: '10px 0px 10px 10px'
複製代碼
thresholds可以如下圖理解:
由上圖所示,當我們設置閾值為[0.25, 0.5]時, 目標元素的25%和50%進入根元素的影響範圍時都會觸發回調.利用這個特性我們往往可以實現位差動畫,或者更根據目標元素的位置變化做不同的交互. 當然Intersection還提供了以下幾個方法來控制觀察對象:
disconnect() 使IntersectionObserver對象停止監聽工作takeRecords() 返回所有觀察目標的IntersectionObserverEntry對象數組unobserve() 使IntersectionObserver停止監聽特定目標元素了解了使用方法和api之後,我們來看看一個實際應用--實現圖片懶加載:
<img src="loading.gif" data-src="absolute.jpg"><img src="loading.gif" data-src="relative.jpg"><img src="loading.gif" data-src="fixed.jpg"><script>let observerImg = new IntersectionObserver((entries, observer) => {entries.forEach(entry => { // 替換為正式的圖片 entry.target.src = entry.target.dataset.src; // 停止監聽 observer.unobserve(entry.target); }); }, { root: documennt.getElementById('scrollView'), threshold: 0.3 });document.querySelectorAll('img').forEach(img => { observerImg.observe(img) });</script>
複製代碼
以上代碼就實現了一個圖片懶加載功能, 當圖片的30%進入根元素時才加載真實的圖片,這又讓我想起了之前在某條做廣告埋點上報時使用react-lazyload的畫面.大家還可以利用它實現無限滾動, H5視差動畫等有意思的交互場景.
1.2 Mutation Observer和Resize Observer
Mutation Observer主要用來實現dom變動時的監聽,同樣也是異步觸發,對監聽性能非常友好. Resize Observer主要用來監聽元素大小的變化,相比於每次窗口變動都觸發的window.resize事件, Resize Observer有更好的性能和對dom有更細粒度的控制,它只會在繪製前或布局後觸發調用. 以上兩個api的使用和Intersection使用非常類似,官方資料也寫得很全,大家可以好好研究一下.
2. 移除script標籤後事件仍然能執行的原因
這個問題主要是之前有朋友問過我,當時的想法就是簡單的認為script內的代碼執行完之後以及與dom綁定了,存放在了瀏覽器內存中,最近查了很多資料發現有一個有點意思的解釋,放出來大家可以感受一下:
JavaScript解釋器在執行腳本時,是按塊來執行的,也就是說瀏覽器在解析HTML文檔流時,如果遇到一個script標籤,javascript解釋器會等待這個代碼塊都加載完了,才進行預編譯,然後才執行。所以,當開始執行這個代碼塊的代碼時,這個代碼段已經被解析完了。這時再從DOM中刪去也就不影響代碼的執行了。
3. Proxy/Reflect
Proxy/Reflect雖然是es6的api,出現也已經有幾年了,但是在項目中用的還是比較少,如果是做底層架構方面的工作,還是建議大家多去使用,畢竟vue/react這種框架源碼把這些api玩的如火純青,還是很有必要掌握一下的。
其實我們認真看mdn的介紹或者阮一峰老師的文章,還是很好理解這些api的用法的,接下來我們詳細介紹一下這兩個api以及應用場景.
3.1 Proxy
Proxy 可以理解成,在目標對象之前架設一層「攔截」,外界對該對象的訪問,都必須先通過這層攔截,因此提供了一種機制,可以對外界的訪問進行過濾和改寫。Proxy在很多場景中都會和Reflect一起使用. 用法也很簡單,我們看看Proxy的基本用法:
const obj = {name: '徐小夕', age: '120' } const proxy = new Proxy(obj, { get(target, propKey, receiver) { console.log('get:' + propKey) return Reflect.get(target, propKey, receiver) }, set(target, propKey, value, receiver) { console.log('set:' + propKey) return Reflect.set(target, propKey, value, receiver) } }) console.log(proxy.name) // get:name 徐小夕 proxy.work = 'frontend' // set:work frontend
複製代碼
以上代碼攔截了obj對象,並重新定義了讀寫(get/set)方法,這樣我們就可以在訪問對象時進行額外的操作了.
Proxy還有apply(攔截 Proxy 實例作為函數調用的操作)和construct(攔截 Proxy 實例作為構造函數調用的操作)等屬性可以使用,我們可以在對象操作的不同階段進行攔截,這裡我就不一一樣舉例了.接下來看看Proxy的實際應用場景.
實現數組讀取負數的索引我們一般操作數組大多數都是正向操作的,不能通過指定負數來逆向查找數組,如下圖:
我們不能通過arr[-1]來拿到數組的尾部元素(字符串同理),這個時候我們就可以用Proxy來實現這一功能,這是我們的結構有點像環狀:
這種實現的好處是如果我們想訪問數組的最後一個元素時,我們不需要先拿到長度,再通過索引訪問了:
// 原始寫法arr[arr.length -1]// 通過proxy改造後寫法arr[-1]
複製代碼
實現代碼如下:
function createArray(...elements) {let handler = { get(target, propKey, receiver) { let index = Number(propKey); if (index < 0) { propKey = String(target.length + index); } return Reflect.get(target, propKey, receiver); } }; let target = []; target.push(...elements); return new Proxy(target, handler);}
複製代碼
我們可以發現以上代碼使用proxy來代理數組的讀取操作,在內部封裝了支持負值查找的功能,當然我們也可以不用proxy來實現同樣的功能,這裡實現參考阮一峰老師的實現.
利用proxy實現更優雅的校驗器一般我們在做表單校驗的時候會寫一些if else或者switch判斷來實現對不同屬性值的校驗,同樣我們也可以用proxy來優雅的實現它,代碼如下:
const formData = {name: 'xuxi', age: 120, label: ['react', 'vue', 'node', 'javascript'] } // 校驗器 const validators = { name(v) { // 檢驗name是否為字符串並且長度是否大於3 return typeof v === 'string' && v.length > 3 }, age(v) { // 檢驗age是否為數值 return typeof v === 'number' }, label(v) { // 檢驗label是否為數組並且長度是否大於0 return Array.isArray(v) && v.length > 0 } } // 代理校驗對象 function proxyValidator(target, validator) { return new Proxy(target, { set(target, propKey, value, receiver) { if(target.hasOwnProperty(propKey)) { let valid = validator[propKey] if(!!valid(value)) { return Reflect.set(target, propKey, value, receiver) }else { // 一些其他錯誤業務... throw Error(`值驗證錯誤${propKey}:${value}`) } } } }) }
複製代碼
有了以上實現模式,我們就可以實現對表單中某個值進行設置時進行校驗了,用法如下:
let formObj = proxyValidator(formData, validators)formObj.name = 333; // Uncaught Error: 值驗證錯誤name:fformObj.age = 'ddd' // Uncaught Error: 值驗證錯誤age:f
複製代碼
以上代碼中當設置了不合法的值時,控制臺將會剖出錯誤,如果在實際業務中,我們可以給用戶做出適當的提醒.
實現請求攔截和錯誤上報實現數據過濾以上幾點筆者在之前的文章中也寫過,所以這裡不在詳細介紹了.大家也可以根據實際情況自己實現更加靈活的攔截操作.當然Proxy提供的API遠遠不止這幾個,我們可以在MDN或者其他渠道了解更多高級用法.
3.2 Reflect
Reflect對象與Proxy對象一樣,也是 ES6 為了操作對象而提供的新 API,更多的應用場景是配合proxy一起使用,在上文中已經用到了.可以將Object對象的一些明顯屬於語言內部的方法放到Reflect對象上,並修改某些Object方法的返回結果. Reflect對象的方法與Proxy對象的方法一一對應,只要是Proxy對象的方法,就能在Reflect對象上找到對應的方法。
4. 自定義事件
CustomEvent API是個非常有意思的api, 而且非常實用, 更重要的是學起來非常簡單,而且被大部分現代瀏覽器支持.我們可以讓任意dom元素監聽和觸發自定義事件,只需要如下操作:
// 添加一個適當的事件監聽器dom1.addEventListener("boom", function(e) { something(e.detail.num) })// 創建並分發事件var event = new CustomEvent("boom", {"detail":{"num":10}})dom1.dispatchEvent(event)
複製代碼
我們來看看CustomEvent的參數介紹:
type 事件的類型名稱,如上面代碼中的'boom'CustomEventInit 提供了事件的配置信息,具體有以下幾個屬性bubbles 一個布爾值,表明該事件是否會冒泡cancelable 一個布爾值,表明該事件是否可以被取消detail 當事件初始化時傳遞的數據我們可以通過dispatchEvent來觸發自定義事件.其實他的用途有很多,比如創建觀察者模式, 實現數據雙向綁定, 亦或者在遊戲開發中實現打怪掉血,比如下面的例子:
筆者上面畫了一個打boss的草圖, 現在的場景是兩個玩家一起打boss, 我們可以在玩家發動攻擊的時候觸發dispatch掉血的自定義事件, boss監聽到事件後將血量自動扣除, 至於不同角色的傷害值,我們可以存放在detail中,然後通過策略模式去分發傷害.筆者曾今在學校開發的H5遊戲時就大量採用類似的模式,還是非常有意思的.
5. fileReader
File API使得我們在瀏覽器端可以訪問文件的數據,比如預覽文件,獲取文件信息(比如文件名,文件內容,文件大小等), 並且可以在前端實現文件下載(可以藉助canvas和 window.URL.revokeObjectURL的一些能力).當然我們還可以實現拖拽上傳文件這樣高用戶體驗的操作.接下來我們來看看幾個實際例子.
顯示縮略圖
function previewFiles(files, previewBox) {for (var i = 0; i < files.length; i++) { var file = files[i]; var imageType = /^image\//; if (!imageType.test(file.type)) { continue; } var img = document.createElement("img"); previewBox.appendChild(img); // 假設"preview"就是用來顯示內容的div var reader = new FileReader(); reader.onload = (function(imgEl) { return function(e) { imgEl.src = e.target.result; }; })(img); reader.readAsDataURL(file); } }
複製代碼
以上代碼可以在reviewBox容器中顯示已上傳好的圖片,當然我們還可以基於此來擴展,利用canvas將圖片畫到canvas上,然後進行圖片壓縮,最後再把壓縮後的圖片上傳到伺服器.這種方式其實目前很多工具型網站都在用,比如在線圖片處理網站,提供的批量壓縮圖片,批處理水印等功能,套路都差不多,感興趣的朋友可以嘗試研究一下.
封裝文件上傳組件這塊筆者之前也寫過詳細的文章,這裡就不一一舉例了, 文章地址:
3分鐘教你用原生js實現具有進度監聽的文件上傳預覽組件記一次老項目中的跨頁面通信問題和前端實現文件下載功能6. Fullscreen
全屏API主要是讓網頁能在電腦屏幕中全屏顯示,它允許我們打開或者退出全屏模式,以便我們根據需要進行對應的操作,比如我們常用的網頁圖形編輯器或者富文本編輯器, 為了讓用戶專心於內容設計,我們往往提供切換全屏的功能供用戶使用.由於全屏API比較簡單,這裡我們直接上代碼:
// 開啟全屏document.documentElement.requestFullscreen();// 退出全屏document.exitFullscreen();
複製代碼
以上代碼的document.documentElement也可以換成任何一個你想讓其全屏的元素.默認情況下我們還可以通過document.fullscreenElement來判斷當前頁面是否處於全屏狀態,來實現屏幕切換的效果.如果是react開發者,我們也可以將其封裝成一個自定義hooks來實現與業務相關的全屏切換功能.
7. URL
URL API是URL標準的組成部分,URL標準定義了構成有效統一資源定位符的內容以及訪問和操作URL的API。
我們利用URL組件可以做很多有意思的事情.比如我們有個需求需要提取url的參數傳給後臺,傳統的做法是自己寫一個方法來解析url字符串,手動返回一個query對象.但是利用URL對象,我們可以很方便的拿到url參數,如下:
let addr = new URL(window.location.href)let host = addr.host // 獲取主機地址let path = addr.pathname // 獲取路徑名let user = addr.searchParams.get("user") // 獲取參數為user對應的值
複製代碼
以上代碼可知,我們如果將url轉化為URL對象,那麼我們就可以很方便的通過searchParams提供的api來拿到url參數而無需自己再寫一個方法了.
另一方面,如果網站安全性比較高,我們還可以對參數進行自然數排序然後再加密上傳給後端.具體代碼如下:
function sortMD5WithParameters() {let url = new URL(document.location.href); url.searchParams.sort(); let keys = url.searchParams.keys(); let params = {} for (let key of keys) { let val = url.searchParams.get(key); params[key] = val }; // ...md5加密 return MD5(params) }
複製代碼
8. Geolocation
地理位置 API 通過 navigator.geolocation 提供, 這個瀏覽器API也比較實用, 我們在網站中可以用此方式確定用戶的位置信息,從而讓網站有不同的展現,增強用戶體驗.
舉幾個有意思的例子可以讓大家感受一下:
根據不同地區,網站展示不同的主題:
根據用戶所在地區,展示不同推薦內容 這一點電商網站或者內容網站用的比較多, 比如用戶在新疆,則給他推薦瓜果類廣告, 在北京,則給他推薦旅遊景點類廣告等,雖然實際應用中往往會更複雜,但是也是一種思路.其實應用遠遠不止如此,程式設計師可以發揮想像來實現更有意思的事情,讓自己的網站更智能.接下來筆者就基於promise寫一段獲取用戶位置的代碼:
function getUserLocation() {return new Promise((resolve, reject) => { if (!navigator.geolocation) { reject() } else { navigator.geolocation.getCurrentPosition(success, error); } function success(position) { const latitude = position.coords.latitude; const longitude = position.coords.longitude; resolve({latitude, longitude}) } function error() { reject() } }) }
複製代碼
使用方式和結果如下圖所示:
我們基於獲取到的經緯度調用第三方api(比如百度,高德)就可以獲取用戶所在為精確位置信息了.
9. Notifications
Notifications API 允許網頁或應用程式在系統級別發送在頁面外部顯示的通知;這樣即使應用程式空閒或在後臺,Web應用程式也會向用戶發送信息。
我們舉個實際的例子,比如我們網站內容有更新,通知用戶,效果如下:
相關代碼如下:
Notification.requestPermission( function(status) {console.log(status); // 僅當值為 "granted" 時顯示通知 var n = new Notification("趣談前端", {body: "從零搭建一個CMS全棧項目"}); // 顯示通知});
複製代碼
當然瀏覽器的Notification還給我們提供了4個事件觸發api方便我們做更全面的控制:
show 當通知被顯示給用戶時觸發click 當用戶點擊通知時觸發close 當通知被關閉時觸發error 當通知發生錯誤的時候觸發有了這樣的事件監聽,我們就可以控制當用戶點擊通知時, 跳轉到對應的頁面或者執行相關的業務邏輯.如下代碼所示:
Notification.requestPermission( function(status) {console.log(status); // 僅當值為 "granted" 時顯示通知 var n = new Notification("趣談前端", {body: "從零搭建一個CMS全棧項目"}); // 顯示通知 n.onshow = function () { // 消息顯示時執行的邏輯 console.log('show') } n.click = function () { // 消息被點擊時執行的邏輯 history.push('/detail/1232432') } n.close = function () { // 消息關閉時執行的邏輯 console.log('close') }});
複製代碼
當然我們在使用前需要獲取權限,方式也很簡單,大家可以在mdn上學習了解.
10. Battery Status
Battery Status API提供了有關系統充電級別的信息並提供了通過電池等級或者充電狀態的改變提醒用戶的事件。這個可以在設備電量低的時候調整應用的資源使用狀態,或者在電池用盡前保存應用中的修改以防數據丟失。
之前的版本中Battery Status API提供了幾個事件監聽函數來監聽電量的變化以及監聽設備是否充電,但是筆者看文檔時這些api都已經廢棄,如下:
chargingchange 監聽設別是否充電levelchange 監聽電量充電等級chargingtimechange 充電時間變化dischargingtimechange 放電時間變化雖然以上幾個看似有用的api已經被棄用,但是筆者親測谷歌還是可以正常使用的,但是為了讓自己代碼更可靠,我們可以用其他方式代替,比如用定時器定期去檢測電量情況,進而對用戶做出不同的提醒.
接下來我們看看基本的用法:
navigator.getBattery().then(function(battery) {console.log("是否在充電? " + (battery.charging ? "是" : "否")); console.log("電量等級: " + battery.level * 100 + "%"); console.log("充電時間: " + battery.chargingTime + " s"); console.log("放電時間: " + battery.dischargingTime + "s");});
複製代碼
我們可以通過getBattery拿到設備電池信息,這個api非常有用,比如我們可以在用戶電量不足時禁用網站動畫或者停用一些耗時任務,亦或者是對用戶做適當的提醒,改變網站顏色等,對於webapp中播放視頻或者直播時,我們也可以用css畫一個電量條,當電量告急時提醒用戶.作為一個優秀的網站體驗師,這一塊還是不容忽視的.