前端面试每日-31-第203天

今天的知识点 (2019.11.05) —— 第203天 (我也要出题)[html] canvas的width与height属性的值可不可以带单位?[css] height和line-height的区别是什么呢?[js] 你平时是怎么调试js的?会断点调试吗?断点调试有什么技巧呢?[软技能] 前端如何防止XSS攻击?《论语》,曾子曰:“吾日三省吾身”(我每天多次反省自己)。 前端面试每日3+1题,以面试题来驱动学习,每天进步一点! 让努力成为一种习惯,让奋斗成为一种享受!相信 坚持 的力量!!!欢迎在 Issues 和朋友们一同讨论学习! 项目地址:前端面试每日3+1 【推荐】欢迎跟 jsliang 一起折腾前端,系统整理前端知识,目前正在折腾 LeetCode,打算打通算法与数据结构的任督二脉。GitHub 地址 微信公众号欢迎大家前来讨论,如果觉得对你的学习有一定的帮助,欢迎点个Star, 同时欢迎微信扫码关注 前端剑解 公众号,并加入 “前端学习每日3+1” 微信群相互交流(点击公众号的菜单:进群交流)。 学习不打烊,充电加油只为遇到更好的自己,365天无节假日,每天早上5点纯手工发布面试题(死磕自己,愉悦大家)。希望大家在这浮夸的前端圈里,保持冷静,坚持每天花20分钟来学习与思考。在这千变万化,类库层出不穷的前端,建议大家不要等到找工作时,才狂刷题,提倡每日学习!(不忘初心,html、css、javascript才是基石!)欢迎大家到Issues交流,鼓励PR,感谢Star,大家有啥好的建议可以加我微信一起交流讨论!希望大家每日去学习与思考,这才达到来这里的目的!!!(不要为了谁而来,要为自己而来!)交流讨论欢迎大家前来讨论,如果觉得对你的学习有一定的帮助,欢迎点个[Star] https://github.com/haizlin/fe...

November 5, 2019 · 1 min · jiezi

调试效果让页面上的元素显示红色框范围

效果: 第二种简单的方法:我们需要借助 Chrome 的书签功能。 打开书签管理页右上角三个点「添加新书签」名称随意,粘贴以下代码到网址中javascript: (function() { var elements = document.body.getElementsByTagName('*'); var items = []; for (var i = 0; i < elements.length; i++) { if (elements[i].innerHTML.indexOf('html * { outline: 1px solid red }') != -1) { items.push(elements[i]); } } if (items.length > 0) { for (var i = 0; i < items.length; i++) { items[i].innerHTML = ''; } } else { document.body.innerHTML += '<style>html * { outline: 1px solid red }</style>'; }})();然后我们就可以在任意网站上点击刚才创建的书签,内部会判断是否存在调试的 style。存在的话就删除,不存在的话就添加,通过这种方式我们就能很方便的通过这个技巧查看任意网页的布局了。 ...

October 15, 2019 · 1 min · jiezi

移动端H5调试技巧

chrome://inspect/#devices

July 10, 2019 · 1 min · jiezi

python进程调试改变运行中进程的代码

后端开发中有时会遇到这种问题:进程运行中偶现,重启进程问题就消失;或者是,进程一定要运行一段时间才会出现问题。 对于这些情况,尽管大部分时候,我们可以通过在可能的地方加log,然后重启进程等待问题复现,但这样相对被动。我们都知道如果要调试C/C++程序,gdb attach上进程就可以,而python虽然有相似的工具pdb,但它无法附加到一个进程上,必须要用pdb启动进程,在实际环境中显然不管用,那么python是否有类似的办法来改变运行中进程的代码呢?这样我们就可以实时加log,能实时加log了,还有什么问题不能定位呢:p 可以参考两篇文章: https://mozillazg.com/2018/07...https://mozillazg.com/2017/07... 简单来说,可以直接用gdb使用类似调试c程序的方式,但要求python进程是使用python-debug这种版本的python,同样不够实用。这里介绍博客中提到的“纯gdb”的方式,通过github上一个开源python包pyrasite,本质上是通过gdb的-eval-command命令。另外,我们还依赖于python作为动态语言的特性,可以在运行时对函数赋值来改变函数。 这个库有一些附加功能,可以通过它的文档去了解。这里只说实现进程注入的核心,是其中一个很短的文件injector.py,核心代码如下: import osimport subprocessimport platformdef inject(pid, filename, verbose=False, gdb_prefix=''): """Executes a file in a running Python process.""" filename = os.path.abspath(filename) gdb_cmds = [ 'PyGILState_Ensure()', 'PyRun_SimpleString("' 'import sys; sys.path.insert(0, \\"%s\\"); ' 'sys.path.insert(0, \\"%s\\"); ' 'exec(open(\\"%s\\").read())")' % (os.path.dirname(filename), os.path.abspath(os.path.join(os.path.dirname(__file__), '..')), filename), 'PyGILState_Release($1)', ] p = subprocess.Popen('%sgdb -p %d -batch %s' % (gdb_prefix, pid, ' '.join(["-eval-command='call %s'" % cmd for cmd in gdb_cmds])), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = p.communicate() if verbose: print(out) print(err) 这个函数做的事很简单,不难看懂,所以,我们需要做的就是调用这个函数,传入pid和文件名,文件是一个你要对这个进程执行的python代码。现在我们运行一个很简单的python进程test.py: ...

June 11, 2019 · 1 min · jiezi

