首页
Search
1
Linux 下 Bash 脚本 bad interpreter 报错的解决方法
69 阅读
2
Arch Linux 下解决 KDE Plasma Discover 的 Unable to load applications 错误
52 阅读
3
Arch Linux 下解决 KDE Plasma Discover 的 Unable to load applications 错误
42 阅读
4
如何在 Clash for Windows 上配置服务
40 阅读
5
如何在 IOS Shadowrocket 上配置服务
40 阅读
clash
服务器
javascript
全部
游戏资讯
登录
Search
加速器之家
累计撰写
1,061
篇文章
累计收到
0
条评论
首页
栏目
clash
服务器
javascript
全部
游戏资讯
页面
搜索到
1061
篇与
的结果
2024-10-20
如何在博客的评论系统中使用随机头像
我的博客评论系统经过重写后,不再使用微博、GitHub 等第三方系统的登录方式了。而是用户输入昵称和邮箱后,就可以进行评论。那么这样存在的一个问题就是头像如何获取呢?之前使用微博、GitHub 等第三方授权登录后,可以获取到用户在授权系统里的头像。怎么设置头像呢? 提现设置好多个头像,用户注册评论时,随机选取一个作为用户的头像; 使用后台服务,根据邮箱的 hash 值,产生点的随机分布的图片。 我们应该也在某些论坛或者博客里,看到一些方块组成的头像,就是按照第 2 种方法实现的默认头像。 头像服务 # 这两种方案都是可以使用的,不过还有一个更简单的方案:使用gravatar系统里的头像,根据邮箱的 hash 值就能获取到头像了: 不过这个头像网站在国内访问比较慢,也就出现了很多代理的网站,我博客目前使用的是:https://gravatar.loli.net网站。如果没有这个网站上注册过头像的话,那么获取到的就是默认头像。还没有注册的话,赶快注册一个吧:设置gravatar 设置随机头像 # 当评论中全是默认的话,也不好看。gravatar 也提供了一个参数,可以将默认头像转为其他的随机头像,这样既能有默认头像了,也能区分用户了。 s:尺寸,像素为单位 d:风格,目前可选 identicon、monsterid、wavatar、retro、robohash 等 我的博客里采用了monsterid的风格: 各位可以评论下,然后看看自己是什么头像!
2024年10月20日
4 阅读
0 评论
0 点赞
2024-10-20
基于 websocket 的多端桥接平台
我们现在的业务是基于新闻客户端实现的,都要经过新闻客户端的环境,进行前后端数据上的交互。但是我们在调试过程中,非常的不方便。通常使用的工具有:modheader, postman, fiddler 等,但这些工具都会存在的问题: 缺少客户端里相应的设备信息; 即使将 cookie 信息复制出来,也是存在过期的问题; 多个设备之间切换时不方便; 针对这些存在的问题和不足,我基于 websocket 双向通信的特点,并实现了“多端桥接管理平台”:通过在 PC 端上的操作,可以直接在新闻客户端内直接执行相应的命令,并将结果、cookie、设备信息等一起返回到 PC 端。 1. 要调试什么 # 我们主要要知道调试什么,最终回去到什么样子的结果: 调试接口,传入接口地址,即可获取对应的结果;并且可以同时调试多个设备; 调试 jsapi,输入对应的方法,则即可在新闻客户端中展示出效果。 在调试接口方面,其实我们有一种方法可以方便地进行调试,但有两个限制条件:Android系统和测试版的客户端,这样通过 Chrome 浏览器进行桥接。但这种方式,在 iOS 系统和正式版的客户端中,就失效了。 2. websocket 的特性 # WebSocket 协议的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。其他特点包括: 建立在 TCP 协议之上,服务器端的实现比较容易。 与 HTTP 协议有着良好的兼容性。默认端口也是 80 和 443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。 数据格式比较轻量,性能开销小,通信高效。 可以发送文本,也可以发送二进制数据。 没有同源限制,客户端可以与任意服务器通信。 协议标识符是 ws(如果加密,则为 wss),服务器网址就是 URL。 3. 建立 socket 连接 # 为了满足我们在第 1 部分设置的调试目标,我们这里要实现的功能有: PC 端相当于房主,建立房间后,其他设备可以进入到该房间,一个设备只能进入到一个房间中; 客户端有断线重连的机制,当客户端断开连接后,可以尝试重连; 服务端维护一个心跳检测的机制,当有新设备进入或者之前的设备退出时,要及时地更新当前房间中的设备列表; 3.1 如何创建房间 # 在浏览器上输入房间的标识,若浏览器与服务端成功建立起 websocket 连接后,则在浏览器端创建对应的二维码。用微信/手 Q 或者其他扫描二维码的设备进行扫描,即可通过提前设定的 scheme 协议,跳转到新闻客户端里对应的调试页面。若客户端里也与服务端成功建立 websocket 连接后,则相当于进入房间成功,PC 端会出现一个对应的图标。ws.open(serverId) .then(() => { // PC 端成功建立连接后 setStatus("linked"); // 更新页面的状态 // 生成二维码 qrcode(`/tools/index.html#/newslist?serverId=${serverId}`).then(url => { setCodeUrl(url); }); }) .catch(e => { // 建立连接失败 console.error(e); Modal.error({ title: "当前服务器出现问题啦,正在抢修中" }); setStatus("unlink"); }); 3.2 客户端的断线重现机制 # 在移动端中的页面有个特点,当屏幕黑屏后,或者因为其他的原因,客户端会自动断开 socket 连接。为了方便进行调试,而不是每次在断开连接后,需要手动点击,或者重新进入页面。我在这里实现了一个简单的断线重连机制。websocket 连接断开时,会执行onclose的回调,因此,我们可以在 onclose 事件中进行再次重连的机制。同时,为了防止无限制的重连尝试,我在这里也进行了下限制,最多重连 3 次,3 次后还没有重新连接上,则停止连接;若重连成功,则将重连次数重置为 3。断开连接时:// 断开连接时 ws.onclose(() => { timer = setTimeout(() => { setStatus("unlink"); setCodeUrl(""); }, 500); reconnectNum--; // 限制重连的次数 if (reconnectNum >= 0) { _open(); // 尝试重新连接 } }); 连接成功时:ws.open(serverId).then(() => { // PC 端成功建立连接后 + reconnectNum = 3; + timer && clearTimeout(timer); setStatus("linked"); // 更新页面的状态 // 生成二维码 qrcode(`/tools/index.html#/newslist?serverId=${serverId}`).then(url => { setCodeUrl(url); }); }); 3.3 心跳检测 # 就像我们在 QQ 群里聊天一样,哪个人在线要一目了然,若有人进入到聊天群,或者有人退出了,都要通知房主,并及时地更新群列表。心跳检测主要有 2 种方式:客户端发起的心跳检测和服务端维护的心跳检测。我们稍微讲解下这两种: 客户端发起的心跳:每隔一段固定的时间,向服务器端发送一个 ping 数据,如果在正常的情况下,服务器会返回一个 pong 给客户端,如果客户端通过 onmessage 事件能监听到的话,说明请求正常。 服务端维护的心跳:每隔一段时间,检测所有连接的状态,若状态为断开时,则将其从列表中剔除。 我在这里使用的是服务端维护的心跳检测,当房间里的设备数量发生变化时,则服务端向客户端推送最新的设备列表:// 持续监测客户端的连接状态 // 若已断开连接,则将客户端清除 let aliveClients = new Map(); let lastAliveLength = new Map(); setInterval(() => { let clients = {}; wss.clients.forEach(function each(ws) { if (ws.isAlive === false) { return ws.terminate(); } const serverId = ws.serverId; if (clients[serverId]) { clients[serverId].push(ws); } else { clients[serverId] = [ws]; } ws.isAlive = false; ws.ping(() => {}); }); for (let serverId in clients) { aliveClients.set(serverId, clients[serverId]); const length = clients[serverId].length; // 若当前serverId连接的设备数量发生变化,则发送消息 if (length !== lastAliveLength.get(serverId)) { // 想当前所有serverId的设备发送消息 sendAll("devices", clients[serverId], serverId); // 存储上次当前serverId的连接数 lastAliveLength.set(serverId, length); } } const size = wss.clients.size; console.log("connection num: ", size, new Date().toTimeString()); }, 2000); 4. 进行接口的调试 # 我们在第 3 节已经成功把 PC 端和新闻客户端连接起来了,那么怎么进行双端数据的通信? 4.1 接口的调试 # 我们在这里要传入 3 个字段: serverId: 即房间号,服务端要将信息广播给所有带有 serverId 的成员; type: 类型,这条指令是要做什么的; msg: 传入的参数; 在接口调试的过程中,则传入的参数是:const params = { type: "post", // 类型 msg: { // 参数 url: "https://api.prize.qq.com/v1/newsapp/answer/share/oneQ?qID=506336" } }; 当客户端正常完成接口的请求后,则将接口结果、cookie 和设备信息等返回到 PC 端:// 请求的方法 const post = url => { if (window.TencentNews && window.TencentNews.post) { window.TencentNews.post(url, {}, window[id], { loginType: "qqorweixin" }, {}); } else if (window.TencentNews && window.TencentNews.postData) { window.TencentNews.postData(url, '{"a":"b"}', id, "requestErrorCallback"); } }; // 移动端向服务端发起的数据 ws.send({ type: "postCb", // 执行的结果 msg: { method: "post", result, cookie: document.cookie, appInfo } }); 这样就能在前端展示出结果了,而且是真实的数据请求。 4.2 历史记录的存储 # 历史记录这块,我们周边的同学在试用的过程中,还是非常迫切需要的需求。要不然每次要测试之前的接口地址时,都需要重新输入或者粘贴,非常不方便。我们把用户请求的 URL、返回的结果、cookie、设备信息等比较完整的信息存储到 boss 中,而本地只存储历史的 URL,当用户需要再次测试之前的接口时,点击一下即可。若需要查看之前调试的接口,可以去鹰眼上进行查看。本地采用的是localStorage的方式进行存储。还有更重要的是,我们也使用mobx的响应式工具,能够在用户完成这次请求后,马上在侧边的历史记录里看到结果。 5. 新闻客户端内 jsapi 的调试 # 除了可以调试接口外,还可以进行一些新闻客户端内的 jsapi 调试。我们新闻客户端的 jsapi 有两种调用的方式:// 直接调用 window.TencentNews.login("qqorweixin", isLogined => console.log(isLogined)); // invoke方式调用 window.TencentNews.invoke("login", "qqorweixin", isLogined => console.log(isLogined)); 这里我选择了使用invoke的方式来调用 jsapi。PC 端发起 jsapi 的调用:ws.send({ type: "call", msg: { method: method, params: slice.call(arguments) } }); 移动端在收到服务端发过来的请求后,进行 jsapi 的调用,并将执行的结果返回到 PC 端即可:const handleNewsApi = async (msg: any): Promise => { await tencentReady(); const { method, params } = msg; return new Promise(resolve => { window.TencentNews.invoke(method, ...params, (result: any) => { resolve({ method, result }); }); }); }; 6. 总结 # 到这里,我的“基于 websocket 的多端桥接平台”基本上已经构建完毕了。不过还是有 2 个问题要简要的说明下。 6.1 为什么要手动输入 serverId # 最开始想着用户创建房间时,由系统随机产生一个 uuid,但后来想,如果用户刷新页面了,这个 uuid 就会发生变化,导致无法连接到之前的 uuid,所以这里就换成了手动输入。 6.2 如何保证一个客户端的 socket 请求都进入到同一个进程中 # 当我们后台采用多个进程时,若用户的请求我们不做干预,会造成请求的随机访问,产生 400 的请求,毕竟最开始连接在 A 进程中,现在发起的请求到 B 进程中,B 进程不知道怎么处理了。这里有多种方式可以进行处理: 方法 介绍 优点 缺点 一致性 hash 算法 所有的主机和连接都分配到 0 ~ 2^32-1 的虚拟圆中 1. 适用在大规模的应用;2. 某个主机或者进程挂掉后,影响小 实现比较复杂 nginx 分配 自带的 ip_hash 可实现负载均衡;同一 ip 会被分配给固定的后端服务器 配置方便 可能会集中到某个进程中 我这里的平台是内部的调试平台,用户量不大,杀鸡焉用牛刀,而且我们只有一台机器,因此我们考虑的是同一个 IP 进入到同一个进程中。这里我借用里 nginx 中的 ip_hash 思想:当请求来到主进程后,我这里对 IP 进行加权计算后,然后按照进程的个数进行取模。显然这种方式也有可能存在一个进程中 socket 连接过多的问题,不过在用户量不多的时候完全可以接受(针对这个问题我也考虑了别的方法,例如瀑布流的方式,每次给子进程分配连接的时候,都首先获取到连接数最少的那个进程,然后连接分配给这个进程,不过还要维护一个表,每次都要计算)。 6.4 多进程之间的通信 # 同一个房间里,当 PC 端的 socket 连接和多个移动端的连接不在同一个进程中时,就会存在跨进程的问题。一个极端的例子,每个 socket 连接都在不同的进程中,那么就要考虑如何通知其他的进程,需要给客户端发送请求了。比较简单的方式利用我们的机制,每个 PC 端的用户就是房主,可以创建一个房间,移动设备就是房间中的成员,每个房间都是独立的,互不干扰。这样我们把房间里所有的 socket 连接,通过房间的标识,都放到同一个进程中,这样就没有跨进程的问题了。但这种方式存在的一个问题是:一个房间里的连接过多时,都需要这同一个进程来承担,而别的进程却闲着的。还有可以使用 redis:利用 redis 的发布/订阅者模式,将当前进程中的房间标识和信息广播到其他的进程中,其他进程中有相同房间标识的 socket 连接,进行相应的操作。
2024年10月20日
5 阅读
0 评论
0 点赞
2024-10-20
深入理解 node 中的 crypto 加密模块
我们在日常的业务中经常会遇到这样的场景: 对比两个文件的内容是否相同; 生成 token; 密码保护; 加密和解密数据; 等等,有各种各样的需要加密的场景。在 node 中也有原生的 crypto 模块,该模块提供了 hash、hmac、加密解密、签名、验证功能等一整套的封装。使用const crypto = require('crypto');即可引入该模块。 1. hash 算法 # hash 算法也被称为摘要算法,该算法可以将任意长度的数据,转换为固定长度的 hash 值,这种方式具有不可逆性。你可以把一本小说转换为 hash 数据,但无法从这 hash 数据再逆转回一本小说。因此,若要获取 hash 的原数据,只能靠字典碰撞。该算法通常在文本校验、存储密码时用的比较多。虽然摘要算法会用于密码的存储,但严格来说,摘要算法不算做是加密算法。使用getHashes()方法,可以获取到所有支持的 hash 算法:crypto.getHashes(); 获取到一个数组:[ "RSA-MD4", "RSA-MD5", "RSA-MDC2", "RSA-RIPEMD160", "RSA-SHA1", "RSA-SHA1-2", "RSA-SHA224", "RSA-SHA256", "RSA-SHA3-224", "RSA-SHA3-256", "RSA-SHA3-384", "RSA-SHA3-512", "RSA-SHA384", "RSA-SHA512", "RSA-SHA512/224", "RSA-SHA512/256", "RSA-SM3", "blake2b512", "blake2s256", "id-rsassa-pkcs1-v1_5-with-sha3-224", "id-rsassa-pkcs1-v1_5-with-sha3-256", "id-rsassa-pkcs1-v1_5-with-sha3-384", "id-rsassa-pkcs1-v1_5-with-sha3-512", "md4", "md4WithRSAEncryption", "md5", "md5-sha1", "md5WithRSAEncryption", "mdc2", "mdc2WithRSA", "ripemd", "ripemd160", "ripemd160WithRSA", "rmd160", "sha1", "sha1WithRSAEncryption", "sha224", "sha224WithRSAEncryption", "sha256", "sha256WithRSAEncryption", "sha3-224", "sha3-256", "sha3-384", "sha3-512", "sha384", "sha384WithRSAEncryption", "sha512", "sha512-224", "sha512-224WithRSAEncryption", "sha512-256", "sha512-256WithRSAEncryption", "sha512WithRSAEncryption", "shake128", "shake256", "sm3", "sm3WithRSAEncryption", "ssl3-md5", "ssl3-sha1", "whirlpool" ] 这么多 hash 算法,我们平时用的比较多的是md5, sha1, sha256, sha512。这里把同一个文本,按照不同的摘要算法来生成 hash 值:// text 要摘要的文本 // hashtype 摘要的算法 function createHash(text, hashtype) { const hash = crypto.createHash(hashtype).update(text).digest("hex"); console.log(hashtype, hash, hash.length); } hashes.forEach((type) => { createHash("蚊子", type); }); 生成的结果:md5 37725295ea78b626efcf77768be478cb 32 sha1 21f226b5a07ed3f74e6ae07e994f36d6a9bf6fac 40 sha256 a200ce289b67afbfb6fbc3d7dd33f7ef493daef64fb159c2e48e8534a0289a9b 64 sha512 b88bd9eac191f58e06c99c256bbcfdf2945aa94b47d5e0242be1f0739bf4adccebf4753e9f38f92603fe3f52f331121540c1dda2ed91796410abcfe49a677fba 128 不同的算法,生成的 hash 值的长度也不一样,碰撞成功的难度也越大。同时,update方法不止可以接收字符串,还可以接收 stream 流:const filename = "./node-crypto.md"; const hash = crypto.createHash("sha1"); const fsStream = fs.createReadStream(filename); fsStream.on("readable", () => { // 哈希流只会生成一个元素。 const data = fsStream.read(); if (data) { hash.update(data); } else { // 数据接收完毕后,输出hash值 console.log(`${hash.digest("hex")} ${filename}`); } }); 既然可以接收 stream 流的格式,那么就使用 pipe 管道进行处理:const filename = "./node-crypto.md"; const hash = crypto.createHash("sha1"); const fsStream = fs.createReadStream(filename); fsStream.pipe(hash).pipe(process.stdout); hash 后传给下个管道进行处理,不过这里输出的通常会是乱码,因此这里我们自己写一个可写流:const { Writable } = require("stream"); const write = Writable(); write._write = function (data, enc, next) { // 将流中的数据写入底层 process.stdout.write(hash.digest("hex") + "\n"); // 写入完成时,调用`next()`方法通知流传入下一个数据 process.nextTick(next); }; fsStream.pipe(hash).pipe(write); // 正常输出hash值 2. hmac 算法 # 我们先看下 hmac 算法的用法:const result = crypto.createHmac("sha1", "123456").update("蚊子").digest("hex"); console.log(result); // 0bdd6c1192e321e34887d965c1140be4361ada65 hmac 算法与 hash 算法的调用方式很像,但createHmac()方法这里多了一个参数,这个参数相当于密钥。密钥不一样,即使要加密的文本一样,生成的结果也会不一样。function createHmac() { const text = "蚊子"; const key = Math.random().toString().slice(-6); const result = crypto.createHmac("sha1", key).update(text).digest("hex"); console.log(text, key, result); } let n = 10; while (n--) { createHmac(); } 生成的结果:蚊子 508028 486d1f539e4bb8adfd601fd6a3302fae74043bfe 蚊子 644233 dcd6501e6eee9e1462625b50c1ff91c613559b35 蚊子 479257 752945c62b87ce1edb24661103b65e612bb849b7 蚊子 445857 0c6399758a2348ea31bc778f87f503b050e036d5 蚊子 954174 a78ff9d4301bb09d249db9fa6c9a3a28c04acff7 蚊子 629736 b7fd4d3836363f029dd9009f51ad6c14280987c1 蚊子 343366 7a8cadf5dd620f8c82315f38de1f6dc60bfc5336 蚊子 168627 cc51e4531449642a5a10357cbf8f206319fb1b1f 蚊子 103054 49b1ad9dc2de5da2cd67dc892f51718aa9475a05 蚊子 477238 82615006638be235a220bcfdee0705b5cc6551fc 由此看到,hmac 算法相当于加盐版的 hash 算法,但内部具体的实现原理,恕在下才疏学浅,实在是没看懂:github-node-crypto-hmac。这种算法实现密码存储就非常的合适,碰撞成功的概率大大减少。在数据库中,我们可以这样存储:{ "username": "蚊子", "password": "486d1f539e4bb8adfd601fd6a3302fae74043bfe", "key": "508028" } 即使脱库得到了这些数据,反向获取到原密码的机会也非常的低。在 stream 流的操作上,hmac 算法和 hash 算法的用法一样。 3. 对称加密和解密算法 # 前面的两种方法都是不可逆的 hash 加密算法,这里我们介绍下可加密和可解密的算法。常见的对称加密算法有aes和des。crypto 模块中提供了createCipheriv和createDecipheriv来进行加密和解密的功能。之前的 createCipher 和 createDecipher 在 10.0.0 版本已经废弃了,我们这里以新的方法为例,写下加密和解密的算法。这两个方法都接收 3 个参数: algorithm:加密解密的类型; key: 加密解密的密钥:密钥必须是 8/16/32 位,如果加密算法是 128,则对应的密钥是 16 位,如果加密算法是 256,则对应的密钥是 32 位; iv: 初始向量,规则与 key 一样 key 和 iv 两个参数都必须是 'utf8' 编码的字符串、Buffer、 TypedArray 或 DataView。 key 可以是 secret 类型的 KeyObject。 如果密码不需要初始化向量,则 iv 可以为 null。加密的算法:function encode(src, key, iv) { let sign = ""; const cipher = crypto.createCipheriv("aes-128-cbc", key, iv); // createCipher在10.0.0已被废弃 sign += cipher.update(src, "utf8", "hex"); sign += cipher.final("hex"); return sign; } 解密的算法:function decode(sign, key, iv) { let src = ""; const cipher = crypto.createDecipheriv("aes-128-cbc", key, iv); src += cipher.update(sign, "hex", "utf8"); src += cipher.final("utf8"); return src; } 使用方法:const key = "37725295ea78b626"; // Buffer.from('37725295ea78b626', 'utf8'); const iv = "efcf77768be478cb"; // Buffer.from('efcf77768be478cb', 'utf8'); // console.log(key, iv); const src = "hello, my name is wenzi! my password is `etu^&&*(^123)`"; const sign = encode(src, key, iv); const _src = decode(sign, key, iv); console.log("key: ", key, "iv: ", iv); console.log("原文:", src); console.log("加密后: ", sign); console.log("解密后: ", _src); // key: 37725295ea78b626 iv: efcf77768be478cb // 原文: hello, my name is wenzi! my password is `etu^&&*(^123)` // 加密后: ce6dc873bfd5a5ae6fe0b2bb3f3de46fb9fc15e0ffc75d12286871dbfa3ed185b3ebf60b8e16dd0057eb0750e897347abeddf5a2741944d5a307ceb25c181276 // 解密后: hello, my name is wenzi! my password is `etu^&&*(^123)` 4. 非对称加密算法 # 我们刚才了解了下对称加密,即加密和解密用的都是相同的密钥。非对称加密相对来说,比对称加密更安全,用公钥加密的内容,必须通过对应的私钥才能解密。双方传输信息时,可以使用先使用对方的公钥进行加密,然后对方再使用自己的私钥解开即可。我们先用创建一个私钥:openssl genrsa -out rsa_private.key 1024 然后根据私钥创建对应的公钥:openssl rsa -in rsa_private.key -pubout -out rsa_public.key 这里我们就可以进行非对称的加密和解密了:const crypto = require("crypto"); const fs = require("fs"); const pub_key = fs.readFileSync("./rsa_public.key"); const priv_key = fs.readFileSync("./rsa_private.key"); const text = "hello, my name is 蚊子"; const secret = crypto.publicEncrypt(pub_key, Buffer.from(text)); const result = crypto.privateDecrypt(priv_key, secret); console.log(secret); // buffer格式 console.log(result.toString()); // hello, my name is 蚊子 使用publicEncrypt进行公钥的加密过程,使用privateDecrypt进行私钥的解密过程。 5. 签名 # 在网络中传输的数据,除可使用 Cipher 类进行数据加密外,还可以对数据生成数字签名,以防止在传输过程中对数据进行修改。签名的过程与非对称加密的过程正好相反,是使用私钥进行加密签名,然后使用公钥进行解密的签名验证。const crypto = require("crypto"); const fs = require("fs"); const pub_key = fs.readFileSync("./rsa_public.key"); const priv_key = fs.readFileSync("./rsa_private.key"); const text = "hello, my name is 蚊子"; // 生成签名 const sign = crypto.createSign("RSA-SHA256"); sign.update(text); const signed = sign.sign(priv_key, "hex"); // 验证签名 const verify = crypto.createVerify("RSA-SHA256"); verify.update(text); const verifyResult = verify.verify(pub_key, signed, "hex"); console.log("sign", signed); // ca364a6e31c1f540737ba3efb1ddf7fa2a087c5c11efe52a9e1f2c88b1fd1e0e50f12da4f22362fdfc3d77f3f538995a27a8206d250dba3572510dfcb33064f48685b96f2b2393f56de4958448cec92a4299434aa3318efe418e166b38100bc3a1d1a9310a510087021da0f66a817043ddfd2fb88db76eb2ace480c17a7f732f console.log("verifyResult", verifyResult); // true 生成签名的 sign 方法有两个参数,第一个参数为私钥,第二个参数为生成签名的格式,最后返回的 signed 为生成的签名(字符串)。验证签名的 verify 方法有三个参数,第一个参数为公钥,第二个参数为被验证的签名,第三个参数为生成签名时的格式,返回为布尔值,即是否通过验证。 6. 总结 # 我从简单的 hash 算法,到对称加密,最后到非对称加密和签名,都有了个大致的了解。后续我们也会对 node 的其他模块进行深入的理解。
2024年10月20日
2 阅读
0 评论
0 点赞
2024-10-20
前端中的 hash 和 history 路由
我们在使用 Vue 或者 React 等前端渲染时,通常会有 hash 路由和 history 路由两种路由方式。 hash 路由:监听 url 中 hash 的变化,然后渲染不同的内容,这种路由不向服务器发送请求,不需要服务端的支持; history 路由:监听 url 中的路径变化,需要客户端和服务端共同的支持; 我们一步步实现这两种路由,来深入理解下底层的实现原理。我们主要实现以下几个简单的功能: 监听路由的变化,当路由发生变化时,可以作出动作; 可以前进或者后退; 可以配置路由; 1. hash 路由 # 当页面中的 hash 发生变化时,会触发hashchange事件,因此我们可以监听这个事件,来判断路由是否发生了变化。window.addEventListener( 'hashchange', function (event) { const oldURL = event.oldURL; // 上一个URL const newURL = event.newURL; // 当前的URL console.log(newURL, oldURL); }, false ); 1.1 实现的过程 # 对 oldURL 和 newURL 进行拆分后,就能获取到更详细的 hash 值。我们这里从创建一个 HashRouter 的 class 开始一步步写起:class HashRouter { currentUrl = ''; // 当前的URL handlers = {}; getHashPath(url) { const index = url.indexOf('#'); if (index >= 0) { return url.slice(index + 1); } return '/'; } } 事件hashchange只会在 hash 发生变化时才能触发,而第一次进入到页面时并不会触发这个事件,因此我们还需要监听load事件。这里要注意的是,两个事件的 event 是不一样的:hashchange 事件中的 event 对象有 oldURL 和 newURL 两个属性,但 load 事件中的 event 没有这两个属性,不过我们可以通过 location.hash 来获取到当前的 hash 路由:class HashRouter { currentUrl = ''; // 当前的URL handlers = {}; constructor() { this.refresh = this.refresh.bind(this); window.addEventListener('load', this.refresh, false); window.addEventListener('hashchange', this.refresh, false); } getHashPath(url) { const index = url.indexOf('#'); if (index >= 0) { return url.slice(index + 1); } return '/'; } refresh(event) { let curURL = '', oldURL = null; if (event.newURL) { oldURL = this.getHashPath(event.oldURL || ''); curURL = this.getHashPath(event.newURL || ''); } else { curURL = this.getHashPath(window.location.hash); } this.currentUrl = curURL; } } 到这里已经可以实现获取当前的 hash 路由,但路由发生变化时,我们的页面应该进行切换,因此我们需要监听这个变化:class HashRouter { currentUrl = ''; // 当前的URL handlers = {}; // 暂时省略上面的代码 refresh(event) { // 当hash路由发生变化时,则触发change事件 this.emit('change', curURL, oldURL); } on(evName, listener) { this.handlers[evName] = listener; } emit(evName, ...args) { const handler = this.handlers[evName]; if (handler) { handler(...args); } } } const router = new HashRouter(); rouer.on('change', (curUrl, lastUrl) => { console.log('当前的hash:', curUrl); console.log('上一个hash:', lastUrl); }); 1.2 调用的方式 # 到这里,我们把基本的功能已经完成了。来配合一个例子就更形象了:// 先定义几个路由 const routes = [ { path: '/', name: 'home', component: , }, { path: '/about', name: 'about', component: , }, { path: '*', name: '404', component: , }, ]; const router = new HashRouter(); // 监听change事件 router.on('change', (currentUrl, lastUrl) => { let route = null; // 匹配路由 for (let i = 0, len = routes.length; i < len; i++) { const item = routes[i]; if (currentUrl === item.path) { route = item; break; } } // 若没有匹配到,则使用最后一个路由 if (!route) { route = routes[routes.length - 1]; } // 渲染当前的组件 ReactDOM.render(route.component, document.getElementById('app')); }); 查看【hash 路由的样例】。 2. history 路由 # 在 history 路由中,我们一定会使用window.history中的方法,常见的操作有: back():后退到上一个路由; forward():前进到下一个路由,如果有的话; go(number):进入到任意一个路由,正数为前进,负数为后退; pushState(obj, title, url):前进到指定的 URL,不刷新页面; replaceState(obj, title, url):用 url 替换当前的路由,不刷新页面; 调用这几种方式时,都会只是修改了当前页面的 URL,页面的内容没有任何的变化。但前 3 个方法只是路由历史记录的前进或者后退,无法跳转到指定的 URL;而pushState和replaceState可以跳转到指定的 URL。如果有面试官问起这个问题“如何仅修改页面的 URL,而不发送请求”,那么答案就是这 5 种方法。如果服务端没有新更新的 url 时,一刷新浏览器就会报错,因为刷新浏览器后,是真实地向服务器发送了一个 http 的网页请求。因此若要使用 history 路由,需要服务端的支持。 2.1 应用的场景 # pushState 和 replaceState 两个方法跟 location.href 和 location.replace 两个方法有什么区别呢?应用的场景有哪些呢? location.href 和 location.replace 切换时要向服务器发送请求,而 pushState 和 replace 仅修改 url,除非主动发起请求; 仅切换 url 而不发送请求的特性,可以在前端渲染中使用,例如首页是服务端渲染,二级页面采用前端渲染; 可以添加路由切换的动画; 在浏览器中使用类似抖音的这种场景时,用户滑动切换视频时,可以静默修改对应的 URL,当用户刷新页面时,还能停留在当前视频。 2.2 无法监听路由的变化 # 当我们用 history 的路由时,必然要能监听到路由的变化才行。全局有个popstate事件,别看这个事件名称中有个 state 关键词,但pushState和replaceState被调用时,是不会触发触发 popstate 事件的,只有上面列举的前 3 个方法会触发。可以点击【popState 不会触发 popstate 事件】查看。针对这种情况,我们可以使用window.dispatchEvent添加事件:const listener = function (type) { var orig = history[type]; return function () { var rv = orig.apply(this, arguments); var e = new Event(type); e.arguments = arguments; window.dispatchEvent(e); return rv; }; }; window.history.pushState = listener('pushState'); window.history.replaceState = listener('replaceState'); 然后就可以添加对这两个方法的监听了:window.addEventListener('pushState', this.refresh, false); window.addEventListener('replaceState', this.refresh, false); 2.3 完整的代码 # 完整的代码如下:class HistoryRouter { currentUrl = ''; handlers = {}; constructor() { this.refresh = this.refresh.bind(this); this.addStateListener(); window.addEventListener('load', this.refresh, false); window.addEventListener('popstate', this.refresh, false); window.addEventListener('pushState', this.refresh, false); window.addEventListener('replaceState', this.refresh, false); } addStateListener() { const listener = function (type) { var orig = history[type]; return function () { var rv = orig.apply(this, arguments); var e = new Event(type); e.arguments = arguments; window.dispatchEvent(e); return rv; }; }; window.history.pushState = listener('pushState'); window.history.replaceState = listener('replaceState'); } refresh(event) { this.currentUrl = location.pathname; this.emit('change', location.pathname); document.querySelector('#app span').innerHTML = location.pathname; } on(evName, listener) { this.handlers[evName] = listener; } emit(evName, ...args) { const handler = this.handlers[evName]; if (handler) { handler(...args); } } } const router = new HistoryRouter(); router.on('change', function (curUrl) { console.log(curUrl); }); 使用方法与上面的 hash 路由一样,这里就不多赘述了。点击查看【history 路由的实现应用】我们腾讯新闻中的抢金达人活动,就是采用的这种路由方式,页面的首次渲染采用服务端直出,二级跳转页面,使用前端 history 路由的前端渲染方式。 3. 总结 # 至此,两种路由的原理和实现方式都介绍完毕了,欢迎拍砖。
2024年10月20日
3 阅读
0 评论
0 点赞
2024-10-20
如何控制多个 toast 提示的展示
我们在平时会用弹窗或者 toast 对用户进行提示,而弹窗是属于强提示类型的(需要用户点击等交互后弹窗才消失),提供的信息量也大,而 toast 提示则属于弱提示类型(无论用户有没有看到,反正我提示了),仅进行小小的提醒。弹窗类型的交互通常可以多个弹窗叠加,用户关闭一个后,再看下一个;而 toast 提示通常在同一时间只显示一个,那么如果同时多个 toast 需要提示时,怎么办? ant design 中的做法是同时将多个 toast 放到一个容器里(没有该容器则创建,有则直接使用),从上往下都展示出来【ant design message】, 我们是为了跟新闻客户端 APP 的提示更加贴近,同一时间只弹一个,其他的先缓存起来。 我们接下来把这两种方式都实现一下。 1. 先实现一个简单的 toast # 我们先来实现一个简单的 toast,然后再说如何控制多个 toast 的先后提示。// 在浏览器上模拟toast提示 export const toast = ( text: string | number, duration: number = 1800, container: HTMLElement = document.body ): Promise => { toastInstance = true; let div: HTMLElement = document.createElement('div'); div.className = 'toast'; div.style.cssText = `position: relative; margin-top: 20px; padding: 12px 20px; border-radius: 5px; background-color: rgba(255, 255, 255, 0.96); color: #111111; font-size: 16px; line-height: 1.5; white-space: nowrap; text-align: center;`; div && (div.innerHTML = text.toString()); container.appendChild(div); return new Promise((resolve) => { setTimeout(() => { div && div.parentNode && div.parentNode.removeChild(div); // 固定时间后消失 toastInstance = false; resolve(); }, duration); }); }; 用 div 模拟一个 toast。 2. 展示多个 toast 的方式 # 2.1 将多个 toast 都展示出来 # 这里我们的重点是要创建一个 toast 所在的唯一的容器,然后给容器一个定位:let toastContainer: any = null; export const getToastContainer = () => { if (toastContainer) { return toastContainer; } toastContainer = document.createElement('div'); toastContainer.style.cssText = `position: fixed; top: 20px; left: 50%; transform: translate3d(-50%, 0, 0); z-index: 9999;`; document.body.appendChild(toastContainer); return toastContainer; }; 最后给一个总入口,把单个 toast 放在容器里:const message = (text) => { getToastContainer(); toast(text, 1800, toastContainer); }; 可以查看 demo 效果:类 ant design 的 toast 提示。 2.2 同时只显示一个 toast 提示 # 上面的提示方式在 PC 端还可以,但是在移动端的小屏幕上,同时显示多个会显得比较拥挤,因此我们这里同时最多只显示一个 toast 提示,上一个消失之后,再展示下一个 toast 提示。这里我们要对toast稍微改造下: 同一时间只能创建一个 toast,因此添加一个 toastInstance 变量进行控制; 修改 toast 的样式; + let toastInstance = false; const toast = (text, duration = 1800, container = document.body) => { + if (toastInstance) { + return; + } + toastInstance = true; let div = document.createElement('div'); div.className = 'toast'; div.style.cssText = `position: fixed; padding: 12px 20px; border-radius: 5px; + top: 20px; + left: 50%; + transform: translate3d(-50%, 0, 0); background-color: rgba(255, 255, 255, 0.96); box-shadow: 0 0 2px #666666; color: #111111; font-size: 16px; line-height: 1.5; white-space: nowrap; text-align: center;`; div && (div.innerHTML = text.toString()); container.appendChild(div); return new Promise((resolve) => { setTimeout(() => { div && div.parentNode && div.parentNode.removeChild(div); // 固定时间后消失 + toastInstance = false; resolve(); }, duration); }); }; 然后通过一个方法和缓存来控制 toast 的显示:let toastList = []; // 后面的提示先缓存起来 const toastWaterFall = async (text) => { if (toastInstance) { toastList.push(text); // 若当前toast正在进行,则缓存 } else { await toast(text); // 否则展示toast提示 if (toastList.length) { while (toastList.length) { await sleep(300); // 延迟一段时间 await toast(toastList.shift() + ''); } } } }; 可以点击链接查看 demo:同时最多只展示一个 toast 提示。 3. 总结 # 关于 toast 的设计还有很多要考虑的问题,这里我们也是探讨了如何控制多个 toast 的展示问题。
2024年10月20日
2 阅读
0 评论
0 点赞
1
...
38
39
40
...
213