├── README.md ├── SUMMARY.md ├── closure.md ├── compile.md ├── file ├── README.md ├── benchmark-scores.png ├── client_server_connect.png ├── docker_pull_registry.png ├── improvements-per-website.png ├── inspect_colors.PNG ├── launching-ignition-and-turbofan.md ├── net_createserver.png ├── node_memory_leak.PNG ├── nodejs.jpg ├── pull_from_private.png ├── push_to_private.png ├── pushing_to_ptivate.png ├── rl_prompt.png ├── scope.png ├── scope_chain.png ├── typeof.png ├── v8-ignition.svg └── v8-turbofan.svg ├── node ├── buffer-chapter1.md ├── buffer-chapter2.md ├── console.md ├── events.md ├── http.md ├── net.md ├── path.md ├── qs_url.md ├── readline.md ├── stream-chapter1.md ├── stream-chapter2.md ├── stream-chapter3.md ├── token_hold.md └── util.md ├── object.md ├── prototype.md ├── scope.md ├── this.md ├── type.md ├── v8.dev └── launching-ignition-and-turbofan.md └── wechat └── mp ├── callapi.md └── start.md /README.md: -------------------------------------------------------------------------------- 1 | ##### 序言 2 |  先写点非技术类的文字吧,当做开篇吧。 3 | 4 | ##### 我 5 |  高中毕业的时候,为了正大光明的玩电脑,直接选择了计算机专业(其实打dota的时间远大于学习,相信大多数程序猿都差不多)。大学毕业之后供职于苏州一家小公司,后来公司资金链断裂,倒闭了(我保证不是我代码烂导致的)。后来(2012.6)到了新公司,一段时间后开始转做Node.js(之前一直做.NET),开启了我的Node之旅。 6 | 7 | ##### 现在 8 | + 2012年6月-2016年7月 做一些框架和底层类的工作,并潜心研究JavaScript。 9 | + 2016年7月-2020年3月 供职于上海一家互联网P2P公司。 10 | + 2016年4月-2020年6月 供职于某图形公司。 11 | + 2020年6月-2022年1月 被朋友忽悠进了工程行业,开始接触工程建设解决方案。 12 | + 2021年1月-至今 整装待发... 13 | 14 | ##### 博客指引 15 | * [JavaScript基础学习心得](https://github.com/swfbarhr/blog) 16 | * [node的基本使用](https://github.com/swfbarhr/blog/tree/master/node) 17 | * [公众号部分](https://github.com/swfbarhr/blog/tree/master/wechat/mp) 18 | * [蹩脚的v8.blog文章翻译](https://github.com/swfbarhr/blog/tree/master/v8.dev) 19 | 20 | ##### 联系我 21 | + 邮箱:swfbarhr@gmail.com 22 | + github:https://github.com/swfbarhr 23 | + 博客:https://swfbarhr.gitbooks.io/erblog/content/ 24 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [关于我](README.md) 4 | - [JavaScript 之作用域](scope.md) 5 | - [JavaScript 之闭包](closure.md) 6 | - [JavaScript 之编译](compile.md) 7 | - [JavaScript 之 this](this.md) 8 | - [JavaScript 之基本类型](type.md) 9 | - [JavaScript 之 object](object.md) 10 | - [JavaScript 之 prototype](prototype.md) 11 | - [Node.js 之 HTTP](node/http.md) 12 | - [Node.js 之 console](node/console.md) 13 | - [Node.js 之 Buffer(一)](node/buffer-chapter1.md) 14 | - [Node.js 之 Buffer(二)](node/buffer-chapter2.md) 15 | - [Node.js 之 util](node/util.md) 16 | - [Node.js 之 events](node/events.md) 17 | - [Node.js 之 Path](node/path.md) 18 | - [Node.js 之 Query Strings&URL](node/qs_url.md) 19 | - [Node.js 之 Readline](node/readline.md) 20 | - [Node.js 之 Net](node/net.md) 21 | - [Node.js 之 Stream(一)](node/stream-chapter1.md) 22 | - [Node.js 之 Stream(二)](node/stream-chapter2.md) 23 | - [Node.js 之 Stream(三)](node/stream-chapter3.md) 24 | - [Node.js 之高并发下微信 access_token 的存储与更新](node/token_hold.md) 25 | - [微信公众号之开篇](wechat/mp/start.md) 26 | - [微信公众号之调用 API](wechat/mp/callapi.md) 27 | -------------------------------------------------------------------------------- /closure.md: -------------------------------------------------------------------------------- 1 | ##### 一句话概括闭包 2 | 相信很多写了很多JavaScript代码的老程序猿都知道有闭包这个概念,但是如果真的要大家用语言来描述闭包的话,很多程序猿都吱吱呜呜说不清楚(包括很大一部分前端攻城狮)。闭包简单来讲就是:当函数记住并访问所在作用域时,就产生了闭包。 3 | 4 | ##### 示例 5 | ```js 6 | function foo(){ 7 | var a = 123; 8 | 9 | return function(){ 10 | return a; 11 | } 12 | } 13 | 14 | var baz = foo(); 15 | console.log(baz()); // 123 16 | ``` 17 | 上面是一个典型的闭包。我们来分析下代码: 18 | 首先我们定义了一个函数foo,然后在其作用域内部又定义了一个匿名函数并且将此匿名函数(在实际编码过程中尽量少用匿名函数,原因我会在后面的相关博客中说明)当做返回值return了出来。当我们调用foo并把结果赋值给baz变量(函数表达式),我们在foo外调用baz时,竟然能访问到foo的内部变量“a”(妈呀!我穿越了吗?)。在词法上,foo和baz是在同一层次的不同函数,应该互不影响才对,那现在这种情况又是怎么回事呢? 19 | 20 | ##### JavaScript中闭包的实现原理 21 | 要知道闭包不是JavaScript独有的,其他语言像C#、JAVA都有闭包的概念。但是各自的实现原理是不一样的,其他语言暂且不论,JavaScript中闭包的实现其实就是靠的作用域链(就是ES5中的词法环境)。在JavaScript作用域一文中我简单地提到过作用域链的概念,现在我就来详细的解读下什么是作用域链,它的形成过程和实现闭包的方法。先思考一下代码: 22 | 23 | ```js 24 | var arg1, foob; 25 | 26 | arg1 = 'world'; 27 | 28 | function fooa(){ 29 | var v1; 30 | 31 | v1 = 'hello' 32 | 33 | return function(arg1){ 34 | 35 | if(arg1){ 36 | return v1 + ' ' + arg1; 37 | } 38 | 39 | return v1 + ' everyone'; 40 | } 41 | } 42 | 43 | foob = fooa(); 44 | foob('Bob'); // hello Bob 45 | ``` 46 | 代码在执行过程中,fooa会把全局作用域的变量(可以认为fooa把全局作用的所有变量放入了一个堆栈里面以备用)引用至其作用域。同样的,fooa中return的匿名函数也会把fooa中的变量加入到这个“堆栈”中并且在其作用域中引用(如图1所示)。 47 | 48 | ![scope chain](./file/scope_chain.png) 49 | 50 | 其实在执行foob时,fooa的运行状态已经不存在了,之所以foob中能访问fooa的变量就是因为foob保存了对fooa作用域变量的引用,这样foob可以在需要时取出fooa中的变量来使用,也就是我们常说的:产生了闭包。(哎妈呀!原来如此!)闭包就是这么简单! 51 | 52 | ##### 闭包的应用 53 | 前面已经介绍了什么是闭包以及闭包的实现原理,那闭包在我们实际编码过程中到底有怎样的用途呢? 54 | 示例1: 55 | ```js 56 | var firFunc, secFunc; 57 | 58 | function func1(arg1){ 59 | return function(arg2){ 60 | return arg1 +' ' + arg2; 61 | } 62 | } 63 | 64 | firFunc = func1('hello'); 65 | secFunc = func1('hello'); 66 | 67 | console.log(firFunc('Lily')); // hello Lily 68 | console.log(secFunc('Lucy')); // hello Lucy 69 | ``` 70 | 我们可以利用闭包来暂时记录参数(例如:配置项、数据库连接等)。就是这么的优雅!就是这么酸爽! 71 | 示例2: 72 | ```js 73 | function foo(logInfo){ 74 | setTimeout(function(){ 75 | console.log(logInfo); 76 | }, 3000); 77 | } 78 | 79 | foo('pizza'); // pizza 80 | ``` 81 | setTimeout大家都很熟悉:延迟指定的时间执行代码。这里也涉及到了一个闭包,setTimeout的第一个参数(function)记住了foo的作用域内的变量logInfo。 82 | 示例3: 83 | ```js 84 | var isIE = true; 85 | 86 | $('#dvTest').click(function(){ 87 | if(isIE){ 88 | console.log('IE'); 89 | } 90 | else{ 91 | console.log('other'); 92 | } 93 | }); 94 | ``` 95 | 这是一段大家都很熟悉的JQuery事件绑定代码,里面也有一个非常明显的闭包。还有很多的例子,可能大家在编写代码的过程中已经很熟练地使用了闭包,自己却没有发现。现在去回顾下自己的代码,从中找出闭包! 96 | 97 | ##### 总结 98 | 讲到这里,我们就把闭包的概念叙述完了。其实也不复杂,很多人一直搞不清楚闭包,并不是因为闭包有多难,只是没有用心去理解。希望我上面的描述能帮助大家理解闭包,拥抱JavaScript。下一篇我们就来谈谈我在[JavaScript之作用域](scope.md)一文中提到的JavaScript编译。 99 | -------------------------------------------------------------------------------- /compile.md: -------------------------------------------------------------------------------- 1 | ##### JavaScript的编译 2 | 很多人说JavaScript是解释型或者动态语言,是不需要编译的。其实不然,JavaScript是需要编译的,其编译发生在代码执行前的一瞬间(几微妙或者更短的时间内)。大多数编译型的语言编译大致都可以分3个步骤:分词,解析,生成代码。JavaScript也不例外,但是JavaScript会有更复杂的过程。 3 | 4 | ##### 分词 5 | 大家知道,其实我们99.99%的代码都是文本类型。也就是说编译器拿到的是一堆实现约定好格式的文本,编译器拿到文本要干的第一件事情就是分词(也可以叫做词法分析)。就拿赋值语句来说(var a = 0;),编译器经过分词之后就会拿到这样的结果:var、a、=、0。 6 | 7 | ##### 解析 8 | 经过词法分析,编译器在拿到分词结果之后便会根据分词的结果生成一个叫做“抽象语法树”(Abstract Syntax Tree)的树形结构,简称AST。就拿(var a = 0;)来说,树结构下一般会有以下节点:存储a的名称(也就是“a”)、a的类型、a的值。 9 | 10 | ##### 生成代码 11 | 在这阶段,编译器根据生成的“抽象语法树”来生成计算机可以识别的指令。在从JavaScript到机器指令之间,JavaScript引擎还做了很多事情,以var a = 0;为例: 12 | 其实这条语句,JavaScript编译器会分为2个步骤执行(感谢[TylerPeng](https://github.com/TylerPeng)的提醒): 13 | 14 | + var a;此时,JavaScript引擎会询问当前作用域中是否已经申明了a变量,如果a已经申明,那么编译器会忽略此次申明;如果没有找到a的身影,那么编译器会在当前作用域中申明一个a变量。 15 | 16 | + a = 0;赋值时,询问当前作用域中是否已经申明了a变量,如果没有则询问当前作用域的上级(此过程与我在[JavaScript之闭包](https://github.com/swfbarhr/blog/blob/master/closure.md)一文中类似)。如果最终没有找到a,那么就会报错;如果找到了a,那么就会直接使用。 17 | 18 | ##### 其他的事 19 | 其实JavaScript引擎除了基本的3个编译步骤,还做了很多其他的事情,例如:代码优化、整理等。 20 | 21 | ##### 变量申明提升 22 | 既然已经讲到了这里,也不得不提到这个概念。其实在JavaScript中,变量申明是会被提升到当前作用域的最顶部,思考一下代码: 23 | ```js 24 | function foo(){ 25 | console.log(a); 26 | 27 | var a = 3; 28 | } 29 | 30 | foo(); // undefined 31 | ``` 32 | 但是按照编译规则,在执行到console.log(a)时,代码应该会报错才对,现在为什么又打印出“undefined”呢,打印出“undefined”很显然变量已经被申明了。上面我有提到,JavaScript引擎会对我们的代码进行优化、整理,真正编译器拿到的代码是这个样子的: 33 | ```js 34 | function foo(){ 35 | var a; 36 | 37 | console.log(a); 38 | 39 | a = 3; 40 | } 41 | 42 | foo(); // undefined 43 | ``` 44 | 就像我们上面说到的一样,申明赋值这个操作,JavaScript会分成2个步骤完成:申明和赋值。 45 | 其实不止变量申明的提升,函数的申明也会提升,思考一下代码: 46 | ```js 47 | foo(); // 3 48 | 49 | function foo(){ 50 | var a = 3; 51 | 52 | console.log(a); 53 | } 54 | ``` 55 | 我们在调用foo时,foo函数还没有申明。但是我们居然能得到我们所期望的值,这说明函数也会提升。 56 | 57 | ##### 总结 58 | 了解了JavaScript的编译规则,应该对它会有不同的认识。其实JavaScript是一门被精心设计的语言,其优势不言而喻。当今有很多人吐槽JavaScript,吐槽JavaScript各种令人讨厌的坑。其实学习和工作哪个不是不断的在填自己知识上填坑呢? 59 | -------------------------------------------------------------------------------- /file/README.md: -------------------------------------------------------------------------------- 1 | # 资源目录 2 | -------------------------------------------------------------------------------- /file/benchmark-scores.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swfbarhr/blog/94f951b947ff439c0020305a0c823e7f14ff0543/file/benchmark-scores.png -------------------------------------------------------------------------------- /file/client_server_connect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swfbarhr/blog/94f951b947ff439c0020305a0c823e7f14ff0543/file/client_server_connect.png -------------------------------------------------------------------------------- /file/docker_pull_registry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swfbarhr/blog/94f951b947ff439c0020305a0c823e7f14ff0543/file/docker_pull_registry.png -------------------------------------------------------------------------------- /file/improvements-per-website.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swfbarhr/blog/94f951b947ff439c0020305a0c823e7f14ff0543/file/improvements-per-website.png -------------------------------------------------------------------------------- /file/inspect_colors.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swfbarhr/blog/94f951b947ff439c0020305a0c823e7f14ff0543/file/inspect_colors.PNG -------------------------------------------------------------------------------- /file/launching-ignition-and-turbofan.md: -------------------------------------------------------------------------------- 1 | # Ignition 与 TurboFan 发布 2 | 3 | 今天我们很激动地宣布:在 V8 v5.9 版本(对应 Chrome v59)中将发布一个新的 JavaScript 执行管道。使用这条新的执行管道,当前的 JavaScript 应用将会在性能上有大幅度的提升并且消耗的内存将会更少。在本文的末尾,我们将会讨论更加详细的数据,在此之前我们先来介绍下这条执行流本身 4 | 5 | 这条新的执行流建立在 V8 的解释器 Ignition 和 V8 的最新优化编译器 TurboFan 的基础之上。对于一直关注我们 V8 blog 的同学来说,这些技术应该还是比较熟悉的,但是切换到新的执行流对于 Ignition 和 TurboFan 都是一个重大的里程碑。 6 | 7 | 8 |

Ignition logo,V8新解释器

9 | 10 | 11 |

TurboFan logo,V8新优化编译器