如何提高后台服务应用问题的排查效率日志-VS-远程调试

转眼间,距离Jerry最近一篇文章推送已经过去了一个多月的时间了。 公众号更新的频率降低,不是因为Jerry偷懒,而是由于从春节过后,我所在的SAP成都研究院数字创新空间整个团队,一直在忙一个5月份需要交付的项目上。 Jerry每天的工作量像下面这张图这样: 这个项目里Jerry负责的是后台开发工作,我用nodejs开发了若干微服务,每个微服务实现一个特定的业务逻辑。这些微服务由Jerry另外开发的一个编排器(Orchestra)统一调度。整套后台实现部署在亚马逊云平台(Amazon Web Service,以下简称AWS)上。 离交付日期越来越近了,我们的功能也赶得差不多了。本地测试运行得很好的场景,部署到AWS上运行后出现了一些bug。比如昨天就遇到一个棘手的bug,因此有了今天这篇文章。 2014年五一节的前一天,当时Jerry还在SAP CRM开发团队工作,负责处理SAP CRM中间件的一个bug。这个bug和代码执行时序有关,每执行一次只有40%的几率能重现,花了我整整一天(8个小时)的时间调试。因为重现bug的场景太复杂,需要调试的ABAP代码量太大,所以让我印象深刻。那个bug处理完之后,我也对自己花了8小时才搞定该bug的效率很不满意,因此写了一篇博客总结这次排错的经验教训: My Tips about how to handle complex and tricky issues https://blogs.sap.com/2014/05... 回到昨天我遇到的在AWS上出现的bug,根据问题的表象,一开始我和负责前端开发的同事,连这个问题出在前端还是后端都没办法判断。当微服务部署在本地并进行测试时一切正常,只有部署在AWS上进行集成测试时才会暴露,而运行在AWS上的nodejs应用,我昨天还不知道如何调试,因此只好采用我大二刚学C语言编程时用过的最笨的排查办法:打日志。 2001年,在结束了一年的计算机专业基础课学习后,Jerry开始了Unix环境下C语言编程的学习。当时我对gdb这种以命令提示行方式进行的调试风格很不适应,大多数时候的排错采用的还是在代码里添加printf语句打印变量内容的方式来进行,被寝室的同学鄙视了好久。 于是昨天我继续采用了这种自己18年前就曾经用过的排错方式: 1. 在可能引起bug的相关代码处逐一加上日志输出语句 2. 执行会出现bug的用户操作 3. 阅读AWS上生成的日志语句 上述三个步骤是一个不断迭代的过程。最开始我加了若干日志输出语句,执行操作后阅读生成的日志,发现没有任何异常。于是不断地增加新的日志打印代码,最后导致了执行一次操作,会生成1200行的日志输出。 我和负责前端开发的同事两人坐在显示器前,一行行检查这海量的日志输出。由于问题是用户第二次操作后才会暴露,每次操作会生成不同的会话,我们被迫不断的上下滑动屏幕来比较这两次会话的uuid和相关的WebSocket uuid等变量。Jerry很快发现,眼睛一眨不眨地盯着显示器逐条检查日志,时间一长眼睛就痛得受不了。无奈之下,只得把这些日志用打印机打印出来,用不同颜色的笔标注出两个会话对应的各种变量,在纸上来回比对。于是就有了下面这些纸张: 虽然最后用这种办法,成功排除了后台出错的可能性,使我们得以把精力花在前台代码的审查上,但是像我一个同事评价的,“这种方式太不环保了”,并且我自己也觉得,效率太低了。 后来好几位热心的同事告诉Jerry,就算运行在SAP Cloud Platform或者AWS这些云平台上的nodejs应用,也是可以单步调试的,Jerry Google了一下,发现远程调试确实很简单,就两条命令而已。 Jerry用我们创新空间团队另外一位同事Haytham开发并部署在AWS上的一个nodejs应用为例来尝试如何在我的本地电脑上对其进行调试。 Haytham虽然是一个大四本科生,但是已经在SAP成都研究院Jerry所在团队实习将近十个月的时间了,最近三个月一直在SAP德国总部参与一个项目的开发。 等Haytham回到成都后,会将自己这十个月的工作感悟,从一个SAP新人的视角给大家分享出来,敬请期待。 Haytham之前写过的文章: SAP成都研究院许聚龙:Hello, Coresystems! Haytham写的这个nodejs应用实际上是Github Webhook的一部分。我们在本地进行微服务nodejs开发,本地git客户端推送代码到远端github仓库。然后需要在AWS上手动git pull把最新的代码拉下来,再用一个开源工具pm2进行微服务部署。Haytham写的这个nodejs应用,能实现本地git推送完毕后一切后续流程的完全自动化,节省了我们大量的部署时间。 下面就来对Haytham这个运行在AWS上的nodejs应用进行远程调试。 1. 用node --inspect-brk在AWS上以调试模式启动应用。 之后控制台上的输出表明有一个nodejs进程以WebSocket协议在127.0.0.1:9229这个地址上监听调试客户端的连接。 2. 我在我的本地电脑上,用如下命令行将我本地电脑的端口9221映射到AWS调试进程监听的9229端口上: ssh -i C:Usersi042416.sshKOI.pem -L 9221:localhost:9229 ubuntu@ec2-us-east-2.compute.amazonaws.com 现在,本地电脑上Chrome浏览器地址栏chrome://inspect里指定监听地址为localhost:9221,  通过第二步建立的SSH tunnel, 我就可以用本地电脑连接到AWS上的nodejs应用并进行调试了。 现在终于可以在Chrome开发者工具里进行愉快的调试了: 因为我平时本地做nodejs开发和调试时,更喜欢用Visual Studio Code,所以下一步我准备试试用Visual Studio Code进行远程调试。 说到Visual Studio Code,Jerry突然想起今天在网上看到的一个关于这个IDE的有意思的扩展,名为"超越鼓励师"。 Jerry试着在自己的Visual Studio Code扩展安装栏里搜索了一下,这个扩展还真的可以下载。不过扩展里出现的"杨超越",Jerry又孤陋寡闻了,咨询了老婆后才知道她是谁。 至于实际效果如何,Jerry不做评价,欢迎Visual Studio Code爱好者自行下载体验。 ...

