01 什么是防抖和节流,他们的应用场景有哪些

在 Issue 中交流与讨论: 01 什么是防抖和节流,他们的应用场景有哪些​[1]

防抖 (debounce)

防抖,顾名思义,防止抖动,以免把一次事件误认为多次,敲键盘就是一个每天都会接触到的防抖操作。

想要了解一个概念,必先了解概念所应用的场景。在 JS 这个世界中,有哪些防抖的场景呢

  1. 登录、发短信等按钮避免用户点击太快,以致于发送了多次请求,需要防抖
  2. 调整浏览器窗口大小时,resize 次数过于频繁,造成计算过多,此时需要一次到位,就用到了防抖
  3. 文本编辑器实时保存,当无任何更改操作一秒后进行保存

代码如下,可以看出来​「防抖重在清零 clearTimeout(timer)​​

function debounce (f, wait) {
let timer
return (...args) => {
clearTimeout(timer)
timer = setTimeout(() => {
f(...args)
}, wait)
}
}

节流 (throttle)

节流,顾名思义,控制水的流量。控制事件发生的频率,如控制为 1s 发生一次,甚至 1 分钟发生一次。与服务端(server)及网关(gateway)控制的限流 (Rate Limit) 类似。

  1. ​scroll​​ 事件,每隔一秒计算一次位置信息等
  2. 浏览器播放事件,每个一秒计算一次进度信息等
  3. input 框实时搜索并发送请求展示下拉列表,每隔一秒发送一次请求 (也可做防抖)

代码如下,可以看出来​「节流重在加锁 timer=timeout​​

function throttle (f, wait) {
let timer
return (...args) => {
if (timer) { return }
timer = setTimeout(() => {
f(...args)
timer = null
}, wait)
}
}

总结 (简要答案)

  • 防抖:防止抖动,单位时间内事件触发会被重置,避免事件被误伤触发多次。​「代码实现重在清零 clearTimeout​​​。防抖可以比作等电梯,只要有一个人进来,就需要再等一会儿。业务场景有避免登录按钮多次点击的重复提交。
  • 节流:控制流量,单位时间内事件只能触发一次,与服务器端的限流 (Rate Limit) 类似。​「代码实现重在开锁关锁 timer=timeout; timer=null​​​。节流可以比作过红绿灯,每等一个红灯时间就可以过一批。

02 在前端开发中,如何获取浏览器的唯一标识

​ 更多描述: 如何获取浏览器的唯一标识,原理是什么 ​

在 Issue 中交流与讨论: 02 在前端开发中,如何获取浏览器的唯一标识​[2]

由于不同的系统显卡绘制 ​​canvas​​​ 时渲染参数、抗锯齿等算法不同,因此绘制成图片数据的 ​​CRC​​ 校验也不一样。

function getCanvasFp () {
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
ctx.font = '14px Arial'
ctx.fillStyle = '#ccc'
ctx.fillText('hello, shanyue', 2, 2)
return canvas.toDataURL('image/jpeg')
}

因此根据 ​​canvas​​ 可以获取浏览器指纹信息。

  1. 绘制​​canvas​​​,获取​​base64​​ 的 dataurl
  2. 对 dataurl 这个字符串进行​​md5​​ 摘要计算,得到指纹信息

但是对于常见的需求就有成熟的解决方案,若在生产环境使用,可以使用以下库

  • fingerprintjs2​[3]

它依据以下信息,获取到浏览器指纹信息,​「而这些信息,则成为 component​​

  1. ​canvas​
  2. ​webgl​
  3. ​UserAgent​
  4. ​AudioContext​
  5. 对新式 API 的支持程度等
requestIdleCallback(function () {
Fingerprint2.get((components) => {
const values = components.map((component) => component.value)
const fp = Fingerprint2.x64hash128(values.join(''), 31)
})
})

在 ​​fingerprintjs2​​​ 中,对于 ​​component​​ 也有分类

  • browser independent component​[4]​:有些​​component​​ 同一设备跨浏览器也可以得到相同的值,有些独立浏览器,得到不同的值
  • stable component​[5]​: 有些​​component​​ 刷新后值就会发生变化,称为不稳定组件