12 | 13 | V8 v5.9 中的全面并且仅仅使用 Ignition 和 TurboFan 还是历史首次。并且从 v5.9 开始,Full-codegen 和 Crankshaft(从 2010 年开始服务于 V8 的技术)将不再使用于 V8 的 JavaScript 执行管道,从此它们将不会与新的语言特新和优化需求保持同步更新。我们打算尽快将它们完全移除,这就意味着 V8 将会朝着一个更加简单、更加可维护的方向发展。 14 | 15 | # 漫长的旅程 16 | 17 | Ignition 和 TurboFan 组合管道已经开发了 3 个半年的时间。它是 V8 团队搜集现实世界 JavaScript 性能表现和对 Full-codegen 与 Crankshaft 缺点考虑的结果,是我们可以持续优化 JavaScript 语言的基础。 18 | 19 | TurboFan 项目最初开始于 2013 年底,旨在解决 Crankshaft 的不足之处。Crankshaft 只能优化 JavaScript 的一部分代码。举个栗子,最初设计时,它是不能优化异常处理例如被 try...catch...finally 包起来的代码块。我们很难在 Crankshaft 中加入新的语言特性,因为这些特性需要我们在 9 个所支持的平台根据不能架构编写对应架构匹配的代码。而且,Crankshaft 架构在某种程度上生成最佳的机器码受到限制。尽管 V8 团队在每个架构上都维护了成千上万行代码,Crankshaft 也就只能挤出那一丁点儿性能了。 20 | 21 | TurboFan 从一开始就被设计成不仅支持现有 JavaScript 标准 ES5 还可以支持计划中的 ES2015 和之后更新的版本。它引入了一个叫分层编译器的设计,使得在高等级编译优化和底层编译器优化有一个明显的分界,这样的设计可以使我们很容易优化新的语言特性而不需要修改面向不同架构的代码(architecture-specific code)。TurboFan 增加了一个显式的指令选择编译阶段,使得用更少的(不同架构的)代码就可以支持不同平台成为可能。引入这个阶段,不同架构的代码只需写一次,而且几乎不需要更改。基于以上以及一些其他因素,一个支持所有 V8 所支持架构的,可维护的、可扩展的优化编译器就产生了。 22 | 23 | V8 Ignition 解释器最初的背后动机是降低在移动设备上的内存消耗。在 Ignition 之前,Full-codegen 基本编译器所生成的代码几乎占了 Chrome 内存堆的 1/3。而实际留给 web 应用的空间就变少了。当在一台内存受限的安卓一定设备的 Chrome M53 上启用 Ignition 时,未被优化的 JavaScript 代码所需要的基本内存消耗会减少 9 倍(基于 ARM64 的移动设备)。 24 | 25 | 之后 V8 团队充分利用了 Ignition 字节码可以配合 TurboFan 直接生成机器码而不是重新编译源码(如 Crankshaft)这一优点。Ignition 的字节码在 V8 中提供了一个更干净、更少错误的基线执行模型,简化了脱优化(deoptimization)机制是 V8 自适应优化的一个关键功能。最后,因为生成字节码要比 Full-codegen 生成编译的代码更快,使用 Ignition 一般来说会改善脚本的启动时间、切换和页面加载。 26 | 27 | 随着 Ignition 和 TurboFan 的组合越来越紧密,在总体架构上还带来了更多的好处。比如 V8 团队使用 TurboFan 的中间语言来表示这些处理器的功能性,让 TurboFan 做优化和为各支持平台生成对应的代码,而不是手写高性能的字节码处理器。这使得 Ignition 在 V8 所支持的所有架构上表现良好,同时也减轻了 9 个相对独立的平台的维护负担。 28 | 29 | # 实测数据 30 | 31 | 撇开历史不谈,让我们来看下新管道在现实世界的表现和内存消耗。 32 | 33 | V8 团队使用 Telemetry - Catapult 框架来持续监控显示世界 V8 的性能表现。之前我们在博客中讨论过为什么使用来自现实世界的数据来驱动我们的性能优化工作是如此重要以及我们是如何使用 WebPageReplay 与 Telemetry 一起工作的。切换到 Ignition 和 TurboFan 展示了现实世界测试用例的性能提升。特别是新的管道显著加快了著名网站的用户交互测试速度。 34 | 35 | 尽管 Speedometer 使用的合成的基准测试,不过我们之前已经揭示过,与其他合成的基准相比,它在现在 JavaScript 工作负载方面的表现更接近现实世界。切换到 Ignition 和 TurboFan,V8 的 Speedometer 跑分根据平台和设备的不同有 5%-10%的提升。 36 | 37 | 新的管道也加快了服务端 JavaScript 速度。AcmeAir:Node.js 的基准测试,模拟虚拟管道的后端实现,在使用 V8 v5.9 后使速度加快了 10%。 38 | 39 | Ignition 与 TurboFan 同时也减少了 V8 的内存占用。在 Chrome M59,新的流程缩减了桌面程序和高端移动设备 V8 的内存约 5%-10%。这个减少(5%-10%)是之前在本博客提到过的 Ignition 对于 V8 所支持的所有设备和平台内存节省(策略)的结果。 40 | 41 | 这些提升仅仅是个开始。新的 Ignition 与 TurboFan 管道在未来的几年为进一步优化 JavaScript 性能和缩小 V8 性能开销铺平道路(同时在 Chrome 浏览器和 Node.js 端)。我们期待在我们向开发者和用户推出它们的时候与您分享这些提升。尽情期待。 42 | 43 | ##### V8 团队发表于 v8.dev( 原文:https://v8.dev/blog/launching-ignition-and-turbofan ) 44 | -------------------------------------------------------------------------------- /file/net_createserver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swfbarhr/blog/94f951b947ff439c0020305a0c823e7f14ff0543/file/net_createserver.png -------------------------------------------------------------------------------- /file/node_memory_leak.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swfbarhr/blog/94f951b947ff439c0020305a0c823e7f14ff0543/file/node_memory_leak.PNG -------------------------------------------------------------------------------- /file/nodejs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swfbarhr/blog/94f951b947ff439c0020305a0c823e7f14ff0543/file/nodejs.jpg -------------------------------------------------------------------------------- /file/pull_from_private.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swfbarhr/blog/94f951b947ff439c0020305a0c823e7f14ff0543/file/pull_from_private.png -------------------------------------------------------------------------------- /file/push_to_private.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swfbarhr/blog/94f951b947ff439c0020305a0c823e7f14ff0543/file/push_to_private.png -------------------------------------------------------------------------------- /file/pushing_to_ptivate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swfbarhr/blog/94f951b947ff439c0020305a0c823e7f14ff0543/file/pushing_to_ptivate.png -------------------------------------------------------------------------------- /file/rl_prompt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swfbarhr/blog/94f951b947ff439c0020305a0c823e7f14ff0543/file/rl_prompt.png -------------------------------------------------------------------------------- /file/scope.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swfbarhr/blog/94f951b947ff439c0020305a0c823e7f14ff0543/file/scope.png -------------------------------------------------------------------------------- /file/scope_chain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swfbarhr/blog/94f951b947ff439c0020305a0c823e7f14ff0543/file/scope_chain.png -------------------------------------------------------------------------------- /file/typeof.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swfbarhr/blog/94f951b947ff439c0020305a0c823e7f14ff0543/file/typeof.png -------------------------------------------------------------------------------- /file/v8-ignition.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /file/v8-turbofan.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /node/buffer-chapter1.md: -------------------------------------------------------------------------------- 1 | ##### JavaScript不足 2 | JavaScript原生支持Unicode,并且对Unicode非常友好。但是,纯JavaScript对于处理二进制数据却是一个弱项。但是node既然是后端,那么不免要和二进制数据打交道,这样Buffer类就应运而生了。 3 | 4 | ##### 全局变量 5 | 与一般的node模块不同,Buffer在node中是全局存在的(这也说明了Buffer模块在node中的重要性)。既然Buffer是全局变量,那么我们不需要显示的引用,直接使用即可: 6 | 7 | ```js 8 | var bfData = new Buffer(4); 9 | ``` 10 | 11 | ##### 格式转换 12 | 我们在Buffer与JavaScript字符串之间转换时,需要显示的指定格式。当前支持的格式有: 13 | 14 | + ascii 目前只支持7比特的ASCII数据(需要注意的是,在此编码中,node会把null转换成空格) 15 | + utf8 16 | + utf16le、ucs2 17 | + base64 18 | + hex 19 | 20 | ```js 21 | var bfASCII = new Buffer('abcd', 'ascii'); 22 | console.log(bfASCII); // 23 | console.log(bfASCII.toString()); // abcd 24 | 25 | var bfHex = new Buffer('b4c127c3d7', 'hex'); 26 | console.log(bfHex); // 27 | console.log(bfHex.toString('hex')); // b4c127c3d7 28 | ``` 29 | 30 | ##### 创建一个Buffer实例 31 | 我们可以有3中创建Buffer实例的方法。 32 | 33 | + new Buffer(size) 创建指定大小的Buffer实例 34 | 35 | ```js 36 | var bfSize = new Buffer(6); 37 | 38 | bfSize.write('abcdef', 0, 6, 'utf8'); 39 | 40 | console.log(bfSize); // 41 | console.log(bfSize.toString('utf8')); // abcdef 42 | ``` 43 | 44 | + new Buffer(array) 给定指定数组创建Buffer实例 45 | 46 | ```js 47 | var bfArray = new Buffer(['97', 98]); 48 | 49 | console.log(bfArray.toString('utf8')); // ab 50 | ``` 51 | 52 | + new Buffer(str, [encoding]) 使用指定的字符串和编码格式(默认为utf8)创建新的Buffer实例 53 | 54 | ```js 55 | var bfStr = new Buffer('你好', 'utf8'); 56 | 57 | console.log(bfStr); // 58 | console.log(bfStr.toString('utf8')); // 你好 59 | ``` 60 | 61 | ##### Buffer.isEncoding(encoding) 62 | 此方法可以方便的知晓给定的字符编码是否在node支持范围内: 63 | 64 | ```js 65 | console.log(Buffer.isEncoding('utf8')); // true 66 | console.log(Buffer.isEncoding('ascii')); // true 67 | console.log(Buffer.isEncoding('hex')); // true 68 | console.log(Buffer.isEncoding('base64')); // true 69 | console.log(Buffer.isEncoding('ucs2')); // true 70 | console.log(Buffer.isEncoding('binary')); // true 71 | console.log(Buffer.isEncoding('utf108')); // false 72 | ``` 73 | 74 | ##### buf.write(string, [offset], [length], [encoding]) 75 | 将指定字符串写入到Buffer实例,并且可以指定从偏移量、写入长度和编码 76 | 77 | ```js 78 | var bfWrite = new Buffer(6); 79 | 80 | bfSize.write('abcdef', 0, 5, 'utf8'); 81 | 82 | console.log(bfWrite); // 83 | console.log(bfWrite.toString('utf8')); // abcde 84 | ``` 85 | 86 | ##### buf.toString([encoding], [start], [end]) 87 | 将Buffer实例转换成字符串 88 | 89 | ```js 90 | var bfToStr = new Buffer(2); 91 | 92 | bfToStr[0] = 108; 93 | bfToStr[1] = 109; 94 | 95 | console.log(bfToStr.toString('ascii')); // lm 96 | ``` 97 | 98 | ##### 未完待续 99 | -------------------------------------------------------------------------------- /node/buffer-chapter2.md: -------------------------------------------------------------------------------- 1 | ##### buf.toJSON() 2 | 将Buffer实例转换成JSON形式的数组。 3 | 4 | ```js 5 | var bfJson = new Buffer('hello'); 6 | 7 | console.log(bfJson.toJSON()); // [ 104, 101, 108, 108, 111 ] 8 | 9 | console.log(new Buffer(bfJson.toJSON()).toString()); // hello 10 | ``` 11 | 12 | ##### Buffer.isBuffer(obj) 13 | 判断对象是否是Buffer实例 14 | 15 | ```js 16 | console.log(Buffer.isBuffer({})); // false 17 | console.log(Buffer.isBuffer(new Buffer(3))); // true 18 | console.log(Buffer.isBuffer(new Buffer('test'))); // true 19 | console.log(Buffer.isBuffer('world')); // false 20 | console.log(Buffer.isBuffer(123)); // false 21 | ``` 22 | 23 | ##### Buffer.byteLength(string, [encoding]) 24 | 获取字符串的字节数 25 | 26 | ```js 27 | console.log(Buffer.byteLength('hello')); // 5 28 | console.log(Buffer.byteLength('你好')); // 6 29 | ``` 30 | 31 | ##### Buffer.concat(list, [totalLength]) 32 | 拼接Buffer实例 33 | 34 | ```js 35 | var buf1 = new Buffer('小明'); 36 | var buf2 = new Buffer('今年'); 37 | var buf3 = new Buffer('5'); 38 | var buf4 = new Buffer('岁'); 39 | 40 | var bufList = []; 41 | 42 | bufList.push(buf1); 43 | bufList.push(buf2); 44 | bufList.push(buf3); 45 | bufList.push(buf4); 46 | 47 | var bufTotal = Buffer.concat(bufList, buf1.length + buf2.length + buf3.length + buf4.length); 48 | 49 | console.log(bufTotal.toString()); // 小明今年5岁 50 | ``` 51 | 52 | ##### buf.copy(targetBuffer, [targetStart], [sourceStart], [sourceEnd]) 53 | 将Buffer实例拷贝到目标对象上 54 | 55 | ```js 56 | var bfSource = new Buffer('可可是个乖宝宝'); 57 | var bfTarget = new Buffer(bfSource.length); 58 | 59 | bfSource.copy(bfTarget, 0, 0, bfSource.length); 60 | 61 | console.log(bfTarget.toString()); // 可可是个乖宝宝 62 | ``` 63 | 64 | ##### buf.slice([start], [end]) 65 | 取Buffer实例片段(不是将原始数据中片段复制出来,而是直接引用。所有当原数据改变时,取出的片段也将随之改变) 66 | 67 | ```js 68 | var bufFull = new Buffer('abcde', 'ascii'); 69 | var bufSlice = bufFull.slice(2, 3); 70 | 71 | console.log(bufSlice.toString('ascii')); // c 72 | 73 | bufFull[2] = 97; 74 | 75 | console.log(bufSlice.toString('ascii')); // a 76 | ``` 77 | 78 | 上面的代码中,我们在调用slice之后,又改变了bufFull的第三个元素值,之后我们打印bufSlice发现bufSlice中的值也随之改变了。 79 | 80 | ##### buf.readUInt8(offset, [noAssert]) 81 | 读取一个8位的无符号整型 82 | 83 | ```js 84 | var bfRead = new Buffer(1); 85 | 86 | bfRead[0] = 0x12; 87 | 88 | console.log(bfRead.readUInt8(0)); // 18 89 | 90 | console.log(bfRead.readUInt8(1, true)); // undefined 91 | 92 | console.log(bfRead.readUInt8(1)); // 程序出错 93 | ``` 94 | 95 | buf.readUInt8只读取一个8位无符号整数,并且可以指定读取的位置。第二个参数(noAssert)表示如果读取位置超过Buffer实例的长度时是否抛出错误,默认为false。如果为false,即使超过Buffer.length,系统也不会抛错,这是之后返回undefined。 96 | 97 | ##### 其他的read方法 98 | 99 | + buf.readUInt16LE(offset, [noAssert]) 100 | + buf.readUInt16BE(offset, [noAssert]) 101 | + buf.readUInt32LE(offset, [noAssert]) 102 | + buf.readUInt32BE(offset, [noAssert]) 103 | + buf.readInt8(offset, [noAssert]) 104 | + buf.readInt16LE(offset, [noAssert]) 105 | + buf.readInt16BE(offset, [noAssert]) 106 | + buf.readInt32LE(offset, [noAssert]) 107 | + buf.readInt32BE(offset, [noAssert]) 108 | + buf.readFloatLE(offset, [noAssert]) 109 | + buf.readFloatBE(offset, [noAssert]) 110 | + buf.readDoubleLE(offset, [noAssert]) 111 | + buf.readDoubleBE(offset, [noAssert]) 112 | 113 | 以上方法与buf.readUInt8类似,就不多做赘述。 114 | 115 | ##### buf.writeUInt8(value, offset, [noAssert]) 116 | 将值写入指定Buffer实例的指定位置 117 | 118 | ```js 119 | var bfWrite = new Buffer(1); 120 | 121 | bfWrite.writeUInt8(0x12, 0); 122 | 123 | console.log(bfWrite); // 124 | ``` 125 | 126 | ##### 其他的write方法 127 | 128 | + buf.writeUInt16LE(value, offset, [noAssert]) 129 | + buf.writeUInt16BE(value, offset, [noAssert]) 130 | + buf.writeUInt32LE(value, offset, [noAssert]) 131 | + buf.writeUInt32BE(value, offset, [noAssert]) 132 | + buf.writeInt8(value, offset, [noAssert]) 133 | + buf.writeInt16LE(value, offset, [noAssert]) 134 | + buf.writeInt16BE(value, offset, [noAssert]) 135 | + buf.writeInt32LE(value, offset, [noAssert]) 136 | + buf.writeInt32BE(value, offset, [noAssert]) 137 | + buf.writeFloatLE(value, offset, [noAssert]) 138 | + buf.writeFloatBE(value, offset, [noAssert]) 139 | + buf.writeDoubleLE(value, offset, [noAssert]) 140 | + buf.writeDoubleBE(value, offset, [noAssert]) 141 | 142 | 以上方法与buf.writeUInt8类似,就不多做赘述。 143 | 144 | ##### buf.fill(value, [offset], [end]) 145 | 向Buffer实例中填充指定的值 146 | 147 | ```js 148 | var bfFill1 = new Buffer(1); 149 | var bfFill2 = new Buffer(6); 150 | 151 | bfFill1.fill(97, 0, 1); 152 | 153 | console.log(bfFill1.toString()); // a 154 | 155 | bfFill2.fill(97); 156 | 157 | console.log(bfFill2.toString()); // aaaaaa 158 | ``` 159 | 160 | 如果不指定offset和end,那么fill方法会把当前Buffer实例填满。 161 | 162 | ##### 总结 163 | 好了,Buffer模块介绍到这里就介绍完了。此模块的方法很多,但是大多数都是读取和写入的方法,形式都差不多。只要我们掌握了一个,其他的稍稍看下API就可以轻松使用了。 -------------------------------------------------------------------------------- /node/console.md: -------------------------------------------------------------------------------- 1 | ##### 调式 2 | 众所周知,node的调试是一个令人头疼的工作,著名的node大神TJ也抱怨过。由此见得,想要像.NET那样方便的调试是很困难的(微软最近出了VSCode倒是可以很好的调式,有时间可以试试)。其实node最快捷、方便的调试方式还是控制台输出日志信息。今天我们就来研究下如何使用node中的console模块,console模块是大多数noder的调试神器。 3 | 4 | ##### console.log 5 | console.log(console是个全局变量)是node最常用的一个日志打印函数,此函数与C和C++中的printf很像,可以设置占位符,并且与前2者一致。 6 | 7 | ```js 8 | console.log('娜塔莎是个%s', '好孩子'); // 娜塔莎是个好孩子 9 | 10 | var info = { 11 | status: 200, 12 | msg: 'everything is ok' 13 | } 14 | 15 | console.log(info); // { status: 200, msg: 'everything is ok' } 16 | ``` 17 | 18 | console.log的参数是可变的,可以有很多个,就像这样: 19 | 20 | ```js 21 | var a = 'hello Peter'; 22 | var b = 'hi Lily'; 23 | 24 | console.log(a, b); // hello Peter hi Lily 25 | ``` 26 | 27 | 这样就可以打印多个变量或者表达式的结果,当我们在调试程序的时候(特别是需要同时打印多个变量的值)特别有用。就如上面演示的一样,console.log方法可以打印数字、字符串、对象。可以说console.log方法非常强大,但是它不是万能的,当我们打印的对象的树层次太深的话就会出现意料之外的结果。 28 | 29 | ```js 30 | var info = { 31 | status: 200, 32 | msg: { 33 | user:{ 34 | name: 'Peter', 35 | country: 'Japan', 36 | company: { 37 | name: '全球无限皮包公司', 38 | ceo: { 39 | name: 'Lucy' 40 | } 41 | } 42 | } 43 | } 44 | } 45 | 46 | console.log(info); // { status: 200, msg: { user: { name: 'Peter', country: 'Japan', company: [Object] } } } 47 | ``` 48 | 49 | 可以看到,当我们打印info变量时,到第四层级“company”只打印出了一个[object]。说明console.log在打印object的时候,最多只支持到第三层级,如果需要打印对象所有层级的话,我们可以与util.inspect方法配合使用(之后我们讲到util模块的时候会讲到这个方法)。 50 | 51 | ```js 52 | var util = require('util'); 53 | 54 | var info = { 55 | status: 200, 56 | msg: { 57 | user:{ 58 | name: 'Peter', 59 | country: 'Japan', 60 | company: { 61 | name: '全球无限皮包公司', 62 | ceo: { 63 | name: 'Lucy' 64 | } 65 | } 66 | } 67 | } 68 | } 69 | 70 | 71 | console.log(util.inspect(info, {depth: null})); // { status: 200, 72 | // msg: 73 | // { user: 74 | // { name: 'Peter', 75 | // country: 'Japan', 76 | // company: { name: '全球无限皮包公司', 77 | // ceo: { name: 'Lucy' 78 | // } 79 | // } 80 | // } 81 | // } 82 | // } 83 | ``` 84 | 85 | 此时,我们就可以打印出info中所有的层级了。 86 | 87 | ##### console.error 88 | 此方法如果我们没有在运行时指定输出文件(包括一般输出和错误输出),在我们运行node的时候可以手动指定日志和错误的输出文件,console.error方法就会输出到错误文件中。 89 | 90 | ```js 91 | node app.js 2> error.log | tee info.log 92 | ``` 93 | 94 | 运行了以上代码,我们的错误信息都会记录到error.log文件中,普通日志信息都会记录到info.log文件中。如果我们在运行的时候没有指定输出文件,那么所有的信息都会输出到控制台,这时我们调用console.log和console.error是一样的效果。 95 | 96 | ##### console.warn、console.info 97 | console.warn的实现与console.error是一样的,console.info的实现与console.log是一样的。 98 | 99 | ##### console.dir 100 | console.dir方法就是与我们在上面介绍的util.inspect方法的效果一致。 101 | 102 | ```js 103 | var info = { 104 | status: 200, 105 | msg: { 106 | user:{ 107 | name: 'Peter', 108 | country: 'Japan', 109 | company: { 110 | name: '全球无限皮包公司', 111 | ceo: { 112 | name: 'Lucy' 113 | } 114 | } 115 | } 116 | } 117 | } 118 | 119 | 120 | console.dir(info); // { status: 200, msg: { user: { name: 'Peter', country: 'Japan', company: [Object] } } } 121 | ``` 122 | 123 | ##### console.time、console.timeEnd 124 | 一般console.time与console.tiemEnd成对出现,用于记录两者之间的代码运行时间 125 | 126 | ```js 127 | console.time('for-time'); 128 | 129 | for(var i = 0;i < 10000000;i++){ 130 | ; 131 | } 132 | 133 | console.timeEnd('for-time'); // for-time: 173ms 134 | ``` 135 | 136 | 以上代码说明,在我的电脑上循环0到10000000需要耗费173毫秒。 137 | 138 | ##### console.trance 139 | 此方法会输出到错误文件中,在日志前添加“Trace:”字符串。 140 | 141 | ##### console.assert 142 | 此方法是一个断言函数,第一个参数是布尔值,如果第一个参数的结果为真,那么什么事情都不做;如果第一个布尔值为假,那么系统就会抛出错误,并且把后面的参数打印出来,并且会打印出调用栈。 143 | 144 | #### 总结 145 | console模块是一个非常简单地模块,所有的API我都已经解释或者演示过了。非常容易理解,这些方法在我们平常的开发过程中是很常见的,给noder提供了单接单并不唯一的调试node程序的方法(还有其他调试方法:node-inspector、vscode等,我以后有机会也会开博演示)。 146 | -------------------------------------------------------------------------------- /node/events.md: -------------------------------------------------------------------------------- 1 | ##### 继承 2 | 之前我们在介绍Utilities模块的时候,遗留了最后一个方法(util.inherits)没有说。这是因为把此方法放到events模块一起讲更合适,不是说events模块对此方法有什么依赖,而是我们在使用events时,利用node提供的继承方法可以更方便和快捷。 3 | 4 | ##### util.inherits(constructor, superConstructor) 5 | 从字面上我们可以很清楚的知道,此方法是用来做JavaScript里面的继承功能的。参数如下: 6 | 7 | + 第一个参数(constructor)表示子类 8 | 9 | + 第二个参数(superConstructor)表示父类 10 | 11 | 与JavaScript复杂的继承代码相比,使用此函数确实非常方便。 12 | 13 | ```js 14 | var util = require('util'); 15 | 16 | var People = function(info){ 17 | this.name = info.name; 18 | this.age = info.age; 19 | } 20 | 21 | People.prototype.showName = function() { 22 | return util.format('My name is %s,', this.name); 23 | } 24 | 25 | People.prototype.showAge = function() { 26 | return util.format('I am %d years old.', this.age); 27 | } 28 | 29 | var Chinese = function(info){ 30 | People.call(this, info); 31 | } 32 | util.inherits(Chinese, People); 33 | 34 | Chinese.prototype.introduce = function(){ 35 | return '你好!' + this.showName() + this.showAge(); 36 | } 37 | 38 | var chinese_poople = new Chinese({name: 'peter', age: 28}); 39 | 40 | console.log(chinese_poople.introduce()); // 你好!My name is peter,I am 28 years old. 41 | ``` 42 | 43 | 首先我们定义了People(父类)和Chinese(子类),然后利用util.inherits使Chinese继承了People。在Chinese中直接可以调用showName和showAge方法,表明util.inherits方法的继承效果已经生效了。util.inherits非常简单,只要提供2个参数即可,没有太多的花花肠子。 44 | 45 | ##### events 46 | 现在进入我们今天真正的主题,events(下称事件)模块是node的核心模块之一。在node中,事件可谓是无处不在。例如在[Node.js之HTTP](https://github.com/swfbarhr/blog/blob/master/node/http.md)提到过的server.on('request', function(){})就是处理request事件。而如果想要实现事件的推送与接收,我们必须要要继承events.EventEmitter 47 | 48 | ##### events.EventEmitter 49 | 只要继承了events.EventEmitter类,我们就可以正常使用emit方法和on方法来推送、接收事件。这里需要注意的是,'error'事件很特殊。如果我们没有监听'error'事件的话,一旦程序发成了错误,node就会在控制台上打印出调用栈,并且退出程序。 50 | 51 | ##### 监听事件 emitter.addListener(event, listener)、emitter.on(event, listener) 52 | 监听事件可以有2中写法一种是addListener,addListener方法有2个参数:第一个参数是事件名称(命名格式一般是驼峰式),第二个参数是对事件的处理函数。 53 | 54 | ```js 55 | var util = require('util'); 56 | var EventEmitter = require('events').EventEmitter; 57 | 58 | var _eventDemo = function(){ 59 | EventEmitter.call(this); 60 | } 61 | 62 | util.inherits(_eventDemo, EventEmitter); 63 | 64 | var _eventInstance = new _eventDemo(); 65 | 66 | _eventInstance.addListener('connection', function(params){ 67 | util.log('连接成功!'); // 3 Mar 14:11:36 - 连接成功! 68 | console.log(params); // { username: 'admin', pwd: '123' } 69 | }); 70 | 71 | util.log('开始连接...'); // 3 Mar 14:11:31 - 开始连接... 72 | setTimeout(function(){ 73 | _eventInstance.emit('connection', {username:'admin', pwd:'123'}); 74 | }, 5000); 75 | ``` 76 | 77 | 首先我们定义了_eventDemo并继承了EventEmitter。既然继承了EventEmitter,就可以使用addListener和on。然后我们就定义了事件'connection'和处理函数,为了方便起见,我在合适的位置打印了日志信息。当我们运行以上代码的时候,首先会打印“开始连接...”,等待5秒之后屏幕上回出现“连接成功!”,并且接着打印我们在触发事件(emit)时所传入的参数。这就表明我们的监听是可以正常运行的。 78 | 79 | 监听事件的另一个方法(on),与addListener用法是一模一样的,大家可以自己试试,这里就不多讲了。 80 | 81 | ##### emitter.once(event, listener) 82 | 此方法与emitter.once方法功能一致,唯一的却别就是,此方法对应一个emitter实例即使推送多次,其所监听的事件也只触发一次: 83 | 84 | ```js 85 | var util = require('util'); 86 | var EventEmitter = require('events').EventEmitter; 87 | 88 | var _eventDemo = function(){ 89 | EventEmitter.call(this); 90 | } 91 | 92 | util.inherits(_eventDemo, EventEmitter); 93 | 94 | var _eventInstance = new _eventDemo(); 95 | 96 | _eventInstance.once('connection', function(){ 97 | console.log('once'); 98 | }); 99 | 100 | _eventInstance.on('connection', function(){ 101 | console.log('ok') 102 | }); 103 | 104 | _eventInstance.emit('connection'); // once 105 | // ok 106 | _eventInstance.emit('connection'); // ok 107 | ``` 108 | 109 | 如果我们运行以上代码,可以看到,即使我们触发了2次'connection'事件。当我们用_eventInstance.on来监听事件时,确实会触发2次;但是_eventInstance.once监听的事件却只触发了一次。这就是emitter.once的与emitter.on最大的区别。 110 | 111 | ##### emitter.removeListener(event, listener) 112 | 此方法是emitter.on方法的逆,此方法会移除掉对应的监听事件。需要注意的是,如果同一事件被监听了多次,那么也需要多次调用emitter.removeListener才能将其全部移除。 113 | 114 | ```js 115 | var util = require('util'); 116 | var EventEmitter = require('events').EventEmitter; 117 | 118 | var _eventDemo = function() { 119 | EventEmitter.call(this); 120 | } 121 | 122 | util.inherits(_eventDemo, EventEmitter); 123 | 124 | var _eventInstance = new _eventDemo(); 125 | 126 | var cb = function() { 127 | console.log('ok') 128 | } 129 | 130 | _eventInstance.on('connection', cb); 131 | 132 | _eventInstance.removeListener('connection', cb); 133 | 134 | _eventInstance.emit('connection'); // 这里什么都不会发生 135 | ``` 136 | 137 | ```js 138 | var util = require('util'); 139 | var EventEmitter = require('events').EventEmitter; 140 | 141 | var _eventDemo = function() { 142 | EventEmitter.call(this); 143 | } 144 | 145 | util.inherits(_eventDemo, EventEmitter); 146 | 147 | var _eventInstance = new _eventDemo(); 148 | 149 | var cb = function() { 150 | console.log('ok') 151 | } 152 | 153 | _eventInstance.on('connection', cb); 154 | _eventInstance.on('connection', cb); 155 | 156 | _eventInstance.removeListener('connection', cb); 157 | 158 | _eventInstance.emit('connection'); // ok 159 | ``` 160 | 161 | 上面2段代码,第一段代码我们添加了一个'connection'监听事件,并且移除了一次,运行代码之后什么事情都没有发生。第二段代码我们添加了两个'connection'监听事件,只移除了一次,所有在推送'connection'事件之后,会触发一次。 162 | 163 | ##### emitter.removeAllListeners([event]) 164 | 移除所有监听事件 165 | 166 | ```js 167 | var util = require('util'); 168 | var EventEmitter = require('events').EventEmitter; 169 | 170 | var Server = function() { 171 | EventEmitter.call(this); 172 | } 173 | 174 | util.inherits(Server, EventEmitter); 175 | 176 | var server1 = new Server(); 177 | 178 | var connected = function() { 179 | console.log('connected'); 180 | } 181 | 182 | var data = function() { 183 | console.log('data') 184 | } 185 | 186 | server1.on('connected', connected); 187 | server1.on('data', data); 188 | 189 | server1.emit('connected'); // connected 190 | server1.emit('data'); // data 191 | 192 | server1.removeAllListeners(); 193 | 194 | console.log('all listeners are removed'); 195 | 196 | server1.emit('connected'); // 什么事也没发生 197 | server1.emit('data'); // 什么事也没发生 198 | ``` 199 | 200 | emitter.removeAllListeners有一个可选参数,如果传入该参数,表示移除所有指定事件名称的所有监听。大家可以自己试一下。 201 | 202 | ##### emitter.setMaxListeners(n) 203 | node默认同一对象的同一事件监听超过十次,就会发出警告信息。这可以帮助我们这个noder更好的找出令人烦恼的且蛋疼的内存泄露问题,因为内存泄露很多时候我们连原因都不知道,更不要说去修复问题了。 204 | 205 | ```js 206 | var util = require('util'); 207 | var EventEmitter = require('events').EventEmitter; 208 | 209 | var Server = function() { 210 | EventEmitter.call(this); 211 | } 212 | 213 | util.inherits(Server, EventEmitter); 214 | 215 | var server1 = new Server(); 216 | 217 | var connected = function() { 218 | console.log('connected'); 219 | } 220 | 221 | for(var i = 0;i <= 10;i++){ 222 | server1.on('connected', connected); 223 | } 224 | ``` 225 | 226 | 如果我们运行以上代码,并且不会任何其他设置的话,就会得到以下的结果: 227 | 228 | ![内存泄露](../file/node_memory_leak.PNG) 229 | 230 | 但是如果对以上代码稍作修改的话,就会避免警告的发生 231 | 232 | ```js 233 | var util = require('util'); 234 | var EventEmitter = require('events').EventEmitter; 235 | 236 | var Server = function() { 237 | EventEmitter.call(this); 238 | } 239 | 240 | util.inherits(Server, EventEmitter); 241 | 242 | var server1 = new Server(); 243 | 244 | var connected = function() { 245 | console.log('connected'); 246 | } 247 | 248 | server1.setMaxListeners(11); 249 | 250 | for(var i = 0;i <= 10;i++){ 251 | server1.on('connected', connected); 252 | } 253 | 254 | console.log(emitter.listeners('connected')) 255 | ``` 256 | 257 | 我们可以指定上限数量,也可以这样 258 | 259 | ```js 260 | emitter.setMaxListeners(0); 261 | ``` 262 | 263 | 将参数设为0表示不设置监听上限数量,意味着这样一来,我们可以添加任意多个监听事件。 264 | 265 | ##### emitter.listeners(event) 266 | 返回指定事件的所有监听函数的集合。 267 | 268 | ```js 269 | var util = require('util'); 270 | var EventEmitter = require('events').EventEmitter; 271 | 272 | var Server = function() { 273 | EventEmitter.call(this); 274 | } 275 | 276 | util.inherits(Server, EventEmitter); 277 | 278 | var server1 = new Server(); 279 | 280 | var connected = function() { 281 | console.log('connected'); 282 | } 283 | 284 | for(var i = 0;i <= 2;i++){ 285 | server1.on('connected', connected); 286 | } 287 | 288 | console.log(server1.listeners('connected')); // [ [Function], [Function], [Function] ] 289 | ``` 290 | 291 | ##### emitter.emit(event, [arg1], [arg2], [...]) 292 | 触发或推送事件和指定参数(之前已经用的够多了,不需要再额外解释了)。 293 | 294 | ##### EventEmitter.listenerCount(emitter, event) 295 | 获取指定实例上,指定事件的数量。 296 | 297 | ```js 298 | var util = require('util'); 299 | var EventEmitter = require('events').EventEmitter; 300 | 301 | var Server = function() { 302 | EventEmitter.call(this); 303 | } 304 | 305 | util.inherits(Server, EventEmitter); 306 | 307 | var server1 = new Server(); 308 | 309 | var connected = function() { 310 | console.log('connected'); 311 | } 312 | 313 | for(var i = 0;i <= 2;i++){ 314 | server1.on('connected', connected); 315 | } 316 | 317 | console.log(EventEmitter.listenerCount(server1, 'connected')); // 3 318 | ``` 319 | 320 | ##### 事件'newListener'和'removeListener' 321 | 每次为实例添加任意事件时触发'newListener'、每次为实例移除任意事件时触发'removeListener' 322 | 323 | ```js 324 | var util = require('util'); 325 | var EventEmitter = require('events').EventEmitter; 326 | 327 | var Server = function() { 328 | EventEmitter.call(this); 329 | } 330 | 331 | util.inherits(Server, EventEmitter); 332 | 333 | var server1 = new Server(); 334 | 335 | var connected = function() { 336 | console.log('connected'); 337 | } 338 | 339 | server1.on('newListener', function(){ 340 | console.log('new listener is added!'); 341 | }); 342 | 343 | server1.on('removeListener', function(){ 344 | console.log('a listener is removed!'); 345 | }); 346 | 347 | for(var i = 0;i <= 9;i++){ 348 | server1.on('connected', connected); 349 | } 350 | 351 | for(var i = 0;i <= 3;i++){ 352 | server1.removeListener('connected', connected); 353 | } 354 | ``` 355 | 356 | ##### 总结 357 | 如果想要学好node,必须要彻底的了解事件的机制。因为事件时node的基础中的基础。好了,事件模块介绍到这里,就全部介绍完成了,如果需要熟练掌握,还是要多练习或者在实际项目中多运用才行。 358 | -------------------------------------------------------------------------------- /node/http.md: -------------------------------------------------------------------------------- 1 | ![nodejs,祝老婆大人、爸妈、岳父岳母身体健康,宝宝健康成长。新年快乐!](../file/nodejs.jpg) 2 | ##### 前言 3 | 在开始node.js(以后简称node)之前,我犹豫了好久:到底我第一篇用什么主题开始,是node的整体框架还是Hello world还是一堆废话呢。最后我决定还是直接上API,简单粗暴有效果,因为大家用node除了node有强大的社区之外,还因为node的API简单易用,容易上手。OK,下面我们就开始node之旅。 4 | 5 | ##### HTTP 6 | 大部分的node使用者,都是用node来做Web API的,而HTTP模块是提供Web API的基础。为了支持所有的HTTP应用,node中的HTTTP模块提供的API是偏向底层化的。利用HTTP模块,我们可以简单快速搭建一个Web Server。 7 | 8 | ##### 引入HTTP模块 9 | 在node[官方API](https://nodejs.org/dist/latest-v5.x/docs/api)中,第一句话是这么说的:“To use the HTTP server and client one must require('http')”,如果想要使用HTTP的服务器端和客户端的方法,首先第一步我们需要使用“require('http')”引入HTTP模块。 10 | 11 | ```js 12 | var http = require('http'); 13 | ``` 14 | 15 | 我们引入了HTTP模块,并且将结果赋值给了http变量,这样http就拥有了HTTP模块的所有方法和对象(关于requirejs的原理,与主题没什么太大关系,就略过。但是记住,require的过程是一个同步的过程,而前端的引入define是一个异步过程)。 16 | 17 | ##### 创建一个简单的Web Server 18 | 在HTTP模块中,node提供给我们一个createServer方法,来创建一个Web Server。代码可以这样写: 19 | 20 | ```js 21 | var http = require('http'); 22 | var server = http.createServer(); 23 | 24 | server.on('request', function(request, response) { 25 | console.log('有人请求了服务器!'); 26 | 27 | response.writeHead(200, { 28 | 'Content-Type': 'text/plain' 29 | }); 30 | response.write('OK'); 31 | response.end(); 32 | }); 33 | 34 | server.listen(88, function() { 35 | console.log('服务器已经开始监听88端口'); 36 | }); 37 | ``` 38 | 39 | 上面的代码: 40 | 41 | + 我们首先用http.createServer(从express4.X时代进入的同学可能有点陌生,express在4.X后把该方法封装到了其内部,但是我们在创建HTTPS连接时,还是要用到此方法)函数创建了一个服务器对象。 42 | 43 | + 然后监听了该对象的“request”事件(了解过node的同学应该知道,node是事件驱动的,一般事件触发和事件接收是成对出现的),这边我们定义接收事件。也就是说,如果当前服务器对象监听的端口一旦被请求,那么就会触发该事件。“request”的回调中,我们首先调用了response.writeHead方法:该方法的第一个参数表示HTTP的响应状态(200)表示一切正常;第二个参数是“Content-Type”,表示我响应给客户端的内容类型。然后我们调用了response.write方法,写入我们需要传递给客户端的内容。最后一步我们调用了response.end,表示此次请求已处理完成。 44 | 45 | + 最后我们调用了server.listen函数,此函数有两个参数,第一个参数表示我们需要监听的端口,第二个参数是回调函数(其实是listening事件),当监听开启后立刻触发。所以当我们用node命令来启动当前的js文件后,在cmd窗口会马上出现“服务器已经开始监听88端口”的字样,表明服务器正常且已经开始监听88端口。 46 | 47 | 当我们请求了服务器的88端口后,服务器会响应给我们“OK”字符串。到此我们的简单Web Server已经搭建完成,但是此时我们的代码还不具备任何业务能力,但是我们只要把代码稍稍修改下,就可以入眼了: 48 | 49 | ```js 50 | var url = require('url'); 51 | var http = require('http'); 52 | var server = http.createServer(); 53 | 54 | function bizLogic(url) { 55 | 56 | if (url.path.toLowerCase() === '/login') { 57 | return { 58 | msg: '登录成功!' 59 | } 60 | } else if (url.path.toLowerCase() === '/user') { 61 | return { 62 | msg: [{ 63 | name: '小明', 64 | class: '一年级1班' 65 | }, { 66 | name: '小红', 67 | class: '一年级3班' 68 | }] 69 | } 70 | } else { 71 | return { 72 | msg: 404 73 | } 74 | } 75 | } 76 | 77 | server.on('request', function(request, response) { 78 | 79 | if ('url' in request) { 80 | response.writeHead(200, { 81 | 'Content-Type': 'application/json; charset=utf-8' 82 | }); 83 | 84 | response.write(JSON.stringify(bizLogic(url.parse(request.url))), 'utf8'); 85 | } else { 86 | response.writeHead(404, { 87 | 'Content-Type': 'application/json; charset=utf-8' 88 | }); 89 | 90 | response.write('找不到页面'); 91 | } 92 | 93 | response.end(); 94 | }); 95 | 96 | server.listen(88, function() { 97 | console.log('服务器已经开始监听88端口'); 98 | }); 99 | ``` 100 | 101 | + 首先我们引入了一个新的模块:“url”。这个模块主要帮助我们将整理请求url中的信息即url.parse方法(如果有兴趣,你可以去研究下url的所有API,其实就几个)。url.parse转换完成之后,url参数信息就变成这样了: 102 | 103 | ```js 104 | { 105 | protocol: null, 106 | slashes: null, 107 | auth: null, 108 | host: null, 109 | port: null, 110 | hostname: null, 111 | hash: null, 112 | search: null, 113 | query: null, 114 | pathname: '/login', 115 | path: '/login', 116 | href: '/login' 117 | } 118 | ``` 119 | 120 | 这里我们取了我们需要的参数:path,简单的根据path来判断接口。然后响应客户端不同的内容。 121 | 122 | + 如果我们直接“response.write(bizLogic(url.parse(request.url)), 'utf8');”系统就会报错,response.write第一个参数必须是string或者Buffer,所以我们在这里将object转换成了string,然后接着write。 123 | 124 | + 最后我们直接结束连接(response.end)。 125 | 126 | 好了,一个简单的业务已经处理完成了,但是实际开过过程没有那么简单:HTTP方法有“GET”、“POST”、“PUT”等等。这也是要我们来处理的,所以如果我们用node来做Web API的话,一般会选择第三方的Web框架如:express、koa、restify等等(好吧,这是后话了,以后有机会我会去研究下他们各自的源码给大家分享)。 127 | 128 | ##### http.request 129 | 上面讲了那么多,其实都是在将HTTP Server的内容,既然node那么强大,当然除了可以作为HTTP Server之外,也可以做HTTP Client。现在就以我们刚刚建立的服务端为服务器,利用http.request来请求刚刚的服务器,看看会发生什么事情。 130 | 131 | ```js 132 | var http = require('http'); 133 | 134 | var option, req; 135 | 136 | option = { 137 | host: '127.0.0.1', 138 | port: 88, 139 | path: '/login' 140 | }; 141 | 142 | req = http.request(option, function(res){ 143 | 144 | if(res.statusCode === 200) { 145 | console.log('ok'); 146 | } 147 | 148 | res.on('data', function(chunk){ 149 | console.log('the result is ', chunk.toString()); 150 | }); 151 | }); 152 | 153 | req.on('error', function(error){ 154 | console.log('something is wrong:', error.message); 155 | }); 156 | 157 | req.end(); 158 | ``` 159 | 160 | + 首先我们当然要引入http模块,之后又定义了option和req对象。option是我们一会需要请求服务器的一些参数,req是请求对象用于接收request返回的结果(并不是请求的结果) 161 | 162 | + option参数是一个对象,使我们对此次请求设置的参数,包括:我们要请求的主机地址(host,注意此处不需要“http://”)、请求的请求的端口号(port)、请求的路由及参数(path) 163 | 164 | + 调用http.request请求,第一个参数就是我们刚刚设置的option,第二个参数是回调函数(用于返回我们请求的结果) 165 | 166 | + 回调函数有一个参数,这个参数是http.IncomingMessage的实例。字面意思就是“进来的消息”,也就是请求收到的响应结果对象。http.IncomingMessage实现了可读流接口,也就是说我们的“data”事件被触发时,回调函数中的结果是Buffer(关于Buffer我以后会详细解释---另一篇博客)对象,所以我在打印的时候调用了Bufer对象的toString方法把Buffer对象转换成字符串打印出来。 167 | 168 | + 最后我们监听了req的错误事件(在node中,我们如果出错不定义error事件的话,程序就会直接把错误抛出,如果没有做错误捕获的话,node程序就会崩掉),用于记录错误信息。最后我们关闭了请求。 169 | 170 | 经过这几个简单地步骤,我们此次请求就完成了,如果服务端没有错误的话,使用node命令运行我们刚刚写好的客户端代码,在cmd窗口先会打印“ok”,然后带打印“the result is {"msg":"登录成功!"}”。一旦请求出错,就会直接出发我们的“error”事件。当然如果不想使用原生的HTTP模块的话,你也可以使用第三方的包:[request](https://github.com/request/request)、[superagent](https://github.com/visionmedia/superagent)等都是非常优秀且方便的第三方包。(原生的HTTP模块可能会遇到各种奇葩的问题,使用第三方包的话吗,坑会少一些)。 171 | 172 | ##### 其他方法和事件 173 | 174 | + http.get是http.request的“GET”方法的方便版本,只能执行“GET”操作。 175 | 176 | + http.ClientRequest的“connection”事件,当TCP(HTTP其实也是TCP)的连接通道建立之后触发的事件。 177 | 178 | + http.ClientRequest的“close”事件,当服务端关闭连接时触发。 179 | 180 | + http.ServerResponse的setTimeout方法,设置响应超时。如果没有设置的话,只能等待socket或服务器超时。(还有很多方法和事件。就不一一列举了。) 181 | 182 | ##### 总结 183 | 在node v0.10.x时代,HTTP模块的稳定系数就已经是3了(一般node的API有4个级别,分别是:Deprecated-0、Experimental-1、Stable-2、Locked-3).所以说HTTP模块的API非常基础和稳定的。我们使用HTTP模块时,可以很放心的使用其方法和事件,不必担心版本兼容问题。需要提到的是,学习node的最好最捷径的方法就是学会读官方API,官方API虽然是英文的,但是里面的解释和英文单词都是很戳中要点和易于理解的。 184 | -------------------------------------------------------------------------------- /node/net.md: -------------------------------------------------------------------------------- 1 | ##### 前言 2 | 最近在公司多个项目同时开工,连更新博客的时间都没了。但是该做的还得做,不能找理由、找借口,今天就挤点时间出来写完一篇吧。今天要说的node模块可以一个大头戏:net。此模块可以很多其他模块的基础,稳定性和重要性不言而喻。 3 | 4 | ##### net.createServer([options], [connectionListener]) 5 | 创建一个TCP服务器,此函数有2个可选参数: 6 | 7 | + options 第一个参数是一个配置项,只有一个配置(allowHalfOpen)。表示允许服务器保持半开连接。 8 | 9 | + connectionListener 第二个参数是连接触发的事件函数,只要有客户端连接上来,此事件就会被触发。 10 | 11 | ```js 12 | // 创建一个服务器 13 | var net = require('net'); 14 | 15 | var server = net.createServer(function(){ 16 | console.log('有客户端连接!'); 17 | }); 18 | 19 | server.listen(8008, function(){ 20 | console.log('服务器一开始监听8008端口!'); 21 | }); 22 | ``` 23 | 24 | 运行以上代码,就会开启本机8008端口的TCP监听,当有客户端连接上来就会打印指定字符串:"有客户端连接!"(效果如下图)。 25 | 26 | ![老婆身体健康,宝宝快快成长](../file/net_createserver.png) 27 | 28 | ##### net.connect(options, [connectionListener])、net.createConnection(options, [connectionListener]) 29 | 这2个方法是一个实现逻辑,其实就是一个方法,node源码是这么写的: 30 | 31 | ```js 32 | exports.connect = exports.createConnection = function() { 33 | // ... 34 | }; 35 | ``` 36 | 37 | 此方法就是作为客户端去连接一个服务器端。此方法有2个参数,第一个参数(options)同样是配置项,第二个参数是一个可选参数,表示连接建立后推送的事件。 38 | 39 | + options 此参数是一个配置项,并且是必须的。配置项包括: 40 | + port 连接到的端口 41 | + host 对方(服务器)的主机地址,默认是"localhost" 42 | + localAddress 需要绑定的本地接口 43 | + path 在UNIX机器上的套接字路径 44 | + allowHalfOpen 是否允许半连接 45 | 46 | + connectionListener 连接建立之后推送的事件 47 | 48 | ```js 49 | // 创建一个服务器 50 | var net = require('net'); 51 | 52 | var server = net.createServer(function(){ 53 | console.log('有客户端连接!'); 54 | }); 55 | 56 | server.listen(8008, function(){ 57 | console.log('服务器一开始监听8008端口!'); 58 | }); 59 | ``` 60 | 61 | ```js 62 | // 创建一个客户端 63 | var net = require('net'); 64 | 65 | net.connect({ 66 | port: 8008 67 | }, function(){ 68 | console.log('已经连接到本机的8008端口'); 69 | }); 70 | ``` 71 | 分别运行上面的服务器和客户端代码,效果如下: 72 | 73 | ![老婆身体健康,宝宝快快成长](../file/client_server_connect.png) 74 | 75 | 另外这个方法还有简单版本,不需要配置options。 76 | 77 | ```js 78 | // 创建一个客户端 79 | var net = require('net'); 80 | 81 | net.connect(8008, function(){ 82 | console.log('已经连接到本机的8008端口'); 83 | }); 84 | ``` 85 | 86 | 或者 87 | 88 | ```js 89 | // 创建一个客户端 90 | var net = require('net'); 91 | 92 | net.connect('/root/c.sock', function(){ 93 | console.log('已经连接到本机的8008端口'); 94 | }); 95 | ``` 96 | 97 | ##### net.Server 98 | 这是一个类,用来创建TCPserver(上面我们提到的net.createServer方法返回的就是一个net.Server实例) 99 | 100 | #### server.listen(port, [host], [backlog], [callback]) 101 | 开启接受连接。此方法有4个参数: 102 | 103 | + port 需要监听的端口,如果端口传入0的话,当前服务器会监听一个随机端口。 104 | + host 此参数是一个可选参数,如果不传此参数,当前服务器会接受所有IPV4的连接。 105 | + backlog 挂起连接的数量(默认值为511) 106 | + callback 监听开启回调函数(即listening事件) 107 | 108 | ```js 109 | var Server = require('net').Server; 110 | var tcpServer = new Server(); 111 | 112 | // 开启监听8800端口 113 | tcpServer.listen(8800); 114 | 115 | // 监听开启事件 116 | tcpServer.on('listening', function(){ 117 | console.log('监听服务已开启,端口:8800'); 118 | }); 119 | ``` 120 | 121 | 以上代码很简单,首先开启了8800端口的监听,然后定义了listening事件,一旦开启8800端口监听成功,就会推送listening事件。当然,server.listen方法还有很多用法: 122 | 123 | ##### server.listen(path, [callback])、server.listen(handle, [callback]) 124 | 125 | + server.listen(path, [callback]) 表示连接一个UNIX套接字 126 | + server.listen(handle, [callback]) 表示根据传入的处理对象(server或者socket)进行连接 127 | 128 | ##### server.close([callback]) 129 | 停止接受新的连接,但是此方法调用之后现有的连接会保持,直到所有的连接都断开并且服务器触发了'close'事件之后,server才会真正的关闭。 130 | 131 | ```js 132 | var net = require('net'); 133 | var server = new net.Server(); 134 | 135 | server.listen(8800); 136 | 137 | server.on('listening', function() { 138 | console.log('started.'); 139 | }); 140 | 141 | setTimeout(function() { 142 | server.close(function() { 143 | console.log('closed.'); 144 | }); 145 | }, 3000); 146 | ``` 147 | 148 | 运行以上代码之后,如果3秒内没有连接,那么服务器会自动退出,否则会等待所有连接处理完毕,才会关闭服务器端。另外以上代码也可以这样写: 149 | 150 | ```js 151 | var net = require('net'); 152 | var server = new net.Server(); 153 | 154 | server.on('listening', function() { 155 | console.log('started.'); 156 | }); 157 | 158 | server.on('close', function() { 159 | console.log('closed.'); 160 | }); 161 | 162 | server.listen(8800); 163 | 164 | setTimeout(function() { 165 | server.close(); 166 | }, 3000); 167 | ``` 168 | 169 | ##### server.address() 170 | 此方法返回一个JSON数据,包括绑定的IP地址、IP类型、端口号。 171 | 172 | ```js 173 | var net = require('net'); 174 | var server = new net.Server(); 175 | 176 | server.on('listening', function() { 177 | console.log(server.address()); // { address: '0.0.0.0', family: 'IPv4', port: 8000 } 178 | }); 179 | 180 | server.listen(8000); 181 | ``` 182 | 183 | 注意,只有在服务器触发了'listening'事件之后,server.address方法才可以调用,否则此方法会返回null值: 184 | 185 | ```js 186 | var net = require('net'); 187 | var server = new net.Server(); 188 | 189 | console.log(server.address()); // null 190 | server.listen(8000); 191 | ``` 192 | 193 | ##### server.unref()、server.ref() 194 | server.unref方法会允许程序退出(在'事件系统'中只有当前服务器时) 195 | 196 | ```js 197 | var net = require('net'); 198 | var server = new net.Server(); 199 | 200 | server.listen(8000); 201 | server.unref(); 202 | ``` 203 | 204 | server.ref方法是server.unref的反操作,会阻止服务器退出(之前调用了server.unref) 205 | 206 | ```js 207 | var net = require('net'); 208 | var server = new net.Server(); 209 | 210 | server.listen(8000); 211 | server.unref(); 212 | 213 | setTimeout(function() { 214 | server.ref(); 215 | }, 3000); 216 | ``` 217 | 218 | 此时服务器不会因为调用了server.unref方法而退出。 219 | 220 | ##### server.getConnections(callback) 221 | 获取当前连接数。 222 | 223 | ```js 224 | var net = require('net'); 225 | var server = new net.Server(); 226 | 227 | server.on('listening', function() { 228 | console.log('server is listening.'); 229 | }); 230 | 231 | server.on('connection', function() { 232 | server.getConnections(function(err, count) { 233 | if(err) return; 234 | 235 | console.log('当前的连接数:%d', count); 236 | }); 237 | }); 238 | 239 | server.listen(8000); 240 | ``` 241 | 242 | ##### net.Server的事件 243 | net.Server的事件的事件有:'listening'、'connection'、'close'、'error',所有的事件都在上面的代码中间接介绍了。 244 | 245 | ##### net.Socket 246 | net.Socket是一个类,是TCP或UNIX套接字抽象。并且实现了复杂模式的Stream接口(关于Stream的更多信息,我会在以后的博客中介绍)。 247 | 248 | ##### new net.Socket([options]) 249 | 创建一个socket实例,此方法有一个可选的配置参数:options,其中有4个属性可以配置 250 | 251 | + fd 指定socket文件描述符(默认值为null) 252 | + allowHalfOpen 是否允许半连接(默认值为false) 253 | + readable 是否可读(默认值为false) 254 | + writable 是否可写(默认值为false) 255 | 256 | ```js 257 | var net = require('net'); 258 | var socket = new net.Socket({ 259 | allowHalfOpen: true 260 | }); 261 | 262 | socket.connect(8088); 263 | 264 | socket.on('connect', function() { 265 | console.log('The socket is established.'); 266 | }); 267 | 268 | socket.on('error', function() { 269 | console.log('Something is wrong.'); 270 | }); 271 | ``` 272 | 273 | ##### socket.connect(port, [host], [connectListener])、socket.connect(path, [connectListener]) 274 | 建立一个socket连接,并且可以指定端口、主机或者是连接文件。如果我们需要建立一个socket连接,可以这个干: 275 | 276 | 服务器端: 277 | 278 | ```js 279 | var net = require('net'); 280 | var server = new net.Server(); 281 | 282 | server.listen(8088, function() { 283 | console.log('listening.'); 284 | }); 285 | 286 | server.on('connection', function(socket) { 287 | console.log('someone is in.'); 288 | }); 289 | ``` 290 | 291 | 在服务器端,我们需要创建一个net.Server实例,用来作为服务器等待连接。 292 | 293 | 客户端: 294 | 295 | ```js 296 | var net = require('net'); 297 | var socket = new net.Socket({ 298 | allowHalfOpen: true 299 | }); 300 | 301 | socket.connect(8088); 302 | 303 | socket.on('connect', function() { 304 | console.log('The socket is established.'); 305 | }); 306 | 307 | socket.on('error', function() { 308 | console.log('Something is wrong.'); 309 | }); 310 | ``` 311 | 312 | 在客户端我们利用net.connect方法连接上了我们刚刚创建的服务器实例,并且定义了'connect'事件(socket连接建立立即推送的事件)和'error'事件。如果连接建立成功,在服务器端就会打印出“listening.”和“someone is in.”,在客户端并打印“The socket is established.”。如果出现出错,客户端会触发'error'事件(打印“Something is wrong.”)。 313 | 314 | ##### socket.bufferSize 315 | 待写入字符大小 316 | 317 | ```js 318 | var net = require('net'); 319 | var socket = new net.Socket({ 320 | allowHalfOpen: true 321 | }); 322 | 323 | socket.connect(8088); 324 | 325 | socket.on('connect', function() { 326 | console.log('The socket is established.'); 327 | console.log(socket.bufferSize); // 0 328 | }); 329 | ``` 330 | 331 | ##### socket.setEncoding([encoding]) 332 | 当socket被设置成可读时,此函数可用来设置Stream编码格式。 333 | 334 | ##### socket.write(data, [encoding], [callback]) 335 | 使用socket发送数据,此函数有2个可选参数: 336 | 337 | + encoding 如果发送的数据为字符串,可以设置编码(默认为UTF-8) 338 | + callback 当数据全部被处理之后调用的回调 339 | 340 | 客户端: 341 | 342 | ```js 343 | var net = require('net'); 344 | var socket = new net.Socket({ 345 | allowHalfOpen: true 346 | }); 347 | 348 | socket.connect(8088); 349 | 350 | socket.on('connect', function() { 351 | console.log('The socket is established.'); 352 | 353 | socket.write('Hello Socket', function() { 354 | console.log('done.'); 355 | }); 356 | }); 357 | ``` 358 | 359 | 服务器端: 360 | 361 | ```js 362 | var net = require('net'); 363 | var server = new net.Server(); 364 | 365 | server.listen(8088, function() { 366 | console.log('listening.'); 367 | }); 368 | 369 | server.on('connection', function(socket) { 370 | console.log('someone is in.'); 371 | 372 | socket.on('data', function(data) { 373 | console.log(data); // Hello Socket 374 | }); 375 | }); 376 | ``` 377 | 378 | ##### socket.end([data], [encoding]) 379 | 半关闭socket,如果传入了可选参数data,那么相当于调用了socket.write(data) 380 | 381 | ```js 382 | var net = require('net'); 383 | var socket = new net.Socket({ 384 | allowHalfOpen: true 385 | }); 386 | 387 | socket.connect(8088); 388 | 389 | socket.on('connect', function() { 390 | console.log('The socket is established.'); 391 | 392 | socket.end('bye.'); 393 | }); 394 | ``` 395 | 396 | ##### socket.destroy() 397 | 调用此方法后,除了一些出错信息,其他一切I/O都不可进行(可以认为是客户端主动关闭连接) 398 | 399 | ```js 400 | var net = require('net'); 401 | var socket = new net.Socket({ 402 | allowHalfOpen: true 403 | }); 404 | 405 | socket.connect(8088); 406 | 407 | socket.on('error', function(err) { 408 | console.log('error:' + err.message); // error:This socket is closed. 409 | }); 410 | 411 | socket.on('connect', function() { 412 | console.log('The socket is established.'); 413 | 414 | socket.destroy(); 415 | 416 | socket.write('Hello'); // 此数据将不被传输,因为连接已经关闭 417 | }); 418 | ``` 419 | 420 | ##### socket.pause()、socket.resume() 421 | 暂停读取数据、回复读取数据 422 | 423 | 发送端代码: 424 | ```js 425 | var net = require('net'); 426 | var socket = new net.Socket({ 427 | allowHalfOpen: true 428 | }); 429 | 430 | socket.connect(8088); 431 | 432 | socket.on('error', function(err) { 433 | console.log('error:' + err.message); 434 | }); 435 | 436 | socket.on('connect', function() { 437 | console.log('The socket is established.'); 438 | 439 | setInterval(function(){ 440 | socket.write(new Date() + 'Hello'); 441 | }, 1000); 442 | }); 443 | ``` 444 | 445 | 接收端代码: 446 | ```js 447 | var net = require('net'); 448 | var server = new net.Server({ 449 | allowHalfOpen: true 450 | }); 451 | 452 | server.listen(8088, function() { 453 | console.log('listening.'); 454 | }); 455 | 456 | server.on('connection', function(socket) { 457 | console.log('someone is in.'); 458 | 459 | socket.on('data', function(data) { 460 | console.log(data.toString()); 461 | }); 462 | 463 | socket.pause(); 464 | 465 | setTimeout(function(){ 466 | socket.resume(); 467 | }, 1500); 468 | }); 469 | ``` 470 | 471 | ##### socket.setTimeout(timeout, [callback]) 472 | 设置socket超时,超时(指定时间)被触发之后,此socket将继续可用,我们必须手动调用socket.end或socket.destroy方法来中断连接 473 | 474 | 服务器端: 475 | ```js 476 | var net = require('net'); 477 | var server = new net.Server({ 478 | allowHalfOpen: true 479 | }); 480 | 481 | server.listen(8088, function() { 482 | console.log('listening.'); 483 | }); 484 | 485 | server.on('connection', function(socket) { 486 | console.log('someone is in.'); 487 | 488 | socket.on('data', function(data) { 489 | console.log(data.toString()); 490 | }); 491 | }); 492 | ``` 493 | 494 | 客户端: 495 | ```js 496 | var net = require('net'); 497 | var socket = new net.Socket({ 498 | allowHalfOpen: true 499 | }); 500 | 501 | socket.on('timeout', function() { 502 | console.log('timeout'); 503 | 504 | socket.end('end'); 505 | }); 506 | 507 | socket.setTimeout(2000); 508 | socket.connect(8088); 509 | ``` 510 | 511 | ##### socket.setNoDelay([noDelay]) 512 | 设置无延迟发送(关闭Nagle算法)。将数据立即发送出去,而无需使用Nagle算法。 513 | 514 | ##### socket.setKeepAlive([enable], [initialDelay]) 515 | 开启心跳 516 | 517 | + enable 是否开启心跳(默认false) 518 | + initialDelay 心跳间隔 519 | 520 | ##### socket.address()、socket.unref()、socket.ref() 521 | 这3个方法与上面net.Server的3个方法类似,这里就不赘述 522 | 523 | ##### 其他属性 524 | 525 | + socket.remoteAddress 远程地址 526 | + socket.localAddress 本地地址 527 | + socket.remotePort 远程端口 528 | + socket.localPort 本地端口 529 | + socket.bytesRead 已接收字节数 530 | + socket.bytesWritten 已发送字节数 531 | 532 | ##### 事件'connect'、'data'、 'end'、'timeout'、'error'、'close' 533 | 以上的事件都在上面的代码中使用过,已经熟悉了。而'drain'事件表示写入buffer为空时触发的事件(可用于上传逻辑)。 534 | 535 | ##### net.isIP(input)、net.isIPv4(input)、net.isIPv6(input) 536 | 537 | + net.isIP(input) 判断是否是IP地址(如果是IPV4就返回数字4,如果是IPV6就返回数字6,如果都不是则返回0): 538 | 539 | ```js 540 | var net = require('net'); 541 | 542 | console.log(net.isIP('172.16.0.90')); // 4 543 | console.log(net.isIP('2001:0DB8:02de::0e13')); // 6 544 | ``` 545 | 546 | + net.isIPv4(input) 判断是否是IPV4 547 | 548 | ```js 549 | var net = require('net'); 550 | 551 | console.log(net.isIPv4('172.16.0.90')); // true 552 | console.log(net.isIPv4('2001:0DB8:02de::0e13')); // false 553 | ``` 554 | 555 | + net.isIPv6(input) 判断是否是IPV6 556 | 557 | ```js 558 | var net = require('net'); 559 | 560 | console.log(net.isIPv6('172.16.0.90')); // false 561 | console.log(net.isIPv6('2001:0DB8:02de::0e13')); // true 562 | ``` 563 | 564 | ##### 总结 565 | 讲到这里,net模块就讲完了。其实与其他语言比较起来,使用node来做socket代码非常简洁并且天生异步的模式也非常符合生产环境使用。 566 | 567 | ##### 题外话 568 | 这边博客与上一篇间隔时间很长,大概有20多天的样子。期间忙于公司的事情与自我学习(主要是docker方向的),并且看到在[cnode](https://cnodejs.org/)已经有人在写关于node源码的文章了,所以我觉得关于node源码的部分我不全部讲解,只是在个别模块与注意点上写几篇跟人观点。 569 | -------------------------------------------------------------------------------- /node/path.md: -------------------------------------------------------------------------------- 1 | ##### 介绍 2 | path模块其实就是对路径的处理,而路径实际就是字符串。那么,说到底path模块就是对字符串的一些特殊处理。这个模块非常简单,拥有很少的API,但是非常实用,在实际开发过程中,如果没有了解path模块或者不够熟悉的话,可能会走很多弯路(这也说明了基础API的重要性)。 3 | 4 | ##### path.normalize(p) 5 | 我们知道,一般而言在程序中“..”和“.”表示上一级文件夹路径和当前路径。而path.normalize方法就是专注于处理这两个符号,并返回对应的路径。 6 | 7 | ```js 8 | var path = require('path'); 9 | 10 | console.log(path.normalize('/a/b/./c')); // \a\b\c 11 | console.log(path.normalize('/a/b/../c')); // \a\c 12 | console.log(path.normalize('/a/b/c/../')); // \a\b\ 13 | console.log(path.normalize('/a/b//c')); // \a\b\c 14 | console.log(path.normalize('/a/b/c/.../')); // \a\b\c\...\ 15 | ``` 16 | 17 | 根据以上代码我们可以看出,此方法只是对“..”和“.”进行处理,并且返回处理结果。需要注意的是,如果是多个斜杠(比如上述代码第四个log),path.normalize会把其处理成一个斜杠。 18 | 19 | ##### path.join([path1], [path2], [...]) 20 | 把多个路径参数拼接成一个路径,并且会用path.normalize处理拼装结果。 21 | 22 | ```js 23 | var path = require('path'); 24 | 25 | console.log(path.join('/a', '/n', '/b', '/h')); // \a\n\b\h 26 | console.log(path.join('/a', '/n', '..', '/h')); // \a\h 27 | console.log(path.join('/a', '/../../', '/h')); // \h 28 | ``` 29 | 30 | path.join的所有参数都是字符串,但是如果你非要捣蛋的话(比如我传入一个object),那会出现什么情况呢。 31 | 32 | ```js 33 | var path = require('path'); 34 | 35 | console.log(path.join('/a', {}, '/h')); // throw new TypeError('Arguments to path.join must be strings'); 36 | ``` 37 | 38 | 好吧,报错了!看来还是不能瞎搞的! 39 | 40 | ##### path.resolve([from ...], to) 41 | 这个方法在官方有两种解释,第一种解释我噼里啪啦讲了半天都没看懂,然后看到下面才发现有第二种解释(简单容易理解)。 42 | 43 | ```js 44 | var path = require('path'); 45 | 46 | console.log(path.resolve('/foo/bar', '/bar/faa', '..', 'a/../c')); // C:\bar\c 47 | ``` 48 | 49 | 以上的代码就相当于一堆shell脚本 50 | 51 | ``` 52 | cd /foo/bar 53 | cd /bar/faa 54 | cd .. 55 | cd a/../c 56 | pwd 57 | ``` 58 | 59 | 代码与shell脚本的区别在于,代码所针对的路径不一定要存在并且可以是文件: 60 | 61 | ```js 62 | var path = require('path'); 63 | 64 | console.log(path.resolve('/foo/bar', '/bar/faa', '..', 'a/../c.txt')); // C:\bar\c.txt 65 | ``` 66 | 67 | ##### path.relative(from, to) 68 | 返回从from到to的相对路径,比如我们要在from对应的文件夹中引用to文件夹中的文件,那么我们就可以使用这个方法,例如: 69 | 70 | 首先我们有一个test文件夹,里面有一个文件:test.js(F:\jsresearch\test\test.js) 71 | 72 | ```js 73 | function showLevel(){ 74 | return 'The level is 5'; 75 | } 76 | 77 | exports.showLevel = showLevel; 78 | ``` 79 | 80 | 然后我们在与test文件夹平级的nodejs文件夹中引用test.js文件,我们可以这么做 81 | 82 | ```js 83 | var path = require('path'); 84 | var test = require(path.relative(__dirname, 'F:\\jsresearch\\test\\test.js')); 85 | 86 | console.log(test.showLevel()); // The level is 5 87 | ``` 88 | 89 | 当我们调用showLevel方法时,就可以正常调用了。其实以上的代码等价于: 90 | 91 | ```js 92 | var path = require('path'); 93 | var test = require('../test/test.js'); 94 | 95 | console.log(test.showLevel()); // The level is 5 96 | ``` 97 | 98 | ##### path.dirname(p) 99 | 获取指定路径的目录 100 | 101 | ```js 102 | var path = require('path'); 103 | 104 | console.log(path.dirname('/ab/cd/a.txt')); // /ab/cd 105 | console.log(path.dirname('/ab/cd/a')); // /ab/cd 106 | console.log(path.dirname('/ab/cd/a/')); // /ab/cd 107 | ``` 108 | 109 | ##### path.basename(p, [ext]) 110 | 获取指定路径文件的文件名 111 | 112 | ```js 113 | var path = require('path'); 114 | 115 | console.log(path.basename('/ab/c.txt')); // c.txt 116 | console.log(path.basename('/ab/c.txt', '.txt')); // c 117 | console.log(path.basename('/ab/c.txt', 'txt')); // c. 118 | console.log(path.basename('/ab/c.txt', 'xt')); // c.t 119 | ``` 120 | 121 | 第一个参数表示路径,第二个参数表示需要除去的结尾字符。举个栗子:如果第二个参数是“xt”那么我们得到的结果就会删除“xt”字符。 122 | 123 | ##### path.extname(p) 124 | 获取对应文件的扩展名 125 | 126 | ```js 127 | var path = require('path'); 128 | 129 | console.log(path.extname('/ab/c.txt')); // .txt 130 | console.log(path.extname('/ab/c.html')); // .html 131 | console.log(path.extname('/ab/c.md')); // .md 132 | console.log(path.extname('/ab/c.')); // . 133 | ``` 134 | 135 | ##### path.sep、path.delimiter 136 | + path.sep 路径分隔符号,一般在Windows是反斜杠,在linux是斜杠 137 | + path.delimiter 路径分割符号,一般在Windows是分号,在linux是冒号,用于分割多个路径 138 | 139 | ##### 总结 140 | path模块非常简单,此模块专注于路径(也就是字符串)的特殊处理。 141 | -------------------------------------------------------------------------------- /node/qs_url.md: -------------------------------------------------------------------------------- 1 | ##### 前言 2 | 由于Query Strings和URL模块的API都比较少和简单,所以我把这两个模块放在一起讲。 3 | 4 | ##### Query Strings 5 | 相信从字面上理解,大家就已经知道此模块是用于处理Query String的。 6 | 7 | ##### querystring.stringify(obj, [sep], [eq]) 8 | 此方法可以将object转换成字符串,三个参数分别的意义是: 9 | 10 | + obj 待转换的对象 11 | 12 | + sep 此参数是一个可选参数,表示多个参数之间的分隔符(一般使用'&'作为分隔符,默认是'&') 13 | 14 | + eq 此参数也是一个可选参数,表示参数与对应值之间的分隔符(一般使用'='作为分隔符,默认是'=') 15 | 16 | ```js 17 | var querystring = require('querystring'); 18 | 19 | var obj = { 20 | method: 'add', 21 | user: 'admin', 22 | age: 31, 23 | score: [98, 91] 24 | }; 25 | 26 | console.log(querystring.stringify(obj)); // method=add&user=admin&age=31&score=98&score=91 27 | console.log(querystring.stringify(obj, ';', ':')); // method:add;user:admin;age:31;score:98;score:91 28 | ``` 29 | 30 | ##### querystring.parse(str, [sep], [eq], [options]) 31 | querystring.parse是querystring.stringify方法的逆。参数sep和eq与querystring.stringify有着相同的意义,但是此方法有第四个可选参数,此参数是可以配置项内目前为止就一个配置参数:maxKeys,表示最大处理的keys数量,超过此数量就默认略过(maxKeys的默认值为1000) 32 | 33 | ```js 34 | var querystring = require('querystring'); 35 | 36 | console.log(querystring.parse('method=add&user=admin&age=31&score=98&score=91')); // { method: 'add', user: 'admin', age: '31', score: [ '98', '91' ] } 37 | console.log(querystring.parse('method:add;user:admin;age:31;score:98;score:91', ';', ':')); // { method: 'add', user: 'admin', age: '31', score: [ '98', '91' ] } 38 | console.log(querystring.parse('method=add&user=admin&age=31&score=98&score=91', '&', '=', { maxKeys: 2 })); // { method: 'add', user: 'admin' } 39 | ``` 40 | 41 | 这里需要注意的是,如果我们需要用到options参数,那么sep和eq是不可以省略的。不然的话,解析会出乎于预期: 42 | 43 | ```js 44 | var querystring = require('querystring'); 45 | 46 | console.log(querystring.parse('method=add&user=admin&age=31&score=98&score=91', { maxKeys: 2 })); // { method: 'add&user=admin&age=31&score=98&score=91' } 47 | ``` 48 | 49 | ##### querystring.escape、querystring.unescape 50 | querystring.escape其实就是对字符串进行urlencode,而querystring.unescape则是对字符串进行urldecode 51 | 52 | ```js 53 | var querystring = require('querystring'); 54 | 55 | console.log(querystring.escape('a=ss&b=111')); // a%3Dss%26b%3D111 56 | console.log(querystring.unescape('a%3Dss%26b%3D111')); // a=ss&b=111 57 | ``` 58 | 59 | ##### URL 60 | 此模块是处理url相关的问题。 61 | 62 | ##### url.parse(urlStr, [parseQueryString], [slashesDenoteHost]) 63 | 将字符串转换成object对象,与querystring.parse功能类似。 64 | 65 | + urlStr 表示需要转换的字符串 66 | 67 | + parseQueryString 如果将此项设置为true,则表示使用querystring模块转换querystring 68 | 69 | + slashesDenoteHost 如果此项设置为true,则表示将双斜线后的当做为host 70 | 71 | ```js 72 | var url = require('url'); 73 | 74 | console.log(url.parse('HTTP://admin:123@172.16.0.1:3000/a/b/c?test=true#list')); // { protocol: 'http:', 75 | // slashes: true, 76 | // auth: 'admin:123', 77 | // host: '172.16.0.1:3000', 78 | // port: '3000', 79 | // hostname: '172.16.0.1', 80 | // hash: '#list', 81 | // search: '?test=true', 82 | // query: 'test=true', 83 | // pathname: '/a/b/c', 84 | // path: '/a/b/c?test=true', 85 | // href: 'http://admin:123@172.16.0.1:3000/a/b/c?test=true#list' } 86 | 87 | console.log(url.parse('HTTP://admin:123@172.16.0.1:3000/a/b/c?test=true#list', true)); // { protocol: 'http:', 88 | // slashes: true, 89 | // auth: 'admin:123', 90 | // host: '172.16.0.1:3000', 91 | // port: '3000', 92 | // hostname: '172.16.0.1', 93 | // hash: '#list', 94 | // search: '?test=true', 95 | // query: { test: 'true' }, 96 | // pathname: '/a/b/c', 97 | // path: '/a/b/c?test=true', 98 | // href: 'http://admin:123@172.16.0.1:3000/a/b/c?test=true#list' } 99 | 100 | console.log(url.parse('//admin:123@172.16.0.1:3000/a/b/c?test=true#list', true, true)); // { protocol: null, 101 | // slashes: true, 102 | // auth: 'admin:123', 103 | // host: '172.16.0.1:3000', 104 | // port: '3000', 105 | // hostname: '172.16.0.1', 106 | // hash: '#list', 107 | // search: '?test=true', 108 | // query: { test: 'true' }, 109 | // pathname: '/a/b/c', 110 | // path: '/a/b/c?test=true', 111 | // href: 'http://admin:123@172.16.0.1:3000/a/b/c?test=true#list' } 112 | ``` 113 | 114 | ##### url.format(urlObj) 115 | 此方法是url.parse的逆方法,就是将object转换成字符串 116 | 117 | ```js 118 | var url = require('url'); 119 | 120 | var obj = { 121 | protocol: 'http:', 122 | slashes: true, 123 | auth: 'admin:123', 124 | host: '172.16.0.1:3000', 125 | port: '3000', 126 | hostname: '172.16.0.1', 127 | hash: '#list', 128 | search: '?test=true', 129 | query: { 130 | test: 'true' 131 | }, 132 | pathname: '/a/b/c', 133 | path: '/a/b/c?test=true', 134 | href: 'http://admin:123@172.16.0.1:3000/a/b/c?test=true#list' 135 | }; 136 | 137 | console.log(url.format(obj)); // http://admin:123@172.16.0.1:3000/a/b/c?test=true#list 138 | ``` 139 | 140 | ##### url.resolve(from, to) 141 | 可以简单地理解成:用后面的对应的属性替换前面的属性。过程可以理解成(内部过程可能更加复杂,以下步骤仅供理解,不是官方步骤): 142 | 143 | + 第一步:调用url.parse 144 | + 第二步:Object.assign 145 | + 第三部:url.stringify 146 | 147 | ```js 148 | var url = require('url'); 149 | 150 | var url = require('url'); 151 | 152 | console.log(url.resolve('/one/two/three', '//host.com:300/four')) // //host.com:300/four 153 | console.log(url.resolve('http://example.com/', '/one')) // 'http://example.com/one' 154 | console.log(url.resolve('http://example.com/one', 'http://ssss.com/one/one/two')) // 'http://ssss.com/one/one/two' 155 | ``` 156 | 157 | ##### 总结 158 | Query Strings模块和URL模块其实都是非常实用的两个模块,但是由于我们做node一般都使用第三方框架,如:express、koa、restify等。里面都已经封装好了对应的方法,所以这两个模块可能在实际开发中用到的不多,但是在做node原生开发时(不使用第三方框架),会非常有用。 159 | -------------------------------------------------------------------------------- /node/readline.md: -------------------------------------------------------------------------------- 1 | ##### 前言 2 | 近一段时间工作太忙,并且在研究docker相关的技术,好久没更新博客了。今天趁任务完成的早,在公司补上一篇。今天要讲的这个模块,让我瞬间就回到了刚学C#的时候,做一些小程序逗同学的年代。今天我要介绍的模块是Readline(记得C#考试的时候Console.ReadLine这个方法我是运用了我的英语知识拼出来的,现在想想都是泪呀!) 3 | 4 | ##### Readline 5 | 这个模块可以读取stream,比如说process.stdin。以前对stdin也是不太理解,最近研究docker就对stdin概念稍稍了解了一下下。std是standard(标准)的缩写,in就是输入,组合起来解释就是标准输入,而stdout我的理解就是标准输出(见识浅薄,大神勿喷)。而Readline可以读取process.stdin,我们就可以利用这一点,使得程序通过控制台的标准输入输出与用户进行简单的交互。 6 | 7 | ```js 8 | var readline = require('readline'); 9 | 10 | var rl = readline.createInterface({ 11 | input: process.stdin, 12 | output: process.stdout 13 | }); 14 | 15 | rl.question('你是2B吗?', function(answer){ 16 | 17 | if(answer === '是'){ 18 | console.log('好吧,再见!2B!'); 19 | rl.close(); 20 | } 21 | 22 | if(answer === '不是'){ 23 | console.log('no no no,显然你就是2B,有本事来咬我呀!咬我呀!咬我呀!'); 24 | } 25 | }); 26 | ``` 27 | 28 | 好吧,我承认代码写的是有点逗逼(以前也经常这样逗同学)。但是需要注意的是,如果我们激活了Readline模块,并且使用了它,node程序是不会自动退出的(除非我们使用代码,比如上面的rl.close()或者直接关闭控制台)。 29 | 30 | ##### readline.createInterface(options) 31 | 创建一个readline的Interface实例,使用Interface实例,我们就可以与用户交互了。此方法有一个参数,此参数是一个配置项: 32 | 33 | + input 设置需要监听哪个可读流,例如:process.stdin 34 | 35 | + output 设置需要将可写流写入到哪里 36 | 37 | + completer 自动完成函数 38 | 39 | + terminal 如果设置为true则把输入输出看做一个虚拟终端 40 | 41 | 输入、输出就不多讲了,我们把输入输出分别设置成process.stdin和process.stdout。先来讲讲completer 42 | 43 | ```js 44 | var readline = require('readline'); 45 | 46 | var completer = function(line){ 47 | var completions = ['Hello', 'World', 'You', 'are', 'man']; 48 | 49 | var shots = completions.filter(function(s){ 50 | return s.indexOf(line) == 0; 51 | }); 52 | 53 | return [shots, line]; 54 | } 55 | 56 | var rl = readline.createInterface({ 57 | input: process.stdin, 58 | output: process.stdout, 59 | completer: completer 60 | }); 61 | ``` 62 | 63 | 运行以上代码,当我们输入'H'然后按TAB键时,就会自动补全'Hello'这个单词。 64 | 65 | ##### rl.setPrompt(prompt, length) 66 | 此方法包含两个参数,第一个参数表示命令行最前面的“前缀”,就像我们直接运行node安装路径下的node.exe程序,最前面会有一个'>'符号,我们也可以通过这个方法在Readline中设置我们自己的“前缀”: 67 | 68 | ```js 69 | var readline = require('readline'); 70 | 71 | var completer = function(line){ 72 | var completions = ['Hello', 'World', 'You', 'are', 'man']; 73 | 74 | var shots = completions.filter(function(s){ 75 | return s.indexOf(line) == 0; 76 | }); 77 | 78 | return [shots, line]; 79 | } 80 | 81 | var rl = readline.createInterface({ 82 | input: process.stdin, 83 | output: process.stdout, 84 | completer: completer 85 | }); 86 | 87 | rl.setPrompt('test>>', 6); 88 | ``` 89 | 90 | 就拿我们刚刚的“自动补全代码”为例,在最后添加了一句代码。我们的在进行刚刚的操作:输入'H'然后按TAB键。就会出现'test>>'在'Hello'之前。此方法第二个参数官方API中也没有说明,并且在最新版本的API(V5.9.1)中,已经没有这个参数。但是以我打破砂锅问到底的精神,我去翻阅了老版本的node源码,发现这个length参数是用来设置光标位置的。其实上面的代码非常傻,只有我们在进行了一系列操作后才会出现“前缀”,还有一个看起来更舒服的处理方式,让我们的程序看起来更专业。 91 | 92 | ##### rl.prompt([preserveCursor]); 93 | 此方法效果就是在新的行前主动添加我们设置的“前缀”,参数为布尔型,我们如果传入true,可以防止光标位置被重置为0: 94 | 95 | ```js 96 | var readline = require('readline'); 97 | 98 | var rl = readline.createInterface({ 99 | input: process.stdin, 100 | output: process.stdout 101 | }); 102 | 103 | rl.setPrompt('test>>', 6); 104 | rl.on('line', function (cmd) { 105 | rl.prompt(); 106 | }); 107 | rl.prompt(); 108 | ``` 109 | 110 | 以上代码的效果如下: 111 | 112 | ![rl.prompt](../file/rl_prompt.png) 113 | 114 | ##### rl.question(query, callback) 115 | 此方法就是我在最开始使用的引入示例,此方法的功能就是先输出query,然后接收用户输入的内容,当检测到用户输入回车时,调用callback并且将用户刚刚输入的内容以回调参数的形式给出,过多的示例我就不举了,上面“2B”示例已经很2了,不想再2一次了。 116 | 117 | ##### rl.pause()、rl.resume()、rl.close() 118 | 暂停接收输入、恢复接收输入。 119 | 120 | ```js 121 | var readline = require('readline'); 122 | 123 | var rl = readline.createInterface({ 124 | input: process.stdin, 125 | output: process.stdout 126 | }); 127 | 128 | setTimeout(function(){ 129 | rl.pause(); 130 | }, 1000); 131 | 132 | setTimeout(function(){ 133 | rl.resume(); 134 | }, 4000); 135 | 136 | setTimeout(function(){ 137 | rl.resume(); 138 | }, 6000); 139 | ``` 140 | 141 | 运行以上代码,1秒之后会有3秒的空白(不可输入),到4秒之后又可以输入。然后6秒的时候程序自动退出了。 142 | 143 | ##### rl.write(data, [key]) 144 | 想stdout输出data,key是一个object对象,可以模拟键盘按键。以下代码就是模拟了ctrl+u 145 | 146 | ```js 147 | var readline = require('readline'); 148 | 149 | var rl = readline.createInterface({ 150 | input: process.stdin, 151 | output: process.stdout 152 | }); 153 | 154 | rl.write('这些文字回本显示出来!3秒后消失!'); 155 | 156 | setTimeout(function(){ 157 | rl.write(null, {ctrl: true, name: 'u'}); 158 | }, 3000); 159 | ``` 160 | 161 | ##### 事件:'line'、'pause'、'resume'、'close' 162 | 分别在换行、暂停、恢复、关闭时触发事件,都非常简单,这里就不多做介绍。 163 | 164 | ##### 事件'SIGINT' 165 | 表示终端信号(一般ctrl+c表示终端信号) 166 | 167 | ```js 168 | var readline = require('readline'); 169 | 170 | var rl = readline.createInterface({ 171 | input: process.stdin, 172 | output: process.stdout 173 | }); 174 | 175 | rl.on('SIGINT', function(){ 176 | rl.question('是否要退出程序?', function(answer) { 177 | if (answer.match(/^y(es)?$/i)) rl.close(); 178 | else rl.prompt(); 179 | }); 180 | }); 181 | ``` 182 | 183 | ##### 事件'SIGTSTP'、'SIGCONT' 184 | 分别表示接收到暂停信号、程序切换到后台运行时触发(这2个事件在Windows上是无法正常工作的) 185 | 186 | ##### 其他方法 187 | + readline.cursorTo(stream, x, y) 将光标移动到stream的指定位置 188 | 189 | + readline.moveCursor(stream, dx, dy) 将光标移动到stream的相对位置置 190 | + readline.clearLine(stream, dir) 清除当前行 191 | 192 | + readline.clearScreenDown(stream) 清除当屏幕 193 | 194 | 195 | ##### 总结 196 | 此模块不是一个非常常用的模块,但是我们可以利用此模块写出很好玩的程序,例如:一个简单的终端程序,或者可以整一下不懂程序的同学。 197 | -------------------------------------------------------------------------------- /node/stream-chapter1.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 今天要讲的模块(stream,下称流)比较特殊,在node官网的API文档中分成了3个部分:第一部分是介绍流的用法,第二部分是介绍如何实现自定义的流,第三部分是介绍流的原理与内部实现机制。针对这3个部分,我会分3篇博客来讲,现在就来讲讲流的用法(第一部分,也是最基础的部分) 3 | 4 | # 概念 5 | 流其实不是一个实质的对象或方法,流是一些抽象的接口,只有这些接口被实现了,流才有了真正的意义。所有的流都是EventEmitter的实例,也就是说所有的流都可以触发、响应事件。 6 | 7 | # 分类 8 | 从功能上讲,流一般可以分为“读取流”、“写入流”、“复杂流”:读取流用来读取,写入流用来写入,复杂流可以读写。 9 | 10 | # stream.Readable 11 | stream.Readable是一个类,利用这个类,我们可以读取流里面的内容。可读流有2中模式:flowing-mode、non-flowing-mode 12 | 13 | + flowing-mode 此模式会尽快把数据读取出来 14 | 15 | + non-flowing-mode 此模式需要显示调用读取方法,才会把数据读取出来 16 | 17 | ### 事件 'readable' 18 | 当流中有可读取的数据块时,就会触发该事件。 19 | 20 | ```js 21 | var fs = require('fs'); 22 | 23 | var readableStream = fs.createReadStream('file.txt'); 24 | 25 | readableStream.on('readable', function(){ 26 | console.log('有数据进来!'); 27 | }); 28 | ``` 29 | 30 | 如果读取到file.txt的数据,那么'readable'就会被触发,在终端上回打印“有数据进来!” 31 | 32 | ### 事件 'data' 33 | 一旦监听了'data'事件,会将流的模式转换成“flowing-mode”,也就是说数据会被尽快读出。 34 | 35 | ```js 36 | var fs = require('fs'); 37 | 38 | var readableStream = fs.createReadStream('file.txt'); 39 | 40 | readableStream.on('data', function(data){ 41 | console.log('data:%s', data); 42 | }); 43 | ``` 44 | 45 | ### 事件 'end' 46 | 当没有可以读数据之后,会触发此事件 47 | 48 | ```js 49 | var fs = require('fs'); 50 | 51 | var readableStream = fs.createReadStream('file.txt'); 52 | 53 | readableStream.on('data', function(data){ 54 | console.log('data:%s', data); 55 | }); 56 | 57 | readableStream.on('end', function(){ 58 | console.log('所有数据读取完毕!'); 59 | }); 60 | ``` 61 | 62 | ### 事件 'close' 63 | 当基础资源被关闭之后,会触发此事件。 64 | 65 | ### 事件 'error' 66 | 当接收数据出错时,触发此事件 67 | 68 | ```js 69 | var fs = require('fs'); 70 | 71 | var readableStream = fs.createReadStream('/NotExistPath/file.txt'); 72 | 73 | readableStream.on('error', function(err){ 74 | console.log('错误:%s', err.message); // 错误:ENOENT, open 'F:\NotExistPath\file.txt' 75 | }); 76 | ``` 77 | 78 | ### readable.read([size]) 79 | 显示读取数据(此方法只能在“non-flowing-mode”使用),此方法有一个可选参数size,表示一次要读取的数据。如果不传入size的话,表示读取所有数据,另外如果没有数据的话此方法会返回null。 80 | 81 | ```js 82 | var fs = require('fs'); 83 | 84 | var readableStream = fs.createReadStream('file.txt'); 85 | 86 | readableStream.on('readable', function(){ 87 | console.log('有数据进来!数据:' + readableStream.read()); // 有数据进来!数据:我到底二不二 88 | }); 89 | ``` 90 | 91 | ### readable.setEncoding(encoding) 92 | 设置可读流的读出数据的编码格式,并且会强制将数据转换成字符串 93 | 94 | ### readable.resume() 95 | 恢复推送'data'事件,并且将模式转换成“flowing-mode” 96 | 97 | ```js 98 | var fs = require('fs'); 99 | 100 | var readableStream = fs.createReadStream('file.txt'); 101 | 102 | readableStream.resume(); 103 | 104 | readableStream.on('end', function(){ 105 | console.log('所有数据读取完毕!'); 106 | }); 107 | ``` 108 | 109 | ### readable.pause() 110 | 暂停触发'data'事件 111 | 112 | ```js 113 | var fs = require('fs'); 114 | 115 | var readableStream = fs.createReadStream('file.txt'); 116 | 117 | readableStream.pause(); 118 | 119 | readableStream.on('data', function(data){ 120 | console.log('data:%s', data); // 此处不会得到数据,因为我们调用了readableStream.pause 121 | }); 122 | ``` 123 | 124 | ### readable.pipe(destination, [options]) 125 | 此方法会将可读流里的所有数据读取出来,并写入destination(通常是一个写入流对象)。此方法还有一个可选的配置参数options: 126 | 127 | + end 默认为true,表示数据读取结束之后是否需要关闭写入流。 128 | 129 | ```js 130 | var fs = require('fs'); 131 | 132 | var readableStream = fs.createReadStream('file.txt'); 133 | var writableStream = fs.createWriteStream('file1.txt'); 134 | 135 | readableStream.pipe(writableStream); 136 | ``` 137 | 138 | 需要注意的是,此方法的返回值还是一个可读的流,所以可以链式调用。 139 | 140 | ```js 141 | var fs = require('fs'); 142 | var zlib = require('zlib'); 143 | 144 | var readableStream = fs.createReadStream('file.txt'); 145 | var writableStream = fs.createWriteStream('file1.txt'); 146 | var zStream = zlib.createGzip(); 147 | 148 | readableStream.pipe(zStream).pipe(writableStream); 149 | ``` 150 | 151 | ### readable.unpipe([destination]) 152 | 此操作为readable.pipe的逆操作,调用之后readable中读取出来的数据块不会再写入destination,destination是可选参数。如果我们没有指定destination,那么readable数据块不会任何一个destination中。 153 | 154 | ### readable.unshift(chunk) 155 | 这个方法比较牛逼,我一开始很不理解,因为它颠覆了我的观念。一般从流中读取是“单向”的,也就是说读出来的数据是不可以再回去的。但是readable.unshift居然能把“吐”出来的东西又“吃”回去(感觉有点恶心),但是有的场景还是很有用的。 156 | 157 | ### readable.wrap(stream) 158 | 此方法是为了兼容老版本而设的,老版本stream没有实现现有的API,我们就可以使用此方法来弥补,这里引用node官方一个例子: 159 | 160 | ```js 161 | var OldReader = require('./old-api-module.js').OldReader; 162 | var oreader = new OldReader; 163 | var Readable = require('stream').Readable; 164 | var myReader = new Readable().wrap(oreader); 165 | 166 | myReader.on('readable', function() { 167 | myReader.read(); // etc. 168 | }); 169 | ``` 170 | 171 | OldReader是老版本的类库,使用wrap方法包裹之后返回的myReader就可以使用当前所有的API了 172 | 173 | # stream.Writable 174 | 可写流表示数据最终目的地的抽象。 175 | 176 | ### writable.write(chunk, [encoding], [callback]) 177 | 写入数据,writable.write有三个参数,第一个参数是需要写入的数据、第二个参数表示写入的编码格式、第三个参数表示写入完成之后的回调。此方法的返回值表示数据是否已经被处理完成,这样就可以判断是否需要继续写入数据。 178 | 179 | ```js 180 | var fs = require('fs'); 181 | 182 | var writableStream = fs.createWriteStream('file.txt'); 183 | 184 | var result = writableStream.write('test write method' , 'utf-8', function() { 185 | console.log('complete'); // complete 186 | console.log(result); // true 187 | }); 188 | ``` 189 | 190 | ### 事件 'drain' 191 | writable.write返回false,那么数据再次被写入时,事件'drain'就会触发 192 | 193 | ```js 194 | var fs = require('fs'); 195 | 196 | var writableStream = fs.createWriteStream('file.txt'); 197 | 198 | writableStream.on('drain', function() { 199 | console.log('data is drain!'); 200 | }); 201 | 202 | var result = writableStream.write('big data'); 203 | 204 | console.log(result); // false 205 | ``` 206 | 207 | 以上代码中,如果我们给出的数据足够大,导致一次处理完成不了,我们得到的result将会是false。然后,在剩余数据继续被处理开始时,就会触发'drain'事件。 208 | 209 | ### writable.end([chunk], [encoding], [callback]) 210 | 写入数据并且结束流通道(此参数与writable.write方法参数一致,这里就不做重复解释)。 211 | 212 | ### 事件 'finish' 213 | 当调用writable.end方法之后,所有数据都被写入基础系统中,就会触发此事件 214 | 215 | ```js 216 | var fs = require('fs'); 217 | 218 | var writableStream = fs.createWriteStream('file.txt'); 219 | 220 | writableStream.write('test data line1'); 221 | writableStream.end('test data line2'); 222 | 223 | writableStream.on('finish', function() { 224 | console.log('line1 and line2 finished!'); 225 | }); 226 | ``` 227 | 228 | ### 事件 'pipe'、'unpipe'、'error' 229 | 230 | + pipe 调用readable.pipe到此可写流时触发 231 | + unpipe 调用readable.unpipe到此可写流时触发 232 | + error 写入或者pipe数据时出错时,触发此事件(是可读流的'error'事件类似) 233 | 234 | # 总结 235 | stream在整个node的核心模块运用都非常广泛,例如:http模块、File System模块等。这说明此模块地位非常重要,并且当我们需要处理大文件时,通常第一个想到的也是stream模块。所以,少年!好好学习,天天向上吧! 236 | -------------------------------------------------------------------------------- /node/stream-chapter2.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | [上一篇](stream-chapter1.md)博客中,我们介绍了stream的基本用法,今天我们来讲讲stream接口如何实现,之后我们就可实现自己的stream类了。 3 | 4 | # stream.Readable 5 | 简单来说,如果需要自定义stream.Readable类,那么我们需要做的就是一件事:重写_read方法。 6 | 7 | ```js 8 | var Readable = require('stream').Readable; 9 | var util = require('util'); 10 | util.inherits(Prefix, Readable); 11 | 12 | function Prefix(pre, opt){ 13 | Readable.call(this, opt); 14 | 15 | this._prefix = pre || 'node:'; 16 | this._count = 0; 17 | this._max = 9; 18 | } 19 | 20 | Prefix.prototype._read = function(){ 21 | 22 | if(this._count++ >this._max) { 23 | this.push(null); 24 | } else { 25 | this.push(new Buffer(this._prefix + this._count.toString())); 26 | } 27 | } 28 | 29 | var preObj = new Prefix('node v6:'); 30 | 31 | console.log(preObj.read().toString()); // node v6:1 32 | console.log(preObj.read().toString()); // node v6:2 33 | console.log(preObj.read().toString()); // node v6:3 34 | ``` 35 | 36 | ### new stream.Readable([options]) 37 | 当我们实现了Readable,就可以使用new来创建一个实例对象,我们可以传入一个可选的options配置参数。options有3项: 38 | 39 | + highWaterMark 表示内部buffer中最大的字节数 40 | + encoding 如果指定了此参数,buffer会被按照此格式转换成字符串 41 | + objectMode 如果此参数设置为true,当我们调用read方法时,每次只能读取一个值即使我们传入了size参数 42 | 43 | 以上面的Prefix为例: 44 | 45 | ```js 46 | var preObj1 = new Prefix('node v6:'); 47 | 48 | console.log(preObj1.read()); // 49 | console.log(preObj1.read(1).toString()); // n 50 | console.log(preObj1.read().toString()); // ode v6:2node v6:3 51 | 52 | console.log('---------------超华丽分割线----------------'); 53 | 54 | var preObj2 = new Prefix('node v6:', {highWaterMark:10, encoding:'utf8', objectMode:true}); 55 | 56 | console.log(preObj2.read()); // node v6:1 57 | console.log(preObj2.read(8)); // node v6:2 58 | console.log(preObj2.read()); // node v6:3 59 | ``` 60 | 61 | 通过对比可以看出: 62 | 63 | + 当我们没有设置encoding时,调用read方法返回的是buffer对象 64 | + 当我们设置objectMode为true时,每次只返回单个值(不可以根据size大小返回值) 65 | 66 | ### readable._read(size) 67 | 这是一个接口方法,此方法是用来实现的并且不可以直接调用(一般默认以下划线开始的方法为内部方法)。具体用法在上面第一个代码示例中已经展示,这里就不多赘述。 68 | 69 | ### readable.push(chunk, [encoding]) 70 | 此方法是向读取队列中添加数据,当我们插入null时,表示数据插入完成。此方法有2个参数: 71 | 72 | + chunk chunk参数接收Buffer或者String或者null 73 | 74 | + encoding 表示当chunk为String时的编码格式 75 | 76 | ```js 77 | var Readable = require('stream').Readable; 78 | var util = require('util'); 79 | util.inherits(PushTest, Readable); 80 | 81 | function PushTest(options) { 82 | Readable.call(this, options); 83 | 84 | // 假设souce对象一有数据就会调用data方法 85 | // 并且数据全部消费完之后调用end方法 86 | this._source = source; 87 | 88 | this._source.data = function(chunk) { 89 | if (!self.push(chunk)){ 90 | this._source.readStop(); 91 | } 92 | } 93 | 94 | this._source.end = function(){ 95 | this.push(null); 96 | } 97 | } 98 | 99 | PushTest.prototype._read = function(size) { 100 | this._source.readStart(); 101 | } 102 | ``` 103 | 104 | # stream.Writable 105 | 与stream.Readable相似,stream.Writable也是一个抽象类,其中Writable.\_write方法不可以直接被调用,Writable.\_write需要被实现。 106 | 107 | ### new stream.Writable([options]) 108 | 与stream.Readable一样,我们自定义的写入流类需要使用new来创建一个实例,当我们创建实例的时候,需要传入一个可选的options,此options同样有3个配置项: 109 | 110 | + highWaterMark 表示写入的缓冲区大小,我们调用write方法时单次写入的大小如果超过设定的阈值,就会返回false,highWaterMark就是这个阈值(默认大小16kb) 111 | + decodeStrings 在写入之前(调用内部_write之前),是否将字符串转成Buffer(默认为true) 112 | + objectMode 是否可以写入任意类型的数据,而不仅仅是Buffer或String(默认为false) 113 | 114 | ### writable._write(chunk, encoding, callback) 115 | 同样的,如果我们需要实现自定义的写入流,我们需要实现一个内部接口,此接口在node源码中是这样的: 116 | 117 | ```js 118 | Writable.prototype._write = function(chunk, encoding, cb) { 119 | cb(new Error('not implemented')); 120 | }; 121 | ``` 122 | 123 | 由此可知,如果我们需要实现自定义的写入流(可读流也是一样),就必须实现\_write方法,否则就会报错。\_write有三个参数: 124 | 125 | + chunk 需要写入的数据 126 | + encoding 如果写入数据是字符串,那么此参数表示该数据的编码格式 127 | + callback 回调函数(数据写入完成后触发) 128 | 129 | ```js 130 | var util = require('util'); 131 | var Writable = require('stream').Writable; 132 | var fs = require('fs'); 133 | 134 | util.inherits(CustomFS, Writable); 135 | 136 | function CustomFS(path, options) { 137 | Writable.call(this, options); 138 | 139 | this._path = path; 140 | } 141 | 142 | CustomFS.prototype._write = function(chunk, encoding, cb) { 143 | 144 | // 判断文件是否存在 145 | fs.exists(this._path, function(exists) { 146 | 147 | if (exists) { 148 | fs.writeFile(this._path, +new Date() + ':' + chunk + '\n', cb); 149 | } else { 150 | cb('找不到文件!'); 151 | } 152 | }.bind(this)); 153 | } 154 | 155 | // 创建实例 156 | var writeFile = new CustomFS('./file.txt', { 157 | highWaterMark: 1024 158 | }); 159 | 160 | // 写入数据 161 | writeFile.write('窗前明月光,地上鞋两双!', function(err){ 162 | if(err) { 163 | return console.log('报错啦!错误:' + err.message); 164 | } 165 | 166 | console.log('写入完成!'); 167 | }); 168 | ``` 169 | 170 | 以上代码在每次写入文本之前,都会在最前面加入时间戳。 171 | 172 | # stream.Duplex类 173 | “复杂”类型的流,在我们实现它时,既要实现可读流的\_read方法,又要实现写入流的\_write方法。也就是说,只有同时实现了可读、可写两个功能,才可以叫做“复杂”型流即'duplex'。具体可读和写入流的自定义实现代码我们在上面已经介绍过了,这里就不多讲。 174 | 175 | # stream.Transform类 176 | 'transform'类型的流属于“复杂”类型,但是我们不需要实现\_read和\_write,但是我们要实现另一个方法:_transform。'transform'类型的流的特点在于输入决定输出。就像crypto(加密模块)一样,输入决定了输出(比如哈希值)。 177 | 178 | ### new stream.Transform([options]) 179 | 当然,如果要创建一个'transform'流的实例,还是要通过new关键字(这样可以保证正确的初始化实例)。同样,新建实例时也需要传入配置参数,此配置参数就是只读流和写入流配置项的和。 180 | 181 | ### transform._transform(chunk, encoding, callback) 182 | '_transform'是我们需要实现一个自定义'transform'流的必须要实现的一个方法,其参数意义与“写入流”的_write方法类似。 183 | 184 | ### transform._flush(callback) 185 | 此方法会在调用end方法之后,但是在触发'end'或'finish'事件之前调用,便于我们做一些事情。一下是一个'transform'类型流例子: 186 | 187 | ```js 188 | var util = require('util'); 189 | var Transform = require('stream').Transform; 190 | 191 | util.inherits(Trans, Transform); 192 | 193 | function Trans(options) { 194 | Transform.call(this, options); 195 | } 196 | 197 | Trans.prototype._transform = function(chunk, encoding, callback) { 198 | 199 | this.push(new Buffer('Transform: ' + chunk.toString())); 200 | 201 | callback(); 202 | } 203 | 204 | Trans.prototype._flush = function(callback) { 205 | console.log('flushed!'); 206 | callback(); 207 | } 208 | 209 | var myTrans = new Trans(); 210 | 211 | myTrans.write('你好,小强!', function(err) { 212 | if (err) { 213 | return console.log('报错啦!错误:' + err.message); 214 | } 215 | 216 | console.log('写入完成!'); 217 | console.log(myTrans.read().toString()); 218 | myTrans.end(); 219 | }); 220 | 221 | myTrans.on('finish', function() { 222 | console.log('finished!'); 223 | }); 224 | ``` 225 | 226 | # 总结 227 | 关于stream第二部分,到这里就算是介绍完了。自定义的stream类只需要我们按照实现规则,去丰富不同的内部方法而已。这些对于混迹于JavaScript界的老司机来说,是一件非常轻松的事情,但是我们需要理解stream并融会贯通之后再来做这些事情的话,又会有另一层理解。 228 | -------------------------------------------------------------------------------- /node/stream-chapter3.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 前面2个部分,分别介绍了Stream的基础使用和自定义进阶。为了更好的帮助大家理解这个模块的,node官方还有第三部分,可以使我们进一步理解Stream模块的内部实现原理 3 | 4 | # 缓冲 5 | 可读流和写入流各自都有一个缓冲区,分别存放在内部对象\_writableState和\_readableState的'buffer'属性中。在可读流中,自定义实现时我们可以通过stream.push方法将数据推入此属性中('buffer'属性),在通过stream.read方法来消费缓冲区的内容;在写入流中,我们通过stream.write方法将我们的数据块写入到'buffer'属性中 6 | 7 | ### stream.read(0) 8 | 当我们不需要读取流中内容,但是又需要触发内部刷新机制。这时,我们可以在调用stream.read方法时传入参数0,此调用的返回值永远是null并且不会消耗任何缓冲信息。这种调用的应用场景并不常见,但是在node源码中确实有这样的调用方式。 9 | 10 | ### stream.push('') 11 | 当我们调用stream.push的时候传入空字符串或者空Buffer时,会产生一个副作用:终止读取过程。 12 | 13 | ```js 14 | var Readable = require('stream').Readable; 15 | var util = require('util'); 16 | util.inherits(Prefix, Readable); 17 | 18 | function Prefix(pre, opt) { 19 | Readable.call(this, opt); 20 | 21 | this._prefix = pre || 'node:'; 22 | this._count = 0; 23 | this._max = 9; 24 | } 25 | 26 | Prefix.prototype._read = function() { 27 | 28 | if (this._count > this._max) { 29 | this.push(null); 30 | } else if (this._count === 2) { 31 | this.push(''); 32 | } else { 33 | this.push(new Buffer(this._prefix + this._count.toString())); 34 | } 35 | 36 | this._count++; 37 | } 38 | 39 | var preObj = new Prefix('node v6:'); 40 | 41 | console.log(preObj.read().toString()); // node v6:0 42 | console.log(preObj.read().toString()); // node v6:1 43 | console.log(preObj.read().toString()); // TypeError: Cannot call method 'toString' of null 44 | ``` 45 | 46 | 可以看到当我们往缓冲区中push了空字符串之后,会导致调用read方法返回null值。其实,需要用到此场景的地方极其少,如果非要使用stream.push(''),最好还是先考虑另择他法。 47 | 48 | # 兼容老版本 49 | 在node v0.10之前,可读流(Readable)接口非常简单,功能也简单: 50 | 51 | + 老版本中不会等待read方法调用之后才会输出数据,而是会立即触发'data'事件,开始推送数据。 52 | + pause方法只是建议型的方法,不作出任何数据保证。我们还是需要使用'data'事件来接收数据,即使当前状态是'暂停状态' 53 | 54 | 为了兼容老版本,node v0.10中一旦我们添加了'data'事件的处理函数或者调用了pause或resume方法,此时stream会切换至flowing-mode,数据会尽快推送至'data'事件中。而在node v0.10我们就再也不用担心数据丢失的问题了。 55 | 56 | # Object Mode 57 | 当我们在创建一个stream实例的时候,将配置参数objectMode设置为true时,此时流就可以推送JavaScript对象(一般情况下流是专注于处理String和Buffer)。当我们处于obejct mode模式下,stream.read(size)中的size参数会被忽略,而每次读取一个单独的值、stream.write(data, encoding)则会忽略编码。 58 | 59 | # State Objects 60 | 一般的,可读流在内部会有一个内置的对象:\_readableState、而写入流也会相应的有一个内置对象:\_writableState。当我们在自定义子类的时候,应该避免修改这2个内置对象,否则会导致不可预料的后果。 61 | 62 | #总结 63 | 这一篇基本是关于流模块的一些收尾工作以及对于流的进一步认识。进一步了解流内部的机制以及实现原理,可以更好的帮助我们在以后用到此模块的时候更加得心应手。好了,关于流的介绍到这里就画上一个句号。 64 | -------------------------------------------------------------------------------- /node/token_hold.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 消失了快2个月,俺又回来了。最近换比较忙,好久没写博客,但是学习的脚步一直没停下。前段时间在[cnode](https://www.cnodejs.org)上看到一个关于微信access_token的问题:[高并发如何保证微信token的有效](https://cnodejs.org/topic/57b330b6670139eb7bf6fd4c#57b51330d6124db37b1d13cb)。其中本人也在上面回复了一下,但是觉得解决方案还是不够好,于是就有了本篇:本文主要以渐进的方式,来一步一步解决高并发情况下access_token的获取与保存。 3 | 4 | # 前提条件 5 | 由于本文讨论是基于微信公众平台开发展开的,所以如果对微信公众平台开发不熟悉的同学可以先去看下微信公众平台的[开发文档](https://mp.weixin.qq.com/wiki/home/) 6 | 7 | # 需要解决的问题 8 | 本文讨论的其实是access_token获取与保存在高并发情况下的边界问题: 9 | + 1.在node单进程情况下 第一个请求(请求A)过来,程序发现access_token过期,这时就会去向微信服务器获取新的access_token,然后更新到全局变量中。这个过程本身没有问题,但是如果请求A在向微信服务器请求新的access_token期间,来了第二个请求(请求B),因为这时新的access_token还未更新到全局变量中,请求B就会认为access_token已过期,也会同请求A一样,去微信服务器请求新的access_token,这样就会导致请求A得到的access_token失效。想象一下,如果在请求A后面又来了N个请求,并且此时请求A还未成功更新access_token,那么后面的所有请求都会去微信服务器获取新的access_token。以上的情况,不仅会导致浪费带宽,而且会导致最后一个请求之前获取的access_token都失效。那我们应该如何做控制,在高并发情况下仅请求一次微信服务器呢? 10 | 11 | + 2.在node多进程模式下 如果我们的项目开启了node多进程,情况将更加复杂:我们不仅会遇到在单进程情况下的问题,而且由于node进程之间是不共享内存的,也就是说上面说到的全局变量是无法使用的,如果第一个请求由进程1处理,而此时access_token已过期,然后第二个请求由进程2处理。即使在进程1成功更新了access_token,但是由于进程1与进程2内存不共享,所以如果不借助外部存储的话,后面一个请求也无法得知access_token已更新。那么,这种情况有该如何解决呢? 12 | 13 | # (单进程模式下)思路 14 | 首先我们来讨论,如何解决单进程模式下高并发遇到的问题。 15 | + 第一步我们需要引入一个新的全局变量,用来标记已经有请求在获取新的access_token 16 | + 第二步就是将后续的请求缓存到node的事件队列中去 17 | + 第三步统一触发事件 18 | 19 | 具体实现代码: 20 | 21 | ```js 22 | var Emitter = require('events').Emitter; 23 | var util = require('util'); 24 | 25 | var access_token, flag; 26 | 27 | function TokenEmitter() { 28 | Emitter.call(this); 29 | } 30 | util.inherits(TokenEmitter, Emitter); 31 | 32 | myEmitter = new TokenEmitter(); 33 | // 消除警告 34 | myEmitter.setMaxListeners(0); 35 | 36 | function getAccessToken(appID, appSecret, callback) { 37 | // 将callback缓存到事件队列中,等待触发 38 | myEmitter.once('token', callback); 39 | 40 | // 判断access_token是否过期 41 | if (!isValid(access_token) && !flag) { 42 | // 标记已经向微信服务器发送获取access_token的请求 43 | flag = true; 44 | 45 | // 向微信服务器请求新的access_token 46 | requestForAccessToken(appID, appSecret, function(err, newToken) { 47 | if (err) { 48 | // 通知出错 49 | return myEmitter.emit('token', err); 50 | } 51 | 52 | // 更新access_token 53 | access_token = newToken; 54 | // 触发所有监听回调函数 55 | myEmitter.emit('token', null, newToken.access_token); 56 | // 还原标记 57 | flag = false; 58 | }); 59 | } else { 60 | process.nextTick(function(){ 61 | callback(null, access_token.access_token); 62 | }); 63 | } 64 | } 65 | ``` 66 | 67 | 以上代码主要的思路就是利用,node自带的事件监听器,也就是代码中的'myEmitter.once()'方法,在access_token失效的情况下把所有调用的回调方法添加为'token'事件监听函数。并且只有第一个调用者可以去更新access_token的值(主要用flag来控制)。当获得新的access_token后,以新access_token为参数,去触发'token'事件。此时,所有监听了'token'事件的函数都会被调用,也就是说,所有调用者的回调函数都会被调用。这样,我们就实现了高并发情况下,防止access_token被多次更新的问题,也就是解决了问题1。 68 | 69 | # (多进程模式下)思路 70 | 解决了单进程模式下的问题,可以说我们多进程问题也解决了一部分。在多进程模式下,我们的主题思路还是与单进程一直,将调用缓存到事件队列中。但是,多进程的各个进程是不共享内存的,所以我们的access_token和flag标记不可以存储在变量中,因此需要引入外部存储:redis。使用redis作为外部存储有以下几个原因: 71 | + 1.在redis中统一存储access_token,各个进程都可以自由访问 72 | + 2.利用redis作为锁媒介(相当于单进程中的flag) 73 | + 3.利用redis发布订阅功能来触发事件(相当于单进程中的emit) 74 | 75 | ## 统一存储access_token 76 | 这一点大家都应该没什么疑问,access_token统一存储的好处就是不需要面对复杂的进程见通信。 77 | 78 | ## 锁媒介 79 | 当我们标记“正在请求微信服务器”的flag标志不可以放在代码的变量中时,那就要寻求代码之外的解决方法,其实我们可以存在mongodb、mysql等等可以存储的媒介中,甚至可以存放在文本文件中。但是为了保证速度,我还是考虑将其存放在速度更快的redis中。 80 | 81 | ## redis发布订阅功能 82 | 当然,如果我们的程序使用的是node的cluster模块开启的多进程模式,进程间通信还是相对容易一些:每个worker都可以向master发送message,利用这一点把master当做中心,来交换数据。但是如果我们是使用pm2开启了多实例,pm2虽然提供了实例间通信的API,但是使用起来各种不顺畅,最终选择redis来作为各个实例接受通知的发起方。 83 | 84 | 以上思路的实现代码大致如下: 85 | 86 | 1.第一步需要做的就是判断access_token是否过期(为了方便起见,直接用appID + appSecret作为存储access_token的键):从redis获取键为appID + appSecret的内容,因为我们在设置access_token时,是将其设为了过期键(设置过程涉及到锁,将在之后给出),所以只要能取到值,就说明access_token没有过期。代码如下: 87 | 88 | ```js 89 | function isValid(appID, appSecret, callback) { 90 | redis.get(appID + appSecret, function(err, token) { 91 | if (err) { 92 | return callback(err); 93 | } 94 | 95 | // 可以取到值 96 | if (tokenInfo) { 97 | return callback(null, token); 98 | } 99 | 100 | // 未取到值 101 | callback(null); 102 | }); 103 | } 104 | ``` 105 | 106 | 2.如果在第一步的判断中,我们得出结论:access_token已经过期,那么我们需要做的下一步就是设置一个代码级别的锁,防止之后的程序访问之后的代码: 107 | ```js 108 | function aquireLock(callback) { 109 | redis.setnx('lock', callback); 110 | } 111 | 112 | function releaseLock(callback) { 113 | redis.del('lock', callback); 114 | } 115 | ``` 116 | 这2个函数,一个用于设置锁,一个用于释放锁。我们设置锁是利用了redis的setnx命令原理:setnx只可以设置不存在的key,即使同一时间有多个setnx命令来设置同一个key,最终只有一个客户端可以成功设置'lock'键,也就是说只有一个请求获得了锁的权限。这样就控制了并发产生的问题。 117 | 118 | 3.最后我们将所有程序写入主函数中: 119 | * 1).主函数中一进来,首先添加监听器 120 | * 2).第二步我们需要订阅redis的new_token和new_token_err频道 121 | * 3).第三步判断access_token是否过期 122 | * 4).如果过期,就尝试去获取锁权限 123 | * 5).如果获取锁失败,就什么都不做,等待事件触发;如果获取锁权限成功后,就可以请求微信服务器来获取新的access_token,当获得新的access_token之后将其更新到redis中,并且设置合理的过期时间 124 | * 6).释放锁并且发出通知 125 | ```js 126 | function getAccessToken(appID, appSecret, callback) { 127 | // 将callback缓存到事件队列中,等待触发 128 | myEmitter.once('token', callback); 129 | 130 | // 处理订阅消息 131 | subscribe.on('message', (channel, message) => { 132 | switch (channel) { 133 | case 'new_token': 134 | myEmitter.emit('token', null, message); 135 | break; 136 | case 'new_token_err': 137 | myEmitter.emit('token', new Error(message)); 138 | break; 139 | default: 140 | break; 141 | } 142 | }); 143 | 144 | // 判断access_token是否过期 145 | isValid(appID, appSecret, function(err, token) { 146 | // 出错 147 | if (err) { 148 | return myEmitter.emit('token', err); 149 | } 150 | 151 | // token正常 152 | if (token) { 153 | return myEmitter.emit('token', null, token.access_token); 154 | } 155 | 156 | // token已过期,获取锁 157 | aquireLock(function(err, result) { 158 | // 如果获取锁成功,则开始更新access_token,如果未得到锁,等待'token'触发 159 | if (result) { 160 | // 向微信服务器请求新的access_token 161 | requestForAccessToken(appID, appSecret, function(err, newToken) { 162 | if (err) { 163 | // 释放锁标记 164 | releaseLock(); 165 | // 通知出错 166 | return myEmitter.emit('token', err); 167 | } 168 | 169 | // 更新access_token,将新的access_token保存到redis,并且提前5分钟过期 170 | redis.setex(appID + appSecret, (newToken.expires_in - 300), newToken.access_token); 171 | // 发布更新 172 | publish.publish('new_token', newToken.access_token); 173 | // 释放锁标记 174 | releaseLock(); 175 | }); 176 | } 177 | }); 178 | 179 | // 订阅 180 | subscribe.subscribe('new_token'); 181 | }); 182 | } 183 | ``` 184 | 185 | # 进一步思考 186 | 到此,一个简单多进程控制access_token并发的解决方法已经呈现在眼前,但是我们还需要考虑一下边界情况: 187 | + 1.当我们获得到锁权限的进程在未知因素下崩溃了,并且此时用于标记锁状态的'lock'已被成功设置 188 | + 2.微信服务器崩溃了(无法正常提供服务) 189 | 以上2个情况,都会导致锁状态永远无法释放,针对这一类问题,我们可以引入超时的概念,在设置'lock'的时候给'lock'键设置一个过期时间,如果过了指定的时间还没有释放锁,那么锁权限就会被收回。代码如下: 190 | 191 | ```js 192 | function aquireLock(callback) { 193 | redis.setnx('lock', function(err, result){ 194 | if(err){ 195 | return callback(err); 196 | } 197 | 198 | redis.expire('lock', 2, function(err, success){ 199 | if(err){ 200 | return callback(err); 201 | } 202 | 203 | callback(null, true); 204 | }); 205 | }); 206 | } 207 | ``` 208 | 209 | # 更进一步的思考 210 | 虽然我们解决了锁问题,但是此时所有未获得锁的请求还处于pending状态,等待着access_token的到来,但是由于获得锁的请求已经走在天堂的路上,已经无法再来给其他这些个请求触发事件了。所以为了解决此类问题,我们需要引入另一个超时,那就是函数调用超时,在一定时间内未完成的话,我们就回调超时错误给调用者: 211 | ```js 212 | function getAccessToken(appID, appSecret, callback) { 213 | // 将callback缓存到事件队列中,等待触发 214 | myEmitter.once('token', callback); 215 | 216 | // 设置函数调用超时 217 | setTimeout(function () { 218 | callback(null, new Error('time out')); 219 | }, 2000); 220 | 221 | // ... 222 | } 223 | ``` 224 | 225 | # 总结 226 | 其实在使用redis的订阅功能之前,我还考虑过[tj](https://github.com/tj)的[axon](https://github.com/tj/axon)作为进程通信的手段,但是由于axon初始化过程有一定的延迟,不符合我的预期,所以放弃了。但是不得不说axon是一个非常好的项目,有条件的话可以用在项目当中。好了,以上就是我对高并发下处理access_token的一些自己的看法。 227 | -------------------------------------------------------------------------------- /node/util.md: -------------------------------------------------------------------------------- 1 | ##### 引子 2 | 话说大家在做框架的时候,除了引用大量的引入第三方类库之外,不免自己要实现一些。例如:判断对象是否是某个类型等。其实node也提供了一部分实用的函数(需要引入“util”模块),下面我们就来一一介绍。 3 | 4 | ##### util.format(format, [...]) 5 | 此方法与C语言中的prinf类似,第一个参数是一个字符串,后面是可变参数(可以有任意多个参数) 6 | 7 | + %s String 可变参数对应位置是字符串。 8 | 9 | ```js 10 | var util = require('util'); 11 | 12 | console.log(util.format('%s%s', '你好!')); // 你好!%s 13 | console.log(util.format('Hello %s', 'World!')); // Hello World! 14 | ``` 15 | 16 | + %d Number 可变参数对应位置是数字(整型或浮点型) 17 | 18 | ```js 19 | var util = require('util'); 20 | 21 | console.log(util.format('你的幸运数字是%d', 6)); // 你的幸运数字是6 22 | ``` 23 | 24 | + %j JSON 可变参数为JSON对象 25 | 26 | ```js 27 | var util = require('util'); 28 | 29 | console.log(util.format('这个接口需要的参数为:%j,正确的返回结果为:%j', {type: 'pet'}, {status: 200})); // 这个接口需要的参数为:{"type":"pet"},正确的返回结果为:{"status":200} 30 | ``` 31 | 32 | 需要注意的是,如果第一个参数不是拥有占位符的字符串,那么此方法会把所有参数拼接成一个字符串,并且每个参数之间有空格分开 33 | 34 | ```js 35 | var util = require('util'); 36 | 37 | console.log(util.format(1,2,3); // 1 2 3 38 | ``` 39 | 40 | ##### util.debug(string) 41 | 在打印的字符串之前添加“DEBUG:”字样,就如我在[Node.js之console](console.md)一文中提到的一样,util.debug会将输出的信息记录到错误日志文件中去,如果没有指定错误文件,那么就会直接输出到控制台 42 | 43 | ```js 44 | var util = require('util'); 45 | 46 | util.debug('这是一条调试信息'); // DEBUG: 这是一条调试信息 47 | ``` 48 | 49 | ##### util.error([...]) 50 | 与util.debug一样,日志会记录到错误日志文件中去,但是不带“DEBUG:”,并且可以记录多个参数 51 | 52 | ```js 53 | var util = require('util'); 54 | 55 | util.error('error1', 'error2', 'error2'); // error1 56 | // error2 57 | // error2 58 | ``` 59 | 60 | ##### util.puts([...])、util.print([...]) 61 | util.puts与util.print功能一致,会将日志信息记录到正常的日志文件中,两者唯一的区别在于:util.puts会在新行输出每一个参数,而util.print将所有参数输出到一行。 62 | 63 | ```js 64 | var util = require('util'); 65 | 66 | uti.puts('arg1', 'arg2', 'arg3'); // error1 67 | // error2 68 | // error2 69 | console.log('======优雅的分割线========='); // ======优雅的分割线========= 70 | uti.print('arg1', 'arg2', 'arg3'); // arg1arg2arg3 71 | ``` 72 | 73 | ##### util.log(string) 74 | 从node源码可以看出util.log在内部内部其实就是调用了console.log方法,只不过在日志前加入了时间点。 75 | 76 | ```js 77 | function timestamp() { 78 | var d = new Date(); 79 | var time = [pad(d.getHours()), 80 | pad(d.getMinutes()), 81 | pad(d.getSeconds())].join(':'); 82 | return [d.getDate(), months[d.getMonth()], time].join(' '); 83 | } 84 | 85 | // log is just a thin wrapper to console.log that prepends a timestamp 86 | exports.log = function() { 87 | console.log('%s - %s', timestamp(), exports.format.apply(exports, arguments)); 88 | }; 89 | ``` 90 | 91 | 以上是util模块的node源码:timestamp是产生时间字符串的函数,当我们调用util.log时,timestamp()产生的字符串会被填充到原日志的前面(更多的模块源码,我以后会有专门的博客介绍)。下面我们来看下util.log(string)用法与效果: 92 | 93 | ```js 94 | var util = require('util'); 95 | 96 | util.log('我是一条日志!'); // 29 Feb 15:50:34 - 我是一条日志! 97 | ``` 98 | 99 | ##### util.inspect(object, [options]) 100 | 这个函数我们在[Node.js之console](console.md)一文中也提到过,功能我就不多做介绍。第一个参数应该没什么异议,第二个参数是可选配置项,通过传入不同的option值,会有不同的效果,下面就来介绍下这个option: 101 | 102 | + showHidden表示是否要返回不可枚举选项,我们在[JavaScript之object](../object.md)提到过,object对象的属性可以设置为“不可枚举”,这样我们的for...in结构就不能访问了。这里也是一样,showHidden默认是false,也就是说使用util.inspect时,默认不返回不可枚举属性的: 103 | 104 | ```js 105 | var util = require('util'); 106 | 107 | var obj = Object.create({}, { 108 | 'enumerable_prop':{ 109 | value: 'a', 110 | enumerable: true 111 | }, 112 | 'non_enumerable_prop':{ 113 | value: 'b', 114 | enumerable: false 115 | } 116 | }); 117 | 118 | var strObjShow = util.inspect(obj, {showHidden: true}); 119 | var strObjHide = util.inspect(obj, {showHidden: false}); 120 | 121 | console.log('strObjShow is %s', strObjShow); // strObjShow is { enumerable_prop: 'a', [non_enumerable_prop]: 'b' } 122 | console.log('strObjHide is %s', strObjHide); // strObjHide is { enumerable_prop: 'a' } 123 | ``` 124 | 125 | + depth表示深度,我们在[Node.js之console](console.md)介绍过,这里就不再赘述。 126 | 127 | + colors 可以在控制台输出有颜色的对象(并且颜色是可以自定义的) 128 | 129 | ```js 130 | var util = require('util'); 131 | 132 | console.log(util.inspect({a:1, b:'123'}, {colors: true})); // { a: 1, b: '123' } 133 | ``` 134 | 135 | 以上代码的实际效果如下: 136 | 137 | ![inspect-colors](../file/inspect_colors.PNG) 138 | 139 | + customInspect如果我们在对象像定义inspect方法,那么util.inspect就会直接调用该对象的inspect方法,如果将customInspect设置为false,则将依旧调用node定义的方法: 140 | 141 | ```js 142 | var util = require('util'); 143 | 144 | var obj = { 145 | name: '张三', 146 | inspect: function(){ 147 | return 'inspect is called!'; 148 | } 149 | } 150 | 151 | console.log(util.inspect(obj)); // inspect is called! 152 | console.log(util.inspect(obj, {customInspect: false})); // { name: '张三', inspect: [Function] } 153 | ``` 154 | 155 | ##### util.isArray(object)、util.isRegExp(object)、util.isDate(object)、util.isError(object) 156 | 这几个方法从英文语义上就可以知道功能了,我就不多述了。(需要提到的是在以前,判断一个对象是否是数组,很长时间没有人给出优雅的解决方法。直到有人利用调用原型上的方法才给出了优雅的解决方案,这就告诉我们,群众的力量是无穷的)。 157 | 158 | ##### util.pump(readableStream, writableStream, [callback]) 159 | 这个方法在v0.10.42时代就被废弃了,就不介绍了。 160 | 161 | ##### util.inherits(constructor, superConstructor) 162 | 虽然我们可以手动做JavaScript的“继承”,但是node还是给出了实用的工具函数。但是我在这里不做介绍,我会把此方法放到events模块一起介绍。 163 | 164 | ##### 总结 165 | 总的来说,介绍API是比较枯燥的(我也觉得)。大家可以作为知识点学习学习,熟悉熟悉,为以后工作中遇到的问题打打基础。另外之前我在[v站](http://v2ex.com)上发的帖子很多人回复,非常感谢大家给我的建议,我会坚持写技术博客的。 166 | -------------------------------------------------------------------------------- /object.md: -------------------------------------------------------------------------------- 1 | ##### 便捷的object对象 2 | 在通常的开发过程中,我们很多时候都会用到object对象。例如:传递object参数、可选择的配置项、数据库输出对象,都可以使用object。这样做使得我们的程序简洁、易用、可扩展。需要使用时,就像如下代码: 3 | 4 | ```js 5 | function getUer(name, age, country){ 6 | var user; 7 | 8 | user = { 9 | name: name, 10 | age: age, 11 | country: country 12 | } 13 | 14 | return user; 15 | } 16 | 17 | console.log(getUer('peter', 28, 'America')); // { 18 | // name: 'peter', 19 | // age: 28, 20 | // country: 'America' 21 | // } 22 | ``` 23 | user = {...}是大家常用的一种定义对象的方式,这样非常的方便、直接。这样定义可以满足99%的业务需求,但是有的时候我们需要满足一些特殊的需求。 24 | 25 | ##### Object.create() 26 | 除了直接创建object对象外,我们还可以使用Object.create方法来创建对象,这个方法给我提供了更多的不同情况下的API。Object.create有1个必传参数和一个可选参数。 27 | 28 | + 第一个参数是必传参数,指定即将创建的对象的prototype(原型,这个我下一篇博客会详细介绍,这里就不赘述)。 29 | + 第二个参数一个可选参数,可以设置我们需要初始化给即将创建的对象的属性及对属性的控制。 30 | 31 | ```js 32 | var newObj = Object.create({}, { 33 | 'a': { 34 | value: 'this is a', 35 | configurable: false, 36 | enumerable: false, 37 | writable: false 38 | } 39 | }); 40 | 41 | console.log(newObj); // {a: 'this is a'} 42 | ``` 43 | 44 | 上面的代码中我们把空对象“{}”作为了newObj的原型,并给newObj设置了一个值为“this is a”的属性“a”。我们看到,除了value属性,还有configurable、enumerable、writable三个配置属性。 45 | 46 | + configurable表示可否对当前对象进行配置。默认情况下使用Object.create创建的对象是不可配置的,也就是说configurable默认是false。 47 | 48 | ```js 49 | var newObj = Object.create({}, { 50 | 'a': { 51 | value: 'this is a', 52 | configurable: false, 53 | enumerable: false, 54 | writable: false 55 | } 56 | }); 57 | 58 | Object.defineProperty(newObj, 'a', { 59 | value: 'this is not a', 60 | enumerable: true 61 | }); // Uncaught TypeError: Cannot redefine property 62 | ``` 63 | 当我们试图对newObj的属性进行修改时,JavaScript引擎就会抛出错误“Cannot redefine property”,因为默认属性“a”是不可配置的。如果把configurable设置成true的话,错误就不会出现。 64 | 65 | + enumerable表示该属性不可枚举。如果需要枚举一个object对象,可以使用for...in方法,一旦我们把当前属性设置成false,for...in将访问不到此属性。 66 | 67 | ```js 68 | var user = Object.create({}, { 69 | 'name': { 70 | value: 'Lucy', 71 | enumerable: true 72 | }, 73 | 'age': { 74 | value: 24, 75 | enumerable: false 76 | }, 77 | 'country': { 78 | value: 'Australia', 79 | enumerable: true 80 | } 81 | }); 82 | 83 | for(p in user){ 84 | console.log(user[p]); // Lucy 85 | // Australia 86 | } 87 | ``` 88 | 89 | 这里age属性的enumerable设置成了false,然后我们对user进行属性枚举的时候,发现age并没有被输出。 90 | 91 | + writable表示是否可以被赋值运算符改变。 92 | 93 | ```js 94 | (function(){ 95 | "use strict"; 96 | 97 | var song = Object.create({}, { 98 | 'name': { 99 | value: 'Welcome to Beijing', 100 | configurable: true, 101 | writable: false 102 | } 103 | }); 104 | 105 | Object.defineProperty(song, 'name', { 106 | value: 'WELCOME TO BEIJING' 107 | }); 108 | 109 | console.log(song); 110 | 111 | song.name = '北京欢迎您'; // Uncaught TypeError: Cannot assign to read only property 'name' of object '#' 112 | 113 | console.log(song); 114 | })(); 115 | ``` 116 | 为了更好的展示效果,这里我们使用了严格模式,在严格模式下,当writable设置成false,利用Object.defineProperty方法可以对“name”属性进行正常修改,但是使用赋值修改时就会抛出系统错误。说明在writable为false的情况下,赋值符号是不可以修改对应属性的。(在非严格模式下赋值会静默失败,不会抛出错误) 117 | 118 | ##### 引用传递 119 | 需要提到的是,如果需要在函数中传递object,请记住我们传递的永远是object的引用地址,如果在函数内部对该参数进行了修改,所有引用该参数的代码都会受到影响。 120 | 121 | ```js 122 | A.js: 123 | var setting = { 124 | user: 'admin', 125 | group: 'vip' 126 | } 127 | 128 | function getSettings() { 129 | return setting; 130 | } 131 | 132 | function setSettings(k, v) { 133 | setting[k] = v; 134 | 135 | return setting; 136 | } 137 | 138 | B.js(B.js引用了A.js) 139 | console.log(getSettings()); // {user: "admin", group: "vip"} 140 | 141 | console.log(setSettings('user', 'test')); // {user: "test", group: "vip"} 142 | 143 | console.log(getSettings()); // {user: "test", group: "vip"} 144 | ``` 145 | 146 | 我们看到,当我们调用setSettings函数时,A文件的setting对象被修改了,然后后面一个getSettings就被影响了。这个习惯性的错误在我们编写JavaScript时很容易犯,并且有的时候这个错误是致命的。所以如果需要拷贝对象,请Object.assign或第三方库(lodash、underscore等)提供的拷贝函数。这里我们来介绍下ES2015中的新方法:Object.assign,将刚刚的代码小小修改下 147 | 148 | ```js 149 | A.js: 150 | var setting = { 151 | user: 'admin', 152 | group: 'vip' 153 | } 154 | 155 | function getSettings() { 156 | return setting; 157 | } 158 | 159 | function setSettings(k, v) { 160 | var _setting = Object.assign({}, setting); 161 | 162 | _setting[k] = v; 163 | 164 | return _setting; 165 | } 166 | 167 | B.js(B.js引用了A.js) 168 | console.log(getSettings()); // {user: "admin", group: "vip"} 169 | 170 | console.log(setSettings('user', 'test')); // {user: "test", group: "vip"} 171 | 172 | console.log(getSettings()); // {user: "admin", group: "vip"} 173 | ``` 174 | 这样我们原来的setting对象就不会被改变了,需要注意的是Object.assign只对对象自身可枚举的属性进行拷贝,其他属性是不会拷贝的。 175 | 176 | ##### 很多其他实用方法 177 | 以上内容只介绍了Object.create和Object.defineProperty、Object.assign三个方法,object对象还有很多其他实用的方法。例如:Object.freeze、Object.seal、Object.prototype.hasOwnProperty等等,都是会在日常开发过程中用到的。这里就不一一介绍,有兴趣的话可以参考JavaScript规范。 178 | 179 | ##### 总结 180 | 可能我们在日常的coding业务时,不需要使用特别复杂的Object方法,但是对于一个框架与底层的开发人员来说,这些都是需要掌握的。 181 | -------------------------------------------------------------------------------- /prototype.md: -------------------------------------------------------------------------------- 1 | ##### Object续 2 | 在[JavaScript之object](object.md)一文中,我简单介绍了object几个常用的方法。之所有此篇博客第一个小标题叫做"Object续",其实是因为我马上要讲的prototype是object的一个很重要的属性或者说是概念。可以说,如果JavaScript没有prototype这个思想的话,早就被世上的程序猿们所抛弃,还不知道在哪个犄角旮旯中哭泣呢。这就说明了prototype的对于JavaScript语言本身的重要性,很多东西需要靠prototype实现。 3 | 4 | ##### 什么是prototype 5 | 很多程序猿都知道prototype是什么概念,可能有很大部分人也是似懂非懂(包括几个月前的俺)。大家都会说,prototype不就是原型嘛,有什么难的。我之前也是这么认为的,但是如果真的要说出个所以然来的时候,却不知道从何说起。prototype其实就是一个对象对另一个对象的引用。 6 | 7 | ##### 原型链 8 | 原型链跟之前我在[JavaScript之作用域](scope.md)一文中提到的作用域链一样,都是逐层查找属性的。 9 | 10 | ```js 11 | var a = { 12 | group: 'administrator' 13 | } 14 | 15 | var b = Object.create(a, { 16 | 'name': { 17 | value: 'Nicolas', 18 | writable: true, 19 | enumerable: true 20 | } 21 | }); 22 | 23 | console.log(b.hasOwnProperty('group')); // false 24 | console.log(Object.getPrototypeOf(b).hasOwnProperty('group')); // true 25 | console.log(b.group); // administrator 26 | ``` 27 | 28 | 上面的代码中,对象b是没有group属性的。但是我们在最后一句打印的时候却拿到了结果,group其实存在b的原型上。这里就有一条简单的原型链,当我们访问b的group属性时,在b上没有查找到group,然后会向b的原型(也就是a上)查找,发现a上有一个叫做group的属性,于是直接读取。(万一还是没有查找到,会向a的原型,也就是Object查找,如果最终还是没有找到,会返回一个undefined)。如果我们调用b.toString方法,最终会查找到Object.toString方法,然后调用,查找规则与我们查找group属性的情况一致。 29 | 30 | ##### 引用另一个对象的好处 31 | 很多人会问,好好的对象,为什么无缘无故地要引用其他对象呢。一个对象引用另一个对象后,可以使用目标对象上的属性和方法。 32 | 33 | ```js 34 | function Vehicle(wheel){ 35 | this.wheel = wheel; 36 | } 37 | 38 | Vehicle.prototype.getWheel = function(){ 39 | return this.wheel; 40 | } 41 | 42 | function Car(wheel, fuel){ 43 | Vehicle.call(this, wheel); 44 | 45 | this.fuel = fuel; 46 | } 47 | 48 | Car.prototype.getWheel = function(){ 49 | return 'wheel is ok'; 50 | } 51 | 52 | Car.prototype = Object.create(Vehicle.prototype); 53 | 54 | Car.prototype.getWheel = function(){ 55 | return this.wheel + 10; 56 | } 57 | 58 | var buickCar = new Car(4, 'gasoline'); 59 | 60 | console.log(buickCar); // {wheel: 4, fuel: "gasoline"} 61 | 62 | console.log(buickCar.hasOwnProperty('getWheel')); // false 63 | 64 | console.log(buickCar.getWheel()); // 14 65 | ``` 66 | 67 | 我们可以看到,当我们的Car引用了Vehicle的对象后,虽然在Car中没有定义getWheel方法,但是我们仍然可以在Car的实例对象(buickCar)中访问到getWheel方法。这也是JavaScript中原型继承的基本思想。这种直接给prototype赋值的方法有点野蛮,这会把原来的prototype废弃掉,我们可以看到代码中原来就有一个getWheel方法,但是我们后来覆盖了prototype,所以之前的一切属性和方法都废弃了。在ES2015中提供了一种更温柔的方式Object.setPrototypeOf,大家可以试试。 68 | 69 | ##### 属性屏蔽 70 | 属性屏蔽也可是叫做属性遮蔽,如果A是B的原型,那么如果给B的属性赋值时,就有可能产生属性遮蔽。为什么是有可能呢,因为属性遮蔽只有在满足特定条件的情况下才会发生。在执行B.a = '0'时: 71 | 72 | + 如果A上有属性a,并且属性a的writable不为false时,这时会在B上新增一个属性a,并且B.a会屏蔽A.a 73 | 74 | ```js 75 | var A = Object.create({}, { 76 | 'a': { 77 | value: 'This is A', 78 | writable: true 79 | } 80 | }); 81 | 82 | var B = Object.create(A); 83 | 84 | console.log(B.a); // This is A 85 | console.log(B.hasOwnProperty('a')); // false 86 | 87 | B.a = 'this is B'; 88 | 89 | console.log(B.a); // This is B 90 | console.log(B.hasOwnProperty('a')); // true 91 | console.log(Object.getPrototypeOf(B)); // {a: "This is A"} 92 | ``` 93 | 94 | 我们可以看到,当B上没有属性a的时候,B.a其实是B的原型上的属性即“this is A”。当我们给B.a赋值后,B.a就是我们所期望的“This is B”,也就是说此时出现了屏蔽现象。原型上的a属性被对象B的a属性屏蔽了。但是如果原型上的a属性的writable是false时,又会发生什么事情呢。 95 | 96 | ```js 97 | (function(){ 98 | "use strict"; 99 | 100 | var A = Object.create({}, { 101 | 'a': { 102 | value: 'This is A', 103 | writable: false 104 | } 105 | }); 106 | 107 | var B = Object.create(A); 108 | 109 | console.log(B.a); // This is A 110 | console.log(B.hasOwnProperty('a')); // false 111 | 112 | B.a = 'this is B'; // Uncaught TypeError: Cannot assign to read only property 'a' of #(…) 113 | console.log(B.a); 114 | })() 115 | ``` 116 | 117 | 为了更好的看出效果,我使用了严格模式。这时JavaScript引擎抛出了一个错误,意思是属性a是只读的。这样就不会出现屏蔽的现象。 118 | 119 | + 如果原型上正好有一个setter,那么会直接设置这个setter。 120 | 121 | ```js 122 | var val = 'This is A'; 123 | 124 | var A = Object.create({}, { 125 | 'a': { 126 | get: function(){ 127 | return val; 128 | }, 129 | set: function(v) { 130 | val = v; 131 | } 132 | } 133 | }); 134 | 135 | var B = Object.create(A); 136 | 137 | console.log(B.a); // This is A 138 | console.log(B.hasOwnProperty('a')); // false 139 | 140 | B.a = 'this is B'; 141 | 142 | console.log(B.a); // This is B 143 | console.log(B.hasOwnProperty('a')); // false 144 | console.log(Object.getPrototypeOf(B)); // {} 145 | ``` 146 | 147 | A上存在一个setter,我们在执行“B.a = 'this is B';”语句的时候,没有出现屏蔽的情况,此行代码的结果是直接执行了A上的setter,根本没有在B上添加新的属性,所以没有出现屏蔽现象。以上两种情况说明,屏蔽不是很多人想象的那么容易就会发生的,一定要满足一定的条件。 148 | 149 | ##### 委托 150 | 我要说的委托,不是.NET中的事件委托。这里的委托是与JavaScript的“类”(其实JavaScript根本没有真正的类,一切都是障眼法,都是模仿的)有一致的功能,但是又号称更加的优雅。 151 | 152 | ```js 153 | var computer = { 154 | screen: '26 inch', 155 | cpu: 'i7', 156 | setScreen: function(_s){ 157 | this.screen = _s; 158 | }, 159 | setCPU: function(_c){ 160 | this.cpu = _c; 161 | } 162 | } 163 | 164 | var dell = Object.create(computer); 165 | 166 | dell = Object.assign(dell, { 167 | brand: 'Dell', 168 | setDellScreen: function(_s){ 169 | this.setScreen(_s); 170 | 171 | // 其他业务逻辑... 172 | console.log('Dell\'s screen is ' + this.screen); 173 | }, 174 | setDellCPU: function(_c){ 175 | this.setCPU(_c); 176 | 177 | // 其他业务逻辑... 178 | console.log('Dell\'s CPU is ' + this.cpu); 179 | } 180 | }); 181 | 182 | dell.setDellScreen('21 inch'); // Dell's screen is 21 inch 183 | ``` 184 | 185 | 我们看到,dell对象没有setScreen方法,运用原型链查找规则,dell实现了setScreen的效果。换句话说,dell对象把setScreen方法委托给了原型computer来做,这样做就是所谓的委托。这么做我们既保留了原来类的“继承”效果,又不会有原型类中复杂的关系(由于这个关系过于复杂,以后有时间会更新一篇相关主题的博客)。 186 | 187 | ##### 总结 188 | 到这里,prototype的简单介绍就完了。如果需要真正的深入了解,还需要我们在日常开发过程中积累,罗马也不是一天建成的,希望我的总结对大家学习JavaScript有帮助。JavaScript系列的博客到这就结束了,但是不代表完结,如果以后我对JavaScript有了新的理解,也会继续更新。预告下,下个系列是NODE.JS相关的博客,谈谈我个人对NODE.JS的理解。 189 | -------------------------------------------------------------------------------- /scope.md: -------------------------------------------------------------------------------- 1 | # 琢磨不透的变量 2 | 我们在使用JavaScript写业务代码时,经常会得到意料之外的变量值。我们来看下以下代码: 3 | 4 | ```js 5 | for(var i = 0; i < 3; i++){ 6 | // 业务... 7 | } 8 | 9 | console.log(i); // 3(纳尼!怎么会是3!不是undefined!) 10 | ``` 11 | 12 | 80%的.NET程序猿都会惊讶这个打印结果(包括我一开始也是),我们都把.NET的作用域思想强加到了JavaScript上了。在.NET中是有局部变量的概念,但是在JavaScript中(ES2015)之前是没有块作用域的概念的。 13 | 14 | # JavaScript中的作用域 15 | 那么在JavaScript中的作用域到底是怎么运作的?其实JavaScript的作用域一点都不难。只要掌握了规律,在写代码的时候就可以避免不必要的错误。 16 | 17 | 我们知道,JavaScript中函数是一等公民。JavaScript中的作用域也是围绕函数来的,每个函数(同一层次)内部的作用域是独立的、互不影响的。 18 | 19 | ```js 20 | function foo(){ 21 | var i = 0; 22 | i++; 23 | 24 | console.log(i) // 1 25 | } 26 | 27 | function baz(){ 28 | var i = 1; 29 | i--; 30 | 31 | console.log(i) // 0 32 | } 33 | ``` 34 | 35 | 函数foo中的变量i和函数baz中的变量i存在于2个完全不同的作用域中,是2个完全不同的变量,foo的调用并不会影响baz中i的值。 36 | 37 | 我们在回过头来看我们一开始提出的问题:为什么在for循环结束之后,我们仍然可以访问到"其内部变量i"呢?其实对于JavaScript引擎来说,for循环只是一个代码块而已,根本不能算一个函数。所以,for循环中定义个变量i,其实是一个全局变量,其实之前的代码等价于: 38 | 39 | ```js 40 | var i; 41 | for(i = 0; i < 3; i++){ 42 | // 业务... 43 | } 44 | 45 | console.log(i); // 3 46 | ``` 47 | 48 | 这里就会涉及到JavaScript的编译规则了(您没看错,是编译)。很多人会觉得奇怪,JavaScript不是解释型动态语言吗,也需要编译?是的,JavaScript是需要编译的,这个我以后会专门写一篇相关的博客。 49 | 50 | # 作用域链 51 | 我们知道JavaScript函数的内部可以定义变量,也可以定义函数。就像这样: 52 | 53 | ```js 54 | var arg2 = 3; 55 | 56 | function foo(arg1){ 57 | var arg2 = arg1 + 1; 58 | 59 | function baz(arg3){ 60 | console.log(arg2 + arg3); 61 | } 62 | 63 | baz(4); 64 | } 65 | 66 | foo(1); // 6 67 | ``` 68 | 69 | 看到这段代码的时候,肯定有很多初学者会疑惑:到底baz中打印的arg2参数,是foo中定义的arg2还是在全局定义的arg2,或者都不是。这里就涉及到我们作用域链的问题。 70 | 71 | 当程序执行到baz内部时,引擎首先会检查当前的作用域中有没有定义arg2,如果没有就向上一级作用域查询,如果上一级作用域有定义arg2,那么就直接使用这个定义;如果上一级作用域也没有arg2的定义,再往上一级作用域查找,依次类推至全局作用域(浏览器中的window或Node.js的global),万一全局作用域也没有,那么JavaScript就会抛出异常("arg2 is not defined")。根据这个规则,我们可以轻松解析以上代码(图1可以帮助我们理解这一过程)。 72 | 73 | ![scope chain](./file/scope.png) 74 | 75 | 所有的变量查找都是从最内部开始,到全局作用域结束。所以很多时候我们会一部小心就访问到全局作用域,并且JavaScript中所有的对象都是可以任意改变的(除非Object.freeze之后),这样一来全局作用域很容易被修改,也就是我们常说的"全局作用域污染"。 76 | 77 | # 避免全局污染 78 | 全局污染是个很头疼的问题,我们经常拿到不是自己想要的变量值。为了更直观的了解全局污染,我们先来看段代码: 79 | 80 | ```js 81 | var arg = 'start'; 82 | 83 | function foo(){ 84 | console.log(arg); 85 | 86 | arg = 'foo_start'; 87 | 88 | console.log(arg); 89 | 90 | console.log(window.arg) 91 | } 92 | 93 | function baz(){ 94 | // 使用arg... 95 | } 96 | 97 | foo(); // start 98 | // foo_start 99 | // foo_start 100 | baz(); 101 | ``` 102 | 103 | foo被调用后,全局变量arg已经被更改,当baz被调用的时候,已经不是原来的arg了(偶漏!怎么会这样!)!如果foo和baz一定要公用一个变量,而又不能污染全局变量,我们可以这么做: 104 | 105 | ```js 106 | var arg = 'start'; 107 | 108 | (function(obj){ 109 | var arg; 110 | 111 | function foo(){ 112 | arg = 'foo_start'; 113 | 114 | console.log(arg); 115 | } 116 | 117 | function baz(){ 118 | console.log(arg); 119 | console.log(obj.arg); 120 | } 121 | 122 | foo(); // foo_start 123 | baz(); // foo_start 124 | // start 125 | })(window); 126 | 127 | function foo1(){ 128 | // 使用arg... 129 | } 130 | 131 | foo1(); 132 | ``` 133 | 134 | 由于自执行函数的作用域是独立的,所有并且我们将window当做参数传入。由于我们没有改变全局变量,所以我们在foo1函数中使用的arg还是原来的'start'(保持初心,吼吼!)。其实这个应用很常见,比如我们熟知的JQuery就是这么干的。 135 | 136 | # 总结 137 | JavaScript的作用域其实不难,只要掌握了规则就可以很好的驾驭。关于文中提到的JavaScript编译,我以后会更新一篇新的相关博客。其实讲到JavaScript的作用域,就不得不讲到一个概念:闭包。我们下一篇博客就来讲讲"闭包"。 138 | -------------------------------------------------------------------------------- /this.md: -------------------------------------------------------------------------------- 1 | ##### 问题 2 | 今天(2016-01-26)在逛[cnode](https://cnodejs.org)的时候,看到了这么一个[问题](https://cnodejs.org/topic/56a6075a073124894b190afd),正好与我今晚要写的东西相关,所以借这个问题开始我们今天的探讨。下面是一段nodejs代码: 3 | 4 | ```js 5 | var fs = require('fs'); 6 | var val1; 7 | 8 | val1 = 'hello me'; 9 | 10 | function test() { 11 | var val1; 12 | 13 | val1 = 'hello he'; 14 | 15 | fs.readFile('/test.js', function(err, data){ 16 | var val1; 17 | 18 | val1 = 'hello you'; 19 | 20 | console.log(this.val1); 21 | console.log(this === GLOBAL); 22 | }); 23 | } 24 | 25 | test(); // undefined 26 | // true 27 | ``` 28 | 29 | 好吧,我承认我看不惯原来的乱糟糟的代码(擅自修改,希望原提问者不要告我侵权)。但是效果是一样的,相信很多新手看到结果之后都很惊讶(我滴乖乖,怎么会出现这个结果?)。 30 | 31 | ##### this到底引用了什么 32 | 很多新手一上来就会问,this到底是什么?其实应该问:this到底引用的什么?因为在不同情况下,this的指向是不一样的。下面我们就来介绍各种不同情况下,this的指向。想要搞清楚this当前指向谁就要弄明白this的指向与变量的查找规则是有很大的不同。变量在其定义的时候就可以确定下来,而this是在调用时才能清楚。这就需要我们明白当前到底是哪些函数被调用了,也就是说要弄清楚调用栈。 33 | 34 | ```js 35 | function test(){ 36 | console.log('test is called'); 37 | 38 | test1(); // test1被调用的位置 39 | } 40 | 41 | function test1(){ 42 | console.log('test1 is called'); 43 | 44 | test2(); // test2被调用的位置 45 | } 46 | 47 | function test2(){ 48 | console.log('test2 is called'); 49 | } 50 | 51 | test(); // test被调用的位置 52 | ``` 53 | 54 | 以上代码很好的展示了调用栈:test--->test1--->test2。了解了基本概念,我们就可以开始介绍this的引用规则了。 55 | 56 | ##### 默认绑定 57 | 什么是默认规则,默认规则就是不能应用后面3种规则的规则。默认规则是最简单的规则,直接观察函数在哪里被调用也就是函数的调用位置。思考以下代码: 58 | 59 | ```js 60 | var company; 61 | 62 | company = 'BAT'; 63 | 64 | function getCompany(){ 65 | var company = '全球无限皮包公司'; 66 | 67 | console.log(this.company); 68 | console.log(this === window); 69 | } 70 | 71 | getCompany(); // BAT --->注意调用位置 72 | // true 73 | ``` 74 | 我们看到getCompany的调用位置是全局作用域,那么this就是指向window(前端),后端指向global(nodejs)。所以this.cpmpany是“BAT”(俺也想进BAT,求推荐)。默认规则就是函数在哪调用,this就指向谁。 75 | 76 | ##### 隐式绑定 77 | 隐式绑定的意思就是没有显示的对this进行绑定操作。思考一下代码: 78 | 79 | ```js 80 | var a = 0; 81 | 82 | var obj = { 83 | a: -1, 84 | getVal: function(){ 85 | return this.a; 86 | } 87 | } 88 | 89 | console.log(obj.getVal()); // -1 90 | ``` 91 | 92 | 其实相信很多人都这么用过,但是不会有太多的人去探究到底为什么this会指向obj本身。其实这是一个典型的隐式绑定的效果,getVal函数的实际调用者是obj,所以this隐式的指向了obj对象。我们把这种绑定规则叫做"隐式绑定"。 93 | 94 | ##### 显示绑定 95 | 与隐式绑定相呼应,我们可以进行显示绑定或者说是硬绑定。就是明确的、手动指定this的指向。估计说到这里,大家都会想到2个函数:call、apply。是的,就是它们。我们来看下面的代码。 96 | 97 | ```js 98 | function foo(iam){ 99 | console.log(iam + this.name); 100 | } 101 | 102 | foo.call({name: '小明'}, '我的名字叫'); // 我的名字叫小明 103 | foo.apply({name: '小花'}, ['我的名字叫']); // 我的名字叫小花 104 | ``` 105 | 106 | call和apply的第一个参数是指定foo此次调用时的this引用(显示指定),关于call和apply的介绍与我们今天的主题无关,我这里就不多赘述了。然后foo中this.name就可以取到我们传入的值,说明此时this的指向就是我们传入的第一个参数。 107 | 108 | ##### new 109 | 在其他语言中,new是用来开辟空间,创建实例。在JavaScript中,new是用来创建一个新的对象实例(JavaScript内一切皆对象,函数也可以认为是对象,new后面只能跟函数),然后调用函数。其实一个new做了2个事情,但是有的时候我们需要分开做(这时候我们不应该用new,怎么做我之后会在prototype相关博客中详细介绍,这里就不多说了)。new之后返回的对象:如果函数没有返回值则返回一个新的对象实例({});如果有返回值则是该返回值。new完之后this的引用是什么呢?思考以下代码: 110 | 111 | ```js 112 | var style = '普通青年'; 113 | 114 | var Foo = function(){ 115 | this.style = '文艺青年'; 116 | } 117 | 118 | var obj = new Foo(); 119 | console.log(obj.style); // 文艺青年 120 | ``` 121 | 按照上面我们提到的,在如果应用默认规则那么obj.style应该是“普通青年”,但是现在遇到“new”这个关键字就不一样了,this指向的是新创建的实例(通过new变成了一个文艺青年)。 122 | 123 | ##### 优先级 124 | 4种情况都介绍完了,那么问题又来了(挖掘机技术哪家强?)。如果几种情况不是单独出现的,是混合在一起的,那么我们该应用哪种情况?思考以下代码: 125 | 126 | ```js 127 | function foo(v){ 128 | this.v = v; 129 | } 130 | 131 | var obj = { 132 | v: 'obj_v', 133 | foo: foo 134 | } 135 | 136 | var obj2 = {} 137 | 138 | console.log(obj.v); // obj_v 139 | obj.foo('v'); 140 | console.log(obj.v); // v 141 | 142 | obj.foo.call(obj2, 'argument_v'); 143 | console.log(obj.v); // v 144 | console.log(obj2.v); // argument_v 145 | 146 | var newFoo = new obj.foo('new_v'); 147 | console.log(newFoo.v); // new_v 148 | console.log(obj.v); // v 149 | ``` 150 | 151 | 通过上面的代码我们可以看出,我们直接打印obj.v是打印出默认值“obj_v”。当我们隐式绑定之后,obj.v就变成了“v”。后面我们有应用了显示绑定,打印的文本说明了foo内部的this指向了我们传入的obj2而不是obj。最后我们把new和隐式绑定比较,newFoo.v的值是“new_v”,obj.v的值还是“v”。一些列的测试说明,显示绑定的优先级高于隐式绑定,而new的优先级也高于隐式绑定。但是显示绑定和new是不可以同时使用的(var newFoo = new obj.foo.call('err_v'); // TypeError)。但是我们可以用另外一种方式来比较。 152 | 153 | ```js 154 | function foo(v, v1){ 155 | this.val = v + v1; 156 | } 157 | 158 | var baz = foo.bind(null, 'bind_v'); 159 | var newFoo = new baz('new_v'); 160 | 161 | console.log(newFoo.val); // bind_vnew_v 162 | ``` 163 | 164 | 其实bind和apply或者call的内部实现机制是类似的。我们可以看到,调用了new之后,this绑定指向了新的实例对象。所以我们可以知道,new比显示绑定的优先级要高。至此,我们搞清楚了各种绑定方式的优先级:new绑定>显示绑定>隐式绑定>默认绑定。 165 | 166 | ##### 绑定丢失 167 | 其实绑定在某些情况下是会丢失的,特别是隐式绑定。思考以下代码: 168 | 169 | ```js 170 | var a = '机器猫'; 171 | 172 | var obj = { 173 | a: '大熊', 174 | getA: function(){ 175 | console.log(this.a); 176 | } 177 | } 178 | 179 | setTimeout(obj.getA, 1000); // 机器猫 180 | ``` 181 | 182 | 按照上面的规则,这里应该应用隐式绑定,但是我们看到打印出来的a居然是“机器猫”。其实上面的代码可以有另外一种形式: 183 | 184 | ```js 185 | var foo, a = '机器猫'; 186 | 187 | var obj = { 188 | a: '大熊', 189 | getA: function(){ 190 | console.log(this.a); 191 | } 192 | } 193 | 194 | foo = obj.getA; 195 | 196 | setTimeout(foo, 1000); // 机器猫 197 | ``` 198 | 199 | 我们把obj.getA赋值给了foo,就是把obj.getA的引用赋值给了foo(function在JavaScript是对象)。而调用foo时,foo处在全局作用域中且应用的是默认规则。所以我们在等待漫长的1秒钟后,看到了“机器猫”而不是“大熊”。现在再回过来看我们一开始的问题,思路就很清晰了:test函数实在全局内调用,readFile函数第二个参数是一个隐式赋值绑定,所以丢失了绑定,最后导致this指向了global。 200 | 201 | ##### 总结 202 | 说了那么一大坨(希望我这一坨话能帮到你),我都已经口干舌燥了。现在相信大家应该知道this的引用规则了,不同的情况用不同的规则,混合情况使用优先级先后顺序,这样无论遇到哪个this都可以一招KO掉。 203 | -------------------------------------------------------------------------------- /type.md: -------------------------------------------------------------------------------- 1 | ##### JavaScript基本类型 2 | 说到类型,在JavaScript中有6中基本类型:boolean、number、string、object、null、undefined,所有的JavaScript代码都不会离开这6个类型。 3 | 4 | ##### 隐式类型转换 5 | 在JavaScript存在着很多隐式的类型转换,很多我们都用到过。思考以下代码: 6 | 7 | ```js 8 | function foo(){ 9 | var isSuperMan = 'no'; 10 | 11 | if(isSuperMan) { 12 | console.log('you are superman'); 13 | } 14 | else { 15 | console.log('you are human'); 16 | } 17 | } 18 | 19 | foo(); // you are superman 20 | ``` 21 | 22 | 我们知道在其他语言中,if后面的判断语句一定是布尔型变量或者结果是布尔型的表达式。但是在JavaScript中,if后面可以跟任意类型。这里就涉及到了隐式转换,JavaScript把不在以下值中的所有非布尔值都转换成true:null、undefined、0、-0、+0、''。还有一种更常见的隐式类型转换出现在“+”的两侧,例如: 23 | 24 | ```js 25 | function stringConcat(val1, val2){ 26 | return val1 + val2; 27 | } 28 | 29 | stringConcat(1, 'abc'); // 1abc 30 | ``` 31 | 32 | 在stringConcat中,数字1被隐式转换成了字符串之后被输出。因为隐式转换,数学中的结合律在这里也是不成立的: 33 | 34 | ```js 35 | console.log(1 + 2 + '3' === 1 + (2 + '3')); // false 36 | ``` 37 | 前一个表达式的结果是字符串“33”,而后面一个表达式的结果是字符串“123”,是2个完全不同的结果。这一切都是拜隐式转换所赐。现在大家可以去自己的代码中去发现隐式转换了。 38 | 39 | ##### 坑爹的浮点数 40 | 要知道在JavaScript中所有的数字都是双精度浮点数,然后浮点数是出了名的不精确(即使它表示的范围已经足够大)。在某些计算时要特别小心掉入此坑中,来看下面的代码: 41 | 42 | ```js 43 | function plus(p1, p2){ 44 | return p1 + p2; 45 | } 46 | 47 | plus(0.0001, 0.0002); // 0.00030000000000000003 48 | ``` 49 | 50 | (妈呀,都说电脑很强大,但是这个时候还不如俺的“猪脑子”)我们看到0.0001与0.0002的和居然不是0.0003,后面多了尾巴。这是因为浮点数本来就不是精确表示的,计算结果都会进行舍入。那么我们如果真的要计算的话,该怎么办呢?其实也不是什么难事: 51 | 52 | ```js 53 | function plus(p1, p2){ 54 | return (p1*10000 + p2*10000)/1000; 55 | } 56 | 57 | plus(0.0001, 0.0002); // 0.0003 58 | ``` 59 | 60 | 方法就是讲小数转换成整数,因为整数是没有小数部分的,不需要舍入,是精确的。 61 | 62 | ##### 既熟悉又陌生的string 63 | string对于任何一个写程序的程序猿都是非常熟悉的,我们几乎每天都会用到。但是问题来了,像"abcd"这种,只是字面量,根本不是对象。但是,我们又可以像这样 64 | 65 | ```js 66 | console.log("123abc".length); // 6 67 | ``` 68 | 69 | 调用length属性或者方法(charAt等)。并且: 70 | 71 | ```js 72 | console.log("123abc" instanceof String); // false 73 | ``` 74 | 75 | "123abc"也不是内置对象String的一个实例,却还可以使用String的length属性和其它所有的方法。"123abc"是字面量没错,每当其获取属性length或者调用其它String的方法时,JavaScript都会自动用字面量生成一个String的实例,这样看上去就是字面量在操作一样。这个对象在使用完成之后会被立即销毁。 76 | 77 | ##### 小心typeof掉坑 78 | 首先大家要记住:typeof是一个操作符,不是一个函数,虽然现在很多JavaScript引擎可以让我们把typeof当做函数来调用,但我还是强烈建议去掉括号,就想这样: 79 | 80 | ```js 81 | console.log(typeof {a: 1}); // object 82 | ``` 83 | 84 | 大多数时候,我们用typeof是没有问题的。但是当我们对null做typeof的时候,结果会令人很惊奇: 85 | 86 | ```js 87 | console.log(typeof null); // object 88 | ``` 89 | 90 | 按照道理应该显示null才对,现在却打印了object。这其实是JavaScript的一个隐形的BUG,在JavaScript设计之初,JavaScript中的值是由两部分组成:一部分是表示类型的标签,另一部分表示值。然后null表示的空指针在大多数平台下都是0x00,与JavaScript中object标签值0正好一样。所以typeof null的结果是“object”(有人提出在ES2015中修复这个BUG,但是后来被否决了)。下图展示了type各种类型的结果: 91 | 92 | ![typeof](./file/typeof.png) 93 | 94 | 所以我们在用typeof的时候,最好加入其它判断条件一起判断。 95 | 96 | ##### 总结 97 | 好了,JavaScript的基本类型其实就6种,只要搞清楚这其中哪里有坑,下次再coding的时候就不会莫名其妙的掉进去了。由于object我会在以后专门写一篇博客来讲(因为object实在太强大了,作为一部分讲会贬低了它)。 98 | -------------------------------------------------------------------------------- /v8.dev/launching-ignition-and-turbofan.md: -------------------------------------------------------------------------------- 1 | # Ignition 与 TurboFan 发布 2 | 3 | 今天我们很激动地宣布:在 V8 v5.9 版本(对应 Chrome v59)中将发布一个新的 JavaScript 执行管道。使用这条新的执行管道,当前的 JavaScript 应用将会在性能上有大幅度的提升并且消耗的内存将会更少。在本文的末尾,我们将会讨论更加详细的数据,在此之前我们先来介绍下这条执行流本身 4 | 5 | 这条新的执行流建立在 V8 的解释器 Ignition 和 V8 的最新优化编译器 TurboFan 的基础之上。对于一直关注我们 V8 blog 的同学来说,这些技术应该还是比较熟悉的,但是切换到新的执行流对于 Ignition 和 TurboFan 都是一个重大的里程碑。 6 | 7 | 8 |