May 9, 2019 · 1 min · jiezi

如何在手机端查看PC端上的Vue项目二外网环境

在《如何在手机端查看PC端上的Vue项目(一)》这篇文章中介绍了如何在同一网络下通过配置Vue项目的host实现在手机端查看Vue项目,但是这种方法毕竟有局限性,如果是台式机就不行了,所以今天介绍一下如何实现手机通过外网也能访问PC端的Vue项目准备工作:如何在手机端查看PC端上的Vue项目(一) 介绍了如何查看本机IP及配置Vue文件花生壳官网 注册账号、下载客户端草料二维码官网 用于将网址转为二维码1、查看本机IP、修改Vue项目中的配置上面的参考文章中已经介绍了如何查看本机IP并配置Vue文件,这里就不在赘述2、注册花生壳、设置内网映射2.1、点击上方花生壳官网链接,并注册账号,下载客户端 2.2、客户端登录后打开如下图所示,点击右下角加号 2.3、选择内网映射 2.4填写配置信息,最后点击保存 应用名称:随意应用类型:因为我们一般都是访问的网页,所以选择HTTP80,你也卡哇伊选择通用外网域名:这是你注册账号后,花生壳免费给你的,因为免费,所以看起来比较乱,想要好记的,你懂,money~~~~往外端口:不用设置,也没法设置,默认80内网主机:就是你电脑的IP,同时你Vue中配置的host也得是这个内网端口:就是你Vue中配置的port,记得别和其他端口号冲突

May 2, 2019 · 1 min · jiezi

如何在手机上查看电脑上的Vue项目

在PC端写好的Vue项目,一般都是npm run dev编译之后再本地浏览器地址栏输入localhost:8080来查看 为什么是输入localhost:8080,原因在于vue项目的配置文件config文件夹下的index.html中,host意为主机,localhost意为本地主机,port意为端口,8080就是默认的端口,当然你可以随意更改 因为是在本地主机启动的项目,所以只能在写代码的那台电脑查看,下面我们就来介绍如何在手机端查看1、保证手机和电脑在同一网络下将你的手机和电脑连接同一个WIFI,台式机的话。。。。,略过,以后会写台式机,哈哈2、查看电脑的内网IP内网IP哦,就是一般就是192.168....这样的打开CMD命令窗口,输入ipconfig,回车 3、修改Vue项目配置文件打开刚才上面提到的那个config文件夹下的index.js文件将host的值修改为你的IP地址 port改不改无所谓 4、重新启动Vue项目 5、在手机端打开浏览器,输入192.168.0.XXX:8080 OK,到此结束,该方法要注意两点:1、手机和电脑在同一网络下2、项目配置文件中设置的host一定要是电脑的IP,因为如果你电脑重启或者断网后,IP可能会变化

May 2, 2019 · 1 min · jiezi

如何在手机端查看PC上的写的页面

该方法的前提是先搭建好本地的IIS服务器,并且手机和电脑要在同一网络下,不知道如何做的可以参考下面我的两篇文章如何搭建本地IIS服务器如何在手机端查看Vue项目看了上面文章,我们已经知道了如何搭建本地IIS服务器,在配置的时候,设置了端口号,但是没有设置IP地址,所以我们要做的仅仅就是查看电脑的IP地址,关于如何查看电脑的IP第二篇文章有介绍,我们将IIS网站配置中的IP地址设置为我们电脑的IP,

May 2, 2019 · 1 min · jiezi

Linux下段错误调试技巧

