bailinlin / bailinlin.github.io Goto Github PK
View Code? Open in Web Editor NEW欢迎访问霸天的网站 :tada: https://bailinlin.github.io :tada:
欢迎访问霸天的网站 :tada: https://bailinlin.github.io :tada:
如果你想要深入学习Node,那你不能错过《深入浅出Node.js》这本书,它从不同的视角介绍了 Node 内在的特点和结构。由首章Node 介绍为索引,涉及Node 的各个方面,主要内容包含模块机制的揭示、异步I/O 实现原理的展现、异步编程的探讨、内存控制的介绍、二进制数据Buffer 的细节、Node 中的网络编程基础、Node 中的Web 开发、进程间的消息传递、Node 测试以及通过Node 构建产品需要的注意事项。最后的附录介绍了Node 的安装、调试、编码规范和NPM 仓库等事宜。在读这本书的过程中我收获颇多,并用思维导图的方式记录下来,方便自己后期温故。如果你刚好没有读过,可以当做度这本书的预习阅读,相信你也会有所收获(思维导图图片可能有点小,记得点开看)
CommonJS提出的规范均十分简单,但是现实意义却十分强大。Node通过模块规范,组织了 自身的原生模块,弥补JavaScript弱结构性的问题,形成了稳定的结构,并向外提供服务。NPM 通过对包规范的支持,有效地组织了第三方模块,这使得项目开发中的依赖问题得到很好的解决, 并有效提供了分享和传播的平台,借助第三方开源力量,使得Node第三方模块的发展速度前所未 有,这对于其他后端JavaScript语言实现而言是从未有过的。从一定的角度上讲,CommonJS规范 帮助Node形成了它的骨骼。只有茁壮的根,才能培养出茂盛的枝叶,并成长为参天大树。正是这 些底层的规范和实践,使得Node有序地发展着,摆脱掉过去JavaScript纷乱和被误解的局面,进 而进化成良性的生态系统。
本章介绍了异步I/O和另一些非I/O的异步方法。可以看出,事件循环是异步实现的核心,它 与浏览器中的执行模型基本保持了一致。而像古老的Rhino,尽管是较早就能在服务器端运行的 JavaScript运行时,但是执行模型并不像浏览器采用事件驱动,而是像其他语言一般采用同步I/O 作为主要模型,这造成它在性能上无所发挥。Node正是依靠构建了一套完善的高性能异步I/O框 架,打破了JavaScript在服务器端止步不前的局面。
在接触Node的过程中,很多人粗略地接触了几个回调函数之后就放弃了。尽管异步编程略微 艰难,但是并非一无是处,一旦习惯,就显得自然。从社区和过往的经验而言,JavaScript异步编 程的难题已经基本解决,无论是通过事件,还是通过Promise/Deferred模式,或者流程控制库。相 信在掌握以上技巧之后,异步编程不是难事,习惯异步编程之后,将会收获许多值得享受的编程 体验。
本章主要介绍了主流的几种异步编程解决方案,这是目前JavaScript中主要使用的方案。但对 于其他语言而言,还有协程(coroutine)等方式。但是由于Node基于V8的原因,在目前EMCAScript5 的实现下还不支持协程。这些标准和规范还在制定中,所以暂时不作介绍。未来的V8如果支持 Generator,也将在Node中能直接使用。
最后,因为人们总是习惯性地以线性的方式进行思考,以致异步编程相对较为难以掌握。这 个世界以异步运行的本质是不会因为大家线性思维的惯性而改变。就像日出月落不会因为你的心 情而改变其自有的运行轨迹。
Node将JavaScript的主要应用场景扩展到了服务器端,相应要考虑的细节也与浏览器端不同, 需要更严谨地为每一份资源作出安排。总的来说,内存在Node中不能随心所欲地使用,但也不是 完全不擅长。本章介绍了内存的各种限制,希望读者可以在使用中规避禁忌,与生态系统中的各 种软件搭配,发挥Node的长处。
体验过JavaScript友好的字符串操作后,有些开发者可能会形成思维定势,将Buffer当做字 符串来理解。但字符串与Buffer之间有实质上的差异,即Buffer是二进制数据,字符串与Buffer 之间存在编码关系。因此,理解Buffer的诸多细节十分必要,对于如何高效处理二进制数据十 分有用。
Node基于事件驱动和非阻塞设计,在分布式环境中尤其能发挥出它的特长,基于事件驱动可 以实现与大量的客户端进行连接,非阻塞设计则让它可以更好地提升网络的响应吞吐。Node提供 了相对底层的网络调用,以及基于事件的编程接口,使得开发者在这些模块上十分轻松地构建网 络应用。下一章我们将在本章的基础上探讨具体的Web应用。
本章涉及的内容较为丰富,在Web应用的整个构建过程中,从处理请求到响应请求的整个过 程都有原理性阐述,整理本章细节就可以完成一个功能完备的Web开发框架。过去的各种Web技 术,随着框架和库的成型,开发者往往迷糊地知道应用框架和库,却不知道细节的实现,这好比 没有地图却在野地里行进。本章的内容希望能为Node开发者带来地图似的启发,在开发Web应用 时能够心有轮廓,明了细微。
现在知名和成熟的Web框架有Connect、Express等,本章中的内容在这些框架中都有实现, 因为行文的原因,本章中的代码实现得较为粗糙,实际使用请使用这些成熟的框架。
尽管Node从单线程的角度来讲它有够脆弱的:既不能充分利用多核CPU资源,稳定性也无 法得到保障。但是群体的力量是强大的,通过简单的主从模式,就可以将应用的质量提升一个 档次。在实际的复杂业务中,我们可能要启动很多子进程来处理任务,结构甚至远比主从模式 复杂,但是每个子进程应当是简单到只做好一件事,然后通过进程间通信技术将它们连接起来 即可。这符合Unix的设计理念,每个进程只做一件事,并做好一件事,将复杂分解为简单,将 简单组合成强大。
尽管通过 child_process 模块可以大幅提升Node的稳定性,但是一旦主进程出现问题, 所 有子进程将会失去管理。在Node的进程管理之外,还需要用监听进程数量或监听日志的方式确 保整个系统的稳定性,即使主进程出错退出,也能及时得到监控警报,使得开发者可以及时处 理故障。
测试是应用或者系统最重要的质量保证手段。有单元测试实践的项目,必然对代码的粒度和 层次都掌握得较好。单元测试能够保证项目每个局部的正确性,也能够在项目迭代过程中很好地 监督和反馈迭代质量。如果没有单元测试,就如同黑夜里没有秉烛的行走。
对于性能,在编码过程中一定存在部分感性认知,与实际情况有部分偏差,而性能测试则能 很好地斧正这种差异。
一般而言,决定用一项技术进行产品开发时,只有最早期是与这门技术完全相关的。随着时 间的迁移,要解决的已经不是原来的问题了,一门技术只能在一定层面上发挥出它的优势来。用 Node也是一样,随着开发的进展、涉及层面的增多,我们看到在产品的角度要解决的问题依然是 大部分技术都要解决的问题。我们希望读者能够将Node纳入到新的层面上进行考虑,使它更适应 产品,在产品中发挥出更大的优势来。
思维导图能比较清晰的还原整本书的知识结构体系,如果你还没用看过这本书,可以按照这个思维导图的思路快速预习一遍,提高学习效率。学习新事物总容易遗忘,我比较喜欢在看书的时候用思维导图做些记录,便于自己后期复习,如果你已经看过了这本书,也建议你收藏复习。如果你有神马建议或则想法,欢迎留言或加我微信交流:646321933
《深入浅出Node.js》PDF
有过面试经验的同学应该都被问过浏览器兼容性的问题,对于面试官的问题,常常猝不及防,因为通常他们都是这么问的。"来谈谈浏览器兼容的问题吧","你对浏览器的兼容性有了解过吗",那么如何才是我们正确回答这个问题的姿势呢。
虽然面试官的问题十分的笼统,浏览器的兼容性无非还是样式兼容性(css),交互兼容性(javascript),浏览器 hack 三个方面。
因为历史原因,不同的浏览器样式存在差异,可以通过 Normalize.css 抹平差异,也可以定制自己的 reset.css,例如通过通配符选择器,全局重置样式
* { margin: 0; padding: 0; }
在CSS3还没有成为真正的标准时,浏览器厂商就开始支持这些属性的使用了。CSS3样式语法还存在波动时,浏览器厂商提供了针对浏览器的前缀,直到现在还是有部分的属性需要加上浏览器前缀。在开发过程中我们一般通过IDE开发插件、css 预处理器以及前端自动化构建工程帮我们处理。
浏览器内核与前缀的对应关系如下
内核 | 主要代表的浏览器 | 前缀 |
---|---|---|
Trident | IE浏览器 | -ms |
Gecko | Firefox | -moz |
Presto | Opera | -o |
Webkit | Chrome和Safari | -webkit |
在还原设计稿的时候我们常常会需要用到透明属性,所以解决 IE9 以下浏览器不能使用 opacit。
opacity: 0.5;
filter: alpha(opacity = 50); //IE6-IE8我们习惯使用filter滤镜属性来进行实现
filter: progid:DXImageTransform.Microsoft.Alpha(style = 0, opacity = 50); //IE4-IE9都支持滤镜写法progid:DXImageTransform.Microsoft.Alpha(Opacity=xx)
事件兼容的问题,我们通常需要会封装一个适配器的方法,过滤事件句柄绑定、移除、冒泡阻止以及默认事件行为处理
var helper = {}
//绑定事件
helper.on = function(target, type, handler) {
if(target.addEventListener) {
target.addEventListener(type, handler, false);
} else {
target.attachEvent("on" + type,
function(event) {
return handler.call(target, event);
}, false);
}
};
//取消事件监听
helper.remove = function(target, type, handler) {
if(target.removeEventListener) {
target.removeEventListener(type, handler);
} else {
target.detachEvent("on" + type,
function(event) {
return handler.call(target, event);
}, true);
}
};
new Date()构造函数使用,'2018-07-05'是无法被各个浏览器中,使用new Date(str)来正确生成日期对象的。 正确的用法是'2018/07/05'.
获取 scrollTop 通过 document.documentElement.scrollTop 兼容非chrome浏览器
var scrollTop = document.documentElement.scrollTop||document.body.scrollTop;
快速判断 IE 浏览器版本
<!--[if IE 8]> ie8 <![endif]-->
<!--[if IE 9]> *气的 ie9 浏览器 <![endif]-->
判断是否是 Safari 浏览器
/* Safari */
var isSafari = /a/.__proto__=='//';
判断是否是 Chrome 浏览器
/* Chrome */
var isChrome = Boolean(window.chrome);
“什么?你们公司要兼容IE6,我们今天的面试就到这里为止吧,告辞”。
CSS透明opacity和IE各版本透明度滤镜filter的最准确用法
《你不知道的 javascript》是一个前端学习必读的系列,让不求甚解的JavaScript开发者迎难而上,深入语言内部,弄清楚JavaScript每一个零部件的用途。本书《你不知道的javascript》中卷介绍了该系列的两个主题:“类型和语法”以及“异步与性能”。这两块也是值得我们反复去学习琢磨的两块只是内容,今天我们用思维导图的方式来精读一遍。(思维导图图片可能有点小,记得点开看,你会有所收获)
JavaScript 有 七 种 内 置 类 型: null 、 undefined 、 boolean 、 number 、 string 、 object 和 symbol ,可以使用 typeof 运算符来查看。
变量没有类型,但它们持有的值有类型。类型定义了值的行为特征。
很多开发人员将 undefined 和 undeclared 混 为 一 谈, 但 在 JavaScript 中 它 们 是 两 码 事。 undefined 是值的一种。 undeclared 则表示变量还没有被声明过。
遗憾的是, JavaScript 却将它们混为一谈, 在我们试图访问 "undeclared" 变量时这样报 错:ReferenceError: a is not defined, 并 且 typeof 对 undefined 和 undeclared 变 量 都 返 回 "undefined" 。
然而,通过 typeof 的安全防范机制(阻止报错)来检查 undeclared 变量,有时是个不错的办法。
JavaScript 中的数组是通过数字索引的一组任意类型的值。字符串和数组类似,但是它们的 行为特征不同, 在将字符作为数组来处理时需要特别小心。 JavaScript 中的数字包括“整 数”和“浮点型”。
基本类型中定义了几个特殊的值。
null 类型只有一个值 null , undefined 类型也只有一个值 undefined 。 所有变量在赋值之 前默认值都是 undefined 。 void 运算符返回 undefined 。
数 字 类 型 有 几 个 特 殊 值, 包 括 NaN ( 意 指“not a number” , 更 确 切 地 说 是“invalid number” )、 +Infinity 、 -Infinity 和 -0 。
简单标量基本类型值(字符串和数字等)通过值复制来赋值 / 传递, 而复合值(对象等) 通过引用复制来赋值 / 传递。 JavaScript 中的引用和其他语言中的引用 / 指针不同,它们不 能指向别的变量 / 引用,只能指向值。
JavaScript 为基本数据类型值提供了封装对象,称为原生函数(如 String 、 Number 、 Boolean 等)。它们为基本数据类型值提供了该子类型所特有的方法和属性(如: String#trim() 和 Array#concat(..) )。
对于简单标量基本类型值,比如 "abc" ,如果要访问它的 length 属性或 String.prototype 方法, JavaScript 引擎会自动对该值进行封装(即用相应类型的封装对象来包装它)来实现对这些属性和方法的访问。
本章介绍了 JavaScript 的数据类型之间的转换,即强制类型转换:包括显式和隐式。
强制类型转换常常为人诟病, 但实际上很多时候它们是非常有用的。 作为有使命感的 JavaScript 开发人员,我们有必要深入了解强制类型转换,这样就能取其精华,去其糟粕。
显式强制类型转换明确告诉我们哪里发生了类型转换, 有助于提高代码可读性和可维 护性。
隐式强制类型转换则没有那么明显,是其他操作的副作用。感觉上好像是显式强制类型转 换的反面,实际上隐式强制类型转换也有助于提高代码的可读性。
在处理强制类型转换的时候要十分小心,尤其是隐式强制类型转换。在编码的时候,要知 其然,还要知其所以然,并努力让代码清晰易读。
JavaScript 语法规则中的许多细节需要我们多花点时间和精力来了解。 从长远来看, 这有 助于更深入地掌握这门语言。
语句和表达式在英语中都能找到类比——语句就像英语中的句子,而表达式就像短语。表 达式可以是简单独立的,否则可能会产生副作用。
JavaScript 语法规则之上是语义规则(也称作上下文)。例如, { } 在不同情况下的意思不 尽相同,可以是语句块、对象常量、解构赋值(ES6)或者命名函数参数(ES6)。
JavaScript 详细定义了运算符的优先级(运算符执行的先后顺序)和关联(多个运算符的 组合方式)。只要熟练掌握了这些规则,就能对如何合理地运用它们作出自己的判断。
ASI(自动分号插入)是 JavaScript 引擎的代码解析纠错机制, 它会在需要的地方自动插 入分号来纠正解析错误。问题在于这是否意味着大多数的分号都不是必要的(可以省略), 或者由于分号缺失导致的错误是否都可以交给 JavaScript 引擎来处理。
JavaScript 中有很多错误类型, 分为两大类:早期错误(编译时错误, 无法被捕获)和运 行时错误(可以通过 try..catch 来捕获)。所有语法错误都是早期错误,程序有语法错误 则无法运行。
函数参数和命名参数之间的关系非常微妙。尤其是 arguments 数组,它的抽象泄漏给我们 挖了不少坑。因此,尽量不要使用 arguments ,如果非用不可,也切勿同时使用 arguments 和其对应的命名参数。
finally 中代码的处理顺序需要特别注意。它们有时能派上很大用场,但也容易引起困惑, 特别是在和带标签的代码块混用时。总之,使用 finally 旨在让代码更加简洁易读,切忌 弄巧成拙。
switch 相对于 if..else if.. 来说更为简洁。需要注意的一点是,如果对其理解得不够透 彻,稍不注意就很容易出错。
实际上, JavaScript 程序总是至少分为两个块:第一块 现在 运行;下一块 将来 运行, 以响 应某个事件。尽管程序是一块一块执行的,但是所有这些块共享对程序作用域和状态的访 问,所以对状态的修改都是在之前累积的修改之上进行的。
一旦有事件需要运行, 事件循环就会运行, 直到队列清空。 事件循环的每一轮称为一个 tick。 用户交互、IO 和定时器会向事件队列中加入事件。
任意时刻,一次只能从队列中处理一个事件。执行事件的时候,可能直接或间接地引发一 个或多个后续事件。
并发是指两个或多个事件链随时间发展交替执行,以至于从更高的层次来看,就像是同时 在运行(尽管在任意时刻只处理一个事件)。
通常需要对这些并发执行的“进程”(有别于操作系统中的进程概念)进行某种形式的交 互协调,比如需要确保执行顺序或者需要防止竞态出现。这些“进程”也可以通过把自身 分割为更小的块,以便其他“进程”插入进来。
回调函数是 JavaScript 异步的基本单元。但是随着 JavaScript 越来越成熟,对于异步编程领域的发展,回调已经不够用了。
第一,大脑对于事情的计划方式是线性的、阻塞的、单线程的语义,但是回调表达异步流 程的方式是非线性的、非顺序的,这使得正确推导这样的代码难度很大。难于理解的代码 是坏代码,会导致坏 bug。
我们需要一种更同步、更顺序、更阻塞的的方式来表达异步,就像我们的大脑一样。
第二,也是更重要的一点,回调会受到控制反转的影响,因为回调暗中把控制权交给第三 方(通常是不受你控制的第三方工具!)来调用你代码中的 continuation。 这种控制转移导 致一系列麻烦的信任问题,比如回调被调用的次数是否会超出预期。
可以发明一些特定逻辑来解决这些信任问题,但是其难度高于应有的水平,可能会产生更 笨重、更难维护的代码,并且缺少足够的保护,其中的损害要直到你受到 bug 的影响才会 被发现。
我们需要一个通用的方案来解决这些信任问题。不管我们创建多少回调,这一方案都应可 以复用,且没有重复代码的开销。
我们需要比回调更好的机制。到目前为止,回调提供了很好的服务,但是未来的 JavaScript 需要更高级、功能更强大的异步模式。本书接下来的几章会深入探讨这些新型技术。
Promise 非常好,请使用。它们解决了我们因只用回调的代码而备受困扰的控制反转问题。
它们并没有摈弃回调,只是把回调的安排转交给了一个位于我们和其他工具之间的可信任 的中介机制。
Promise 链也开始提供(尽管并不完美)以顺序的方式表达异步流的一个更好的方法,这 有助于我们的大脑更好地计划和维护异步 JavaScript 代码。我们将在第 4 章看到针对这个 问题的一种更好的解决方案!
生成器是 ES6 的一个新的函数类型, 它并不像普通函数那样总是运行到结束。 取而代之 的是, 生成器可以在运行当中(完全保持其状态)暂停, 并且将来再从暂停的地方恢复 运行。
这种交替的暂停和恢复是合作性的而不是抢占式的,这意味着生成器具有独一无二的能力 来暂停自身,这是通过关键字 yield 实现的。不过,只有控制生成器的迭代器具有恢复生 成器的能力(通过 next(..) )。
yield / next(..) 这一对不只是一种控制机制,实际上也是一种双向消息传递机制。 yield .. 表 达式本质上是暂停下来等待某个值,接下来的 next(..) 调用会向被暂停的 yield 表达式传回 一个值(或者是隐式的 undefined )。
在异步控制流程方面,生成器的关键优点是:生成器内部的代码是以自然的同步 / 顺序方 式表达任务的一系列步骤。其技巧在于,我们把可能的异步隐藏在了关键字 yield 的后面, 把异步移动到控制生成器的迭代器的代码部分。
换句话说,生成器为异步代码保持了顺序、同步、阻塞的代码模式,这使得大脑可以更自 然地追踪代码,解决了基于回调的异步的两个关键缺陷之一。
本部分的前四章都是基于这样一个前提:异步编码模式使我们能够编写更高效的代码,通 常能够带来非常大的改进。但是,异步特性只能让你走这么远,因为它本质上还是绑定在 一个单事件循环线程上。
因此,在这一章里,我们介绍了几种能够进一步提高性能的程序级别的机制。
Web Worker 让你可以在独立的线程运行一个 JavaScript 文件(即程序),使用异步事件在 线程之间传递消息。 它们非常适用于把长时间的或资源密集型的任务卸载到不同的线程 中,以提高主 UI 线程的响应性。
SIMD 打算把 CPU 级的并行数学运算映射到 JavaScript API, 以获得高性能的数据并行运算,比如在大数据集上的数字处理。
最后, asm.js 描述了 JavaScript 的一个很小的子集, 它避免了 JavaScript 难以优化的部分 (比如垃圾收集和强制类型转换),并且让 JavaScript 引擎识别并通过激进的优化运行这样 的代码。可以手工编写 asm.js, 但是会极端费力且容易出错,类似于手写汇编语言(这也 是其名字的由来)。实际上, asm.js 也是高度优化的程序语言交叉编译的一个很好的目标, 比如 Emscripten 把 C/C++ 转换成 JavaScript(https://github.com/kripken/emscripten/wiki) 。
JavaScript 还有一些更加激进的思路已经进入非常早期的讨论, 尽管本章并没有明确包含 这些内容,比如近似的直接多线程功能(而不是藏在数据结构 API 后面)。不管这些最终 会不会实现,还是我们将只能看到更多的并行特性偷偷加入 JavaScript, 但确实可以预见, 未来 JavaScript 在程序级别将获得更加优化的性能。
对一段代码进行有效的性能测试,特别是与同样代码的另外一个选择对比来看看哪种方案 更快,需要认真注意细节。
与其打造你自己的统计有效的性能测试逻辑,不如直接使用 Benchmark.js 库,它已经为你 实现了这些。但是,编写测试要小心,因为我们很容易就会构造一个看似有效实际却有缺 陷的测试,即使是微小的差异也可能扭曲结果,使其完全不可靠。
从尽可能多的环境中得到尽可能多的测试结果以消除硬件/ 设备的偏差, 这一点很重要。 jsPerf.com 是很好的网站,用于众包性能测试运行。
遗憾的是,很多常用的性能测试执迷于无关紧要的微观性能细节,比如 x++ 对比 ++x 。编 写好的测试意味着理解如何关注大局, 比如关键路径上的优化以及避免落入类似不同的 JavaScript 实现细节这样的陷阱中。
尾调用优化是 ES6 要求的一种优化方法。它使 JavaScript 中原本不可能的一些递归模式变 得实际。 TCO 允许一个函数在结尾处调用另外一个函数来执行,不需要任何额外资源。这意味着,对递归算法来说,引擎不再需要限制栈深度。
思维导图能比较清晰的还原整本书的知识结构体系,如果你还没用看过这本书,可以按照这个思维导图的思路快速预习一遍,提高学习效率。学习新事物总容易遗忘,我比较喜欢在看书的时候用思维导图做些记录,便于自己后期复习,如果你已经看过了这本书,也建议你收藏复习。如果你有神马建议或则想法,欢迎留言或加我微信交流:646321933,备注技术交流
概念:设备id 即设备指纹,用来表示用户设备的唯一性
最近在做用户行为分析项目的开发,需要采集用户的设备信息,需要用设备指纹来唯一表示用户操作设备。web 存储都和浏览器相关,我们无法通过js 来标识一台电脑,只能以浏览器作为设备维度来采集设备信息。即用户电脑中一个浏览器就是一个设备。
web 变量存储,我们第一时间想到的就是 cookie,sessionStorage,localStorage,但是这3种存储方式都和访问资源的域名相关。我们总不能每次访问一个网站就新建一个设备指纹吧,所以我们需要通过一个方法来跨域共享设备指纹
我们想到的方案是,通过嵌套 iframe 加载一个静态页面,在 iframe 上加载的域名上存储设备id,通过跨域共享变量获取设备id,共享变量的原理是采用了iframe 的 contentWindow通讯,通过 postMessage 获取事件状态,调用封装好的回调函数进行数据处理
SDK 采集端,调用方初始化的时候调用方法
collect.setIframe = function () {
var that = this
var iframe = document.createElement('iframe')
iframe.src = "http://localhost:82/"
iframe.style = 'display:none'
document.body.appendChild(iframe)
iframe.onload = function () {
iframe.contentWindow.postMessage('loaded','*');
}
//监听message事件
window.addEventListener("message", function(){
that.deviceId = event.data.deviceId
console.log('获取设备id',that.deviceId)
sessionStorage.setItem('PageSessionID',helper.upid())
helper.send(that.getParames(), that.eventUrl);
helper.sendDevice(that.getDevice(), that.deviceUrl);
}, false);
}
嵌套在 iframe 静态页面里的脚本
<script>
var getDeviceId = function() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
};
var hasCreated = false
var _deviceId = ''
if(document.cookie){
document.cookie.split(';').forEach(function(i){
if(i.indexOf('deviceId')>-1){
hasCreated = true
_deviceId = i.split('=')[1]
}
})
}
if(!_deviceId && (sessionStorage.getItem('deviceId')||localStorage.getItem('deviceId'))){
hasCreated = true
_deviceId = sessionStorage.getItem('deviceId')||localStorage.getItem('deviceId')
}
if(!hasCreated) {
_deviceId = getDeviceId()
document.cookie = 'deviceId=' + _deviceId
sessionStorage.setItem('deviceId',_deviceId)
localStorage.setItem('deviceId',_deviceId)
}
//回调函数
function receiveMessageFromIndex ( event ) {
console.log( 'receiveMessageFromIndex', event )
parent.postMessage( {deviceId: _deviceId}, '*');
}
//监听message事件
window.addEventListener("message", receiveMessageFromIndex, false);
</script>
跨浏览器cookie
跨浏览器指纹识别
浏览器指纹追踪
使用postMessage解决iframe跨域通信问题
window.postMessage
作为一个前端,如果能够深刻理解 HTTP 通信,能够让我们在日常开发工作中快速定位问题。所以我十分建议大家去读一下《图解HTTP》和《HTTP权威指南》。权威指南讲解的十分详细,内容也十分的全面,但是这本书的厚度也让很多同学望而却步。推荐大家从 《图解HTTP》 这本书开始学习,这本书虽然没有权威指南详细,但涵盖了很多我们日常开发需要的知识点,理解他,能让我们的开发效率事半功倍。
《图解HTTP》这本书对互联网基盘—— HTTP 协议进行了全面系统的介绍。作者从 HTTP 发展史开始,严谨地剖析了 HTTP 协议的结构,列举很多常见通信场景及实战案例,最后延伸到Web安全、最新技术动向等方面。通过书中大量生动形象的通信图例,我们能够更全面地理解 HTTP 通信过程中客户端与服务器之间的交互情况。在读这本书的过程中我收获颇多,并用思维导图的方式记录下来,方便自己后期温故。如果你刚好没有读过,可以当做度这本书的预习阅读,相信你也会有所收获(思维导图图片可能有点小,记得点开看)
这一章介绍了 Web 及网络协议的基础,http 通讯被拆分成四层,应用层,传输层,网络层和链路层,每层只要考虑分派给自己的任务,不需要弄清整个协议链路的细节。应用层使我们肉眼能看见的,我们常用的 http 协议和 FTP 协议就处在这一层,往下就是传输层,TCP/IP 协议工作的地方(我们的IP是IP地址,和这里的IP 协议有做区分),再往下是网络层,传输层建立连接之后,网络层负责将数据包的传输(数据包是网络传输的最小单位)。最后是链路层,用来连接网络配件的部分,举个栗子就是我们常说的网卡啊,光纤啊。在一个完整的数据传输中,客户端会按照应用层,传输层,网络层,链路层的顺序进行进行处理,每一层都会加一个首部,服务器端在接收的时候按照链路层,网路层,传输层,应用层的顺序去移除首部。
我们常说的3次握手就是 TCP 协议采用的策略,为了传输方便,TCP 将大数据分割成以报文为单位的数据包,IP 协议负责把数据包发送出去。在发送的过程中常常需要经过多个路由器的中转,这个时候会运用 ARP 协议来查找下一个路由器的地址。
通常用户的习惯会去访问域名而不是IP地址,将域名解析成对应的 IP 就需要用到 DNS 协议域名解析的服务。
日常开发中我们感知不到 http 协议的底层实现,我们所了解的 HTTP 协议总是由客户端发起,服务端接收。我们关注到的请求,常常是请求的URI,协议版本,头部信息,及内容实体,我们常使用的响应信息则包括了响应状态,响应内容。
我们常常会使用不通的 http 方法来执行不同的操作。我们常使用 GET 来获取资源,使用 POST 传输实体主题,使用 PUT 传输文件,使用 DELETE 删除文件,使用 OPTIONS 询问支持的方法(常常在跨域的场景中使用),使用TRACE 获取访问路径,使用 CONNECT 用隧道协议链接代理。
http 协议是一种无状态协议,不会去记录上一次访问状态,这使得当我们要做类似于登录这样的公能的时候,需要通过 cookie 来进行状态的管理。
通常HTTP报文(用于HTTP协议交互的信息)的结构包括,请求行,状态行,首部字段等,从 HTTP 的报文中,我们可以获得很多信息。在 MIME 扩展中会使用一种称为多部分对象集合的方法,来容纳多份不同的数据类型, 在 HTTP 报文中使用多部分对象时,需要在首部字段上加上 Content-type 。
通过设置首部字段,来达到获取部分内容范围请求(请求资源中断后,不需要重新开始请求),将传输内容编码的目的,来提高加载效率。
有的时候不同的场景需要我们获取不同的内容,就比如页面的中英文切换的功能,在HTTP请求中,我们通过设置 accept 类的请求头字段实现,也就是内容协商的方式,返回最合适的内容。协商方式分为,服务器驱动协商,客户端驱动协商,透明协商。
在HTTP通讯中,通过转态码,告知客户端的请求状态。状态码大致可以被分为 5 大类,1xx 表示接受的请求正在处理,2xx 表示请求正常处理完毕,3xx 表示需要进行附加操作以完成请求,4xx 表示客户端无法处理请求,5xx 表示服务器处理出错。
在这5大类请求中,我们常用的十几种状态码,需要我们重点去掌握,详细介绍看👆的思维导图。
通过使用虚拟机,可以实现单台主机多个域名的站点部署。在实际通讯中,我们常常使用,代理,网关,隧道协助请求转发,安全通信。
为了提高网站性能,我们会使用缓存方式来提高站点请求速率,通常是通过设置代理服务器缓存和客户端缓存来实现。需要注意的是,缓存都会设置一个过期时间,站点内容更新时,缓存内容也需要及时更新。
在请求中,HTTP 报文由请求方法,URI,HTTP版本,HTTP 首部字段等部分构成。在响应中,HTTP 的报文有 HTTP 版本,状态码,HTTP首部字段3部分构成。首部信息尤为重要,我们可以通过首部字段的设置来传递请求信息,类比于缓存控制,报文创建时间,是否压缩编码,是否支持跨域等。
HTTP 简单灵活的设置也造就了他的缺点。1. 通信使用明文,内容可能被窃听。2、不验证通讯方省份,因此有可能会遭遇伪装。3、无法证明报文的完整性,所有有可能被篡改。HTTPS 其实也不算是一个全新的协议,HTTPS = HTTP + 加密 + 认证 + 完整性保护。HTTPS 完善了 HTTP 的相关缺点,在 HTTP 的部分通信接口采用 SSL 和 TLS 协议替代,使用了数字证书认证机构和其他相关机关颁发的公开秘钥证书
HTTP 的认证方式通常是 BASIC认证(基本认证),DIGEST认证(摘要认证),SSL 客户端认证,FormBase 认证(基于表单认证)。BASIC 认证和 DIGEST 认证通过首部字段 Authorization 确认认证信息,SSL 借由 HTTPS 客户端证书完成认证方式。 表单认证则是依赖于 cookie 保存用户的登录状态。
SPDY 在TCP/IP 的应用层与运输层之间通过新加会话层的形式运作,使用 SPDY 使得 HTTP 协议的功能得到了扩展,实现了多路复用流,赋予请求优先级,压缩 HTTP 首部,推送功能,服务器提醒功能。
使用 WebSocket 进行全双工通信,只要建立了链接,客户端和服务器都能都主动得向对方发送信息。和 HTTP 相比,减少了每次建立连接的开销,减少了通信首部信息。
HTTP/2.0 改善了使用 web 时的速度体验。
web 应用开发语言的三剑客,HTML,CSS,javascript 。书出版的有点久所以书中没有提到,现在火爆的 web 构建技术,Vue,React 等流行前端框架。XML,JSON 数据发布语言,其中 JSON 比较常用,日常开发都离不了他。
在 Web 应用中,从浏览器那接收到的 HTTP 请求的全部内容,都可以在客户端自由的变更、篡改,在HTTP请求报文中加载攻击代码,就能发起对 Web 应用的攻击。Web 应用的攻击模式主要是主动攻击和被动攻击。
常见的攻击方式有,跨站脚本攻击,SQL 注入攻击,HTTP 首部注入攻击。这些攻击造成的影响见👆思维导图。通常简单的 HTTP 协议本身不会存在安全性的问题,协议本身也不会成为攻击对象,黑客利用的常常是因为设计和设置上的缺陷,会话管理疏忽等引发的安全漏洞进行攻击。
《图解HTTP》这本书读完啦,下一期我们来读读《HTTP权威指南》,我在 github 建了仓库放这些思维导图的原件,如果觉得图片不清晰,同学们可以去 github 上下载查阅。如果你也喜欢用思维导图的方式来记录读书笔记,也欢迎和我一同维护这个仓库,欢迎留言或则微信(646321933)与我交流
我们通常理解的 javascript 垃圾回收机制都停留在表面,"会释放不被引用变量内存",最近在读《深入浅出node.js》的书,详细了解了下 v8 垃圾回收的算法,记录了一些学习笔记。
V8的垃圾回收策略主要基于分代式垃圾回收机制,现代的垃圾回收算法中按对象的存活时间将内存的垃圾回收进行不同的分代,然后分别对不同分代的内存施以更高效的算法。在V8中,主要将内存分为新生代和老生代两代。新生代中的对象为存活时间较短的对象, 老生代中的对象为存活时间较长或常驻内存的对象。
在分代的基础上,新生代中的对象主要通过Scavenge算法进行垃圾回收,在Scavenge的具体 实现中,主要采用了Cheney算法
Cheney 算法是一种采用复制的方式实现的垃圾回收算法。它将堆内存一分为二,每一部分空间称为 semispace。在这两个 semispace 空间中,只有一个处于使用中,另一个处于闲置状态。处于使用状态的 semispace 空间称为 From 空间,处于闲置状态的空间称为 To 空间。当我们分配对象时,先是在 From 空间中进行分配。当开始进行垃圾回收时,会检查 From 空间中的存活对象,这 些存活对象将被复制到 To 空间中,而非存活对象占用的空间将会被释放。完成复制后,From 空 间和To空间的角色发生对换。 简而言之, 在垃圾回收的过程中, 就是通过将存活对象在两个 semispace 空间之间进行复制。
Scavenge算法通过牺牲空间换时间的算法非常适合生命周期短的新生代,但是,当一个对象经过多次复制,生命周期较长的时候或则To空间不足的时候,对象会被分配到进入到老生代中,需要采用新的算法进行垃圾回收。
Mark-Sweep 并不将内存空间划分为两半,所以不存在浪费一半空间的行为。与 Scavenge 复制活着的对象不同, Mark-Sweep 在标记阶段遍历堆中的所有对象,并标记活着的对象,在随后的清除阶段中,只清除没有被标记的对象。可以看出,Scavenge 中只复制活着的对象,而 Mark-Sweep 只清理死亡对象。
Mark-Sweep 在进行一次标记清除回收后,内存空间会出现不连续的状态。这种内存碎片会对后续的内存分配造成问题,因为很可能出现需要分配一个大对象的情况,这时所有的碎片空间都无法完成此次分配,就会提前触发垃圾回收,而这次回收是不必要的。Mark-Compact 对象在标记为死亡后,在整理的过程中,将活着的对象往一端移动,移动完成后,直接清理掉边界外的内存
为了避免出现 JavaScript 应用逻辑与垃圾回收器看到的不一致的情况,垃圾回收的 3 种基本算法都需要将应用逻辑暂停下来,待执行完垃圾回收后再恢复执行应用逻辑,这种行为被称为“全停顿",长时间的"全停顿"垃圾回收会让用户感受到明显的卡顿,带来体验的影响。以1.5 GB的垃圾回收堆内存为例,V8做一次小的垃圾回收需要50毫秒以上,做一次非增量式的垃圾回收甚至要1秒以上。这是垃圾回收中引起JavaScript线程暂停执行的时间,在 这样的时间花销下,应用的性能和响应能力都会直线下降。
为了降低全堆垃圾回收带来的停顿时间,V8先从标记阶段入手,将原本要一口气停顿完成的动作改为增量标记(incremental marking),也就是拆分为许多小“步进”,每做完一“步进” 就让 JavaScript 应用逻辑执行一小会儿,垃圾回收与应用逻辑交替执行直到标记阶段完成
《深入浅出Node.js》PDF
使用过 vue 进行项目开发的同学,一定知道或者使用过 vue-cli 脚手架,他能够很好的搭建项目结构和工程,让我们能够把足够的精力放在业务开发上。也正是因为这样,很多时候我们会因为项目工期短等原因来不及或则不会刻意去了解项目工程配置,我们今天不去介绍脚手架的使用,我们去了解下脚手架为我们创建好的打包工程是怎么做的。
├── build --------------------------------- webpack相关配置文件
│ ├── build.js --------------------------webpack打包配置文件
│ ├── check-versions.js ------------------------------ 检查npm,nodejs版本
│ ├── logo.png ---------------------------------- 项目 logo
│ ├── utils.js --------------------------------------- 配置资源路径,配置css加载器
│ ├── vue-loader.conf.js ----------------------------- 配置css加载器等
│ ├── webpack.base.conf.js --------------------------- webpack基本配置
│ ├── webpack.dev.conf.js ---------------------------- 用于开发的webpack设置
│ ├── webpack.prod.conf.js --------------------------- 用于打包的webpack设置
├── config ---------------------------------- 配置文件
├── index.js ------------------------------ 开发和生产环境配置文件
├── node_modules ---------------------------- 存放依赖的目录
├── src ------------------------------------- 源码
│ ├── assets ------------------------------ 静态文件
│ ├── components -------------------------- 组件
│ ├── main.js ----------------------------- 主js
│ ├── App.vue ----------------------------- 项目入口组件
│ ├── router ------------------------------ 路由
├── package.json ---------------------------- node配置文件
├── .babelrc--------------------------------- babel配置文件
├── .editorconfig---------------------------- 编辑器配置
├── .gitignore------------------------------- 配置git可忽略的文件
在看项目配置文件之前,我们先了解下 webpack 几个常用的工具和插件,如果你已经十分熟悉,你可以跳过这一小节,直接去看,配置文件解析
path 是 node.js 中的一个模块,用于处理目录的对象,提高开发效
常用方法:
path.join(): 用于连接路径。该方法的主要用途在于,会正确使用当前系统的路径分隔符,Unix 系统是 ”/“,Windows系统是 ”\“
path.resolve() 用于将相对路径转为绝对路径
常使用的文件路径
__dirname: 总是返回被执行的 js 所在文件夹的绝对路径
__filename: 总是返回被执行的 js 的绝对路径
process.cwd(): 总是返回运行 node 命令时所在的文件夹的绝对路径
process对象是Node的一个全局对象,提供当前Node进程的信息。
process 对象提供一系列属性,用于返回系统信息
process.argv:返回当前进程的命令行参数数组。
process.env:返回一个对象,成员为当前Shell的环境变量,比如process.env.HOME
process.pid:当前进程的进程号
简单说,Source map就是一个信息文件,里面储存着位置信息。也就是说,转换后的代码的每一个位置,所对应的转换前的位置。有了它,出错的时候,debug 工具将直接显示原始代码,而不是转换后的代码。这无疑给开发者带来了很大方便。webpack 的 devtool里有 7种 SourceMap 模式
模式 | 解释 |
---|---|
eval | 每个 module 会封装到 eval 里包裹起来执行,并且会在末尾追加注释 //@ sourceURL |
source-map | 生成一个 SourceMap 文件. |
hidden-source-map | 和 source-map 一样,但不会在 bundle 末尾追加注释. |
inline-source-map | 生成一个 DataUrl 形式的 SourceMap 文件. |
eval-source-map | 每个 module 会通过 eval() 来执行,并且生成一个 DataUrl 形式的 SourceMap . |
cheap-source-map | 生成一个没有列信息(column-mappings)的 SourceMaps 文件,不包含 loader 的 sourcemap(譬如 babel 的 sourcemap) |
cheap-module-source-map | 生成一个没有列信息(column-mappings)的 SourceMaps 文件,同时 loader 的 sourcemap 也被简化为只包含对应行的。 |
开发环境(development)和生产环境(production)的构建目标差异很大。在开发环境中,我们需要具有强大的、具有实时重新加载(live reloading)或热模块替换(hot module replacement)能力的 source map 和 localhost server。而在生产环境中,我们的目标则转向于关注更小的 bundle,更轻量的 source map,以及更优化的资源,以改善加载时间。由于要遵循逻辑分离,我们通常建议为每个环境编写彼此独立的 webpack 配置。通用的配置部分,我们抽象出一个公共文件,通过 webpack-merge 工具的“通用”配置,我们不必在环境特定的配置中重复代码。
ExtractTextWebpackPlugin 插件通常用来做样式文件的分离,被分离的文件不会被内嵌到 JS bundle 中,而会被放到一个单独的文件中,在样式文件比较大的时候,能够提前样式的加载,配置示例如下
const ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: "css-loader"
})
}]
},
plugins: [
new ExtractTextPlugin("styles.css"),
]
}
它会将所有的入口 chunk(entry chunks)中引用的 *.css,移动到独立分离的 CSS 文件。因此,你的样式将不再内嵌到 JS bundle 中,而是会放到一个单独的 CSS 文件(即 styles.css)当中。 如果你的样式文件大小较大,这会做更快提前加载,因为 CSS bundle 会跟 JS bundle 并行加载。
如果你有多个 webpack 入口点, 他们都会在生成的HTML文件中的 script 标签内。如果你有任何 CSS assets 在 webpack 的输出中(例如, 利用ExtractTextPlugin提取CSS), 那么这些将被包含在HTML head中的标签内。通常在开发中,我们为了避免 CDN 和浏览器的缓存通常会个输出文件 bundle.js 加上一个hash 值例如 [hash].bundle.js
,使用 html-webpack-plugin 能够在创建新的 html 文件的时候将我们把带有哈希值的 bundle.js 引用到 html 文件.
用来优化从脚本里提炼出来的 css ,配置示例如下
var OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = {
module: {
rules: [
{
test: /\.css$/,
loader: ExtractTextPlugin.extract('style-loader', 'css-loader')
}
]
},
plugins: [
new ExtractTextPlugin('styles.css'),
new OptimizeCssAssetsPlugin({
assetNameRegExp: /\.optimize\.css$/g,
cssProcessor: require('cssnano'),
cssProcessorOptions: { discardComments: { removeAll: true } },
canPrint: true
})
]
};
CopyWebpackPlugin从插件名称上我们不难看出他的作用,通常用来拷贝资源,对项目文件进行归类整合
friendly-errors-webpack-plugin能够更好在终端看到webapck运行的警告和错误,提高开发体验
UglifyjsWebpackPlugin用来压缩 js 代码
webpack 项目服务,我们通常会在开发阶段用来配置项目的热刷新,服务压缩,项目代理等,常用的几个配置参数介绍如下
const config = require('../config')
// config 文件里做了用户自定的服务参数配置
devServer: {
clientLogLevel: 'warning', // 在开发攻击的控制台中显示信息,便于开发调试,你可以将参数配置成 "none" 来进行关闭
historyApiFallback: { // 当使用 HTML5 History API 时,任意的 404 响应都可能需要被替代为 index.html
rewrites: [
{ from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
],
},
hot: true, //启用项目的热刷新,即模块热替换特性
contentBase: false, // 告诉服务器从哪里提供内容。只有在你想要提供静态文件时才需要。这里禁用,因为配置了 CopyWebpackPlugin 的使用
compress: true,
host: HOST || config.dev.host, //指定使用一个域名。默认是 localhost
port: PORT || config.dev.port, //指定要监听请求的端口号:
open: config.dev.autoOpenBrowser, //open 参数配置,如果配置成 true ,项目启动后会自动打开浏览器
overlay: config.dev.errorOverlay //当有错误或则警告的时候在页面上显示一个全屏的遮罩提示
? { warnings: false, errors: true }
: false,
publicPath: config.dev.assetsPublicPath, //此路径下的打包文件可在浏览器中访问
proxy: config.dev.proxyTable, //代理API的请求
quiet: true, //启用 quiet 后,除了初始启动信息之外的任何内容都不会被打印到控制台,特别是使用了 FriendlyErrorsPlugin 插件的时候
watchOptions: { //与监视文件相关的控制选项。是否使用轮询
poll: config.dev.poll,
}
},
通过了解了上面的配置,我们应该对 webpack 的常用插件和工具有了一定了解,我们来看下 vue-cli 脚手架给我们生成的配置情况
'use strict'
const path = require('path') // 引用项目的 path 模块
module.exports = {
dev: {
// 路径配置
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {},
// 各种开发服务配置
host: 'localhost', // 开发环境域名 可以被 node 全局变量process.env.HOST 重写
port: 8080, //配置开发服务端口,可以被 node 全局变量 process.env.PORT 重写, 需要使用未被占用的端口
autoOpenBrowser: false, //服务启动是否自动代开浏览器
errorOverlay: true, //是否在发生错误的时候,在页面整屏增加一个错误遮罩
notifyOnErrors: true, //是否通知错误 ,在我们的项目配置中和 friendly-errors-webpack-plugin 结合使用
poll: false, // 服务监听是否轮询操作
// 配饰是否使用 Eslint Loader 进行语法检测
// 如果使用,在开发构建阶段,会对你的代码会进行检测
// 检测出来的警告和错误会白展示在开发工具的控制台
useEslint: true, //进行语法检测
// 配置是否将 eslint 语法检测的警告和错误展示在页面整屏的遮罩上
showEslintErrorsInOverlay: false, // 语法检测的警告和错误不展示在遮罩上
/**
* Source Maps
*/
// https://webpack.js.org/configuration/devtool/#development
// 在上面的介绍中,我们知道 source map 是用来将我们构建后被转化的代码对应构建前的代码,便于 debug
// cheap-module-eval-source-map 和我们介绍的 cheap-module-source-map 很类似,但是 SourceMap 会被作为数据添加到包中
devtool: 'cheap-module-eval-source-map',
// 如果你的开发工具不能进行 vue-files 的 debug ,可以将以下设置设置成 false
cacheBusting: true,
cssSourceMap: true
},
build: {
// index.html 文件模板
index: path.resolve(__dirname, '../dist/index.html'),
// 打包路径配置
assetsRoot: path.resolve(__dirname, '../dist'),
assetsSubDirectory: 'static',
assetsPublicPath: '/',
/**
* Source Maps
*/
//生产环境 source map 配置
productionSourceMap: true,
devtool: '#source-map',
// 因为很多的主流服务都会 通过 gzip 压缩过你的所有静态资源,我们的配置默认不开启 gzip
// 如果要设置成开启,请先确保已经安装好 compression-webpack-plugin 插件
productionGzip: false,
productionGzipExtensions: ['js', 'css'],
// 启动 build 命令的时候,额外添加一个参数,打包后会自动生成一个分析报告文件,例如 npm run build --report ,可以通过配置 true ,false 来关闭
bundleAnalyzerReport: process.env.npm_config_report
}
}
这个文件主要是用来检测当前环境中的node和npm版本和我们需要的是否一致的。
'use strict'
const chalk = require('chalk') // 改变命令行中的字体颜色,大致这样用chalk.blue('Hello world')
const semver = require('semver') //是用来对特定的版本号做判断的
const packageConfig = require('../package.json') // 项目 npm 配置文件,获取依赖及版本信息,requrie返回的就是json对象
const shell = require('shelljs') //用来执行Unix系统命令,调用系统命令更加方便
//把cmd这个参数传递的值转化成前后没有空格的字符串,也就是版本号
function exec (cmd) {
return require('child_process').execSync(cmd).toString().trim()
}
const versionRequirements = [
{
name: 'node',
currentVersion: semver.clean(process.version), // 提取进程版本信息转化成规定格式,也就是 ' =v1.2.3 ' -> '1.2.3' 这种功能
versionRequirement: packageConfig.engines.node // package.json 的 node 的版本信息
}
]
if (shell.which('npm')) {
versionRequirements.push({
name: 'npm',
currentVersion: exec('npm --version'), //当前的版本信息
versionRequirement: packageConfig.engines.npm //package.json 的 node 的版本信息
})
}
module.exports = function () {
const warnings = []
for (let i = 0; i < versionRequirements.length; i++) {
const mod = versionRequirements[i]
// 如果当前版本号不符合 package.json 要求的版本号,红色表示当前版本信息,绿色表示要求的版本信息,添加到 warnings 待输出
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
warnings.push(mod.name + ': ' +
chalk.red(mod.currentVersion) + ' should be ' +
chalk.green(mod.versionRequirement)
)
}
}
//输出版本号不相符的提示 warnings
if (warnings.length) {
console.log('')
console.log(chalk.yellow('To use this template, you must update following to modules:'))
console.log()
for (let i = 0; i < warnings.length; i++) {
const warning = warnings[i]
console.log(' ' + warning)
}
console.log()
process.exit(1)
}
}
'use strict'
//打包前判断当先开发环境的 node 和 npm 版本和 package.json 要求的时候一样
require('./check-versions')()
process.env.NODE_ENV = 'production'
const ora = require('ora') // 在用户打包的时候能够让用户知道正在进行,一个加载中的样式,转啊转
const rm = require('rimraf') //这个模块是用来清除之前的打的包,因为在vue-cli中每次打包会生成不同的hash
const path = require('path') //node 路径模块,便于我们操作文件路径
const chalk = require('chalk') //带颜色的输出模块,能在控制台中输出不同的样色
const webpack = require('webpack') //webpack 不解释
const config = require('../config') // 项目中的配置文件,👆上面已经进行了配置介绍
const webpackConfig = require('./webpack.prod.conf') // 生产环境的配置文件
const spinner = ora('building for production...')// 实例一个打包加载中实例
spinner.start() //开始转圈,营造一个正在打包的场景
// 删除上一次打包的文件,删除成功,开始按照生产环境配置进行打包
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) throw err
//开始打包,打包结束停止 spinner 转圈,有报错则在控制台输出
webpack(webpackConfig, (err, stats) => {
spinner.stop()
if (err) throw err
// node 环境里的输出配置,process.stdout.write 你可以理解成 js 里的 console
process.stdout.write(stats.toString({
colors: true, //让打包的时候有颜色。
modules: false, //去掉内置模块信息
children: false, // 去掉子模块,如果你使用了 ts-loader,设置成 true 会在打包构建阶段展示错误信息
chunks: false, // 增加包信息(设置为 false 能允许较少的冗长输出)
chunkModules: false //去除包里内置模块的信息
}) + '\n\n')
//打包出错在控制台输出 Build failed with errors ,退出打包程序
if (stats.hasErrors()) {
console.log(chalk.red(' Build failed with errors.\n'))
process.exit(1)
}
//打包成功则输出 Build complete 结束打包
console.log(chalk.cyan(' Build complete.\n'))
console.log(chalk.yellow(
' Tip: built files are meant to be served over an HTTP server.\n' +
' Opening index.html over file:// won\'t work.\n'
))
})
})
'use strict'
const path = require('path') // node 路径模块
const utils = require('./utils') //node 内部常用的工具类,其中包括:格式化字符串、对象的序列化、实现对象继承等常用方法
const config = require('../config') //👆上面我们介绍的,项目配置文件
const vueLoaderConfig = require('./vue-loader.conf') //👆 上面我们介绍的 vue 加载器配置文件
//返回当前配置文件位置是 build ,该方法放回 build/../dir 的相对路基
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
// eslint 语法检测配置
const createLintingRule = () => ({
test: /\.(js|vue)$/,
loader: 'eslint-loader',
enforce: 'pre',
include: [resolve('src'), resolve('test')],
options: {
formatter: require('eslint-friendly-formatter'),
emitWarning: !config.dev.showEslintErrorsInOverlay
}
})
// webpack 通用配置内容
module.exports = {
context: path.resolve(__dirname, '../'), // 上下文,基础目录,用于从配置中解析入口起点和 loader
entry: {
app: './src/main.js' //起点或是应用程序的起点入口。从这个起点开始,应用程序启动执行。如果传递一个数组,那么数组的每一项都会执行。
},
output: {
path: config.build.assetsRoot, //输出 bundle 的路径
filename: '[name].js', //输出 bundle 的名称
publicPath: process.env.NODE_ENV === 'production' // 指定资源文件引用的目录,例如图片
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
},
resolve: {
extensions: ['.js', '.vue', '.json'], //配置模块如何解析,
alias: { // 创建应用的别名,
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
}
},
module: {
rules: [
//判断配置中是否要是用 eslint 语法检测,如果使用,就将 createLintingRule 配置对象返回
...(config.dev.useEslint ? [createLintingRule()] : []),
//👇是一些比较常用的加载器,及配置,不做详细介绍了
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
},
{
test: /\.(css | scss)$/,
loader: 'style-loader!css-loader!!sass-loader'
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('media/[name].[hash:7].[ext]')
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}
]
},
node: {
//防止因为 vue 资源本身就自带的 无用的 node 注入,浏览器兼容处理
setImmediate: false,
dgram: 'empty',
fs: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty'
}
}
'use strict'
const utils = require('./utils') //node 工具模块
const webpack = require('webpack') //webpack 不解释
const config = require('../config')//👆提到的配置文件
const merge = require('webpack-merge') // merge 工具,用来合并生产和开发环境通用的基础 webpack 配置
const path = require('path') //node 的路径模块
const baseWebpackConfig = require('./webpack.base.conf') //生产和开发环境通用的基础 webpack 配置
const CopyWebpackPlugin = require('copy-webpack-plugin') //拷贝插件
const HtmlWebpackPlugin = require('html-webpack-plugin') //动态生成 html 插件
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') //友好的错误输出插件
const portfinder = require('portfinder') //能够获取一个可用的随机端口号
const HOST = process.env.HOST //node 全局环境变量的主机
const PORT = process.env.PORT && Number(process.env.PORT) //node 全局环境变量的端口
//合并基础配置加载器的配置部分
const devWebpackConfig = merge(baseWebpackConfig, {
module: {
// 为 .vue 文件意外的独立样式文件配置加载器
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
},
// cheap-module-eval-source-map 在开发环境中很快
devtool: config.dev.devtool,
// 开发服务配置,👆 已经细讲过,顺便回顾一下
devServer: {
clientLogLevel: 'warning', // 在开发攻击的控制台中显示信息,便于开发调试,你可以将参数配置成 "none" 来进行关闭
historyApiFallback: { // 当使用 HTML5 History API 时,任意的 404 响应都可能需要被替代为 index.html
rewrites: [
{ from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
],
},
hot: true, //启用项目的热刷新,即模块热替换特性
contentBase: false, // 告诉服务器从哪里提供内容。只有在你想要提供静态文件时才需要。这里禁用,因为配置了 CopyWebpackPlugin 的使用
compress: true,
host: HOST || config.dev.host, //指定使用一个域名。默认是 localhost
port: PORT || config.dev.port, //指定要监听请求的端口号:
open: config.dev.autoOpenBrowser, //open 参数配置,如果配置成 true ,项目启动后会自动打开浏览器
overlay: config.dev.errorOverlay //当有错误或则警告的时候在页面上显示一个全屏的遮罩提示
? { warnings: false, errors: true }
: false,
publicPath: config.dev.assetsPublicPath, //此路径下的打包文件可在浏览器中访问
proxy: config.dev.proxyTable, //代理API的请求
quiet: true, //启用 quiet 后,除了初始启动信息之外的任何内容都不会被打印到控制台,特别是使用了 FriendlyErrorsPlugin 插件的时候
watchOptions: { //与监视文件相关的控制选项。是否使用轮询
poll: config.dev.poll,
}
},
plugins: [
// DefinePlugin 允许创建一个在编译时可以配置的全局常量。这可能会对开发模式和发布模式的构建允许不同的行为非常有用
new webpack.DefinePlugin({
'process.env': require('../config/dev.env')
}),
new webpack.HotModuleReplacementPlugin(), //启用热替换模块(Hot Module Replacement),也被称为 HMR
new webpack.NamedModulesPlugin(), // 当开启 HMR 的时候使用该插件会显示模块的相对路径,建议用于开发环境
new webpack.NoEmitOnErrorsPlugin(), 在编译出现错误时,使用 NoEmitOnErrorsPlugin 来跳过输出阶段
//HtmlWebpackPlugin简化了HTML文件的创建,以便为你的webpack包提供服务。这对于在文件名中包含每次会随着编译而发生变化哈希的 webpack bundle 尤其有用
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true
}),
// 拷贝自定义的静态资源文件
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.dev.assetsSubDirectory,
ignore: ['.*']
}
])
]
})
// 实例一个异步对象,执行 devWebpackConfig 配置编译
module.exports = new Promise((resolve, reject) => {
portfinder.basePort = process.env.PORT || config.dev.port //设置基础端口
portfinder.getPort((err, port) => {获取端口,输出构建新
if (err) {
reject(err)
} else {
// 如果进行 e2e 测试,需要发布新端口
process.env.PORT = port
// 更新 devServer 的端口
devWebpackConfig.devServer.port = port
devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
compilationSuccessInfo: {
messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
},
onErrors: config.dev.notifyOnErrors
? utils.createNotifierCallback()
: undefined
}))
//执行打包配置文件
resolve(devWebpackConfig)
}
})
})
'use strict'
const path = require('path') // node 路径模块
const utils = require('./utils') //小工具函数
const webpack = require('webpack') // webpack 不解释
const config = require('../config')//👆提到的配置文件
const merge = require('webpack-merge') // merge 工具,用来合并生产和开发环境通用的基础 webpack 配置
const baseWebpackConfig = require('./webpack.base.conf')//产和开发环境通用的基础 webpack 配置
const CopyWebpackPlugin = require('copy-webpack-plugin') //拷贝插件
const HtmlWebpackPlugin = require('html-webpack-plugin') //动态生成 html 插件
const ExtractTextPlugin = require('extract-text-webpack-plugin')//用来做文件分离的插件
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')//优化提炼出来的css
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')// 压缩 js 文件插件
//生产环境配置
const env = require('../config/prod.env')
//合并基础配置加载器的配置部分
const webpackConfig = merge(baseWebpackConfig, {
//为独立分离出来的样式配置加载器和source,map
module: {
rules: utils.styleLoaders({
sourceMap: config.build.productionSourceMap,
extract: true,
usePostCSS: true
})
},
//配置线上的 source map 便于排查问题
devtool: config.build.productionSourceMap ? config.build.devtool : false,
//配置输出,路径,文件名
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
},
plugins: [
// DefinePlugin 允许创建一个在编译时可以配置的全局常量。这可能会对开发模式和发布模式的构建允许不同的行为非常有用
new webpack.DefinePlugin({
'process.env': env
}),
// 使用 UglifyJsPlugin 插件对 js 进行压缩
new UglifyJsPlugin({
uglifyOptions: {
compress: {
warnings: false
}
},
//配置插件的source map
sourceMap: config.build.productionSourceMap,
parallel: true
}),
// 提取 css 到单独的文件,分离文件异步加载,提高加载速度
new ExtractTextPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css'),
//如果把 allChunks 参数设置陈 false ,就不会把css 从代码块中分离出来
//代码块加载的时候 css 会被 styles-loader 动态的加载
allChunks: true,
}),
//使用这个插件,从不同的组件中复制脱离出来,进行 css 压缩
new OptimizeCSSPlugin({
cssProcessorOptions: config.build.productionSourceMap
? { safe: true, map: { inline: false } }
: { safe: true }
}),
//自动生成 html 文件,通常 index.html 文件都会带一个哈希值来清除缓存
new HtmlWebpackPlugin({
filename: config.build.index,
template: 'index.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
},
chunksSortMode: 'dependency'
}),
//该插件会根据模块的相对路径生成一个四位数的hash作为模块id, 渲染模块没有变化的时候,id 不会变。
new webpack.HashedModuleIdsPlugin(),
// 提升或者预编译所有模块到一个闭包中,提升你的代码在浏览器中的执行速度。
new webpack.optimize.ModuleConcatenationPlugin(),
// 分离渲染的js 到独立的文件中
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks (module) {
//被引用到的包会从 node_modules 中提取出来
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
)
}
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
minChunks: Infinity
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'app',
async: 'vendor-async',
children: true,
minChunks: 3
}),
// 拷贝自定义的静态资源文件
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.build.assetsSubDirectory,
ignore: ['.*']
}
])
]
})
//判断如果配置了生产环境压缩,是则使用插件进行压缩
if (config.build.productionGzip) {
const CompressionWebpackPlugin = require('compression-webpack-plugin')
webpackConfig.plugins.push(
new CompressionWebpackPlugin({
asset: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp(
'\\.(' +
config.build.productionGzipExtensions.join('|') +
')$'
),
threshold: 10240,
minRatio: 0.8
})
)
}
//是否要生成代码打包分析报告
if (config.build.bundleAnalyzerReport) {
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}
module.exports = webpackConfig
👆这篇文章详细的介绍了脚手架项目的 webpack 配置,但是只是 webpack 的一部分,还有很多内容值得我们去探究,如果你还感兴趣,可以阅读下面这些文章。也欢迎随时与我进行交流,微信号:646321933
可以添加评论留言这些功能吗?
最近开发的埋点项目,需要记录用户行为轨迹即用户页面访问顺序。需要在页面跳转的时候,记录用户访问的信息(比如 url ,请求头部等),非单页面应用可以给 window 对象加上一个 beforeunload 事件,在页面离开时触发采集开关,但是现在很多业务是单页面应用,用户切换地址的时候,是无刷新的局部更新,没有办法触发 beforeunload。所以单页面应用的路由插件一定运用了 window 自带的,无刷新修改用户浏览记录的方法,pushState 和 replaceState。
history 提供了两个方法,能够无刷新的修改用户的浏览记录,pushSate,和 replaceState,区别的 pushState 在用户访问页面后面添加一个访问记录, replaceState 则是直接替换了当前访问记录
history 对象的详细信息已经有很多很好很详细的介绍文献,这里不再做总结,我们引用阮老师的教程介绍,history对象 -- JavaScript 标准参考教程(alpha)
history.pushState方法接受三个参数,依次为:
state:一个与指定网址相关的状态对象,popstate事件触发时,该对象会传入回调函数。如果不需要这个对象,此处可以填null。
title:新页面的标题,但是所有浏览器目前都忽略这个值,因此这里可以填null。
url:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址。
假定当前网址是example.com/1.html,我们使用pushState方法在浏览记录(history对象)中添加一个新记录。
var stateObj = { foo: 'bar' };
history.pushState(stateObj, 'page 2', '2.html');
添加上面这个新记录后,浏览器地址栏立刻显示 example.com/2.html,但并不会跳转到 2.html,甚至也不会检查2.html 是否存在,它只是成为浏览历史中的最新记录。这时,你在地址栏输入一个新的地址(比如访问 google.com ),然后点击了倒退按钮,页面的 URL 将显示 2.html;你再点击一次倒退按钮,URL 将显示 1.html。
总之,pushState 方法不会触发页面刷新,只是导致 history 对象发生变化,地址栏会有反应。
如果 pushState 的 url参数,设置了一个新的锚点值(即hash),并不会触发 hashchange 事件。如果设置了一个跨域网址,则会报错。
// 报错
history.pushState(null, null, 'https://twitter.com/hello');
上面代码中,pushState想要插入一个跨域的网址,导致报错。这样设计的目的是,防止恶意代码让用户以为他们是在另一个网站上。
history.replaceState 方法的参数与 pushState 方法一模一样,区别是它修改浏览历史中当前纪录,假定当前网页是 example.com/example.html。
history.pushState({page: 1}, 'title 1', '?page=1');
history.pushState({page: 2}, 'title 2', '?page=2');
history.replaceState({page: 3}, 'title 3', '?page=3');
history.back()
// url显示为http://example.com/example.html?page=1
history.back()
// url显示为http://example.com/example.html
history.go(2)
// url显示为http://example.com/example.html?page=3
开发过单页面应用的同学,一定比较清楚,单页面应用的路由切换是无感知的,不会重新进行 http 请求去获取页面,而是通过改变页面渲染视图来实现。所以他的实现原理一定也是通过原生的 pushState 或则 replaceState 来实现的。所以在页面跳转的时候一定会调用 pushState 或则 replaceState ,要记录用户的跳转信息,我们只要拦截 pushState 和 replaceState,在执行默行为前先执行我们的方法就能够采集到用户的跳转信息了
// 改写思路:拷贝 window 默认的 replaceState 函数,重写 history.replaceState 在方法里插入我们的采集行为,在重写的 replaceState 方法最后调用,window 默认的 replaceState 方法
collect = {}
collect.onPushStateCallback : function(){} // 自定义的采集方法
(function(history){
var replaceState = history.replaceState; // 存储原生 replaceState
history.replaceState = function(state, param) { // 改写 replaceState
var url = arguments[2];
if (typeof collect.onPushStateCallback == "function") {
collect.onPushStateCallback({state: state, param: param, url: url}); //自定义的采集行为方法
}
return replaceState.apply(history, arguments); // 调用原生的 replaceState
};
})(window.history);
既然知道了这个原理,我们来看下 vue-router 的实现,我们打开 vue-router 项目地址,把项目克隆下来,或则直接在 github 上预览,在 Vue 开发的项目里,我们通过 router.push('home') 来实现页面的跳转,所以我们检索下,push 方法的实现
我们检索到了 20 个 js 文件,😂,一般到这个时候,我们会放弃源码阅读,那么我们今天的文章就到这结束,谢谢大家!
开个玩笑,源码阅读不能这么粗糙,我们找到 src 目录,点开 index.js 文件,看到 history对象的定义和 mode 参数有关
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${mode}`)
}
}
看到 history 对象的实例与配置的 mode 有关,vue-router 通过3中方式实现了路由切换。与我们今天讲的内容相匹配的是 HTML5History 的实现方案,其他的将不再文章中做扩展,若果你感兴趣想要了解,可以看文章后面的扩展阅读
我们来看 vue-router 中的 HTML5History 源码:
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
pushState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
replaceState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
// src/util/push-state.js
export function pushState (url?: string, replace?: boolean) {
saveScrollPosition()
// try...catch the pushState call to get around Safari
// DOM Exception 18 where it limits to 100 pushState calls
const history = window.history
try {
if (replace) {
history.replaceState({ key: _key }, '', url)
} else {
_key = genKey()
history.pushState({ key: _key }, '', url)
}
} catch (e) {
window.location[replace ? 'replace' : 'assign'](url)
}
}
export function replaceState (url?: string) {
pushState(url, true)
}
在使用 Vue 开发的过程中,我们一定用到过 push 和 replace 来改变路由,和视图。
router 实例调用的 push 实际是 history 的方法,通过 mode 来确定匹配 history 的实现方案,从代码中我们看到,push 调用了 src/util/push-state.js 中被改写过的 pushState 的方法,改写过的方法会根据传入的参数 replace?: boolean 来进行判断调用 pushState 还是 replaceState ,同时做了错误捕获,如果,history 无刷新修改访问路径失败,则调用 window.location.replace(url)
,有刷新的切换用户访问地址 ,同理 pushState 也是这样。这里的 transitionTo 方法主要的作用是做视图的跟新及路由跳转监测,如果 url 没有变化(访问地址切换失败的情况),在 transitionTo 方法内部还会调用一个 ensureURL 方法,来修改 url。 transitionTo 方法中应用的父方法比较多,这里不做长篇赘述,具体代码分析可以关注后我以后的文章
通过上面的学习,我们知道了,单页面应用路由的实现原理,我们也尝试去实现一个。在做管理系统的时候,我们通常会在页面的左侧放置一个固定的导航 sidebar,页面的右侧放与之匹配的内容 main 。点击导航时,我们只希望内容进行更新,如果刷新了整个页面,到时导航和通用的头部底部也进行重绘重排的话,十分浪费资源,体验也会不好。这个时候,我们就能用到我们今天学习到的内容,通过使用 HTML5 的 pushState 方法和 replaceState 方法来实现,
思路:首先绑定 click 事件。当用户点击一个链接时,通过 preventDefault 函数防止默认的行为(页面跳转),同时读取链接的地址(如果有 jQuery,可以写成$(this).attr('href')),把这个地址通过pushState塞入浏览器历史记录中,再利用 AJAX 技术拉取(如果有 jQuery,可以使用$.get方法)这个地址中真正的内容,同时替换当前网页的内容。
为了处理用户前进、后退,我们监听 popstate 事件。当用户点击前进或后退按钮时,浏览器地址自动被转换成相应的地址,同时popstate事件发生。在事件处理函数中,我们根据当前的地址抓取相应的内容,然后利用 AJAX 拉取这个地址的真正内容,呈现,即可。
最后,整个过程是不会改变页面标题的,可以通过直接对 document.title 赋值来更改页面标题。
好了,我们今天通过多个方面来讲了 pushState 方法和 replaceState 的应用,你应该对这个两个方法能有一个比较深刻的印象,如果想要了解更多,你可以参考以下链接
埋点,是网站分析的一种常用的数据采集方法。我们主要用来采集用户行为数据(例如页面访问路径,点击了什么元素)进行数据分析,从而让运营同学更加合理的安排运营计划。现在市面上有很多第三方埋点服务商,百度统计,友盟,growingIO 等大家应该都不太陌生,大多情况下大家都只是使用,最近我研究了下 web 埋点,你要不要了解下。
用户行为分析是一个大系统,一个典型的数据平台。由用户数据采集,用户行为建模分析,可视化报表展示几个模块构成。现有的埋点采集方案可以大致被分为三种,手动埋点,可视化埋点,无埋点
我们暂时放弃可视化埋点的实现,在 手动埋点
和 无埋点
上进行了尝试,为了便于描述,下文我会称采集脚本为 SDK。
埋点开发需要考虑很多内容,贯穿着不轻易动手写代码的原则,我们在开发前先思考下面这几个问题
第一期我们先实现对 PV(即页面浏览量或点击量) 、UV(一天内同个访客多次访问) 、点击量、用户的访问路径的基础指标的采集。精细化分析的流量转化需要和业务相关,需要和数据分析方做约定,我们预留扩展。所以我们的采集接口需要进行以下的约定
{
"header":{ // HTTP 头部
"X-Device-Id":" 550e8400-e29b-41d4-a716-446655440000", //设备ID,用来区分用户设备
"X-Source-Url":"https://www.baidu.com/", //源地址,关联用户的整个操作流程,用于用户行为路径分析,例如登录,到首页,进入商品详情,退出这一整个完整的路径
"X-Current-Url":"", //当前地址,用户行为发生的页面
"X-User-Id":"",//用户ID,统计登录用户行为
},
"body":[{ // HTTP Body体
"PageSessionID":"", //页面标识ID,用来区分页面事件,例如加载和离开我们会发两个事件,这个标识可以让我们知道这个事件是发生在一个页面上
"Event":"loaded", //事件类型,区分用户行为事件
"PageTitle": "埋点测试页", //页面标题,直观看到用户访问页面
"CurrentTime": “1517798922201”, //事件发生的时间
"ExtraInfo": {
} //扩展字段,对具体业务分析的传参
}]
}
以上就是我们现在约定好了的通用的事件采集的接口,所传的参数基本上会根据采集事件的不同而发生变化。但是在用户的整一个访问行为中,用户的设备是不会变化的,如果你想采集设备信息可以重新约定一个接口,在整个采集开始之前发送设备信息,这样可以避免在事件采集接口上重复采集固定数据。
{
"header":{ // HTTP 头部
"X-Device-Id" :"550e8400-e29b-41d4-a716-446655440000" , // 设备id
},
"body":{ // HTTP Body体
"DeviceType": "web" , //设备类型
"ScreenWide" : 768 , // 屏幕宽
"ScreenHigh": 1366 , // 屏幕高
"Language": "zh-cn" //语言
}
}
埋点应该让调用的业务方,尽可能少有工作量,最好是什么都不用做,😁,但是实现起来有点难额。我们采用的方案是让业务方在代码里通过 script 脚本来引用我们的 SDK ,业务方只要配置一些需要的参数进行埋点定制(👆我们讲到过的无埋点的流量控制),然后什么都不做就可以进行基础数据的采集。
(function() {
var collect = document.createElement('script');
collect.type = 'text/javascript';
collect.async = true;
collect.src = 'http://collect.trc.com/index.js';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(collect, s);
})();
//用户自定义要进行无埋点采集的元素,如果不进行无埋点采集,可以不配置
var _XT = [];
_XT.push(['Target','div']);
如果业务方需要采集更多业务定制的数据,可以调用我们暴露出的方法进行采集
//自定义事件
sdk.dispatch('customEvent',{extraInfo:'自定义事件的额外信息'})
我们使用 userId 来做用户标识,同一个设备的用户,从游客用户切换到登录用户,如果我们要把他们关联起来,需要有一个设备Id 做关联
用户通过浏览器来访问 web 页面,设备Id需要存储在浏览器上,同一个用户访问不同的业务方网站,设备Id要保持一样。web 变量存储,我们第一时间想到的就是 cookie,sessionStorage,localStorage,但是这3种存储方式都和访问资源的域名相关。我们总不能每次访问一个网站就新建一个设备指纹吧,所以我们需要通过一个方法来跨域共享设备指纹
我们想到的方案是,通过嵌套 iframe 加载一个静态页面,在 iframe 上加载的域名上存储设备id,通过跨域共享变量获取设备id,共享变量的原理是采用了iframe 的 contentWindow通讯,通过 postMessage 获取事件状态,调用封装好的回调函数进行数据处理具体的实现方式
//web 应用,通过嵌入 iframe 进行跨域 cookie 通讯,设置设备id,
collect.setIframe = function () {
var that = this
var iframe = document.createElement('iframe')
iframe.id = "frame",
iframe.src = 'http://collectiframe.trc.com' // 配置域名代理,目的是让开发测试生产环境代码一致
iframe.style.display='none' //iframe 设置的目的是用来生成固定的设备id,不展示
document.body.appendChild(iframe)
iframe.onload = function () {
iframe.contentWindow.postMessage('loaded','*');
}
//监听message事件,iframe 加载完成,获取设备id ,进行相关的数据采集
helper.on(window,"message",function(event){
that.deviceId = event.data.deviceId
if(event.data && event.data.type == 'loaded'){
that.sendDevice(that.getDevice(), that.deviceUrl);
setTimeout(function () {
that.send(that.beforeload)
that.send(that.loaded)
},1000)
}
})
}
iframe 与 SDK 通讯
function receiveMessageFromIndex ( event ) {
getDeviceInfo() // 获取设备信息
var data = {
deviceId: _deviceId,
type:event.data
}
event.source.postMessage(data, '*'); // 将设备信息发送给 SDK
}
//监听message事件
if(window.addEventListener){
window.addEventListener("message", receiveMessageFromIndex, false);
}else{
window.attachEvent("onmessage", receiveMessageFromIndex, false)
如果你想知道可以看我的另一篇博客 web 浏览器指纹跨域共享
我们知道单页面应用都是无刷新的页面加载,所以我们在页面
跳转
的处理和我们的普通的页面会有所不同。单页面应用的路由插件运用了 window 自带的无刷新修改用户浏览记录的方法,pushState 和 replaceState。
window 的 history 对象 提供了两个方法,能够无刷新的修改用户的浏览记录,pushSate,和 replaceState,区别的 pushState 在用户访问页面后面添加一个访问记录, replaceState 则是直接替换了当前访问记录,所以我们只要改写 history 的方法,在方法执行前执行我们的采集方法就能实现对单页面应用的页面跳转事件的采集了
// 改写思路:拷贝 window 默认的 replaceState 函数,重写 history.replaceState 在方法里插入我们的采集行为,在重写的 replaceState 方法最后调用,window 默认的 replaceState 方法
collect = {}
collect.onPushStateCallback : function(){} // 自定义的采集方法
(function(history){
var replaceState = history.replaceState; // 存储原生 replaceState
history.replaceState = function(state, param) { // 改写 replaceState
var url = arguments[2];
if (typeof collect.onPushStateCallback == "function") {
collect.onPushStateCallback({state: state, param: param, url: url}); //自定义的采集行为方法
}
return replaceState.apply(history, arguments); // 调用原生的 replaceState
};
})(window.history);
这块介绍起来也比较的复杂,如果你想了解更多,可以看我的另一篇博客你需要知道的单页面路由实现原理
现在大部分的应用都不是纯原生的应用, app 与 h5 的混合的应用是现在的一种主流。
纯 web 数据采集我们考虑到前端存储数据容易丢失,我们在每一次事件触发的时候都用采集接口传输采集到的数据。考虑到现在很多用户的手机会有流量管家的软件监控,如果在 App 中 h5 还是采集到数据就传输给服务端,很有可能会让流量管家检测到,给用户报警,从而使得用户不再信任你的 App , 所以我们在用户操作的时候将数据传给 app 端,存储到 app。用户切换应用到后台的时候,通过 app 端的 SDK 打包传输到服务器,我们给 app 提供的方法封装了一个适配器
// app 与 h5 混合应用,直接将数信息发给 app
collect.saveEvent = function (jsonString) {
collect.dcpDeviceType && setTimeout(function () {
if(collect.dcpDeviceType=='android'){
android.saveEvent(jsonString)
} else {
window.webkit && window.webkit.messageHandlers ? window.webkit.messageHandlers.nativeBridge.postMessage(jsonString) : window.postBridgeMessage(jsonString)
}
},1000)
}
通过上面几个问题的思考,我们对埋点的实现大致已经有了一些想法,我们使用思维导图来还原下我们即将要做的事情,图片记得放大看哦,太小了可能看不清。
我们定义了几个工具方法,提高开发的幸福指数 😝
var helper = {};
// 生成一个唯一的标识,pageSessionId (用这个变量来关联开始加载、加载完成、离开页面的事件,计算出页面加菜时间,停留时间)
helper.uuid = function(){}
// 元素绑定事件监听,兼容浏览器到IE8
helper.on = function(){}
//元素移除事件监听的适配器函数,兼容浏览器到IE8
helper.remove = function(){}
//将json转为字符串,事件传输的参数类型转化
helper.changeJSON2Query = function(){}
//将相对路径解析成文档全路径
helper.normalize = function(){}
var collect = {
deviceUrl:'http://collect.trc.com/rest/collect/device/h5/v1',
eventUrl:'http://collect.trc.com/rest/collect/event/h5/v1',
isuploadUrl:'http://collect.trc.com/rest/collect/isupload/app/v1',
parmas:{ ExtraInfo:{} },
device:{}
};
//获取埋点配置
collect.setParames = function(){}
//更新访问路径及页面信息
collect.updatePageInfo = function(){}
//获取事件参数
collect.getParames = function(){}
//获取设备信息
collect.getDevice = function(){}
//事件采集
collect.send = function(){}
//设备采集
collect.sendDevice = function(){}
//判断才否采集,埋点采集的开关
collect.isupload = function(){
1. 判断是否采集,不采集就注销事件监听(项目中区分游客身份和用户身份的采集情况,这个方法会被判断两次)
2. 采集则判断是否已经采集过
a.已经采集过不做任何操作
b.没有采集过添加事件监听
3. 判断是 混合应用还是纯 web 应用
a.如果是web 应用,调用 collect.setIframe 设置 iframe
b.如果是混合应用 将开始加载和加载完成事件传输给 app
}
//点击事件处理函数
collect.clickHandler = function(){}
//离开页面的事件处理函数
collect.beforeUnloadHandler = function(){}
//页面回退事件处理函数
collect.onPopStateHandler = function(){}
//系统事件初始化,注册离开事件,浏览器后退事件
collect.event = function(){}
//获取记录开始加载数据信息
collect.getBeforeload = function(){}
//存储加载完成,获取设备类型,记录加载完成信息
collect.onload = function(){
1. 判断cookie是否有存设备类型信息,有表示混合应用
2. 采集加载完成时间等信息
3. 调用 collect.isupload 判断是否进行采集
}
//web 应用,通过嵌入 iframe 进行跨域 cookie 通讯,设置设备id
collect.setIframe = function(){}
//app 与 h5 混合应用,直接将数信息发给 app,判断设备类型做原生方法适配器
collect.saveEvent = function(){}
//采集自定义事件类型
collect.dispatch = function(){}
//将参数 userId 存入sessionStorage
collect.storeUserId = function(){}
//采集H5信息,如果是混合应用,将采集到的信息发送给 app 端
collect.saveEventInfo = function(){}
//页面初始化调用方法
collect.init = function(){
1. 获取开始加载的采集信息
2. 获取 SDK 配置信息,设备信息
3. 改写 history 两个方法,单页面应用页面跳转前调用我们自己的方法
4. 页面加载完成,调用 collect.onload 方法
}
collect.init(); // 初始化
//暴露给业务方调用的方法
return {
dispatch:collect.dispatch,
storeUserId:collect.storeUserId,
}
👆就是我这段时间研究的成果了,代码的篇幅比较长,就不放在博客里了,感兴趣的同学可以加我微信进行交流,或则在文章下面留言,也欢迎大家给我提意见,帮忙优化 😝。
《你不知道的 javascript》是一个前端学习必读的系列,让不求甚解的JavaScript开发者迎难而上,深入语言内部,弄清楚JavaScript每一个零部件的用途。本书介绍了该系列的两个主题:“作用域和闭包”以及“this和对象原型”。这两块也是值得我们反复去学习琢磨的两块只是内容,今天我们用思维导图的方式来精读一遍。(思维导图图片可能有点小,记得点开看,你会有所收获)
作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对 变量进行赋值,那么就会使用 LHS 查询;如果目的是获取变量的值,就会使用 RHS 查询。赋值操作符会导致 LHS 查询。 的赋值操作。 =操作符或调用函数时传入参数的操作都会导致关联作用域的赋值操作。
JavaScript 引擎首先会在代码执行前对其进行编译,在这个过程中,像 var a = 2 这样的声 明会被分解成两个独立的步骤:
LHS 和 RHS 查询都会在当前执行作用域中开始,如果有需要(也就是说它们没有找到所 需的标识符),就会向上级作用域继续查找目标标识符,这样每次上升一级作用域(一层 楼),最后抵达全局作用域(顶层),无论找到或没找到都将停止。
不成功的RHS引用会导致抛出 ReferenceError 异常。不成功的 LHS 引用会导致自动隐式地创建一个全局变量(非严格模式下),该变量使用 LHS 引用的目标作为标识符,或者抛 出 ReferenceError 异常(严格模式下)。
词法作用域意味着作用域是由书写代码时函数声明的位置来决定的。编译的词法分析阶段 基本能够知道全部标识符在哪里以及是如何声明的,从而能够预测在执行过程中如何对它 们进行查找。
JavaScript 中有两个机制可以“欺骗”词法作用域: eval(..) 和 with 。 前者可以对一段包 含一个或多个声明的“代码”字符串进行演算,并借此来修改已经存在的词法作用域(在 运行时)。后者本质上是通过将一个对象的引用 当作 作用域来处理,将对象的属性当作作 用域中的标识符来处理,从而创建了一个新的词法作用域(同样是在运行时)。
这两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因为引擎只能谨慎地认 为这样的优化是无效的。使用这其中任何一个机制都 将 导致代码运行变慢。 不要使用它们。
函数是 JavaScript 中最常见的作用域单元。本质上,声明在一个函数内部的变量或函数会 在所处的作用域中“隐藏”起来,这是有意为之的良好软件的设计原则。
但函数不是唯一的作用域单元。块作用域指的是变量和函数不仅可以属于所处的作用域, 也可以属于某个代码块(通常指 { .. } 内部)。
从 ES3 开始, try/catch 结构在 catch 分句中具有块作用域。在 ES6 中引入了 let 关键字( var 关键字的表亲), 用来在任意代码块中声明变量。 if(..) { let a = 2; } 会声明一个劫持了 if 的 { .. } 块的变量,并且将变量添加到这个块 中。
有些人认为块作用域不应该完全作为函数作用域的替代方案。两种功能应该同时存在,开 发者可以并且也应该根据需要选择使用何种作用域,创造可读、可维护的优良代码。
我们习惯将 var a = 2; 看作一个声明,而实际上 JavaScript 引擎并不这么认为。它将 var a 和 a = 2 当作两个单独的声明,第一个是编译阶段的任务,而第二个则是执行阶段的任务。
这意味着无论作用域中的声明出现在什么地方,都将在代码本身被执行前 首先 进行处理。 可以将这个过程形象地想象成所有的声明(变量和函数)都会被“移动”到各自作用域的最顶端,这个过程被称为提升。
声明本身会被提升,而包括函数表达式的赋值在内的赋值操作并不会提升。
要注意避免重复声明,特别是当普通的 var 声明和函数声明混合在一起的时候,否则会引 起很多危险的问题!
闭包就好像从 JavaScript 中分离出来的一个充满神秘色彩的未开化世界,只有最勇敢的人 才能够到达那里。但实际上它只是一个标准,显然就是关于如何在函数作为值按需传递的 词法环境中书写代码的。
当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时 就产生了闭包。
如果没能认出闭包,也不了解它的工作原理,在使用它的过程中就很容易犯错,比如在循 环中。但同时闭包也是一个非常强大的工具,可以用多种形式来实现 模块 等模式。模块有两个主要特征:
(1)为创建内部作用域而调用了一个包装函数;
(2)包装函数的返回 值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭 包。
现在我们会发现代码中到处都有闭包存在,并且我们能够识别闭包然后用它来做一些有用 的事!
如果要判断一个运行中函数的 this 绑定,就需要找到这个函数的直接调用位置。找到之后 就可以顺序应用下面这四条规则来判断 this 的绑定对象。
由 new 调用?绑定到新创建的对象。
由 call 或者 apply (或者 bind )调用?绑定到指定的对象。
由上下文对象调用?绑定到那个上下文对象。
默认:在严格模式下绑定到 undefined ,否则绑定到全局对象。
一定要注意,有些调用可能在无意中使用默认绑定规则。如果想“更安全”地忽略 this 绑 定,你可以使用一个 DMZ 对象,比如 ø = Object.create(null) ,以保护全局对象。ES6中的箭头函数并不会使用四条标准的绑定规则, 而是根据当前的词法作用域来决定 this ,具体来说,箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。这 其实和 ES6 之前代码中的 self = this 机制一样。
JavaScript 中的对象有字面形式(比如 var a = { .. } )和构造形式(比如 var a = new Array(..) )。字面形式更常用,不过有时候构造形式可以提供更多选项。
许多人都以为“JavaScript 中万物都是对象”,这是错误的。对象是 6 个(或者是 7 个,取 决于你的观点)基础类型之一。对象有包括 function 在内的子类型,不同子类型具有不同 的行为,比如内部标签 [object Array] 表示这是对象的子类型数组。
对象就是键 / 值对的集合。可以通过 .propName 或者 ["propName"] 语法来获取属性值。访 问属性时, 引擎实际上会调用内部的默认 [[Get]] 操作(在设置属性值时是 [[Put]] ), [[Get]] 操作会检查对象本身是否包含这个属性,如果没找到的话还会查找 [[Prototype]] 链(参见第 5 章)。
属性的特性可以通过属性描述符来控制,比如 writable 和 configurable 。此外,可以使用 Object.preventExtensions(..) 、 Object.seal(..) 和 Object.freeze(..) 来设置对象(及其 属性)的不可变性级别。
属性不一定包含值——它们可能是具备 getter/setter 的“访问描述符”。此外,属性可以是 可枚举或者不可枚举的,这决定了它们是否会出现在 for..in 循环中。
你可以使用 ES6 的 for..of 语法来遍历数据结构(数组、对象, 等等)中的值, for..of 会寻找内置或者自定义的 @@iterator 对象并调用它的 next() 方法来遍历数据值。
类是一种设计模式。 许多语言提供了对于面向类软件设计的原生语法。 JavaScript 也有类 似的语法,但是和其他语言中的类完全不同。
类意味着复制。
传统的类被实例化时,它的行为会被复制到实例中。类被继承时,行为也会被复制到子类 中。
多态(在继承链的不同层次名称相同但是功能不同的函数)看起来似乎是从子类引用父 类,但是本质上引用的其实是复制的结果。
JavaScript 并不会(像类那样)自动创建对象的副本。
混入模式(无论显式还是隐式)可以用来模拟类的复制行为,但是通常会产生丑陋并且脆 弱的语法,比如显式伪多态( OtherObj.methodName.call(this, ...) ),这会让代码更加难 懂并且难以维护。
此外, 显式混入实际上无法完全模拟类的复制行为, 因为对象(和函数!别忘了函数也 是对象)只能复制引用, 无法复制被引用的对象或者函数本身。 忽视这一点会导致许多 问题。
总地来说,在 JavaScript 中模拟类是得不偿失的,虽然能解决当前的问题,但是可能会埋下更多的隐患。
如果要访问对象中并不存在的一个属性, [[Get]] 操作(参见第 3 章)就会查找对象内部 [[Prototype]] 关联的对象。这个关联关系实际上定义了一条“原型链”(有点像嵌套的作用域链),在查找属性时会对它进行遍历。
所有普通对象都有内置的 Object.prototype ,指向原型链的顶端(比如说全局作用域),如 果在原型链中找不到指定的属性就会停止。 toString() 、 valueOf() 和其他一些通用的功能 都存在于 Object.prototype 对象上,因此语言中所有的对象都可以使用它们。
关联两个对象最常用的方法是使用 new 关键词进行函数调用, 在调用的 章)中会创建一个关联其他对象的新对象。4个步骤(第2章)中会创建一个关联其他对象的新对象。
使用 new 调用函数时会把新对象的 .prototype 属性关联到“其他对象”。带 new 的函数调用 通常被称为“构造函数调用”,尽管它们实际上和传统面向类语言中的 类构造函数 不一样。
JavaScript 是 中的机制有一个核心区别, 那就是不会进行复制, 对象之间是通过内部的
虽然这些 机制和传统面向类语言中的“类初始化”和“类继承”很相似, 但是 javascript 机制和传统面向对象类语言中的“类初始化”和“类继承”很相似但是 javascript 中的机制有一个核心区别,就是不会进行复制,对象之间是通过内部的 [[Prototype]] 链关联的。
出于各种原因,以“继承”结尾的术语(包括“原型继承”)和其他面向对象的术语都无 法帮助你理解 JavaScript 的 真实 机制(不仅仅是限制我们的思维模式)。
相比之下,“委托”是一个更合适的术语,因为对象之间的关系不是 复制 而是委托。
在软件架构中你可以 选择是否 使用类和继承设计模式。大多数开发者理所当然地认为类是 唯一(合适)的代码组织方式,但是本章中我们看到了另一种更少见但是更强大的设计模式: 行为委托 。
行为委托认为对象之间是兄弟关系, 互相委托, 而不是父类和子类的关系。 JavaScript 的 [[Prototype]] 机制本质上就是行为委托机制。也就是说,我们可以选择在 JavaScript 中努 力实现类机制(参见第 4 和第 5 章),也可以拥抱更自然的 [[Prototype]] 委托机制。
当你只用对象来设计代码时,不仅可以让语法更加简洁,而且可以让代码结构更加清晰。
对象关联(对象之前互相关联)是一种编码风格,它倡导的是直接创建和关联对象,不把 它们抽象成类。对象关联可以用基于 [[Prototype]] 的行为委托非常自然地实现。
思维导图能比较清晰的还原整本书的知识结构体系,如果你还没用看过这本书,可以按照这个思维导图的思路快速预习一遍,提高学习效率。学习新事物总容易遗忘,我比较喜欢在看书的时候用思维导图做些记录,便于自己后期复习,如果你已经看过了这本书,也建议你收藏复习。如果你有神马建议或则想法,欢迎留言或加我微信交流:646321933
本期我来给大家推荐的书是《高性能JavaScript》,在这本书中我们能够了解 javascript 开发过程中的性能瓶颈,如何提升各方面的性能,包括代码的加载、运行、DOM交互、页面生存周期等。同样我们今天还是用思维导图的方式来精读一遍。(思维导图图片可能有点小,记得点开看,你会有所收获)
管理浏览器中的 JavaScript 代码是个棘手的问题,因为代码执行阻塞了其他浏览器处理过程,诸如用户 界面绘制。每次遇到<script>
标签,页面必须停下来等待代码下载(如果是外部的)并执行,然后再继续处 理页面其他部分。但是,有几种方法可以减少 JavaScript 对性能的影响:
<script>
标签放置在页面的底部,紧靠 body 关闭标签</body>
的上方。此法可以保证页面在脚本 运行之前完成解析。<script>
标签越少,页面的加载速度就越快,响应也更加迅速。不论外部脚本 文件还是内联代码都是如此。有几种方法可以使用非阻塞方式下载 JavaScript:
<script>
标签添加 defer 属性(只适用于 Internet Explorer 和 Firefox 3.5 以上版本)<script>
元素,用它下载并执行代码通过使用上述策略,你可以极大提高那些大量使用 JavaScript 代码的网页应用的实际性能。
在 JavaScript 中,数据存储位置可以对代码整体性能产生重要影响。有四种数据访问类型:直接量,变 量,数组项,对象成员。它们有不同的性能考虑。
直接量和局部变量访问速度非常快,数组项和对象成员需要更长时间。局部变量比域外变量快,因为它位于作用域链的第一个对象中。变量在作用域链中的位置越深,访问所需 的时间就越长。全局变量总是最慢的,因为它们总是位于作用域链的最后一环。避免使用 with 表达式,因为它改变了运行期上下文的作用域链。而且应当小心对待 try-catch 表达式的 catch 子句,因为它具有同样效果。嵌套对象成员会造成重大性能影响,尽量少用。
一个属性或方法在原形链中的位置越深,访问它的速度就越慢。一般来说,你可以通过这种方法提高 JavaScript 代码的性能:将经常使用的对象成员,数组项,和域外变 量存入局部变量中。然后,访问局部变量的速度会快于那些原始变量。通过使用这些策略,你可以极大地提高那些需要大量 JavaScript 代码的网页应用的实际性能。
DOM 访问和操作是现代网页应用中很重要的一部分。但每次你通过桥梁从 ECMAScript 岛到达 DOM 岛 时,都会被收取“过桥费”。为减少 DOM 编程中的性能损失,请牢记以下几点:
最小化 DOM 访问,在 JavaScript 端做尽可能多的事情。在反复访问的地方使用局部变量存放 DOM 引用.小心地处理 HTML 集合,因为他们表现出“存在性”,总是对底层文档重新查询。将集合的 length 属性缓 存到一个变量中,在迭代中使用这个变量。如果经常操作这个集合,可以将集合拷贝到数组中。
如果可能的话,使用速度更快的 API,诸如 querySelectorAll()和 firstElementChild。注意重绘和重排版;批量修改风格,离线操作 DOM 树,缓存并减少对布局信息的访问。动画中使用绝对坐标,使用拖放代理。使用事件托管技术最小化事件句柄数量。
正如其他编程语言,代码的写法和算法选用影响 JavaScript 的运行时间。与其他编程语言不同的是, JavaScript 可用资源有限,所以优化技术更为重要。
for,while,do-while 循环的性能特性相似,谁也不比谁更快或更慢。除非你要迭代遍历一个属性未知的对象,否则不要使用 for-in 循环。改善循环性能的最好办法是减少每次迭代中的运算量,并减少循环迭代次数。
一般来说,switch 总是比 if-else 更快,但并不总是最好的解决方法。当判断条件较多时,查表法比 if-else 或者 switch 更快。
浏览器的调用栈尺寸限制了递归算法在 JavaScript 中的应用;栈溢出错误导致其他代码也不能正常执行。如果你遇到一个栈溢出错误,将方法修改为一个迭代算法或者使用制表法可以避免重复工作。
运行的代码总量越大,使用这些策略所带来的性能提升就越明显。
密集的字符串操作和粗浅地编写正则表达式可能是主要性能障碍,但本章中的建议可帮助您避免常见缺陷。当连接数量巨大或尺寸巨大的字符串时,数组联合是 IE7 和它的早期版本上唯一具有合理性能的方法。如果你不关心 IE7 和它的早期版本,数组联合是连接字符串最慢的方法之一。使用简单的+和+=取而代之, 可避免(产生)不必要的中间字符串。
回溯既是正则表达式匹配功能基本的组成部分,又是正则表达式影响效率的常见原因。回溯失控发生在正则表达式本应很快发现匹配的地方,因为某些特殊的匹配字符串动作,导致运行缓慢 甚至浏览器崩溃。避免此问题的技术包括:使相邻字元互斥,避免嵌套量词对一个字符串的相同部分多次 匹配,通过重复利用前瞻操作的原子特性去除不必要的回溯。
提高正则表达式效率的各种技术手段,帮助正则表达式更快地找到匹配,以及在非匹配位置上花费更少 时间(见《更多提高正则表达式效率的方法》)。正则表达式并不总是完成工作的最佳工具,尤其当你只是搜索一个文本字符串时。
虽然有很多方法来修整一个字符串,使用两个简单的正则表达式(一个用于去除头部空格,另一个用于 去除尾部空格)提供了一个简洁、跨浏览器的方法,适用于不同内容和长度的字符串。从字符串末尾开始 循环查找第一个非空格字符,或者在一个混合应用中将此技术与正则表达式结合起来,提供了一个很好的 替代方案,它很少受到字符串整体长度的影响。
JavaScript 和用户界面更新在同一个进程内运行,同一时刻只有其中一个可以运行。这意味着当 JavaScript 代码正在运行时,用户界面不能响应输入,反之亦然。有效地管理 UI 线程就是要确保 JavaScript 不能运行 太长时间,以免影响用户体验。最后,请牢记如下几点:
网页工人线程是新式浏览器才支持的特性,它允许你在 UI 线程之外运行 JavaScript 代码而避免锁定 UI。网页应用程序越复杂,积极主动地管理 UI 线程就越显得重要。没有什么 JavaScript 代码可以重要到允 许影响用户体验的程度。
高性能 Ajax 包括:知道你项目的具体需求,选择正确的数据格式和与之相配的传输技术。
作为数据格式,纯文本和 HTML 是高度限制的,但它们可节省客户端的 CPU 周期。XML 被广泛应用 普遍支持,但它非常冗长且解析缓慢。JSON 是轻量级的,解析迅速(作为本地代码而不是字符串),交 互性与 XML 相当。字符分隔的自定义格式非常轻量,在大量数据集解析时速度最快,但需要编写额外的 程序在服务器端构造格式,并在客户端解析。
当从页面域请求数据时,XHR 提供最完善的控制和灵活性,尽管它将所有传入数据视为一个字符串, 这有可能降低解析速度。另一方面,动态脚本标签插入技术允许跨域请求和本地运行 JavaScript 和 JSON, 虽然它的接口不够安全,而且不能读取信息头或响应报文代码。多部分 XHR 可减少请求的数量,可在一次响应中处理不同的文件类型,尽管它不能缓存收到的响应报文。当发送数据时,图像灯标是最简单和最 有效的方法。XHR 也可用 POST 方法发送大量数据。
除这些格式和传输技术之外,还有一些准则有助于进一步提高 Ajax 的速度:
Ajax 是提升你网站潜在性能之最大的改进区域之一,因为很多网站大量使用异步请求,又因为它提供 了许多不相关问题的解决方案,这些问题诸如,需要加载太多资源。对 XHR 的创造性应用是如此的与众 不同,它不是呆滞不友好的界面,而是响应迅速且高效的代名词;它不会引起用户的憎恨,谁见了它都会 爱上它。
JavaScript 提出了一些独特的性能挑战,关系到你组织代码的方法。网页应用变得越来越高级,包含的 JavaScript 代码越来越多,出现了一些模式和反模式。请牢记以下编程经验:
开发和部署过程对基于 JavaScript 的应用程序可以产生巨大影响,最重要的几个步骤如下:
所有这些步骤应当自动完成,不论是使用公开的开发工具诸如 Apache Ant,还是使用自定义的开发工具 以实现特定需求。如果你使这些开发工具为你服务,你可以极大改善那些大量使用 JavaScript 代码的网页 应用或网站的性能。
当网页或应用程序变慢时,分析网上传来的资源,分析脚本的运行性能,使你能够集中精力在那些需要 努力优化的地方。使用网络分析器找出加载脚本和其它页面资源的瓶颈所在,这有助于决定哪些脚本需要延迟加载,或者 进行进一步分析。传统的智慧告诉我们应尽量减少 HTTP 请求的数量,尽量延迟加载脚本以使页面渲染速度更快,向用户 提供更好的整体体验。使用性能分析器找出脚本运行时速度慢的部分,检查每个函数所花费的时间,以及函数被调用的次数, 通过调用栈自身提供的一些线索来找出哪些地方应当努力优化。虽然花费时间和调用次数通常是数据中最有价值的点,还是应当仔细察看函数的调用过程,可能发现其 它优化方法。这些工具在那些现代代码所要运行的编程环境中不再神秘。在开始优化工作之前使用它们,确保开发时 间用在解决问题的刀刃上。
为了系统的串联前端知识,我平时喜欢用思维导图来记录读书笔记,我在 github 建了仓库放这些思维导图的原件,和读书笔记。如果你也喜欢用思维导图的方式来记录读书笔记,也欢迎和我一同维护这个仓库,欢迎留言或则微信(646321933)与我交流
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.