Ignition logo,V8新解释器

9 | 10 | 11 |

TurboFan logo,V8新优化编译器

12 | 13 | V8 v5.9 中的全面并且仅仅使用 Ignition 和 TurboFan 还是历史首次。并且从 v5.9 开始,Full-codegen 和 Crankshaft(从 2010 年开始服务于 V8 的技术)将不再使用于 V8 的 JavaScript 执行管道,从此它们将不会与新的语言特新和优化需求保持同步更新。我们打算尽快将它们完全移除,这就意味着 V8 将会朝着一个更加简单、更加可维护的方向发展。 14 | 15 | # 漫长的旅程 16 | 17 | Ignition 和 TurboFan 组合管道已经开发了 3 个半年的时间。它是 V8 团队搜集现实世界 JavaScript 性能表现和对 Full-codegen 与 Crankshaft 缺点考虑的结果,是我们可以持续优化 JavaScript 语言的基础。 18 | 19 | TurboFan 项目最初开始于 2013 年底,旨在解决 Crankshaft 的不足之处。Crankshaft 只能优化 JavaScript 的一部分代码。举个栗子,最初设计时,它是不能优化异常处理例如被 try...catch...finally 包起来的代码块。我们很难在 Crankshaft 中加入新的语言特性,因为这些特性需要我们在 9 个所支持的平台根据不能架构编写对应架构匹配的代码。而且,Crankshaft 架构在某种程度上生成最佳的机器码受到限制。尽管 V8 团队在每个架构上都维护了成千上万行代码,Crankshaft 也就只能挤出那一丁点儿性能了。 20 | 21 | TurboFan 从一开始就被设计成不仅支持现有 JavaScript 标准 ES5 还可以支持计划中的 ES2015 和之后更新的版本。它引入了一个叫分层编译器的设计,使得在高等级编译优化和底层编译器优化有一个明显的分界,这样的设计可以使我们很容易优化新的语言特性而不需要修改面向不同架构的代码(architecture-specific code)。TurboFan 增加了一个显式的指令选择编译阶段,使得用更少的(不同架构的)代码就可以支持不同平台成为可能。引入这个阶段,不同架构的代码只需写一次,而且几乎不需要更改。基于以上以及一些其他因素,一个支持所有 V8 所支持架构的,可维护的、可扩展的优化编译器就产生了。 22 | 23 | V8 Ignition 解释器最初的背后动机是降低在移动设备上的内存消耗。在 Ignition 之前,Full-codegen 基本编译器所生成的代码几乎占了 Chrome 内存堆的 1/3。而实际留给 web 应用的空间就变少了。当在一台内存受限的安卓一定设备的 Chrome M53 上启用 Ignition 时,未被优化的 JavaScript 代码所需要的基本内存消耗会减少 9 倍(基于 ARM64 的移动设备)。 24 | 25 | 之后 V8 团队充分利用了 Ignition 字节码可以配合 TurboFan 直接生成机器码而不是重新编译源码(如 Crankshaft)这一优点。Ignition 的字节码在 V8 中提供了一个更干净、更少错误的基线执行模型,简化了脱优化(deoptimization)机制是 V8 自适应优化的一个关键功能。最后,因为生成字节码要比 Full-codegen 生成编译的代码更快,使用 Ignition 一般来说会改善脚本的启动时间、切换和页面加载。 26 | 27 | 随着 Ignition 和 TurboFan 的组合越来越紧密,在总体架构上还带来了更多的好处。比如 V8 团队使用 TurboFan 的中间语言来表示这些处理器的功能性,让 TurboFan 做优化和为各支持平台生成对应的代码,而不是手写高性能的字节码处理器。这使得 Ignition 在 V8 所支持的所有架构上表现良好,同时也减轻了 9 个相对独立的平台的维护负担。 28 | 29 | # 实测数据 30 | 31 | 撇开历史不谈,让我们来看下新管道在现实世界的表现和内存消耗。 32 | 33 | V8 团队使用 Telemetry - Catapult 框架来持续监控显示世界 V8 的性能表现。之前我们在博客中讨论过为什么使用来自现实世界的数据来驱动我们的性能优化工作是如此重要以及我们是如何使用 WebPageReplay 与 Telemetry 一起工作的。切换到 Ignition 和 TurboFan 展示了现实世界测试用例的性能提升。特别是新的管道显著加快了著名网站的用户交互测试速度。 34 | 35 | 36 |