更新于2019.04.16我们写的程序, 尤其是C/C++程序有时候会段错误, 而且往往发生在部署环境而非调试环境, 对问题定位带来很大困难. 这时一般有两种方法来解决问题, 一种是生成core dump文件, 然后用gdb调试这个文件; 另一种是不生成core dump文件, 而使用其他工具来定位问题.生成core dump文件并用gdb调试最简单的方法是运行ulimit -c unlimited命令, 然后在错误发生后用gdb调试这个文件. 但这种方法往往不好用, 比如段错误发生时没有生成core dump文件, 或core dump文件过大不完整等等.使用dmesg和addr2line命令这种方法是使用dmesg和addr2line命令. 这需要代码以-g选项编译 比如以下代码故意产生段错误:#include <stdio.h>int main(void){ int *p = NULL; *p = 0; printf(“bad\n”); return 0;}编译运行这个程序, 立即会发生段错误:$ gcc -O3 -g -o test test.c$ ./test段错误(吐核)这时先调用dmesg命令查看段错误信息:$ dmesg | grep segfault[422855.897248] test[63448]: segfault at 0 ip 0000000000400449 sp 00007ffd06202b70 error 6 in test[400000+1000]注意其中指令指针寄存器(IP)的值, 接下来调用addr2line命令, 把IP所指的地址转换为源码行号:$addr2line -e test 0000000000400449/root/tmp/test.c:6可见排查出源码第6号有错.如果段错误出在动态库而非可执行程序中, 在调用addr2line命令时, 需要将dmesg输出中IP的值减去行最后的地址值, 比如下面一行中[422855.897248] test[63448]: segfault at 0 ip 0000000000400449 sp 00007ffd06202b70 error 6 in test[400000+1000]addr2line的参数 = 400449 -400000.下面是动态库段错误的示例, 假设我们有3个文件, 主程序test.c, 动态库头文件foo.h和实现文件foo.c, 内容分别如下.test.c:#include “foo.h"int main(void){ foo(); return 0;}foo.h:#ifndef FOO_LIB_H#define __FOO_LIB_H__int foo(void);#endiffoo.c:#include “foo.h"int foo(){ int *p = 0; *p = 0; return 0;}先编译动态库, 再编译主程序, 让它链接动态库, 最后运行之:$ gcc -O3 -g -o libfoo.so -shared -fPIC foo.c$ gcc -O3 -g -o test test.c -L. -lfoo$ export LD_LIBRARY_PATH=.$ ./test段错误(吐核)调用dmesg:$dmesg | grep segfault[423801.507232] test[63800]: segfault at 0 ip 00007f8adeb08680 sp 00007ffeb7f29ab8 error 6 in libfoo.so[7f8adeb08000+1000]IP值减去动态库地址值(00007f8adeb08680 -7f8adeb08000=680), 调用addr2line, 注意-e参数后文件名改为动态库名:$ addr2line -e libfoo.so 680/root/tmp/foo.c:6让程序段错误时自动输出堆栈信息并调试以上方法还是显得不够方便, 还要调用dmesg命令等. 我们可以使用execinfo.h里的backtrace函数及信号处理机制, 来让程序在发生段错误时自动打印调用堆栈:#include <unistd.h>#include <signal.h>#include <execinfo.h>#include <stdio.h>#include <stdlib.h>#include <stdint.h>#include <string.h>#include <stdarg.h>#define BACKTRACE_SIZE 256void segv_handler(int sig){ void *func[BACKTRACE_SIZE]; char **symb = NULL; int size; size = backtrace(func, BACKTRACE_SIZE); backtrace_symbols_fd(func, size, STDERR_FILENO); exit(1);}int main(void){ int *p = NULL; signal(SIGSEGV, segv_handler); *p = 0xdeadbeef; return 0;}然后以-g选项编译, 运行:$ gcc -O3 -g -o auto auto.c$ ./auto./auto[0x400654]/lib64/libc.so.6(+0x35250)[0x7fbbe4283250]./auto[0x400543]/lib64/libc.so.6(__libc_start_main+0xf5)[0x7fbbe426fb35]./auto[0x40057e]输出的调用堆栈逆序打印, 最先调用的在最下面, 我们可以判断出是第3行的 ./auto[0x400543] 导致了段错误, 因为前面2行是信号SIGSEGV的处理函数调用. 运行addr2line:$ addr2line -e auto 0x400543/root/tmp/auto.c:29可知第29行代码 *p = 0xdeadbeef; 引起错误.参考How to automatically generate a stacktrace when my program crashes拒绝超大coredump - 用backtrace和addr2line搞定异常函数栈 ...

April 16, 2019 · 2 min · jiezi

[译] 监测与调试 Vue.js 的响应式系统:计算属性树(Computed Tree)

