浏览器缓存,Cookie和Session

近段时间都是在更新前端学习相关的文章,今天来学习和总结一下一个web通用知识: 浏览器缓存,Cookie和Session。这玩意对前端的性能优化也有一定作用,应该属于前端必须掌握的知识之一了。

缓存

在说这些之前,先说说浏览器的__缓存__。为什么会出现浏览器的缓存技术?不想下东西呗。同样的东西,我每次请求都要下一次,不管是从用户体验还是速度的方面考虑,都非常的僵硬。于是聪明的前端——哦不,浏览器设计者就想到了增加缓存这个东西。

前端想要自己控制缓存,可以在header上设置cache control(Link至MDN,以下部分内容来自MDN)。不算非标准的拓展命令,可以使用的属性值为以下:

缓存请求指令:

Cache-Control: max-age=<seconds>、max-stale[=<seconds>]、 min-fresh=<seconds>、 no-cache 
、no-store、 no-transform、 only-if-cached

缓存响应指令:

Cache-control: must-revalidate、no-cache、no-store、no-transform、 public、 private、proxy-revalidate、 max-age=<seconds>、 s-maxage=<seconds>

一个个说的话就太麻烦了,还是直接看MDN比较好。这里我画一个简单的思维导图做辅助记忆用。

http://ooibweb9s.bkt.clouddn.com//17-6-19/65594827.jpg

Cache-control

看图细心的话应该能够发现,图中有一个s-maxage,对它的设置会覆盖maxage和Expire。Expire是什么?这就是我们能够在Header中设置的另外一个值。将Expires-cache contrl设置为一个时间,那么只要当前时间没有到这个时间,我们所有对服务器进行的请求都将不被接受转而直接走硬盘。(Expire的解释在MDN上没有中文,我认为应该直接翻译为“过期时间”?)

但是Expire有一个小小的问题,就是这个时间怎么来。要知道,浏览器的取时间方法(比如getTime)都是直接取得系统时间,这样就带来一个问题。根据客户端信息不可靠原则,如果我本地的时间错乱(比如我这种装了双系统然后Linux每次都会莫名改写windows时间的),那么Expire就形同虚设。所以嘛,一般不推荐使用Expire.

我们还能够设置的一个属性则是Last-Modified。如果设置了Last-Modified,那么它将默认保存缓存的内容300S。

Cookie是啥我想我就不需要解释了,最简单的Cookie设置方法就是document.cookie/Cookies.set方法。在Set Cookies以后,对__相同域名的所有请求__都会带上这个Cookie回传。Cookie一般分为两种类型,分别为非持久Cookie(内存Cookie)和持久Cookie(硬盘Cookie)。内存Cookie由浏览器维护,保存在内存中,浏览器关闭后就消失了,而硬盘Cookie则有一个过期时间,过期时间内是持续有效的。

为什么要有Cookie?

因为HTTP协议是无状态的,服务器无法记录用户上一次的操作,这样就造成了交互上的阻碍。而Cookie就可以做到绕开HTTP的无状态。服务器借由从Cookie中读取包含的信息,借以维护用户和服务器会话中的状态。(比如购物/登录)。

Cookie的路径

Cookie一般都是由于用户访问一个页面才产生的,但是我们会遇到一个问题——并不是只有在创建Cookie的页面才需要访问这个Cookie。如果出于安全考虑,只有与创建Cookie的页面出于同一个目录或在创建Cookie页面的子目录下的网页才可以访问。

那么这样就又会带来一个问题:如果者说我希望其父级乃至整个网页都能够使用Cookie,怎么做呢?

可以这样:

document.cookie=”userName=CoderMageFox;path=/”;

Cookie的域

路径问题解决了,又一个问题摆在了面前。如果我们在同一个主域下有不同的域名(如img.codermagefox.com和test.codermagefox.com)那么如何互相访问呢?要知道,这个需求的场景相当多。这个时候,我们可以选择指定可访问Cookie的主机名来进行设置。

document.cookie=”name=codermagefox;domain=codermagefox.com path=/;”