用户交互基准测试V8的时间消耗

37 | 38 | 尽管 Speedometer 使用的合成的基准测试,不过我们之前已经揭示过,与其他合成的基准相比,它在现在 JavaScript 工作负载方面的表现更接近现实世界。切换到 Ignition 和 TurboFan,V8 的 Speedometer 跑分根据平台和设备的不同有 5%-10%的提升。 39 | 40 | 新的管道也加快了服务端 JavaScript 速度。AcmeAir:Node.js 的基准测试,模拟虚拟管道的后端实现,在使用 V8 v5.9 后使速度加快了 10%。 41 | 42 | 43 |

在web和Node.js上的基准测试提升

44 | 45 | Ignition 与 TurboFan 同时也减少了 V8 的内存占用。在 Chrome M59,新的流程缩减了桌面程序和高端移动设备 V8 的内存约 5%-10%。这个减少(5%-10%)是之前在本博客提到过的 Ignition 对于 V8 所支持的所有设备和平台内存节省(策略)的结果。 46 | 47 | 这些提升仅仅是个开始。新的 Ignition 与 TurboFan 管道在未来的几年为进一步优化 JavaScript 性能和缩小 V8 性能开销铺平道路(同时在 Chrome 浏览器和 Node.js 端)。我们期待在我们向开发者和用户推出它们的时候与您分享这些提升。尽情期待。 48 | 49 | ##### V8 团队发表于 v8.dev( 原文:https://v8.dev/blog/launching-ignition-and-turbofan ) 50 | -------------------------------------------------------------------------------- /wechat/mp/callapi.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 3 | [上一篇](start.md)我们讲了如果简单的接收微信的消息,当然,公众号不止会推送消息,还提供了丰富的 API 供开发者调用。想要了解微信提供了哪些接口,可以访问微信[官方文档](https://mp.weixin.qq.com/wiki)。在公众号后台,“开发”--“接口权限”菜单中也可以看到所有 API,并且微信已经做好了分类。 4 | 5 | # access_token 6 | 7 | 首先,所有的 API 都不是随意调用的。在调用任何 API 之前,都需要先获取令牌(即接口调用凭证:access_token)。不同公众号的 access_token 是不同的,微信会根据 access_token 来定位公众号,执行相应的操作。接下来我们就以最常用的“自定义菜单”接口为例,来介绍下如何调用微信公众号 API: 8 | 9 | - 获取 access_token 10 | 上面说到,在调用 API 之前,先要获取 access_token。根据官方[获取 access_token 文档](https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140183),获取 access_token 是一个 3 个参数的 HTTP GET 请求: 11 | 12 | - grant_type 在说明中,我们看到,微信这个参数是定死的,就传 client_credential 13 | - appid 第三方用户唯一凭证 14 | - secret 第三方用户唯一凭证密钥 15 | 16 | grant_type 没有问题,我们直接传 client_credential 就行了,那 appid 和 secret 哪来呢?我们可以登录公众号后台,在“开发”--“基本配置”中找到“开发者 ID(AppID)”和“开发者密码(AppSecret)”。开发者 ID 就是 appid,开发者密码就是 secret。好了,万事俱备,让我们来获取下 access_token: 17 | 18 | ```js 19 | 'use strict'; 20 | 21 | const rp = require('request-promise'); 22 | 23 | async function getAccessToken(appId, secret) { 24 | const result = await rp( 25 | `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appId}&secret=${secret}` 26 | ); 27 | 28 | if (result.access_token & result.expires_in) { 29 | // 打印结果 30 | return console.log(result); 31 | } 32 | } 33 | 34 | getAccessToken('myAppId', 'mySecret'); 35 | ``` 36 | 37 | 如果没有问题,我们会得到一个 json 的响应。json 里面有 2 个属性值:access_token 和 expires_in,access_token 就是我们需要的,而 expires_in 是表示 access_token 的有效实现,一般情况下是 7200 秒,也就是 2 小时有效期。所以,我们必须在获取到 access_token 后 2 小时内使用它,否则就会过期。如果 access_token 过期,我们必须重新获取一次。 38 | 39 | # 调用创建菜单的 API 40 | 41 | 根据微信的创建菜单的[文档](https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141013),创建菜单是一个 HTTP POST 方法, 这个 API 有 2 个参数,其中一个就是刚刚获取的 access_token,另一个就是菜单的内容。注意,所有接口 access_token 都是作为 query 参数传过去的。 42 |  43 | 44 | ```js 45 | 'use strict'; 46 | 47 | const rp = require('request-promise'); 48 | 49 | async function setMenu(accessToken, menu) { 50 | const result = await rp({ 51 | method: 'POST', 52 | uri: `https://api.weixin.qq.com/cgi-bin/menu/create?access_token=${accessToken}`, 53 | json: true, 54 | body: menu 55 | }); 56 | 57 | if (result.errcode === 0) { 58 | // 设置成功 59 | return console.log('success'); 60 | } 61 | 62 | // 设置失败 63 | console.log('failed'); 64 | } 65 | 66 | const menuObj = { 67 | button: [ 68 | { 69 | type: 'click', 70 | name: '今日歌曲', 71 | key: 'V1001_TODAY_MUSIC' 72 | }, 73 | { 74 | name: '菜单', 75 | sub_button: [ 76 | { 77 | type: 'view', 78 | name: '搜索', 79 | url: 'http://www.soso.com/' 80 | }, 81 | { 82 | type: 'miniprogram', 83 | name: 'wxa', 84 | url: 'http://mp.weixin.qq.com', 85 | appid: 'wx286b93c14bbf93aa', 86 | pagepath: 'pages/lunar/index' 87 | }, 88 | { 89 | type: 'click', 90 | name: '赞一下我们', 91 | key: 'V1001_GOOD' 92 | } 93 | ] 94 | } 95 | ] 96 | }; 97 | setMenu('accessToken', menuObj); 98 | ``` 99 | 100 | 大部分公众号的 API 调用之后,都会有一个错误码 errcode,如果 errcode 为 0,则表示成功;如果 errcode 为其他值,那表示 API 调用有问题。 101 | 如果返回了其他错误码,可以通过微信的[全局返回码说明](https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1433747234)来查询具体的错误原因。 102 | 103 | # 总结 104 | 105 | 只要记住,在调用微信 API 时,access_token 是必不可少且一定是放在 query 里面的。如果调用出了什么问题,可以根据微信返回的错误码去官方查询具体原因。不过,根据我的经验,有些错误码微信也没有给出解释,祝各位好运! 106 | -------------------------------------------------------------------------------- /wechat/mp/start.md: -------------------------------------------------------------------------------- 1 | # 开篇 2 | 3 | 笔者大概 2013 年开始接触围绕微信相关的开发,到今年(2018)也有 5 年的时间了。记得,刚开是接触微信公众号相关开发的时候,微信公众号还没有开放 API。市场上只有几家很牛逼的企业(如招商银行、印象笔记)有拿到授权。我们不得不利用脚本的形式,来操作 dom 实现业务,是一个很蠢但是最快的接入方式。后来微信开放了 API,我们也第一时间跟进,在此期间也遇到了很多坑(包括腾讯自己的问题),所以我打算写一系列文章,为了可以让后来者不至于掉坑。由于笔者主要开发是基于 nodejs(go、c#也有涉及),所以代码实例均以 nodejs 为主。 4 | 5 | # 建议 6 | 7 | 当然, 再好的文章也只能当做辅助手段。最好、最直接的  还是参考微信[官方文档](https://mp.weixin.qq.com/wiki) 8 | 9 | # 公众号的分类 10 | 11 | 开始的时候,公众号并没有类型的区分,大家都是一样的。不过,在公众号上线一段时间之后,就有了订阅号和服务号的区分。 12 | 13 | - 订阅号 订阅号的申请者主要以个人为主,订阅号每天可以推送一个群发消息,不过在 API 权限方面较弱。 14 | - 服务号 服务号的申请者主要是企业,推送群发消息有限制,不过微信提供了丰富的 API 接口。 15 | - 小程序 小程序不属于公众号,不过小程序的开发流程和部分 API 和公众号是公用的,所以  也列在此。 16 | 17 | # 开始开发 18 | 19 | 正如上面所说的,最好先参考微信[官方文档](https://mp.weixin.qq.com/wiki)。官方文档中有一个菜单叫做“开始开发”, 我就按照上面的步骤来结束我们的“开篇” 20 | 21 | ## 1.准备服务器 22 | 23 | 第一步叫做“填写服务器配置”,说明在开发之前,我们需要准备一台服务器。这台服务器必须要有固定的公网 IP,这是为了接受微信发送过来的请求。填写的时候会要求填写 3 项信息: 24 | 25 | - url  简单来说,url 就是一个 HTTP 接口,不过此接口必须遵守微信的规范,微信会以调接口的形式,将相关的消息、事件  推送过来 26 | - token 由于服务器是暴露在公网环境的,为了过滤非微信的请求,保证只处理正确的请求,而 token 是用来做签名的,只处理正确的签名 27 | - EncodingAESKey EncodingAESKey 是给消息加密的 key 28 | 29 | ## 2.验证消息 30 | 31 | 第二步“验证消息的确来自微信服务器”。就像  在上一步说的,我们必须验证请求是微信服务器发来的。 正常情况下,微信服务器会 GET 方法来请求 url 并带上 4 个 query 参数: 32 | 33 | - signature 签名 34 | - timestamp 时间戳 35 | - nonce 随机数 36 | - echostr 随机字符串 37 | 38 | 生成签名代码如下 39 | 40 | ```js 41 | 'use strict'; 42 | 43 | const crypto = require('crypto'); 44 | 45 | // 排序并拼接 46 | const str = [token, timestamp, nonce].sort().join(''); 47 | // 生成签名 48 | const sha1 = crypto.createHash('sha1').update(str); 49 | const mySignature = sha1.digest('hex'); 50 | ``` 51 | 52 | 如果我们需要验证消息,需要做一下步骤 53 | 54 | - 将 token、timestamp、nonce 三个参数进行字典序排序 55 | - 将排序好的三个参数进行拼接 56 | - 将拼接好的字符串进行 sha1 得到签名 mySignature 57 | - 然后将 mySignature 和 signature 进行对比,如果一致,则验证成功!如果不一致,则验证失败 58 | 如果验证成功,我们需要将 echostr 原样返回给微信,否则微信会认为此次调用未成功,会进行重试操作 59 | 60 | ## 3.业务实现 61 | 62 | 验证成功之后,我们就可以根据业务需求,实现对应的业务。 63 | 64 | # 总结 65 | 66 | 开篇比较简单,主要介绍下概念和简单的接入,接下来一篇我会开始介绍微信公众号 access_token 的获取与保存。 67 | --------------------------------------------------------------------------------