原文地址:Tracing or Debugging Vue.js Reactivity: The computed tree原文作者:Michael Gallagher译文出自:掘金翻译计划本文永久链接:https://github.com/xitu/gold-miner/blob/master/TODO1/tracing-or-debugging-vue-js-reactivity-the-computed-tree.md译者:SHERlocked93校对者:Reaper622, hanxiansen关于 Vue 的下一个主版本,公布的很多新特性引发了激烈的讨论,但其中有一个特性引起了我的注意:更良好的可调试能力:我们可以精确地追踪到一个组件发生重渲染的触发时机和完成时机,及其原因在本文中,我们将讨论在 Vue2.x 中如何监测响应式机制,并且将演示一些和性能调优相关的代码段。为什么响应式系统相关代码需要调优如果你的项目比较大,那么你很有可能在用 Vuex。你会将 store 分割为模块,并且为了关联数据的访问一致性你甚至需要将你的状态范式化。你可能使用 Vuex 的 getter 来派生状态,事实上,你还会使用复合的派生数据,即一个 getter 会引用另一个 getter 派生的数据。在 Vue 组件中,你会使用各种分层的模式,当然也包括经常用的 slots。在这样的组件树中,肯定会有计算属性(派生出来的数据)。当这些发生的时候,从 store 中的状态到渲染的组件之间的响应式依赖关系将很难理清楚。这就是计算属性树了,如果不把它弄清楚的话,那么翻转一个看似不起眼的布尔值可能会触发一百个组件的更新。基础知识我们将学习一些响应式机制的内部工作原理。如果你还没有(比较深地)理解 Dependency 类(译者注:Dep — 为与源码一致,后文都采用 Dep)与 Watcher 类之间的关系,可以考虑学习一下内容丰富、条例清晰的高级 Vue 课程:建立一个响应式系统。在浏览器开发工具中调试过程中见过 ob 么?承认吧,当时是不是有点好奇,ob 看起来是不是像这样?这些在 subs 中的 Watcher 将会在这个响应式数据发生改变的时候更新。有时候你会在开发者工具中浏览一下这些对象,并且找到一些有用的信息,有时候找不到。有时候你会发现 Watcher 远不止 5 个。举个例子我们用一些简单的代码说明一下:JSFiddle这个例子的 store 中的状态有散列数组 users 和 currentUserId 两个属性。还有一个 getter 用来返回当前用户的信息。另外还有一个 getter 只返回状态为活跃的用户数组。然后这里有两个组件,其中有三个计算属性:validCurrentUser — 若当前用户是有效用户则为 truetotal — 引用反映当前所有活跃用户的 getter,将返回活跃用户数upperCaseName — 将用户的姓名映射为大写形式希望举的这个特别的例子,对理解我们讨论的内容有所帮助。计算属性的响应式机制是如何运转的?通常,当从一个 Dep 类实例获取到更新的通知时,响应机制将会触发对应的 Watcher 函数。当我变更一个被组件渲染所依赖的响应式数据时,将触发重渲染。但我们看看派生的数据,它的情况有点复杂。首先,计算属性的值是被缓存起来的,以便在它计算出来之后就一直可用计算后的值,只有当它的缓存失效才会被重新计算,换句话说,只在其依赖的数据发生改变时它们才会重新求值。我们再来看看之前的例子。currentUserId 状态被 currentUser 这个 getter 引用了,然后在 validCurrentUser 计算属性引用了 currentUser,validCurrentUser 又是根组件 render 函数的 v-if 表达式的一部分。这条引用链看起来不错。实际上,响应数据的存储是通过一个 Watcher 的配置选项来处理的。当我们使用组件中的 Watcher 时,API 文档中介绍了两个可选选项(deep,immediate),但其实还有一些没被文档记录的选项,我并不推介你使用这些没被记录的选项,但理解他们却很有益处。其中一个选项是 lazy,配置它之后 Watcher 将会维护一个 dirty 标志,如果依赖的响应数据已经更改但这个 Watcher 还未运行时它将为 true,也就是说,此时缓存已过时。在我们的例子中,如果 currentUserId 被改成 3。任何依赖于它且被设置了 lazy 的 Watcher 都会被标记为 dirty,但 Watcher 并没有运行。currentUser 和 validCurrentUser 都是这个状态的 lazy Watcher。根渲染函数同样会依赖于这个状态,渲染将在下一个 tick 时被触发。当渲染函数执行时,将会访问已经被标记为 dirty 的 validCurrentUser,它将重新运行它的 getter 函数,进而访问同样需要更新的 currentUser。至此,这个组件将会被正确重渲染,并且相关缓存将被更新。等等,我似乎听见你在问,为什么所有 3 个 Watcher 都是依赖于这个状态的呢?难道他们不是相互依赖的么?计算属性 watcher 有一个特性就是不仅它自身的值是响应式的,而且当计算属性的 getter 被调用时,如果当前有 Wathcer 在读取这个计算属性的话(即 Dep.target 中有值–译者),所有这个计算属性的依赖也将会被这个 Wathcer 收集起来。这种依赖收集关系链的扁平化对性能表现更优,而且也是个比较简单的解决方案。这意味着一个组件将发生更新,即使它所依赖的计算属性在重新计算后的值并没有发生变化,这种更新显然没有什么意义。其中一些逻辑可以阅读一下 watcher 类源码的优雅实现,代码量 240 行左右。那么从 ob 中我们可以得到哪些关于计算属性响应式机制的信息呢我们可以看到有哪些 Watcher 订阅(subs)了响应式数据的更新。记住,响应式机制在下面这些情景下起作用:对象数组对象的属性最后一个情景很有可能被忽略,因为在开发者工具中是无法浏览它的 Dep 类实例(译者注:ob)。因为 Dep 类是在最初响应式化的时候就被实例化的,但是并没有在这个对象中的什么地方把它记录下来。稍后我们将回头讨论这个问题,因为我将用一个小技巧来间接拿到它。然而通过观察对象和数组的 Watcher 也可以让我们收获良多,下面是一个简单的 Watcher:将示例跑起来之后打开开发者工具,它应该在页面全部渲染完成之后暂停运行。你可以输入下面的表达式,就能看到跟上面这个图一样的情况了:this.$store.state.users[2].ob.dep.subs[5]这是一个组件的渲染 Watcher,也是一个对象引用。能看到 dirty 和 lazy 这两个我之前提到过的标志位。同时,我们还可以知道它不是一个用户创建的 Watcher(译者注:user 为 false)。 有时,试图找出这个 Watcher 是哪个组件的渲染 Watcher 是困难的,因为如果这个组件没有全局注册,或者这个组件没有设置 name 属性,那么基本可以说它是匿名的。然而如果你从另一个组件引用了这个匿名组件的时候,它的 $vnode.tag 属性通常包含它被引用时所用的名称。上面的这个 Watcher 来自于被其父组件定义为 Comp 的子组件。它与 upperCaseName 计算属性相关。计算属性通常有一个在 getter 函数上指明的有意义的名称,这是因为计算属性通常被定义为对象属性。Vuex 的 getter通常计算属性会给出他们的名称及其所属的组件,但是 Vuex 的 getter 却并不如此。currentUser 这个 Watcher 看起来长这样:唯一能证明它是 Vuex 中的 getter 的线索是:它的函数体定义在 vuex.min.js 中(译者注:[[FunctionLocation]])。所以我们应该怎样获取 getter 的名称呢?在开发者工具中你通常可以访问 [[Scopes]],你可以在 [[Scopes]] 中找到它的名称,然而这并不是通过编程的方式来获取的。下面是我的一个解决方法,在创建 Vuex 的 store 之后运行:const watchers = store._vm._computedWatchers;Object.keys(watchers).forEach(key => { watchers[key].watcherName = key;});第一行可能看起来有点奇怪,但其实 Vuex 的 store 中会维护一个 Vue 的实例,来帮助实现 getter 的功能,实际上,getter 就是一个伪装起来的计算属性!现在,当我们查看 subs 数组中的 Watcher 时,我们可以通过获取 watcherName 来获取 Vuex 的 getter 的名称。对象属性的 Dep 类实例上面我提到调试响应式数据时你是看不到对象属性的 Dep 类实例。在示例中,每个 user 对象都有一个 name 属性,每个属性都包含各自的 Watcher,这些 Watcher 将会在属性发生变更时收到更新通知。尽管 Dep 实例并不能直接访问到,但是可以被监听他们的 Watcher 访问到。Watcher 保留有一份它所依赖的所有依赖项的数组。我的小技巧是给属性增加一个 Watcher,然后拿到这个 Watcher 的依赖项但是这并不简单,我可以通过 Vue 的 $watch 接口来添加一个 Watcher,但是返回的并不是 Watcher 实例。因此我需要从 Vue 实例的内部属性中获取到 Watcher 实例。const tempVm = new Vue();tempVm.$watch(() => store.state.users[2].name, () => {});const tempWatch = tempVm._watchers[0];// now pull the subs from the depstempWatch.deps.forEach(dep => dep.subs .filter(s => s !== tempWatch) .forEach(s => subs.add(s)));想把这个功能包装成一个工具函数吗?我已经把这些小的代码片段封装到了一个任何人都可以获取到的工具库中:vue-pursue。可以看看使用示例。例子中的 () => this.$store.state.users[2].name 经过 vue-pursue 处理后返回:{ “computed”: [ “currentUser”, “validCurrentUser”, “Comp.upperCaseName” ], “components”: [ “Comp” ], “unrecognised”: 1}需要注意的是,根组件将会在操作后更新,但因为根组件没有名称,所以其显示为 unrecognised。currentUser 这个 Vuex 的 getter 将会更新,且这个更新并不来源于 name 的更新。通过传递一个箭头函数给 vue-pursue,这个箭头函数所具有的所有依赖将会被将会被订阅者考虑在内,这意味着 users 和 users[2] 对象也包括在内。或者,如果我们传递 (this.$store.state.users[2], ‘name’),输出将会是:{ “computed”: [ “validCurrentUser”, “Comp.upperCaseName” ], “components”: [ “Comp” ], “unrecognised”: 1}最后一点…我需要着重强调的是,要谨慎使用任何以下划线作为开头的属性,因为这不是公共 API 的一部分,它们可能会在没有任何警告的情况下被移除。上面介绍的这个功能,一开始就没打算使用于生产环境,也没打算使用在运行时环境,这只是一个方便调试的开发者工具。最终随着 Vue3.0 的出现,这将会被更全面、更简单易用、更可靠的替代。如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。 ...