这样就可以解决啦。

Cookie的缺陷
(以下引用自MDN)
1.Cookie会被附加在每个HTTP请求中,所以无形中增加了流量。
2.由于在HTTP请求中的Cookie是明文传递的,所以安全性成问题。(除非用HTTPS)
3.Cookie的大小限制在4KB左右。对于复杂的存储需求来说是不够用的。[3]

然而我在翻阅《JavaScript权威指南》的时候发现了一点,现代浏览器遵循的HTTP标准是RFC2965

书上原文:

而这个标准中对于这一段是这么写的:

5.3  Implementation Limits

   Practical user agent implementations have limits on the number and
   size of cookies that they can store.  In general, user agents' cookie
   support should have no fixed limits.  They should strive to store as
   many frequently-used cookies as possible.  Furthermore, general-use
   user agents SHOULD provide each of the following minimum capabilities
   individually, although not necessarily simultaneously:

      *  at least 300 cookies

      *  at least 4096 bytes per cookie (as measured by the characters
         that comprise the cookie non-terminal in the syntax description
         of the Set-Cookie2 header, and as received in the Set-Cookie2
         header)

      *  at least 20 cookies per unique host or domain name

看到这里我有点懵逼了。不是说at least 吗?所以说这些标准其实是浏览器自己定的而不是文档规定的?这个坑我暂时没弄明白,以后弄明白了再回来填。

Session

正如同上面所说,HTTP的传输有一个很大的问题,就是明文传输。明文传输会导致一个什么问题?我可以抓包来获取很多用户信息(比如使用WireShark\Fiddle等工具)而且HTTP请求是可以篡改的,并且十分容易篡改。又根据客户端信息不可靠原则,我们需要一个东西来验证用户的身份怎么办呢?

这个时候,Session就出马了。Session也可以做到Cookie的部分功能,不过,Session是保存在服务器上的。服务端的信息无法随便篡改,所以Session可以做到可靠。具体的做法一般是服务端和客户端之间不传输明文数据,而是传输一段经过特殊加密的密文,密文对应的服务器硬盘数据中才是真实的数据。这段数据在Session激活后从服务器磁盘中取出到内存中,再返回给浏览器。

需要注意的是,Session是一种抽象的概念,开发者为了实现中断和继续等操作,将 user agent 和 server 之间一对一的交互,抽象为“会话”,进而衍生出“会话状态”。而而 cookie 是一个实际存在的东西,http 协议中定义在 header 中的字段。Session可以被认为是一种Cookie的后端无状态实现。

当然,现在很多大流量的网站使用的一般是Secret Cookie,这又是另外一种做法了。

便于记忆的思维导图:

写了这么多,对于Cookie/Session的理解算是有一些了。今天收获不错,Nice!

RequiereJs学习笔记——RequireJS的由来和垃圾回收机制理解的闭包

Why?为什么要使用RequireJs?

一个程序员,不管注释写的再好,总是会难以维护大型项目的代码。100行没问题,1000行没问题,3000行呢?5000行呢?那我光是浏览一遍都得花很多时间,极大的浪费了精力。

那怎么办呢?拆。

把一个3000行的项目拆成几百行几百行的小项目(按组件)。最傻的方式就是写一个Script:

/*topbar区域代码*/
var $topbar=$('topbar')
//省略100行
$topbar.on('click',function(){
  console.log('topbar')
})
/*$banners区域代码*/
var $banners=$('banners')
//省略100行
$banners.on('click',function(){
  console.log('topbar')
})
/*slides区域代码*/
var $slides=('slides')
//省略100行
$slides.on('click',function(){
  console.log('topbar')
})

这样就成功的把代码分成了几个部分。我们十分开心的把代码们都隔开了,不会找错修改的位置了。

但是这样又产生了一个问题,如果哪个人手贱硬要在不同部分之间调用怎么办呢?我本来划分的好好的不同段代码又会变得杂乱无章,无法整理。

闭包问题:

于是有一个聪明的前端就想到了使用立即执行函数来进行隔离,让不同区域之间的代码变成函数内代码,也就是把它做成局部变量放在函数里。(顺带一提,ES6中,立即执行函数是一句废话,因为有let的存在。)

function xxx(){

var $topbar =$(‘#topbar’)

$topbar.on(‘click’),function(){

console.log(‘topbar’)

}

出了函数作用域的块,$topbar就是Undefined,这样就能做到消除全局变量了。

但是这样又产生了一个问题,我们又重新把函数xxx()变成了全局变量。这就很僵硬,我们并没有消灭掉全局变量,消灭掉一个又使用了一个。

于是,又有一个聪明的前端想到了使用!function.

这么做的优点是,我将无法越权修改模块。这就是立即执行函数的意义,可以将环境分割开,让各模块之间不互相干扰。

然而写着写着突然发现了一个新需求:既然已经隔开了,但是我现在想在其他模块上做一个功能需要调用到这个作用域内的函数,又怎么办呢?

函数作用域是不可能互相访问的,我们无法把作用域打通,但是我们可以做一个桥梁。

每当我们新生成一个作用域,它就相当于一个孤岛。

那么想让两个作用域交流怎么办呢?

前端们自然就想到了在函数内使用window.xxxyyy=xxxyyy,将函数内的xxxyyy映射到window上,这样全局不就都可以访问了吗?真是机智。

但是这样就又发生了一个问题,我们将username映射到了全局。如果一个不小心误修改啥的怎么办呢?还是不妥。

我们需要的是什么?是让username只能读,不能写。

于是:

window.userGetter={

nameGetter:function(){

return user.name

},

ageGetter:function(){

return user.age

}

}

我们使用了一次闭包,将这个问题解决了。别的作用域只能读取user,不能改user.

闭包在哪?

在这里说说我的理解。

闭包的用途就是如上:有一个独立的作用域,可以通过全局变量直接将内部的变量映射出去。但是我暴露出去就会有一定的风险,容易被人修改,于是我就不暴露一个变量,而是暴露一个函数。这个函数可以修改这个变量,但是只做有限的操作。外部调取只知道函数名而去访问它,不知道实际上自己操作的是哪个变量名,从而对变量的值进行保护,闭包就是一种作用域的特殊使用方式。

如果要个人做一个定义:

闭包:如果一个函数,它的内部使用了它外部的变量,那么这个函数和这个变量就组合成了一个闭包。

而闭包的作用机制,我认为用JS的垃圾回收机制来说明可能比较清晰明了。

如果我定义一个函数a,并在函数a内写了return b.

然后定义并调用 var fb=a();

那么fb,就是return b 闭包函数的引用。在我执行var fb后,调用了a(),作用域链为函数b→a()→window.

fb会依次往上寻找,直到找到变量为止(找不到则为undefined)

JS垃圾回收机制是不引用则销毁,而这里我们可以看到,作用域链中a()被fb引用,而b被a()引用,也就是说只要fb的引用存在,那么b就一直不会被销毁。

顺便一提,方法访问变量,变量存在内存中,这样做应该会让内存使用的更多才对。

——————————————————————————————————————————————

回到正题。这么使用似乎已经解决了所有问题。但是很快的,会搞事的前端又觉得不爽了。

代码根本没有关联,为啥要放在一个文件里?太sb了。分开吧。

于是:

拆成topbar.js,slide.js,banner.js,他们之间根本无法互相影响。完美!简单粗暴的模块化!!搞那么多弯弯绕绕干嘛!!!还不如一个切图仔想的办法好!!!

前端工程师:你呀,毕竟Naive.

那我有些功能写了一遍并不想再写一遍,比如topbar和banner有相近似的功能,我就是想用他们之中共通的部分,又怎么办呢?写个插件吧。头疼的事又来了,他们之间没法互相访问啊!我写了也没法在其他JS文件中调用。你这样模块化是不是略僵硬了?比如:

new Plugin({element:’#banners>slides.js.’})

写了这个插件之后,我就只能先调用plugin而不能调用别的了。顺序固定了。

而我们知道,两个局部作用域,如果没有window,是永远无法调用其他部分的。

但是分JS文件后,我这么做都是白费功夫,一旦我要依赖,还是要暴露到全局。

暴露到全局的变量太多,太烦了。于是聪明的前端先做一个window.app={},然后将所有东西都挂载在window.app里。那么我所有的调用都是调用app里的,只使用了一个全局变量。是不是跨时代的做法!!(旁人:兄弟,你这不是骗自己吗…该暴露的一个没少啊)

而这个方案,在前端维持了很长一段时间。最著名的库就是:

Jquery($符号)、YUI等。他们都是这么做的

直到requireJs的出现。

Jquery的做法是:分割变量不就是需要立即执行函数吗,那传给我,我帮你执行。上面的需求如果用JQ做,那么我把它挂载在JQ上:

var Plugin=$.Plugin

然后再从$上去取我要的函数。而这种方式,我们叫做命名空间(所有命名都是以同一个空间作为挂载)

requireJS:我觉得你这个方法不错,那我….我就把命名空间的名字固定一下吧。(骗star吗兄弟!!)

window.require([’./plugin.js’],function(Plugin ))

接着就只需要写main.js了,main.js声明了自己依赖了三个js,分别为A\B\C.

ABC里进行了分类,声明了依赖的顺序,全部加载完后就会执行main.js

QQ图片20170613134855

于是RequireJS就成为了一个很棒棒的库,一直到了今天…..

嗯,接下来就是学习如何使用了,这个坑慢慢填。

FP的一个重要知识点:柯里化

FP入门概念必须掌握的是“纯函数”,“柯里化”,“函数组合”。就算只是作为一个FP新手,理解柯里化也是基本的要求。但是我对柯里化的理解一直很模糊,那么今天写个总结,搞定它。

模糊在哪?

不知道什么是真正的柯里化,对柯里化的理解就是减少一个接收的参数,反柯里化就是添加一个接收的参数(我相信大多数人和我是一样一样的)。这种理解其实是非常粗浅的。

首先上Wiki:

柯里化

柯里化(英语:Currying),又译为卡瑞化加里化,是把接受多个参数函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

在直觉上,柯里化声称“如果你固定某些参数,你将得到接受余下参数的一个函数”。

这不还是等于什么都没说嘛! 还是用ScriptOJ的一道题目来理解理解。

curry函数

Q:

函数式编程当中有一个非常重要的概念就是 函数柯里化。一个接受 任意多个参数 的函数,如果执行的时候传入的参数不足,那么它会返回新的函数,新的函数会接受剩余的参数,直到所有参数都传入才执行操作。这种技术就叫柯里化。请你完成 curry 函数,它可以把任意的函数进行柯里化,效果如下:

const f = (a, b, c d) => { ... }
const curried = curry(f)

curried(a, b, c, d)
curried(a, b, c)(d)
curried(a)(b, c, d)
curried(a, b)(c, d)
curried(a)(b, c)(d)
curried(a)(b)(c, d)
curried(a, b)(c)(d)
// ...
// 这些函数执行结果都一样

// 经典加法例子
const add = curry((a, b) => a + b)
const add1 = add(1)

add1(1) // => 2
add1(2) // => 3
add1(3) // => 4

注意,传给 curry 的函数可能会有任意多个参数。

这道题的意思其实就是,不管我传进去几个函数,得到的答案都是相同的。它产生了一系列函数方法,每个函数都只有一个参数,实现是通过每次在另一个新的Curry函数中隐藏一个参数来实现。这种思想大概就是化繁为简,分而治之?

怎么理解呢?

如果我定义一个函数,需要a,b,c,d四个参数,那么如果想要程序正常跑起来,就得给它传四个参数,一个都不能少,否则就会报错。那么,如果我们把它柯里化了呢?那么来解答一下这道题:

var curry=function curry(f){

    var arr=arguments.length>1 && argument[1]!==undefined?arguments[1]:[];
//参数的个数是否大于1或第2个参数不等于undefined?是的话,arr为第2个参数,否的话,arr为空数组
    return function f1(){
//返回一个函数f1
        for (var len=arguments.length,args=Array(len),i=0;i<len;i++){

            args[i]=arguments[i];
//遍历argument,将其参数存入arg[]中
        }

        return function f2(a){
//返回一个函数f2
            return a.length === f.length ? f(a):curry(f,a)};
//a的长度是否等于f的长度?是的话,f2为f(a);否,f2为curry(f,a)(重新调用一次curry) 
          
    }([].concat(arr,args));
//将arr,args连接起来
};

然后抄一下别人的代码:

ES6:(果然简洁):

const curry = (f, arr = []) => {
  return (...args) => {
    return (a) => {
      return a.length === f.length ? f(a) : curry(f, a);
    }([...arr, ...args]);
  };
};

老司机系列:

const curry = ( f, arr = []) => (...args) => ( a => a.length === f.length ? f(...a) : curry(f, a))([...arr, ...args]);

老司机系列写的就很有灵性了…看起来我ES6还完全没入门啊,多多加油:)

记一个Vue项目中的双向绑定和异步导致的Bug

最近赶项目,一周上7天班每天还得搞到十点十一点,实在是没有精力再看书了。 但是写项目的时候碰到的一个Bug完全超出了我的知识范围,我隐隐感觉如果解决了应该会让我对JS有更好的认识,于是没有求助大佬,恳求给我些时间来解决。大佬正忙着也不想弄什么疑难杂症于是同意了。我可得好好看看这玩意什么毛病再写个博客记一记了…….. Bug是这样的:

gif1

做了一个选择本月、本年、全部的部分,但是很奇怪,调的都是同样一个函数只是传参不同,点击本月、本年后可以正常的把展示的样式改为点击后的 ,但是全部却点击不上。

pic2

可以看到,我是用dateBtnChoose这个变量来进行选择判断的。二话不说先Console。

pic3

先在本月后面加了dateBtnChoose的调用,然后再在调用的函数里写了console.奇怪的事情发生了。

gif5

发生了我根本无法理解的事情,同样一个函数传参,同样的显示,参数是1、2时正常,但是参数为3的时候可以确定type=3,dateBtnChoose同样等于3,但是dateBtnChoose显示出来居然是0?排查之后发现,也没有对dateBtnChoose=3进行判断的部分,也就是说根本不是代码里进行操作导致的Bug.

png4

开始头疼了。排查了半天无果,向大佬求助,大佬看了一下给了提示:“双向绑定”。

于是开始思考。已知这些值是双向绑定的….有什么问题吗?苦苦思索,有哪里把dateBtnChoose置0了吗?找了一遍,发现日期input框中@change绑定的@dateSelectClear只有一行:

this.dateBtnChoose= 0;

于是尝试删除@change,发现问题没有了。但需求是,如果我手动在UI的日期框输入了日期,那么按钮选择置0,这个@change不能随便删除。再仔细想想,我大概明白是怎么一回事了。

在点击button后,调用了函数selectsearchDate(type),此时将this.dateBtnChoose的值置为type.但是selectsearchDate这个函数同样操作了绑定在el-date-picker上的this.searchParamsObj,这个操作会引起el-date-picked的改变,继而触发@change,然后触发函数dateSelectClear,将this.dateBtnChoose置0.

那么怎么解决呢?想了半天,有点束手无策。问大佬,大佬提示:“你傻吗,加个读写开关不就行了。”

大概明白怎么做了。加入一个读写开关值isBtnStatus,在值change的时候做读写保护。

使用值isBtnStatus:

pic5

只有当isBtnStatus不为1时,才在触发@change的时候将dataBtnChoose置为0,并在触发@change后将isBtnStatus置为0.

pic6

在selectsearchDate中加入对isBtnStatus值的操作。赋值之后,将isBtnStatus置0。因为!this.isBtnStatus的值为0,接下来对startdate和enddate的操作并不会触发@change中的置0,而在对startdate和enddate进行操作、触发@change后,重新将isBtnStatus置0.此时的@change再进行触发,即可为正常值。

但是,又发现了一个新的问题,那就是点击多次以后偶尔会出现点击日期框后按钮样式并未转变的问题。

点击多次以后会出现偶尔有一次,点击一个按钮后,通过更改日期值并没有办法将按钮值清零。出现频率大概是点击7-8次出现一次。这下我就确实有些不知如何是好了,于是无奈又只能请教大佬。

大佬看了以后陷入了沉思。语重心长的给了我提示:“这么明显的异步请求问题都看不出来?执行完选择后加个setTimeout不就好了吗?是不是傻?”

我感觉大佬说的很对。searchByType()函数调用了接口,js进行异步执行,此时会出现接口还在调取中,

this.searchParamsObj.StartTime=startdate;

this.searchParamsObj.EndTime=enddate;

这两句还没有执行完,就直接执行

this.isBtnStatus = 0;

然后接口又调到数据了,触发@change,血崩,功能失效。

于是我把函数改成:

pic8

终于没有问题了,感动。

这次的Bug还是让我感到有点惭愧的,这么多基础知识居然理解的这么浅薄,碰到实际问题就蠢的找不着北,真鸡儿丢人……

默默刷Vue官方文档去了….

Jenkins自动部署Vue项目测试环境

每次写代码都会被测试烦到,Bug标了解决还是追着问为啥测的时候还没解决啊?答曰还没发测试,又被追着问啥时候发啊?于是只能说好好好这就发,发测试环境又得先build,我的i7 256GSSD 12G内存Build都慢的一笔,烦不胜烦。想了想,其实测这种项目完全可以不用Build,Vue自己数据驱动,编译以后直接就出结果,命令都省了。为了优化流程(偷懒),这不上Jenkins能忍?

(此处应有图片:愤怒的切图仔)

上Jenkins之前我首先考虑了一下要不要上Docker,后来想想看还是算了。一则本来就准备弄个虚拟机当服务器,再弄个docker没啥必要。二则需求也不是很复杂,就装个Jenkins就好,直接上手简单暴力。

首先,打开 Jenkins 下载最新版的Jenkins,然后用Xshell/SSH/Samba随便什么方法把它拷到你的随便什么目录下(在这里我使用的是用户目录)

然后开始安装?不不不,你还没装JDK呢。输入:

yum search java|grep jdk

找到各种版本的JDK。随便挑一个,就:

yum install java-1.7.0-openjdk

等它装完就成。哦,最好不要装1.6以下,我被坑了一次。

下一步正式开始装Jenkins了。CD到你使用的目录:

cd ~

这东西最好是放在后台运行,不然你没法打其他命令。确保这个目录下有你下载的jenkins.war文件,然后输入命令:

nohup java -jar ./jenkins.war --httpPort=80

为什么要映射到80?因为方便啊…

然后CD到你的项目目录,我的目录是用户目录/web。项目怎么跑我想我应该没必要说明了。直接:

npm i nohup npm run dev > /dev/null 2>&1 & 这样后台运行项目也好了。然后我们来设置Jenkins.

打开浏览器,输入http://你的IP地址,直接就可以进入Jenkins Welcome界面。 根据提示一步一步配置好就行了,进入Jenkins开始配置项目。因为Vue会实时更新,所以只是测试的话没必要设置build,只要设置好更新目录就行——这又省事了,下次用到了再写。

在主界面点击新建——构建一个自由风格的软件项目(名字自己填)

然后点击General标签下的第一个高级,定义自己的项目目录:

接着在源码管理里填写你的SVN地址,并添加账号密码登录,进行代码拉取

最后还需要设置项目运行的时间间隔:

全部设置完毕后,点击保存即可。为什么不需要做构建步骤?因为Vue会自己做数据更新,只需要你更改了源码,项目就会自己更新。全部设置完以后,在面板就可以看到项目了。等它自己执行就行了!

OK,下一个目标就是把windows服务器上的项目部署也搞定,把持续集成自动化搞起来!