在实际业务中,可根据业务选择合适的组件

const options = {
excludes: {userAgent: true, language: true}
}

简答

根据 ​​canvas​​ 可以获取浏览器指纹信息

  1. 绘制​​canvas​​​,获取​​base64​​ 的 dataurl
  2. 对 dataurl 这个字符串进行​​md5​​ 摘要计算,得到指纹信息

若在生产环境使用,可以使用 fingerprintjs2​[6]​,根据业务需求,如单设备是否可跨浏览器,以此选择合适的 ​​component​

03 在服务端应用中如何获得客户端 IP

在 Issue 中交流与讨论: 03 在服务端应用中如何获得客户端 IP​[7]

「如果有 x-forwarded-for​​ 的请求头,则取其中的第一个 IP,否则取建立连接 socket 的 remoteAddr。」

而 ​​x-forwarded-for​​ 基本已成为了基于 proxy 的标准 HTTP 头,格式如下,可见第一个 IP 代表其真实的 IP,可以参考 MDN X-Forwarded-For​[8]

X-Forwarded-For: 203.0.113.195, 70.41.3.18, 150.172.238.178
X-Forwarded-For: <client>, <proxy1>, <proxy2>

以下是 ​​koa​​ 获取 IP 的方法

get ips() {
const proxy = this.app.proxy;
const val = this.get(this.app.proxyIpHeader);
let ips = proxy && val
? val.split(/\s*,\s*/)
: [];
if (this.app.maxIpsCount > 0) {
ips = ips.slice(-this.app.maxIpsCount);
}
return ips;
},

get ip() {
if (!this[IP]) {
this[IP] = this.ips[0] || this.socket.remoteAddress || '';
}
return this[IP];
},

参见源码: https://github.com/koajs/koa/blob/master/lib/request.js#L433

04 js 如何全部替代一个子串为另一个子串

​ 更多描述: 假设有一个字符串 `hello. hello. hello. ` 需要替换为 `AAA`,即把 `hello. ` 替换为 `A` ​

在 Issue 中交流与讨论: 04 js 如何全部替代一个子串为另一个子串​[9]

如果需要全量替换字符串,可以使用 ​​String.prototype.replace(re, replacer)​​​,其中正则表达式需要开启 ​​global​​ flag

const s = 'foo foo foo'
s.replce(/foo/g, 'bar')

那如题中,​「是否可以使用正则表达式来替代子串」

答:​「不可以,因为使用子串构建正则时,有可能有特殊字符,就有可能出现问题」​,如下

// 期待结果: 'AhelloX hello3 '
> 'hello. helloX hello3 '.replace(new RegExp('hello. ', 'g'), 'A')
< "AAA"

而在 ​​javascript​​​ 中替换子串只能使用一种巧妙的办法:​​str.split('foo').join('bar')​

> 'hello. hello. hello. '.split('hello. ').join('A')
< "AAA"

真是一个巧(笨)妙(拙)的办法啊!!!!!​「大概 TC39 也意识到了一个问题,于是出了一个新的 API」​,在 ​​ESNext​​ 中

String.prototype.replaceAll()

'aabbcc'.replaceAll('b', '.');
// 'aa..cc'

详细文档在 String.prototype.replaceAll​[10]

总结(及直接答案)

两种办法

  • ​str.split('foo').join('bar')​
  • ​str.replaceAll('foo', 'bar')​​​,在​​ESNext​​ 中,目前支持性不好

05 如何获取一个进程的内存并监控

​ 更多描述: 在编写脚本时,有时会出现内存过大发生 OOM 的事情,那我们如何得知某个进程的内存?另外又如何监控它 ​

在 Issue 中交流与讨论: 05 如何获取一个进程的内存并监控​[11]

通过 ​​ps​​ 可以获知一个进程所占用的内存

$ ps -O rss -p 3506
PID RSS S TTY TIME COMMAND
3506 6984 S pts/1 00:00:00 vim

如果要监控内存,肯定使用对进程万能的命令 ​​pidstat​​ (PS: 这名字一听就知道是干嘛的)