March 28, 2019 · 2 min · jiezi

Node程序debug小记

有时候,所见并不是所得,有些包,你需要去翻他的源码才知道为什么会这样。背景今天调试一个程序,用到了一个很久之前的NPM包,名为formstream,用来将form表单数据转换为流的形式进行接口调用时的数据传递。 这是一个几年前的项目,所以使用的是Generator+co实现的异步流程。 其中有这样一个功能,从某处获取一些图片URL,并将URL以及一些其他的常规参数组装到一起,调用另外的一个服务,将数据发送过去。 大致是这样的代码:const co = require(‘co’)const moment = require(‘moment’)const urllib = require(‘urllib’)const Formstream = require(‘formstream’)function * main () { const imageUrlList = [ ‘img1’, ‘img2’, ‘img3’, ] // 实例化 form 表单对象 const form = new Formstream() // 常规参数 form.field(’timestamp’, moment().unix()) // 将图片 URL 拼接到 form 表单中 imageUrlList.forEach(imgUrl => { form.field(‘image’, imgUrl) }) const options = { method: ‘POST’, // 生成对应的 headers 参数 headers: form.headers(), // 告诉 urllib,我们通过流的方式进行传递数据,并指定流对象 stream: form } // 发送请求 const result = yield urllib.request(url, options) // 输出结果 console.log(result)}co(main)也算是一个比较清晰的逻辑,这样的代码也正常运行了一段时间。 如果没有什么意外,这段代码可能还会在这里安静的躺很多年。 但是,现实总是残酷的,因为一些不可抗拒因素,必须要去调整这个逻辑。 之前调用接口传递的是图片URL地址,现在要改为直接上传二进制数据。 所以需求很简单,就是将之前的URL下载,拿到buffer,然后将buffer传到formstream实例中即可。 大致是这样的操作:- imageUrlList.forEach(imgUrl => {- form.field(‘image’, imgUrl)- })+ let imageUrlResults = yield Promise.all(imageUrlList.map(imgUrl => + urllib.request(url)+ ))+ + imageUrlResults = imageUrlResults.filter(img => img && img.status === 200).map(img => img.data)++ imageUrlResults.forEach(imgBuffer => {+ form.buffer(‘image’, imgBuffer)+ })下载图片 -> 过滤空数据 -> 拼接到form中去,代码看起来毫无问题。 不过在执行的时候,却出现了一个令人头大的问题。 最终调用yield urllib.request(url, options)的时候,提示接口超时了,起初还以为是网络问题,于是多执行了几次,发现还是这样,开始意识到,应该是刚才的代码改动引发的bug。开始 debug定位引发 bug 的代码我习惯的调试方式,是先用最原始的方式,眼,看有哪些代码修改。 因为代码都有版本控制,所以大多数编辑器都可以很直观的看到有什么代码修改,即使编辑器中无法看到,也可以在命令行中通过git diff来查看修改。 这次的改动就是新增的一个批量下载逻辑,以及URL改为Buffer。 先用最简单粗暴的方式来确认是这些代码影响的,注释掉新增的代码,还原老代码。 结果果然是可以正常执行了,那么我们就可以断定bug就是由这些代码所导致的。逐步还原错误代码上边那个方式只是一个rollback,帮助确定了大致的范围。 接下来就是要缩小错误代码的范围。 一般代码改动大的时候,会有多个函数的声明,那么就按照顺序逐个解开注释,来查看运行的效果。 这次因为是比较小的逻辑调整,所以直接在一个函数中实现。 那么很简单的,在保证程序正常运行的前提下,我们就按照代码语句一行行的释放。 很幸运,在第一行代码的注释被打开后就复现了bug,也就是那一行yield Promsie.all(XXX)。 但是这个语句实际上也可以继续进行拆分,为了排除是urllib的问题,我将该行代码换为一个最基础的Promise对象:yield Promise.resolve(1)。 结果令我很吃惊,这么一个简单的Promise执行也会导致下边的请求超时。 当前的部分代码状态:const form = new Formstream()form.field(’timestamp’, moment().unix())yield Promise.resolve(1)const options = { method: ‘POST’, headers: form.headers(), stream: form}// 超时const result = yield urllib.request(url, options)再缩小了范围以后,进一步进行排查。 目前所剩下的代码已经不错了,唯一可能会导致请求超时的情况,可能就是发请求时的那些options参数了。 所以将options中的headers和stream都注释掉,再次执行程序后,果然可以正常访问接口(虽说会提示出错,因为必选的参数没有传递)。 那么目前我们可以得到一个结论:formstream实例+Promise调用会导致这个问题。冷静、忏悔接下来要做的就是深呼吸,冷静,让心率恢复平稳再进行下一步的工作。 在我得到上边的结论之后,第一时间是崩溃的,因为导致这个bug的环境还是有些复杂的,涉及到了三个第三方包,co、formstream和urllib。 而直观的去看代码,自己写的逻辑其实是很少的,所以难免会在心中开始抱怨,觉得是第三方包在搞我。 但这时候要切记「程序员修炼之道」中的一句话:“Select” Isn’t Broken “Select” 没有问题所以一定要在内心告诉自己:“你所用的包都是经过了N久时间的洗礼,一定是一个很稳健的包,这个bug一定是你的问题”。分析问题当我们达成这个共识以后,就要开始进行问题的分析了。 首先你要了解你所使用的这几个包的作用是什么,如果能知道他们是怎么实现的那就更好了。 对于co,就是一个利用yield语法特性将Promise转换为更直观的写法罢了,没有什么额外的逻辑。 而urllib也会在每次调用request时创建一个新的client(刚开始有想过会不会是因为多次调用urllib导致的,不过用简单的Promise.resolve代替之后,这个念头也打消了) 那么矛头就指向了formstream,现在要进一步的了解它,不过通过官方文档进行查阅,并不能得到太多的有效信息。源码阅读源码地址所以为了解决问题,我们需要去阅读它的源码,从你在代码中调用的那些 API 入手:构造函数fieldheaders构造函数营养并不多,就是一些简单的属性定义,并且看到了它继承自Stream,这也是为什么能够在urllib的options中直接填写它的原因,因为是一个Stream的子类。util.inherits(FormStream, Stream);然后就要看field函数的实现了。FormStream.prototype.field = function (name, value) { if (!Buffer.isBuffer(value)) { // field(String, Number) // https://github.com/qiniu/nodejs-sdk/issues/123 if (typeof value === ’number’) { value = String(value); } value = new Buffer(value); } return this.buffer(name, value);};从代码的实现看,field也只是一个Buffer的封装处理,最终还是调用了.buffer函数。 那么我们就顺藤摸瓜,继续查看buffer函数的实现。FormStream.prototype.buffer = function (name, buffer, filename, mimeType) { if (filename && !mimeType) { mimeType = mime.lookup(filename); } var disposition = { name: name }; if (filename) { disposition.filename = filename; } var leading = this._leading(disposition, mimeType); this._buffers.push([leading, buffer]); // plus buffer length to total content-length this._contentLength += leading.length; this._contentLength += buffer.length; this._contentLength += NEW_LINE_BUFFER.length; process.nextTick(this.resume.bind(this)); return this;};代码不算少,不过大多都不是这次需要关心的,大致的逻辑就是将Buffer拼接到数组中去暂存,在最后结尾的地方,发现了这样的一句代码:process.nextTick(this.resume.bind(this))。 顿时眼前一亮,重点的是那个process.nextTick,大家应该都知道,这个是在Node中实现微任务的其中一个方式,而另一种实现微任务的方式,就是用Promise。修改代码验证猜想拿到这样的结果以后,我觉得仿佛找到了突破口,于是尝试性的将前边的代码改为这样:const form = new Formstream()form.field(’timestamp’, moment().unix())yield Promise.resolve(1)const options = { method: ‘POST’, headers: form.headers(), stream: form}process.nextTick(() => { urllib.request(url, options)})发现,果然超时了。 从这里就能大致推断出问题的原因了。 因为看代码可以很清晰的看出,field函数在调用后,会注册一个微任务,而我们使用的yield或者process.nextTick也会注册一个微任务,但是field的先注册,所以它的一定会先执行。 那么很显而易见,问题就出现在这个resume函数中,因为resume的执行早于urllib.request,所以导致其超时。 这时候也可以同步的想一下造成request超时的情况会是什么。 只有一种可能性是比较高的,因为我们使用的是stream,而这个流的读取是需要事件来触发的,stream.on(‘data’)、stream.on(’end’),那么超时很有可能是因为程序没有正确接收到stream的事件导致的。 当然了,「程序员修炼之道」还讲过:Don’t Assume it - Prove It 不要假定,要证明所以为了证实猜测,需要继续阅读formstream的源码,查看resume函数究竟做了什么。 resume函数是一个很简单的一次性函数,在第一次被触发时调用drain函数。FormStream.prototype.resume = function () { this.paused = false; if (!this._draining) { this._draining = true; this.drain(); } return this;};那么继续查看drain函数做的是什么事情。 因为上述使用的是field,而非stream,所以在获取item的时候,肯定为空,那么这就意味着会继续调用_emitEnd函数。 而_emitEnd函数只有简单的两行代码emit(‘data’)和emit(’end’)。FormStream.prototype.drain = function () { console.log(‘start drain’) this._emitBuffers(); var item = this._streams.shift(); if (item) { this._emitStream(item); } else { this._emitEnd(); } return this;};FormStream.prototype._emitEnd = function () { this.emit(‘data’, this._endData); this.emit(’end’);};看到这两行代码,终于可以证实了我们的猜想,因为stream是一个流,接收流的数据需要通过事件传递,而emit就是触发事件所使用的函数。 这也就意味着,resume函数的执行,就代表着stream发送数据的动作,在发送完毕数据后,会执行end,也就是关闭流的操作。得出结论到了这里,终于可以得出完整的结论:formstream在调用field之类的函数后会注册一个微任务 微任务执行时会使用流开始发送数据,数据发送完毕后关闭流 因为在调用urllib之前还注册了一个微任务,导致urllib.request实际上是在这个微任务内部执行的 也就是说在request执行的时候,流已经关闭了,一直拿不到数据,所以就抛出异常,提示接口超时。 那么根据以上的结论,现在就知道该如何修改对应的代码。 在调用field方法之前进行下载图片资源,保证formstream.field与urllib.request之间的代码都是同步的。let imageUrlResults = yield Promise.all(imageUrlList.map(imgUrl => urllib.request(url)))const form = new Formstream()form.field(’timestamp’, moment().unix())imageUrlResults = imageUrlResults.filter(img => img && img.status === 200).map(img => img.data)imageUrlResults.forEach(imgBuffer => { form.buffer(‘image’, imgBuffer)})const options = { method: ‘POST’, headers: form.headers(), stream: form}yield urllib.request(url, options)小结这并不是一个有各种高大上名字、方法论的一个调试方式。 不过我个人觉得,它是一个非常有效的方式,而且是一个收获会非常大的调试方式。 因为在调试的过程中,你会去认真的了解你所使用的工具究竟是如何实现的,他们是否真的就像文档中所描述的那样运行。 关于上边这点,顺便吐槽一下这个包:thenify-all。 是一个不错的包,用来将普通的Error-first-callback函数转换为thenalbe函数,但是在涉及到callback会接收多个返回值的时候,该包会将所有的返回值拼接为一个数组并放入resolve中。 实际上这是很令人困惑的一点,因为根据callback返回参数的数量来区别编写代码。 而且thenable约定的规则就是返回callback中的除了error以外的第一个参数。 但是这个在文档中并没有体现,而是简单的使用readFile来举例,很容易对使用者产生误导。 一个最近的例子,就是我使用util.promisify来替换掉thenify-all的时候,发现之前的mysql.query调用莫名其妙的报错了。// 之前的写法const [res] = await mysqlClient.query(SELECT XXX)// 现在的写法const res = await mysqlClient.query(SELECT XXX)这是因为在mysql文档中明确定义了,SELECT语句之类的会传递两个参数,第一个是查询的结果集,而第二个是字段的描述信息。 所以thenify-all就将两个参数拼接为了数组进行resolve,而在切换到了官方的实现后,就造成了使用数组解构拿到的只是结果集中的第一条数据。 最后,再简单的总结一下套路,希望能够帮到其他人:屏蔽异常代码,确定稳定复现(还原修改)逐步释放,缩小范围(一行行的删除注释)确定问题,利用基础demo来屏蔽噪音(类似前边的yield Promise.resolve(1)操作)分析原因,看文档,啃源码(了解这些代码为什么会出错)通过简单的实验来验证猜想(这时候你就能知道怎样才能避免类似的错误) ...

January 17, 2019 · 2 min · jiezi