-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcontent.json
1 lines (1 loc) · 154 KB
/
content.json
1
{"meta":{"title":"sanks的博客","subtitle":"知识付诸行动才有转换为能力的效果","description":"对于JavaScript(ES6)、VUE、React、NodeJS、等相关技术的实战经验分享","author":"sanks","url":"https://www.sanks-blog.com"},"pages":[{"title":"Page not found","date":"2020-07-17T08:27:45.167Z","updated":"2019-03-01T02:27:32.000Z","comments":false,"path":"/404.html","permalink":"https://www.sanks-blog.com//404.html","excerpt":"","text":""},{"title":"sanks简介","date":"2018-09-17T00:46:50.000Z","updated":"2022-03-14T16:22:49.365Z","comments":false,"path":"about/index.html","permalink":"https://www.sanks-blog.com/about/index.html","excerpt":"","text":"About me13年3月从事前端开发至今 现研究方向目前自己的后台项目已经具备了以下功能模块: 文件上传服务 短信发送服务 权限认证服务 socket即时通讯服务 自己梦想开发的模块和功能都已经完成了,后续就是把每个功能存在的问题修补一下。 2022完成项目的拆解,还有在线聊天和大文件分片上传的功能, 2022-02-28完善在线聊天的功能的开发,并成功与后台socket服务对接 2022-02-22完成了大文件分片功能的开发,http协议和websocket协议 2022-01-26从 react-egg-typescript 服务中,拆解出子服务 react-socket-server 2021完善登录验权机制OAuth2.0, 还有更合理、更切实际的加密解密方案。 2021-10-21开发倒计时组件(react) 2021-10-11全线撤掉ECIES的加密解密方案,宣告失败,由于nodejs生成的 Buffer 与 前台的 ArrayBuffer 无法转换处理改造加密的方式为 RSA + AES 加密的方案 2021-05-06重构登录验权机制从JWT到Oauth2.0的转换 2021-03-09统一返回体的数据结构和调用方式 2021-02-22完善 AES+RSA 加密方案 2021-02-09修改认证方式为Oauth2的机制 2021-02-01增加响应体的统一返回处理的Controler和权限(token)验证的权限中间件 2021-01-30增加egg-jwt插件,并增加token的权限验证中间件 2020完善自己的前台项目,tsx文件与jsx文件的共存使用 2020-09-27(重大项目改造)将自己的项目前后台代码拆解成两个项目,前台 react-koa-typescript 和后台 react-egg-typescript 2020-09-24将网站路由从browserHistory模式改为hashHistory模式更改页面之间的路由跳转传参形式(刷新页面,参数不丢失) 2020-09-17增加验证码登录的接口 2020-08-30登录超时或者无权限则转至注册页加入了阿里巴巴图标库在线地址 2020-08-23为注册页增加全局提示组件,并与redux关联调整接口统一返回报文格式 2020-08-22将登录页和注册页合并成一个页面 2020-08-16解决登录后获取权限时,请求头中未加入token的问题 2020-08-02实现了页面跳转的转场动画效果 2020-07-25重新进行数据库建模,并把登录逻辑完善,判断逻辑转交于controller层修改了登录流程上的拦截问题和存储数据问题粗略规定了返回正确和错误数据的格式 2020-07-11尝试了使用 MongoDB 和 GraphQL,与路由完美的融合了 2020-06-092020年已过了一半,我未停止脚步,暂时博客专属自己学习和参考用 2020-01-10APP方面学习flutter和Dart语法,移动web继续使用VUE和ReactJS,并写一些nodejs相关的中间件或服务器。 2019自己决定重新调整自己在前端领域的发展方向,努力拓展自己未涉及的领域。 2019-05-04利用周六日的闲暇时间,填了搭建react + typescript 的一些坑,框架已经成型,项目中想到的,需要的配置都有了;好的前端框架搭建是做一个可维护,可拓展项目的基础,不会给以后接手的程序员带来麻烦。 2019-03-10几天的努力之下,自己的react项目总算成型,决定新建一个“发布”分支,以供以后开发用。 2019-03-04开始深入研究reactjs,弥补之前的浅尝辄止;一开始自己用官方的项目生成器生成了一个简单的架构,自己从这个简化版逐步加入babel, webpack, eslint 等相关的配置, 但是你仔细阅读react项目下的 READEME.md, 你就会发现我绕了远路,其实react提供了 npm run eject 来注入webpack, eslint, label 等相关依赖和配置,可能这就是react给大家提供的 脚手架 吧,需要注意的是:这个命令只能执行一次,而且不可逆转。 2019-02-25为博客每篇文章(包括首页)也显示字数统计和阅读时长, Nginx 优化配置 - Gzip 压缩, 博文分享换成addthis。 2019-02-22为博客加入了百度统计功能,实际的去观察网站访问情况 2019-02-15为自己的博客网站进行了SEO,包括百度搜索和谷歌搜索,并加入了相关的站点地图,在hexo中添加百度主动推送功能, 每次部署主动推送一次 2019-02-09把自己的博客成功迁移到自己服务器上,配置了git远程资源库,配置nginx :能够用https协议访问博客地址,强制http转https协议访问博客,http://sanks-blog.com -> https://sanks-blog.com, http://www.sanks-blog.com -> https://www.sanks-blog.com 2019-01-27服务器部署gitlab失败后,自己在家测试了一下ping自己的博客的github地址,和ping自己的服务器对比了一下,发现github的延迟132ms,而且丢包;自己的服务器74ms,毅然决定把自己的博客网站迁移到自己的服务器上,一开始弄了FTP,但是有些舍近求远了;发现其实自己在服务器端搭建跟本地一样的开发环境即可,只是多了ngnix的安装和配置。 2019-01-25趁着工作午休时间,把自己博客的评论功能加上了,并填了首页插入的图片不显示的问题的坑。 2019-01-24经过折腾了linux安装docker, 并且在docker内装了jenkins后,感觉缺点什么,想弄个GitLab,在服务器存储自己的代码。踩坑开始,经过层层扒坑埋坑的过程,总算把GitLab建立起来了,但是访问是502页面。最后找到原因:由于服务配置太低(CPU 1核,内存2GB),无法满足Gitlab的(CPU 2核,内存4GB)的要求,页面报502,踩坑结束。 2018研究vue2.0 VUEX状态管理机制 webpack 等"},{"title":"archives","date":"2020-07-17T08:27:45.258Z","updated":"2019-03-01T02:27:32.000Z","comments":false,"path":"archives/index.html","permalink":"https://www.sanks-blog.com/archives/index.html","excerpt":"","text":""},{"title":"categories","date":"2020-07-17T08:27:45.301Z","updated":"2019-03-01T02:27:32.000Z","comments":false,"path":"categories/index.html","permalink":"https://www.sanks-blog.com/categories/index.html","excerpt":"","text":""},{"title":"","date":"2020-07-17T08:27:45.342Z","updated":"2019-03-01T02:27:32.000Z","comments":true,"path":"categories/package.json","permalink":"https://www.sanks-blog.com/categories/package.json","excerpt":"","text":"{\"dependencies\":{\"hexo-deployer-git\":\"^0.3.0\"}}"},{"title":"tags","date":"2020-07-17T08:27:45.401Z","updated":"2019-03-01T02:27:32.000Z","comments":false,"path":"tags/index.html","permalink":"https://www.sanks-blog.com/tags/index.html","excerpt":"","text":""}],"posts":[{"title":"树形表格组件","slug":"tree-table-component","date":"2021-12-15T15:25:41.000Z","updated":"2022-01-03T15:09:23.381Z","comments":true,"path":"tree-table-component/","link":"","permalink":"https://www.sanks-blog.com/tree-table-component/","excerpt":"引言 距离上一篇博客有一月有余了吧,对于写博客的目的而言,我其实就是为了抒发自己无处宣泄的技术主张,在编写组件的过程中,很多同事嘲笑我“沉迷于组件”,也有的人让我“拿来主义”,并逼迫产品修改设计的重要显示和功能,但是我认为一个合格的程序员真的不应该是当这样的程序员,我何尝不知道工期紧,任务重,虽说我是修改别人的组件实现的最终效果吧,但是我这起码是当躲不开“造轮子”环节的时候,依然迎难而上。","text":"引言 距离上一篇博客有一月有余了吧,对于写博客的目的而言,我其实就是为了抒发自己无处宣泄的技术主张,在编写组件的过程中,很多同事嘲笑我“沉迷于组件”,也有的人让我“拿来主义”,并逼迫产品修改设计的重要显示和功能,但是我认为一个合格的程序员真的不应该是当这样的程序员,我何尝不知道工期紧,任务重,虽说我是修改别人的组件实现的最终效果吧,但是我这起码是当躲不开“造轮子”环节的时候,依然迎难而上。 说点题外话,没有谁是天生的技术大拿,只要你对技术这一行有兴趣,有不懈的动力,你一定会先人一步到达终点,好多人在这条路上半路放弃,或者对自己从事的工作出现疲态、厌倦的态度。对于我自己,自己给自己的压力远胜于比人给的,不逼自己一把就不会有进步,任何人都可以对我失去信心或者加以否定,唯独自己不可以,否定自己那就永远不可能有进步了,就废了。 回到正题,写这个博客的目的很简单,“轮子”我造完了,但是自己觉得还有优化的空间,先简要介绍下这个组件,后期把组件放到我自己的github上,而且我还要通知原作者,我要发表了;如果你看到这篇博客,恰巧这是你想要的效果,可以给我发邮件,我看到会及时的回复你,组件虽说有缺陷,但是我一定会优化好给大家用的,在后期大家提的 issues 中慢慢加强组件的健壮性。 参照的组件 - vue-table-with-tree-grid npm 资源库地址:https://www.npmjs.com/package/vue-table-with-tree-gridgithub 地址:https://github.com/MisterTaki/vue-table-with-tree-grid 我选择这个组件的原因有以下两点: 这个组件可以源码下载,自己可以做出调整和修改 这个组件是从iView 组件库中分离的,与 element-ui 组件库样式极为贴近 这个组件缺少我想要的功能点: 树形结构层级线展示(这正是我们公司产品迫切要求的) 异步加载扩展行数据的功能,作者提供了很好的插槽,当然也可以自己监听这个展开事件进行异步获取数据并渲染,这也不是什么难事 没有点击选中树节点的事件,并抛出(emit)给父组件; 没有类似于 element-UI 中 Tree 组件的 “节点过滤” 功能; 基于以上的业务需求点,目前组件唯独缺少的是第 4 点功能,我决定在最近的业余时间给加上 你可能会想到的大组件 vxe-table 组件演示地址:https://xuliangzhan_admin.gitee.io/vxe-table/#/table/base/basic我不建议你们用这个大组件,因为里面的好多功能,并不是业务想得那个样子,最主要的(敲黑板)是: 截止本博客发表,它对我来说存在重大缺陷 - “线是断的” ,在 “扩展行” 这一级别,没有画它组件中说到的 “连接线”,而且样式不太美观吧,至于到现在(你看到这篇博客)有没有修补这个问题,请自行检验。 组件功能示意图 造好的轮子给你用 github 地址:https://github.com/SKSSSX/vue-tree-table-with-linenpm 包地址:https://www.npmjs.com/package/vue-tree-table-with-line 后续工作 后期我要做到尽善尽美,没用的属性删掉,像 selected-node 这个属性,代码尽量做到清晰可读,供使用者进行很好的扩展。","categories":[{"name":"Vue","slug":"Vue","permalink":"https://www.sanks-blog.com/categories/Vue/"}],"tags":[{"name":"Vue","slug":"Vue","permalink":"https://www.sanks-blog.com/tags/Vue/"}]},{"title":"requestAnimationFrame解决传统定时器的BUG","slug":"requestAnimationFrame","date":"2021-11-07T03:05:45.000Z","updated":"2022-01-03T15:09:23.378Z","comments":true,"path":"requestAnimationFrame/","link":"","permalink":"https://www.sanks-blog.com/requestAnimationFrame/","excerpt":"引言 当你写一个倒计时组件时,可能会出现的解决方案是应用 setInterval来做的。我摘取网上的setInterval的实例代码,测试一下这个倒计时的误差。 读者的疑问:一个倒计时组件,需要做的这么精准吗?我的回答:这要看你对性能的要求是否是精益求精了,本着对计时精度的严格要求,尽量缩小倒计时误差,减小手机浏览器的内存消耗等很有必要;最重要的是,为以后的购物“秒杀”做准备,此组件扩展成“秒杀组件”是很简单的事。","text":"引言 当你写一个倒计时组件时,可能会出现的解决方案是应用 setInterval来做的。我摘取网上的setInterval的实例代码,测试一下这个倒计时的误差。 读者的疑问:一个倒计时组件,需要做的这么精准吗?我的回答:这要看你对性能的要求是否是精益求精了,本着对计时精度的严格要求,尽量缩小倒计时误差,减小手机浏览器的内存消耗等很有必要;最重要的是,为以后的购物“秒杀”做准备,此组件扩展成“秒杀组件”是很简单的事。 并不精确的setInterval 作者:善宝橘请阅读文章:https://juejin.cn/post/6882287025400070152代码在线演示:https://codepen.io/liuluffy/pen/NWrqdbz 具体代码如下:JS: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061class Clock extends React.Component{ constructor(props){ super(props) this.state={ minute:60, second:60 } this.start = this.start.bind(this) this.no = this.no.bind(this) this.end = this.end.bind(this) } start(){ if(this.state.second>0){ this.setState({ minute:this.state.minute, second:this.state.second - 1 }) } else{ console.log('结束', new Date().getTime()) this.setState({ minute:this.state.minute - 1, second:this.state.second + 60 }) } } no(){ console.log('开始', new Date().getTime()) this.id = setInterval(this.start,1000) } end(){ clearInterval(this.id) } render(){ return( <div> <div id="container"> {this.state.minute} : {this.state.second} </div> <button onClick={this.no}> start </button> <button onClick={this.end}> end </button> </div> ) }}ReactDOM.render( <Clock/>, document.getElementById("root")) CSS: 123456789101112131415161718#container{ height:10em; width:10em; border: 1px solid grey; text-align:center; line-height:1000%; font-size:40px;}button{ background-color:blue; color:white; height:40px; width:10em; margin-top:10px; margin-right:2px;} DOM: 12<div id="root"><div> 测试结果如下图所示: 问题很明显 (1) 最明显的问题莫过于 setInterval 的使用。写一个定时任务,不少小伙伴第一反应想到的也是 setTimeout 和 setInterval 函数,但是它们真的足够精确吗?这就要从 JS 的任务队列及微任务队列(也有称 macrotask queue 和 microtask queue)说起了… 我们言简意赅总结下:JS 主线程执行时有一个栈存储运行时的函数相关变量,遇到函数时会先入栈执行完后再出栈。当遇到 setTimeout setInterval requestAnimationFrame 以及 I/O 操作时,这些函数会立刻返回一个值(如 setInterval 返回一个 intervalID )保证主线程继续执行,而异步操作则由浏览器的其它线程维护。 当异步操作完成时,浏览器会将其回调函数插入主线程的任务队列中,当主线程执行完当前栈的逻辑后,才会依次执行任务队列中的任务。 但是在每个任务之间,还有一个微任务队列的存在。在当前任务执行完后,将先执行微任务队列中的所有任务,例如 Promise process.nextTick 等操作。也就是说当 setInterval(fn, 1000) 等待 1 秒钟后,fn 函数会被插入任务队列中,但并不一定会立刻执行,还需要等待当前任务以及微任务队列中的所有任务执行完。长此以往,使用 setInterval 的计时器超时将越来越严重。 (2) 有人不注意 setState的异步问题,这个异步会存在时间误差;直接用js选择器来操作DOM即可。 (3) 网上还有人对React生命周期没有做研判,正确的做法应放在 componentDidMount 生命周期中; (4) 为了刷新页面,有的人把时间戳存储在cookie中,每隔一秒更新一次,完全没有必要,只需要存储最终倒计时结束的时间戳即可。 (5) 有人会考虑计算浮点数的误差,其实完全没必要,只需要对时间戳操作即可,在倒计时跨越“整秒”这一时机时,秒数减1,误差可忽略的,不影响最终的相减结果 (6) 更细致的是,每行代码执行也需要时间,这个也可以忽略不计。 基于以上的(1-3)描述存在的误差的原因,导致最终的倒计时会出现偏差,经过测试会大于1s,在我看来,已经是不可接受的误差了,如果在真实的代码环境下,可能会存在多个宏任务、微任务,误差会更大,对以后的代码扩展也是不利的。 更好的更新策略 用requestAnimationFrame来解决这一问题: requestAnimationFrame是浏览器用于定时循环操作的一个接口,类似于setTimeout,主要用途是按帧对网页进行重绘。 设置这个API的目的是为了让各种网页动画效果(DOM动画、Canvas动画、SVG动画、WebGL动画)能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。代码中使用这个API,就是告诉浏览器希望执行一个动画,让浏览器在下一个动画帧安排一次网页重绘。 requestAnimationFrame的优势,在于充分利用显示器的刷新机制,比较节省系统资源。显示器有固定的刷新频率(60Hz或75Hz),也就是说,每秒最多只能重绘60次或75次,大多数电脑显示器的刷新频率是60Hz,大概相当于每秒钟重绘60次。大多数浏览器都会对重绘操作加以限制,不超过显示器的重绘频率,因为即使超过那个频率用户体验也不会有提升。因此,最平滑动画的最佳循环间隔是1000ms/60,约等于16.6ms。requestAnimationFrame的基本思想就是与这个刷新频率保持同步,利用这个刷新频率进行页面重绘。此外,使用这个API,一旦页面不处于浏览器的当前标签,就会自动停止刷新。这就节省了CPU、GPU和电力。 不过有一点需要注意,requestAnimationFrame是在主线程上完成。这意味着,如果主线程非常繁忙,requestAnimationFrame的动画效果会大打折扣。 requestAnimationFrame使用一个回调函数作为参数。这个回调函数会在浏览器重绘之前调用。 计时器一直是javascript动画的核心技术。而编写动画循环的关键是要知道延迟时间多长合适。一方面,循环间隔必须足够短,这样才能让不同的动画效果显得平滑流畅;另一方面,循环间隔还要足够长,这样才能确保浏览器有能力渲染产生的变化。 而setTimeout和setInterval的问题是,它们都不精确。它们的内在运行机制决定了时间间隔参数实际上只是指定了把动画代码添加到浏览器UI线程队列中以等待执行的时间。如果队列前面已经加入了其他任务,那动画代码就要等前面的任务完成后再执行。 requestAnimationFrame代码实践(React)网上好多代码示例,要么是局部代码段,要么是不可执行的代码,或执行起来报错,或没有根据使用的实际情况调用cancelAnimationFrame取消定时器,以减少主线程的性能消耗。 本代码的核心思想是在规避其他会造成计时误差的前提下,存储时间戳的形式来进行计时操作的,一般执行60次。秒数会减1。如果正在倒计时,页面刷新不会重置。并且在页面“卸载”时或倒计时结束时,用cancelAnimationFrame取消定时器。经过自己反复测试,最后的误差在 [-10ms, 10ms],大大缩减了计时误差。 具体测试结果见下图: 具体代码如下:Countdown.tsx123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130import * as React from 'react';import * as Cookies from 'js-cookie';import './coutdown.scss';interface CountProps { onRefCount: any; seconds: number; retryFetchCode: Function; // 子组建向父组件传递参数的函数}interface CountState { endStamp: number; text: string; nodes: any;}let myReq: Function;class CountDown extends React.Component<CountProps, CountState> { constructor(props: CountProps) { super(props); // 刷新页面时,先检查cookie中是否存在这个值,如果有,则读取这个值 // 此处会出现两种情况,在打开多个标签页的情况,但是登录的还是同一个用户 // 此时,不需要重新计时,需要从之前的节点计时,也就是说是最终的截止时间(cookie中存在的那个值) const spareTime = Cookies.get('countDeadline'); // 当前的时间戳 var currStamp = new Date().getTime(); // 最终剩余的整秒数 let spareSeconds; if (spareTime) { spareSeconds = Math.ceil((spareTime - currStamp) / 1000); } else { spareSeconds = 60; } this.state = { endStamp: spareTime || new Date().getTime() + this.props.seconds, text: '重新获取(' + spareSeconds + ')', nodes: [] }; // 将最终终点的时间戳存入到cookie中 Cookies.set('countDeadline', this.state.endStamp); } render() { return ( <span className="count-down-timer" onClick={() => this.props.retryFetchCode(this.state.text)} > {this.state.text} </span> ); } componentDidMount() { //通过props接收父组件传来的方法 this.props.onRefCount(this); this.setState( { nodes: document.querySelectorAll('.count-down-timer')[0] }, () => { console.log('countdown start', new Date().getTime()); this._CountDownLoop(); } ); } componentWillUnmount() { console.log('卸载'); cancelAnimationFrame(myReq); // 清除cookie Cookies.remove('countDeadline'); } /** * 重新计入倒计时 */ _retryCountDown() { this.setState( { endStamp: new Date().getTime() + this.props.seconds, text: '重新获取(60)', nodes: [] }, () => { // 更新最终终点的时间戳,存入到cookie中 Cookies.set('countDeadline', this.state.endStamp); console.log('countdown restart', new Date().getTime()); this._CountDownLoop(); } ); } /** * 倒计时循环 * @private */ _CountDownLoop() { var currStamp = new Date().getTime(); let text = ''; // 显示的文本 var isEnd = false; //如果结束时间戳减去当前时间时间戳小于等于0则设置倒计时结束标识为true if (this.state.endStamp - currStamp <= -1) { isEnd = true; text = '重发短信验证码'; } //如果结束则调用结束回调 if (isEnd === true) { console.log('countdown end', new Date().getTime()); cancelAnimationFrame(myReq); } else { // console.log(Math.ceil((this.state.endStamp - currStamp) / 1000)); let remaining = Math.ceil((this.state.endStamp - currStamp) / 1000); text = '重新获取(' + remaining + ')'; var that = this; myReq = requestAnimationFrame(function() { that._CountDownLoop(); }); } const target = this.state.nodes; if (target) { target.innerHTML = text; this.setState({ text: text }); } else { cancelAnimationFrame(myReq); } }}export default CountDown; countdown.scss1234@import '../../scss/index';.count-down-timer { display: inline-block;} Verify.tsx123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212import * as React from 'react';import Button from '../../../node_modules/antd-mobile/es/button';import '../../../node_modules/antd-mobile/es/button/style';import { toParams, getQueryString } from '../../utils';import { connect } from 'react-redux';import { navigatorActions, userActions } from '../../redux/actions';import { sendVerifyCode } from '../../api/sms';import NavBar from '../../components/NavBar/NavBar';import GlobalMessage from '../../components/GlobalMessage/GlobalMessage';import * as lodash from 'awesome-utils-normal';import PasswordInput from '../../components/PasswordInput/PasswordInput';import CountDown from '../../components/CountDown/CountDown';import './verify.scss';interface Props extends React.Props<Props> { history: any; match: any; register: Function; form: any; title: string; verifyLogin: Function; changeNavbarStatus: Function; changeBackStatus: Function; changeFilterStatus: Function; changeTitle: Function; changeSidebarStatus: Function;}interface State { title: string; phone: string; verifyCode: string;}export class Verify extends React.Component<Props, State> { constructor(props: Props) { super(props); this.state = { title: '输入验证码', phone: '', verifyCode: '' }; } // 验证码是否正确 validateCode = () => { if (this.state.verifyCode.length === 4) { return true; } else { return false; } }; // 定义一个拿子组件返回值this的函数 onRef = (ref: Object) => { this.child = ref; }; onRefCount = (ref: Object) => { this.childCount = ref; }; // 调用错误提示方法 showError = (message: Object) => { this.child.showMessage(message); }; // 调用重新倒计时的方法 retryCountDown = () => { this.childCount._retryCountDown(); }; // 验证码登录 onSignup = () => { const { match } = this.props; if (this.validateCode()) { const userInfo = { activity_id: getQueryString('from'), phone: this.state.phone, verifyCode: Number(this.state.verifyCode), grant_type: 'password', from: match.params.from }; this.props.verifyLogin(toParams(userInfo)); } else { this.showError({ text: '请输入正确的验证码', type: 'message-error' }); } }; // 重新获取验证码 retryFetchCode(text: string) { if (text === '重发短信验证码') { // 调用发送验证码的接口 sendVerifyCode({ phone: this.state.phone }) .then((res: any) => { console.log(res); }) .catch((err: any) => { console.log(err); }); this.retryCountDown(); } } getChildTextData(value: string) { if (!value) { return; } const tempArr = value.split(''); const finalArr = tempArr.map(item => Number(item)); this.setState({ verifyCode: finalArr.join('') }); } static getDerivedStateFromProps(props: any) { props.changeNavbarStatus(true); props.changeBackStatus(true); props.changeFilterStatus(false); props.changeTitle(''); // this.props.changeSidebarStatus(false); const { match } = props; return { phone: match.params ? match.params.phone : '' }; } render() { return ( <div className="verify"> <NavBar {...this.props} /> <GlobalMessage onRef={this.onRef} _left="0" /> <form className="verify-form"> <div className="header"> <h4 style={ this.state.title === '输入验证码' ? { paddingBottom: '32px' } : {} } > {this.state.title} </h4> <span> 已发送4位验证码至 <i>+86 {this.state.phone}</i> </span> </div> <div> <PasswordInput length={4} mask={false} gutter={4} focused={true} getChildTextData={this.getChildTextData.bind(this)} /> </div> <div className="btn-warpper"> <Button type="primary" className="verify-btn" activeClassName="verify-btn-active" /* loading={true} */ onClick={lodash.debounce({ fn: this.onSignup, wait: 1000, immediate: true })} > 确定 </Button> <a className="text-link"> <CountDown seconds={60000} retryFetchCode={this.retryFetchCode.bind(this)} onRefCount={this.onRefCount} ></CountDown> </a> </div> </form> </div> ); }}function mapState(state: any) { const { signIning } = state.authentication; const { navbarStatus, backStatus, filterStatus, title, sidebarStatus } = state.navigator; return { signIning, navbarStatus, backStatus, filterStatus, title, sidebarStatus };}const actionCreators = { verifyLogin: userActions.verifyLogin, changeNavbarStatus: navigatorActions.changeNavbarStatus, changeBackStatus: navigatorActions.changeBackStatus, changeFilterStatus: navigatorActions.changeFilterStatus, changeTitle: navigatorActions.changeTitle, changeSidebarStatus: navigatorActions.changeSidebarStatus};export default connect( mapState, actionCreators)(Verify); Verify.scss123456789101112131415161718192021222324252627282930313233343536373839404142434445@import '../../scss/index';.verify { position: relative; padding-top: 50px; box-sizing: border-box; z-index: 0; height: 100vh; background-color: $black; &-form { width: 80vw; background: transparent; overflow: hidden; margin: 0 auto; color: $font-color; .header { > h4 { margin: 0; padding: 0 0 16px; font-size: 24px; font-weight: 400; } > span { display: inline-block; padding-bottom: 32px; font-size: 14px; i { font-style: normal; opacity: 0.6; } } } } .btn-warpper { text-align: center; } &-btn { border-radius: 42px !important; margin-top: 20px; margin-bottom: 24px; opacity: 0.9; &-active { @extend .am-green-active; } }} 总结 上面示例中我们可以看到,通过requestAnimationFrame不断的自己调用自己,实现高频度刷新倒计时,从而解决了页面切换窗口等传统setTimeout和setInterval假死问题。 倒计时组件以及调用组件的父组件的完整代码在这了,其他关联性不大的组件代码没有粘贴在这。需要这个组件代码的朋友可以随取随用,如果对倒计时组件有优化意见,可以向我发邮件提出建议。","categories":[{"name":"React","slug":"React","permalink":"https://www.sanks-blog.com/categories/React/"}],"tags":[{"name":"React","slug":"React","permalink":"https://www.sanks-blog.com/tags/React/"}]},{"title":"程序员的自我修养","slug":"The-interview-experience","date":"2020-12-27T13:41:55.000Z","updated":"2021-12-26T09:13:13.627Z","comments":true,"path":"The-interview-experience/","link":"","permalink":"https://www.sanks-blog.com/The-interview-experience/","excerpt":"Welcome to my blog, enter password to read.","text":"Welcome to my blog, enter password to read. Incorrect Password! No content to display! U2FsdGVkX19LtwmLx6Qdr839S+ygaV3oZkZte+dZVv7OHn9F7pDmCULTLm1V8Fw+FV39ne0NZ/whyPv/HCpgZavyUMa+YybTRyGd2/5c2R1wwSaQRYeHmwIuenXrrHV9zs39mYMtdb7KZKau2bvPWgoAeEsEtajl09F96UIWLy+LN44wKTT/XtD4F1plygO+DLLdjk+eLn0Nv2eEG0wfD4tRYm2j4EbLiDpp7QMAp+eNHlqJJbF+gsF7joUUfaQiB4Mx9VA3EZZuIrMxHf0iwsi39EicpjyM+AzuMMtH7vnprA+G13tkKAHdmBecFvkmstvXxJJaVYXKSFxIP+FfgGVb/1sez7Bl/5Q+NRYTK2feXo/bo4KPLaAlykvaKgGPEuDEsAunXfBUO+xvxqeuuOlh3zjTjjBb8O04hlaGYVKVizh7W2vYQ2Kl+SL0uAKZoyryvStFpzzLXOdjG81ETdWWTB44ual87oQCo+vOX7ExXkbJurCoKCOjcuI1xeh22ND1u24gx0RGyY6VBcSAg9yuYFhSRhv5o8RYtUyWTNl28OYaSpKE7DdZc3N7FX25UYq/HsM4OqouJ0sBaFs+O874XdvzRUfDlNM213VFCH9yLl37trsoo7i+dkzxwSQMy0K64BRgc5UJWQWLLjtFSqS+MKjqoKvGQPbK0MnJZfhsG2D2XntkmqrCl2CHK+8HVur/Ze5ldPKU5sZJFhqS53Uu4OphCeDU8MHWxK3vK2JKzPWOuG6HN2EgxuF7CBld8pLgOEyigZIgepsGeSxaXIxE7g7if3lHJkBzK7zVZ4LIJKxIABe+BIvhbK5LrmXxIX5GBGmW10T6ZTjE+dAK5A0GkKqGJFWQ7dH4K69pWHo3QiG425dV/bCGuY8o334gbN7fFfwsPFBC8T0ZGsNE9xAY1bFOtiFk0gcLrGN4+9XgF5KQxRMINu3/D0vFCwOvzw5ZELloxmJaCM/rGve+UaO7oUQnmdRNrxtGJDJE0h+e2jcMCb5WuOmapI75c5rgf8w+aFL3cJEgL3Hmxcbl+9vvv6K+c6yLwpsWxAtm5z7bK2SFmBqSzYh0PZQInOIvuLcG+2apr/PS9qY9xtWAVmjDXiPlzJZGuY+nP3tnYSYC4YkT+VobgsNDGlGw+V2aKKgiPtExKzGeza6psbn+g8BynXYU3c8Vywtl/GIurW6QhlelIxP0eApQKfGJVs8OMVeTUbjr25DCBAT9Qr7zwEQschPg5M4eHK9ybh3sf/6PgQoGd7ejnK3qhWHU2XHZN+RPGbRlfCHC0l0YAzWHJ9tzQHMvHFh3FWwu4A2H5KwRlFX/TFX0hB9WgGGySNj+uMgl0tZfbSpcVetSRiTsgooYcVAlpPVyG7dRfLp33/oi8lq5My7Z5C7a+S+TPWi4Ffn1bj7QsU9bVv1NmjbJw4BdQBoD3PcxjZruBF8+ujlF8juUbUG/TY096HP1LbokUs9InXXCgAK+snDizWQujgI2pm7KifT0t4PSjDVsw9C361A2x/O4Ggj2uk43lo+mYDBdQYq6XUm8rfczidZ3S2XPYuDSb/fobWELxxBlgoqqC9UZFY1YBNT0SvJxCHnHlKXMSJEAfePNBRDeAg7SyDFaa3JxIzZAeM0vMq/TwVb5lbFlBIL53sBUaxWfOMemMNY6cmRAMFTNEt/86azZ01mnNojZKKKmKoi5HmT+wizGJ5PdP967PNR/5HPW6/Qoh69QecgjtXypaIFmepg3lGabYl1u6JW6q+R1Sdh9cxbLZZqmckZ/8levRhtT6411gKBnm5M+Ucvvo/h+wuy5BTCEQibXs5yVZI6hxF6UmGdNnc6KTaoBR6ampQHQ93mCz7J7APvP33mLlzKMSXM/NsX02h/Xa0I/D8OCp7Yn5J9BIYGDfYVSvYpjpoe+OgbY4jc36ZpScBENe6O0TRPr4ESpAEh/BF+VWRB+cMk9GVQO7y7ieUFnxnXZJfXqGF27iuGP2iC39ZGsQ62e8UdHiaFjHbCIjZZnn9zT9EDF5iZR9pMZHfmALROiKhzNd48GHTl4lsCQszgGGLZry2GLl0tETgfrknQXiZRoF+TfP82jN+hH8zNdl9DC6TJddHJmiGJ20xtthKYVGjhf9MXzDWbYkhnwduABiXSCD3w4wf6i4v58+VsDVbX4cwK0VuvOhul+TutiY7uWoZO/RLm0WcZUNZitSJadjFLSzTBXCT1CxjAvCR+QJbPiOLaDHlTp5hpdtHdRT7PLTqLksFxMYdG1UeqkpLwx/iDkhL5JIKprndnKNfQcjlPUF2w0EyI2qaBzgoC4KvHTVr/n9gIbx35H64sth44jVFaW0Jt2SIoQgSkB1Xb0B/lTNIYB64mxJgkUo+9ebWaqvyfhtYYJZLtXQC/VQijfazswuibzS60lo5d3s1ogqni5VPDluWP4VBD7H/yaxdj4XBIABTkrk6KCdRwqOCeSy6W7nG0sIc478L7ryA8LeNANbCHTK+1y9iSqzdz9XV9bRa4CW8tYUjp9PRbGEg79kvygfslJ05RYpOkKHV0es9CQA3qy3IbeTGlKWCro/sw/HrqRRoQ3UhW3ynpa97odGq9p0pcm+OCtRe90CMotVN9QGkibz9CP35g4lt4JKN+RnFoGRICJQGu2cO6zP0fQ7oxARwrEaxfLT8dadQXvbj4vbVSyY3T0mRaoC+oCwlcFa9Em0V1/ni/aoaZ5gNJkcV+xUnSS7KXmmF0M1A1o38uia7KP9y7pv2YdxnFdFKKt2uHIQQu8bUF0llK6WzSF+No676zSabmAhdv8VIGi8HTIaPXsoaH41qz19UECQGWS82CvVb8rGCcuRWuANCbJZ4HYXtVfgYtxH/UkS3Lw+Y+uratvPR8//rmPNdR/ByhZORHxWEBUSJUdzURQOCULtwU/+TF2gUT7XH/bWLK0W2Wc/nP5Nhpnw7OwJd+P4fvTty4ZnRXBZ2mRAex12MqE4mJocAJJw3wpbgWD23p7Oi0sm/Thi40TcW3BhSkcpUtRAvuvH7KtNAJmoKul4hQMQ7sdXCFBiQduSIl+VPv3PEQROsFp7iV5lvhVIeZ99jRUl8fOgVX1vFJfmt5qBFK3DPtLKfyQQxQSD8L2XevpwN+yEIj6DNLxmthQDt4o6sBZDGMNIOrTUULwgzoPLaKma9us3NRO6ze3Zye7h57Ni6F33dVl4Lm6XotCI7PEOPQSAWCKQB6VeWhvfu01pkXOCHFp+9Aec2Nkl4lvmq9hZ2se7DqBadJV4liFItxo1mWxoiY7BYMDTuob1xm1gGPkTGfwavHnqHQIP2Rxjc5bhrci4L017eGCmPwQMFniuLARk01/n3N47oemOUvh+d4cB1U8a6unLoeJthGkCtnxrzw3NC8ZlYd0C6jqnEJSLVcZqWAgXmvWAjWiomwE4BcGlugWgfFiAvcw+Rr2XhxYuHxK8UcAbfNwi6YJ5HNV3C/lRzv/YRC4XBYhJ8c/kd8dKODo/CErWKFo/TTQ+fGs+/myZtn2JKz8a5v4wePHPY65NQUwmBQlkxQ6ThN54elsAeJthTRe3N3hSqYeRRrfq3hNwgmg4Yh5WCt+3qij6fV4ee6IcWJ1GzST1Xf0ok69RFYBDcM4/Oimhz9lfXT+EkMhuxPOZx6T5+nquJ5qkrjz4AE3GFCk1JMmMXpQKE9quwRq6luDPo4BrLD/bkxHMt8J/KPlJmvkBFliLV/SYPr38rcQwJci5fzMhIu7Bq4lrhcxoU5UWYhwoAv/8NSByaXRqP95itHUVx3Hxlf0iqgmRYKEfhoXMklmLu0iuYRRWxTO3s1nRJEofdfILK3CSiHLGAL+QfdviEv51rK/C09sxgdPcyJA820wOf1nT/nS4P9CQUShjwnoSkRKZ70t66Dy8rAPbvuZwenvsGS/LePos0qJvZsKtlL3jgebeD7viwgBFHtPjRWWHYsx6pILh0HF/MO8LEn4P76DCbcKm7AfnJxxosI5iw+IP0J8hmdU8uS71moNc8u06zb0WWTOiyuqHFfh5l6MGly6rU4hm2LFOm/RDWMcNG34xXeLHj3efdtyQpNN5huiTx5/vRjB7BZsq+skSnNe5Z09z22o+Idf3y1VAyJMlgNkPYIot4RUi8uuAvaOkWePvEXeWs1UBkubgkcl1FhhJfK3EzL/q7tfhvAfkOHN5MfU2S4CQEB68hFrIyZ/QwMz3FnIU0rzNxtaiCke8ZZa3voAGkGvXy6xeGLmZskwHDYFRbx5m9gZsqiYHNO+pmyKh/E+3RtFclAUr1cAr63WjM2vCUns7leas5djnQPrn97RYC9kV2a1B7L+rvlSDVUbNPYzcy/xrIyVF5jquh/rCRW7ky4y7bBT7cxVN2MYSSAJSVTETqqb+GKkPYrEBvn0bszsjnvQipJkkopL4UcAOMFxRLAZtqWqnBofkWNHdnLTaE/0YTQhhkXoOB0tuqVkoraMEMkLhogYyYTZuz3DaaDAU112jbhH83UGBh4qI5yjEDD1zZGS5EZG4TtpnjEwO1JzOBDVCev7geYBBhSWrOiwyHBIdEBboRoZbijujQ0aTldKX0IVDJMvhw4jxC0gA6RihCWgob4/du9K8+6xObdGzArc+p+6qBGhjUwijyPGrHVU8YgfXkS4nMqCGHdV4ZckidxDYl5rmVR17x7as6cSyMeIG12rIvK9af9Ddeep6u8VXe6kw3Dw1du6P1SgOihXZNEA5/SjDM1dwlvPQPYO2pcW0SMKE73utX4hhtWncv9DpjphJr5zGOgY24sd7ZQGdAdEIFXiSabmmfaJZPJuGQY8Pps5axrcfX74nonvRJ+Ezsb+1qMZXAAroRzNiKcM0vv+YgdMI7jh4WeB1LZigQIXkNoMeh6Y36uIBaUW2hkDqux7ht7zGd9t6/BuCnZhH/Lv3JAPi7Zq4l3t4r4Z4UHfC2J2scUOZeZOBtObKPw4yYBccj8FnyMlVZA=","categories":[{"name":"随笔","slug":"随笔","permalink":"https://www.sanks-blog.com/categories/随笔/"}],"tags":[{"name":"随笔","slug":"随笔","permalink":"https://www.sanks-blog.com/tags/随笔/"}]},{"title":"如何使js进入休眠或等待","slug":"javascript-go-sleep-to-wait-next-execute","date":"2020-08-22T01:32:38.000Z","updated":"2020-08-22T02:08:38.751Z","comments":true,"path":"javascript-go-sleep-to-wait-next-execute/","link":"","permalink":"https://www.sanks-blog.com/javascript-go-sleep-to-wait-next-execute/","excerpt":"引言在做react的转场动画时,偶然想得一个解决方案中,需要拦截 React-router4 中 Prompt 的操作时,想让进程休眠,来执行转场动画,遗憾的是没有成功,但是意外获得了新技能 - javascript的休眠方法 如何编写sleep函数不卖关子了,直接上代码,不过需要你自己去验证,正所谓授之以鱼不如授之以渔","text":"引言在做react的转场动画时,偶然想得一个解决方案中,需要拦截 React-router4 中 Prompt 的操作时,想让进程休眠,来执行转场动画,遗憾的是没有成功,但是意外获得了新技能 - javascript的休眠方法 如何编写sleep函数不卖关子了,直接上代码,不过需要你自己去验证,正所谓授之以鱼不如授之以渔 方法一1234567891011121314151617function sleep(numberMillis: number) { var now = new Date(); var exitTime = now.getTime() + numberMillis; while (true) { now = new Date(); if (now.getTime() > exitTime) return; }}function test() { console.log(new Date()); console.time('sleep_times'); sleep(1000); console.timeEnd('sleep_times'); console.log(new Date());}test(); 实际上,该例子不是使js脚本进入休眠,而是因为js是单线程,while(true){} 死循环调度执行, 使得 whlie(true){} 后面的程序被阻塞,从而实现休眠的假象。 方法二通过Promises,async 和 await 的功能,你可以编写一个 sleep2() 函数,该函数将按预期运行。 但是,你只能从 async 函数中调用此自定义 sleep2() 函数,并且需要将其与 await 关键字一起使用。 123456789101112131415function sleep2(time: number) { return new Promise(resolve => { setTimeout(() => { resolve(); }, time || 1000); });}async function test() { console.log(new Date()); console.time('use_times'); await sleep2(1000); console.timeEnd('use_times'); console.log(new Date());}test(); 此JavaScript sleep2() 函数的功能与预期的完全一样,因为 await 导致代码的同步执行暂停,直到Promise被resolve()为止。 总结:以上两个方法请自行执行脚本,来看看执行的时间误差上有什么区别,哪个好就用哪个(最后还是卖了个关子)。","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://www.sanks-blog.com/categories/JavaScript/"},{"name":"ES6","slug":"JavaScript/ES6","permalink":"https://www.sanks-blog.com/categories/JavaScript/ES6/"}],"tags":[{"name":"ES6","slug":"ES6","permalink":"https://www.sanks-blog.com/tags/ES6/"},{"name":"JavaScript","slug":"JavaScript","permalink":"https://www.sanks-blog.com/tags/JavaScript/"}]},{"title":"前端框架设计及技术预研","slug":"front-end-framework","date":"2020-06-25T11:38:37.000Z","updated":"2021-12-26T10:17:06.541Z","comments":true,"path":"front-end-framework/","link":"","permalink":"https://www.sanks-blog.com/front-end-framework/","excerpt":"权限拓展 针对以往的权限控制职能到达页面及按钮级别,结合nodeJS中间件(路由代理),做出了延伸到接口的权限控制,概要图如下所示:","text":"权限拓展 针对以往的权限控制职能到达页面及按钮级别,结合nodeJS中间件(路由代理),做出了延伸到接口的权限控制,概要图如下所示: 权限树结构: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869{ "resultCode": 200, "resultJson": { "code": 0, "msg": null, "data": [ { "id": 1000, "api": [ "/api/token/getRefreshToken", "/api/login/phoneValidCode", "/api/login/imageValidCode", "/api/register/saveUser" ], "parentId": -1, "children": [], "name": "登录页", "path": "login", "sort": 0 }, { "id": 2000, "api": ["/api/register/isExistUser"], "parentId": -1, "children": [], "name": "注册页", "path": "register", "sort": 1 }, { "id": 3000, "api": [], "parentId": -1, "children": [], "name": "首页", "path": "home", "sort": 2 }, { "id": 4000, "api": [], "parentId": -1, "children": [ { "id": 4100, "api": [], "parentId": 4000, "children": [], "name": "客户列表", "path": "list", "sort": 0 }, { "id": 4200, "api": [], "parentId": 4000, "children": [], "name": "客户详情", "path": "detail", "sort": 1 }], "name": "客户", "path": "customer", "sort": 3 }] }, "resultMessage": "获取权限成功"} 接口白名单配置: 12345678910module.exports = { //免验证路由 FreeRoute: [ '/api/login/signIn', '/api/auth/resources', // 获取前台路由权限的接口 '/api/login/phoneValidCode', // 获取手机验证码接口 '/api/login/imageValidCode', // 获取图像验证码的接口 '/api/register/saveUser' // 保存单个用户 ]}; JWT登录机制 JWT我放在了Nodejs中间件层,JWT的实现原理如下图所示; 工作流水线 在这里我做一下简单的描述: 前端代码请求接口->中间件路由代理层接到请求(GraphQL)->控制器层(controller)做逻辑判断等->从mongoDB数据中抓取数据 这一流水线的好处是可多处拆解,可抛弃最后两层,直接与后台接口对接;如果有可通过nodejs服务来存储一些简单的数据的话,可使用整个流程。 打印日志功能 后台的服务通常都有日志输出和存储,为了查找项目开发过程中的问题,或者是上线后存在的性能瓶颈;nodejs服务我同样加入了日志打印功能。 未完待续…","categories":[{"name":"Framework","slug":"Framework","permalink":"https://www.sanks-blog.com/categories/Framework/"}],"tags":[{"name":"Framework","slug":"Framework","permalink":"https://www.sanks-blog.com/tags/Framework/"}]},{"title":"通过例子解释防抖动和节流","slug":"debounce-and-throttle","date":"2020-05-20T15:40:45.000Z","updated":"2020-07-29T11:46:25.270Z","comments":true,"path":"debounce-and-throttle/","link":"","permalink":"https://www.sanks-blog.com/debounce-and-throttle/","excerpt":"译文说明作者:David Corbacho原文链接:https://css-tricks.com/debouncing-throttling-explained-examples/ 引言以下是伦敦前端工程师 David Corbacho 的客座文章。我们已经之前讨论过这个主题,但是这次,David将通过交互式演示来讲解这些概念,使事情变得非常清楚。 Debounce 和 throttle 是两种类似(但不同的!)的技术,用于控制我们允许一个函数在一段时间内执行多少次。 在将函数附加到DOM事件时,具有函数的防抖动或节流的版本尤其有用。为什么呢?因为我们在事件和函数的执行之间给了自己一个控制层。请记住,我们不控制这些DOM事件的发出频率。它可以变化。","text":"译文说明作者:David Corbacho原文链接:https://css-tricks.com/debouncing-throttling-explained-examples/ 引言以下是伦敦前端工程师 David Corbacho 的客座文章。我们已经之前讨论过这个主题,但是这次,David将通过交互式演示来讲解这些概念,使事情变得非常清楚。 Debounce 和 throttle 是两种类似(但不同的!)的技术,用于控制我们允许一个函数在一段时间内执行多少次。 在将函数附加到DOM事件时,具有函数的防抖动或节流的版本尤其有用。为什么呢?因为我们在事件和函数的执行之间给了自己一个控制层。请记住,我们不控制这些DOM事件的发出频率。它可以变化。 例如,让我们讨论一下滚动事件。看这个例子: See the Pen Scroll events counter by Corbacho (@dcorb) on CodePen. 当使用触控板、滚动轮或仅仅通过拖动滚动条滚动时,每秒可以轻松触发30个事件。但在我的测试中,在智能手机上缓慢滚动(调换)可能会每秒触发多达100个事件。您的滚动处理程序是否为这种执行速度做好了准备? 2011年,Twitter网站上出现了一个问题:当你向下滚动你的Twitter feed时,它变得缓慢和没有响应。John Resig发表了一篇关于这个问题的文章,文中解释了将开销大的函数直接附加到滚动事件是多么糟糕的想法。 John提出的解决方案(五年前)是在onScroll事件之外,每250ms运行一次循环。这样处理程序就不会耦合到事件。使用这个简单的技术,我们可以避免破坏用户体验。 如今,处理事件的方式稍微复杂了一些。让我来介绍一下Debounce, Throttle, 和requestAnimationFrame。我们还会看到匹配用例。 DebounceDebounce技术允许我们在一个调用中“分组”多个连续调用。 想象你在电梯里。门开始关上,突然另一个人试图上电梯,电梯没有开始它的功能去换楼层,门又开了。现在这种情况再次发生在另一个人身上。电梯推迟了它的功能(移动楼层),但优化了它的资源。 你自己尝试一下。点击或移动按钮上方的鼠标: See the Pen Debounce. Trailing by Corbacho (@dcorb) on CodePen. 你可以看到连续快速的事件是如何由单个已删除的事件表示的。但如果这些事件是由巨大的差距引发的,那么 debouncing 就不会发生。 Leading edge (or “immediate”)你可能会发现,在触发函数执行之前,debouncing 事件会一直等待,直到如此快速的事件停止执行。为什么不立即触发函数执行,让它的行为就与原始的非debouncing 处理程序完全相同?除非快速调用暂停,否则不会激发处理函数。 你也可以这样做!下面是一个带着Leading标志的例子: 在 underscore.js 里,该选项被称为 immediate 而不是 leading 自己尝试一下吧: See the Pen Debounce. Leading by Corbacho (@dcorb) on CodePen. Debounce Implementations我第一次看到debounce在JavaScript中实现是在 2009年 John Hann 的文章中(他也创造了这个术语) 。 不久之后,Ben Alman创建了一个jQuery插件(不再维护),一年后,Jeremy Ashkenas将其添加到underscore.js中。后来,它被添加到Lodash中, 完全替代了underscore。 3种实现在内部有点不同,但它们的接口几乎是相同的。 曾经有一段时间,underscore 从Lodash中采用了debounce/throttle实现,在2013年我在_.debounce函数中发现一个错误后,从那时起,这两种实现就分道扬镳了。 Lodash为它的 .debounce 和 .throttle 添加了更多的功能。原来的 immediate 标志被替换为leading 和 trailing 选项。你可以选择一个,或者两个都选。默认情况下,只启用了trailing 。 新的maxWait选项(目前只在Lodash中)不在本文中介绍,但它非常有用。实际上,正如您在lodash源代码中看到的,使用_.debounce和maxWait定义了throttle函数。 Debounce ExamplesResize Example在调整(桌面)浏览器窗口的大小时,它们可以在拖动“调整大小”操控时,会发出许多 resize 事件。 在这个演示中,自己尝试一下 See the Pen Debounce Resize Event Example by Corbacho (@dcorb) on CodePen. 如你所见,我们使用了resize事件的默认 trailing 选项,因为我们只对最终值感兴趣,即用户停止调整浏览器的大小之后。 在带有Ajax请求的自动完成表单中输入为什么要在用户仍在输入的情况下,每隔50毫秒向服务器发送一次Ajax请求呢? _.debounce可以帮助我们避免额外的工作,并且只在用户停止输入时发送请求。 在这里,把leading标志位打开是没有意义的。我们要等到最后一个字母打完。 See the Pen Debouncing keystrokes Example by Corbacho (@dcorb) on CodePen. 类似的用例是等待用户停止输入后再验证输入。“您的密码太短”类型的消息。 如何使用 debounce 和throttle,和常见的陷阱创建自己的debounce/throttle函数,或者从一些随机的博客文章中复制它,这可能很诱人。我的建议是直接使用underscore 或Lodash。如果你只需要 .debounce 和 .throttle函数,您可以使用Lodash自定义生成器来输出一个自定义的2KB缩小库。用这个简单的命令构建它: 12npm i -g lodash-clilodash include = debounce, throttle 也就是说,大多数人使用模块形式“lodash/throttle”和“lodash/debounce” 或“lodash”。或者带有“webpack/browserify/rollup” 的 “lodash/throttle” 和 “lodash.debounce” 软件包。 一个常见的陷阱是多次调用_.debounce函数: 1234567// WRONG$(window).on('scroll', function() { _.debounce(doSomething, 300); });// RIGHT$(window).on('scroll', _.debounce(doSomething, 200)); 为debounce函数创建一个变量将允许我们调用私有方法debounced_version.cancel(),如果你需要它的话,该方法可在lodash和underscore.js中获得。 12345var debounced_version = _.debounce(doSomething, 200);$(window).on('scroll', debounced_version);// If you need itdebounced_version.cancel(); Throttle通过使用_.throttle,我们不允许函数每X毫秒执行一次以上。 这与debouncing 的主要区别是,throttle保证定期执行该函数,至少每X毫秒执行一次。 和debounce一样,throttle 技术也被Ben的plugin, underscore.js和lodash所涵盖。 Throttling 示例无限滚动一个很常见的例子。用户向下滚动无限滚动页面。您需要检查用户离底部有多远。如果用户接近底部,我们应该通过Ajax请求更多内容并将其附加到页面中。 在这里,我们热衷的 .debounce是没有用的。只有当用户停止滚动时才会触发。我们需要在用户到达底部之前开始获取内容。用.throttle我们可以保证我们不断地检查我们离底部有多远。 See the Pen Infinite scrolling throttled by Corbacho (@dcorb) on CodePen. requestAnimationFrame (rAF)requestAnimationFrame是另一种限速(rate-limiting)函数执行的方法。 它可以被认为是一个_.throttle(dosomething, 16)。但它的保真度要高得多,因为它是一个浏览器原生API,目标是更好的精确度。 我们可以使用rAF API,作为throttle 功能的替代,考虑到这些利弊: 利: 目标是60fps(16毫秒的帧),但内部将决定如何安排渲染的最佳时间。 相当简单和标准的API,未来不会改变。更少的维护。 弊: rAFs的启动/取消是我们的责任,不像内部管理的.debounce或.throttle 如果浏览器选项卡不活动,它就不会执行。不过对于滚动、鼠标或键盘事件,这并不重要。 尽管所有的现代浏览器都支持rAF,但IE9、Opera Mini和老Android仍然不支持。时至今日,仍然[需要]我们增加(https://caniuse.com/#feat=requestanimationframe)[一个polyfill](https://www.paulirish.com/2011/requestanimationframe-for-smart-animating/) 。 node.js中不支持rAF。因此,你不能在服务器上使用它来控制文件系统事件。 根据经验,如果您的JavaScript函数是“绘制”或直接动画属性,我将使用requestAnimationFrame,在所有涉及重新计算元素位置的情况下使用它。 要发出Ajax请求,或者决定是否添加/删除一个类(这会触发CSS动画),我将考虑.debounce或.throttle,您可以在这里设置更低的执行速率(例如,200ms,而不是16ms) 如果您认为rAF可以在underscore 或lodash中实现,那么它们都拒绝了这个想法,因为它是一个专门的用例,而且很容易直接调用。 Examples of rAF受Paul Lewis文章的启发,我将只讨论这个示例,以便在滚动中使用requestAnimation框架,在这篇文章中,他一步一步地解释了这个示例的逻辑。 在16ms的情况下,我将它和 _.throttle放在一起比较。提供类似的性能,但是rAF可能会在更复杂的场景中提供更好的结果。 See the Pen Scroll comparison requestAnimationFrame vs throttle by Corbacho (@dcorb) on CodePen. 我在headroom.js库中看到过这种技术的更高级的例子。在这里,逻辑被解耦并封装在一个对象中。 总结使用debounce、throttle和requestAnimationFrame来优化事件处理程序。每种技术略有不同,但这三种技术都是有用的,并且相互补充。 概括的说: debounce: 将突然发生的一系列事件(如击键)组合成一个事件。 throttle: 保证每X毫秒执行一次。比如每隔200毫秒检查一次滚动位置,以触发CSS动画。 requestAnimationFrame: 一个节流的选择。当你的函数在屏幕上重新计算和渲染元素,你想要保证平滑的变化或动画。注意:不支持IE9。 个人补充:防抖动(debounce):所谓的抖动就是浏览器频繁布局时,由于算力不足导致的页面颤动现象。防抖动就是利用类似于节流的手段——无视短时间内重复回调,避免浏览器发生抖动现象的技术。 比较常见的抖动场景是在 auto index 的搜索设计上;当我们在搜索框内输入不同索引时,页面会频繁计算索引并渲染列表,以致产生抖动。但事实上在这类场景里,有价值的请求只会发生在用户停止输入后,通俗来说就是用户输入过程中的字符串不必当真。 节流(throttle):节流指的都是某个函数在一定时间间隔内只执行第一次回调。 总结:前端常用的节流和防抖动技术,他们是 JS 闭包和高阶函数的现实应用。","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://www.sanks-blog.com/categories/JavaScript/"}],"tags":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://www.sanks-blog.com/tags/JavaScript/"}]},{"title":"connected React Router","slug":"connected-React-Router","date":"2020-05-06T02:16:42.000Z","updated":"2020-07-29T13:43:17.861Z","comments":true,"path":"connected-React-Router/","link":"","permalink":"https://www.sanks-blog.com/connected-React-Router/","excerpt":"前言最近在使用 Connnected React Router 发现神奇的问题,居然没办法正常控制路由,身为专业的工程师,我们发挥福尔摩斯的精神,抽丝剥茧的找出问题出在哪。 什么是 Connected React Router如果你使用Redux,那么在 Redux 中使用 Side Effect Handler(像是 Redux Thunk、Redux Observable 或是 Redux Saga)做「流程管理」的时候,那么你一定会有一种情境是 如果你做完某件事,把目前页面跳转到另一个页面 如果你也使用 React Router,那么 Connected React Router 就是帮你在 Redux 的流程中也可以使用 Router 的功能,(像是 push、 goBack … etc 之类的 Api 帮助你管理路由) 也就是说,Connected React Router 是帮助同步「Router」与「Redux」的状态,帮助你可以使用 Redux 管理 React Router 的工具!","text":"前言最近在使用 Connnected React Router 发现神奇的问题,居然没办法正常控制路由,身为专业的工程师,我们发挥福尔摩斯的精神,抽丝剥茧的找出问题出在哪。 什么是 Connected React Router如果你使用Redux,那么在 Redux 中使用 Side Effect Handler(像是 Redux Thunk、Redux Observable 或是 Redux Saga)做「流程管理」的时候,那么你一定会有一种情境是 如果你做完某件事,把目前页面跳转到另一个页面 如果你也使用 React Router,那么 Connected React Router 就是帮你在 Redux 的流程中也可以使用 Router 的功能,(像是 push、 goBack … etc 之类的 Api 帮助你管理路由) 也就是说,Connected React Router 是帮助同步「Router」与「Redux」的状态,帮助你可以使用 Redux 管理 React Router 的工具! 举例如果我们想要在发完对 Server 的更新资料 Api 后,跳转回「首页路由( /home )」,代码大概会像是底下这样: 更多关于 Connected Recact Router 文件请看这里 发生的问题我的状况是,Web Application 都根据 Connected React Router 官网文件正确修改接上,Component 上的 history.push 可以正常运作,一开始进入页面 Redux 也有听到 @@router/LOCATION_CHANGE 的 Action,但是却没办法使用 push Api 来改变画面上的路由,整个前端程式也没有发生这个 Action,也没有任何错误发生。 小技巧:要如何知道前端程式有听到什么 Action,可以使用 Redux Logger 这个 Middleware,但记得只在开发模式使用就好! 解决方法由上述问题来看,因为 Web Application 有听到 @@router/LOCATION_CHANGE 的 Action,对于 Connected React Router 修改应该是没有错的,而且没有错误,也能用 history.push 方法,大致上的 Code 应该都没有问题才对。 经过一番思考后,想我有一个习惯是,把 createBrowserHistory 的方法包成 history.js ,会不会是 history 本身有问题?一查果然,我在定义 的 history 与 Redux Store 所使用的 history 居然是不同的档案,所以造成在画面上可以正常运作,Redux Store 的却不能正常运作 结论Store 跟 Router 必须使用同一个 history 物件,否则会有其中一方不能正常运作,如果以后还有遇到必须要先检查一次才行,记录一下。","categories":[{"name":"Redux","slug":"Redux","permalink":"https://www.sanks-blog.com/categories/Redux/"}],"tags":[{"name":"Redux","slug":"Redux","permalink":"https://www.sanks-blog.com/tags/Redux/"}]},{"title":"前端感悟","slug":"front-end-comprehension","date":"2020-04-16T12:11:40.000Z","updated":"2022-01-03T14:47:09.772Z","comments":true,"path":"front-end-comprehension/","link":"","permalink":"https://www.sanks-blog.com/front-end-comprehension/","excerpt":"Welcome to my blog, enter password to read.","text":"Welcome to my blog, enter password to read. Incorrect Password! No content to display! U2FsdGVkX19dTW79xGAIHW+4Y/RZ5TrQq+R75S6byRQwgD2s8jEEC3QzqNh4QGiDw4TWt99EdT/FTppuKVV+HODs/MZMWfPP9VlMyGyFEsuDzeiRNRTvb22uB7C4Ld6iP4m4MObPE8rFieXX74OdrYKD4P72a1lXDt/e74zD45naejyALJIhqYMDt1nWaIqvwVnwlGVg2N5FDkW++kkY0mRqw3s6sZLV+0CAdQIrXPIC/uISwwzB4suN1ieDjR+0vmcGfLoxZM18cCDUBlKbNg5prdSoGb+dk2NpkzVyhst0/7S6esruxCaSgrE/BvQQ5jrwlkDF9RMjSP2bPGkNLDIHDL6fAyNtk0vFxKNgfoDmkJJhN/pO6ny8qqEpOyoqyjcSXb89swkDSglddvEBuWke5o4oNN0PHo2qiMZaUJOXMTaxJqHhePyDRlYZbdyhZC/Vft9yN+eRQ/mTUCGy10luIM/fiBFurvyUmsckJI4HafDlPHDIBSYqbiUBYaSiPhjbuUy8qyYx0HP9wG4BABQeURd5GZ3M0xkMYISbssXjeeDmPu0b+f5k558nhkIx3a7aAJj43ALyfAQifIT2ul0Jms4QvuSvRfoqcrHN/iZWcj4/6UICYXUAYdFIM4r8VhX9zQ/sNhV2ehoG/ng83HM8ZxsUH+uEWWXUymrYJsyrWsAh7XuNDK5bQBlVH14a05GG1cydqW8gsIWe/M9vbQtvkR4YdDG07TXXfucEhsEdocKo0CefWOH79eCEVVu1NhuYWPmRRC1xW2aLsmOAgoZN9wvOLzg4LX1ZU4sLUnf9EZKLdcPBU1ni1b+n6ts25Zc/YJ2ro8JdnSbNwxHj8lZGqt05z3tItfMsiOGr/NEyEVmnw8P7MnnINTeldniAAN5SN0Ma7KjOVk1QfquAByO39B3AtQBUlQt0IpmBGc09IrDuiPn9tca4Ghqm4E45IJCyNOF7htXtbmi5uilvLOUrRc4yk+mK7ZbEyjwjaYfJqLhDswj3vfzy2GKg8zzp58sgSHRL/LYva5Y7UN2kVLKIusCkal8ShBZOwJLaUUte3p6L1hRg3nTY0p3I43qe9q4KlzvVj9eTuZ8CetjoByJcWDg4QG09+ZNqLazwJSgZdgj4BCFUlcz0YqT9hhtvkYQb9Zr9Fq87bMHOwSG9FDVJ0BD+B06cs/VY5QZ3b6DqL0WPE/hi3vURnWRlwysbm7RDxfp4y5ogb4TCY3NCT3aIM09R+WJJQq2qsTKh+sWzJv9nSNUKvIFMX4j31bkVtwRvTwFTTFvky3g1wIpmJzHrNF0u0Sh8BVZk34BprEdRiF0pHLkn+Crudm9WQIY+dOotVEupMJikSOWpN3zPVjGfBipk2DwSUbPqXpIfeUbKVlv7VsN9anAAIbk5OXQDFHTNSxvf4acDPhRSjPyGoG3hHSQ7h1eeZEiCPtPDgigEVkAXOkBY/Msrof1csHF6x9a9koZmAIgMRvohfZR8lyNNfPHPAmihFT7I8qxsS3FUjHw3N0u62FzqoHjEhKgUlh1uRUwrvJxDl4qHL80Q2Sd7VJ4vTdjwR9N9peRawSbzSW2D7Bz7epaD7bU3gLfIEFanvDg3OFmboORt7xhzw2fRKrIdqLCVDqXuD6TdY/t2TGhtoAHxfV9dbADEE7/zZjXxFqfxuWeEXE8TKIxsu703uQjwJVvAt70/CN8s0mlAPdNoc51bpteRLgz1CMkkq3siOwcCBPSyUXUCuDRYVtsXL6o9Zcw/QsbMOigNy1WLZLRCeKgqj+KDm1JClDIjytUWSwj4V201RkcRwjvuPW/DU3DwSNVKOSm+k+Uww42D+16BTyGjN3YeL53nGpy2p7rCCrY/ZetF5BmlUQWc+F23SKjhW/uz9L5F59/Rw326QQp+Iy4NXbZSh+fXa8HUoywEe67g8aakTbw1bFGs/xKhDk9zwGC+TuBo8oCD/8uQ9kVsQ2Lk6WOlwbSTo7xU1hPNPmsFhnQlDDiezHqS2f58bbnLB0kfg0+pkh87nyfM6VlP7Wu7duMVd72icjr+4SCwxyQX9FtcofyKWn/h3HIXF5L8jLAlKLBOdh/Rk/hY/Sdl0/Ph/JNAJobsmhq2B2OQWUkZSDLeJlkOclVF8dsvjIQeUWaY8xp6o1MIdROJuFNLR6YvSAmiCve16zkmL9lbUEVJetHMbzcF/jDrjHWY1t6d5OsonQeeS6KK8Yi2LTLubXE9fJz0Y0Vrp42TAbdQyxLvwix9R3rB5SKqE4PbRnIXfvHohxYqk4f6lN8Y680Eb3Ju4BtIhbS5CPO0NpoDOkQr5k695NQNyCkbRLO4j93Pu5pwYc77J9wvlU1lXfRCPjzRYvjMpu8lopkQCjzz7NWtahQfhKfCeciye4At+Qqr2R6Aue3WAiocOxS+2v6ll2D5t5I0Bo7rSnVs4M7mNn6FCNebHSPB5W1daemooUZA/9LBbsIiZ2+w9IGmj/o+RtSVK17Qzux0UbKP1cxTDvByrnl6uukPkwR65sSIFLxdz1HNIBtyUeaBIueUXJM+vvdKRIW0qxjnteRBb8Piq5AhpERBogSIHwWbzfz0Kt1qOiP2v+VwIBjKN2TZeMXRS1HwVaGUUiNA8AQ05F+x2/JSwflhQhur1oU835I1XcXJtuYeq8MLoJ8dlryJXVG0coymp8rDxTj2KhGCbgPE2VkfFX5vHy5rQa8JvYWWQg2lkLyLNsUaXUt8sInPNhSSoOYGKFrTUwKdU3tJvw9XXKL8n7QLw00d8MrdNvPAwt/GfYrwMFvTXYmsr0De35bRIPY8s/l/JrUVbguq96VEdw/vCFqmei+zrZd8A+ykZIPzanVnYulYliALzHTvUdq2tmyJOo9mChtVM65xAvo9IwL/t1yvVh0OnPQxbVfkf+aJbEt4mvDafslwxyoZyumPCoc4MO41vz7LM/sft6D2w7Jim+wmBfqJGrsz4Tkksyrt2+Rcvi1P2o+vjt2RDRWka73DiU/dFA+ShOBn6uGfBiPYuaD+WlGScxxXpq7xwXdsJKerI+7u26ihwjguWvgVL4EtOTLH5BiXo2KR90U3ojLzfSYnChNN7qi4QLmrthrjvz2Uju6ZqQUFIBuLqhPW1Qi9+LKM7woZOfn5yQ8Zw9nQ2+zmx63uTBoaQhFtVARZ6LTX0EuELvrcp7PRnIGT1jGeBvHMvusbffXvylH4AwLs3gCuhogPHAlCpj2fL8U5RT7N/A17QrFztDeF3aAX1CZ74Fyk5jAt1NXf1nrHh7DdOiORqDwxJdGIm2LdvENjBlOt1SOb9gQOSVXolFVng0zcmpwRmU7HtoBlQckMV5os4BvmEVSMdUptLaWTxzkeUewySLNfwpmtXLxENgPOpUi4P8EjecJ3Fodedx1zRLdMLJuS6WeyaHekA1DiyqvHbT7u58xt/ABN+ojEDipZ5IPcb7L1pFxTUHE0LuZee++WaLztP73E8VwJA5CXDLc7jHVQkVA85UG9EYrx7F28TjblUOS7e1pE1QozGmJiG6clVn7g2KiVdimyTp3683fMV33qIdAhUkimDrWALh4bJB5rcRdVxqiz2MCjLX11ervDH0DHmj3DMwBTOqj5jG9qEwiWU42dseqAlqFE4TDPbBCfGPNpcu9OqDiyfKTCRSmItnP67IRdB+rTVjsjfYemrW5xr/cBa+FNQVlabHODBBu8xYLk1w4aFx7U/6leydM2B0sf7nXtIuI7OfTguZFnSpNZ/H/GUGpZuKlJymT5jWUWxeZYlfhI2yr48MgYKUqBzuhSuPTXmxxCTkHHcxQYTmSs+xW+Cbo3S9dCMwt+BCCR6RU8XOataUqqkTEefKxSWzn/RKKV7EuNRZzmYfBYCjiTJG/uQLYUqv8ahU58UcByUyjC01hkaABIxSz3zTIWrrEjGFmAgMQYpOWQvwv7jvCbOSaAxJr+zJTaQ7J23Gcxz4ahF4cX5KXvISsFSnCXx09jNxRQmCLC6Ldulmj2zEODUMPsb6KEenrxLF194WIRGNueSfOyY9ACschmtLUiHSAx07t/sjPmwl00RYIYqSaeqocHN6nf1azGaOm4Fgb8vKLdvQjZ/0E19BlPIoaMcOluTAzcK4PVzOYwnSIODHZRCpYaGU6QgHVw0DQjGIn5epQCcuwPnq8BDDDInrFC73sGuIZxrFn8Ipy43hMvHH5PzCMBTtm/GD3n/vBCGQiH3Ch+r95jlPSBxdsM9SPCTc+QnW/oawHI/2nfkHZcU6Hc+QDnNAI0RpkSt2Ph57qExbfpzWk51yYKzHUvGx5v1kSppkr4EIIMJsXFdWJbp4AfLcYVDiXgRO531k35Xvg1ZgOiByWk/yT/Xbs3l7id1nacwlOYGNJI20pp6asgYzgkPlEQj3bN4r3BmIPYFXUtMetljy6v5+zKfKGxi4vAZ4S6Ypgbb8ydMhX1YF7StqjjtpaDhYiS3yj7XupHgSknVmcsD15tJfiDEntVWujOMBzjOC9mZrQU6XX795kv9vhyjpPoJZ94FZggDpDZ7movyxf/Uw3gZNVQgX1iCh4jkTaLsRh3wrvt7fvZkv7Rxa+6FS50l1SQBNmIYtRq+jFPIDAEDQ9sO0zuA2GXQ59VPvyuwwwkBekWxDW8hgFRuUCa88jDekesIMywF8W7opOFp60D9C90Cj9/cKuw4KBJyJMUdAGQFjmq+hztrjlpOInmhGXNY3LiDOz3FzHhhsP7DU/+HaYTdKoIJvhTDsI227AdUIh0RmdAnD5vjG/om+L76KZzvDs7MGYiZ+LOTDgmwjoaISZG7rrAwn5ST4ni1DMk4FZmFaIiwySkjVWv+blSipNVxkrcNTovbyYtpU1u9SJ6iIRNrs7yWt/h6S24BaJZveb+22ywfHO5gLCUnaqHRo5SH5qIb93cz4+iR93VDUldGCtzGvR/8vbbMk0Po8I71FFxKD12cA9UI4amTv2CTd3OgppVfgqerVZwq4CQF+OHIY7YfOzjSjxaqBmLdV48ZoE+6nzRTdr8nhI7kmutd707rvFO1P/Ytow0dBs5v4xr9s76wiebiBqxI99bkRh33DMeD0rubEE0cehB3C0MzHdA2diRo47hwtqS0+b+NwmmaEsWjRv496MiEInqIkCeUFmG6gwAWvh+CCIvYF8tdkk7vth5CgMcRUYwBhq5elWE9/ktLIoxU34qsXfanus3XMKolcbXmSdbaacsyJrXnzrMYVeGCYJ6aJ2y0j48HCicSOcikPPFtgdrsMLBhdWQq7BgC6m7rphKS7cX0x96/9SE9W8jnjTm/oI9TB6av2MDBocEL2PMNnsRUy4cniFIjRwuCVNg4hfv1dPicKcEFRdPmZO6gzdR7J7bNVV4Dyb+hGOpI7F5gJ2FMPEUquqRjFiHHTpl+MyIngoYhqqj/jXb+mJiF3kquyCg3tHiLvs7vmewsovIAnnzcDjA0piCA8mhuaAIWBMdez0IZg7OrEDLuwKUsM5HLy0bhuJMu/JkeB9ENsuCE6tu3hlOpLv9FDc2OghHJx3SoSnRqxHNE8MsZrGtrP9AjqWvcq8SFDcCi8Uxcv2cNzEcpkLkCq/0DZPWOVIp4eqmVbM2XK05O9T5p5mDf4/cus0+d+ygZtTHjs0GWJsYDDdgmkCRcPgT8bhb4ji81HcYtHegaq7NYeIJnbNnahKefJgwHEQFV23JX40jMin3IuxT7JOUh7E8Y3CqfrOoSwdcO1kdS/c8Yi71+LvegU8vYnpFO21OWVSUdWMn6xUzKatJ+eHrzAaCxB8n1dU4Vklfh11uO9NDWuRVz0o0NXjOuOS3A92y9282yazOdDCCd4RpzpOIML6rIoHGjDPv+LPThzyr6DkIezaOKwRZ4vkHsQ9RXyw1dt6twwQve0DqV0qJlh7fwH2IBv7wrsbTPFTAwYkGmQidd/FDdgpDoEgorEHOwfDJXaFExx3lsNTqHXdGfIwU0GnpENu00p3vjsxXR68dhnZfK19jhG3g5ObPnRe5WWVNowAgxlsdFBeVv1H2gdsUfXgiDGlXIxOYcYO0DxvE4fbtPVBFvNObS8wuFrg6vNfNt+jDd664KIBlKPxUCkEStJgfWHrUy6L+KpStTz6WpUkafKf4/AjGLtJRDBJfWMebqVbCwCGCDlTcOAYCdUXWl0YDki3iLWhpayc2SreZTcvWWzFpQnDllJQIxrGcbM5FZvJUExWH3tQ+pN8oFe6hTFfA6k7BbHw94seKiIEC+7ZwS/ks5iC+kQck6MpLDrLpbOXxzlCu5YaNzKHIdOTUNnmyUZmwRwWAykcYz8/v1ryfXqeQcQ5q/p7G136c/P64pwqzOWBVhcblQj5FYEpZbpAhqnL+29uZRMC5B1VUzLru2pbKdNu+XlNg6ZLbDemEGPhL8PQxehG9q9VPLCMS6Cb7b/tMWTwbGd5bYq25M4lWZ0Sj8vmHRq7aep8kEqOwY9im1UdIWuzl2HcWV6csnMScOVejDZhg+TZCuB0GVq119z62mAj46CEjoESK79wV+P4UmpfXmrpybSth3QejAetNW+RB3eEhlvAUJh8gTWUCmd1DzT354fFoWADSi/QPyBIAugjGJSLc7D1yeizyRG0oQlzFockkJuU4iU1Yu30ofnLsfxKKVlRIxrjjG0jxwK4vUMNBv89BJI53yl4yaF9MiiEGzbb00LZxjmOBHLr65XuiC8ktNR1mdSwPqNO3Prm3AUSBzuxFk7FWbtOl1mvbBksFlLNqnLwXWRbo0qpvgyjf463ZakIUnfKeyutB/ls70uDFujjBNktgS6X0hysfiZAhWKS/hlgoJKfEAs5ZK92O2oowK3xijV8WJ9VcrK7mo2TYC4UMm5b1rx+ezEzU5Q2BzI1+35LGFtAHXMGWSHbVyr1JrUQNf0lP+aV5u83wF1a15dqVTnQeRDbyPlijfcM6xLvkNmDMSE9nn02kwf1XUIcFLfIRssKdINqQoeKAX6gyamYlYGjZxDB3E8DyiiOjsO5ZmnKovTxDhbDh20TyGhH11/r8mga5IrdJ0MuE7H9biOmbsydQw2B1DEHkrvqU49/j09Ke7vKVtSMQktDEAXFEKwJiRkiiZKJ5UBahxxXcEVmtTfGBkbTURnSpEoPFfaC0SLGczKIR3rzufNb/7/ABnwn1XSKyZN255lp3d6S9PUDXhNp1bqSoFtiIQMxnhukfh9qU7tGrtMtCgUUk/BcgDBATU7ilcHVP6sSt9XBjanGx2+7gPMIE9Pdro5C+V9sqYpL1AB1a2WP4Slf2mwti6gL19ndmP44HGn953XzqYYWQGohMM6v//OOyYiF9bLMJdW85NhQv3RJJA/+Pd9JjS8VwAiGZ4XYAUpb+5/Ipdjzj9CbUr8/i7yEuXOEPNuwXIV1FpS8fmuvaJu0j6Sly8PzOsaYw2KGCpdJVvB6qet6prQFLnAKA8ikjfJmMbqfpR1N6iGOnukoLnZrZPRz5t4wxOxgxX3Fmew1wcb0tN4HBTomM/cMJYzC9xUX1xKfN5OBmUNzxKd7PFfHjoQCL6339grjit+oG+FV3OZZibtHpaiRDmBKoCgw0Zgq8pvln1IUtc8CUbAZTNPUHt02RVaL2TgdmgmSqyCHPWPtCJRqkb9VmgUpcbO1gtotgKEXl9/t6VGjU0zZC8mUMnvW3eYf45EQq0KpBx8YzgWgvaA23hCNnf54+Dn7yocRXu3A3GyKAkZGjzs2Pqr1/gN5MVhjJbf3nQytHxsfgDkuIN6mDq+OZ5tyuzFiUfgMkOp/QQUa54fHXPXh2m55SIdHLQHMe3lGFGuhwOsP/1cYNtJzum4H0U/sIHRdu57qMvyKbyPqPcZZ9xrRzFnwy1xfeNF/C91Z48cF2XJIfVNMo6mrNRLU0Ix8mRgav+pFDNKK9RadCORhMSt4RFBx/k88MxgtRNo0LcxBm/ouV3JaDkN5+VIPEumpiBZtwUOBPujonjXIZEb6KEdoIc5kRGZsMEHrBhVuXUuOltuwn0Ktenke6Ke47E9m/OM0Uqrpss3yjM949IQUhSVajU1tNoK1qxfqg8uJOEYo7XCzkddIHDYUkyioIEHcv3bqehZXsOhWT+VZzIqxJZqF0goZAzRcGycX+bbzrLZXufGNku6ezW8Yf1vGCItGbAtvgSdDNK6AyJqzm3pBDIMoKz9sy8GwfkSKHmbjA0gz6fgcOKAKs2GFEtHWHtNOG/phtGDwz56jZbcnPBk2ULIDPyD3S47vMjJSsPrgFXKZ8giB+AZkKNV6+fWuHDLayQHQ3IL8oiqI/VARONxtUVlqDXDfcOXEedpow5ycCLHsjRIN+2z0cE790uLnRkYh/uJE1nceRtoderiy75mt1cErj6nw0dOijsGSwRFS0O1xWMwf1AifW7EAb1HVTuuvSydHReuTMuljaeV2fJwqCEkfQPoZ7r6YlpGqQ9ZAdbwa+jLFRDdrY6fK4HyZdwc9BEUCcamL05lW7qOsJmB7/IICIOlPUqk5eOU5GEn1tu3ua2XT5tXMljcGLOT2BZDXMpan+jVuubaAE78NvRIn/zV3hs4/jWoLMQmFgAIbhc08pvz7qPG5JIpOQlQmy70TY6rO9M/MJ5/BSE2HG/axCTR5YaBE080lihl/QVxqD+SjyeXeghH9kencYifKz3GfcHKOFSPtxad05fWaV6hn8BqMHDP48CIuVIutUZcah94N9WgRa+cEHNuF5mkkygpH2LyGoc5KZM9NU5zu4v834TYkWU40NA7csW7epku8HsPF6JQJknDYMijLLA/Ysc/RRpdFZPVb2Tf6BYCWvu6G0s1jo8q+iqNOPbOq5mtlv5VnmTAZwOE29nWvFx8yLzUlxAqG68lzT3swVMOis9ijOCIsTPsZTLMTo2LshS+Kk6zJ93W9wJh36P1UwdL9IsjOUW6zxwVZyqE3Oi5CHqB56JO+3SSOf9VBleivYAh/RTo3ugxFA4nfy09hOfgnTLwN3m3cDk3whKzcbvWB2Ovzt0GZsizuT+Tq+PmcFHLrooLA5rAh2X2X5rKoIOM9aEPaVkdXZe0E1kclpj7ambu8Cy3HN9pyb/lxVINCtd0Q/N4YAn6Q3IkvzCyW+VhVTSgQju+LVNRERXwQsH3x34dJo/9NcTfKEsQHDfubCimlAkgDD33/x+96EEGWYefzX3BSH9Usr5tsYe7GLq/4UUA8lFN8t4HGc0II8jjoJink9R0aucKlef1LE0o1zxjDVMyy4xyX9aIWwPL9HhWajW9OwCBa5D04BEz3lBY8sFYDxF4gTqG1tqwi0hKpoQTCq2DLZ/BAqsXB19IjqenoKCRiPQyVrTDwgO7+bExibLNjhxRMqVGtCpaDOIDc/rI49PN3EIUm9h8mSuoHe8WwQVC2aSfNSbFtCgGDsQTdUtNjuHSaBGx8FLPPxglzshJRx2FggdjcvPv+HqHk9yhKFLd9Hlz6SzF8jFy2UF7hpNbQ5nCu894PfTzH8oWOvo6gsMl2735/+9v9sStOKaPLb8aUJMkflukrW3oZJT8PIKiYMyrL7LDjLrRZwTkvpP6p/I5jc0Z52Acon9+/eR0O+6xdyxiJZwnMzWdV06uB0qd/eswBBm0W3Dmtosl5UusMzhd0qTiAu4wnLBKs5rjAz05uQI8UphC+vOlpfvCFW242F6j1EvrXlyB5qWbpYG21B/llYVMoQtk6pVLHSpuOsNc+Zwra8f1vrPMl5/LbiS9Su3oRlJRIRWpFjqZBBS9y0H1ccOuaslvMllscOz+sZFJI4BHckFJCn+VT4cSEY6LwzDrF9OMYkpLgjiQow4AGn86R95E39h7r/K7TECuwxVwA76s5ebsDfn75UP8IP2TCYn+YDyDHzMI6i+INHm4isk9BDT0H6TpvmTGdLwBj4u2HsrR3NlTynY24Iw/jhfZY8G4SsBjuZqIPYvXUgrnufOedt+lxGU2XZXHgJFR9jnyPIHxv3pgzIuGfS57IHq07sg0xoghQ4D5i2AqT4yx6vyoGd/V7LUA3A0ukwJp9/Zb438U8VMiVPksyGy8NzEaBCZv8Y//2xJEVlKkhrcFydmwieIbxVGI2dnY2t+kcOWTbLjh9jFnyTRZ/a4XsMBPeuF8jAbim3QTKBjsaJatDejsG3Vz74nMs/8AMtjK+1GMRTi8d4ptoaaS2af/DKvEpsLSB1DAYm7vOoIgcbR2srtCGsiE3rNLQ5jbz2ddidpTwOP6Bj6cCwR0orIhXQ3+kR/ejjRsvM6jRia71ejzHbcMYZOu/fg0Ljo4R+TMkctK85SVffpE24b9nS5ad6IH1RgvNCpaw3HaSTCwHjdC5nT61bZTYs0K7LgGZkLUKke9Uz3IfTMTYr53mbQd38H3tfZ5UBhryfI48n9pFHx/fbOODN0fsyBH6zWAw7xNuy10GnBHSF7D31s48fimoR/I9aDrJ5NTtQeMuQKK/sJ2XYJ/RTDxi/1Cdr6sTdiQjgXm0pgRaZZFWfMRtL0g1Sluo/vG+nvcXz+UG0DTG2RhaJsu9OwrfsoVrHMk5arfbu4q2KKV1HmzwM8P37eKJYYziACFVGb9++edwfimtMN9Q3rBSamUPJlKDzDJbTjyFEhFdqT91bXLvYTpp+bA/kXE10VuVmbqVZDXW67trV4UFCBLJws1phzR4oUJPA0/EQibXof4YX6mYx/KhMCE0aEbqZ+rPu4VQkU4JlKD5U/0GmEGLR9gswulTSCMZujEEGgX6INSg2CeDuKftpZd9seEHfOivfXr14dQ5II7iaf7jUX307H1XbaLxfj37TxINjPY/iqo+cAyrn8zk8YR1CeA3AAWSArk9YYpy36IW18t7JcNrNXL9TDjV/AzjuBivP22hjWaYXg81cabY3T08wijU2sUGo/ifl4vT3rMw/adMxpRzuI6MI0AfwWtTerkHCObDQ7o8WTIKlOTbSZY8IQkUGnKpQ0AIQkNYaKCzwP0hsTXNMaQyMob3xTCfaTHhK8enUWRpK1fcHUGXLIGbsKchcwoes1AmgIkssgLLJv2OHDJ/jaXc9u2R1TrqGF2zAZbxBFtHLsQTKgaN2NmO4YSU6e9ZUSkkKE59lWdwSv8uB7vLlJvm2g1ctiA8ekmjGd9lFu8KWwmGieCk7L+oINleva0biyJ7UV9sVvDdiwQRfwfQkL/UHVejUQkbFSKt0vjlBb49TczrVJPpuLCfM2qLOawNmx0t/XzkhduA590a9/IX4SU48szUhmKKEcwjNoUX9d2kqTAli5yvROgJ2EGlggUm86M8yAzDXifiCS7QVqlJvlIHozkpxzUhmHQOsRuSxQKNHOEgH4s5EzSxRHTMcFYyPnojKwE50tZHME28z1dkeZTC0J4aCeDEV9E7BGMV/z0bN2wCZKf02JSoBE3CZ63rN7cuzKsgqCHRXOB/mPP/ZeJ8ATuTCuVL0Bb/g+Mkj1JrUx8ixm5D2qeV1IGBCUnQnlySYziQ1ZVt82FMtQZ4CCuTa7SNo3oyi0Q07Fc0RrFr68qPnWxj3UrmyAJvCqH0ochEAnWkfrVGZknUJpJxwxRVcFHTPMLawwB4wr7cTw5+hg3cPMI0tdnZM66GqcJ2Wiq45GWFMFUzD6k2lYWyTSDcgzcJJmYQt/09tbOOrBtjq+UWXs2GsOsgPC9YoWT4WvzvTPqJ70nHWyGX7q9lJaH3nRyozMT9JyQSuD4/KBvQMIvG0AawQHRelmm0GCoIEVNOcksrEal7zSR3CCf+B+xGDAyZg3ep0ta5/LuiCjMtIFourMByzHOdJzBVTF15xXCoQKF4AKjwMD3sfNsuYgGFvgokno/AJbG8Wj98r6zU2WLHjR0UKGE0Bk8wDRRxodbhR3FS381tQlwHv4c718bxz0yCO0Rf4fcCoPa2i3CGYddZgpxCe93yS8lgFCjkqg7lkvkF+TOZ4z1M4zFFcwfQ8mifLEv84OHZandECVop/Hz5HjqBHeEwUoyoOf0Oz46XjE6g7RKN/mciEF13lc/FuBqkt/gBvxJHRbhzF+WnRIrcrcKTscV2wpsPy473IPVSSZMHaqC4sMGbnZZcgZD4AOap/nVakZiI2OJTwz4fR7OTKHPblu5RMcxXFNgDJPJt6LMkFUBmKWUx/0RpglP2/W0MzXkl0SXmai42+VgXm6OchYqANNpNdEnbcd3R0U7SLJPYDnJFvsTvAUUmwYIGqVHcHeWbNgYlW/H0RMUzMZL+El7nLpb6Jy6zhTt+FtGyp0yrcbLSMmFpHrHz3G1orgNljBmveAhOkmIiEhoUQs0RedvwRvEjJhvfdDqO6kRLzMsT7Dwn2/asPWpMruZuA+nqYCtVHXHa+JQ7fcmS5zRM1+HqVffI1L2Fa9ZXx25iXyxRv62914kjWvq+VV/Ebs2rmWOMirbIXcbDHWM/Pl6FFB2O1fN1ubsCgTy32xu7xpdIVSGetl8QwpIJZfx0HUSvszkbqBmT2Wc6aiFJKfObZ7WIszWoi8YZwxE3/pQHLTJGCn8WPhU7n2lIH473SnV3W+bdNjWwFiDOMzw6rngeHO6lWBkbdoZw9cHRgA/VXWTze9ZeD0Xxjg4YqDiHckI0XpJilRiQVT8glZx4IGt32ASxTXBK18JBmQfAcOy/TZElF14tlOvYzwBXp+gcYO2I3ypV8IIkvFfImqHB1xOvPronTObqV8HT4dDnvPz83Af5fMfIr65imsiwKqv/imCASyQb4oGDLD4iYPlfQqR3zFV+UtXIgido8G2JPYF7RW5gXAzy+efSIDpoAGcxV7Ux7L3uQMGUZdO3vlpOKSWKFOmX1AipDwBUxNa/81f7PY95Zyt2v8n1IPd4XkUplJvPpTAPvUv1UvgGwEIzmzCwkQ+kSEnbrVUDRwHKniv+TNJRLdKgCVKOqW4+LZzibJiTKbaz/CMOnle40X2i7vQfntXtmIR38rzsfN1SvNqQp+PSYcO5RDGz0R2QZd3H8GXoI1ynsCYxo/EafdSx5fRSPuvCUH96KNcq//MjUnHzhmni/Wn8l8IIdsUGOpxodeNWujDBn1gLFVCi7n9LrJTvF76Tqbqj4HTZBVG4rfE2OLuxvKuiD14v5dktUjLXqDz4frUsO6B5Bw5vI9HD98xSi5Gpa3LOP9aZuQ3dFPiPWMD5hU0rP3o8b292fNt6dcgsl/CYZlM22lUd5x69VKZfwWxgD+PwMNP/61dkEGxfNbxc/LwKECZSZDknJ0wtDOIwdRCsVblt/YgoNPyDOIgY4YhbGgURbA5cTZDHI48LkvensUxLXne0lTpYwqqxAp2IcoDKvN6f4GpkIt9+y2Ekbj1Nn8/NrFBz0YHVDkPd0fmQmpGGPQkNwMpQtaz3nhaGgdeoSyJEWRJoJAPwhKuAtyFbznNMBQOaJ3oWrhHziUTIJQ2+4vHWi/cKnaT8FGGdfFekGRDEEtKJhNhHwPWIBc/9zxTwQqILOCP88upKEwLjbWfPjs5iTa848XDcr/5ykBzUoxZ5Fi1NQYf4SZjsArCHeo8Dk8/fyd9wXUS5x04C1y4uY2QKT2hpi0atdgMJY9KJZ0sy8WpFwKhCJ9BUaYg8kFqPtglFvxlBp5IziPvQnwHorjqHvA9yS/npJfIC7geneIg5H5njvYx7nnQShFm2yTPl0cvRpOwz4+QXrO7V0rFfK6L+8p5fkJwk9SZNRd15S943FPDT6tGozOvGIExFeo7FJgFwMJeVvxPzyNf6LW+gVUQiX04+KmN1dCBTBiL5klIBzslAtno4dvmJ9KsLZisWsnrgqaz6ByWkCBOxmobBlest4ftX83mJfiUX9g5WJZvt+IsErMivofUsGlvQzroeFySK4Obr6Gr5W+qbvUP8big8nSdO9+xR+aWTjbGO+xi+mDKHIEC4asR0+MgJejw+LKawA5A1VtjrcbXbhocfl9gIV7GGyQiKvb/LfVgMV5w3R6juV8x7rEsORZ5PQqRnVjZNL6Dmjau8rS+421gj6QkY/i0mH6bU2maHCeT0x8JY7y9zjWXFj6OxkIUkdVEaDtQ1DdedIQMdW8PsFfWwem1vdZXiN2tb7UmKsFM4k5NdAIBCCtYmzdnQ0bVp1iQW+iLXZDAaWnWjGMw86oz357U45sAQOimyjkLoBXMUECvsxqzsnGlkk62h8JNYtHQmM958D+Zrrjqcclf7YsSnCughPSXEVb8m+4lV5aDMralkYbgTBLLW70fSQq/cayZwPgJYGS1KgZ2mzs42R3Li/LoXrvEoYM4JD7vq6BjeFjA83y4ITInCELd2FoXF4HYlELKUAM6fkhH8J7PRfLeiPnD0ahazBNGK1oXUiRuEEk0kodEjVINAdQokoHQfi/MYqEuVYzop1B/ikgQET8oZFJ3MT6YOl2hD9t+XpghiKLV2MqhJ6g7mcFM64JVyNnBvePzPhcqZpC6uT8uvseIFzgJo0GEzff+7+6qQgNn3/JxOEDMUZm2scAwogzmoerBvbMNKSTLowKOPJ0dTClBiaFEQsap1trXSkjcIe63pkHkhviMXps4EX9wef57wJsKLp3D7/TnBQnOngDf1A1LdjgFZvCTjlzXcFh/Klv5rxwE7HPxJlLS1Qhg4CmETuCRbNLYRIZFQvNouABgcKOlnMTQRMdtgk1QJ7Ouw2KPz5z7IlUodo4g0102Vfd9jhb6KmGQYX/+Da4NoB8MwKB7vk+2+jUPo6cs7sA/VN574OfKV1YkGWvc27zwU4tarAv1RkX9wdyCspLAyi09T7iu6wVD4WQZFNjs1wDEl8Joxcse1/sBXG0JsVjxk5LwWsiq/KxfawuSvJbFjbuuGf3pWTo+xGECQEvTsgEF4xbeY5UxbaiZF3T92VTIDRTa+NKlUrfotgz/vEdcL+9Uof+9rR2yDHQQHdPsi49vKV+8JZKB6qvqOk+vJTEIyjhsEk2XtypoDKJeC8iGQbjlUH1jKbClraH2jQ4suwRHiwNpfUXPPqpV9n3XunFdmmKAn73E6X3HLldJzKqtL2XqTOluAweBWNReB6Zbe4Ru4zT55cLmvoYbiq7qKEN+g8X9MSCovGaRUoDDAoKYQMpzubSP/xopVxmDsGWuLIq3404lzJ1lZ4HaKYNv3VKGaZmBtgjc1PNt3jfzvUWo5wY7438XuEiCIskK7EGgNRXUHNZAtSwRrvK3/rfvw55ey1NfNCGdL6ZSJVzwu/yt0vssmn+k+4s/kPETNQx22Eli8pbNdIZjSBJtF4lNRIAO62esXsA2h7MiiU/vdWvzl5RPIzZ+4uPMyePPrKHfpUH9+wvT3jiuXZE84b1vTsc4IypSwSsaSOU1jeMUIRZhDJszBMV2Oy5RtvKFTjAdFOUJzGTFt2EtL3hKdvyk1aFlDqS9Rjqdx4gUyewLwZA4RbOhJbGmZgt0dytUsPZ8rGAGZvCaKIUzixCNhCqMf+QU6hu+unE7dcjLgpQ3w4MVtCTSR4mgthDqVnIJZOvwqyk7KMn9bGhEq9jevGc5m1Tnvgtu2qVHujs6IDKsx9ydG76Vrt2T12esKFLOV66G1J+rnd4dHnjUVWYaaDBLvSPxlhn/P2SSxtURHzHinIeAndAsWTU9c1svRk5s8grHoUT8/pdYjlzw2LhfbX37aURtOc/i5yoPVwuT+TC52ioaGBlSRdrEDmdIXxkcqOOk1bNs8TR5Zk8Us7q8XItG6oUkNgr/n5H9TPnoHPA9K6DbkVrN0Cl2AlCh0xbKr+wXmsfb9Tq80ShuuSl2dPBYZJmrYGi0Y6kTOfLDqqUg71+pazpEfajQcQUxgMH6Zjcpgn0zJSdTtZdiKXkzJRdnZ85cjU7JQOKDX1bu617BFeoFbcfnOAxpFR30yk5mjGWExUDCsJwk/eBm9H4ZridI2xXX3156566TP8NedzC2oIW7dmmfxybMzdajweX7JmYSfUZFqbHMaB9jdWPxfQHamp+0vPViFOHFsCr77pqOrZrLIwf6unozLk","categories":[{"name":"随笔","slug":"随笔","permalink":"https://www.sanks-blog.com/categories/随笔/"}],"tags":[{"name":"随笔","slug":"随笔","permalink":"https://www.sanks-blog.com/tags/随笔/"}]},{"title":"http request timeout","slug":"http-request-timeout","date":"2020-04-13T01:24:58.000Z","updated":"2020-07-12T09:30:58.713Z","comments":true,"path":"http-request-timeout/","link":"","permalink":"https://www.sanks-blog.com/http-request-timeout/","excerpt":"引语鉴于之前axios版本升级,去掉了baseUrl配置的问题,前端对于axios库的升级未做版本固定,导致项目启动报错;除了写成固定版本,还可以自己写一套请求响应机制。","text":"引语鉴于之前axios版本升级,去掉了baseUrl配置的问题,前端对于axios库的升级未做版本固定,导致项目启动报错;除了写成固定版本,还可以自己写一套请求响应机制。 方法一:Promise.race(timeout, request)顾名思义,Promse.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。 流程图 具体代码如下:123456789101112timeoutPromise(timeout) { return new Promise((resolve, reject) => { setTimeout(() => { resolve(new Response("timeout", {status: 504, statusText: "timeout "})); // reject(new Error('请求超时')) controller.abort(); // 判断网络是否连接 // TODO... }, timeout) });} 123456789wrapperRequest(request) { return Promise.race([this.timeoutPromise(10000), request]) .then(resp => { return Promise.resolve(resp); }) .catch(error => { return Promise.reject(error); });} 12345678910get(url, querystring = {}, options = {}) { const getOptions = Object.assign( { method: HTTP_METHOD.GET, qs: querystring, }, options ); return this.wrapperRequest(this.sendRequest(url, getOptions));} 方法二:clearTimeout()流程图 具体代码如下:1234567891011// Handle request timeoutif (options.timeout && !this.timer) { this.timer = setTimeout(function handleRequestTimeout() { // reject(new Error('请求超时')) controller.abort(); reject(new Response("timeout", {status: 504, statusText: "timeout "})); // 判断网络是否连接 // TODO... }, options.timeout);} 12345return fetch(url, apiOptions).then(response => { // Response has been received so kill timer that handles request timeout clearTimeout(this.timer); this.timer = null;}) 总结axios固然好用,但是我的宗旨是:自己动手,丰衣足食;自己写了网络请求响应模块,如果还需要其他功能,自己拓展,自己维护这个模块。方法一是网上给出的解决方案,思路固然好,但是对于正常平添了一个请求(一定会执行timeout的逻辑),而且无法满足我后续监听网络是否断开的操作(这个模块已经实现),有兴趣的朋友可以给我发邮件,后续我会新增这个模块的博客。方法二是借鉴axios的源码,对请求超时响应的处理","categories":[{"name":"axios","slug":"axios","permalink":"https://www.sanks-blog.com/categories/axios/"}],"tags":[{"name":"request","slug":"request","permalink":"https://www.sanks-blog.com/tags/request/"}]},{"title":"NodeJS Middleware","slug":"NodeJS-Middleware","date":"2020-02-03T06:59:38.000Z","updated":"2020-07-12T09:30:58.623Z","comments":true,"path":"NodeJS-Middleware/","link":"","permalink":"https://www.sanks-blog.com/NodeJS-Middleware/","excerpt":"Nodejs 作为中间层能做的事nodejs中间层能减少开发过程中的一些实际问题,比如跨域,也能分担后台开发人员的工作,比如文件上传服务器,也能做简单的即时消息聊天功能,都是基于nodejs的特性。 反向代理和跨域同源策略(SOP)是为了防止CSRF(跨域请求伪造)的攻击,浏览器引入的策略。","text":"Nodejs 作为中间层能做的事nodejs中间层能减少开发过程中的一些实际问题,比如跨域,也能分担后台开发人员的工作,比如文件上传服务器,也能做简单的即时消息聊天功能,都是基于nodejs的特性。 反向代理和跨域同源策略(SOP)是为了防止CSRF(跨域请求伪造)的攻击,浏览器引入的策略。 跨域的解决方案有很多,比如 JSONP Nodejs做反向代理 CORS”跨域资源共享”(Cross-origin resource sharing),现在使用最多的是后两种,nodejs做反向代理,改变HTTP协议中header的属性,是前端开发最常用的方式。 文件上传服务器项目中避免不了需要上传图片,Excel文件等上传文件的功能,需要上传进度,存储文件,压缩等操作用nodejs来实现文件上传,并且可异步上传多个文件,是再好不过的了 SSR服务器端渲染的好处有好多,提高首屏加载速度,提高搜索引擎对网页的抓取现基于 koa 做了服务器端渲染的操作 socket.ioSocket.io是一个WebSocket库,包括了客户端的js和服务器端的nodejs Express vs Koa express 门槛更低,koa 更强大更优雅。 express 封装更多的东西,开发速度快速,koa 可定制性更高 整体的项目架构图","categories":[{"name":"Nodejs","slug":"Nodejs","permalink":"https://www.sanks-blog.com/categories/Nodejs/"}],"tags":[{"name":"Nodejs","slug":"Nodejs","permalink":"https://www.sanks-blog.com/tags/Nodejs/"}]},{"title":"Flutter","slug":"Flutter","date":"2020-02-01T07:53:14.000Z","updated":"2020-07-29T13:43:17.859Z","comments":true,"path":"Flutter/","link":"","permalink":"https://www.sanks-blog.com/Flutter/","excerpt":"引语Flutter 是 Google 研发的 移动 UI 框架,可以快速在iOS和Android上构建高质量的原生用户界面。 DartDart 是 Flutter 的开发语言, 如同 IOS开发用 swift 一样, Flutter SDKFlutter SDK里面有什么? 深度优化了的、移动优先的2D渲染引擎 现代、响应式框架 丰富的Android和iOS套件 单元和集成测试的API 连接到系统和第三方SDK的Interop和插件API 无头的测试运行器,用于在Windows、Linux和Mac上运行测试 用于创建、构建、测试和编译应用程序的命令行工具","text":"引语Flutter 是 Google 研发的 移动 UI 框架,可以快速在iOS和Android上构建高质量的原生用户界面。 DartDart 是 Flutter 的开发语言, 如同 IOS开发用 swift 一样, Flutter SDKFlutter SDK里面有什么? 深度优化了的、移动优先的2D渲染引擎 现代、响应式框架 丰富的Android和iOS套件 单元和集成测试的API 连接到系统和第三方SDK的Interop和插件API 无头的测试运行器,用于在Windows、Linux和Mac上运行测试 用于创建、构建、测试和编译应用程序的命令行工具 IDEAndroid Studio 开发Flutter的环境配置1、JDK 需要配置环境变量 JAVA_HOME=D:\\Environment\\jdk-14.0.2 %JAVA_HOME%\\bin %JAVA_HOME%\\jre\\bin $ java –version 2、Gradle 依赖于 JDK,同样需要配置环境变量 GRADLE_HOME=D:\\Environment\\gradle\\bin $ gradle -v 3、Android SDK 需要配置三个环境变量 ANDROID_HOME=D:\\Environment\\Android\\android-sdk %ANDROID_HOME%\\emulator %ANDROID_HOME%\\platform-tools %ANDROID_HOME%\\tools 4、Flutter SDK 需要配置环境变量D:\\Environment\\flutter\\bin $ flutter doctor 5、Android Studio 中配置 打开我们的studio ,分别安装插件• dart• flutter 安装完成 重启软件 后 配置SDK 路径Dart SDK 配置Flutter SDK配置 个人项目","categories":[{"name":"Flutter","slug":"Flutter","permalink":"https://www.sanks-blog.com/categories/Flutter/"}],"tags":[{"name":"Flutter","slug":"Flutter","permalink":"https://www.sanks-blog.com/tags/Flutter/"}]},{"title":"Mutiple SSH keys for diffrent github accounts","slug":"Mutiple-SSH-keys-for-diffrent-github-accounts","date":"2020-01-05T12:01:31.000Z","updated":"2022-03-14T16:06:49.809Z","comments":true,"path":"Mutiple-SSH-keys-for-diffrent-github-accounts/","link":"","permalink":"https://www.sanks-blog.com/Mutiple-SSH-keys-for-diffrent-github-accounts/","excerpt":"create different public keyNote: blog’s git configuration is global, others is in your project create different ssh key according to your need12$ ssh-keygen -t rsa -f ~/.ssh/id_rsa_activehacker -C "[email protected]"$ ssh-keygen -t rsa -f ~/.ssh/id_rsa_jexchan -C "[email protected]" If your command line has no arguments “-f ~/.ssh/id_rsa_activehacker”, as following 12$ ssh-keygen -t rsa -C "[email protected]"$ ssh-keygen -t rsa -C "[email protected]" 运行上面那条命令后会让输入一个文件名,用于保存刚才生成的 SSH key 代码,此时需要输入完整的绝对路径,或者只输入文件名,在当前目录生成,生成后移动到指定的.ssh文件夹内,如: 12Generating public/private rsa key pair.Enter file in which to save the key (/c/Users/SKS/.ssh/id_rsa): /c/Users/SKS/.ssh/id_rsa_activehacker","text":"create different public keyNote: blog’s git configuration is global, others is in your project create different ssh key according to your need12$ ssh-keygen -t rsa -f ~/.ssh/id_rsa_activehacker -C "[email protected]"$ ssh-keygen -t rsa -f ~/.ssh/id_rsa_jexchan -C "[email protected]" If your command line has no arguments “-f ~/.ssh/id_rsa_activehacker”, as following 12$ ssh-keygen -t rsa -C "[email protected]"$ ssh-keygen -t rsa -C "[email protected]" 运行上面那条命令后会让输入一个文件名,用于保存刚才生成的 SSH key 代码,此时需要输入完整的绝对路径,或者只输入文件名,在当前目录生成,生成后移动到指定的.ssh文件夹内,如: 12Generating public/private rsa key pair.Enter file in which to save the key (/c/Users/SKS/.ssh/id_rsa): /c/Users/SKS/.ssh/id_rsa_activehacker 你也可以不输入文件名,使用默认文件名,那么就会生成 id_rsa 和 id_rsa.pub 两个全局默认的秘钥文件,前者为私钥,后者为公钥。当然我们有两个代码仓库,所以最好写上文件名,如id_rsa(公司)或id_rsa_user2(个人). 这样ssh目录下会生成id_rsa.pub和id_rsa_user2.pub两个文件 接着又会提示你输入两次密码(该密码是你push文件的时候要输入的密码,而不是github管理者的密码)。也可以直接按回车键,那么push的时候就不需要输入密码,直接提交到github上了,如: 12Enter passphrase (empty for no passphrase):Enter same passphrase again: 当你看到下面这段代码的时候,那就说明,SSH key 已经创建成功,只需要添加到github的SSH key上就可以了。 1234Your identification has been saved in /c/Users/SKS/.ssh/id_rsa_activehacker.Your public key has been saved in /c/Users/SKS/.ssh/id_rsa_activehacker.pub.The key fingerprint is:SHA256:Iyie1VCcJRLoOmM2VvY/5XF4KPb9MbQpLmEeOLuVDfA [email protected] 2 keys created at: 12~/.ssh/id_rsa_activehacker~/.ssh/id_rsa_jexchan then, add these two keys as following(添加到 ssh-agent 信任列表) 12$ ssh-add ~/.ssh/id_rsa_activehacker$ ssh-add ~/.ssh/id_rsa_jexchan you can delete all cached keys before 1$ ssh-add -D finally, you can check your saved keys 1$ ssh-add -l 请注意:此处有坑,你可能会遇到这样的问题 1Could not open a connection to your authentication agent. 解决方案:(也可以是其他的,参考资料里边stackoverflow里边的答案你都可以试试) 1$ ssh-agent bash 这之后,再添加。看到如下所示的情况,就证明添加成功了 12$ ssh-add ~/.ssh/id_rsa_activehackerIdentity added: /c/Users/dong/.ssh/id_rsa_activehacker (/c/Users/dong/.ssh/id_rsa_activehacker) 添加ssh-key到github在 Github 的后台,可以看到一个叫做 SSH and GPG keys 的选项:这里面列出了当前账号绑定的 SSH Key。 每一个 key 对应一台独立的设备。 设置好两个ssh key之后就要配置下它们的使用场景登录你的github账号,从右上角的设置( Settings )进入,然后点击菜单栏的 SSH key 进入页面添加 SSH key。点击 Add SSH key 按钮添加一个 SSH key 。把你复制的 SSH key 代码粘贴到 key 所对应的输入框中,记得 SSH key 代码的前后不要留有空格或者回车。当然,上面的 Title 所对应的输入框你也可以输入一个该 SSH key 显示在 github 上的一个别名。 Modify the ssh config123$ cd ~/.ssh/$ touch config$ subl -a config Then added 1234567891011#activehacker accountHost github.com-activehackerHostName github.comUser gitIdentityFile ~/.ssh/id_rsa_activehacker#jexchan accountHost github.com-jexchanHostName github.comUser gitIdentityFile ~/.ssh/id_rsa_jexchan 这样,在我们创建的 config 文件中,配置了两条记录。 分别指向两个 SSH key。 HostName是原本的域名 Host是与HostName对应的自定义的名字。 Clone you repo and modify your Git configclone your repo在项目的下载地址中,有一个 Use SSH 的链接,点击它之后,就可以得到 SSH 格式的地址比如 [email protected]:activehacker/gfs.git。 我们需要对它稍作加工,把域名部分替换成我们在 config 中配置的 Host: [email protected]:activehacker/gfs.git。 1$ git clone [email protected]:activehacker/gfs.git gfs_jexchan 这样本地仓库就和对应的密钥建立起了联系。 以后的操作中,都会自动使用这个 SSH key 来访问 Github 远程仓库了。 如果想同时在另外一个本地仓库使用其他 Github 账户,只需要在 ~/.ssh/config文件中配置好相应的 SSH key 和对应的 Host,就可以了。 cd gfs_jexchan and modify git config(为每个仓库单独设置用户) 12345$ git config user.name "jexchan"$ git config user.email "[email protected]" $ git config user.name "activehacker"$ git config user.email "[email protected]" then use normal flow to push your code 123$ git add .$ git commit -m "your comments"$ git push","categories":[{"name":"Git","slug":"Git","permalink":"https://www.sanks-blog.com/categories/Git/"}],"tags":[{"name":"Git","slug":"Git","permalink":"https://www.sanks-blog.com/tags/Git/"}]},{"title":"ES6 的新特性","slug":"ECMAScript6-New-Features","date":"2019-05-18T10:26:14.000Z","updated":"2020-07-12T09:30:58.613Z","comments":true,"path":"ECMAScript6-New-Features/","link":"","permalink":"https://www.sanks-blog.com/ECMAScript6-New-Features/","excerpt":"写这篇文章的目的就是告诉前端的同学们,ES6 已经是前端程序员必不可少的技能之一,后期再追加 Typescript 的新语法 关键字 async/await 的应用 async function 是 Promise 的语法糖封装 异步编程的终极方案 - 以同步的方式写异步 await 关键字可以 “暂停” async function 的执行 await 关键字可以以同步的写法获取 Promise 的执行结果 try-catch 可以获取 await 所得到的错误 一个穿越事件循环存在的 function","text":"写这篇文章的目的就是告诉前端的同学们,ES6 已经是前端程序员必不可少的技能之一,后期再追加 Typescript 的新语法 关键字 async/await 的应用 async function 是 Promise 的语法糖封装 异步编程的终极方案 - 以同步的方式写异步 await 关键字可以 “暂停” async function 的执行 await 关键字可以以同步的写法获取 Promise 的执行结果 try-catch 可以获取 await 所得到的错误 一个穿越事件循环存在的 function 用 async/await 来处理异步请求, 从服务端获取数据,代码更简洁,其已被标准化,用的最多的就是,当你后面的数据过滤整理操作,需要依赖于前面接口返回的数据时,此语法方便解决了此需求,注意:await 后面的函数必须返回一个promise想获取到async 函数的执行结果,就要调用promise的then 或catch 来给它注册回调函数(类同promise),代码如下: 123456789101112131415161718192021getTree() { return getRegionTree() .then(res => { this.interfaceData = res.data; this.interfaceData.name = "选择区域"; }) .catch(err => { console.log(err); });},async init(originArea, readonly) { await this.getTree(); if (originArea) { originArea.name = "选择区域"; let oldArray = this.handleEmptyTree([this.interfaceData], readonly); originArea = this.handleNotEmptyTree([originArea], readonly); this.handleLeafTree(originArea, oldArray); } else { this.region = this.handleEmptyTree([this.interfaceData]); } } 重点:vue 中 this.$nextTick() 也会返回一个promise,也可用 async/await,这在工作中很有用,解决一些组件不刷新数据的问题。例如: 123456async publishFn(name) { this.isShowDiseaseTagsMessage = false; await this.$nextTick(() => { this.isShowDiseaseTagsMessage = (this.queryModel.diseaseTags && this.queryModel.diseaseTags.length === 0) })} es6判断数组已存在某个对象。find() 方法返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined。findIndex()方法返回数组中满足提供的测试函数的第一个元素的索引。否则返回-1。 1234567var objArr = [{id:1, name:'jiankian'}, {id:23, name:'anan'}, {id:188, name:'superme'}, {id:233, name:'jobs'}, {id:288, name:'bill', age:89}, {id:333}] ;var ret2 = objArr.find((v) => { return v.id == 233;});console.log(ret2);// return {id:233, name:'jobs'}// 当返回undefined时,则说明objArr中没有,可以添加 1234567var objArr = [{id:1, name:'jiankian'}, {id:23, name:'anan'}, {id:188, name:'superme'}, {id:233, name:'jobs'}, {id:288, name:'bill', age:89}, {id:333}] ;var ret2 = objArr.findIndex((v) => { return v.id == 233;});console.log(ret2);// return 3// 当返回-1时,则说明objArr中没有,可以添加了 ES5和ES6中分别是怎么判断变量为数组的JS的弱类型机制导致判断变量类型,类型检查很重要,为编码的首要检索入口 在ES5中判断变量是否为数组1234567891011var a = []; // 1.基于instanceof a instanceof Array; // 2.基于constructor a.constructor === Array; // 3.基于Object.prototype.isPrototypeOf Array.prototype.isPrototypeOf(a); // 4.基于getPrototypeOf Object.getPrototypeOf(a) === Array.prototype; // 5.基于Object.prototype.toString Object.prototype.toString.apply(a) === '[object Array]'; 以上,除了Object.prototype.toString外,其它方法都不能正确判断变量的类型。 要知道,代码的运行环境十分复杂,一个变量可能使用浑身解数去迷惑它的创造者。且看: 123456789101112var a = { __proto__: Array.prototype}; // 分别在控制台试运行以下代码 // 1.基于instanceof a instanceof Array; // => true // 2.基于constructor a.constructor === Array; // => true // 3.基于Object.prototype.isPrototypeOf Array.prototype.isPrototypeOf(a); // => true // 4.基于getPrototypeOf Object.getPrototypeOf(a) === Array.prototype; // => true 以上,4种方法将全部返回true,为什么呢?我们只是手动指定了某个对象的proto属性为Array.prototype,便导致了该对象继承了Array对象,这种毫不负责任的继承方式,使得基于继承的判断方案瞬间土崩瓦解。 不仅如此,我们还知道,Array是堆数据,变量指向的只是它的引用地址,因此每个页面的Array对象引用的地址都是不一样的。iframe中声明的数组,它的构造函数是iframe中的Array对象。如果在iframe声明了一个数组x,将其赋值给父页面的变量y,那么在父页面使用y instanceof Array ,结果一定是false的。�而最后一种返回的是字符串,不会存在引用问题。实际上,多页面或系统之间的交互只有字符串能够畅行无阻。 当检测Array实例时, Array.isArray 优于 instanceof,因为Array.isArray能检测iframes. 123456789var iframe = document.createElement('iframe');document.body.appendChild(iframe);xArray = window.frames[window.frames.length-1].Array;var arr = new xArray(1,2,3); // [1,2,3]// Correctly checking for ArrayArray.isArray(arr); // true// Considered harmful, because doesn't work though iframesarr instanceof Array; // false 在ES6中判断变量是否为数组鉴于数组的常用性,在ES6中新增了Array.isArray方法,使用此方法判断变量是否为数组,则非常简单,如下 12Array.isArray([]); // => true Array.isArray({0: 'a', length: 1}); // => false 实际上,通过Object.prototype.toString去判断一个值的类型,也是各大主流库的标准。因此Array.isArray的polyfill通常长这样:假如不存在 Array.isArray(),则在其他代码之前运行下面的代码将创建该方法。 12345if (!Array.isArray){ Array.isArray = function(arg){ return Object.prototype.toString.call(arg) === '[object Array]'; };}","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://www.sanks-blog.com/categories/JavaScript/"}],"tags":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://www.sanks-blog.com/tags/JavaScript/"}]},{"title":"在 react 项目的基础上增加一些配置(typescript支持,webpack别名等)","slug":"create-react-app-complex","date":"2019-03-12T01:47:05.000Z","updated":"2020-07-12T09:30:58.663Z","comments":true,"path":"create-react-app-complex/","link":"","permalink":"https://www.sanks-blog.com/create-react-app-complex/","excerpt":"引言react脚手架并不能直接运用到项目中去,需要改造,自己进行了进一步探索,总结创建项目的心酸历程 创建项目的流程 npx create-react-app jelly3 备注:你的环境没有全局安装npx,放心,它会自动安装上并执行创建项目的命令 cd jelly3 切换到自己创建项目根目录下 yarn eject yarn start 启动项目","text":"引言react脚手架并不能直接运用到项目中去,需要改造,自己进行了进一步探索,总结创建项目的心酸历程 创建项目的流程 npx create-react-app jelly3 备注:你的环境没有全局安装npx,放心,它会自动安装上并执行创建项目的命令 cd jelly3 切换到自己创建项目根目录下 yarn eject yarn start 启动项目 接下来就是思考自己的项目结构了如果你刚开始一个项目,不要花超过五分钟在选择一个文件结构上。从以上方法(或者你自己想到的)中任意挑一个然后开始编程吧!在写完一些真实的代码之后,你可能会想重新考虑它。 @歌特式灵魂摆渡人www.jianshu.com/p/eb7d518b05b8 引入路径别名发现更改目录结构后,相对路径的名字写起来很麻烦,想跟以前VUE项目一样,src 路径 以 别名 “@” 代替我们可以通过使用 webpack 中的 resolve.alias 配置别名,将某些文件目录配置成固定的引入。例如: 我们可以将 ../../src 这样的相对路径的目录,设置成一个 @ 别名, 以后就可以用 @ 代替这个目录引入就行了,而不需要写一坨 ../../../ 1234567891011const path = require('path');module.exports = { ... resolve: { alias: { '@': path.resolve(__dirname, '../src') } }, ...}; 给React项目添加TypeScript支持请参阅给React项目添加TypeScript支持, 多少跟实际项目有些出入,需要灵活变通,与官网的配置结合看最佳。熟悉webpack配置和总览生成项目的配置后,我发现已经对TypeScript部分支持(未对.ts和.tsx进行webpack解析)注:TypeScript 官网 有 React & Webpack 这篇教程,但是有需要改进的地方, 比如 ts-loader 比 awesome-typescript-loader 打包速度更快, 构建项目大多数用 ts-loader, 详见Speed of Awesome-typescript-loader vs ts-loader 加入代码语法格式检查工具eslint + tslint 并不能满足自己的代码洁癖,后又引入了prettier vscode + prettier 专治代码洁癖,经过艰苦的VSCode插件和配置文件的调试过程,代码检查机制总算配置好了。 解决相关的问题 .tsx 文件中引入的 webpack 别名,TS语法检查报错的问题详见一次解决React+TypeScript+Webpack 别名(alias)找不到问题的过程 同时,当天发现react项目存在的问题就是 package.json 文件中,没有开发环境依赖和生产环境依赖的区分,把所有的依赖全部写入到了生产依赖中,如下 package.json123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121{ "name": "jelly3", "version": "0.2.0", "private": true, "dependencies": { "@babel/core": "7.2.2", "@svgr/webpack": "4.1.0", "babel-core": "7.0.0-bridge.0", "babel-eslint": "9.0.0", "babel-jest": "23.6.0", "babel-loader": "8.0.5", "babel-plugin-named-asset-import": "^0.3.1", "babel-preset-react-app": "^7.0.1", "bfj": "6.1.1", "case-sensitive-paths-webpack-plugin": "2.2.0", "css-loader": "1.0.0", "dotenv": "6.0.0", "dotenv-expand": "4.2.0", "eslint": "5.12.0", "eslint-config-react-app": "^3.0.7", "eslint-loader": "2.1.1", "eslint-plugin-flowtype": "2.50.1", "eslint-plugin-import": "2.14.0", "eslint-plugin-jsx-a11y": "6.1.2", "eslint-plugin-react": "7.12.4", "file-loader": "2.0.0", "fork-ts-checker-webpack-plugin-alt": "0.4.14", "fs-extra": "7.0.1", "html-webpack-plugin": "4.0.0-alpha.2", "identity-obj-proxy": "3.0.0", "jest": "23.6.0", "jest-pnp-resolver": "1.0.2", "jest-resolve": "23.6.0", "jest-watch-typeahead": "^0.2.1", "mini-css-extract-plugin": "0.5.0", "node-sass": "^4.12.0", "optimize-css-assets-webpack-plugin": "5.0.1", "pnp-webpack-plugin": "1.2.1", "postcss-flexbugs-fixes": "4.1.0", "postcss-loader": "3.0.0", "postcss-preset-env": "6.5.0", "postcss-safe-parser": "4.0.1", "react": "^16.8.3", "react-app-polyfill": "^0.2.1", "react-dev-utils": "^7.0.3", "react-dom": "^16.8.3", "resolve": "1.10.0", "sass-loader": "7.1.0", "style-loader": "0.23.1", "terser-webpack-plugin": "1.2.2", "url-loader": "1.1.2", "webpack": "4.28.3", "webpack-dev-server": "3.1.14", "webpack-manifest-plugin": "2.0.4", "workbox-webpack-plugin": "3.6.3" }, "scripts": { "start": "node scripts/start.js", "build": "node scripts/build.js", "test": "node scripts/test.js" }, "eslintConfig": { "extends": "react-app" }, "browserslist": [ ">0.2%", "not dead", "not ie <= 11", "not op_mini all" ], "jest": { "collectCoverageFrom": [ "src//*.{js,jsx,ts,tsx}", "!src//.d.ts" ], "resolver": "jest-pnp-resolver", "setupFiles": [ "react-app-polyfill/jsdom" ], "testMatch": [ "<rootDir>/src//tests//.{js,jsx,ts,tsx}", "<rootDir>/src/*/?(.)(spec|test).{js,jsx,ts,tsx}" ], "testEnvironment": "jsdom", "testURL": "http://localhost", "transform": { "^.+\\.(js|jsx|ts|tsx)$": "<rootDir>/node_modules/babel-jest", "^.+\\.css$": "<rootDir>/config/jest/cssTransform.js", "^(?!.*\\.(js|jsx|ts|tsx|css|json)$)": "<rootDir>/config/jest/fileTransform.js" }, "transformIgnorePatterns": [ "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$", "^.+\\.module\\.(css|sass|scss)$" ], "moduleNameMapper": { "^react-native$": "react-native-web", "^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy" }, "moduleFileExtensions": [ "web.js", "js", "web.ts", "ts", "web.tsx", "tsx", "json", "web.jsx", "jsx", "node" ], "watchPlugins": [ "E:\\Study\\jelly3\\node_modules\\jest-watch-typeahead\\filename.js", "E:\\Study\\jelly3\\node_modules\\jest-watch-typeahead\\testname.js" ] }, "babel": { "presets": [ "react-app" ] }}根据我以往的经验,把这些依赖进行了拆解,分成开发依赖(devDependencies,其中大部分是开发依赖)和 生产依赖 (dependencies)最新的 package.json 相关配置,请参考我的 github - jelly3 typescript里面引入图片时,TS语法检查报错,项目没办法正常启动参阅了(https://stackoverflow.com/questions/43638454/webpack-typescript-image-import?rq=1),解决了这个问题。 为项目引入react-router(v4) 、antd-mobileyarn add react-router-domantd-mobile 请参照官网进行配置和引入,最好是按需加载遇到的问题:如果babel-plugin-import按需加载的js不符合tslint规范,怎么办?修改 tslint 的语法检查配置tsconfig.json 中添加 “allowSyntheticDefaultImports”: true, // 允许模块没有默认导出 浏览器兼容问题—babel-polyfill代码分割,路由动态加载(react-loadable)(react-loadable)[https://github.com/jamiebuilds/react-loadable] 高阶组件:路由守卫模拟VUE的路由守卫机制 react按需引入(lodash)[https://www.cnblogs.com/savokiss/p/8514868.html]定制antd-mobile主题vsCode安装函数注释插件(KoroFileHeader )和git源代码管理插件(GitLens)、Import Cost、REST Client、vscode-icons添加webpack对less和sass的解析配置","categories":[{"name":"React","slug":"React","permalink":"https://www.sanks-blog.com/categories/React/"}],"tags":[{"name":"React","slug":"React","permalink":"https://www.sanks-blog.com/tags/React/"},{"name":"TypeScript","slug":"TypeScript","permalink":"https://www.sanks-blog.com/tags/TypeScript/"}]},{"title":"react的基础之上进行引入webpack、eslint、babel的框架搭建","slug":"create-react-app-simple","date":"2019-03-04T07:13:00.000Z","updated":"2020-07-12T09:30:58.673Z","comments":true,"path":"create-react-app-simple/","link":"","permalink":"https://www.sanks-blog.com/create-react-app-simple/","excerpt":"引言开始深入研究reactjs,弥补之前的浅尝辄止;一开始自己用官方的项目生成器生成了一个简单的架构,自己从这个简化版逐步加入babel, webpack, eslint 等相关的配置,有兴趣的朋友可以移步 react的基础之上进行引入webpack、eslint、babel的框架搭建, 但是你仔细阅读react项目下的 READEME.md, 你就会发现我绕了远路,其实react提供了 yarn eject 来注入webpack, eslint, label 等相关依赖和配置,可能这就是react给大家提供的 脚手架 吧 需要注意的是:这个命令只能执行一次,而且不可逆转。 虽说自己饶了远路,但是还是学到些东西的,也温习了一些 webpack,babel 的配置和原理等,想亲自动手,亲自实践的朋友按照如下步骤进行就行,我已经为你们绕过了一些坑比如:babel 升级 6.x 到 7.x, 请参阅 babel 7.x 和 webpack 4.x 配置vue项目, 如果以下步骤有什么不妥之处,欢迎大家给我评论,我会及时修正并回复大家的问题。","text":"引言开始深入研究reactjs,弥补之前的浅尝辄止;一开始自己用官方的项目生成器生成了一个简单的架构,自己从这个简化版逐步加入babel, webpack, eslint 等相关的配置,有兴趣的朋友可以移步 react的基础之上进行引入webpack、eslint、babel的框架搭建, 但是你仔细阅读react项目下的 READEME.md, 你就会发现我绕了远路,其实react提供了 yarn eject 来注入webpack, eslint, label 等相关依赖和配置,可能这就是react给大家提供的 脚手架 吧 需要注意的是:这个命令只能执行一次,而且不可逆转。 虽说自己饶了远路,但是还是学到些东西的,也温习了一些 webpack,babel 的配置和原理等,想亲自动手,亲自实践的朋友按照如下步骤进行就行,我已经为你们绕过了一些坑比如:babel 升级 6.x 到 7.x, 请参阅 babel 7.x 和 webpack 4.x 配置vue项目, 如果以下步骤有什么不妥之处,欢迎大家给我评论,我会及时修正并回复大家的问题。 创建项目的流程 npx create-react-app jelly备注:你的环境没有全局安装npx,放心,它会自动安装上并执行创建项目的命令 cd jelly 切换到自己创建项目根目录下 导入 react-dom 、react-router-dom 、 redux 、 react-redux 、lodash 依赖包yarn add react-dom react-router-dom redux react-redux lodash 安装 Webpack, 现在最流行的模块打包工具yarn add webpack webpack-cli webpack-dev-server webpack-merge –dev 安装一些必要的 Webpack 打包插件 和 eslintyarn add html-webpack-plugin copy-webpack-plugin css-loader file-loader eslint babel-eslint –dev 安装Babel, 可以把ES6转换为ES5,注意Babel最新的V6版本分为babel-cli和babel-core两个模块,这里只需要用babel-core即可yarn add @babel/core –dev 安装其他的babel依赖yarn add @babel/polyfill @babel/runtime @babel/plugin-transform-runtime @babel/preset-env @babel/preset-react –dev 安装 cross-env , cross-env能跨平台地设置及使用环境变量yarn add cross-env –dev 打开 package.json , 添加或者修改下面的命令脚本 package.json12345"scripts": { "start": "cross-env NODE_ENV=development webpack-dev-server --cache --colors --profile --progress -d", "build": "cross-env NODE_ENV=production webpack --cache --colors --profile --progress --hide-modules"} 命令行输入 yarn start 将要启动 webpack dev server.命令行输入 yarn build 将会进行生产环境打包. 要想成功用webpack打包,还需要 webpack.config.js 和 webpack.ini.js , .babelrc","categories":[{"name":"React","slug":"React","permalink":"https://www.sanks-blog.com/categories/React/"}],"tags":[{"name":"React","slug":"React","permalink":"https://www.sanks-blog.com/tags/React/"}]},{"title":"JS 的深拷贝与浅拷贝","slug":"deep-copy-and-shallow-copy-for-JavaScript","date":"2019-02-24T01:21:09.000Z","updated":"2020-07-12T09:30:58.700Z","comments":true,"path":"deep-copy-and-shallow-copy-for-JavaScript/","link":"","permalink":"https://www.sanks-blog.com/deep-copy-and-shallow-copy-for-JavaScript/","excerpt":"原文链接作者:ziwei3749https://segmentfault.com/a/1190000012828382 这篇文章的受众 第一类,业务需要,急需知道如何深拷贝JS对象的开发者。 第二类,希望扎实JS基础,将来好去面试官前秀操作的好学者。 写给第一类读者你只需要一行黑科技代码就可以实现深拷贝 123456var copyObj = { name: 'ziwei', arr : [1,2,3]}var targetObj = JSON.parse(JSON.stringify(copyObj)) 此时 copyObj.arr !== targetObj.arr 已经实现了深拷贝 别着急走,利用window.JSON的方法做深拷贝存在2个缺点: 如果你的对象里有函数,函数无法被拷贝下来 无法拷贝copyObj对象原型链上的属性和方法","text":"原文链接作者:ziwei3749https://segmentfault.com/a/1190000012828382 这篇文章的受众 第一类,业务需要,急需知道如何深拷贝JS对象的开发者。 第二类,希望扎实JS基础,将来好去面试官前秀操作的好学者。 写给第一类读者你只需要一行黑科技代码就可以实现深拷贝 123456var copyObj = { name: 'ziwei', arr : [1,2,3]}var targetObj = JSON.parse(JSON.stringify(copyObj)) 此时 copyObj.arr !== targetObj.arr 已经实现了深拷贝 别着急走,利用window.JSON的方法做深拷贝存在2个缺点: 如果你的对象里有函数,函数无法被拷贝下来 无法拷贝copyObj对象原型链上的属性和方法 当然,你明确知道他们的缺点后,如果他的缺点对你的业务需求没有影响,就可以放心使用了,一行原生代码就能搞定。目前我在开发业务场景中,大多还真可以忽略上面2个缺点。往往需要深拷贝的对象里没有函数,也不需要拷贝它原型链的属性。 写给第二类读者下面我会尽可能全面的讲解清楚JS里对象的拷贝,要讲清楚拷贝,你需要一点点前置知识 你需要的前置知识: 理解JS里的引用类型和值类型的区别,知道Obj存储的只是引用 对原型链有基本了解 关于对象拷贝的全部: 深拷贝、浅拷贝是什么 深拷贝、浅拷贝在业务里的最常见的应用场景 深拷贝和浅拷贝的实现方式 总结与建议 1. 深拷贝、浅拷贝是什么我们讨论JS对象深拷贝、浅拷贝的前提 只有对象里嵌套对象的情况下,才会根据需求讨论,我们要深拷贝还是浅拷贝。 比如下面这种对象 1234var obj1 = { name: 'ziwei', arr : [1,2,3]}因为,如果是类似这样{name: ‘ziwei’},没有嵌套对象的对象的话,就没必要区分深浅拷贝了。只有在有嵌套的对象时,深拷贝和浅拷贝才有区别浅拷贝是什么样子的 (我们暂时不管具体如何实现,因为下面会单讲)调用shallowCopy()后,obj2拷贝obj1所有的属性。但是obj2.arr和obj1.arr是不同的引用,指向同一个内存空间12345var obj2 = shallowCopy( obj1 , {})console.log( obj1 !== obj2 ) // true 无论哪种拷贝,obj1和obj2一定都是2个不同的对象(内存空间不同)console.log( obj2.arr === obj1.arr ) // true 他们2个对象里arr的引用,指向【相同的】内存空间 所以, 2个obj经过拷贝后,虽然他们属性相同,也的确是不同的对象,但他们内部的obj都是指向同一个内存空间,这种我们叫浅拷贝 深拷贝是什么样子的 (我们暂时不管具体如何实现,因为下面会单讲) 调用deepCopy()后,obj2拷贝obj1所有的属性,而且obj2.arr和obj1.arr是指向不同的内存空间, 2个obj2除了拷贝了一样的属性,没有任何其他关联。 12345var obj2 = deepCopy( obj1 , {})console.log( obj1 !== obj2 ) // true 无论哪种拷贝,obj1和obj2一定都是2个不同的对象(内存空间不同)console.log( obj2.arr === obj1.arr ) // false 他们2个对象里arr的引用,指向【不同的】内存空间 所以, 2个obj经过拷贝后,除了拷贝下来相同的属性之外,没有任何其他关联的2个对象,这种我们叫深拷贝 2. 深拷贝在业务里的最常见的应用场景举个栗子,业务需求是 : 一个表格展示商品各种信息,点击【同意】时,是可以弹出对话框调整商品数量的。 这种业务需求下,我们就会用到对象的深拷贝。因为【商品表格】的属性和【调整商品表格】的属性几乎一样,我们需要拷贝。 下面的伪代码和图片就是展示使用浅拷贝存在的问题 这样得到的adjustTableArr和tableArr里,内部对象都是相同的,所以就出现了图中红线标注的情况, 当我们修改【调整商品表格】里的商品数量时,【商品表格】也跟着改变了,这并不是我们想要的 123456789101112// 表格对象的数据结构var tableArr = [ {goods_name : '长袖腰背夹' , code : 'M216C239E0864' , num : '2'}, {goods_name : '长袖腰背夹' , code : 'M216C240B0170' , num : '3'}, {goods_name : '短塑裤' , code : 'M216D241C04106' , num : '3'}, ] var adjustTableArr = [] // 调整表格用的数组for (var key in tableArr) { // 浅拷贝 adjustTableArr[key] = tableArr[key]} 而实际上,我们希望这2个表格里的数据完全独立,互不干扰,只有在确认调整之后才刷新商品数量。 这种情况下我们就可以使用前面说的深拷贝的一行黑科技 1var adjustTableArr = JSON.parse(JSON.stringify(tableArr)) 还记得它的缺陷吗? 对象里的函数无法被拷贝,原型链里的属性无法被拷贝。这里就对业务没有影响,可以很方便的深拷贝。 3. 深拷贝和浅拷贝的实现方式其实JQ里已经有$.extend()函数,实现就是深拷贝和浅拷贝的功能。有兴趣的小伙伴也可以看看源码。 浅拷贝 浅拷贝比较简单,就是用for in 循环赋值 123456789function shallowCopy(source, target = {}) { var key; for (key in source) { if (source.hasOwnProperty(key)) { // 意思就是proto上面的属性,我不拷贝 target[key] = source[key]; } } return target;} 深拷贝的实现 深拷贝,就是遍历那个被拷贝的对象 判断对象里每一项的数据类型 如果不是对象类型,就直接赋值,如果是对象类型,就再次调用deepCopy,递归的去赋值。 1234567891011121314function deepCopy(source, target = {}) { var key; for (key in source) { if (source.hasOwnProperty(key)) { // 意思就是proto上面的属性,我不拷贝 if (typeof(source[key]) === "object") { // 如果这一项是object类型,就递归调用deepCopy target[key] = Array.isArray(source[key]) ? [] : {}; deepCopy(source[key], target[key]); } else { // 如果不是object类型,就直接赋值拷贝 target[key] = source[key]; } } } return target;} 以上的无论深、浅拷贝,都用了source.hasOwnProperty(key),意思是判断这一项是否是其自有属性,是的话才拷贝,不是就不拷贝。 也就是说proto上面的属性,我不拷贝。这个其实你可以根据业务需求,来决定加上和这个条件 (JQ的$.extend()是会连proto上的属性也拷贝下来的,但是是直接拷贝到对象上,而不是放到之前的proto上) 4. 总结与建议虽然大家可能经常用框架提供的api来实现深拷贝。 这篇文章分享的目的,更多还是希望用一篇文章整理清楚深浅拷贝的含义、递归实现思路,以及小伙伴们如果使用了JSON.parse()这种黑科技,一定要清楚这样写的优缺点。 5. 修正上面的deepCopy方法有漏洞,没有考虑source一开始就是数组的情况 下面是一个修改后版本 1234567891011function deepCopy( source ) { let target = Array.isArray( source ) ? [] : {} for ( var k in source ) { if ( typeof source[ k ] === 'object' ) { target[ k ] = deepCopy( source[ k ] ) } else { target[ k ] = source[ k ] } } return target}","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://www.sanks-blog.com/categories/JavaScript/"}],"tags":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://www.sanks-blog.com/tags/JavaScript/"}]},{"title":"前端工程师成长的痛,你占几条?","slug":"前端工程师成长的痛,你占几条?","date":"2019-02-23T01:48:01.000Z","updated":"2020-07-12T09:30:58.731Z","comments":true,"path":"前端工程师成长的痛,你占几条?/","link":"","permalink":"https://www.sanks-blog.com/前端工程师成长的痛,你占几条?/","excerpt":"原文链接作者:真传Xhttps://mp.weixin.qq.com/s?__biz=MzAxODE2MjM1MA==&mid=2651555898&idx=1&sn=1a523de9728c65c03bc851620a06240e 引言对于很多前端工程师,很容易进入工作的舒适区,该熟悉的业务已熟悉了,然后就是重复用轮子,这样很容易让自己的成长处于原地打转以及低水平重复的状态。 去年11月,我们累计交流了203人(1-3年的前端工程师,遇到职业瓶颈),有的是主观原因造成的 ,有的是客观原因造成的,本文从客观跟主观两方面进行了总结,帮助大家 自检 。","text":"原文链接作者:真传Xhttps://mp.weixin.qq.com/s?__biz=MzAxODE2MjM1MA==&mid=2651555898&idx=1&sn=1a523de9728c65c03bc851620a06240e 引言对于很多前端工程师,很容易进入工作的舒适区,该熟悉的业务已熟悉了,然后就是重复用轮子,这样很容易让自己的成长处于原地打转以及低水平重复的状态。 去年11月,我们累计交流了203人(1-3年的前端工程师,遇到职业瓶颈),有的是主观原因造成的 ,有的是客观原因造成的,本文从客观跟主观两方面进行了总结,帮助大家 自检 。 一、客观原因 没有经过系统的计算机学科学习导致计算机功底不扎实 这种问题主要出现在以下三类小伙伴身上: (1)计算机科班出身,但是在校期间基础不扎实。 (2)非计算机科班,通过自学,但不够系统,所学不成体系。 (3)非计算机科班,通过0基础的培训班,短时间速成,不够扎实。 因工作环境环境限制,导致成长性不足 以上客观主要导致问题表现如下: 因一些基础算法、数据结构理论不扎实导致一些编程思维难于理解。 比如 原型链,如果清楚 数据结构中链表结构,那么这个东西不难理解,再比如 哈希值,懂得数据结构中哈希表,哈希值也就迎刃而解。 计算机体系结构、操作系统理论、网络理论不扎实导致到后期一些东西难于理解。 比如有同学从前端学习入手,后来学习node开发 ,在 I/O ,进程、线程、IPC 、线程锁方面有些概念就比较难于理解,而导致不能很好得使用node 的api 。 工作业务型驱动,重复低水平劳动,基本上工作第一年对前端开发已经比较熟悉了,每天做表层业务模块,重复做已经会的东西,技术深度没有成长。 工作环境中无高手,前端开发团队大家水平相当,没有高手能够指导自己进入下一个层次。 二、主观原因 没有意识建立自己的底层系统 前端整体体系架构没有做过深入思考,导致用会用,但不知道为什么用,用另外一个有什么区别。 个人行动力不足,没有针对性的刻意练习 (1)有意识但是没有行动,比如网盘收集了一堆资料或者一堆视频,然后就没有然后了。 (2)自律性不足,很容易被外界影响,导致时间碎片化。 三、提升路径 学习东西,自学是一种途径,然而在自学的过程中,很多人处于不知道自己不知道的状态,无监督 ,非常容易放弃。 另一种是跟着高手,开阔视野,达到 知道自己不知道的境界,从而有方向,在短时期内进入到一个新的境界,节约时间成本,借助外力突破瓶颈。 @真传Xmp.weixin.qq.com/s?__biz=MzAxODE2MjM1MA==&mid=2651555898&idx=1&sn=1a523de9728c65c03bc851620a06240e","categories":[{"name":"随笔","slug":"随笔","permalink":"https://www.sanks-blog.com/categories/随笔/"}],"tags":[{"name":"随笔","slug":"随笔","permalink":"https://www.sanks-blog.com/tags/随笔/"}]},{"title":"Hexo-主题文件夹上传不到自己的github上","slug":"Hexo-主题文件夹上传不到自己的github上","date":"2019-02-10T06:44:49.000Z","updated":"2020-07-12T09:30:58.617Z","comments":true,"path":"Hexo-主题文件夹上传不到自己的github上/","link":"","permalink":"https://www.sanks-blog.com/Hexo-主题文件夹上传不到自己的github上/","excerpt":"引言好多人都已经用Hexo博客框架搭建了自己的博客,而且也部署到了自己的github上(或者是自己的云服务器上),再简单购买一个域名,让别人也可浏览自己的博客。但是搭建过程中遇到了好多坑,自己的博客代码上传至自己的github上,但是发现除了主题文件夹下的文件,其他都 push 上去了 而自己本地的主题文件夹是有文件的 探索大家可能想到是因为.gitignore里面忽略了这两个文件夹下的所有文件,但是经过自己的检查,发现并不是这儿的问题,自己的项目的.gitignore内容如下:","text":"引言好多人都已经用Hexo博客框架搭建了自己的博客,而且也部署到了自己的github上(或者是自己的云服务器上),再简单购买一个域名,让别人也可浏览自己的博客。但是搭建过程中遇到了好多坑,自己的博客代码上传至自己的github上,但是发现除了主题文件夹下的文件,其他都 push 上去了 而自己本地的主题文件夹是有文件的 探索大家可能想到是因为.gitignore里面忽略了这两个文件夹下的所有文件,但是经过自己的检查,发现并不是这儿的问题,自己的项目的.gitignore内容如下: 真正的解决办法经过多番探索,终于找到了症结,先来说如何解决 凡是通过git clone从github上拉取的代码,删除除了项目根目录以外的任何位置的 .git文件夹,.gitignore (或者编辑这个文件夹,删除那些你想上传但是被忽略的文件或文件夹) 和 .github 文件夹 操作完成之后,用SourceTree还是看不到需要上传的主题文件在“未暂存文件”一栏中,不要失望接着往下看 还要删除掉 SourceTree 中的 主题 子模块 ,如下图: 删除时,一定要勾选 “强制删除” ,要不然会删除不掉,而且SourceTree报错 出现此问题的原因主要根源是每次我们下载主题时,都会用git命令clone源代码,例如像这样: 1$ git clone --branch v5.1.2 https://github.com/iissnan/hexo-theme-next themes/next 最终导致自己的主题文件夹下多了个.git文件夹,会被认为是另一个资源库,从属于自己的项目之下,在SourceTree中显示成“子模块”,而这些项目需要的主题文件不会被push到自己的github仓库中。","categories":[{"name":"Hexo","slug":"Hexo","permalink":"https://www.sanks-blog.com/categories/Hexo/"}],"tags":[{"name":"hexo","slug":"hexo","permalink":"https://www.sanks-blog.com/tags/hexo/"}]},{"title":"Hexo 插入图片","slug":"Hexo-插入图片","date":"2019-02-09T06:13:18.000Z","updated":"2021-12-26T09:13:13.615Z","comments":true,"path":"Hexo-插入图片/","link":"","permalink":"https://www.sanks-blog.com/Hexo-插入图片/","excerpt":"引言图片资源放在本地 source/ 文件夹后,本地服务器浏览时图片正常显示,但部署到 github 上会找不到图片。 究其原因,是图片路径出现问题。开始时自己在 source/ 文件夹下建了 assets 文件夹,专门用于存放文章相关的图片: 12345source |- _posts |- assets |- images |- image-1.png 使用 markdown 引用图片的方式为 1 查看结构,发现部署以后,图片会自动添加日期相关的文件结构目录: 而实际存放的目录是 http://www.sanks-blog.com/assets/images/image-1.png ,导致图片资源访问不到。 为了解决这个问题查了很多资料,才知道原来除了本地存放图片,还可以使用图床。","text":"引言图片资源放在本地 source/ 文件夹后,本地服务器浏览时图片正常显示,但部署到 github 上会找不到图片。 究其原因,是图片路径出现问题。开始时自己在 source/ 文件夹下建了 assets 文件夹,专门用于存放文章相关的图片: 12345source |- _posts |- assets |- images |- image-1.png 使用 markdown 引用图片的方式为 1 查看结构,发现部署以后,图片会自动添加日期相关的文件结构目录: 而实际存放的目录是 http://www.sanks-blog.com/assets/images/image-1.png ,导致图片资源访问不到。 为了解决这个问题查了很多资料,才知道原来除了本地存放图片,还可以使用图床。 图床所谓图床,就是储存图片的服务器,支持创建图片的对外链接地址便于引用。使用时只要引入图片的绝对地址就可以,方便简单。 图床分为免费和收费的。无论是国内还是国外的免费图床都存在隐患,毕竟小本买卖,一旦停止服务,图片自然也就变成了小红叉。收费图床稳定一些,不管从服务还是稳定性上,都更推荐收费图床。 目前大家推荐比较多的国内图床: 七牛云储存新注册用户可免费使用 10G 存储空间。 极简图床其实也是依赖七牛云储存账号的。 本地图片在本地存放图片的方法经过修改以后也可以完美使用。重点在于 _config.yml 文件中设置 post_asset_folder: true,开启资源文件夹功能,该功能支持用户通过相对路径标签引用资源。 下面我们来看会发生什么。执行 hexo new [layout] [title]创建一篇新的文章,会发现 source/_posts 下自动生成了一个和 md 文件同名的目录(也可以自己手动创建),这就是用于存放与文章有关的图片文件夹。 例如,我们想写一篇名为 hexo.md 的文章,执行 hexo new “hexo”,那么 _post 文件夹下的结构将是: 123_posts |- hexo.md |_ hexo 将相关的图片放入 hexo 文件夹 1234_posts |- hexo.md |_ hexo |_ image-2.png 需要注意的是,使用该种方式在 markdown 文件中引用图片将不再使用 markdown 语法,而是使用标签插件引用相对路径,否则可能造成图片和其他资源显示不正确。引用语法如下: 123{% asset_path slug %}{% asset_img slug [title] %}{% asset_link slug [title] %} 在上述语法下,插入图片的方法: 1{% asset_img image-2.png This is an example image %} 查看页面,发现图片已经可以正常显示了,图片的路径和实际存放目录是一致的。 如果想使用 markdown 语法插入相对路径的图片,可以利用插件。设置 post_asset_folder:true 后,在根目录下执行: 1npm install https://github.com/CodeFalling/hexo-asset-image --save 确保在 source/_posts 下创建和 markdown 文件同名的目录,里面存放需要的图片,然后在 markdown 中插入图片: 1! [hexo image] (hexo/image-1.png) 生成的页面中图片引用路径 1<img src="/2019/02/10/hexo/image-1.png" alt="hexo image"> 至此,用 markdown 完美实现本地图片插入。 参考文档:https://hexo.io/zh-cn/docs/asset-folders.html","categories":[{"name":"Hexo","slug":"Hexo","permalink":"https://www.sanks-blog.com/categories/Hexo/"}],"tags":[{"name":"hexo","slug":"hexo","permalink":"https://www.sanks-blog.com/tags/hexo/"}]},{"title":"vue指令中寻找元素parentNode为null的问题(指令中钩子函数的运用)","slug":"vue指令中寻找元素parentNode为null的问题(指令中钩子函数的运用)","date":"2019-01-23T04:20:59.000Z","updated":"2020-07-12T09:30:58.724Z","comments":true,"path":"vue指令中寻找元素parentNode为null的问题(指令中钩子函数的运用)/","link":"","permalink":"https://www.sanks-blog.com/vue指令中寻找元素parentNode为null的问题(指令中钩子函数的运用)/","excerpt":"引语在VUE中运用 VUE 指令,发现之前的写法存在问题,刷新页面后报错,先贴代码, 再看报错 12345678// 权限指令Vue.directive('has', { bind: function (el, binding) { if (el.parentNode && !Vue.prototype.$_has(binding.value)) { el.parentNode.removeChild(el) } }}) 只要刷新页面会出现如下问题:","text":"引语在VUE中运用 VUE 指令,发现之前的写法存在问题,刷新页面后报错,先贴代码, 再看报错 12345678// 权限指令Vue.directive('has', { bind: function (el, binding) { if (el.parentNode && !Vue.prototype.$_has(binding.value)) { el.parentNode.removeChild(el) } }}) 只要刷新页面会出现如下问题: 猜测并寻找出现报错的原因看到这样的报错就顺藤摸瓜到这个VUE指令中,初步猜想是 函数中 el 元素未找到,但实际则是 el 元素的父节点 el.parentNode 未找到,值为 null,于是写如下代码验证自己的猜测 123456789101112// 权限指令Vue.directive('has', { bind: function (el, binding) { console.log(el.parentNode) if (el.parentNode && !Vue.prototype.$_has(binding.value)) { el.parentNode.removeChild(el) } setTimeout(() => { console.log(el.parentNode) }, 2000) }}) 结果是这样的: 进而验证了我的猜测 寻找解决方案经过百度查阅资料,终于恍然大悟,这篇文章说到点上了vue指令与$nextTick 操作DOM的不同之处或许这篇文章跟我要解决的不是一个问题,但是已经写的很明了,把关键的地方摘抄到这: vue指令钩子函数 一个指令定义对象可以提供如下几个钩子函数 (均为可选): bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。 inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。 update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。 componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。 unbind:只调用一次,指令与元素解绑时调用。 最为关键的一句,解决问题的关键inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。 更改后的代码为12345678// 权限指令Vue.directive('has', { inserted: function (el, binding) { if (el.parentNode && !Vue.prototype.$_has(binding.value)) { el.parentNode.removeChild(el) } }}) 总结:如果当前节点刚刚被建立,还没有被插入到DOM树中,则该节点的parentNode属性会返回null. 此时,完美解决问题。所以在用任何东西之前都熟读文档是很有必要的,免得出现问题像我这样绕一大圈","categories":[{"name":"Vue","slug":"Vue","permalink":"https://www.sanks-blog.com/categories/Vue/"}],"tags":[{"name":"vue","slug":"vue","permalink":"https://www.sanks-blog.com/tags/vue/"}]},{"title":"Axios 执行多个并发请求(使用Promise解决多层异步调用)","slug":"Axios-执行多个并发请求-promise","date":"2018-12-18T00:21:43.000Z","updated":"2020-07-12T09:30:58.607Z","comments":true,"path":"Axios-执行多个并发请求-promise/","link":"","permalink":"https://www.sanks-blog.com/Axios-执行多个并发请求-promise/","excerpt":"引语场景:工作中遇到一个数据接口同时依赖于另外两个接口的情况,需要两个接口返回的数据才能实现进一步操作,下面介绍 3 种方法 首先普及一下promise的一些冷门知识执行 then 和 catch 会返回一个新的 Promise ,该 Promise 最终状态根据 then 和 catch 的回调函数的执行结果决定 如果回调函数最终是 throw , 该 Promise 是 rejected 状态 如果回调函数最终是 return , 该 Promise 是 resolved 状态 但如果回调函数最终 return 了一个 Promise , 该 Promise 会和回调函数的 reurn 的状态保持一致 利用js回调嵌套的方式1234567891011121314151617181920212223242526// 异步接口1: 科室列表function getDepartmentsList(callback){ //模拟实现 var departmentList = Math.ceil(Math.random()*1000) setTimeout(function(){ callback(departmentList) },Math.random()*1000)}// 异步接口2: 级别列表function getLevelList(callback){ //模拟实现 var levelList = Math.ceil(Math.random()*1000)+1000 setTimeout(function(){ callback(levelList) },Math.random()*1000)}//异步接口,列表中科室和级别码转换成对应的中文,需要前两个接口的数据function registrationList(departmentList,levelList,callback){ //模拟实现 var percent = Math.ceil(departmentList/levelList*100) setTimeout(function(){ callback(percent) },Math.random()*1000)}","text":"引语场景:工作中遇到一个数据接口同时依赖于另外两个接口的情况,需要两个接口返回的数据才能实现进一步操作,下面介绍 3 种方法 首先普及一下promise的一些冷门知识执行 then 和 catch 会返回一个新的 Promise ,该 Promise 最终状态根据 then 和 catch 的回调函数的执行结果决定 如果回调函数最终是 throw , 该 Promise 是 rejected 状态 如果回调函数最终是 return , 该 Promise 是 resolved 状态 但如果回调函数最终 return 了一个 Promise , 该 Promise 会和回调函数的 reurn 的状态保持一致 利用js回调嵌套的方式1234567891011121314151617181920212223242526// 异步接口1: 科室列表function getDepartmentsList(callback){ //模拟实现 var departmentList = Math.ceil(Math.random()*1000) setTimeout(function(){ callback(departmentList) },Math.random()*1000)}// 异步接口2: 级别列表function getLevelList(callback){ //模拟实现 var levelList = Math.ceil(Math.random()*1000)+1000 setTimeout(function(){ callback(levelList) },Math.random()*1000)}//异步接口,列表中科室和级别码转换成对应的中文,需要前两个接口的数据function registrationList(departmentList,levelList,callback){ //模拟实现 var percent = Math.ceil(departmentList/levelList*100) setTimeout(function(){ callback(percent) },Math.random()*1000)} 利用es6的promise解决回调地狱问题《ES6标准入门》对Promise的描述所谓Promise,就是一个对象,用来传递异步操作的消息。它代表了某个未来才会知道的结果的事件(通常是一个异步操作),并且这个事件提供统一的API,可供进一步处理。 MDN对Promise的描述:Promise 对象是一个代理对象(代理一个值),被代理的值在Promise对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。 这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的promise对象 当时看到Promise最头疼的,就是初学者看起来匪夷所思,也是最被js程序员广为称道的特性:then函数调用链。then函数调用链,从其本质上而言,就是对多个异步过程的依次调用,本文就从这一点着手,对Promise这一特性进行研究和学习。Promise的相关知识,请参阅 Promise的链式调用 基于以上对Promise的了解,我们知道可以使用它来解决多层回调嵌套后的代码蠢笨难以维护的问题。下面请看具体代码: 每个异步接口 返回一个promise对象123456789101112131415161718192021222324252627282930// 异步接口1: 科室列表function getDepartmentsList(){ //模拟 return new Promise(function(resolve,reject){ var departmentList = Math.ceil(Math.random()*1000) setTimeout(function(){ resolve(departmentList) },Math.random()*1000) }) }// 异步接口2: 级别列表function getLevelList(){ return new Promise(function(resolve,reject){ var levelList = Math.ceil(Math.random()*1000)+1000 setTimeout(function(){ resolve(levelList) },Math.random()*1000) }) }// 异步接口, 列表中科室和级别码转换成对应的中文,需要前两个接口的数据function registrationList(departmentList,levelList){ return new Promise(function(resolve,reject){ var percent = Math.ceil(departmentList/levelList*100) setTimeout(function(){ resolve(percent) },Math.random()*1000) })} 利用promise.all方法保证接口数据成功返回再执行操作123456Promise.all([getDepartmentsList(),getLevelList()]).then(function([departmentList,levelList]){ //这里写等这两个ajax都成功返回数据才执行的业务逻辑 registrationList(departmentList,levelList).then(function(percent){ console.log(percent) })}) Axios 解决方案(VUE)每个异步接口 返回一个axios对象12345678// 异步接口1: 科室列表getDepartmentsList () { return axios.get(process.env.BASE_API_WAP + 'category/2');},// 异步接口2: 级别列表getLevelList () { return axios.get(process.env.BASE_API_WAP + 'category/3');} 利用axios.all方法执行多个并发请求1234567891011121314151617181920212223242526272829303132333435363738394041// 过滤数据函数filterData (targetArray) { targetArray.forEach(item => { for (let key in this.departmentsList) { if (Number(item.departments) === this.departmentsList[key].id) { item.departments = this.departmentsList[key].name; } } for (let key in this.levelList) { if (Number(item.level) === this.levelList[key].id) { item.level = this.levelList[key].name; } } });}// 初始化init () { this.title = ''; registrationList(this.current, this.pageSize, this.title).then(data => { this.data = data.content; // 过滤数据 this.filterData(this.data); this.totalSize = data.total; this.pages = data.pages; }).catch(error => { console.log(error); });}mounted () { // 执行多个并发请求, 列表中科室和级别码转换成对应的中文,需要前两个接口的数据 let _this = this; axios.all([this.getDepartmentsList(), this.getLevelList()]) .then(axios.spread(function (list1, list2) { _this.departmentsList = list1.data; _this.levelList = list2.data; // 两个请求现在都执行完成 _this.init(); }));} 总结 前端解决异步的问题时常都会遇到,Promise给前端程序员带来了新的解决思路,在它基础之上的promise的工具库(如Axios),也是在此上的封装。只要明白了其中的原理,在什么开发框架下都能灵活运用。","categories":[{"name":"Promise","slug":"Promise","permalink":"https://www.sanks-blog.com/categories/Promise/"}],"tags":[{"name":"promise","slug":"promise","permalink":"https://www.sanks-blog.com/tags/promise/"},{"name":"axios","slug":"axios","permalink":"https://www.sanks-blog.com/tags/axios/"},{"name":"ES6","slug":"ES6","permalink":"https://www.sanks-blog.com/tags/ES6/"},{"name":"回调地狱","slug":"回调地狱","permalink":"https://www.sanks-blog.com/tags/回调地狱/"}]},{"title":"你真的会检查自己系统安装的VUE版本吗?","slug":"你真的会检查自己系统安装的VUE版本吗?","date":"2018-12-17T05:55:52.000Z","updated":"2020-07-12T09:30:58.726Z","comments":true,"path":"你真的会检查自己系统安装的VUE版本吗?/","link":"","permalink":"https://www.sanks-blog.com/你真的会检查自己系统安装的VUE版本吗?/","excerpt":"引语或许你觉得我这篇文章写的很傻,和无聊,但是我跟你说,即使你从事VUE开发一段时间,也不见得求在一些小问题上所求甚解。 有些人认为的VUE版本检查命令是:1vue -V 或者1vue --version 如下图","text":"引语或许你觉得我这篇文章写的很傻,和无聊,但是我跟你说,即使你从事VUE开发一段时间,也不见得求在一些小问题上所求甚解。 有些人认为的VUE版本检查命令是:1vue -V 或者1vue --version 如下图 其实你们大错特错这哪里是检查VUE版本的,那是vue-cli的版本,vue-cli是搭vue框架的脚手架,是vue的生态环境之一 检查自己项目的VUE版本项目根目录下 package.json 中的VUE版本为安装依赖的最低支持版本,例如: “VUE”: “^2.5.13”, 要想项目运行正常,安装的VUE版本最低为 2.5.13如果要检查VUE版本,需要到node_modules中vue文件夹下的package.json中查找,或者是任意一个文件的头部注释 总结现在就目前而言,vue-cli已经进入3.0时代,与2.0的脚手架使用差别有些大了,项目构建初期的选择性更灵活了,比如 PWA ,目前VUE版本还在2.0时代,据说VUE3.0就快出来了,很期待。","categories":[{"name":"Vue","slug":"Vue","permalink":"https://www.sanks-blog.com/categories/Vue/"}],"tags":[{"name":"vue","slug":"vue","permalink":"https://www.sanks-blog.com/tags/vue/"}]},{"title":"element-ui table展开行,设置type=\"expand\",如何添加表头?如何去掉展开按钮并设置成文字?","slug":"element","date":"2018-09-27T15:08:37.000Z","updated":"2020-07-12T09:30:58.706Z","comments":true,"path":"element/","link":"","permalink":"https://www.sanks-blog.com/element/","excerpt":"","text":"解决方案从项目的可维护和可扩展性考虑,还是改 element-ui 的源码,是最好的解决方案。添加了了一个属性 look, 更改了展开行中的图标 >,如下图 表头 label 属性,源码本身就支持,用就可以了用的时候这样用,如下:改变了之后,变为文字,如下图 用法12<el-table-column label=\"操作\" type=\"expand\" look=\"查看\"></el-table-column> 源代码文件夹替换替换将修改后的 lib 文件夹 替换掉你的 element-ui 中的 lib 文件夹, 路径:node-modules/element-ui点击下载","categories":[{"name":"Element-UI","slug":"Element-UI","permalink":"https://www.sanks-blog.com/categories/Element-UI/"}],"tags":[{"name":"element-ui","slug":"element-ui","permalink":"https://www.sanks-blog.com/tags/element-ui/"}]},{"title":"VUE的权限控制","slug":"VUE的权限控制","date":"2018-08-22T07:17:06.000Z","updated":"2020-07-12T09:30:58.625Z","comments":true,"path":"VUE的权限控制/","link":"","permalink":"https://www.sanks-blog.com/VUE的权限控制/","excerpt":"概述如果VUE权限控制问题困扰着你,那么这篇文章将拯救你。关于VUE的前台路由控制和视图控制是大家最需要的前端技术解决方案。 Vue-Access-Control本解决方案是基于 Vue-Access-Control 进行改造的,深度剖析了里面的路由控制和视图控制(资源控制还是后台做比较靠谱) 心路历程 权限数据由后台接口获得(权限树),但是前端不能贸然存储到本地浏览器里(localStorage、sessionStorage、Cookie等),如果被恶意篡改,麻烦可就大了!!!!! 想要用 VUEX (状态管理模式)来存储,但是网页一刷新,就会被重置成空,所以我推断 VUEX 适合用在 “无刷新” 的 APP 中;再者,权限树这么复杂的结构,并不是 VUEX 所实现的 “共享状态” 模式,只是单一的对每个页面(路由控制)、按钮(视图控制)、接口(请求控制) 针对实际的应用场景,请求控制,就是某个角色是否有调用某个接口的权限,这种后台会做权限控制的,没有权限会给你报401的,只有“路由控制”和“视图控制”是前端人员需要去解决的。 对于 Vue-Access-Control 这套权限解决方案貌似也有不完美之处,不能贴合实际的开发需要,需要稍作调整,比如路由嵌套两层还是可以使用的,要是三层及其以上就得修改递归的函数了。","text":"概述如果VUE权限控制问题困扰着你,那么这篇文章将拯救你。关于VUE的前台路由控制和视图控制是大家最需要的前端技术解决方案。 Vue-Access-Control本解决方案是基于 Vue-Access-Control 进行改造的,深度剖析了里面的路由控制和视图控制(资源控制还是后台做比较靠谱) 心路历程 权限数据由后台接口获得(权限树),但是前端不能贸然存储到本地浏览器里(localStorage、sessionStorage、Cookie等),如果被恶意篡改,麻烦可就大了!!!!! 想要用 VUEX (状态管理模式)来存储,但是网页一刷新,就会被重置成空,所以我推断 VUEX 适合用在 “无刷新” 的 APP 中;再者,权限树这么复杂的结构,并不是 VUEX 所实现的 “共享状态” 模式,只是单一的对每个页面(路由控制)、按钮(视图控制)、接口(请求控制) 针对实际的应用场景,请求控制,就是某个角色是否有调用某个接口的权限,这种后台会做权限控制的,没有权限会给你报401的,只有“路由控制”和“视图控制”是前端人员需要去解决的。 对于 Vue-Access-Control 这套权限解决方案貌似也有不完美之处,不能贴合实际的开发需要,需要稍作调整,比如路由嵌套两层还是可以使用的,要是三层及其以上就得修改递归的函数了。 实现原理详见Vue-Access-Control 具体实现授之以鱼不如授之以渔 本着这个原则,代码的主要设计思想是: 将接口所得数据(菜单树形结构)存至本地(sessionStorage); 每次刷新页面重新调取接口,更新数据到本地(sessionStorage),保证设置权限的实时效应; 其次是用VUEX的状态机管理机制,由于父子组件相互调用问题,会导致数据不能及时从父组件更新到子组件的视图上,就是不显示在页面上;故而用VUEX来更新数据,让其获取到全局性质的值,这种设计来源于同事遇到的这个坑。 App.vue123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139<template> <div id=\"app\" class=\"app-main\"> <router-view></router-view> </div></template><script>import Vue from 'vue'import {getBankAuth} from '@/api/auth'import userPath from '@/router/fullpath'import * as util from '@/utils/util.js'export default { name: 'App', data () { return { menuData: null, // 导航树 userAuth: null // 接口返回权限数据 } }, methods: { getRoutes (userAuth) { if (!userAuth[0].children) { return console.warn(userAuth) } let allowedRouter = [] // 将菜单数据转成多维数组格式 let arrayMenus = util.buildMenu(userAuth[0].children) // 将多维数组转成对象格式 let hashMenus = {} hashMenus = util.getPath(arrayMenus) // 全局挂载hashMenus,用于实现路由守卫 this.$root.hashMenus = hashMenus // 筛选本地路由方法 let findLocalRoute = function (array, base) { let replyResult = [] array.forEach(function (route) { let pathKey = (base ? base + '/' : '') + route.path if (hashMenus.hasOwnProperty(pathKey)) { if (Object.prototype.toString.call(route.children) === '[object Array]') { route.children = findLocalRoute(route.children, pathKey) } replyResult.push(route) } }) if (base) { return replyResult } else { allowedRouter = allowedRouter.concat(replyResult) } } let originPath = util.deepcopy(userPath) findLocalRoute(originPath) return allowedRouter }, extendRoutes (allowedRouter) { let vm = this let actualRouter = util.deepcopy(allowedRouter) actualRouter.map(e => { // 复制子菜单信息到meta用于实现导航相关效果,非必需 if (e.children) { if (!e.meta) e.meta = {} e.meta.children = e.children } // 为动态路由添加独享守卫 return e.beforeEnter = function (to, from, next) { if (vm.$root.hashMenus[to.path]) { next() // 按钮权限检验方法 Vue.prototype.$_has = function (p) { let permission = false // 校验权限 this.hashButtons.forEach(item => { if (item.hasOwnProperty(to.path)) { if (item[to.path].indexOf(p) !== -1) { permission = true } } }) return permission } } else { next('/401') } } }) // let originPath = util.deepcopy(userPath) let originPath = actualRouter // 注入路由 vm.$router.addRoutes(originPath.concat([{ path: '*', redirect: '/login' }])) }, // 获取权限数据 getAuthority (role) { let vm = this // 检查登录状态 let localUser = util.session('token') if (!localUser || !localUser.authorities) { return vm.$router.push({ path: '/login', query: { from: vm.$router.currentRoute.path } }) } if (role === 'bank') { getBankAuth().then(data => { let userAuth = data // 获得实际路由 let allowedRouter = vm.getRoutes(userAuth) // 若无可用路由限制访问 if (!allowedRouter || !allowedRouter.length) { util.session('token', '') return document.body.innerHTML = ('<h1>账号访问受限,请联系系统管理员!</h1>') } // 动态注入路由 vm.extendRoutes(allowedRouter) // 保存数据用作他处,非必需 vm.menuData = allowedRouter vm.userAuth = userAuth }).catch(error => { console.log(error) }) } } }, created () { this.getAuthority('bank') }}</script><style lang=\"scss\"> @import 'assets/sass/sks.scss'; @import 'assets/fonts/iconfont.css';/*阿里字体图标*/ @import 'assets/sass/table.scss';/*table样式*/ @import 'assets/sass/dialog.scss'; /* dialog样式 */ .app-main { width: 100%; height: 100%; overflow: auto; }</style> 代码剖析Layout.vue1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950export default { name: 'layout', created () { this.getNav() }, methods: { getNav () { // 设置导航 let menus = this.$parent.menuData if (!localStorage.navBankArray) { if (menus) { // 整理导航数据结构 menus.forEach((item, index) => { if (index === 0) { menus[index].active = true } else { menus[index].active = false } }) this.routerMap = menus localStorage.navBankArray = JSON.stringify(menus) } } else { let tempBankArray = JSON.parse(localStorage.navBankArray) this.routerMap = tempBankArray } }, logout () { // 清除session util.session('token', '') // 清除菜单权限 this.$root.hashMenus = {} // 退出登录 logoutBank() .then(res => { store.commit('logout', this) this.$router.replace({name: 'Login'}) localStorage.removeItem('navBankArray') }) .catch(error => { console.log(error) }) } }, watch: { $route () { this.getNav() } }}","categories":[{"name":"Vue","slug":"Vue","permalink":"https://www.sanks-blog.com/categories/Vue/"}],"tags":[{"name":"vue","slug":"vue","permalink":"https://www.sanks-blog.com/tags/vue/"},{"name":"permission","slug":"permission","permalink":"https://www.sanks-blog.com/tags/permission/"}]},{"title":"Hello World","slug":"hello-world","date":"2018-08-21T07:17:06.000Z","updated":"2020-07-12T09:30:58.708Z","comments":true,"path":"hello-world/","link":"","permalink":"https://www.sanks-blog.com/hello-world/","excerpt":"","text":"Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub. Quick StartCreate a new post1$ hexo new \"My New Post\" More info: Writing Run server1$ hexo server More info: Server Generate static files1$ hexo generate More info: Generating Deploy to remote sites1$ hexo deploy More info: Deployment","categories":[{"name":"Hexo","slug":"Hexo","permalink":"https://www.sanks-blog.com/categories/Hexo/"}],"tags":[{"name":"hexo","slug":"hexo","permalink":"https://www.sanks-blog.com/tags/hexo/"}]}]}