## -r 显示内存信息
## -p 指定 pid
## 1: 每个一秒打印一次
$ pidstat -r -p 3506 1
Linux 3.10.0-957.21.3.el7.x86_64 (shanyue) 11/04/19 _x86_64_ (2 CPU)

20:47:35 UID PID minflt/s majflt/s VSZ RSS %MEM Command
20:47:36 0 3506 0.00 0.00 139940 6984 0.18 vim
20:47:37 0 3506 0.00 0.00 139940 6984 0.18 vim
20:47:38 0 3506 0.00 0.00 139940 6984 0.18 vim
20:47:39 0 3506 0.00 0.00 139940 6984 0.18 vim
20:47:40 0 3506 0.00 0.00 139940 6984 0.18 vim
20:47:41 0 3506 0.00 0.00 139940 6984 0.18 vim

​pidstat​​​ 是属于 ​​sysstat​​​ 下的 linux 性能工具,但在 mac 中,如何定位内存的变化?此时可以使用万能的 ​​top/htop​

$ htop -p 31796

总结

简而言之,有以下三个命令

  1. ​pidstat -r​
  2. ​htop/top -p​
  3. ​ps -O rss -p​

关于更多指标的监控可以参考我的文章: linux 各项监控指标小记​[12]

06 CORS 如果需要指定多个域名怎么办

在 Issue 中交流与讨论: 06 CORS 如果需要指定多个域名怎么办​[13]

​CORS​​​ 通过控制 ​​Access-Control-Allow-Origin​​ 控制哪些域名可以共享资源,取值如下

Access-Control-Allow-Origin: <origin> | *

其中 ​​*​​​ 代表所有域名,​​origin​​ 代表指定特定域名,那如何设置多个域名了?

此时需要通过代码实现,​「根据请求头中的 Origin​​ 来设置响应头 ​Access-Control-Allow-Origin​​,那 Origin 又是什么东西?

请求头: Origin

并不是所有请求都会自动带上 ​​Origin​​​,在浏览器中带 ​​Origin​​ 的逻辑如下

  1. 如果存在跨域,则带上​​Origin​​,值为当前域名
  2. 如果不存在跨域,则不带​​Origin​

逻辑理清楚后,关于服务器中对于 ​​Access-Control-Allow-Origin​​ 设置多域名的逻辑也很清晰了

  1. 如果请求头不带有​​Origin​​,证明未跨域,则不作任何处理
  2. 如果请求头带有​​Origin​​​,证明跨域,根据​​Origin​​​ 设置相应的​​Access-Control-Allow-Origin: <Origin>​

使用伪代码实现如下:

// 获取 Origin 请求头
const requestOrigin = ctx.get('Origin');

// 如果没有,则跳过
if (!requestOrigin) {
return await next();
}

// 设置响应头
ctx.set('Access-Control-Allow-Origin', requestOrigin)

Vary: Origin

此时可以给多个域名控制 CORS,但此时假设有两个域名访问 ​​static.shanyue.tech​​ 的跨域资源

  1. ​foo.shanyue.tech​​​,响应头中返回​​Access-Control-Allow-Origin: foo.shanyue.tech​
  2. ​bar.shanyue.tech​​​,响应头中返回​​Access-Control-Allow-Origin: bar.shanyue.tech​

看起来一切正常,但如果中间有缓存怎么办?

  1. ​foo.shanyue.tech​​​,响应头中返回​​Access-Control-Allow-Origin: foo.shanyue.tech​​,被 CDN 缓存
  2. bar.shanyue.tech​​,因由缓存,响应头中返回 ​Access-Control-Allow-Origin: foo.shanyue.tech​,跨域出现问题」

此时,​​Vary: Origin​​​ 就上场了,代表为不同的 ​​Origin​​ 缓存不同的资源

总结 (简要答案)

CORS 如何指定多个域名?

「根据请求头中的 Origin​​ 来设置响应头 ​Access-Control-Allow-Origin​​,思路如下

  1. 总是设置​​Vary: Origin​​,避免 CDN 缓存破坏 CORS 配置
  2. 如果请求头不带有​​Origin​​,证明未跨域,则不作任何处理
  3. 如果请求头带有​​Origin​​​,证明浏览器访问跨域,根据​​Origin​​​ 设置相应的​​Access-Control-Allow-Origin: <Origin>​

