第九期:前端九條啟發分享
作者一直忙別的事好久沒寫文章慚愧慚愧, 工作變動求靠譜初創公司推薦ღ( ´・ᴗ・` )比心
一、瀏覽器tab間通訊利器 BroadcastChannel
瀏覽器tab間通訊的場景雖然不多但我們也要會(就要就要), 我下面舉幾個使用場景大家品品:
- 下圖是一個常見的
列表
, 點選列表裡的詳情按鈕會跳到詳情頁
, 那麼也許我們在詳情頁修改了資料狀態
, 此時可能需要把修改後的狀態直接傳給列表頁
從而本地直接更新列表, 這樣就不用傳送新的api請求與後端互動了。 - 與上一個例子相同, 如果我從列表頁開啟了很多的
詳情頁
, 就會出現很多的tab看著讓人不舒服, 那麼我在列表頁
就可以提供一鍵關閉詳情頁tab的能力。
一起看看用法吧
我們以上述的第二個例子為例, 這裡是列表頁
程式碼
const bt = document.getElementById('bt');
const bc = new BroadcastChannel('test_channel');
bt.onclick = function () {
bc.postMessage({ close: true });
}
new BroadcastChannel('test_channe')
建立了一個名為test_channe
的頻道。- 當button被點選時觸發
bc.postMessage({ close: true });
對test_channe
頻道釋出訊息{ close: true }
。 BroadcastChannel
的優點之一就是他釋出的訊息可以是物件
, 但是如果透過localStorage
傳遞資訊的話就是字串的形式。
這裡是詳情頁面
程式碼
const bc = new BroadcastChannel('test_channel');
bc.onmessage = function (res) {
if(res.data.close){
window.close()
}
}
bc.onmessageerror = function () {
// 錯誤處理
}
new BroadcastChannel('test_channel')
同樣需要監聽test_channel
頻道。bc.onmessage
是收聽頻道的意思。
接受到的res資料如下:
動效如圖:
瀏覽器的實現各不相同
火狐與 safari 360極速等瀏覽器無法直接使用本地的html檔案進行BroadcastChannel
的使用, 本地除錯需要啟動page-server
才行, 但是谷歌不用啟動服務, 直接用html檔案即可除錯。
相容性
方法二: 使用localStorage完成通訊
發出資訊頁面程式碼
localStorage.setItem('key999', '要傳遞的值')
- 這個命令會向localStorage裡儲存一個值
接收資訊頁面
window.addEventListener('storage', (res) => {
console.log(res.storageArea.key999)
})
- 監聽storage的變化, 獲取key對應的val
- 缺點是隻能傳字串
- 其他的localStorage變化也會導致觸發, 無法確定是否為自己想監聽的
- key的值不變, 但是呼叫了
localStorage.setItem
, 也會觸發監聽
總的來說localStorage
就是比較浪費資源並且不太專業, 所以推薦使用BroadcastChannel
。
二、 主動插入微任務佇列 window.queueMicrotask
我們觸發一個微任務大部分都是利用Promis
, 一般的方法是Promise.resolve().then(() => { console.log('promise) })
, 但是這樣寫不是很專業, 其實瀏覽器給我們準備了專業的方法:
Promise.resolve().then(() => { console.log(1) })
queueMicrotask(() => {
console.log('執行了')
})
Promise.resolve().then(() => { console.log(2) })
上圖可以看出queueMicrotask
定義的任務在兩個Promise
中間執行了, 語法更簡潔, 但是作者認為你寫程式碼更多要考慮同事的接受程度, 如果同事理解這個api
比較費勁的話那我推薦依然使用Promise.resolve().then
來寫, 畢竟程式碼不是用來秀的。
小心混淆requestIdleCallback (瀏覽器空閒執行)
大家千萬不要搞混了,requestIdleCallback
是在瀏覽器空閒執行, 與微任務不同的是requestIdleCallback
的執行時機不是很確定, 例如下圖:
Promise.resolve().then(() => {
console.log(1)
})
requestIdleCallback(() => {
console.log('執行了')
})
Promise.resolve().then(() => {
console.log(2)
})
setTimeout(() => {
console.log(3)
})
setTimeout(() => {
console.log(4)
}, 10)
狀況1:
狀況2:
之所以這個樣子是因為requestIdleCallback
並不在事件迴圈的佇列裡, 我們可以透過一個屬性執行一個超時時間:
Promise.resolve().then(() => {
console.log(1)
})
requestIdleCallback(() => {
console.log('執行了')
}, { timeout: 1 }) // 注意這裡
Promise.resolve().then(() => {
console.log(2)
})
setTimeout(() => {
console.log(3)
})
setTimeout(() => {
console.log(4)
}, 10)
上述程式碼裡面增加了 { timeout: 1 }
這個選項, 它的意思是當超過1
毫秒目標函式仍未被執行則將其加入時間迴圈的佇列裡, 這樣就可以保證requestIdleCallback
內的函式必然會被執行, 而不是瀏覽器一直不空閒導致一直不執行。
瀏覽器何時空閒這個不太好判斷, 比如垃圾回收機制就可能導致瀏覽器晚一些才空閒, 那麼requestIdleCallback
的執行時機就不是很確定了, 但是它本身很適合差異渲染, 比如將不重要的邏輯放在requestIdleCallback
裡面, 這樣不影響主要程式的執行。
三、csp 內容安全策略
CSP 的主要目標是減少和報告 XSS 攻擊。XSS 攻擊利用了瀏覽器對於從伺服器所獲取的內容的信任。惡意指令碼在受害者的瀏覽器中得以執行,因為瀏覽器信任其內容來源,即使有的時候這些指令碼並非來自於它本該來的地方。
上面提到了csp主要針對指令碼檔案,舉例說明: 當一個網頁遭受xss攻擊時,可能會載入到惡意的 <script src="惡意連結"> </script>
, 那麼其實我們只要阻止網站獲取與執行陌生url載入到的指令碼就解決了啊。
要配置csp還需要server大兄弟前來助力, 伺服器返回 Content-Security-Policy HTTP 標頭, 以及前端html裡面可以做一些配置:
某些功能(例如傳送 CSP 違規報告)僅在使用 HTTP 標頭時可用。
<meta
http-equiv="Content-Security-Policy"
content="script-src 'self' " />
這裡content="script-src 'self' "
表明頁面只載入當前域名下的指令碼檔案: 更多配置可以看這裡
亞馬遜csp的配置真是誇張啊
server的返回也有配置
有空研究下csp讓你的網站更安全吧!
四、Window.getSelection 讓選中的文字起舞
先看效果:
1: 選中文字
在做複製文案相關需求的時候我接觸到了Window.getSelection
方法, 下面的方法可以輸出使用者選中的文字:
<div>
1234567890
</div>
<script>
setTimeout(() => {
let selObj = window.getSelection();
alert(selObj)
}, 3000)
</script>
2: 框選物件
window.alert 的引數將呼叫物件的 toString 方法
所以其實selObj
物件是有很多值的, 他本身並不是字串:
其中我們可以透過baseNode
與extentNode
兩個屬性獲取到框選的初始dom與結束dom。
3: 切分文字方便動畫
為了方便操控每一個字元我們將文章內容切分成一個個span
標籤, 並且從0開始標記序號:
let res = ''
let str = `我們鼓勵開發者大開腦洞,結合日常生活中的痛點需求、興趣愛好和專業方向,進行 Generative AI 應用的構建,例如:藝術品、音樂、漫畫頭像、求職簡歷生成器,智慧客服機器人,AI Code Review 工具,基於 AI 的資料視覺化平臺,醫療影像分析應用等等 —— 以上這些場景聯想由 ChatGPT 生成。當然,你也可以選擇任何你感興趣的方向。`
for (let i = 0; i < str.length; i++) {
res += `<span class="sp" data-index="${i}">${str.charAt(i)}</span>`
}
最後使用res
作為body
的innerHTML
, 下圖是渲染後的效果, 所以不要用在文字內容多的場景下:
4: 賦予型別, 操作動畫
我們可以透過baseNode
與extentNode
兩個屬性獲取到框選的初始dom與結束dom, 再透過獲取dom身上的data-index
屬性求出左側元素與右側元素, 接下來就是從左往右逐個元素進行賦予className
:
const i1 = +selObj.baseNode.parentElement.getAttribute('data-index')
const i2 = +selObj.extentNode.parentElement.getAttribute('data-index')
let max = Math.max(i1, i2)
let min = Math.min(i1, i2)
let targetdom;
if (i1 > i2) {
targetdom = selObj.extentNode.parentElement;
} else {
targetdom = selObj.baseNode.parentElement;
}
有了目標元素我們就可以逐個獲取兄弟元素了:
(function task() {
if (min <= max) {
selectedDom.push(targetdom) // 放在陣列裡方便統一處理
targetdom.classList.add('動畫屬性')
targetdom = targetdom.nextElementSibling;
min++
timer = setTimeout(() => { // 用timer記錄方便後續清理
task()
}, 50)
}
})()
觸發條件: 每次滑鼠抬起觸發
document.onmouseup = () => {
foo() // 動畫函式
}
要注意: 選擇模式, 當selObj.type === 'Caret'
時使用者並沒有框選文字, 而是點選了某處使游標停在某處, 所以此時我們應該直接return
而不進行動畫操作。
五、Window.stop() 提高效能神技
window.stop() 方法的效果相當於點選了瀏覽器的停止按鈕。由於指令碼的載入順序,該方法不能阻止已經包含在載入中的文件,但是它能夠阻止圖片、新視窗、和一些會延遲載入的物件的載入。
初見這個方法感覺也沒啥太大作用, 但是當你遇到一個圖片資源很大的頁面時它就是神級的方法了, 比如商品的詳情頁裡可能會有不少商品詳情的清晰圖, 但是當我們從商品列表頁進入商品詳情頁後立即返回列表再點開其他商品時, 其實上一個頁面的圖片資源沒必要繼續載入了, 如果圖片資源較大甚至會導致頁面明顯示卡頓, 那我們其實可以主動呼叫window.stop()
。
六、 writing-mode 文字排布與padding-inline 的默契配合
writing-mode 屬性定義了文字水平或垂直排布以及在塊級元素中文字的行進方向。為整個文件設定該屬性時,應在根元素上設定它(對於 HTML 文件,應該在 html 元素上設定)
writing-mode
可以做到的功能之一就是將文字從橫向書寫變成縱向書寫:
在父級元素設定值:
writing-mode: vertical-lr;
此時可能你已經想到問題了, 如果改成豎著書寫那麼如果有padding
的話豈不是就出bug了:
padding-left: 20px;
padding-right: 10px;
左右的邊距還是需要改的, 因為這個邊距是針對dom容器的而不是文字的, 那其實就要可以請出我們的主角padding-inline:
writing-mode: vertical-lr;
padding-inline: 20px 10px;
我們還可以使用padding-inline-end
與padding-inline-start
這樣語義化更好一些。
七、@counter-style 處理有序列表(官方演示不靠譜!)
這個屬性是可以給列表增加序號的css屬性, 雖然我們不太可能會用它但是我們還是要體會一下, 我們先來看一下MDN官方提供的例子:
@counter-style pad-example {
system: numeric;
symbols: "0" "1" "2" "3" "4" "5";
pad: 2 "0";
}
.list {
list-style: pad-example;
}
<ul class="list">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
圖片上看起來這個屬性就是用來設定序號, 並且可以補位的, 但是此時如果我們增加更多的li
:
<ul class="list">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<li>8</li>
</ul>
這個現象還是蠻奇怪的, 從第六個開始由於我們們沒有定義symbols
屬性結果這裡直接從10開始了計數, 這裡當時看的一頭霧水, 該不會css的計算規則寫錯了吧!
而且還有另一個疑問symbols: "0" "1" "2" "3" "4" "5";
這裡面的"0"哪裡去了?
要解開上述兩個謎題就需要我們換一個寫法:
@counter-style pad-example {
system: numeric;
symbols: "xxx" "1" "2" "3";
}
<ul class="list">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<li>8</li>
<li>9</li>
<li>10</li>
<li>11</li>
</ul>
我們做的再明顯一點:
@counter-style pad-example {
system: numeric;
symbols: "&" "-" "*";
}
其實當我們symbols
傳入2個值的時候它相當於是2進位制計算, 輸入n個值就是n進位制。
神奇的pad屬性
我們可以使用pad屬性進行序號的補全, 但是一定注意這裡說的補全是針對symbols
屬性, 如下寫法是不生效的:
@counter-style pad-example {
system: numeric;
pad: 2 "0";
}
但是寫成這樣他就有效了:
@counter-style pad-example {
system: numeric;
symbols: "x" "y";
pad: 4 "0";
}
結論是這個屬性還蠻有趣的, 我們能感受到css的個性, 但真不建議使用。
八、react裡多個請求只使用最後一個
場景是這樣的我們在一段時間內傳送了多個請求, 但我只想要最後發出的請求的返回值, 假設我當前傳送了一個請求a1
,過了5秒請求a1
沒有返回我又傳送了一次相同的請求請求a2
, 立刻請求a2
返回了值, 我使用返回值更新了列表, 但是2秒後請求a1
的值返回了, 那麼此時我就不應該再更新一遍列表了, 所以有了如下的hook方法:
同一個請求多次觸發為啥不用防抖或者節流? 因為防抖與節流不好處理時間較長的情況, 就像例子中請求a1
5秒都沒有返回結果並且你不知道請求a
10秒還是20秒後會返回結果, 或者永遠不返回。
function useGetEndPromise() {
let ref = useRef(0);
return function (promise: any, cb: any) {
ref.current += 1;
let n = ref.current;
promise.then((res: any) => {
if (n === ref.current) {
cb(res);
}
});
};
}
- 利用ref計數, 每次ref+1, 最終返回值時如果當前的ref不等於請求時的ref則表示
使用方法:
export default function Home() {
const getEndPromise = useGetEndPromise();
// ....
return <button
onClick={() => {
getEndPromise(你請求的Promise, promis返回時執行的函式);
}}
>
}
- 要注意的是
useGetEndPromise();
生成的函式使用時接收的所有函式都會按照只有最後發起的生效, 所以某些需求裡面建議用useGetEndPromise();
生成多個函式再分開使用。
九、react中ref引起的一個bug
useref的值的改變不會觸發react的重新渲染, 所以它值的改變也是同步執行的不用非同步獲取。
這是我的一個真實案例, 同事封裝的一個外掛需要我傳入一些生命週期hooks, 但是出現了bug 我傳入的函式里面無法執行hook操作, 我們直接看例子:
function Demo(props: any) {
const { fn, n } = props;
const ref = useRef(fn);
return <button onClick={ref.current}>點選展示 {n}</button>;
}
export default function StateDemo() {
const [n, setN] = useState(1);
const fn = () => {
setN(n + 1);
};
return (
<div>
<Demo fn={fn} n={n} />
</div>
);
}
很奇怪吧, button點選後n變成2之後就不再變化了, 這裡其實好理解因為const ref = useRef(fn);
這一步其實只會執行一次, 所以ref.current
一直是同一個函式, 現在我們加大難度:
function Demo(props: any) {
const { fn, n } = props;
const ref = useRef(fn);
useEffect(() => {
ref.current = fn;
console.log(ref.current === fn)
}, [fn]);
return <button onClick={ref.current}>點選展示 {n} {ref.current === fn ? 'true' : "flase"}</button>;
}
這裡我們新加了useEffect
當fn
變化的時候我們更新ref.current
, 但是此時點選button後的效果仍然與上面相同:
我們再升級一下程式碼, 奇怪的現象又來了:
function Demo(props: any) {
const { fn, n } = props;
const ref = useRef(fn);
useEffect(() => {
ref.current = fn;
console.log(ref.current === fn)
}, [fn]);
return <button onClick={ref.current}>點選展示 {n} {ref.current === fn ? 'true' : "flase"}</button>;
}
export default function StateDemo() {
const [n, setN] = useState(1);
const fn = () => {
setN(n + 1);
};
return (
<div>
<button onClick={() => { // 新增了外層觸發n的變化
setN(n + 1)
}}>點選n+1 </button>
<Demo fn={fn} n={n} />
</div>
);
}
雖然我們在useEffect
中ref.current = fn;
但是別忘了, ref值的變化不會觸發react重新render, useEffect
在react的所有render執行結束後才執行, 所以就是說dom上onClick方法掛載的fn永遠是上一個, 所以導致了點選看似無效, 實則是執行了上一個fn:
被坑了後我扒了下元件庫的原始碼才找到這個問題, 當時真是被坑哭了, 但願大家別採坑吧。
end
這次就是這樣, 希望與你一起進步。