首页
Search
1
解决visual studio code (vscode)安装时没有选择安装路径问题
320 阅读
2
如何在 Clash for Windows 上配置服务
215 阅读
3
Linux 下 Bash 脚本 bad interpreter 报错的解决方法
150 阅读
4
Arch Linux 下解决 KDE Plasma Discover 的 Unable to load applications 错误
149 阅读
5
uniapp打包app提示通讯录权限问题,如何取消通讯录权限
113 阅读
clash
服务器
javascript
全部
游戏资讯
登录
Search
加速器之家
累计撰写
1,195
篇文章
累计收到
0
条评论
首页
栏目
clash
服务器
javascript
全部
游戏资讯
页面
搜索到
1195
篇与
的结果
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日
6 阅读
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日
5 阅读
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日
4 阅读
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日
9 阅读
0 评论
0 点赞
2024-10-20
学完这 4 个小技巧,让你的移动端交互体验更加优秀
现在在手机等移动端设备访问的人越来越多,我们前端开发者一直致力于将设计稿还原成页面,供用户访问。但除高度还原设计稿外,交互上的良好体验也是我们应该做到的。 1. 即时反馈 # 我们在玩游戏的过程中,通常都会遇到一个词:“打击感”,通俗的理解就是我们做出的每一个操作,都有很强烈的反馈,比如视觉上的动画变化,听觉上产生的声音,或者移动设备的震动感等。 1.1 按钮的即时反馈 # 在前端页面中,也应当像游戏中的打击感一样,用户任何的操作都应当予以即时的反馈,告诉用户他的操作是有效的,系统已收到他的操作,内部正在处理中。例如用户在点击页面中的按钮时,按钮最好有一种被按下的效果:button:active { transform: translateY(4px); } 若按钮被下压的效果不太适合页面整体的风格,您也可以做一个背景颜色上的变化。 1.2 持续性的反馈 # 每个用户的设备型号、网络状态等情况都不一样,我们不能要求每个用户都在良好的 WiFi 下操作我们的页面。若用户的某个行为产生了网络请求,并要根据请求返回的结果,反馈给用户。这种情况,页面都应当给用户一种持续性的反馈,表示一个动作正在后台执行。如果没有这种效果,即使已经在请求接口了,用户也会认为点击没有反应,会多次的去点击按钮,以期望得到响应。我们可以在这里给自己定下一条规则: 凡是有网络请求的情形,均要有 loading 效果的持续性反馈。 我们通常可以在用户触发的按钮上展示 loading 效果,也可以在全局页面上展示 loading 效果,这个根据每个页面的风格自行选择即可。例如页面上有个红包需要点击按钮开启,当用户点击按钮后,按钮就可以展示出一个旋转的 loading 效果,待接口返回结果再打开红包,展示具体的金额,或者其他的结果。 1.3 页面初始化 # 在现在大部分前后端分离的场景下(同时没有使用同构直出方案),都是先展示出一个没有数据的前端页面,然后请求数据,待数据返回后再渲染页面。这种情形和上面 1.2 中是一样的,不过这个是在刚进入页面就触发的!这里我们也是要展示出 loading 效果的,只不过是 loading 展示的时机的问题。 先一个全局 loading 的开启页,在数据没有返回回来时,看不到任何相关活动元素; 先用初始化的假数据或者兜底数据,渲染一个基本框架,然后在某个位置展示 loading 效果,并请求数据,数据返回后再替换假数据进行渲染。 这两种方式也是各有不同的使用场景,就我个人而言,我更喜欢第 2 种方式,能够第一时间将页面中的元素展示给用户;但如果页面布局因接口的数据改变较大,建议还是采用第 1 种方式,这样 loading 结束时,不会出现页面大幅度闪动的感觉。 1.4 数据的展示 # 我们拿到接口的数据后,通常会有两种展示状态: 无数据,进行“暂无数据”之类的提示; 有数据,正常展示数据; 比如一个展示奖品列表中数据中,这里我们通常会初始化一个 list 变量来接收接口返回的数据:const List = () => { const [list, setList] = useState([]); useEffect(() => { // 设置数据 // setList([]); }, []); return ( {list.length ? ( {list.map((item) => ( {item.title} ))} ) : ( 暂无数据 )} ); }; 在请求接口的过程中,页面会展示什么?“暂无数据”,给用户的第一视觉感受就是:我的奖品丢了。等过一会儿接口返回数据了,然后又重新将数据展示出来。这里,我们就忽略了一个很重要的状态:loading状态。因为“暂无数据”,也是一种结果,不是过程,是要告诉用户,您当前是没有数据的。因此,不能把“暂无数据”作为 loading 状态来展示。const List = () => { const [loading, setLoading] = useState(true); const [list, setList] = useState([]); useEffect(() => { // 设置数据 // setList([]); setLoading(false); // 请求完接口,再把loading状态取消,该展示什么结果就展示什么 }, []); if (loading) { return ( 请求数据中... ); } return ( {list.length ? ( {list.map((item) => ( {item.title} ))} ) : ( 暂无数据 )} ); }; 2. 行为跟随 # 这里我也不太想好用个什么名字,概况来说,告诉用户刚才发生了什么,将用户操作可视化, 来增强用户对操作行为的感知度, 同时也能对元素内容的认知。因用户行为产生的新交互,应当与当前用户的行为相关。 2.1 点击按钮后呼起弹窗 # 用户点击按钮后,会弹出一个弹窗,弹窗可以从按钮所在的方向或者位置,弹出到整个页面的中心。给到用户的感受就是该弹窗与按钮是相关的。 2.2 列表中有对象变动时 # 例如在一个表格或者列表中,有新增、修改或者删除一行(一列)的行为,可以用一个动画和背景色来区分该元素, 过一段时间再恢复正常。 2.3 丝滑的滑动跟随 # 在不添加任何 CSS 属性时,滑动有滚动条区域时,总感觉有一种卡顿感,就是手指滑动时页面就跟着滑动,手指离开则页面停止滑动。这里我们添加上一个属性即可:body { -webkit-overflow-scrolling: touch; } 3. 考虑移动设备的握持姿势 # 在现在手机屏幕越来越大的趋势下,单手握持手机时,大模板只能在以左下角或者右下角为中心的区域活动。因此,在底部区域操作的情况越来越多,例如底部区域的导航,弹窗中点击空白区域即可关闭等等。 3.1 避免滚动穿透 # 在一个可滚动的页面中,呼起一个弹窗,这个弹窗中的内容也比较多,也需要滚动,如果不加处理的话,可能会造成两个区域同时滚动,体验不好。也就是避免滚动穿透。这里我们就要把底层的滚动锁住,只可以滚动处在最上层的区域。这里的原理我就不多讲解,推荐一个我一直在使用的组件tua-body-scroll-lock,该组件导出了 2 个方法: lock: 锁定区域,传入 dom 元素,则表示该 dom 区域内是可以滚动的; unlock: 解除锁定,当弹窗消除时,需要解除被锁定的区域; 在 react 中的使用方式:useEffect(() => { // 锁定body的滚动,只在弹窗内部滚动 // 只有需要设置可以滚动区域时,才使用该方法 if (props.scrollContainer) { lock(props.scrollContainer); } return () => { if (props.scrollContainer) { unlock(props.scrollContainer); } }; }, [props.scrollContainer]); 同时的,我们最好在遮罩区域添加可以关闭弹窗的操作,避免用户伸手够弹窗右上角的关闭按钮。 3.2 原生 select 标签的使用 # 在移动端开发中,下拉框我们使用原生 select 标签时,iOS 和 Android 的表现是不一样的,iOS 会出现在屏幕的底部,滚动选择某个选项;而 Android 中,则是屏幕中间弹出一个弹层,然后可以进行选择。如果图方便的话,其实可以使用原生的 select 标签。但这种方式,总感觉与页面元素之间产生了割裂,因此如果可以的话,尽量模拟出一个 select 标签。 4. 良好的兜底策略 # 每个用户的设备型号、网络状态等情况都不一样。总会因为各种各样的原因,导致页面展示异常。因此,我们应当做好提示和一些兜底策略。 4.1 全屏沉浸式页面应当保持关闭操作 # 通常情况下,在移动端 APP 中打开的页面,顶部都会有一个白色的标题栏。但有些活动页面为了更好地沉浸式体验,会把白色标题栏去掉,同时还去掉了右划退出的操作,只能点击自定义的返回按钮才能退出。例如这个页面,左上角的返回按钮是页面本身自定义的。而这个页面必须是接口正常返回数据后才展示出来,在最开始时,如果有异常时,会展示错误信息,但没有返回按钮。这就导致用户无法退出该活动,只能杀掉 APP 再重新进入。体验非常不好,这里我们就要保证:全屏沉浸式页面不管是哪种状态,应当全程保持关闭操作!当然,现在已经没有这个问题了。 4.2 永远不要相信后台一直很稳定 # 后台经常说的一句话是“不要相信任何从前端传过来的数据”,我们也一样: 永远不要相信后台一直很稳定。 我们要做好接口服务可能会挂掉的预案: 设置请求接口的超时时间,不要让用户无限制等待; 良好的提示; 有条件时,可以自动重试,或者让用户手动尝试重试请求接口; 采用兜底策略遮盖; 前 3 种我们都可以理解,当接口异常并无法继续后续的操作时,应当告知用户有服务有异常了,可以稍后重试。对于第 4 种,通常可能会发生在高并发的抽奖过程中,越是让用户重试,并发量就越高。因此在抽奖异常时,可以直接告诉用户未中奖,而不是“服务异常”之类的话术。要不然,一方面会引起用户的不满,另一方面会造成用户的大量重试。这个百度在春晚发红包中,就有用到过,在服务器短时间内承受到高并发量时,则直接告诉用户未抽中红包;同时,对于一些抽奖会同时发放多个奖品时,也要做好每个奖品服务都可以会挂掉的准备,比如同时会发放 3 个奖品: 服务都正常,正常发放; 2 个正常,就只发放 2 个奖品,左右排列; 只有 1 个服务正常,则只发放 1 个奖品,居中排列; 均异常,则告诉用户未中奖; 千万不要留有空间或者槽位告诉用户“该位置本应该有奖品,但实际上没有”的感觉。 4.3 懒加载 # 懒加载是一个老生常谈的话题,这里我们只针对图片懒加载来进行梳理。在页面中图片比较多时,请尽量使用图片懒加载,并考虑好图片加载失败的情况,可以先创建一个 Image 来先加载图片,加载城后再给到页面中的 dom 元素,否则使用兜底图片:// 判断图片是否可以加载成功 const loadImage = (imgUrl: string): Promise => { return new Promise((resolve, reject) => { const img = new Image(); img.src = imgUrl; if (img.complete) { return resolve(img); } img.onload = () => { resolve(img); }; img.onerror = reject; }); }; // IntersectionObserver的回调,当dom元素进入到可是区域内时 const targetExposeCallback = async (dom: HTMLElement) => { let original = dom.getAttribute('data-original'); if (original) { try { await loadImage(original); } catch (err) { // 1x1的图片 original = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; } setLoading(false); if (dom.tagName.toLowerCase() === 'img') { dom.setAttribute('src', original); } else { // eslint-disable-next-line dom.style.backgroundImage = `url("${original}")`; } dom.setAttribute('data-original', ''); } }; 同时,我们在体验的过程中发现,在有些华为手机里,图片还没加载完毕时,会展示一个裂开的图片,如果该图片 alt 注释,也把 alt 注释显示出来,稍过一会儿,等图片加载完毕了,就正常展示图片了。这种情况,我们也可以使用图片懒加载,或者将图片设置为背景图片,避免出现图片裂开的状态。 5. 总结 # 我们在移动端开发的过程中,总会有多种解决方案。如果我们站在用户的角度多想一想,就能让产品的交互体验变的更好。
2024年10月20日
5 阅读
0 评论
0 点赞
1
...
69
70
71
...
239