使用伪代码实现如下

// 获取 Origin 请求头
const requestOrigin = ctx.get('Origin');

ctx.set('Vary', 'Origin')

// 如果没有,则跳过
if (!requestOrigin) {
return await next();
}

// 设置响应头
ctx.set('Access-Control-Allow-Origin', requestOrigin)

相关问题:如何避免 CDN 为 PC 端缓存移动端页面​[14]

07 既然 cors 配置可以做跨域控制,那可以防止 CSRF 攻击吗

在 Issue 中交流与讨论: 07 既然 cors 配置可以做跨域控制,那可以防止 CSRF 攻击吗 ​[15]

「对 CORS 一点用也没有」

  1. form​​ 提交不通过 ​CORS​ 检测」​,你可以在本地进行测试
  2. 即使通过​​xhr​​​ 及​​fetch​​ 进行提交被 CORS 拦住,​「但是对于简单请求而言,请求仍被发送」​,已造成了攻击

08 如何避免 CDN 为 PC 端缓存移动端页面

在 Issue 中交流与讨论: 08 如何避免 CDN 为 PC 端缓存移动端页面​[16]

如果 PC 端和移动端是一套代码则不会出现这个问题。​「这个问题出现在 PC 端和移动端是两套代码,却共用一个域名。」

使用 ​​nginx​​ 配置如下,根据 UA 判断是否移动端,而走不同的逻辑 (判断 UA 是否移动端容易出问题)

location / {
// 默认 PC 端
root /usr/local/website/web;

# 判断 UA,访问移动端
if ( $http_user_agent ~* "(Android|webOS|iPhone|iPad|BlackBerry)" ){
root /usr/local/website/mobile;
}

index index.html index.htm;
}

解决方案通常使用 ​​Vary​​ 响应头,来控制 CDN 对不同请求头的缓存。

「此处可以使用 Vary: User-Agent​​ ,代表如果 User-Agent 不一样,则重新发起请求,而非从缓存中读取页面」

Vary: User-Agent

当然,​​User-Agent​​ 实在过多,此时缓存失效就会过多。

简答

使用 ​​Vary: User-Agent​​,根据 UA 进行缓存。

Vary: User-Agent

但最好不要出现这种情况,PC 端和移动端如果是两套代码,建议用两个域名,理由如下

  1. ​nginx​​ 判断是否移动端容易出错
  2. 对缓存不友好

09 如何实现表格单双行条纹样式

在 Issue 中交流与讨论: 09 如何实现表格单双行条纹样式​[17]

通过 ​​css3​​​ 中伪类 ​​:nth-child​​​ 来实现。其中 ​​:nth-child(an+b)​​​ 匹配下标 ​​{ an + b; n = 0, 1, 2, ...}​​ 且结果为整数的子元素

  • ​nth-child(2n)​​​/​​nth-child(even)​​: 双行样式
  • ​nth-child(2n+1)​​​/​​nth-child(odd)​​: 单行样式

其中 ​​tr​​ 在表格中代表行,实现表格中单双行样式就很简单了:

tr:nth-child(2n) {
background-color: red;
}


tr:nth-child(2n+1) {
background-color: blue;
}

同理:

  1. 如何匹配最前三个子元素:​​:nth-child(-n+3)​
  2. 如何匹配最后三个子元素:​​:nth-last-child(-n+3)​

10 简述下 css specificity

在 Issue 中交流与讨论: 10 简述下 css specificity​[18]

​css specificity​​ 即 css 中关于选择器的权重,以下三种类型的选择器依次下降

  1. ​id​​​ 选择器,如​​#app​
  2. ​class​​​、​​attribute​​​ 与​​pseudo-classes​​​ 选择器,如​​.header​​​、​​[type="radio"]​​​ 与​​:hover​
  3. ​type​​​ 标签选择器和伪元素选择器,如​​h1​​​、​​p​​​ 和​​::before​

