首页
Search
1
Linux 下 Bash 脚本 bad interpreter 报错的解决方法
70 阅读
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
如何打造一款高可用的全屏红包雨
我们腾讯新闻在年底前搞了一波大的烟花活动和跨年红包雨活动,我正好负责了全屏红包雨的特效。在活动结束后,我们来复盘下整个红包雨的实现过程,或许对您有什么帮助呢!在实现红包雨时,我们首先要明确实现什么样子的功能,要给上层的调用方什么配置,调用方需要做什么,就能启动这个红包雨。 为提高性能用 canvas 实现,用 div 的话,需要频繁地创建 div 并进行重绘,红包比较多时,可能会特别消耗性能; 每个红包要有不同的降落速度,一样的速度太单调; 确定红包的属性(如速度、位置坐标、大小、放大缩小和旋转的变形数据、是否已被销毁等); 可以手动启动和暂停红包雨; 点击时,告诉调用方是否命中了红包; 接下来我们一步步地来实现一套完整的红包雨。代码较多,请静下心来。 1. 创建红包雨的画布 # 我们设置组件的原则是要开箱即用,让调用方尽可能少的配置就能使用我们的组件。我们要实现红包雨,首先就要创建一个画布来画这个红包雨。class RedpackRain { private rainCtx: CanvasRenderingContext2D | null = null; private containerRect = { width: 0, height: 0, top: 0, left: 0 }; private ratio = window.devicePixelRatio || 3; // 画布放大的倍数 // 在指定容器内创建canvas private creatCanvas = () => { // 传入一个选择器,表示要在哪个里面展示红包雨 const selector: HTMLElement = this.config.selector as HTMLElement; // 获取外层容器的属性,根据容器的属性来设置画布 const { top, left, width, height } = selector.getBoundingClientRect(); // 将容器的属性记下来 // ratio表示放大的倍数,这是为了避免在一些高清屏下canvas模糊的问题 this.containerRect.width = width * this.ratio; this.containerRect.height = height * this.ratio; this.containerRect.top = top; this.containerRect.left = left; // 容器中还没有创建canvas画布时,就创建一个 // 若已经有了,则不再创建 if (selector.getElementsByTagName('canvas').length === 0) { const canvasRain = document.createElement('canvas'); canvasRain.style.cssText = 'position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 2'; canvasRain.width = this.containerRect.width; canvasRain.height = this.containerRect.height; const div = document.createElement('div'); div.style.cssText = 'position: relative; height: 100%'; div.appendChild(canvasRain); selector.appendChild(div); const rainCanvasSelector: HTMLCanvasElement | null = selector.querySelector('.rain-redpack-canvas'); if (rainCanvasSelector) { // 获取画布 this.rainCtx = rainCanvasSelector.getContext('2d'); } } } } 这里其实我们要注意两点: 获取容器的属性(top, left, width, height),width 和 height 是为了设置画布的宽度和高度,而 top 和 left 的值,是为了在计算用户的点击坐标时,要减去容器的 top 和 left,才是用户在画布上的坐标;为了通用性,也是考虑了容器不是全屏的情况; 一个是 ratio 属性,为了避免在一些高清屏下 canvas 模糊的问题,对 canvas 进行了放大处理;那么 canvas 中所有的数据都是等比例放大了,这点在后面判断点击是否命中红包时非常有用,因为红包的坐标、宽度和高度都是等比例处理过了,点击事件产生的 top 和 left 也要先乘以对应的系数后,才能跟红包的坐标对应上。 2. 红包元素 # 在创建完成画布后,我们就可以画红包元素了,在 canvas 中,我们直接画一个图片即可。这里我们分成几个小节来分开将红包元素。 2.1 红包的创建 # 一个红包特有的几个属性:具体代码如下:class Item { redpackId = 1; // 红包标识 x = 100; // 红包的横坐标 y = 200; // 红包的竖坐标 width = 100; height = 150; speed = 10; // 下降的速度,创建后则确定 angle = 2; // 渲染的角度 ratio = 1.1; // 放大的系数 imgurl = ''; // 红包素材的地址 redpackCtx: CanvasRenderingContext2D | null = null; // 画布 constructor({ redpackId, x, y, imgurl, width, height, speedMax, speedMin, redpackCtx, onDestoryed }) { this.redpackId = redpackId; this.x = x; this.y = y; this.width = width; this.height = height; this.speed = (speedMax - speedMin) * Math.random() + speedMin; this.imgurl = imgurl; this.redpackCtx = redpackCtx; this.containerHeight = containerHeight; this.onDestoryed = onDestoryed; const random = Math.random(); const angle = ((random * 30 + 10) * Math.PI) / 180; // 随机一个旋转偏移角度 this.angle = random < 0.5 ? angle : 0 - angle; // 随机向左或者向右旋转 this.ratio = random * 0.4 + 0.8; // 随机放大的效果,0.8~1.2倍之间 } } 虽然整个红包雨中,有的红包落的快,有的红包落的慢,但具体到某一个红包中,我们则在创建时,就已经确定了这个红包的速度,一直到当前红包被销毁。为了不让红包显得过于单调,我们在红包素材固定的情况下,对元素进行了渲染和放大处理:const random = Math.random(); const angle = ((random * 30 + 10) * Math.PI) / 180; // 随机一个旋转偏移角度 this.angle = random < 0.5 ? angle : 0 - angle; // 随机向左或者向右旋转 this.ratio = random * 0.4 + 0.8; // 随机放大的效果,0.8~1.2倍之间 2.2 红包的移动 # 在 canvas 中的移动,其实就是擦除刚才坐标的图像,然后在新的坐标重新画一个。将这个行为一直持续下去,红包就会一直动起来了。class Item { render() { const { redpackCtx } = this; // 还在画布内时 if (this.y < this.containerHeight) { // 绘制下一个位置 const nextx = this.x; // 横轴坐标不变 const nexty = this.y + this.speed; // 根据速度获取下一个竖轴坐标 redpackCtx.save(); redpackCtx.beginPath(); redpackCtx.rect(nextx, nexty, this.width, this.height); redpackCtx.translate(nextx + this.width / 2, nexty + this.height / 2); redpackCtx.scale(this.ratio, this.ratio); redpackCtx.rotate(this.angle); redpackCtx.drawImage(this.redpackImg, -this.width / 2, -this.height / 2, this.width, this.height); redpackCtx.strokeStyle = 'transparent'; redpackCtx.stroke(); redpackCtx.restore(); this.y = nexty; } else { // 超过边界,回调该元素被销毁的事件 // 告诉主流程当前红包已销毁,可以将其剔除 if (typeof this.onDestoryed === 'function') { this.onDestoryed(this.redpackId); } } } } 当红包一直绘制到边界了,还没有被用户点中,则告诉主控制流程,这个红包已经达到边界了,可以将其从列表中删除了。细心的人也发现了,这个 render()只是一次的绘制操作,它怎么能动的起来呢?本来做 demo 的过程中,我是把 requestAnimationFrame 放在红包 Item 中,也就是说每一个红包都有自己完整的一套绘制流程;而且每次移动的过程中,只擦除自己刚才所在的位置,并不擦除整个画布。同时 canvas 中还有一个isPointInPath方法用来判断点击是否在画出的封闭路径内,那么我正好可以用isPointInPath方法用户是否命中了红包?然而这里有一个问题:isPointInPath方法只能判断最后一次绘制的图形,如果画布中有多个红包时,需要把每个红包都挨个儿重新绘制一次,然后一个一个进行判断。这样就会多次的重绘,每个红包运动时,自己都有一个 requestAnimationFrame 在重复绘制;用户点击时,还是会清除重新绘制,如果用户点击比较快,那么画布就需要频繁的擦除和重绘。而且在真机测试的过程中发现,准确率不高,手指明明点中了红包,程序却判定没有点中。体验效果不好。是不是有点跟不上了,别急,还没到最后呢! 3. 主流程 # 针对上面存在的问题,这里我在进行了下一步的优化。主流程中一个变量redpackItemList保存着当前整个画布中正在存在的红包实例,若被点击命中或者降落到查出了画布,则将其移除。 3.1 全局只有一个 rAF 来控制 # 把用 requestAnimationFrame 控制红包下一次绘制的操作,放在了主流程中,整个红包雨只有一个 requestAnimationFrame 来控制所有的红包绘制,每次绘制前,先把整个画布擦除掉,然后循环redpackItemList,调用每个红包实例中的 render()再重新绘制出来。如果 redpackMap 已经不存在某个红包实例,则下次绘制时,则不会再进行绘制。在修改了绘制流程后,我并没有同步修改对点击命中的判断逻辑,发现在 PC 上调试时命中率更低了,isPointInPath不好使了。主要是因为所有的红包都是在一次的渲染中完成的,点击的红包在 for 循环的渲染过程中,非常快地就过去了,根本就没有被做为当时最后一个画布的元素来判断。为了解决判定不准的问题,这里改成了通过坐标进行判断。class RedpackRain { clickListener() { // 获取所有的手指坐标 touchClients.forEach(({ clientX, clientY }) => { // 减去外层容器的偏移,并乘以画布放大的系数 const myClientX = (clientX - left) * this.ratio; const myClientY = (clientY - top) * this.ratio; // 循环当前所有存在画布上的红包坐标 for (const key in this.redpackItemList) { const redpackItem = this.redpackItemList[key]; const { x, y, width, height } = redpackItem; const diff = 14; // 比红包区域大一点,类似于padding let hitedNum = 0; // 被命中的红包个数 if ( myClientX >= x - diff && myClientX = y - diff && myClientY { this.createRedpackItem(); }, this.config.interval); } }); 这里我做了一个比较粗暴的处理,凡是页面可见性发生变化时,都会清除定时器,然后页面可见时再重新创建定时器。 4. 红包雨的使用 # 目前我们已经基本把红包雨的流程跑通了,红包雨已经可以下了。我们就要考虑对外提供什么方法,方便调用者使用了。这里我提供了 3 个方法来供调用者使用。 4.1 start # 启动整个红包雨。用户可能会重复多次启动红包雨,这里我们就要先清除上次的配置,然后重新设置:class RedpackRain { start() { // 先停止上一个 /** * 1. 清除定时器; * 2. 移除点击事件的监听; * 3. 移动页面可见性的监听; * 4. 停止创建红包 & 停止画布渲染; */ this.clear(); // 下面会讲 // 注册画布的点击事件 this.config.selector.addEventListener(this.config.eventType, this.clickListener, false); // 创建红包 this.createRedpackItem(); // 创建一个新的红包雨 this.timer = setInterval(() => { this.createRedpackItem(); }, this.config.interval); this.render(); // 注册监听页面可见性事件 this.pageVisibility?.visibilityChange((isShow) => { if (this.timer) { clearInterval(this.timer); this.timer = null; } if (isShow) { // 创建一个新的红包雨 this.timer = setInterval(() => { this.createRedpackItem(); }, this.config.interval); } }); } } 4.2 clear # 该方法类似于暂停事件,将清除画布中所有的事件和红包。但红包数据还在,若重新 start 时,则还继续上次的红包雨。 4.3 stop # 先调用 clear()方法,然后清除所有的红包数据。当调用该方法后并重新 start 时,则是一场新的红包雨。 4.4 setOptions # 在红包雨的过程中,可能还会有红包雨越来越快的情况,这就需要随时可以修改配置。于是我就提供了一个setOptions方法,可以随时修改任何配置(除所在的容器外):class RedpackRain { setOptions(options) { if (!this.timer) { // 若没有启动红包雨,则无法调用该方法 return console.error('please use start() before setOptions'); } const beforeInterval = this.config.interval; this.createConfig(options); // 当下红包雨的间隔变化后,则重置定时器; // 若间隔不变,则还是使用之前的定时器 if (beforeInterval !== this.config.interval) { this.start(); } } } 由此,我们形成一个完整的类图: 4.5 最终的成果 # 我们把内部的原理,都一步步的实现了,那么来看下最终的效果吧。RedpackRain是一个类,在构建实例时,除selector外,其他均为非必须参数。 参数 是否必填 类型 默认值 说明 selector 是 string 或 HTMLElement 要渲染红包雨的容器可以是选择器也可以是 dom 元素 interval number 1600 下红包雨的间隔,单位毫秒 onClick function(hited: number){} 空 整个红包雨区域的点击,hited 表示命中红包的个数 onMonitor function({fps}){} 空 红包雨的 fps 监控 redpack 红包配置 redpack.speedMin number 10 红包下降速度的最小值 redpack.speedMax number 10 红包下降速度的最大值 redpack.imgUrl string 红包的图片 redpack.width number 192 红包的宽度 redpack.height number 216 红包的高度 bubble 命中红包后的上升气泡的配置 bubble.imgUrl string 气泡的图片 bubble.width number 156 气泡的宽度 bubble.height number 111 气泡的高度 bubble.speed number 5 气泡每帧上升的高度 bubble.opacitySpeed number 0.04 气泡每帧减少的透明度 使用方式:const rain = new RedpackRain({ selector: document.body, interval: 1600, redpack: { speedMin: 10, speedMax: 10, imgUrl: 'https://sola.gtimg.cn/aoi/sola/20201226100322_I1ltnkzJVc.png', width: 126, height: 174, }, bubble: { imgUrl: 'https://sola.gtimg.cn/aoi/sola/20201225103914_2QQ9bXg2rU.png', width: 156, height: 111, speed: 5, opacitySpeed: 0.04, }, onClick: (isHited) => { console.log(isHited); }, onMonitor(monitors) { console.log(monitors); }, }); rain.start(); // rain.stop(); // rain.setOptions({ // interval: 400, // }) 当时线上的效果没有截图,这里就用 demo 样例做参考吧。点击链接查看demo:全屏红包雨。 5. 总结 # 虽然红包雨只是整个活动的一部分而已,但是也学到了很多,尤其是在 canvas 操作和组件的代码组织上。这次贴的代码也有点多,感谢大家能看到最后。
2024年10月20日
2 阅读
0 评论
0 点赞
2024-10-20
前端中 try-catch 捕获不到哪些异常和错误
在开发过程中,我们的目标是 0error,0warning。但有很多因素并不是我们可控的,为了避免某块代码的错误,影响到其他模块或者整体代码的运行,我们经常会使用try-catch模块来主动捕获一些异常或者错误。比如我们在获取 url 中的参数后,对其进行 JSON 解析,这里就要用try-catch包裹一下,因为我们不能保证获取到的参数一定是可以正常解析的:const addparams = getQueryString('_addparams'); if (addparams) { try { const { openid, token } = JSON.parse(addparams); console.log(openid, token); } catch (err) { console.error(err); } } 用户在复制链接的过程中,有可能会有意无意地复制不完全,导致整个参数不完整,JSON.parse无法解析不完整的 json string。为了避免因数据不完整造成的 JSON 解析错误,我们可以将其用try-catch包括起来。 1. try-catch 不能捕获哪些错误 # 我们经常会使用try-catch模块来主动捕获一些异常或者错误,避免此块的代码影响到其他模块或者整体代码的运行。但有些情况,try-catch 并不能捕获到代码中的异常! 1.1 跨域的错误 # 当我们使用 xhr 请求接口,若接口不支持跨域时,浏览器会在控制台提示错误:Access to XMLHttpRequest at 'https://xxxxxxx.qq.com/qq/userInfo' from origin 'https://www.xiabingbao.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource 通过图片中我们可以看到,请求接口时产生了跨域错误,但并没有进入到catch中。之前我在做 canvas 合成图片时,某些图片的域名不支持跨域,本来想的是先直接请求图片,如果图片跨域了,则请求后台接口,后台接口将该图片转为 base64。但在测试的过程中发现,这个跨域的错误,一定会展示出来。可是,我本来就不想展示给调用方,因为我已经设置了兜底的方案了。但用 try-catch 是兜不住这个错误的。当然,这并不是说前端不知道产生了跨域的错误,我们通过 xhr.onerror 的监听,是可以知道 xhr 请求产生了请求错误的。 1.2 异步错误 # 若 try 中异步的模块产生了错误,catch 也是捕获不到的,例如:// setTimeout中的错误 try { setTimeout(function () { throw new Error('error in setTimeout'); // 200ms后会把异常抛出到全局 }, 200); } catch (err) { console.error('catch error', err); // 不会执行 } // Promise中的错误 try { Promise.resolve().then(() => { throw new Error('error in Promise.then'); }); } catch (err) { console.error('catch error', err); } 通过图片中的运行结果可以看到,这两种代码,均没有进入到 catch 模块中。那么我们应该怎么捕获这种异步的错误呢?答案就是把 try-catch 放到异步代码的里面。// 将try-catch放到setTimeout内部 setTimeout(() => { try { throw new Error('error in setTimeout'); } catch (err) { console.error('catch error', err); } }, 200); // 将try-catch放到then内部 Promise.resolve().then(() => { try { throw new Error('error in Promise.then'); } catch (err) { console.error('catch error', err); } }); // 使用Promse自带的catch捕获异常 Promise.resolve() .then(() => { throw new Error('error in Promise.then'); }) .catch((err) => { console.error('Promise.catch error', err); }); Promise 有一个很大的优势是,它自带着异常捕获方法catch(),在 then()方法产生错误导致代码无法运行时,会自动进入到 catch()方法中。因此建议写 Promise 时,都把 catch()写上,否则未捕获的异常,就会冒泡到全局。 1.3 async-await 的异常如何捕获 # async-await的语法糖,可以让我们像写同步代码一样写 Promise,那么在 async-await 中如何捕获异常呢?这里我们通常就会使用try-catch来捕获异常了。const request = async () => { try { const { code, data } = await somethingThatReturnsAPromise(); } catch (err) { console.error('request error', err); } }; 当 somethingThatReturnsAPromise()方法产生 reject 的异常时,就会被 catch 捕获到。当然,async-await 还有一种捕获异常的方式,在通过 await 返回正确数据时,还可以顺带写上catch()捕获异常,当 somethingThatReturnsAPromise()方法异常时,就会自动进入到 catch()方法中:const request = async () => { try { const { code, data } = await somethingThatReturnsAPromise().catch((err) => console.error('catch error', err)); } catch (err) { console.error('request error', err); } }; 但这种捕获异常后,外层的 catch()方法就捕获不到异常了,不再继续向外层冒泡了。正确的做法是,底层模块产生的错误,应当直接抛出给业务层,让业务层决定这个错误怎么处理,而不是直接吞掉。 1.4 多层 try-catch # 多层 try-catch 时,会被最内层的 catch()方法捕获到,然后就不再向外层冒泡:try { try { throw new Error('error'); } catch (err) { console.error('内层的catch', err); // 内层的catch Error: error } } catch (err) { console.error('最外层的catch', error); } 接下来我们理解下 js 中出现的错误类型。 2. 原生的错误类型 # 在了解使用try-catch之前,我们先来了解下 js 中有哪些个原生的错误类型。js 代码在运行时可能产生的错误错误共有 6 种类型: 语法错误(SyntaxError); 类型错误(TypeError); 范围错误(RangeError); eval 错误(EvalError); 引用错误(ReferenceError); URI 错误(URIError); 这些错误类型都继承自Error类。 2.1 语法错误(SyntaxError) # 语法错误,通常是开发者在开发过程,代码语句写的有问题,浏览器无法对其进行解析:const a=; console.log(a); // Uncaught SyntaxError: Unexpected token ';' 2.2 类型错误(TypeError) # 类型错误通常会出现在两种情况: 操作符使用在了不适当的类型变量上,例如对数字类型使用 concat 操作; 操作的变量遇到不可预期的 null 或者 undefined 值: const obj = {}; obj.concat([1]); // Uncaught TypeError: obj.concat is not a function const a = null; a.nickname; // Uncaught TypeError: Cannot read property 'nickname' of null 在编写一些方法供给其他模块调用时,当在检查到参数传入为空或者 null 等空置时,可以抛出TypeError的错误。 2.3 范围错误(RangeError) # 该错误通常是因为传入的参数,超出了规定的范围。例如toFixed()方法可以接受 0-100 范围内的数值,当超过这个范围时,就会抛出该错误。Math.PI.toFixed(105); // Uncaught RangeError: toFixed() digits argument must be between 0 and 100 2.4 eval 错误(EvalError) # 这种错误一般很少会遇到,因为使用 eval 操作时,即使不正当的错误,也会抛出其他类型的错误。new eval(); // Uncaught TypeError: eval is not a constructor eval = 1234; // 正确执行 2.5 引用错误(ReferenceError) # 引用错误表示师徒访问一个未经声明的变量:console.log(nick); // Uncaught ReferenceError: nick is not defined 2.6 URI 错误(URIError) # 该错误通常是一些操作 uri 函数抛出的错误,主要包括:encodeURI(), decodeURI(), encodeURIComponent(), decodeURIComponent(), escape(), unescape()。decodeURIComponent('%'); // Uncaught URIError: URI malformed decodeURIComponent('%23'); // # 正确执行 3. 自定义错误类型 # 对于稍微大点的模块,我们想自定义一些错误类型,通过这些错误类型,就能看出是某个模块抛出的错误。该怎么写呢?我们自定义的错误类型也是要继承自Error类的,实现起来非常简单:class FingerError extends Error { constructor(message) { super(message); this.name = 'FingerError'; // 该错误的名称 Error.captureStackTrace(this, this.constructor); // 获取错误堆栈的信息 } } const err = new FingerError('get name error'); console.error(err); // FingerError: get name error err instanceof FingerError; // true 4. 总结 # 前端中还有很多种产生错误的方式的,我们平时就要注意避免这些错误。我们接下来也可以从错误监控的角度来分析下,如何来监控页面中出现的错误和错误类型。
2024年10月20日
3 阅读
0 评论
0 点赞
2024-10-20
react 开发中的一些小技巧,助你一臂之力
我使用 react 开发已经 1 年的时间了,不能说很精通,不过在使用的过程中,确实领悟和总结了一些小技巧,可以加快我们后续的开发。 1. useState 中的一些技巧 # 我们在使用 react 开发 function comppoent 时,必然绕不开useState,可谓是随处可见,那么 useState 中有哪些坑和需要注意的地方呢? 1.1 useState 是异步的 # 有些使用 react 时间较短的同学,经常会这样写,在调用 set 方法设置 state 的值后,立刻去获取这个值:const [data, setData] = useState(); const func = () => { console.log(data); }; useEffect(() => { setData({ name: '蚊子' }); func(); }, []); 然而在使用 log 输出时,却发现没有变化。这是因为 useState 中的 set 是异步操作,而函数 func 的调用是同步的,这就导致 func()要比 set 先执行。那么如何解决这个异步的问题呢?这里有几种方式可以参考: 1.1.1 用 useEffect 辅助 # useEffect(() => { if (data) { // 当满足条件时,执行func() func(); } }, [data]); 1.1.2 将需要的数据直接传入到 func 方法中 # useEffect(() => { const data = { name: '蚊子' }; setData(data); func(data); }, []); 1.1.3 延时触发 # 延时触发也可以解决这个问题,但是最不建议这种方式,一个异步问题,如果再用一个延时更大的异步操作来兜底,可能还会产生其他一些未知的问题。useEffect(() => { setData({ name: '蚊子' }); setTimeout(func, 10); }, []); 1.2 setTimeout 中的 state 永远是初始值 # 我们想要延迟设置一些数据,例如让一个计数器固定简单的增加+1,我们可能会这样写:const [count, setCount] = useState(0); useEffect(() => { const timer = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(timer); // 注意清除定时器 }, []); 可是运行时我们发现,计数器只是从 0 涨到了 1,然后就再也不增加了,在 setInterval 中的回调函数里打 log,发现回调函数还在执行,可是就是 count 没有发生变化。这是因为 useEffect 的第二个参数依赖项导致的,在我们上面的示例中,依赖项为空数组,这表示在第一次初始化后,内部就再也不发生变化,那么 count 依赖的就永远是第最开始的初始值。可是如何实现在定时器中持续操作 state 呢?这里有几种解决方式可供参考: 1.2.1 将 state 添加到依赖项中 # 刚才我们也说是因为依赖项,导致每次读取都是最开始初始化时的值,那么我们就可以把count添加到依赖项中:const [count, setCount] = useState(0); useEffect(() => { const timer = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(timer); }, [count]); 这样就能实现计数器一直增加了。但这样会导致另一个问题:每次 count 发生变化时,都会先执行clearInterval,然后再重启一个新的定时器setInterval,setInterval 就蜕变成了 setTimeout 了,只执行一次 setInterval 就会被清理掉。因此这里其实换成 setTimeout 即可:useEffect(() => { const timer = setTimeout(() => { setCount(count + 1); }, 1000); return () => clearTimeout(timer); }, [count]); 还有一种更简单粗暴的方式,就是不添加任何依赖项,但这种方式,会导致所有的 state 变化,都会触发 useEffect 的重新执行:useEffect(() => { const timer = setTimeout(() => { setCount(count + 1); }, 1000); return () => clearTimeout(timer); }); 这种没有任何依赖项的方式,建议在组件中只有一个 count 的 state 中使用! 1.2.2 使用 useRef 保存上次的 state # 我们可以利用 useRef 的特性,来保存刚才发生变化的 state 值,下次使用时,可以直接从 useRef 中获取。useRef 中的值并不受依赖项的限制:const [count, setCount] = useState(0); const countRef = useRef(0); // 创建一个ref来保存count的值 useEffect(() => { countRef.current = count; // count发生变化时,将其存储起来 }, [count]); useEffect(() => { const timer = setInterval(() => { setCount(countRef.current + 1); // 每次从countRef.current中获取到最新的值 }, 1000); return () => clearInterval(timer); }, []); 2. useRef 的妙用 # 刚才 useRef 已经小试牛刀,试用了一下。现在我们再来看看它具体的用法。 2.1 保存一切可变的值 # useRef 可以保存一切可变的值,例如上面 state 中的值,同时还有定时器的 timer,requestAnimationFrame 的 id 等。例如在递归的操作中,timer 和 id 每次都是不一样的,当取消的时候,应当取消哪个就很头疼,这里我们就可以使用 useRef 来进行存储:const requestRef = useRef(0); // 开始渲染 const render = () => { console.log('render', Date.now()); requestRef.current = requestAnimationFrame(() => { render(); }); }; // 取消渲染 const cancelRender = () => { cancelAnimationFrame(requestRef.current); }; 2.2 同时保存多个变量 # 我们在上面的例子中,都是只保存一个值,然后再被另一个新值给覆盖掉。可是如果我们想同时保存多个值时,怎么办呢?如果是单纯的数据的话,那把 ref 初始化成数组,然后按照数组的方式操作即可。const listRef = useRef([]); useEffect(() => { const timer = setInterval(() => { listRef.current.push(Date.now()); }, 1000); return () => clearInterval(timer); }, []); 可是我们有一个 dom 元素列表,想把所有的 dom 元素都存储起来?const domListRef = useRef([]); const getDomList = useCallback((dom: HTMLDivElement | null) => { if (dom) { domListRef.push(dom); } }, []); { list.map((item) => ); } 这里要特别注意,getDomList 方法要用useCallback包裹起来,否则每次 state 变化时,都会导致 domListRef 数据的增加! 2.3 自定义 useInterval 的 hook # 我们通过第 1 节和第 2 节了解了 useEffect 中的 setTimeout(setInterval)和 useRef 的表现,基于这两者,我们可以自己实现一个 useTimeout 或者 useInterval。在之前的文章 如何构建自己的 react hooks 里,我们已经实现过 useInterval,现在理解起来更加轻松一些了。useInterval 的实现:const useInterval = (callback, delay) => { const saveCallback = useRef(); useEffect(() => { // 每次callback发生变化时,都将最新的callback保存起来 saveCallback.current = callback; }); useEffect(() => { function tick() { saveCallback.current(); } if (delay !== null) { let id = setInterval(tick, delay); // setInterval只运行ref.current里的方法 return () => clearInterval(id); } }, [delay]); }; 在 useInterval 中执行 state 的 set 操作时,就不用再关心 state 是否会更新的问题了:const [count, setCount] = useState(0); useInterval(() => { setCount(count + 1); }, 1000); 3. createPortal 的骚操作 # 我们在引入组件并渲染时,会按照引入的位置进行渲染。不过对于一些公共组件,例如弹窗、toast、悬浮挂件角标等,不希望这些组件的样式收到父级元素的样式影响(如overflow: hidden,transform(父级元素有 transform 样式时,"position: fixed"会降级为"position: absolute"),z-index等),最好她可以渲染到根目录或者其他指定的目录中。这时应该怎么办呢?我们就用到 createPortal 了,该方法的使用规则非常简单,就 2 个参数: 要渲染的组件; 要挂载的 DOM 节点; 代码说明下:import { createPortal } from 'react'; const Modal = () => { return createPortal( , document.body, ); }; export default Modal; 这个组件可以在任何地方引用,然后渲染时,会被渲染到 document.body 的最后(类似于 appendChild 操作):import Modal from './modal'; const User = () => { return ( ); }; 4. 小结 # 其实在 react 中还有很多的小技巧操作,如果我们熟悉这些小技巧后,就能快速地用 react 搭建我们的应用。如果这些文章对你有帮助,欢迎转发和点“好看”。
2024年10月20日
3 阅读
0 评论
0 点赞
2024-10-20
再见2020,你好2021
又到了一年回头总结的时候了,刚刚过去的 2020 年,真的是不太平的一年的,这一整年都有新冠疫情,而且在将来的几年里,新冠疫情很有可能成为常态。 1. 回顾下最初定下的目标 # 先回顾下在 2020 年年初定下的目标: 再见 2019,你好 2020 ,看看完成的怎么样了。 熟悉了下 node 中的 crypto 加密模块;不过没有完成 Buffer 模块; 目标是完成 leetcode 上 250 道题目,实际上完成了 274 道题,超额完成 24 道; 公众号的粉丝并没有到达预期,主要是公众号的更新频率太不固定了; 本来想了解和涉及下微信小程序的东西,但因工作原因,并没有接触小程序; 博客数量目标是在 2020 年完成 40 篇,但实际上只完成了 26 篇文章,这里还差着不少,还有很大的进步空间; 梳理完之前在 2020 年年初定下的目标完成情况,再来回顾下把没有完成目标的时间,都浪费(用功)在哪儿了? 2. 完成的成就 # 整个 2020 年算是个比较有成就的一年,从开发和融合一些公共基础组件,到搭建我们前端团队的开发和发布体系;从编写团队的文档,到分享自己的文档;一点点点亮自己身上的技能树!下面是公司git仓库记录下的提交记录,可以说是诚意满满,一天最多的时候,能有50个贡献:除开假期外,几乎填满了所有的格子! 2.1 完善团队的基础组件 # 前端的基础公共组件,是一直在迭代中的,不过是 2020 年加快了研发的脚步: 打开和下载 APP 组件:用于打开和下载腾讯新闻等 APP; 分享组件:可以在微信、QQ、新闻客户端中设置分享信息,并且在新闻客户端中还可以单独拉起某个分享渠道,拉起分享面板灯; 保存和分享图片组件:很多活动有分享图片的需求,而图片是根据用户一些信息和行为生成的,封装的这个组件,可以很方便地分享图片; 数据请求组件:我们的活动会在多终端发起接口请求,如在微信、新闻客户端、node 端等,每个终端发起请求的方式都不一样,这里基于 axios 封装了一个通用的数据请求组件,开发者不用再关心请求是从哪个终端发起的; 行为上报组件:为了便于统计活动的流失状况,我封装了一个行为数据上报组件,并接入了多个数据收集系统,后续有时间会将该组件改造成插件的形式; 常用工具组件:封装了一些团队前端开发中常用的工具方法; 登录和获取用户信息组件:封装了一个可以在微信、QQ 和新闻客户端中,登录和获取用户头像、昵称的组件; 前端基本的 UI 组件,不过还很不成熟,目前仅限自己使用中; 每个前端开发者都有一个想拥有自己的 UI 组件库的梦,虽然现在有很多成熟的组件了,但这并妨碍我们开发出一套属于自己的的 UI 库,可以顺便看下我之前的文章 前端工程师如何通过造轮子提高自己。在造轮子的过程中,既培养了我们封装组件的能力,也能让我们了解 UI 组件的开发过程。之后就能更加快速地了解其他成熟的 UI 组件。 2.2 搭建团队的发布体系 # 之前我们是在一个很老的 cms 系统上发布页面,可是这个 cms 系统太老了,所有的操作都是手动操作的,完全没有 CI/CD 的过程中。导致在需要开发和发布一个活动时,全部手工操作。后来虽然有些人通过抓取 cms 的接口,开发了一些自动化的工具,但依然还不方便。随着公司在推动上云的趋势,我们也打通了自动化的构建和发布体系。所有的构建和发布过程中,全部在流水线上完成。开发者不用再在本地构建、然后手动复制 html 页面中,再进行发布。从创建项目到本地启动开发,从发布构建到部署上线,完全实现了自动化的流程。方便快捷,开发者只需要敲几个 git 或者 npm 命令,可以完成一个活动的创建、开发和上线。在发布的过程中只会发布当前已发生变动的项目,其他项目不受影响!并且上线流程也大大简化,只需要将代码推送到对应的分支上,即可完成构建和部署。同时,测试环境、预发布环境和正式环境,天然地形成了隔离。最后,基于我们业务的特点,在发布过程中,会自动地为该项目加上前端性能监控体系。每个发布上线的项目,都自带性能监控体系。 2.3 axios 源码的分析 # 今年着重对数据请求库 axios 进行了深入了解和源码分析,梳理出几篇关于 axios 的文档: 如何实现 axios 的自定义适配器 adapter; axios 源码系列之拦截器的实现; 必然会用到的 axios 中自带的工具方法; axios 源码系列之如何取消请求; 通过 4 篇文章的剖析,对 axios 也有了更多的认识。在接下来的 2021 年,我的目标是自己先试着开发一套 UI 组件,然后熟悉 ant.design 源码。 2.4 晋级终于通过啦 # 答辩晋级这件事儿,对我来说,真的是感触颇多。终于在第 N 多次的答辩之后,晋级成功。每一次的答辩,都是一次磨炼,让自己有更多的思考。平时的工作比较琐碎,那么如何提炼升华,也是对自己的一种要求。而且,用功在平时,看平时有没有特别注意对技术和数据的积累,而不是在答辩时,才临时抱佛脚。这里也看到了一些所谓的互联网黑话:架构设计、稳定性、高可用、必备知识点&技能,复盘,赋能,沉淀,倒逼,落地,串联,协同,反哺,兼容包装,重组,履约,响应,量化,发力,布局,联动,细分梳理,输出,加速,共建,支撑,融合,聚合,解藕,集成对齐,对标,对焦,抓手,拆解,拉通,抽象,摸索,提炼打通,打透,吃透,迁移,分发,分层,分装,穿梭,辐射围绕,复用,渗透,扩展,开拓,漏斗,中台,闭环,打法拉通,纽带,矩阵,刺激,规模,场景,聚焦,维度,格局形态,生态,话术,体系,认知,玩法,体感,感知,调性心智,战役,合力,心力,赛道,因子,模型,载体,横向通道,补位,链路,试点。颗粒度,感知度,方法论,组合拳,引爆点,点线面,精细化,差异化,平台化,结构化,影响力,耦合性,易用性,一致性,端到端,短平快。 生命周期,价值转化,强化认知,资源倾斜,完善逻辑,抽离透传,复用打法,商业模式,快速响应,定性定量,关键路径,去中心化,结果导向,垂直领域,如何收口,归因分析,体验度量,信息屏障。使用上这些词汇后,是不是瞬间觉得高大上了许多。 3. 2021 的目标 # 2021 年也要好好加油,争取在新的一年更上一个新的台阶。 了解不同状态管理组件的区别,如 mobx, redux, useContext 等; ES6 的标准已经发布好几年了,但还是有有些东西不太熟悉,在 2021 年多熟悉熟悉,例如 Proxy, Symbol, Reflect 等; 编写几个 UI 组件,并熟悉 ant.design 的源码; 在保证质量的前提下,希望博客的数量能达到 40 篇以上; 感恩一切帮助过我的人,愿大家在新的一年大家顺顺利利,都能发大财。加油!
2024年10月20日
1 阅读
0 评论
0 点赞
2024-10-20
使用 react 的 hook 实现一个 useRequest
我们在使用 react 开发前端应用时,不可避免地要进行数据请求,而在数据请求的前后都要执行很多的处理,例如 展示 loading 效果告诉用户后台正在处理请求; 若要写 async-await,每次都要 try-catch 进行包裹; 接口异常时要进行错误处理; 当请求比较多时,每次都要重复这样的操作。这里我们可以利用 react 提供的 hook,自己来封装一个useRequest。umi 框架中已经有实现了一个 useRequest 方法,useRequest-umi-hook,他这里实现的功能很多,我们只实现一个基本的功能,其他更多的功能,您可以自己拓展。 1. 搭建一个基本结构 # 在 useRequest 中,我们基于 axios 来封装,不过这里我们不会对外暴露太多的配置,首先我们来明确 useRequest 的输入和输出。要输入的数据: url:要请求的接口地址; data:请求的数据(默认只有 get 方式); config:其他一些配置,可选,如(manual?: boolean; // 是否需要手动触发); 要返回的数据: loading:数据是否在请求中; data:接口返回的数据; error:接口异常时的错误; 2. 具体的实现 # 我们在确定输入和输出的格式后,就可以具体来实现了。 2.1 不带 config 配置的 # 首先定义好输入和输出的数据:const useRequest = (url, data, config) => { const [loading, setLoading] = useState(false); const [result, setResult] = useState(null); const [error, setError] = useState(null); return { loading, result, error, }; }; export default useRequest; 然后就可以添加数据请求了:const useRequest = (url, data, config) => { const [loading, setLoading] = useState(true); const [result, setResult] = useState(null); const [error, setError] = useState(null); const request = async () => { setLoading(true); try { const result = await axios({ url, params: data, method: 'get', }); if (result && result.status >= 200 && result.status { request(); }, []); return { loading, result, error, }; }; 在我们不考虑第三个配置 config 的情况下,这个useRequest就已经可以使用了。const App = () => { const { loading, result, error } = useRequest(url); return ( loading: {loading} {JSON.stringify(result)} ); }; 2.2 添加取消请求 # 我们在请求接口过程中,可能接口还没没有返回到数据,组件就已经被销毁了,因此我们还要添加上取消请求的操作,避免操作已经不存在的组件。关于 axios 如何取消请求,您可以查看之前写的一篇文章:axios 源码系列之如何取消请求。const request = useCallback(() => { setLoading(true); const CancelToken = axios.CancelToken; const source = CancelToken.source(); axios({ url, params: data, method: 'get', cancelToken: source.token, // 将token注入到请求中 }) .then((result) => { setResult(result.data); setLoading(false); }) .catch((thrown) => { // 只有在非取消的请求时,才调用setError和setLoading // 否则会报组件已被卸载还会调用方法的错误 if (!axios.isCancel(thrown)) { setError(thrown); setLoading(false); } }); return source; }, [url, data]); useEffect(() => { const source = request(); return () => source.cancel('Operation canceled by the user.'); }, [request]); 2.3 带有 config 配置的 # 然后,接着我们就要考虑把配置加上了,在上面的调用中,只要调用了 useRequest,就会马上触发。但若在符合某种情况下才触发怎办呢(例如用户点击后才产生接口请求)? 这里我们就需要再加上一个配置。{ "manual": false, // 是否需要手动触发,若为false则立刻产生请求,若为true "ready": false // 当manual为true时生效,为true时才产生请求 } 这里我们就要考虑 useRequest 中触发 reqeust 请求的条件了: 没有 config 配置,或者有 config,但 manual 为 false; 有 config 配置,且 manual 和 ready 均为 true; const useRequest = () => { // 其他代码保持不变,并暂时忽略 useEffect(() => { if (!config || !config.manual || (config.manual && config.ready)) { const source = request(); return () => source.cancel('Operation canceled by the user.'); } }, [config]); }; 使用方式:const App = () => { const [ready, setReady] = useState(false); const { loading, result, error } = useRequest(url, null, { manual: true, ready }); return ( loading: {loading} {JSON.stringify(result)} setReady(true)}>产生请求 ); }; 当然,实现手动触发的方式有很多,我在这里是用一个config.ready来触发,在 umi 框架中,是对外返回了一个 run 函数,然后执行 run 函数再来触发这个 useRequest 的执行。const useRequest = () => { // 其他代码保持不变,并暂时忽略 const [ready, setReady] = useState(false); const run = (r: boolean) => { setReady(r); }; useEffect(() => { if (!config || !config.manual || (config.manual && ready)) { if (loading) { return; } setLoading(true); const source = request(); return () => { setLoading(false); setReady(false); source.cancel('Operation canceled by the user.'); }; } }, [config, ready]); }; 3. 总结 # 我们在这里实现的useRequest,也只是实现几个基本功能!对于更大的业务来说,可能需求点更多,例如轮训,防抖等,这些功能就需要自己去添加啦!
2024年10月20日
3 阅读
0 评论
0 点赞
1
...
42
43
44
...
213