作为一个前端,最近我也走上了不归路。本来想每天抽出一个小时来复习和反思自己每天刷的Leetcode,但是因为Leetcode服务器真的很渣,而且国内的访问速度出奇的慢,所以这个过程的体验极其恶心。于是自己写了一个leetcode爬虫,将自己在leetcode上传递的代码爬取到本地。核心是ES6generator和co,工具在这里:leetcode-spider。结合自己的经验,对它进行了多次优化改造。现在这个工具非常好用并且已经发布到NPM。代码已经爬下来了。作为前端,我肯定是想做点什么(错误的),我必须把它们呈现出来。我有一个博客,那我是不是应该把代码一行一行的复制到博客里,所以我的博客上贴了很多leetcode解题报告,我的博客完全被leetcode解题报告淹没了。所以我就想着怎么用心去呈现。如果是low,那就用node写个后台,读取文件内容用ajax返回给我前端,前端网页就可以呈现了,但是这些源码都是Static文件,这个网页不需要动态逻辑,所以我可以基于backend-free(无后台)架构来做。如果你用过hexo之类的静态博客,那你就明白我想干什么了。我会先用leetcode把解题代码写成json,然后用Vue2.x做一个单页应用网页。JS直接向静态服务器发送Ajax请求,请求json文件,呈现里面的代码内容。一个leetcode源码呈现的网站是这样搞的,而且是纯静态的。发布到githubpages或者自己的服务器,直接上线!在线地址在这里:leetcode使用了一个社交评论插件,多说说blabla,畅所欲言,还有评论功能。如果我想写下我解决问题的过程、经历、想法和情感怎么办?也可以这样,我在代码文件旁边写一个markdown文件,然后网页也向静态服务器发送请求获取这个文件,就可以呈现在网页上了。于是就这样搭建了一个有搜索功能,有评论,有自己的解题经验,有源码,有leetcode题目的leetcodeblog。而如果你只是写解题经验,那么根本不需要其他操作,实时编写,实时呈现,如果你爬取了新的解题源码,那么执行一条命令更新json(持续时间不超过一秒)。整个过程比建立一个leetcode博客要容易得多。项目地址在这里:leetcode-viwer。可以点击链接查看爬虫工具leetcode-spider和Vue单页应用leetcode-viewer的详细使用方法。都已经发到github了。就个人而言,我认为这是一个非常好的工具。可以用来和千千万万写过题的同学交流,更重要的是可以作为一个人的展示平台。找工作的时候,把链接放在简历上作为个人算法能力的展示也是相当不错的(嘿嘿,作为一个即将找工作的同学,我是这么想的)。下面说一下具体的实现过程。leetcode-spider既然是爬虫,肯定有很多异步请求。对于这种高I/O密集的场景,Node.js自然是适合的,但是由于是大量的异步操作,而且是一个链式的异步爬虫(也就是说,根据上一步的结果),那么肯定注定要写很多回调,即使是完全基于Promise,还是有很多.then和.catch绑定的回调,流控能力很强贫穷的。大量的.then并不比回调地狱中的花括号好多少。所以使用【generatorandco】或者【asyncandawait】是很有必要的,但是因为我想把这个工具开源发布到npm让更多人使用,使用async意味着用户需要Node7.X版本的是上面,场景还是太局限了,命令行模式--harmony-async-await在低版本node上报错,所以决定使用[generatorandco]。如果你用过koa,你肯定会明白这一点。两兄弟在异步场景下的丝滑体验。如果你使用co,你必须保证你yield的东西是一个promise或者一个thunk函数,而最新版本的co已经明确表示请不要再yieldthunk了。Co可能哪天都不支持thunk了,所以需要完全基于promise,那么问题来了,是不是意味着我需要大量的promise包装?当然,这不是必须的。借助死马大牛写的thenify和thenify-all,可以将基于回调的函数转换为返回promise的函数。所以我一行promise封装的代码都没有写,交给了thenify兄弟。还有一点,thenify是用来promisify一个函数的,那thenify-all呢。比如node自带的文件模块fs、fs.state、fs.mkdir、fs.writeFile等都是fs对象上的回调方法。如果使用thenify,需要将以上三种方法一一promify,而且使用3个变量保存起来比较麻烦。Thenify-all仅使用一行代码:letthenFs=thenifyAll(fs,{},['stat','mkdir','writeFile']);现在thenFs的新对象就是promisifyfs的三个方法下面的对象,thenFs.stat,thenFs.mkdir,thenFs.writeFile都是可以返回promise的方法。说完爬取过程中的promise包,再说说具体的爬取过程。爬取过程使用request发起请求,使用cheerio解析返回的HTML内容。解析完之后,就可以使用jquery的方式找到指定的DOM了,这两个都是Node爬虫常用的工具,这里不再赘述。一开始,用户需要编写一个JSON文件,其中包含用户名、密码和用于解码leetcode的语言。然后用账号密码登陆leetcode,做好cookie管理,然后请求这个接口api/problems/algorithms/就可以获取到这个账号AC的题目,然后在这些页面的代码往下爬就行了问题。直接一个一个爬取肯定太慢了,而且用node的好处是可以并行爬取,我不需要管理并行线程的运行。co对并行发起请求有非常友好的支持。你可以yield一个数组,将你想要发起的promise并行存储在数组中,或者使用Promise.all将数组处理成一个Promise然后yield。这些都是可行的工艺方案。现在的问题是,用户第一次爬取的时候,可能AC200个问题。如果我直接无节制的爬取200道题,一方面带宽有问题,另一方面leetcode的服务器是真实存在的。这个题很火,国内用过leetcode的人一定对这个题印象很深,我经常同时开几个题,同时写几个题,因为打开网页太慢,提交代码太慢,代码的结果也太慢卡...所以如果直接发送大量请求,会导致频繁丢包,响应返回不完整,连接中断等,所以我决定改变策略,限制并发数。当任务数量达到上限时,将完成部分任务。后续任务只有在任务完成后才能开始。这部分的实现是利用了TJ的co-parallel。事实上,协程团队已经开发了大量的协程辅助控制工具,比如并发请求的容错控制协聚,只取并发请求中最快的协任等,可以在合作团队的项目列表中查看。剩下的工作就是保存爬取的结果。这里没有什么特别的。可以借助node自带的模块fs来完成。同时,我还将爬取的结果保存在result.json文件中,这样下次再爬取的时候,通过result.json中的信息和从Leetcode网站上抓取的信息进行对比,就可以知道哪些代码被抓取了之前爬过,就不用再爬了(毕竟Leetcode真的很卡,可以省点save时间)。leetcode-viewer的构建过程leetcode-viewer是一个用Vue2.x搭建的单页应用。之前大量使用Vue1.x,自己也用Vue1.x搭建了一个博客,但是自从开始写题之后,一直没有跟上Vue的最新发展。我基本上把时间花在给研究生写题和开题上。所以我也想借此机会学习一下2.0版本。看了文档,其实并没有什么大的变化,主要是增加了新的功能。虽然API变了,但是主要思想没变,所以上手还是比较快的。首先我想到了用户应该如何使用这个网页来搭建自己的Leetcode博客。一方面,他觉得用Leetcode的同学很多,但是做前端的同学不多。于是想到后台完全没必要,后台写逻辑很麻烦,而且其他同学要搭建还得看懂代码自己改,所以决定写资料以json的形式作为数据源来呈现,因为任何一个静态资源服务器如Nginx、Apache或githubpages、国内的gitcafe等。当你把json、txt等静态文件放在上面时,你直接请求它们(比如在浏览器中输入地址),服务器会返回给你。所以让用户先爬取代码,爬下代码后执行一行npmrungenerate(不会超过1秒)将爬取的代码写入json,然后在单页应用的时候请求json开始跑起来。这样就创建了一个以前应用于hexo等静态博客的无后端架构的网页。把这个网页扔到任何一个静态服务器上,它就会上线。Vue2.0的踩坑体验生命周期钩子Vue2.0感觉最大的变化就是生命周期的一堆钩子函数变了,但是原来1.0的钩子函数其实不好用,主要是1.0时期,开启keep-alive并结合vue-router后,就比较复杂了。当一个组件被关闭时,它的destroyhook不会被执行,因为组件没有被销毁,它仍然在内存中。切换回来需要使用vue-router的deactivatehook。到时候组件的readyhook就没用了,需要改成vue-router的canreusehook,但是!坑爹的地方来了,有时候canreuse没有hook,deactivate也没有hook。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。.麻烦,调试的过程很痛苦。现在生命周期的钩子变了,vue-router的组件期的钩子没有了,data、deactivate、deactivate、canActivate等都干掉了,只剩下导航期的钩子,这使得alwaysinthedata刚开始在hook里写数据获取逻辑的时候有点晕,但是写了几行代码之后,发现很清楚了。其实1.0版本vue-router的很多hooks都替换了vue的hooks,这让vue本身的hooks有点没用的感觉,但是vue-router的hook没有vue的hook好用。现在一刀切之后,思路简单了,干扰也减少了。但是vue-router的datahook已经换掉了,所以根据官方文档,获取数据的逻辑放在了watch函数中,通过watch$route对象的变化获取数据,但是$在之前你切换到其他页面。watch方法也是起作用的,所以比如你在watch中写了当前页面的数据获取逻辑,那么当你转到其他页面时,数据获取逻辑仍然会被执行。这个小问题是不对的。所以我把我的数据获取逻辑写在一个if语句中。if首先检查this.$route.path的正则性,判断该路径是否为当前页面的路由。如果你切换到其他页面,那我什么都不做。但是这种方法让我在代码中耦合了路由设置。如果更改路由配置,不仅要更改Vue-router的配置,还要记得更改这里的正则表达式。这实际上可能会导致问题。.如果你有好的方法,请推荐。如何保证dom已经在文档中因为我想为leetcode源码的展示网页引入一个评论功能,方便源码的作者和读者交流解题方法和代码。第三方评论插件不多,实现这个功能自然没有问题。而畅言是需要备案的,所以我又回到了多多。不过说多了是多年前写的一个插件。我不会不时地谈论任何关于错误的事情。貌似官方已经不再维护了(前段时间微信无法登陆的问题多说了一个多月后修复了),最关键的是还是之前那个基于dom操作的插件,需要全局写一个叫duoshuoQuery的对象,在里面写参数,然后加载一个外部的JS,加载完JS之后,需要自己创建一个div,在div的属性中也写一些参数,然后对div执行DUOSHUO.EmbedThread方法,执行后将div追加到文档中已经存在的元素上。期间踩过的一些坑我就不说了,就说最后的append是对一个dom中已经存在的元素。vue1.0时代在vue上用的太多了,那么如何保证vue组件中的这个组件dom中的一个元素已经在dom中了,其实是踩过的坑。同学们别急着告诉我用$nextTick。我在vue1.0的时候第一时间想到了$nextTick,但是发现不行。执行$nextTick时,无法保证组件的元素已加载到dom中。后来仔细查询,看到友达在vue-router的一个issue下回复:nextTick是打算在你修改了一些reactive数据后马上使用的。nextTick是计划在你更改一些反应性数据时使用的。换句话说,nextTick应该用在某些计算属性或手表或绑定到按钮单击事件的方法中。它用来保证这些响应式的数据变化已经在dom中体现出来,而不是用来保证dom在组件加载过程中已经真正加载到文档中了。并且我尝试了那期yuda提到的attachedhook,但是并不能保证元素已经加载到dom中了。后来采用了比较hacky的方法,就是不断setTimeout检测元素是否在dom中来实现功能。以上vue1.0时代的坑,现在vue2.0一定要找到解决方案,第一选择,如我所料,$nextTick在这种场景下还是不行(其实很好理解,$nextTick是基于MutationObserver,你可以使用这个API,它是一个dom,当它发生变化时会触发一个事件告诉你它发生了变化,现在我们的使用场景中不存在这个dom,$nextTick当然不起作用).不过好在mountedhook可以用了,但是问题又来了。开启keep-alive后,mounted钩子只在元素第一次加载到文档时执行一次。如果你切换到另一个页面再切换回去,这时候因为实际上挂载了组件,所以不会执行,所以回来的时候发现评论框加载不出来,激活和去激活两个钩子对于keep-alive组件只能告诉你组件被切回和切出。,不保证dom在文档中。哎,累死了。。。后来放弃了用vue的方案来解决。我是在看jquery的$(document).ready()的源码的时候了解到的。不可靠,所以为了知道dom是否已经进入异步事件状态,jquery使用如下代码实现ready():try{top.doScroll("left");}catch(e){returnsetTimeout(doScrollCheck,50);}就是不断触发网页的滚动事件。如果不能滚动,说明还在loading阶段,绑定50毫秒后执行scrolling事件,直到网页可以滚动,即网页加载完成,进入异步事件监听状态。所以我的实现方式是将启动逻辑绑定在组件的滚动事件上,这样leetcode源码和文章出来后,只有在用户滚动的时候才会加载multi-talk组件,并且它还可以起到延迟加载的作用。影响。还是很hack,但是性能比之前vue1.0时代的setTimeout要好。值得总结的要点之一是错误处理。以前用koa的时候,大多是yield一个异步任务,但是现在我爬出来你的AC有什么问题之后,我要并行发起很多异步请求,就是yield是一个数组,里面有一个数组中存储了很多Promises,所以我是不是应该在每个Promise之后写一个catch,或者在这个数组上执行Promise.all然后在返回的promise上写一个catch,或者直接用trycatch将yield包裹起来,这里需要熟悉co的整个处理流程,同时你也要考虑你的容错策略,你并行发起请求后,你不能收回你嫁出去的女儿。改了怎么办?你知道你已经犯了一个错误。发送的请求无法撤回怎么办?如果你犯了很多错误,你该怎么办?所以结合了自己的一些思考和研究。目前co/koa中的错误处理已经写了一篇,正在填坑中,等写完发出来。二是并发控制。如前所述,使用的是共并联。事实上,它的代码量很短。最近,我们将仔细分析实现方法。todolist网页的响应式改造已经完成。转换是使用Vuex进行的。当时觉得这个应用的state不多,state的兄弟节点也挺少的,所以没有用vuex。现在看代码,状态管理还是太hacky了。大致就是我写这两个gadget的时候的思路和过程。原文发表于我的博客:欢迎观看