其中通配符选择器 ​​*​​​,组合选择器 ​​+ ~ >​​​,否定伪类选择器 ​​:not()​​ 对优先级无影响

另有内联样式 ​​<div class="foo" style="color: red;"></div>​​​ 及 ​​!important​​(最高) 具有更高的权重

`:not` 的优先级影响 - codepen​[19]​ 可以看出 ​​:not​​ 对选择器的优先级无任何影响

11 node 中 module.exports 与 exports 有什么区别

在 Issue 中交流与讨论: 11 node 中 module.exports 与 exports 有什么区别​[20]

「一句话:exports​​​module.exports​ 的引用,如果 ​exports​ 没有重赋值,则二者没有任何区别」

类似如下所示

const exports = module.exports

那如下结果会如何导出?

module.exports = 100
exports = 3

很显然会导出 100,毕竟 ​​exports​​ 进行了重赋值。

「那在 node 源码中如何实现的呢?」​ 从源码里可以看出 ​「exports」​ 的实质

精选十几道前端面试题及解答_githubmodule wrapper

详见源码: https://github.com/nodejs/node/blob/master/lib/internal/modules/cjs/loader.js#L1252,可以看出符合猜想

众所周知,node 中所有的模块代码都被包裹在这个函数中

(function(exports, require, module, __filename, __dirname) {
exports.a = 3
});

而以下源码指出,​​exports​​ 是如何得来

const dirname = path.dirname(filename);
const require = makeRequireFunction(this, redirects);
let result;
// 从这里可以看出来 exports 的实质
const exports = this.exports;
const thisValue = exports;
const module = this;
if (requireDepth === 0) statCache = new Map();
if (inspectorWrapper) {
result = inspectorWrapper(compiledWrapper, thisValue, exports,
require, module, filename, dirname);
} else {

// 这里是模块包装函数
result = compiledWrapper.call(thisValue, exports, require, module,
filename, dirname);
}

12 如何获取当前系统中的在线用户数 (并发用户数)

​ 更多描述: 一些 SaaS 系统基于 Pricing 的考虑,会限制团队人数及同时在线数,如何实现 ​

在 Issue 中交流与讨论: 12 如何获取当前系统中的在线用户数 (并发用户数)​[21]

一些 SaaS 系统基于定价策略的考虑,会限制团队人数及同时在线数,如何实现?

通过 ​​redis​​​ 的 ​​zset​​ 可实现并发用户数。

当一个用户请求任何接口时,实现一个 middleware,处理以下逻辑

// 当一个用户访问任何接口时,对该用户Id,写入 zset
await redis.zadd(`Organization:${organizationId}:concurrent`, Date.now(), `User:${userId}`)

// 查询当前机构的并发数
// 通过查询一分钟内的活跃用户来确认并发数,如果超过则抛出特定异常
const activeUsers = await redis.zrangebyscore(`Organization:${organizationId}:concurrent`, Date.now() - 1000 * 60, Date.now())

// 查出并发数
const count = activeUsers.length

// 删掉过期的用户
await redis.zrembyscore(`Organization:${organizationId}:concurrent`, Date.now() - 1000 * 60, Date.now())

总结

  1. 每当用户访问服务时,把该用户的 ID 写入优先级队列,权重为当前时间
  2. 根据权重(即时间)计算一分钟内该机构的用户数
  3. 删掉一分钟以上过期的用户

13 如何把 json 数据转化为 demo.json 并下载文件

在 Issue 中交流与讨论: 13 如何把 json 数据转化为 demo.json 并下载文件​[22]

json 视为字符串,可以利用 ​​DataURL​​ 进行下载

​Text -> DataURL​

除了使用 DataURL,还可以转化为 Object URL 进行下载

​Text -> Blob -> Object URL​

可以把以下代码直接粘贴到控制台下载文件

function download (url, name) {
const a = document.createElement('a')
a.download = name
a.rel = 'noopener'
a.href = url
// 触发模拟点击
a.dispatchEvent(new MouseEvent('click'))
// 或者 a.click()
}

const json = {
a: 3,
b: 4,
c: 5
}
const str = JSON.stringify(json, null, 2)

