1.自我介绍,可以简单介绍你毕业什么学校,什么专业
2.出来负责的项目,每个项目自己充当的角色
3.每个项目使用前端技术+后端技术
简单大概的说出来就可以了,不要详细到你哪年进入公司,哪年离开公司,负责项目做什么的,说一大通。这个对我们面试官来说,想让你自我简单介绍,其实考察点有两方面:
A.面试者的表达能力和概括能力
B.面试者目前掌握什么技术,做过什么项目,在项目中的角色来初步判定这个人的能力
在本人问他们做过项目中,自己感觉挑战最大的项目是什么,你在这个项目中做了什么,遇到什么问题,怎么解决这些问题的
1.有些面试者给我的答案直接说没有比较大的挑战;这个让我感觉这个人可能没有听懂我的问题,工作那么多年,难道没有一个项目可以拿来说的,那我拿什么来判定你的能力呢?难道凭你几句话,说我很牛逼的,没有什么困难难得到我?我就信任你了,伯乐寻找千里马都要知道几个千里马的特征吧。所以没有你也要在面试前准备好自己做过的项目和总结,在项目中自己做了什么,充当什么觉得。
2.有些面试者回答这个问题,在介绍项目的时候,很大概的说,还有就是一句话前端都是我做的,这些都是我设计的;这个让我感觉这个人也没有听懂我的意思,我在问你这个问题的时候,肯定是希望你详细介绍你的项目,这个项目使用者是谁,让我知道谁在使用这个项目;你负责哪些模块,哪些组件,那些模块实现什么业务逻辑,用到什么技术,这个能让我更加知道你项目的业务,才能从你描述中我知道这类的业务会遇到什么问题,以及你使用的技术是否合理,能让我更加判定你的能力,才能更好的提出问题,才能更好的面试下去。
3.回答问题的时候不要想到什么就答什么,要有陈述性,比如1,xxx;2.yyyyy;3.wwww的陈述,这样能让我知道你总结过,表达和陈述上比较清晰,思维好;想到什么答什么的,让我感觉思维可能比较乱,有可能我听懂了你的表述,但是让我感觉表达能力有些欠缺。
4.以及在你项目中遇到的问题,你要陈述问题,你是怎么思考的,而不是针对问题就直接说百度,google就解决了。我们大家都知道,遇到问题都会去百度,google.但在这问题的前提条件是问题是,架构上的问题,还是代码的bug问题,还是方案上的问题。你至少陈述清楚,是不是还有其他方案,在其他方案中,你为什么选择这个方案。这个能让知道你在面对问题的时候,你是否思考了其他的问题,想得越多,知道能体现出的思维比较发散,遇到问题可以有其他方式解决,而不是死磕一棵树上。
前端技能在问到很多面试者的时候感觉自己都懂前端,其实他们只是懂使用js敲代码而已,很多前端知识,以及前端原理都不懂,只会使用的话,那么永远只会走来人家的后面。就那一个比喻来说吧,如果你只是一个会开车的司机,不会修车或者造车,那么如果哪天车出了毛病,你都不知道,到时候才去学习车的构造原理。或者你会说我直接给维修厂不就可以了,如果维修厂关门了呢怎么办。所以我们前端开发人员还是要脚踏实地,不要说我会使用vuejs,react前端框架,问你一个mvvm模式是什么,你都说不知道,怎么实现mvvm框架,在不使用别人开发mvvm框架,自己可以开发一个简单的mvvm框架?前端的开发者问问自己?以我个人的要求,前端开发者必须掌握
1.HTTP协议
2.前端安全
3.常用前端框架的三驾马车 react ,vuejs ,angularjs 目前比较流行的 以及 jquery(工具库)的使用
4.前端基础知识,跨域,es6新语法
5.Nodejs的开发,express,koa等常用框架
6.知道一些数据库知识
7.能封装业务组件和公用组件
8.在技术选型上,能给出你选择的方案是最优的数据说明
9.前端性能优化
10.前后端分离
更加深层次的
1.前端架构,设计模式
2.前端工程化开发,测试,打包,发布
3.自己实现前端架构代码以及开发工具
我眼中的前段
入坑前端到今天也将近两年半了,这两天突然想到了第一次面试时面试官的一个问题-------你怎样理解前端的工作?
对于当时我一个小白而言完全是胡说一通,词不达意,搞得面试官一脸懵逼,现在想想那可能就叫尬聊吧……时隔两年在不断爬坑中对这个问题有了自己新的认识,今天趁着上午没什么事情,写下这篇博客,想到哪写到哪,谈一谈我所理解的前端。
一个前端初学者必须所掌握的核心技能HTML,CSS,JavaScript,这三项是前端最底层的技术支持了,如果你看几年前的回答应该还会有一项jquery,但我个人觉得现阶段的前端圈jquery可以不作为必备技能,虽然Jquery对新人很友好,但现在mvvm框架满天飞Vue, Angular,React三分天下,用起来要比直接操作dom的jquery舒服很多,当然在这个阶段是打基础的阶段框架,类库什么的可以往后靠。原生Js永远都是重中之重,只会用框架不懂底层原理永远达不到精通,推荐红宝书Javascript高级程序设计,吃透红宝书打牢基础再去学习其他框架,妈妈就再也不用担心你的学习。接下来还有一项额外的技能PhotoShop,要知道ps可以不用去做,但必须要会,而且在一些小公司里UI只会丢给你一个PSD,没有什么Sketch之类的东西,也没人帮你切图,这些都需要你自己来处理,所以ps是额外的必备技能。
进入告诉成长阶段,开始打怪升级,这个阶段的时间持续最长,在这期间你需要爬无数的坑,积累各种失败的经验,一关一关的往下刷,关于HTML和CSS你需要知道各种UI框架的使用,如BootStrap,ElementUI……,关于不同图片的格式标准,浏览器的兼容性,移动和pc端的区别,响应式布局,flex布局,栅格布局,对设计审美的提升…等关于提高你页面开发效率的各种技能,UI框架这一块比较杂选自己感兴趣的看看就好。
Js方面这时候已经可以开始挑一种主流框架进行学习了,前面提到的Vue, Angular,React都是不错的选择, 并且对面向对象编程,对象封装,原型继承,闭包,同步异步差异,等一系列的js进阶知识应该进行深入了解,同时对es6标准也需要了解,可以参考阮一峰老师的es6入门,书中包含了es6的各种新特性,默认参数,模版表达式,多行字符串,拆包表达式,改进的对象表达式,箭头函数 =&>,Promise,块级作用域的let和const,class类,模块化等常用特性.可以做到自己封装组件,编写维护性高,可读性强的代码. 而且在平时需要多看别人写的代码,汲取别人的优点,并且阅读大量的技术文献,最重要的是要总结自己的问题,比如说你遇到一个bug,迷迷糊糊的就解决了,下一次你又遇到相同的问题,这个时候有没有对之前问题进行总结的效果就看出来了.
了解各种设计模式,看得懂各种框架源码,前后端通吃,可以自己手写js框架...好吧,我还没到这个阶段就不写了..............
一个完整的的工作流程应该是:
立项--项目研讨--需求确认----产品出原型----后台开发同时设计师拿到原型进行UI设计--前端开始开发--测试提bug--改bug--重复n次--产品验收
上面只是一套笼统的流程,至少在前端这方面我们需要做的有梳理业务逻辑并理解业务逻辑,这对你后面的开发很有用处,同时根据需求进行应用技术的选择,项目结构的划分,需求模块的划分,完整项目的搭建,当然现在有很多可以自动化构建工具可以节省你很多时间, 现在的前端开发已经不再仅仅只是静态网页的开发了,日新月异的前端技术已经让前端代码的逻辑和交互效果越来越复杂,更加的不易于管理,模块化开发和预处理框架把项目分成若干个小模块,增加了最后发布的困难,没有一个统一的标准,让前端的项目结构千奇百怪。前端自动化构建在整个项目开发中越来越重要,但新手入门还是应该去尝试自己一点一点的去构建一个项目,等你多做几个项目觉得每次都这样重复好烦,自然而然的就入了自动化构建的坑,毕竟这样能让你更深刻的理解,为什么要使用自动化构建……比如我们主栈是vue,我们最常用的就是vue-cli,自动化工具有很多选择如Bower、Gulp、Grunt、node、yeoman,我们应该根据需求选择最适合自己的去研究。
前端是团队里最应该学会沟通的人,界面有问题需要和UI沟通,数据有问题需要和后台沟通,功能有问题需要和产品沟通,测试的时候给你提bug你还需要和测试沟通……emmm心累
前端是最接近用户的人,用户对一个网站,软件最直观的感受是反映到前端的,可能你会说最直观的不应该是UI设计师么,你要知道我是前端我为设计师代言!!!
和UI的沟通,在工作中我们不应该是被动的实现UI的设计,而是应该合理化的提出自己的想法,不然日后返工浪费的是双方的时间,比如最开始刚来公司的时候,项目里对一些小图标的图片还在使用雪碧图,但很明显随着浏览器的支持越来越好,svg和字体图标慢慢占据主流,我在阿里巴巴图标库建了一个项目把UI也拉了进来,UI把他用到的图标直接添加进项目,前端直接从项目生成字体图标引入到项目,绝逼要比自己慢慢切图,扣图标,合并雪碧图要省事的多,而且用起来也特别爽,想改颜色就改颜色。再比如你需要做一个图表,用到了echarts,你完全可以让UI基于echarts去设计样式,而不是让他在那里自由发挥,因为你永远不知道设计师的脑子里装了多少创意,这样节省的是两个人的时间,不会出现他做好样式而你实现不了的尴尬。
一般来说程序员和产品经理之间是最难沟通的,只有相杀没有相爱,毕竟子曾经曰过:’这个需求很简单,怎么实现我不管,明天上线!’,
下面引用lensuntop的一篇文章,我觉得写的非常好
记得有一个段子:
产品汪:程序猿,我们来实现一个紧急需求?
程序猿:请说。
产品汪:请根据手机壳的颜色,来实现APP启动的颜色。
程序猿已经在风中凌乱。。。
从这个段子中多少能折射出产品和技术之间的各种激情“火花”。产品经理眼中简单的需求,而在我们看来是不可能实现的。而程序员也无法理解产品经理为什么要实现这样的需求。那么,站在一个程序员的角度应该怎么样和产品经理沟通呢?
我们程序员一定会在问,产品经理为什么想要根据手机壳的颜色来动态实现APP启动时的颜色。既然想听解析,那就先别急着说出自己的结论——技术上无法实现!既然有疑问,那就先将自己的疑问解决。
产品有产品的角度。作为程序员我们追求的是什么?逻辑正确,更快,更容易扩展。产品追求的是什么?说实话,我自己没有深刻去思考过这个问题。站在一个惯性的角度思考可以想到:一个产品为什么存在,他的存在能解决什么问题,他的用户体验好不好。这些才是决定一个产品的核心价值。毕竟工作性质影响了一个人的思维逻辑,所以这时候,我们能站在一个产品的角度去思考每一个需求,便显得尤其重要。
作为程序员想必对这句话都是深深认同的。因为一个标点符号或者类型的错误,会导致一个自己意想不到的bug。产品经理在设计一个产品的时候,都是从大方向去想问题的,大方向没有错就行了,细节脱离不了大方向。这是他们想的。但是对于程序来说,却万万不能。因为一个细节的逻辑往往决定了整个大方向。举个例子:有一个需求,用户的作品需要提交审核,经过审核才可以让所有人看到。当产品经理交这个需求给你的时候,你能察觉到什么问题了吗?这里面有几个细节:1.用户提交审核后,用户可以不可以再编辑作品;2.作品是否会多次审核;3.需不需要记录审核历史;4.用户作品是否需要有版本的控制,如要产生版本,版本又是如何产生的;5.审核通过后,用户可以不可以再修改作品,若不可以,那么是不是其他人就看不见用户作品......话说回来这只是一个简单的逻辑需求!但是涉及的细节却是太多太多。我们往往在编码的时候写不下去,就是因为给的需求太模糊,没有细化到点上。
不能实现,这句话想必我们都是经常说。但是直接对产品经理说,没准会让产品经理抓狂。因为我们会让他们觉得他们提出的任何需求,我们都不能实现。但是事实并非如此,因为不能实现是有条件的,比如时间不够。所以我们要先认同产品经理的观点(“能实现”),再提出自己实现他的需求的条件是什么。因为现实产品经理也不会经常犯傻,经常提出一些不合理的需求,但是面对需求,我们需要评估实现的时间,而且这个时间不是那么容易评估准确的。
就拿段子里面的需求来说,让我们提供几种APP皮肤给用户进行选择,肯定比原先的需求容易实现,而且也更加符合人性化。说另外一个故事,有家智能家居的公司,要实现厨房水龙头,根据人声说水温几度,就可以达到几度。换个角度想,你会感觉出40度和45度水的温差吗?而且根据人声判断,这又涉及到声音识别系统,你要兼容多少种语言?其实我就觉得左右切换就挺智能的,完全没有必要搞的那么复杂。所以程序员要找到一种更好更容易实现的方法。别给产品经理的想当然自乱阵脚。
在开发的时候,我们往往会另外与产品经理进行细节化的讨论。但是这种讨论结果,我们并没有记录到产品原型里面或者需求列表里面。但是过了几个月后,我们自己往往会忘记我们当初为什么会讨论出这样或者那样的一个细节。所以一切的需求必须是根据的。从另一方面来说,也保障了双方的利益,别等到出问题的时候,不知道是谁的责任,而在这一方面,程序员往往很吃亏。
有人说过,当需求影响到代码扩展性的时候,会首先砍需求,而不是改代码!在一定程度上,我是认同这句话的。在我看来,程序是一件思想上的作品,要达到艺术的境界,从功能、体验和逻辑上都必须是合情合理的。就像一件艺术品一样,看起来是浑然天成的!因为一件看起来很“丑陋”作品,一定是不符合人的逻辑和习惯的。
写到最后,感觉绕回到程序员自身了。其实跟产品经理沟通,最重要的是要明白到:我们是在解决问题,而不是在制造问题!主要抱着这个核心,一切问题迎刃而解
一般来说和后台沟通没那么多的麻烦,约定好规则后,一般来说你们是通过api来沟通的,但当你调试接口时,出现一些未知的,你感觉不是自己问题的时候,及时的沟通后台是最明智的。
相信大家在这一点上都深有感触,因为前端是最后一关,所有的需求都是在前端手里变成一个具体的产品的,这样也就导致你很容易变成背锅侠,导致项目延期的情况有很多种,设计图不及时,后台数据出现问题,产品临时改需求,如果你不能证明是这些问题导致项目延期,这个锅你必背无疑,唯一的方法就是--à口头确认--à发email到责任人确认--à通知上级,千万不要觉得这个麻烦,出问题的时候会比这个更麻烦的,
写不动了,以上就是个人爬坑后对前端的一些理解(ps:虽然我还在坑里),也算对自己工作的一个总结吧,写的比较絮叨,不喜勿喷,祝大家2018升职加薪,找到女朋友!!!
起初,什么都没有。 造物主说:没有东西本身也是一种东西啊,于是就有了null:
现在我们要造点儿东西出来。但是没有原料怎么办? 有一个声音说:不是有null嘛? 另一个声音说:可是null代表无啊。 造物主说:那就无中生有吧! 于是:
JavaScript中的1号对象产生了,不妨把它叫做No. 1。 这个No. 1对象可不得了,它是真正的万物始祖。它拥有的性质,是所有的对象都有的。 __proto__是什么呢?是“生”的意思,或者叫做继承。
既然已经有了一个对象,剩下就好办了,因为一生二,二生三,三生万物嘛。 不过造物主很懒,他不想一个一个地亲手制造对象。于是他做了一台能够制造对象的机器:
他给这台机器起了一个名字:Object。 这台机器并不能凭空造出对象,它需要一个模板对象,按照这个模板对象来制造对象。很自然的,它把目前仅有的No. 1对象作为模板。图中的prototype就代表机器的模板对象。
机器如何启动呢?通过new命令。你对着机器喊一声:“new!”,对象就造出来了。
机器的产生,实现了对象的批量化自动化生产,解放了造物主的双手。于是造物主忙别的去了。 如果机器只是按照模板的样子,机械地复制出一模一样的对象,那就太笨了。 人类的后代在继承了父辈的性状的基础上,可以产生父辈没有的性状。同样地,机器在制造对象时,除了继承模板对象的属性外,还可以添加新的属性。这使得JavaScript世界越来越多样化。
比如说,有一天Object机器制造一个对象,它有一个特殊的属性,叫做flag,属性值是10。用图形表示是这样的:
写成代码就是:
var obj = new Object({ flag: 10 });
轰轰烈烈的造物运动开始了……
一天天过去了,造物主来视察工作。看到Object制造出了好多好多对象,他非常高兴。
同时他还发现:根据“物以类聚”的原则,这些对象可以分成很多类。聪明的造物主想,我何不多造几台机器,让每一台机器专门负责制造某一类对象呢?于是,他动手造出了几台机器并给它们起了名字。它们分别是:
String:用来制造表示一段文本的对象。 Number:用来制造表示一个数字的对象。 Boolean:用来制造表示是与非的对象。 Array:用来制造有序队列对象。 Date:用来制造表示一个日期的对象。 Error:用来制造表示一个错误的对象。 ……
多台机器齐开动,各司其责,造物运动进入了一个新的阶段…… 造物主又开始思考了:虽然机器是用来制造对象的,但是机器本身实际上也是一种特殊对象啊。现在有了这么多机器,我得好好总结一下它们的共同特征,把它们也纳入对象体系。
于是,造物主基于No. 1对象,造出了一个No. 2对象,用它来表示所有机器的共同特征。换句话说,把它作为所有机器的原型对象。
(注:__proto__写起来太麻烦了,后面我们用[p]来代替)
当然了,和Object一样,这些机器也需要各自有一个模板对象,也就是它们的prototype属性指向的那个对象。显然它们的模板对象应该是继承自No. 1对象的,即
这张图显示了JavaScript世界中那些最基本的机器本身的原型链,以及它们的模板对象的原型链。不过看起来太复杂了,所以后面我们就不再把它们完整地画出来了。
造物主高兴地想:这下可好了,我造出了Object机器,实现了对象制造的自动化。然后又造出了String、Number等机器,实现了特定类别的对象制造的自动化。但是,为啥总感觉似乎还缺点什么呢?
对啦,还缺少一台制造机器的机器啊!
很快,万能的造物主就把它造了出来,并把它命名为Function。有了Function机器后,就可以实现自动化地制造机器了。 让我们来观察一下Function: 首先,Function是一台机器,所以它的原型对象也是No. 2对象。 其次,Function又是一台制造机器的机器,所以它的模板对象也是No. 2对象。 所以我们得到了Function的一个非常特别的性质:
Function.__proto__ === Function.prototype
哇,太奇妙了!
不要奇怪,这个性质不过是”Function是一台制造机器的机器“这个事实的必然结果。
于是JavaScript的世界的变成了下面的样子:
从这张图中,我们发现:所有的函数(包括Function)的原型都是No. 2对象,而同时Function.prototype也是No. 2对象。这说明了:
从逻辑上,我们可以认为所有机器(包括Function自己)都是由Function制造出来的。
同时,如果再仔细瞧瞧,你会发现:
Object作为一个机器可以看做是有由Function制造出来的,而Function作为一个对象可以看做是由Object制造出来的。
这就是JavaScript世界的“鸡生蛋,蛋生鸡”问题。那么到底是谁生了谁呢?Whatever!
就像前面所说,机器用来制造某一类对象。正因如此,机器可以作为这类对象的标志,即面向对象语言中类(class)的概念。所以机器又被称为构造函数。在ES6引入class关键字之前,我们常常把构造函数叫做类。
然而,除了作为构造函数来制造对象外,函数通常还有另一个功能:做一件事情。正是有了这个功能,JavaScript的世界才由静变动,变得生机勃勃。
比如说,我们现在用Function机器制造了鸟类(即用来造鸟的机器):
function Bird(color) { this.color = color; }
然后,对着造鸟机说:“new!”,于是造鸟机发动起来,制造一个红色的鸟:
var redBird = new Bird('#FF0000');
如果现在我们想让鸟飞起来,该怎么办呢?我们需要再次用Function制造出一台机器,不过这台机器不是用来制造对象的,而是用来做事儿的,即“让鸟飞起来”这件事情
// 这是一台通过晃动鸟的翅膀,让鸟飞起来的简陋的机器。 function makeBirdFly(bird) { shakeBirdWing(bird); }
我们知道,让一台制造对象的机器发动,只需要对它喊“new”即可;那么怎样让一台做事情的机器发动呢?更简单,对它咳嗽一声就行了。咳咳咳,
makeBirdFly(redBird);
于是红鸟飞了起来,世界充满了生机。
从上面的Bird和makeBirdFly的定义可以看出:实际上,制造对象的机器和做事情的机器没什么明显区别,不同的只是它们的使用方式。在两种情况下,它们分别被叫做构造函数和普通函数。
说明1:function xxx语法可以看成new Function的等价形式。 说明2:用户自定义的函数通常既可以作为普通函数使用,又可以作为构造函数来制造对象。ES6新增的class语法定义的函数只能作为构造函数,ES6新增的=>语法定义的箭头函数只能作为普通函数。
造物主对目前的世界还是不太满意,因为几乎所有的机器的模板对象都是No. 2,这使得JavaScript世界看起来有点扁。
于是造物主再次研究世界万物的分类问题。他发现有些对象会动、还会吃东西,于是把它们叫做动物,然后造了一台Animal机器来制造它们。他进一步发现,即使都是动物,也还是可以进一步分类,比如有些会飞、有些会游,他分别把它们叫做鸟类、鱼类。于是他想,我何不单独造几台机器,专门用来制造某一类动物呢。于是它造出了Bird、Fish等机器。
接下来,在选择这些机器的模板对象时碰到一个问题:如果还像之前那样直接复制一个No. 1对象作为Bird、Fish的模板,那么结果就是这样的:
这样可不好。首先没体现出鸟类、鱼类跟动物的关系,其次它们的模板对象存了重复的东西,这可是一种浪费啊。怎么办呢?简单,让Bird和Fish的模板对象继承自Animal的模板对象就好了。就是说
Bird.prototype.__proto__ === Animal.prototype Fish.prototype.__proto__ === Animal.prototype
于是:
用同样的方法,造物主造出了一个立体得多的JavaScript世界。
然而这样还不够。虽然那些纯对象现在充满了层次感,但是那些机器对象之间的关系还是扁平的:
那又该怎么办呢?其实用类似的办法就行了:
为了更方便地做到这一点,造物主发明了class关键字。
经过一番折腾,JavaScript世界发生了大变化。变得丰富多彩,同时变得很复杂。用一张图再也没法画出它的全貌,只能画出冰山一角:
JavaScript的世界还在不断进化中……
如果您正在测试前端应用程序,则应该了解前端测试金字塔。
在本文中,我们将看到前端测试金字塔是什么,以及如何使用它来创建全面的测试套件。
前端测试金字塔
前端测试金字塔是一个前端测试套件应该如何构建的结构化表示。
理想的测试套件由单元测试,一些快照测试和一些端到端(e2e)测试组成。
这是测试金字塔的改进版本,特定于测试前端应用程序。
在这篇文章中,我们将看到每个测试类型的样子。 为此,我们将为示例应用程序创建一个测试套件。
应用
要详细了解前端测试金字塔,我们来看看如何测试一个 Web 应用。
该应用是一个简单的 modal 应用。 点击一个按钮打开一个 modal ,点击 modal 上的 OK 按钮关闭 modal。
我们将从基于组件的框架构建应用。 别担心细节,我们会保持这个(详细)的级别。
该应用由三个组件组成 – 一个 Button 组件,一个 Modal 组件和一个 App 组件。
我们要写的第一个测试是单元测试。 在前端测试金字塔中,大部分测试都是单元测试。
单元测试测试的是代码库的单元。
它们直接调用函数或单元,并确保返回正确的结果。
在我们的应用中,我们的组件是单元。所以我们将为 Button 和 Modal 编写单元测试。没有必要为我们的应用组件编写测试,因为它没有任何逻辑。
单元测试会浅渲染组件,并断言当我们与它们交互时,它们的行为是正确的。
浅渲染意味着我们渲染组件一层深度。这样我们可以确保只测试组件,单元,而不是几个级别的子组件。
在我们的测试中,我们将触发组件上的操作,并检查组件的行为是否与预期一致。
我们不用盯着代码。但是我们的组件规格会如下所示:
我们的测试将浅渲染组件,然后检查每一项规格的工作。
单元测试应该占据我们的测试套件的绝大部分有以下几个原因:
几百个单元测试套件能在几秒钟内运行。
这使得单元测试对开发很有用。 当重构代码时,我们可以更改代码,并在没有中断组件的情况下运行单元测试来检查更改。 我们会在几秒钟之内知道我们是否破坏了代码,因为其中一个测试会失败。
换句话说,他们是非常具体的。
如果一个单元测试失败了,那么这个测试会告诉我们它是如何以及为什么失败的。
单元测试能很好地检查我们的应用程序工作的细节。 它们是开发时最好的工具,特别是如果你遵循测试驱动的开发。
但是它们无法测试一切。
为了确保我们呈现正确的样式,我们还需要使用快照测试。
快照测试是测试你的渲染组件的图片,并将其与组件的以前的图片进行比较。
用 JavaScript 编写快照测试的最好方法是使用 Jest 。
Jest 不是拍摄渲染组件的图片,而是渲染组件标记的快照。 这使得 Jest 快照测试比传统快照测试快得多。
要在 Jest 中注册快照测试,需要添加如下代码:
const renderedMarkup = renderToString(ModalComponent) expect(renderedMarkup).toMatchSnapshot()
一旦你注册一个快照,Jest 将顾及其它的一切。 每次运行单元测试时,都会重新生成一个快照,并将其与之前的快照进行比较。
如果代码改变,Jest 会抛出一个错误,并警告标记已经改变。 然后开发者可以手动检查没有类被误删的情况。
在下面的测试中,有人从<footer>中删除了 modal-card-foot 类。
快照测试是一种检查组件样式或标记的方法。
如果快照测试通过,我们知道代码更改不会影响组件的显示。
如果测试失败,那么我们知道确实影响了组件的渲染,并可以手动检查样式是否正确。
每个组件至少应有一次快照测试。 一个典型的快照测试呈现组件的状态,以检查它正确呈现。
现在我们已经有了单元测试和快照测试,是时候看看端到端(e2e)测试。
端到端(e2e)测试是高层测试。
它们执行与我们手动测试应用程序时相同的操作。
在我们的应用程序中,我们有一个用户(操作)旅程。当用户点击按钮时,模式将打开,当他们点击模式中的按钮时,模式将关闭。
我们可以编写一个贯穿这一旅程的端到端测试。测试将打开浏览器,导航到网页,并通过每个操作来确保应用程序正常运行。
这些测试将告诉我们,我们的单元正确地协同工作。它使我们高度自信,该应用程序的主要功能是可以正常工作的。
对 JavaScript 应用程序来说有几种方法可以编写端到端测试。像 test cafe 这样的程序会记录您在浏览器中执行操作并将其作为测试源重播。
还有类似 nightwatch 的项目,可让你用 JavaScript 编写测试项目。我会推荐使用类似 nightwatch 的库。拿起来直接用很容易,该测试运行速度比记录的测试更快。
也就是说,night1qtch 的测试还是比较慢的。一套200个单元测试需要花费几分钟的时间,一套200个端到端测试仅需要几分钟时间来运行。
端到端测试的另一个问题是难以调试。当测试失败时,很难找出失败的原因,因为测试涵盖了太多功能。
要有效地测试基于前端组件的 Web 应用程序,你需要三种类型的测试:单元测试,快照测试和 e2e 测试。
你应该对每个组件进行多个单元测试,对每个组件进行一次或两次快照测试,以及测试链接在一起的多个组件的一次或两次端到端测试。
整体单元测试将涵盖大部分测试,你将有一些快照测试和一些 e2e 测试。
如果你遵循前端测试金字塔,你就可以使用杀手级测试套件创建可维护的 Web 应用程序。
你可以在 GitHub 上看到应用程序的快照测试、单元测试和端到端测试的示例源码库。
1.断点调试是啥?难不难?
断点调试其实并不是多么复杂的一件事,简单的理解无外呼就是打开浏览器,打开sources找到js文件,在行号上点一下罢了。操作起来似乎很简单,其实很多人纠结的是,是在哪里打断点?(我们先看一个断点截图,以chrome浏览器的断点为例)
步骤记住没?
用chrome浏览器打开页面 → 按f12打开开发者工具 → 打开Sources → 打开你要调试的js代码文件 → 在行号上单击一下,OK!恭喜你的处女断点打上了,哈哈~~
2.断点怎么打才合适?
打断点操作很简单,核心的问题在于,断点怎么打才能够排查出代码的问题所在呢?下面我继续举个例子方便大家理解,废话不多说,上图:
假设我们现在正在实现一个加载更多的功能,如上图,但是现在加载更多功能出现了问题,点击以后数据没有加载出来,这时候我们第一时间想到的应该是啥?(换一行写答案,大家可以看看自己的第一反应是啥)
我最先想到的是,我点击到底有没有成功?点击事件里的方法有没有运行?好,要想知道这个问题的答案,我们立马去打个断点试试看,断点打在哪?自己先琢磨一下。
接着上图:
各位想到没?没错,既然想知道点击是否成功,我们当然是在代码中的点击事件处添加一个断点,切记不要添加在226行哦,因为被执行的是click方法内的函数,而不是226行的选择器。断点现在已经打上了,然后做什么呢?自己再琢磨琢磨~
继续上图:
然后我们当然是回去点击加载更多按钮啦,为什么?额。。。如果你这么问,请允许我用这个表情
,不点击加载更多按钮,怎么去触发点击事件?不触发点击事件,怎么去执行点击事件里的函数?咆哮状。。不过我相信大家肯定不会问这么low的问题~不瞎扯了~
继续正题,上面的图就是点击加载更多按钮后的情况,我们可以看到左侧的页面被一个半透明的层给盖住了,页面上方还有一串英文和两个按钮,右侧代码227行被添加上了背景色,出现这个情况,先不管那些按钮英文是啥意思有啥作用,你从这个图得到了什么信息?继续琢磨琢磨~
如果出现了上图这个情况,说明一点,click事件中的函数被调用了,进一步说明了点击事件生效。那么我们对于这个问题产生的第一个“犯罪嫌疑人”就被排除了。
补充一下:
如果没有出现上面的情况咋办?那是不是说明点击事件没有生效呢?那是什么导致点击事件没有生效?大家自己思考思考~
可能导致点击事件没生效的原因很多,比多选择器错误,语法错误,被选择的元素是后生成的等。怎么解决呢?
选择器错误,大家可以继续往后看到console部分的内容,我想大家就知道怎么处理了
语法错误,细心排查一下,不熟悉的语法可以百度对比一下
被选择的元素是后生成的,最简单的处理就是使用.on()方法去处理,这个东东带有事件委托处理,详情可以自行百度。
那么接下来”犯罪嫌疑人“的身份锁定在哪里呢?
我们将目光投向事件内部,click事件触发了,那么接下来的问题就是它内部的函数问题了。如果你要问为什么?请给我一块豆腐。。。
打个比方,给你一支笔,让你写字,然后你在纸上写了一个字,发现字没出来,为啥?你说我写了呀,纸上都还有划痕。那是不是可能笔没有墨水或者笔尖坏了了?这个例子和点击加载更多一个道理,写字这个动作就是点击操作,而内部函数就是墨水或者笔尖。明白了不~
接着我们分析下点击事件里面的内容,里面包含三句话,第一句话是变量i自增长,第二句话是给按钮添加一个i标签,第三句话是调用请求数据的方法。
就通过这三句话的本身作用,我们可以将较大一部分嫌疑放在第三句话,一小部分放在第一句和第二句话上,有人可能会疑惑,第二句话怎么会有嫌疑呢?他的作用只不过是添加一个标签,对于数据完全没有影响啊,确实,这句话对于数据没有影响,但是出于严谨考虑,它仍然有可能出错,例如它要是少了一个分号呢?或者句子内部某个符号错误呢?往往就是这种小问题浪费我们很多时间。
好,为了进一步锁定”犯罪嫌疑人“,给大家介绍一个工具,也是上图出现两个图标之一,见下图:
这个小图标的功能叫”逐语句执行“或者叫”逐步执行“,这是我个人理解的一个叫法,意思就是,每点击它一次,js语句就会往后执行一句,它还有一个快捷键,F10。下图示范一下它被点击以后的效果:
我单击了两次这个按钮(或者使用F10快捷键),js代码从227行执行到了229行,所以我管它叫”逐语句执行“或者”逐步执行“。这个功能非常的实用,大部分的调试都会使用到它。
上面介绍到我单击了两次“逐语句执行”按钮,代码从227行运行到229行,大家觉得这意味着啥?是不是说明从语法上来说,前两句是没有问题的,那么是不是也同时意味着前两句就排除嫌疑了呢?我看不然。
大家都知道,加载更多就是一个下一页的功能,而其中最核心的一个就是传给后台的页码数值,每当我点击加载更多按钮一次,页码的数值就要加1,所以如果下一页的数据没出来,是不是有可能是因为页码数值也就是[i变量](下面统一称呼i)有问题?那么如何排查页码是否存在问题呢?大家自己先思考思考。
下面教大家两种查看页码数值i]实际输出值的方法,上图:
第一种:
操作步骤如下:
1.仍然是在227行打上断点 → 2. 点击加载更多按钮 → 3. 单击一次“逐语句执行“按钮,js代码执行到228行 → 4.用鼠标选中i++(什么叫选中大家里不理解?就是你要复制一个东西,是不是要选中它?对,就是这个选中) → 5. 选中以后,鼠标悬浮在目标上方,你就看到上图的结果。
第二种:
这个方法其实和第一种差不多,只不过是在控制台输出i的值,大家只需要按照第一种方法执行到第三步 → 4. 打开和sources同一级栏目的console → 5. 在console下方的输入栏里输入i → 6. 按enter回车键即可。
上面的第二种方法里,提到了console这个东西,我们可以称呼它为控制台或者其他什么都可以,这不重要~console的功能很强大,在调试的过程中,我们往往需要知道某些变量的值到底输出了什么,或者我们使用选择器[$”.div”)这种]是否选中了我们想要的元素等,都可以在控制台打印出来。当然直接用第一种方法也可以。
给大家示范一下在console里打印我们想要选中的元素。上图~
在控制台中输入$(this),即可得到选择的元素,没错,正是我们所点击的对象——加载更多按钮元素。
在这里给大家说说我对console这个控制台的理解:这个东东就是一个js解析器,是浏览器本身用来解析运行js的家伙,只不过浏览器通过console让我们开发者在调试过程中,可以控制js的运行以及输出。通过上面的两种方法,大家可能觉得使用起来很简单,但是我要给大家提醒一下,或者说是一些新手比较容易遇到的困惑。
困惑一:在没有打断点的情况下,在console输入i,结果console报错了。
这应该是新手很常见的问题,为什么不打断点我就没有办法在控制台直接输出变量的值呢?个人理解这时候i只是一个局部变量,如果不打上断点,浏览器会把所有的js全部解析完成,console并不能访问到局部变量,只能访问到全局变量,所以这时候console会报错i未定义,但是当js打上断点时,console解析到了局部变量i所在的函数内,这时候i是能够被访问的。
困惑二:为什么我直接在console里输入$(“.xxx”)能打印出东西来呢?
很简单,console本身就是一个js解析器,$(“.xxx”)就是一个js语句,所以自然console能够解析这个语句然后输出结果。
介绍完“逐语句执行”按钮和console控制台的用法,最后再介绍一个按钮,上图:
这个按钮我称呼它为“逐过程执行”按钮,和“逐语句执行”按钮不同,“逐过程执行”按钮常用在一个方法调用多个js文件时,涉及到的js代码比较长,则会使用到这个按钮。
上图:
假设上图我只在227行打了个断点,然后一直点击逐语句执行”按钮到229行,这时候如果再点击一次“逐语句执行”按钮呢?则会进入下图的js里:
这些都是zepto库文件的内容,没啥好看的,里面运行很复杂,我们不可能一直使用“逐语句执行”按钮,这样你会发现你按了大半天还在库文件里面绕。。。这时候咋办?那就该“逐过程执行”按钮上场了。
上图:
我除了在227行打了一个断点,同时还在237行打了一个断点,当我们运行到229行时,直接单击“逐过程执行”按钮,你会发现,js直接跳过了库文件,运行到了237行,大家可以自己使用体验一下。
最后总结:
本文主要介绍了“逐语句执行”按钮、“逐过程执行”按钮、console控制台这三个工具,以及调试bug时的一些思路。工具的用法我就不再赘述了,大家知道用法就行,具体怎么去更合理的使用,还需要大家通过大量的实践去总结提升~
我其实在本文主要想讲的是调试bug的一个思路,但是由于选的例子涉及东西太多。。。怕全部写下来内容太长,大家也没兴趣看,所以我就简单的选了一部分给大家讲解,不知道大家有没有收获。别看我调试三句话写了一堆的东西,如果真的在实际项目中你也像我这样去做,估计你调试一个Bug的时间会比写一个脚本的时间还长很多。。。在实际情况下,我们应该养成拿到问题的第一时间,自行在脑海中排查问题,找到最有可能出现问题的点,如果没办法迅速的排查出最重要的点,那么你可以使用最麻烦但是很靠谱的方法,利用“逐语句执行”按钮将整个和问题相关的js依次去执行一遍,在执行的过程中,自己也跟着理清思路,同时注意下每个变量的值以及选择器选中的元素是否正确,一般来说,这样做一遍下来,bug都解决的差不多了。
所以个人认为,我们调试bug的思路应该是这样的:首先,js是否成功的执行进来;其次,js是否存在逻辑问题,变量问题,参数问题等等;最后,如果上述都没有问题,请仔细查看各种符号。。。
OK~断点就讲到这里~有不明白的同学可以在下面留言~还有如果大家有什么不懂的知识点或者对前端比较困惑的地方,也可以在下面留言,有空的时候我也会继续针对大家的留言写一些文档的哦~
不少人都曾经在 npm 上发布过自己开发的 JavaScript 模块,而在使用一些模块的过程中,我经常产生“这个模块很有用,但如果能 xxx 就更好了”的想法。所以,本文将站在模块使用者的角度总结一下,如何能让模块变得更好用。
webpack 和 rollup 都支持对 ES6 模块做一些静态优化(例如 Tree Shaking 和 Scope Hoisting),它们都会优先读取 package.json 中的 module 字段作为 ES6 模块的入口,若没有 module 才会读取 main 字段作为 CommonJS 模块的入口。通常的做法是:使用 ES6 语法编写源码,然后用模块打包工具结合语法转换工具生成 CommonJS 模块和 ES6 模块,这样就可以同时提供 main 和 module 字段了。
如果你的用户使用了 TypeScript 但你的模块没有提供声明文件,他们就不得不在项目中添加一段代码避免 TypeScript 的编译错误;另外,这样做并不只是对使用 TypeScript 的用户友好,因为大部分代码编辑器(Webstorm、VS Code 等)都能识别 TypeScript 的类型声明,它们可以据此提供更精准的代码提示并在用户传入错误的参数个数或类型时给出提示。
最好的做法是使用 TypeScript 编写你的模块,编译时会自动生成类型声明。除此之外,你也可以参照文档手动维护一份声明文件。你可以在你的模块根目录下添加 index.d.ts 文件,或者在 package.json 中声明 typings 字段提供声明文件的位置。
你可以通过检测是否有名为 window 的全局变量(例如 !!typeof window)来判断模块当前是运行在 Node.js 还是浏览器中,然后使用不同的方式实现你的功能。
这种方法比较常见,但如果用户使用了模块打包工具,这样做会导致 Node.js 与浏览器的实现方式都会被包含在最终的输出文件中。针对这个问题,开源社区提出了在 package.json 中添加 browser 字段的提议,目前 webpack 和 rollup 都已经支持这个字段了。
browser 字段有两种使用方式:
举个例子,假设你的模块里有两个文件:http.js 和 xhr.js,第一个文件使用 Node.js 中的 http 模块发起请求,另一个使用浏览器中的 XMLHTTPRequest 实现了同样的功能。为了使用适当的文件,你的模块代码中应该始终 require(‘./path/to/http.js’),并在 package.json 中声明:
{ "browser": { "./path/to/http.js": "./path/to/xhr.js" } }
这样一来,当你的模块在打包工具中使用时,打包工具只会将 xhr.js 的代码包含在最终的输出文件中。
大部分 JavaScript 项目都是开源的,而开源社区也提供了很多针对开源项目的免费服务,它们可以给你的项目提供更有力的帮助,这里列举几个比较常用的。
一个项目最常使用的服务就是持续集成了。持续集成服务能将测试、代码风格检测、打包等任务放在服务器上,并在你提交代码时自动运行,常用的有 Travis CI、CircleCI 和 AppVeyor。Travis CI 对开源项目免费,提供 Linux 与 OS X 运行环境;CircleCI 对开源与私有项目都免费,但每个月有 1500 分钟的运行时间限制;AppVeyor 提供 Windows 运行环境,同样对开源项目免费。
运行完测试之后,你还可以将测试覆盖率上传到 Coveralls。这个服务能让你在线浏览代码的测试覆盖情况。
如果你想让你的模块在各个版本的各种浏览器、平台下得到充分的测试,你还可以使用 Sauce Labs 和 BrowserStack,它们都是对开源项目免费的,但需要发邮件申请。
最后,Shields IO 提供了各种图标,这些图标能为你的项目提供很多额外信息,包括但不限于 npm 版本号、下载量、测试通过状态、测试覆盖率、文件大小、依赖是否过期等。
虽然以上的建议大多属于锦上添花,但这会让你的模块对用户更加友好,希望以上的建议能在你开发自己的模块时给你一点帮助。
但是你懂JavaScript 的时间消耗吗?
随着我们的网站越来越依赖 JavaScript, 我们有时会(无意)用一些不易追踪的方式来传输一些(耗时的)东西. 在这篇文章中, 我会介绍一些能让你的网站在移动设备上快速加载且可交互的方式.
摘要: 更少的代码 = 更少的解析/编译(时间) + 更少的传输(时间) + 更少的解压(时间)
大多数开发者考虑 JavaScript 的时间消耗时, 都会首先考虑到 JavaScript 的下载和执行消耗. 脚本传输的字节越多, 花费的时间越长, 用户连接的就越慢.
即使在网络发达的国家, 这也是需要面对的一个问题, 因为用户有效的网络连接类型不一定就是 3G、4G 或者 Wifi. 你可以连接咖啡店的 Wifi, 也可能连接上一个 2G 网络的蜂窝热点.
因而, 开发者需要想办法减少 JavaScript 在网络上的传输时间. 我这提供一些参考的方式:
max-age
和 Etag
等方式来缓存脚本, 减少字节的传输. Service Worker 缓存技术能使你的应用具备网络弹性, 并且能使用像 V8 code cache 一样的特性. 同时, 也可以了解下通过 文件哈希名 实现长久缓存.脚本下载之后, JavaScript 最消耗时间的地方就是 JS 引擎对代码的解析/编译. 在 Chrome DevTools 的性能面板中, JS 的解析和编译是 Scripting time
中的黄色部分.
从 Bottom-Up/Call Tree 可以看到更精确的解析/编译时间.
但是, 为什么会这样呢?
花费很长时间去解析/编译代码会严重延迟用户在网站上的可交互时间. 传输的脚本越多, 在网站可交互之前, 就会花费更多的时间去解析/编译代码.
和脚本相比, 浏览器也会花费很多时间来处理同等大小的图片(图片仍需要被解码). 但是在大多数移动设备上, JS 更有可能对页面的交互性产生负面影响.
当我们谈论脚本的解析和编译很慢时, 上下文是很重要的–我们说的是普通的手机设备. 普通用户的手机是配置低配的 CPU 和 GPU, 可能由于手机内存的限制, 也没有 L2/L3 级缓存设置.
在 JavaScript 性能 一文中, 我注意到在低配手机和高配手机上解析约 1M 被解压后的脚本文件所用的时间是不同的. 对于市面上解析最快的手机和普通手机之间, 大约有 2~5x 的时间差异.
那么不同配置的手机访问 CNN.com 又会是怎么样的呢?
与普通手机(Moto G4) 需要花费约 13s 来解析/编译 CNN 网站的 JS 相比, 高配 iPhone 8 仅需要约 4s 时间.这可以显著地影响用户与该站点完全交互的速度.
这突出了测试普通手机设备(如 Moto G4)的重要性而不仅仅是你口袋里的手机设备. 然而, 上下文关系也很重要: 优化网站用户的硬件设备和网络环境.
深入分析真实用户访问你的网站所使用的移动设备类型, 这样才可能明白他们真实的 CPU/GPU 等硬件约束.
另一方面, 也需要反思我们是否真的传输了太多的脚本?
通过 HTTP Archive 分析约前 500K 网站在移动设备上传输的脚本大小, 可以发现 50% 的网站需要占据 14s, 用户才可以与网站交互, 但是这些网站仅用 4s 时间来解析和编译 JS.
在获取和处理 JS 以及其他资源所需的时间中, 用户需要在页面可交互之前等待一段时间, 这一点也不奇怪, 但我们可以在这里做得更好.
移除页面上的非关键脚本不仅能减少传输时间, 也能减少 CPU 的解析/编译时间和潜在的内存开销, 这可提高页面可交互的速度.
不仅脚本的解析和编译需要时间, 脚本的执行也需要时间. 长时间的执行时间也会延迟用户与站点的交互速度.
如果脚本的执行时间超过 50ms, 那么可交互时间的延迟将是脚本下载、编译和执行脚本所花费时间的总和. — Alex Russell
为减少脚本的执行时间, 可以将脚本分成小块来执行, 以避免锁住主线程. 可以考虑是否能减少脚本在执行过程中需要完成的工作量, 如果工作量很多, 就将脚本分成小块来分解工作量, 以提高页面可交互的速度.
当你尝试着降低 JavaScript 的解析/编译和网络传输时间时, 也可以试试基于路由的代码分割或 PRPL 模式来降低 JavaScript 的交付成本.
PRPL 是一种通过代码分割和缓存来优化页面交互的模式:
通过 V8’s Runtime Call Stats, 我们可以分析一些受欢迎移动站以及 PWA 应用的加载时间. 从下图可以看出, 脚本解析所需要的时间(橙色部分)是页面加载中最耗时的一部分:
除上述方式外, JavaScript 还能通过如下方式影响页面性能:
requestAnimationFrame()
或 requestIdleCallback()
进行任务调度)可以最小化响应性问题.因为优化交互性的成本比较高, 许多网站会考虑去优化内容的可见性. 当 JavaScript Bundles 很大时, 为了减少白屏时间(First paint time), 一些开发者会采用服务端渲染的方式, 当 JS 处理完成之后再将其 “升级” 为事件处理.
但这种方式也是有时间消耗的: 1) 通常会发送一个很大的 HTML 文件作为响应, 2) 在 JavaScript 完成处理之前, 页面可能只有一部分是可交互的.
因而逐步引导可能是一个更好的方式. 浏览请请求一个最小化的功能页面(仅由当前路由需要的 HTML/JS/CSS 组成), 当有更多资源请求时, 应用可以进行资源懒加载, 然后逐步解锁更多功能.
Loading code proportionate to what’s in view is the holy grail. PRPL and Progressive Bootstrapping are patterns that can help accomplish this.
是不是很懵逼,看看浏览器缓存机制剖析
缓存一直是前端优化的主战场, 利用好缓存就成功了一半. 本篇从http请求和响应的头域入手, 让你对浏览器缓存有个整体的概念. 最终你会发现强缓存, 协商缓存 和 启发式缓存是如此的简单.
导读
我不知道拖延症是有多严重, 反正去年3月开的题, 直到今年4月才开始写.(请尽情吐槽吧)
浏览器对于请求资源, 拥有一系列成熟的缓存策略. 按照发生的时间顺序分别为存储策略, 过期策略, 协商策略, 其中存储策略在收到响应后应用, 过期策略, 协商策略在发送请求前应用. 流程图如下所示.
废话不多说, 我们先来看两张表格.
1.http header中与缓存有关的key.
key | 描述 | 存储策略 | 过期策略 | 协商策略 |
---|---|---|---|---|
Cache-Control | 指定缓存机制,覆盖其它设置 | ✔️ | ✔️ | |
Pragma | http1.0字段,指定缓存机制 | ✔️ | ||
Expires | http1.0字段,指定缓存的过期时间 | ✔️ | ||
Last-Modified | 资源最后一次的修改时间 | ✔️ | ||
ETag | 唯一标识请求资源的字符串 | ✔️ |
2.缓存协商策略用于重新验证缓存资源是否有效, 有关的key如下.
key | 描述 |
---|---|
If-Modified-Since | 缓存校验字段, 值为资源最后一次的修改时间, 即上次收到的Last-Modified值 |
If-Unmodified-Since | 同上, 处理方式与之相反 |
If-Match | 缓存校验字段, 值为唯一标识请求资源的字符串, 即上次收到的ETag值 |
If-None-Match | 同上, 处理方式与之相反 |
下面我们来看下各个头域(key)的作用.
Cache-Control
浏览器缓存里, Cache-Control是金字塔顶尖的规则, 它藐视一切其他设置, 只要其他设置与其抵触, 一律覆盖之.
不仅如此, 它还是一个复合规则, 包含多种值, 横跨 存储策略, 过期策略 两种, 同时在请求头和响应头都可设置.
语法为: “Cache-Control : cache-directive”.
假设所请求资源于4月5日缓存, 且在4月12日过期.
当max-age 与 max-stale 和 min-fresh 同时使用时, 它们的设置相互之间独立生效, 最为保守的缓存策略总是有效. 这意味着, 如果max-age=10 days, max-stale=2 days, min-fresh=3 days, 那么:
由于客户端总是采用最保守的缓存策略, 因此, 4月9日后, 对于该资源的请求将重新向服务器发起验证.
Pragma
http1.0字段, 通常设置为Pragma:no-cache, 作用同Cache-Control:no-cache. 当一个no-cache请求发送给一个不遵循HTTP/1.1的服务器时, 客户端应该包含pragma指令. 为此, 勾选☑️ 上disable cache时, 浏览器自动带上了pragma字段. 如下:
Expires
Expires:Wed, 05 Apr 2017 00:55:35 GMT
即到期时间, 以服务器时间为参考系, 其优先级比 Cache-Control:max-age 低, 两者同时出现在响应头时, Expires将被后者覆盖. 如果Expires, Cache-Control: max-age, 或 Cache-Control:s-maxage 都没有在响应头中出现, 并且也没有其它缓存的设置, 那么浏览器默认会采用一个启发式的算法, 通常会取响应头的Date_value - Last-Modified_value值的10%作为缓存时间.
如下资源便采取了启发式缓存算法.
其缓存时间为 (Date_value - Last-Modified_value) * 10%, 计算如下:
const Date_value = new Date('Thu, 06 Apr 2017 01:30:56 GMT').getTime();
const LastModified_value = new Date('Thu, 01 Dec 2016 06:23:23 GMT').getTime();
const cacheTime = (Date_value - LastModified_value) / 10;
const Expires_timestamp = Date_value + cacheTime;
const Expires_value = new Date(Expires_timestamp);
console.log('Expires:', Expires_value); // Expires: Tue Apr 18 2017 23:25:41 GMT+0800 (CST)
可见该资源将于2017年4月18日23点25分41秒过期, 尝试以下两步进行验证:
1) 试着把本地时间修改为2017年4月18日23点25分40秒, 迅速刷新页面, 发现强缓存依然有效(依旧是200 OK (from disk cache)).
2) 然后又修改本地时间为2017年4月18日23点26分40秒(即往后拨1分钟), 刷新页面, 发现缓存已过期, 此时浏览器重新向服务器发起了验证, 且命中了304协商缓存, 如下所示.
3) 将本地时间恢复正常(即 2017-04-06 09:54:19). 刷新页面, 发现Date依然是4月18日, 如下所示.
从⚠️ Provisional headers are shown 和Date字段可以看出来, 浏览器并未发出请求, 缓存依然有效, 只不过此时Status Code显示为200 OK. (甚至我还专门打开了charles, 也没有发现该资源的任何请求, 可见这个200 OK多少有些误导人的意味)
可见, 启发式缓存算法采用的缓存时间可长可短, 因此对于常规资源, 建议明确设置缓存时间(如指定max-age 或 expires).
ETag
ETag:"fcb82312d92970bdf0d18a4eca08ebc7efede4fe"
实体标签, 服务器资源的唯一标识符, 浏览器可以根据ETag值缓存数据, 节省带宽. 如果资源已经改变, etag可以帮助防止同步更新资源的相互覆盖. ETag 优先级比 Last-Modified 高.
If-Match
语法: If-Match: ETag_value 或者 If-Match: ETag_value, ETag_value, …
缓存校验字段, 其值为上次收到的一个或多个etag 值. 常用于判断条件是否满足, 如下两种场景:
If-None-Match
语法: If-None-Match: ETag_value 或者 If-None-Match: ETag_value, ETag_value, …
缓存校验字段, 结合ETag字段, 常用于判断缓存资源是否有效, 优先级比If-Modified-Since高.
Last-Modified
语法: Last-Modified: 星期,日期 月份 年份 时:分:秒 GMT
Last-Modified: Tue, 04 Apr 2017 10:01:15 GMT
用于标记请求资源的最后一次修改时间, 格式为GMT(格林尼治标准时间). 如可用 new Date().toGMTString()获取当前GMT时间. Last-Modified 是 ETag 的fallback机制, 优先级比 ETag 低, 且只能精确到秒, 因此不太适合短时间内频繁改动的资源. 不仅如此, 服务器端的静态资源, 通常需要编译打包, 可能出现资源内容没有改变, 而Last-Modified却改变的情况.
If-Modified-Since
语法同上, 如:
If-Modified-Since: Tue, 04 Apr 2017 10:12:27 GMT
缓存校验字段, 其值为上次响应头的Last-Modified值, 若与请求资源当前的Last-Modified值相同, 那么将返回304状态码的响应, 反之, 将返回200状态码响应.
If-Unmodified-Since
缓存校验字段, 语法同上. 表示资源未修改则正常执行更新, 否则返回412(Precondition Failed)状态码的响应. 常用于如下两种场景:
强缓存
一旦资源命中强缓存, 浏览器便不会向服务器发送请求, 而是直接读取缓存. Chrome下的现象是 200 OK (from disk cache) 或者 200 OK (from memory cache). 如下:
对于常规请求, 只要存在该资源的缓存, 且Cache-Control:max-age 或者expires没有过期, 那么就能命中强缓存.
协商缓存
缓存过期后, 继续请求该资源, 对于现代浏览器, 拥有如下两种做法:
以上, ETag优先级比Last-Modified高, 同时存在时, 前者覆盖后者. 下面通过实例来理解下强缓存和协商缓存.
如下忽略首次访问, 第二次通过 If-Modified-Since 命中了304协商缓存.
协商缓存的响应结果, 不仅验证了资源的有效性, 同时还更新了浏览器缓存. 主要更新内容如下:
Age:0
Cache-Control:max-age=600
Date: Wed, 05 Apr 2017 13:09:36 GMT
Expires:Wed, 05 Apr 2017 00:55:35 GMT
Age:0 表示命中了代理服务器的缓存, age值为0表示代理服务器刚刚刷新了一次缓存.
Cache-Control:max-age=600 覆盖 Expires 字段, 表示从Date_value, 即 Wed, 05 Apr 2017 13:09:36 GMT 起, 10分钟之后缓存过期. 因此10分钟之内访问, 将会命中强缓存, 如下所示:
当然, 除了上述与缓存直接相关的字段外, http header中还包括如下间接相关的字段.
Age
出现此字段, 表示命中代理服务器的缓存. 它指的是代理服务器对于请求资源的已缓存时间, 单位为秒. 如下:
Age:2383321
Date:Wed, 08 Mar 2017 16:12:42 GMT
以上指的是, 代理服务器在2017年3月8日16:12:42时向源服务器发起了对该资源的请求, 目前已缓存了该资源2383321秒.
Date
指的是响应生成的时间. 请求经过代理服务器时, 返回的Date未必是最新的, 通常这个时候, 代理服务器将增加一个Age字段告知该资源已缓存了多久.
Vary
对于服务器而言, 资源文件可能不止一个版本, 比如说压缩和未压缩, 针对不同的客户端, 通常需要返回不同的资源版本. 比如说老式的浏览器可能不支持解压缩, 这个时候, 就需要返回一个未压缩的版本; 对于新的浏览器, 支持压缩, 返回一个压缩的版本, 有利于节省带宽, 提升体验. 那么怎么区分这个版本呢, 这个时候就需要Vary了.
服务器通过指定Vary: Accept-Encoding, 告知代理服务器, 对于这个资源, 需要缓存两个版本: 压缩和未压缩. 这样老式浏览器和新的浏览器, 通过代理, 就分别拿到了未压缩和压缩版本的资源, 避免了都拿同一个资源的尴尬.
Vary:Accept-Encoding,User-Agent
如上设置, 代理服务器将针对是否压缩和浏览器类型两个维度去缓存资源. 如此一来, 同一个url, 就能针对PC和Mobile返回不同的缓存内容.
怎么让浏览器不缓存静态资源
实际上, 工作中很多场景都需要避免浏览器缓存, 除了浏览器隐私模式, 请求时想要禁用缓存, 还可以设置请求头: Cache-Control: no-cache, no-store, must-revalidate .
当然, 还有一种常用做法: 即给请求的资源增加一个版本号, 如下:
<link rel="stylesheet" type="text/css" href="../css/style.css?version=1.8.9"/>
这样做的好处就是你可以自由控制什么时候加载最新的资源.
不仅如此, HTML也可以禁用缓存, 即在页面的
节点中加入标签, 代码如下:
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate"/>
上述虽能禁用缓存, 但只有部分浏览器支持, 而且由于代理不解析HTML文档, 故代理服务器也不支持这种方式.
IE8的异常表现
实际上, 上述缓存有关的规律, 并非所有浏览器都完全遵循. 比如说IE8.
资源缓存是否有效相关.
浏览器 | 前提 | 操作 | 表现 | 正常表现 |
---|---|---|---|---|
IE8 | 资源缓存有效 | 新开一个窗口加载网页 | 重新发送请求(返回200) | 展示缓存的页面 |
IE8 | 资源缓存失效 | 原浏览器窗口中单击 Enter 按钮 | 展示缓存的页面 | 重新发送请求(返回200) |
Last-Modified / E-Tag 相关.
浏览器 | 前提 | 操作 | 表现 | 正常表现 |
---|---|---|---|---|
IE8 | 资源内容没有修改 | 新开一个窗口加载网页 | 浏览器重新发送请求(返回200) | 重新发送请求(返回304) |
IE8 | 资源内容已修改 | 原浏览器窗口中单击 Enter 按钮 | 浏览器展示缓存的页面 | 重新发送请求(返回200) |
参考文章
最近在看《JavaScript高级程序设计》一书,书中讲到相等操作符(==)时说,要比较相等性之前,不能将 null 和 undefined 转换成其他任何值,但要记住 null == undefined 会返回 true 。的确,在ECMAScript规范中也是这样定义的,但我认为这样来理解这件事情,似乎有些浮于表面,网上也有很多关于这个问题的文章,下面我希望从一个全新的角度来分析 null 和 undefined 的区别,从而理解两者为何会相等:
Undefined 和 Null 是 Javascript 中两种特殊的原始数据类型(Primary Type),它们都只有一个值,分别对应 undefined 和 null ,这两种不同类型的值,即有着不同的语义和场景,但又表现出较为相似的行为:
1、undefined
undefined 的字面意思就是未定义的值,这个值的语义是,希望表示一个变量最原始的状态,而非人为操作的结果 。 这种原始状态会在以下 4 种场景中出现:
【1】声明了一个变量,但没有赋值
var foo;
console.log(foo); //undefined
访问foo,返回了undefined,表示这个变量自从声明了以后,就从来没有使用过,也没有定义过任何有效的值,即处于一种原始而不可用的状态。
【2】访问对象上不存在的属性
console.log(Object.foo); // undefined
访问Object对象上的 foo 属性,同样也返回 undefined , 表示Object 上不存在或者没有定义名为 “foo” 的属性。
【3】函数定义了形参,但没有传递实参
//函数定义了形参 a
function fn(a) {
console.log(a); //undefined
}
fn(); //未传递实参
函数 fn 定义了形参a, 但 fn 被调用时没有传递参数,因此,fn 运行时的参数 a 就是一个原始的、未被赋值的变量。
【4】使用 void 对表达式求值
void 0 ; // undefined
void false; //undefined
void []; //undefined
void null; //undefined
void function fn(){} ; //undefined
ECMAScript 规范 void 操作符 对任何表达式求值都返回 undefined ,这个和函数执行操作后没有返回值的作用是一样的,JavaScript中的函数都有返回值,当没有 return 操作时,就默认返回一个原始的状态值,这个值就是undefined,表明函数的返回值未被定义。
因此,undefined 一般都来自于某个表达式最原始的状态值,不是人为操作的结果。当然,你也可以手动给一个变量赋值 undefined,但这样做没有意义,因为一个变量不赋值就是 undefined 。
2、null
null 的字面意思是 空值 ,这个值的语义是,希望表示 一个对象被人为的重置为空对象,而非一个变量最原始的状态 。 在内存里的表示就是,栈中的变量没有指向堆中的内存对象,即:
当一个对象被赋值了null 以后,原来的对象在内存中就处于游离状态,GC 会择机回收该对象并释放内存。因此,如果需要释放某个对象,就将变量设置为null,即表示该对象已经被清空,目前无效状态。试想一下,如果此处把 null 换成 undefined 会不会感到别扭? 显然语义不通,其操作不能正确的表达其想要的行为。
与 null 相关的另外一个问题需要解释一下:
typeof null == 'object'
null 有属于自己的类型 Null,而不属于Object类型,typeof 之所以会判定为 Object 类型,是因为JavaScript 数据类型在底层都是以二进制的形式表示的,二进制的前三位为 0 会被 typeof 判断为对象类型,而 null 的二进制位恰好都是 0 ,因此,null 被误判断为 Object 类型。
其实,我们可以通过另一种方法获取 null 的真实类型:
Object.prototype.toString.call(null) ; // [object Null]
通过 Object 原型上的toString() 方法可以获取到JavaScript 中对象的真实数据类型,当然 undefined 类型也可以通过这种方式来获取:
Object.prototype.toString.call(undefined) ; // [object Undefined]
3、相似性
虽然 undefined 和 null 的语义和场景不同,但总而言之,它们都表示的是一个无效的值。 因此,在JS中对这类值访问属性时,都会得到异常的结果:
ECMAScript 规范认为,既然 null 和 undefined 的行为很相似,并且都表示 一个无效的值,那么它们所表示的内容也具有相似性,即有
undefined == null; //true
不要试图通过转换数据类型来解释这个结论,因为:
Number(null); // 0
Number(undefined); // NaN
//在比较相等性之前,null 没有被转换为其他类型
null == 0 ; //false
但 === 会返回 false ,因为全等操作 === 在比较相等性的时候,不会主动转换分项的数据类型,而两者又不属于同一种类型:
undefined === null; //false,类型不相同
undefined !== null; //true, 类型不相同
4、总结
用一句话总结两者的区别就是:undefined 表示一个变量自然的、最原始的状态值,而 null 则表示一个变量被人为的设置为空对象,而不是原始状态。所以,在实际使用过程中,为了保证变量所代表的语义,不要对一个变量显式的赋值 undefined,当需要释放一个对象时,直接赋值为 null 即可。