学了点技术,我要开始装X了
今天不看技术文,而是带大家装x,就从 獨釣寒江雪 的《前端装逼技巧 108 式》偷师几招吧~ 绝对让你过把瘾!
作者:獨釣寒江雪
原文:https://segmentfault.com/a/1190000040437981
第一式:你见过页面跳舞吗?
还记得魔性的小苹果和抖音吗,作为前端的你,有想过让页面也 High 起来、舞动起来吗?
先看效果:
下面这段代码可直接在控制台执行,略长。可直接跳到本小节末,使用简短 JS 引入的方式进行体验(带音乐效果哟)
setTimeout(letDance, 1000);
var bgmSrc =
'https://nd002723.github.io/carnival/audio/Martin%20Jensen%20-%20Fox%20(Loop%20Remix).mp3';
var cssHref = 'https://nd002723.github.io/carnival/css/high.css';
function letDance() {
function loadCss() {
//将css文件引入页面
var myCss = document.createElement('link');
myCss.setAttribute('type', 'text/css');
myCss.setAttribute('rel', 'stylesheet');
myCss.setAttribute('href', cssHref); //css文件地址
myCss.setAttribute('class', l);
document.body.appendChild(myCss);
}
function h() {
var e = document.getElementsByClassName(l);
for (var t = 0; t < e.length; t++) {
document.body.removeChild(e[t]);
}
}
function p() {
var e = document.createElement('div');
e.setAttribute('class', a);
document.body.appendChild(e);
setTimeout(function () {
document.body.removeChild(e);
}, 100);
}
function getSize(e) {
//获取目标的宽高
return {
height: e.offsetHeight,
width: e.offsetWidth,
};
}
function checkSize(i) {
//判断目标大小是否符合要求
var s = getSize(i); //获取目标的宽高
return (
s.height > minHeight &&
s.height < maxHeight &&
s.width > minWidth &&
s.width < maxWidth
); //判断目标是否符合条件
}
function m(e) {
var t = e;
var n = 0;
while (!!t) {
n += t.offsetTop;
t = t.offsetParent;
}
return n;
}
function g() {
var e = document.documentElement;
if (!!window.innerWidth) {
return window.innerHeight;
} else if (e && !isNaN(e.clientHeight)) {
return e.clientHeight;
}
return 0;
}
function y() {
if (window.pageYOffset) {
return window.pageYOffset;
}
return Math.max(
document.documentElement.scrollTop,
document.body.scrollTop
);
}
function E(e) {
var t = m(e);
return t >= w && t <= b + w;
}
function setBgm() {
//设置音乐
var e = document.createElement('audio');
e.setAttribute('class', l);
e.src = bgmSrc; //bgm地址
e.loop = false;
e.addEventListener(
'canplay',
function () {
setTimeout(function () {
x(k);
}, 500);
setTimeout(function () {
N();
p();
for (var e = 0; e < O.length; e++) {
T(O[e]);
}
}, 15500);
},
true
);
e.addEventListener(
'ended',
function () {
N();
h();
},
true
);
e.innerHTML =
' <p>If you are reading this, it is because your browser does not support the audio element. We recommend that you get a new browser.</p> <p>';
document.body.appendChild(e);
e.play();
}
function x(e) {
e.className += ' ' + s + ' ' + o;
}
function T(e) {
e.className += ' ' + s + ' ' + u[Math.floor(Math.random() * u.length)];
}
function N() {
var e = document.getElementsByClassName(s);
var t = new RegExp('\\b' + s + '\\b');
for (var n = 0; n < e.length; ) {
e[n].className = e[n].className.replace(t, '');
}
}
var minHeight = 3; //最小高度
var minWidth = 3; //最小宽度
var maxHeight = 800; //最大高度
var maxWidth = 1400; //最大宽度
var s = 'mw-harlem_shake_me';
var o = 'im_first';
var u = ['im_drunk', 'im_baked', 'im_trippin', 'im_blown'];
var a = 'mw-strobe_light';
var l = 'mw_added_css'; //最终要移除的css
var b = g();
var w = y();
var C = document.getElementsByTagName('*');
var k = null;
for (var L = 0; L < C.length; L++) {
var targetDiv = C[L];
if (checkSize(targetDiv)) {
if (E(targetDiv)) {
k = targetDiv;
break;
}
}
}
if (targetDiv === null) {
//如果没找到合适大小的
console.warn('没能找到合适的大小. 换一个页面试试?.');
return;
}
loadCss(); //将自定义css文件引入页面
setBgm(); //添加背景音乐
var O = [];
for (var L = 0; L < C.length; L++) {
var targetDiv = C[L];
if (checkSize(targetDiv)) {
O.push(targetDiv);
}
}
//网页整体倾斜效果(这块儿本来是JQuery实现的,为了避免引入JQuery,做了改动。)
var style = document.createElement('style');
style.type = 'text/css';
try {
style.appendChild(
document.createTextNode(
'body{overflow-x:hidden;transform: rotate(1deg);-webkit-transform: rotate(1deg);-moz-transform: rotate(1deg);-o-transform: rotate(1deg);-ms-transform: rotate(1deg)}'
)
);
} catch (ex) {
style.styleSheet.cssText = 'body{background-color:red}'; //针对IE
}
var head = document.getElementsByTagName('head')[0];
head.appendChild(style);
}
或者更简洁一点,在页面 URL 栏或者控制台键入以下代码直接体验:
在浏览器地址栏黏贴以下内容的话,有三点需要注意,一是必须是已有内容的页面;二是如果是通过复制黏贴代码到浏览器地址栏的话,IE 及 Chrome会自动去掉代码开头的
javascript:
,所以需要手动添加起来才能正确执行,而 Firefox 中虽然不会自动去掉,但它根本就不支持在地址栏运行 JS 代码;三是引用的carnival.js
会依赖JQuery
(没有的话也没事,只是页面少了一个倾斜的效果)。
javascript: void (function () {
var d = document,
a = 'setAttribute',
s = d.createElement('script');
s[a]('tyle', 'text/javascript' "a");
s[a]('src', 'https://nd002723.github.io/carnival/js/carnival.js' "a");
d.head.appendChild(s);
})();
一个能让你的网站 high 起来的 js[1]
第二式:一个可以拿来装逼的题库
作为程序员,需求无处不在,bug 无处不在,自然,装逼也可以无处不在。回想你的每一次面试,是否曾被面试官鄙视或者秀一脸?回想你和同事的每一次交流,是否曾被同事展示的古怪面试题或者奇技淫巧搞的无语难受 😓,想秀回去却苦于一时“脑中羞涩”,JavaScript Puzzlers![2]就是一个收录的各种表态 JavaScript 题目的网站,有了它,现在妈妈再也不怕我找不到可以装逼的代码/题目了。
我们不妨先看几道题目:
var min = Math.min(),
max = Math.max();
min < max; // 答案: false
// 有趣的是, Math.min 不传参数返回 Infinity, Math.max 不传参数返回 -Infinity 😆
// 这个还是相对容易的
var name = 'World!';
(function () {
if (typeof name === 'undefined') {
var name = 'Jack';
console.log('Goodbye ' + name);
} else {
console.log('Hello ' + name);
}
})();
// 答案:Goodbye Jack
'1 2 3'.replace(/\d/g, parseInt); // 答案: 1, NaN, 3
不,我是一个前端工程师,工程师的事儿,能叫装逼么?也许它是没有太大用处的脑筋急转弯,但是这叫小技巧后面藏着的大智慧,哈哈~
话说回来,这些变态题目涉及的知识点很广,如果你能掌握这些题目背后的原理,也许就会对 JavaScript 的理解有一个不小的提升,下面将题目奉上,供诸君参阅:
目前由于未知原因,也许是去装逼答题的太多了吧 😄,JavaScript Puzzlers![3]网站已不可访问,所幸您还可以通过下面两个 GitHub 链接查看具体的题目及解析。
44 个 Javascript 变态题解析 (上)[4] 44 个 Javascript 变态题解析 (下)[5] JavaScript Puzzlers![6]
第三式:浏览器地址栏,🐂🍺克拉斯
作为一名前端开发者,我们甚至每天与浏览器相伴的时间比女朋友/男朋友(如果有的话 😄)陪伴你的都要久,想想那每一个令人“不是那么期待”的早晨,每一个争分夺秒完成任务的黄昏,只有浏览器和编辑器一直是你忠实的伙伴,那么,你了解浏览器吗?了解那个每天都要输入http://localhost:3000/
的地址栏吗?本节带你认识那些关于地址栏的好玩儿的细节。
浏览器地址栏运行 JavaScript 代码
对,你没看错,在“浏览器地址栏运行 JavaScript 代码”,做法是以javascript:
开头后跟要执行的语句。需要注意的是如果是通过复制、黏贴代码到浏览器地址栏的话,IE 及 Chrome 会自动去掉代码开头的 javascript:
,所以需要手动添加起来才能正确执行。并且,你需要在网站的地址栏运行javascript:
,而不能在新打开的空标签页面上运行,那样也不会生效。
// 点击确定后,会接着弹出:孤舟蓑笠翁,独钓寒江雪,点击取消则不会弹出
javascript: if (confirm('千山鸟飞绝,万径人踪灭'))
alert('孤舟蓑笠翁,独钓寒江雪');
浏览器地址栏运行 HTML 代码
如果说在“浏览器地址栏运行 JavaScript 代码”知道的人还算多的话,在“浏览器地址栏运行 HTML 代码”知道的人就要少一些了,在非 IE 内核的浏览器地址栏可以直接运行 HTML 代码!在地址栏输入以下代码然后回车运行,会出现下图所示的页面:
data:text/html,
<h1>Nothing is given, Everything is earned!</h1>
浏览器可以是你的记事本
这个还是在浏览器地址栏上面做文章,将以下代码复制粘贴到浏览器地址栏,运行后浏览器就变成了一个简单原始的编辑器,话不多说,直接试试吧。
data:text/html,
<html contenteditable></html>
另外,补充一个万能的浏览器撤销关闭页快捷键:MAC OS 下,command + shift + t
;Windows:ctrl + shift + t
。这个重新打开被关闭的页面的快捷键,可以一直按,会依次恢复被关闭的页面哟~,要不,看看小伙伴都干了些什么,看看会不会被打 😜
参考资料:这些鲜为人知的前端冷知识,你都 GET 了吗?
第四式:让汪峰轻松走上百度热搜
“汪峰上头条”一直是娱乐界里的一个梗,每次在微博热搜榜看到汪峰,以为能登顶榜首,不想都会有其他的爆点压制住,不管粉丝怎么努力,汪峰还是迟迟上不了头条!
最后“帮汪峰上头条”反而成了热搜,甚至被收录入百度百科[7]🐶。
蒽 🤔,如果汪峰是个前端工程师,那热搜的事儿不是分分钟搞定吗?就算不懂 HTML,只要知道 HTML5 的contenteditable
属性,控制台输入一个document.body.contentEditable='true';
,热搜还不是随便改,想要多少有多少 😄。
蒽 🤔,学了这招,甚至你可以轻松通过热搜向对象告白了...不过作为专业的 web 应用和网站开发人员,我们怎能满足于此呢?
同理,也是利用了 HTML5 中的contenteditable
属性,巧妙的在 body 增加一个可编辑的 style 标签,那这个样式,我们在页面上都可以修改了,想想就...没有太多卵用,哈哈哈 😂,不过话说回来,这个属性在部分富文本编辑器上还是很有用处的。很多富文本编辑器就是基于contenteditable
实现的,具体可以参考深入浅出 contenteditable 富文本编辑器[8]。
<style style="display:block; height:50px;" contenteditable>
body {
background: red;
}
</style>
效果图(图片来自下面的参考文章):
你以为contenteditable
只有true
和false
?其实它的可选值包括:
contenteditable="" contenteditable="events" contenteditable="caret" contenteditable="plaintext-only" contenteditable="true" contenteditable="false"
除了 HTML5 的contenteditable
属性,其实还有一个不常用的 css 属性 —— user-modify
可以实现类似的效果,user-modify
可取值为以下四个:
read-only
read-write
write-only
read-write-plaintext-only
其中,write-only
不用在意,当下这个年代,基本上没有浏览器支持,以后估计也不会有。read-only
表示只读,就是普通元素的默认状态。然后,read-write
和read-write-plaintext-only
会让元素表现得像个文本域一样,可以 focus 以及输入内容,前者可以输入富文本,而后者只能输入纯文本。
具体效果你可以通过审查元素,给元素添加 CSS 样式查看,也可以直接看张鑫旭大佬的CSS user-modify 属性行为表现测试实例页面[9]demo。
这些鲜为人知的前端冷知识,你都 GET 了吗? 小 tip: 如何让 contenteditable 元素只能输入纯文本[10]
第五式:鼠标消失术
这个一个隐藏页面上鼠标的技巧,其实不值一提,也没有什么卵用。可以直接复制以下代码到控制台试试(此时如果在页面上吊起右键菜单,还是可以看见鼠标的):
var style = document.createElement('style');
document.head.appendChild(style);
style.type = 'text/css';
style.styleSheet
? (style.styleSheet.cssText = '* { cursor: none;!important; }')
: style.appendChild(
document.createTextNode('* { cursor: none;!important; }')
);
原理非常简单,设置cursor
属性为none
即可:
* {
cursor: none !important;
}
或者在浏览器地址栏输入以下内容:
IE 和 Chrome 会自动隐去前面的
javascript:
然后把后面的部分当做查询字段。你需要复制以下代码黏贴后手动在前面加上javascript:
, 然后回车效果就出来了。
javascript:function play(){var style = document.createElement('style');document.head.appendChild(style);style.type = 'text/css';style.styleSheet ? (style.styleSheet.cssText = '* { cursor: none !important; }') : style.appendChild(document.createTextNode('* { cursor: none !important; }'));}play();
参考资料:前端随便玩儿[11]
第六式:让你的网站模糊不清
也许你经常碰到这样的页面,当没有登录的时候,只能看到下面的效果:
当然,这里的模糊不清效果是通过背景图占位来做的,其实如果不考虑安全性、被破解等因素,我们也完全可以使用 css 来实现类似的效果。
javascript:function play(){var style = document.createElement('style');document.head.appendChild(style);style.type = 'text/css';style.styleSheet ? (style.styleSheet.cssText = '* { color: transparent !important; text-shadow: #333 0 0 10px !important; }') : style.appendChild(document.createTextNode('* { color: transparent !important; text-shadow: #333 0 0 10px !important; }'));}play();
显而易见,这里主要是使用了以下两个 CSS 属性:
color: transparent !important;
text-shadow: #333 0 0 10px !important;
参考资料:前端随便玩儿[12]
第七式:让网站屏蔽开发者工具
浏览器开发者工具是给我们这些专业的 web 应用和网站开发人员使用的工具(当然,到底专不专业,自己心里都会有点 B 数 😄),它的作用在于,帮助开发人员审查元素、对网页进行布局、帮助前端工程师更好的断点调试等,还可以使用工具查看网页加载过程,进行性能分析和优化,获取网页请求等(这个过程也叫做抓包)。笔者可以豪不违心的说,离开了开发者工具,火热而有趣的的前端开发将变得索然无味,因为我感受不到有比浏览器自带开发者工具更趁手的利器。当然,据说真正的大神写出的 JS 和 CSS 都是不需要进行调试的,那我们另当别论,显然我和真正的大神不是一类人。
既然开发者工具这么可爱、这么好用,那我们为什么要屏蔽它呢?—— 可能是因为我们用过了、完成了开发工作,不想让别人有机会发现那些我们自己也看不懂的代码以及其中蕴含的商业机密吧,哼哼,果然是渣男~
那么,到底该如何做一个有能力可以屏蔽开发者工具的渣男呢?
也许依据打开控制台的几种方式,你自然就想到了:
监听 F12; 监听和禁止右键菜单(因为右键菜单里有“检查”选项可以打开控制台);
但是这样真的就行了吗?不,我们依然可以通过浏览器右上角的三个点,找到更多工具中的开发者工具,然后点击打开。
那我们该以什么思路解决这一问题呢?网上解法有很多,有些非主流,有些由于浏览器的升级已失效,相关思路及链接会在本小节末尾给出,这里只说两个我觉得还不错的方法:
const im = new Image();
Object.defineProperty(im, 'id', {
get: function () {
// 在这里放入你的代码,比如我这里会让他跳到百度
console.log('Console is opened');
window.location.href = 'http://www.baidu.com';
},
});
console.log(im); //谷歌最新版失效
let num = 0; //谷歌最新版有效
const devtools = new Date();
devtools.toString = function () {
num++;
if (num > 1) {
// 在这里放入你的代码,比如我这里会让他跳到百度
console.log('Console is opened');
// window.location.href = "http://www.baidu.com";
return Date.prototype.toString.call(devtools); // 返回devtools结果(这一步不是必须的)
}
};
console.log(devtools);
以上方法的核心原理在于一点:只有打开控制台,才会执行 console 方法,而使用console
打印Date
,会调用Date
的toString
方法,我们对toString
方法做了改写。如果直接注入代码,如console = 1
,以上代码将失效。
其他几种思路包括:
监听 F12 或者 shift+ctrl+i 调起开发者工具(无法防止先打开开发者工具,然后在地址栏输入网址的访问); 监听和禁止右键菜单(因为右键菜单里有“检查”选项可以打开控制台); 监视窗口大小(适用于未将开发工具设置成独立窗口的情况); 监视 DOM 修改(适用于水印保护等场景); 利用 debugger 的特性,无限递归。
除了以上方法,也有诸如devtools-detector[13]一类的插件,用来对开发者工具打开的监测问题,在此不过多赘述。
其他参考:
网站如何检测到是否开启开发者工具?[14] JS 检测,禁用浏览器开发者工具之 6 大方法探讨[15] 前端开发中如何在 JS 文件中检测用户浏览器是否打开了调试面板(F12 打开开发者工具)?[16] 网站这样来屏蔽开发者工具,不比监听 MouseDown 舒服?[17] JS 禁止打开控制台[18]
第八式:代码性能大pk
性能、各种写法的优劣是我们在日常开发、技术讨论中最常提及和关注的。在写一段代码的时候,很多同学可能都会想要知道它的性能到底如何,和其他写法比起来哪个更快,但却苦于没有好用的工具,只能手动测试运行时间,这样一个是不方便,二是因为样本数太少误差较大。那么,除了对原理解析这种理论性的东西之外,我们可以怎样简洁、清晰、高效的对比各种不同 JS 写法的执行速度和性能呢?这就涉及到 JS性能测试工具了。
JS 性能测试工具原理一般是将给定的测试用例循环在指定环境下运行许多次,然后输出比对结果。JSBench.Me[19]就是这样一款在线代码性能测试利器。
同时也有一款 npm 插件 —— 强大的基准测试库Benchmark.js[20]官方说:
Benchmark.js 是一个强大的**基准测试**[21]库,支持高分辨率计时器并返回具有统计意义的结果。正如在 jsPerf 上看到的那样。
上文提到的jsPerf[22]本来是我想要介绍的一个工具,奈何这款工具无情的拒绝了我 😭。
所以,我们还是看看Benchmark.js
这个库的使用吧:
var suite = new Benchmark.Suite();
// add tests
suite
.add('RegExp#test', function () {
/o/.test('Hello World!');
})
.add('String#indexOf', function () {
'Hello World!'.indexOf('o') > -1;
})
// add listeners
.on('cycle', function (event) {
console.log(String(event.target));
})
.on('complete', function () {
console.log('Fastest is ' + this.filter('fastest').map('name'));
})
// run async
.run({ async: true });
// logs:
// => RegExp#test x 4,161,532 +-0.99% (59 cycles)
// => String#indexOf x 6,139,623 +-1.00% (131 cycles)
// => Fastest is String#indexOf
第九式:浏览器也会摸鱼 🐟 ?
浏览器一帧都会干些什么?
我们都知道,页面的内容都是一帧一帧绘制出来的,浏览器刷新率代表浏览器一秒绘制多少帧。原则上说 1s 内绘制的帧数也多,画面表现就也细腻。
电影放映的标准是每秒放映 24 帧,也就是说电影每秒放映 24 幅画面,以达到动画的效果,超过 24 帧/s 连续的变化,视觉暂留就会将静态的画“动”起来。研究表明,人眼承受的极限为每秒 55 帧,还有研究表明,每秒 60 帧以上可以明显提升观众的观影感受。每秒 120 帧是每秒 24 帧的 5 倍,采用这样的拍摄技术可以让画面更加栩栩如生,让观众仿佛置身其中,给人一种似真似幻的感觉。
目前浏览器大多是 60Hz(60 帧/s),每一帧耗时也就是在 16.6ms 左右。那么在这一帧的(16.6ms) 过程中浏览器又干了些什么呢?
通过上面这张图可以清楚的知道,浏览器一帧会经过下面这几个过程:
接受输入事件,处理用户的交互,如点击、触碰、滚动等事件 执行事件回调 开始一帧 执行 RAF ( RequestAnimationFrame
)页面布局,样式计算 绘制渲染 执行 RIC ( RequestIdelCallback
)
第七步的 RIC 事件不是每一帧结束都会执行,只有在一帧的 16.6ms 中做完了前面 6 件事儿且还有剩余时间,才会执行。如果一帧执行结束后还有时间执行 RIC 事件,那么下一帧需要在事件执行结束才能继续渲染,所以 RIC 执行不要超过 30ms,如果长时间不将控制权交还给浏览器,会影响下一帧的渲染,导致页面出现卡顿和事件响应不及时。
requestIdleCallback 的启示
我们以浏览器是否有剩余时间作为任务中断的标准,那么我们需要一种机制,当浏览器有剩余时间时通知我们。
requestIdleCallback((deadline) => {
// deadline 有两个参数
// timeRemaining(): 当前帧还剩下多少时间,最大值50ms
// didTimeout: 是否超时
// 另外 requestIdleCallback 后如果跟上第二个参数 {timeout: ...} 则会强制浏览器在当前帧执行完后执行。
if (deadline.timeRemaining() > 0) {
// TODO
} else {
requestIdleCallback(otherTasks);
}
});
// 用法示例
let tasksNum = 10000;
requestIdleCallback(unImportWork);
function unImportWork(deadline) {
while (deadline.timeRemaining() && tasksNum > 0) {
console.log(`执行了 ${10000 - tasksNum + 1}个任务`);
tasksNum--;
}
if (tasksNum > 0) {
// 在未来的帧中继续执行
requestIdleCallback(unImportWork);
}
}
其实部分浏览器已经实现了这个 API,这就是 requestIdleCallback。但是由于以下因素,Facebook 在 React 的重构升级中, 抛弃了 requestIdleCallback 的原生 API,而实现了功能更完备的 requestIdleCallbackpolyfill
,这就是Scheduler。除了在空闲时触发回调的功能外,Scheduler 还提供了多种调度优先级供任务设置:
浏览器兼容性; 触发频率不稳定,受很多因素影响。比如当我们的浏览器切换 tab 后,之前 tab 注册的 requestIdleCallback 触发的频率会变得很低。
requestIdleCallback
的 callback 会在浏览器的空闲时间运行,而在w3c 文档[23]里,空闲时间分两种:
在执行一段连续的动画时,将给定帧提交到屏幕与开始处理下一帧之间的时间,这段时间内属于空闲时间。在连续动画和屏幕更新期间,此类空闲时间会频繁发生,但通常会非常短(即,如果我们的屏幕是 60hz(1s 内屏幕刷新 60 次)的设备,小于 16 毫秒),如下图所示。
另外一种空闲时间,当用户属于空闲状态(没有与网页进行任何交互),并且屏幕中也没有动画执行。此时空闲时间理论上是无限长的。但为了避免在不可预测的任务(例如处理用户输入)引起用户可察觉的延迟,这些空闲时间段的长度应限制为最大值 50ms,一旦空闲期结束,浏览器可以安排另一个空闲期。
也就是说,即使浏览器一直处于空闲状态的话,deadline.timeRemaining
可以得到的最长时间,也是 50ms,这是 w3c 标准[24] 规定的。一些低优先级的任务可使用 requestIdleCallback
等浏览器不忙的时候来执行,同时因为时间有限,它所执行的任务应该尽量是能够量化,细分的微小任务。
50 ms 的最大截止时间来自一个
RESPONSETIME
研究,该研究表明,对 100 毫秒内用户输入的响应通常被人类感知为瞬时的。将空闲期限限制为 50 ms 意味着即使用户输入在空闲任务开始后立即发生,用户代理仍有剩余的 50 ms 时间来响应用户输入,而不会产生用户可察觉的延迟。
当设备的性能越来越好,浏览器支持的效果越来越炫,浏览器的开发者开始越来越多的考虑使用原生 API 来处理一些之前特别占用性能的功能,自从最初的 requestAnimationFrame
,InsterSectionObserver
,到requestIdleCallback
,对于前端的将来,充满希望,没错,我们都会有“光明的未来”,哈哈 😄,关于浏览器的更多细节,可以参考我之前的两篇文章:
浏览器是如何工作的:Chrome V8 让你更懂 JavaScript[25] 47 张图带你走进浏览器的世界[26]
拓展阅读与参考
requestIdleCallback-后台任务调度[27] 浏览器帧原理剖析[28] Accurately measuring layout on the web[29] Cooperative Scheduling of Background Tasks[30]
第十式:自制hash生成器
项目中,也许我们会遇到需要使用 JS 生成特定长度随机字符串的需求,比如用来做 Hash 值、uuid、随机码等,除了可以借助一些库和插件之外,其实部分场景下,我们完全可以自定义函数实现指定长度随机字符串的生成。
简洁版函数只需要两行代码:
/**
* 生成长度为len的包含a-z、A-Z、0-9的随机字符串
*/
function generateStr(len = 18) {
// 一行代码生成0-9、A-Z、a-z、总长度为62的字符数组
var arr = [...new Array(62)].map((item, i) =>
String.fromCharCode(i + (i < 10 ? 0 : i < 36 ? 7 : 13) + 48)
);
return [...new Array(len)]
.map(() => arr[Math.floor(Math.random() * arr.length)])
.join('');
}
generateStr(18);
如果担心重复,则可以添加一个Map
来缓存已经生成的字符串,每次返回时判断一下:
/**
* 生成长度为len的包含a-z、A-Z、0-9的随机字符串
*/
const cacheMap = new Map(); // 缓存已经生成过了的字符串
// 一行代码生成0-9、A-Z、a-z、总长度为62的字符数组
const arr = [...new Array(62)].map((item, i) =>
String.fromCharCode(i + (i < 10 ? 0 : i < 36 ? 7 : 13) + 48)
);
function generateStr(len = 18) {
const str = [...new Array(len)]
.map(() => arr[Math.floor(Math.random() * arr.length)])
.join('');
if (cacheMap.has(str)) {
// 这里会有死循环的问题,比如下面的for循环,i设置的大于62
console.log(cacheMap, str);
// i 值越大,len越小,重复的概率越大
return generateStr(len);
} else {
cacheMap.set(str, true);
return str;
}
}
for (let i = 0; i < 20; i++) {
// 长度选小一点,测试20次
// i设置的大于62会出现死循环,可先算出排列组合数进行预防
// i 值越大,len越小,重复的概率越大,执行时间越长
generateStr(1);
}
console.log(cacheMap);
1 行代码生成指定长度数字:这种方法有缺点,低概率会出现位数不足的问题(原因是 0.00566 * 100000 = 566,会丢失前面的 0),不推荐使用。
// len 最多16,可能出现
function generateNum(len = 16) {
return Math.floor(Math.random() * Math.pow(10, len));
}
2 行代码生成包含大小写字母和数字的随机字符串[31]
第十一式:如何在离开页面时发送请求?
用户卸载网页的时候(关闭浏览器、刷新浏览器或者跳转其他页面时),有时需要向服务器发送一些统计数据;同时,前端在做异常监控、统计页面访问时长时,也会需要在页面崩溃、关闭的时候发送请求。很自然的做法是在 unload
事件或 beforeunload
事件的监听函数里面,使用 XMLHttpRequest
对象发送数据。但是,这样做不是很可靠,因为 XMLHttpRequest
对象是异步发送,很可能在它即将发送的时候,页面和相关资源已经卸载,会引起 function not found
的错误,从而导致发送取消或者发送失败。
解决方法就是 AJAX
通信改成同步发送,即只有发送完成,页面才能卸载。但是,很多浏览器已经不支持同步的 XMLHttpRequest
对象了(即 open()
方法的第三个参数为 false
):
window.addEventListener('unload', logData, false);
function logData() {
var client = new XMLHttpRequest();
// 第三个参数表示同步发送
client.open('POST', '/log', false);
client.setRequestHeader('Content-Type', 'text/plain;charset=UTF-8');
client.send(analyticsData);
}
同步通信有几种变通的方法:
一种做法是新建一个 <img>
元素,数据放在 src 属性,作为 URL 的查询字符串,这时浏览器会等待图片加载完成(服务器回应),再进行卸载。另一种做法是创建一个循环,规定执行时间为几秒钟,在这几秒钟内把数据发出去,然后再卸载页面。
通过在 unload 事件处理器中,创建一个图片元素并设置它的 src 属性的方法来延迟卸载以保证数据的发送。因为绝大多数浏览器会延迟卸载以保证图片的载入,所以数据可以在卸载事件中发送。
const reportData = (url, data) => {
let img = document.createElement('img');
const params = [];
Object.keys(data).forEach((key) => {
params.push(`${key}=${encodeURIComponent(data[key])}`);
});
img.onload = () => (img = null);
img.src = `${url}?${params.join('&')}`;
};
这些做法的共同问题是,卸载的时间被硬生生拖长了,后面页面的加载被推迟了,用户体验不好。
而Navigator.sendBeacon
就是天生用来解决“网页离开时的请求发送”问题的,该方法可用于通过 HTTP 将少量数据异步传输到 Web 服务器。可以发现,同样是采用异步的方式,但是Navigator.sendBeacon
发出的异步请求是作为浏览器任务执行的,与当前页面是脱钩的。因此该方法不会阻塞页面卸载流程和延迟后面页面的加载。当用户代理成功把数据加入浏览器传输队列时,sendBeacon()
方法将会返回 true
,如果受到队列总数、数据大小的限制后,会返回 false
。返回true
后,只是表示进入了发送队列,浏览器会尽力保证发送成功,但是否成功了,无法判断。
目前 Google Analytics 使用 Navigator.sendBeacon
来上报数据。Navigator.sendBeacon
方法接受两个参数,第一个参数是目标服务器的 URL,第二个参数是所要发送的数据(可选),可以是任意类型(字符串、表单对象、二进制对象等等)。这个方法的返回值是一个布尔值,成功发送数据为 true,否则为 false。该方法发送数据的 HTTP 方法是 POST,可以跨域,类似于表单提交数据。它不能指定回调函数。
window.addEventListener('unload', analytics, false);
function analytics(state) {
if (!navigator.sendBeacon) return;
var URL = 'http://example.com/analytics';
var data = 'state=' + state + '&location=' + window.location;
navigator.sendBeacon(URL, data);
}
sendBeacon
方法具有如下特点:
发出的是异步请求,并且是 POST 请求,后端解析参数时,需要注意处理方式; 发出的请求,是放到的浏览器任务队列执行的,脱离了当前页面,所以不会阻塞当前页面的卸载和后面页面的加载过程,用户体验较好; 只能判断出是否放入浏览器任务队列,不能判断是否发送成功; Beacon API
不提供相应的回调,因此后端返回最好省略response body
。
参考资料
Google Analytics added sendBeacon functionality to Universal Analytics JavaScript API[32] Navigator.sendBeacon 无阻塞发送统计数据[33] Navigator.sendBeacon() —— MDN[34]
第十二式:让 VSCode、浏览器和你心有灵犀
在刚接手比较大型项目时,也许你会经常碰到这样的问题:需要修改一个页面,却苦于对项目结构不熟悉、文件夹结构不规范等,不知道文件在哪个目录下;要改一个 bug,却难以迅速定位到问题所在文件,这时候你是否幻想过,如果可以点击页面上的组件,在 VSCode 中自动跳转到对应文件,并定位到对应行号岂不美哉?
react-dev-inspector [35]就是满足你这些幻想的梦中女神,这个插件允许用户通过简单的点击直接从浏览器 React 组件跳转到本地 IDE 代码。TA 不仅能满足你的幻想,使用起来也是非常简单方便,看完这张动图,懂得都懂 😜:
如果看完图,你还不放心的话,不妨现在就先在在线预览体验[36]地址体验一下(在线体验地址里,激活点击跳转的快捷键是四个按键的组合,不过你完全不用担心,因为这个组合是可以自定义的,你完全可以改成两个按键的组合)。
前面说了,使用方式非常简单,只需三步:
首先,保证你的命令行本身可以通过 code
命令打开 VSCode 编辑器,比如code .
,用 VSCode 打开当前文件夹下的文件;如果没有配置这个,可以参考以下步骤:首先打开 VSCode。 使用 command + shift + p
(注意 window 下使用ctrl + shift + p
) 然后搜索 code,选择install 'code' command in path
。安装 react-dev-inspector
,修改babelrc.js
和webpack.config.ts
文件:
// babelrc.js
export default {
plugins: [
// plugin options docs see:
// https://github.com/zthxxx/react-dev-inspector#inspector-babel-plugin-options
'react-dev-inspector/plugins/babel',
],
};
// webpack.config.ts
import type { Configuration } from 'webpack';
import { launchEditorMiddleware } from 'react-dev-inspector/plugins/webpack';
const config: Configuration = {
/**
* [server side] webpack dev server side middleware for launch IDE app
*/
devServer: {
before: (app) => {
app.use(launchEditorMiddleware);
},
},
};
对项目入口文件进行以下修改:
import React from 'react';
import { Inspector, InspectParams } from 'react-dev-inspector';
const InspectorWrapper =
process.env.NODE_ENV === 'development' ? Inspector : React.Fragment;
export const Layout = () => {
// ...
return (
<InspectorWrapper
// props docs see:
// https://github.com/zthxxx/react-dev-inspector#inspector-component-props
// 这里可以随便配置快捷键,你可以改成两个按键的组合
keys={['control', 'shift', 'command', 'c']}
disableLaunchEditor={false}
onHoverElement={(params: InspectParams) => {}}
onClickElement={(params: InspectParams) => {}}
>
{/*这里是你原来的入口组件jsx*/}
<YourComponent>...</YourComponent>
</InspectorWrapper>
);
};
当然,这个插件目前也支持在Vite2
、create-react-app
、Umi
中使用,接入也都很简单,可以参考react-dev-inspector GitHub 仓库及使用[37]文档。
这个插件的原理,简单说也分为三步:
构建时:添加一个 webpack loader
去遍历编译前的 AST 节点,在 DOM 节点上加上文件路径、名称等相关的信息。使用DefinePlugin
注入项目运行时的根路径,以便后续用来拼接文件路径,打开 VSCode 相应的文件。运行时:需要在项目的最外层包裹 Inspector
组件,用于在浏览器端监听快捷键,弹出 debug 的遮罩层,在点击遮罩层的时候,利用fetch
向本机服务发送一个打开 VSCode 的请求。本地服务:需要启动 react-dev-utils
里的一个中间件,监听一个特定的路径,在本机服务端执行打开 VSCode 的指令,如code src/pages/index.ts
。
如果你对其中的原理很感兴趣,可以参考字节跳动 Web Infra 团队的文章——开发提效——我点了页面上的元素,VSCode 乖乖打开了对应的组件?原理揭秘[38]。
第十三式:让浏览器告诉大家你正在浏览不正经网站
上班滑水摸鱼,同事来了赶紧切换浏览器界面?何不直接让同事以为你在浏览某些“正经”网站?
以下代码,当切换浏览器 tab,使得页面不可见时,会更改不可见页面的 title 和 icon 显示效果,可以直接复制以下代码在控制台尝试。
let interval = null;
(function () {
// 获取icon所在link,rel*="icon"是为了兼容rel="shortcut icon"的情况
const Link = document.querySelector('link[rel*="icon"]');
const sourceTitile = document.title;
const sourceLink = Link.href;
document.addEventListener('visibilitychange', function () {
if (document.hidden) {
// 让title文字动起来,更加醒目
interval = setInterval(scroll, 1000);
// 修改title,这里也可以直接写文字,之所以用编码后的嘛,🤔,是因为不想让你一眼看出代码里下了毒...
document.title = decodeURI(
'%E6%82%A8%E6%AD%A3%E5%9C%A8%E6%B5%8F%E8%A7%88%E9%BB%84%E8%89%B2%E7%BD%91%E7%AB%99...'
);
Link.href =
'https://king-hcj.github.io/images/posts/zhuangbility100/nh.gif?raw=true';
Link.type = 'image/gif';
} else {
clearInterval(interval);
document.title = sourceTitile;
Link.href = sourceLink;
}
});
})();
function scroll() {
// 让title文字动起来,更加醒目
const titleInfo = document.title;
const firstInfo = titleInfo.charAt(0);
const lastInfo = titleInfo.substring(1, titleInfo.length);
document.title = lastInfo + firstInfo;
}
效果图:
第十四式:带有 Id 属性的元素,会创建全局变量
关于 HTML 中的 Id 属性,想必您已经再熟悉不过了:
id 属性规定 HTML 元素的唯一的 id; id 在 HTML 文档中必须是唯一的; id 属性可用作链接锚(link anchor),通过 JavaScript(HTML DOM)或通过 CSS 为带有指定 id 的元素改变或添加样式。
对于 DOM 树中具有 ID 的给定 HTML 元素,可以使用其 ID 作为变量名来检索 div。所以,下面的console.log(foo)
会打印出 id 为 foo 的这个 div DOM 元素:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<div id="foo"></div>
<script type="text/javascript">
console.log(foo); // 会输出id为foo的div 这个 DOM元素
</script>
</body>
</html>
如果您不信,那就此时打开您的网站看看:
那么,这是否也意味着可以在这些浏览器中使用它来替代 getElementById 方法呢?实际项目中最好还是老老实实该怎么写就怎么写,毕竟如果这样,大概率你会被后来的接手者或者同事问候的 😄 ~
Do DOM tree elements with ids become global variables?[39]
第十五式:利用 a 标签解析 url
很多时候我们有从一个 URL 中提取域名,查询关键字,变量参数值等的需要,而万万没想到可以让浏览器方便地帮我们完成这一任务而不用我们写正则去抓取。方法是在 JS 代码里先创建一个 a 标签然后将需要解析的 URL 赋值给 a 的 href 属性,然后就得到了一切我们想要的了。
var a = document.createElement('a');
a.href = 'https://juejin.cn/user/2796746682939054/posts';
console.log(a.host);
利用这一原理,稍微扩展一下,就得到了一个更加健壮的解析 URL 各部分的通用方法了,下面提供一个网上常见的封装示例。
function urlParse(url, key) {
var a = document.createElement('a');
a.href = url;
var result = {
href: url,
protocol: a.protocol.replace(':', ''),
port: a.port,
query: a.search,
params: (function () {
var ret = {},
centArr,
seg = a.search.replace(/^\?/, '').replace(/^\?/, '').split('&');
for (i = 0, len = seg.length; i < len; i++) {
if (!seg[i]) {
continue;
}
centArr = seg[i].split('=');
ret[centArr[0]] = centArr[1];
}
return ret;
})(),
hash: a.hash,
file: (a.pathname.match(/\/([^\/?#]+)$/i) || [, ''])[1],
path: a.pathname.replace(/^([^\/])/, '/$1'),
relative: (a.href.match(/tps?:\/\/[^\/]+(.+)/) || [, ''])[1],
segments: a.pathname.replace(/^\//, '').split('/'),
};
a = null;
return key ? result[key] : result;
}
H5 有新的 API URL 也可以快速的处理一个链接,相对更加简洁。
var url = new URL('https://www.baidu.com/')
url.hash
...
这些鲜为人知的前端冷知识,你都 GET 了吗?
第十六式:一个可以在浏览器运行 Node.js 的神器
在最新的 Google I/O
主题演讲中 stackblitz
向大家介绍了他们与 Next.js
和 Google
团队合作开发的在线 IDE WebContainers
:
几年前,我们意识到网络正朝着一个关键的拐点发展。
WebAssembly
的出现让我们可以有能力编写基于WebAssembly
的操作系统,这个操作系统的功能强大到可以完全在浏览器中运行Node.js
。我们设计了一个比本地环境更快,更安全和一致的高级开发环境,可以实现无缝代码协作,而不需要设置本地环境,两年后的今天,他终于诞生了!
官网声称`StackBlitz.com`[40]是这个星球上最快、最安全的开发环境,它的 logo 也是一个闪电的标识:
WebContainers
允许你创建一个完整的 Node.js
环境,它可以在毫秒内启动,并且可以实现一键联机和链接共享。这个环境具有 VS Code
强大的编辑功能,完整的终端,还有 npm 等功能。它也完全在你的浏览器中运行,这带来了一些关键的好处:
比本地环境快。构建速度比 yarn/npm
快 20%,包安装速度可以快 5 倍。支持在浏览器中调试 Node.js
。与Chrome DevTools
的无缝集成可实现本机后端调试,无需安装扩展。默认安全。所有代码执行都发生在浏览器的安全沙箱中,而不是在远程 VM
或本地二进制文件上。
同样,这些环境不需要在远程服务器上运行。而是每个环境都完全包含在你的 Web 浏览器中。没错:Node.js 运行时本身是第一次在浏览器内部本机运行。你可以在`StackBlitz.com`[41] 上自己尝试一下,下面是我截取的页面截图:
推荐一个神器!可以在浏览器运行 Node.js 在线 IDE WebContainers 体验地址[42]
第十七式:一起富强民主、文明、和谐、自由、平等?
别人写文章都妙笔生花,我上个网,我上个网,鼠标点过的地方都“富强、民主、文明、和谐、自由、平等”,我骄傲了吗?
复制以下代码到控制台执行,然后,开始点击你的页面吧~ 😎
(function () {
var playWords = [
'富强',
'民主',
'文明',
'和谐',
'自由',
'平等',
'公正',
'法制',
'爱国',
'敬业',
'诚信',
'友善',
], // 点击展示的词库
colors = ['#ff4545', '#3eff00'], // 颜色库
wordIdx = Math.floor(Math.random() * playWords.length); // 随机取词下标
document.body.addEventListener('click', function (e) {
// 监听点击事件
if (e.target.tagName == 'A') {
// a标签
return;
}
var x = e.pageX,
y = e.pageY, // 获取点击位置
span = document.createElement('span'); // 创建展示playWords的span
span.textContent = playWords[wordIdx];
wordIdx = (wordIdx + 1) % playWords.length;
color = colors[Math.floor(Math.random() * colors.length)]; // 随机取色
span.style.cssText = [
'z-index: 9999; position: absolute; top: ',
y - 20,
'px; left: ',
x,
'px; font-weight: bold; color: ',
color,
].join('');
document.body.appendChild(span);
renderWords(span);
});
function renderWords(el) {
var i = 0,
top = parseInt(el.style.top);
var playTimer = setInterval(function () {
if (i > 180) {
clearInterval(playTimer);
el.parentNode.removeChild(el);
} else {
i += 3;
el.style.top = top - i + 'px';
el.style.opacity = (180 - i) / 180;
}
}, 16.7);
}
})();
参考资料:前端随便玩儿[43]
本文首发于个人博客[44],欢迎指正和star[45]。
参考资料
一个能让你的网站 high 起来的 js: https://link.segmentfault.com/?url=https%3A%2F%2Floli-rbq.top%2Fcarnival%2F
[2]JavaScript Puzzlers!: https://link.segmentfault.com/?url=http%3A%2F%2Fjavascript-puzzlers.herokuapp.com%2F
[3]JavaScript Puzzlers!: https://link.segmentfault.com/?url=http%3A%2F%2Fjavascript-puzzlers.herokuapp.com%2F
[4]44 个 Javascript 变态题解析 (上): https://link.segmentfault.com/?url=https%3A%2F%2Fgithub.com%2Fxiaoyu2er%2Fblog%2Fissues%2F1
[5]44 个 Javascript 变态题解析 (下): https://link.segmentfault.com/?url=https%3A%2F%2Fgithub.com%2Fxiaoyu2er%2Fblog%2Fissues%2F3
[6]JavaScript Puzzlers!: https://link.segmentfault.com/?url=http%3A%2F%2Fjavascript-puzzlers.herokuapp.com%2F
[7]百度百科: https://link.segmentfault.com/?url=https%3A%2F%2Fbaike.baidu.com%2Fitem%2F%E5%B8%AE%E6%B1%AA%E5%B3%B0%E4%B8%8A%E5%A4%B4%E6%9D%A1%2F15079279
[8]深入浅出 contenteditable 富文本编辑器: https://link.segmentfault.com/?url=https%3A%2F%2Fzhuanlan.zhihu.com%2Fp%2F37051858
[9]CSS user-modify 属性行为表现测试实例页面: https://link.segmentfault.com/?url=https%3A%2F%2Fwww.zhangxinxu.com%2Fstudy%2F201601%2Fuser-modify.html
[10]小 tip: 如何让 contenteditable 元素只能输入纯文本: https://link.segmentfault.com/?url=https%3A%2F%2Fwww.zhangxinxu.com%2Fwordpress%2F2016%2F01%2Fcontenteditable-plaintext-only%2F
[11]前端随便玩儿: https://link.segmentfault.com/?url=https%3A%2F%2Fxiaohuazheng.github.io%2F2018%2F06%2F02%2Ffed-play%2F
[12]前端随便玩儿: https://link.segmentfault.com/?url=https%3A%2F%2Fxiaohuazheng.github.io%2F2018%2F06%2F02%2Ffed-play%2F
[13]devtools-detector: https://link.segmentfault.com/?url=https%3A%2F%2Fgithub.com%2FAEPKILL%2Fdevtools-detector
[14]网站如何检测到是否开启开发者工具?: https://segmentfault.com/q/1010000039917621
[15]JS 检测,禁用浏览器开发者工具之 6 大方法探讨: https://link.segmentfault.com/?url=https%3A%2F%2Fblog.csdn.net%2Fcplvfx%2Farticle%2Fdetails%2F108518077
[16]前端开发中如何在 JS 文件中检测用户浏览器是否打开了调试面板(F12 打开开发者工具)?: https://link.segmentfault.com/?url=https%3A%2F%2Fwww.zhihu.com%2Fquestion%2F24188524
[17]网站这样来屏蔽开发者工具,不比监听 MouseDown 舒服?: https://segmentfault.com/a/1190000040157555
[18]JS 禁止打开控制台: https://segmentfault.com/a/1190000021459140
[19]JSBench.Me: https://link.segmentfault.com/?url=https%3A%2F%2Fjsbench.me%2F
[20]Benchmark.js: https://link.segmentfault.com/?url=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fbenchmark
[21]基准测试: https://link.segmentfault.com/?url=https%3A%2F%2Fblog.csdn.net%2Fwoniu317%2Farticle%2Fdetails%2F82560312
[22]jsPerf: https://link.segmentfault.com/?url=https%3A%2F%2Fjsperf.com%2F
[23]w3c 文档: https://link.segmentfault.com/?url=https%3A%2F%2Fwww.w3.org%2FTR%2Frequestidlecallback%2F%23idle-periods
[24]w3c 标准: https://link.segmentfault.com/?url=https%3A%2F%2Fwww.w3.org%2FTR%2Frequestidlecallback%2F%23idle-periods
[25]浏览器是如何工作的:Chrome V8 让你更懂 JavaScript: https://segmentfault.com/a/1190000037435824
[26]47 张图带你走进浏览器的世界: https://segmentfault.com/a/1190000040330317
[27]requestIdleCallback-后台任务调度: https://link.segmentfault.com/?url=http%3A%2F%2Fwww.zhangyunling.com%2F702.html
[28]浏览器帧原理剖析: https://link.segmentfault.com/?url=https%3A%2F%2Fgithub.com%2Fxitu%2Fgold-miner%2Fblob%2Fmaster%2FTODO1%2Fthe-anatomy-of-a-frame.md
[29]Accurately measuring layout on the web: https://link.segmentfault.com/?url=https%3A%2F%2Fnolanlawson.com%2F2018%2F09%2F25%2Faccurately-measuring-layout-on-the-web%2F
[30]Cooperative Scheduling of Background Tasks: https://link.segmentfault.com/?url=https%3A%2F%2Fwww.w3.org%2FTR%2Frequestidlecallback%2F
[31]2 行代码生成包含大小写字母和数字的随机字符串: https://link.segmentfault.com/?url=http%3A%2F%2Fblog.haoji.me%2Fgenerate-random-string.html
[32]Google Analytics added sendBeacon functionality to Universal Analytics JavaScript API: https://link.segmentfault.com/?url=https%3A%2F%2Fwww.thyngster.com%2Fgoogle-analytics-added-sendbeacon-functionality-universal-analytics-javascript-api
[33]Navigator.sendBeacon 无阻塞发送统计数据: https://link.segmentfault.com/?url=https%3A%2F%2Fblog.csdn.net%2Fu012193330%2Farticle%2Fdetails%2F102778979
[34]Navigator.sendBeacon() —— MDN: https://link.segmentfault.com/?url=https%3A%2F%2Fdeveloper.mozilla.org%2Fzh-CN%2Fdocs%2FWeb%2FAPI%2FNavigator%2FsendBeacon
[35]react-dev-inspector : https://link.segmentfault.com/?url=https%3A%2F%2Fgithub.com%2Fzthxxx%2Freact-dev-inspector
[36]在线预览体验: https://link.segmentfault.com/?url=https%3A%2F%2Freact-dev-inspector.zthxxx.me%2F
[37]react-dev-inspector GitHub 仓库及使用: https://link.segmentfault.com/?url=https%3A%2F%2Fgithub.com%2Fzthxxx%2Freact-dev-inspector
[38]开发提效——我点了页面上的元素,VSCode 乖乖打开了对应的组件?原理揭秘: https://link.segmentfault.com/?url=https%3A%2F%2Fjuejin.cn%2Fpost%2F6901466406823575560
[39]Do DOM tree elements with ids become global variables?: https://link.segmentfault.com/?url=https%3A%2F%2Fstackoverflow.com%2Fquestions%2F3434278%2Fdo-dom-tree-elements-with-ids-become-global-variables
[40]StackBlitz.com
: https://link.segmentfault.com/?url=https%3A%2F%2Fstackblitz.com%2F
StackBlitz.com
: https://link.segmentfault.com/?url=https%3A%2F%2Fstackblitz.com%2F
在线 IDE WebContainers 体验地址: https://link.segmentfault.com/?url=https%3A%2F%2Fstackblitz.com%2F
[43]前端随便玩儿: https://link.segmentfault.com/?url=https%3A%2F%2Fxiaohuazheng.github.io%2F2018%2F06%2F02%2Ffed-play%2F
[44]个人博客: https://link.segmentfault.com/?url=https%3A%2F%2Fking-hcj.github.io%2F2021%2F08%2F01%2FJavaScript-108-tips4%2F
[45]指正和star: https://link.segmentfault.com/?url=https%3A%2F%2Fgithub.com%2Fking-hcj%2Fking-hcj.github.io