// 方案一:Text -> DataURL
const dataUrl = `data:,${str}`
download(dataUrl, 'demo.json')

// 方案二:Text -> Blob -> ObjectURL
const url = URL.createObjectURL(new Blob(str.split('')))
download(url, 'demo1.json')

总结

  1. 模拟下载,可以通过新建一个​​<a href="url" download><a>​​​ 标签并设置​​url​​​ 及​​download​​ 属性来下载
  2. 可以通过把​​json​​​ 转化为​​dataurl​​ 来构造 URL
  3. 可以通过把​​json​​​ 转换为​​Blob​​​ 再转化为​​ObjectURL​​ 来构造 URL

14 在浏览器中如何监听剪切板中内容

在 Issue 中交流与讨论: 14 在浏览器中如何监听剪切板中内容​[23]

通过 ​​Clipboard API​​​ 可以获取剪切板中内容,但需要获取到 ​​clipboard-read​​ 的权限,以下是关于读取剪贴板内容的代码:

// 是否能够有读取剪贴板的权限
// result.state == "granted" || result.state == "prompt"
const result = await navigator.permissions.query({ name: "clipboard-read" })

// 获取剪贴板内容
const text = await navigator.clipboard.readText()

注: 该方法在 ​​devtools​​ 中不生效


Reference​[1]

01 什么是防抖和节流,他们的应用场景有哪些: https://github.com/shfshanyue/Daily-Question/issues/3

[2]

02 在前端开发中,如何获取浏览器的唯一标识: https://github.com/shfshanyue/Daily-Question/issues/28

[3]

fingerprintjs2: https://github.com/Valve/fingerprintjs2

[4]

browser independent component: https://github.com/Valve/fingerprintjs2/wiki/Browser-independent-components

[5]

stable component: https://github.com/Valve/fingerprintjs2/wiki/Stable-components

[6]

fingerprintjs2: https://github.com/Valve/fingerprintjs2

[7]

03 在服务端应用中如何获得客户端 IP: https://github.com/shfshanyue/Daily-Question/issues/288

[8]

X-Forwarded-For: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For

[9]

04 js 如何全部替代一个子串为另一个子串: https://github.com/shfshanyue/Daily-Question/issues/361

[10]

String.prototype.replaceAll: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/replaceAll

[11]

05 如何获取一个进程的内存并监控: https://github.com/shfshanyue/Daily-Question/issues/4

[12]

linux 各项监控指标小记: https://shanyue.tech/op/linux-monitor.html

[13]

06 CORS 如果需要指定多个域名怎么办: https://github.com/shfshanyue/Daily-Question/issues/364

[14]

如何避免 CDN 为 PC 端缓存移动端页面: https://github.com/shfshanyue/Daily-Question/issues/330

[15]

07 既然 cors 配置可以做跨域控制,那可以防止 CSRF 攻击吗 : https://github.com/shfshanyue/Daily-Question/issues/366

[16]

08 如何避免 CDN 为 PC 端缓存移动端页面: https://github.com/shfshanyue/Daily-Question/issues/330

[17]

09 如何实现表格单双行条纹样式: https://github.com/shfshanyue/Daily-Question/issues/309

[18]

10 简述下 css specificity: https://github.com/shfshanyue/Daily-Question/issues/311

[19]

​:not​​ 的优先级影响 - codepen: https://codepen.io/shanyue/pen/dyGQqBe

[20]

11 node 中 module.exports 与 exports 有什么区别: https://github.com/shfshanyue/Daily-Question/issues/351

[21]

12 如何获取当前系统中的在线用户数 (并发用户数): https://github.com/shfshanyue/Daily-Question/issues/368

[22]

13 如何把 json 数据转化为 demo.json 并下载文件: https://github.com/shfshanyue/Daily-Question/issues/352

[23]

14 在浏览器中如何监听剪切板中内容: https://github.com/shfshanyue/Daily-Question/issues/315

[24]

【Q019】如何实现选中复制的功能: https://github.com/shfshanyue/Daily-Question/issues/20

关注「前端加加」, 第一时间获取优质文章.