第九期:前端九条启发分享
作者一直忙别的事好久没写文章惭愧惭愧, 工作变动求靠谱初创公司推荐ღ( ´・ᴗ・` )比心
一、浏览器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
这次就是这样, 希望与你一起进步。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。