[Node.js] 01 - How to learn node.js
基本概念
链接:https://www.zhihu.com/question/47244505/answer/105026648
链接:How to decide when to use Node.js?
JavaScript 是一种(最好的)编程语言, 主要作为前端开发中用来增加网页的动态功能,比如操作DOM, 读取用户输入, 动画效果, 提 交服务器请求(Ajax). JavaScript 是什么? - 前端开发
NodeJs 是基于JavaScript的,可以做为后台开发的语言. 提供了很多系统级的API,如文件操作、网络编程等. 用事件驱动, 异步编程,主要是为后台网络服务设计. Node.js是用来做什么的? - 编程
ReactJS 为Facebook开发的,更多的像一个JS的库.主要是在前端Web开发中, 对MVC中的V进行操作. AngularJS和ReactJS分别是干什么的?不会Javascript是否可以学习 - 前端开发
React Native 它基于开源框架ReacJS,并可用来开发iOS和Android原生应用, 主要为移动端服务. 深入浅出 React Native:使用 JavaScript 构建原生应用 - 前端外刊评论 - 知乎专栏学习指导
The ideal candidate will have:
- Expereienced in web application developement using node.js
- Experience using Chinese API, i.e. WeChat Open API
The following skills are an advantage:
- Experience in React
Experience:
- node.js: 1 year (Required)
作者:厂长
原文地址 Node.js is the New Black
浏览器给网站发请求的过程一直没怎么变过:
- 当浏览器给网站发了请求。
- 服务器收到了请求,然后开始搜寻被请求的资源。如果有需要,服务器还会查询一下数据库
- 最后把响应结果传回浏览器。
不过,在传统的web服务器中(比如Apache),每一个请求都会让服务器创建一个新的进程来处理这个请求。
部分页面请求:
后来有了Ajax。有了Ajax,我们就不用每次都请求一个完整的新页面了,取而代之的是,每次只请求需要的部分页面信息就可以了。这显然是一个进步。
案例:
你的好友会随时的推送新的状态,然后你的新鲜事会实时自动刷新。要达成这个需求,我们需要让用户一直与服务器保持一个有效连接。目前最简单的实现方法,就是让用户和服务器之间保持长轮询(long polling)。
长轮询HTTP请求不是持续的连接,你请求一次,服务器响应一次,然后就完了。
长轮训是一种利用HTTP模拟持续连接的技巧。
具体来说,只要页面载入了,不管你需不需要服务器给你响应信息,你都会给服务器发一个Ajax请求。【客户端先发了再说】
这个请求不同于一般的Ajax请求,服务器不会直接给你返回信息,而是它要等着,直到服务器觉得该给你发信息了,它才会响应。
比如,你的好友发了一条新鲜事,服务器就会把这个新鲜事当做响应发给你的浏览器,然后你的浏览器就刷新页面了。
浏览器收到响应刷新完之后,再发送一条新的请求给服务器,这个请求依然不会立即被响应。
于是就开始重复以上步骤。利用这个方法,可以让浏览器始终保持等待响应的状态。虽然以上过程依然只有非持续的Http参与,但是我们模拟出了一个看似持续的连接状态
我们再看传统的服务器(比如Apache)。每次一个新用户连到你的网站上,你的服务器就得开一个连接。每个连接都需要占一个进程,这些进程大部分时间都是闲着的(比如等着你好友发新鲜事,等好友发完才给用户响应信息。或者等着数据库返回查询结果什么的)。虽然这些进程闲着,但是照样占用内存。这意味着,如果用户连接数的增长到一定规模,你服务器没准就要耗光内存直接瘫了。
非阻塞和事件驱动这种情况怎么解决?解决方法就是刚才上边说的:非阻塞和事件驱动。
你把非阻塞的服务器想象成一个loop循环,这个loop会一直跑下去。一个新请求来了,这个loop就接了这个请求,把这个请求传给其他的进程(比如传给一个搞数据库查询的进程),然后响应一个回调(callback)。完事了这loop就接着跑,接其他的请求。
这样下来。服务器就不会像之前那样傻等着数据库返回结果了。
如果数据库把结果返回来了,loop就把结果传回用户的浏览器,接着继续跑。在这种方式下,你的服务器的进程就不会闲着等着。
从而在理论上说,同一时刻的数据库查询数量,以及用户的请求数量就没有限制了。服务器只在用户那边有事件发生的时候才响应,这就是事件驱动。
FriendFeed是用基于Python的非阻塞框架Tornado (知乎也用了这个框架) 来实现上面说的新鲜事功能的。
不过,Node.js就比前者更妙了。Node.js的应用是通过javascript开发的,然后直接在Google的变态V8引擎上跑。用了Node.js,你就不用担心用户端的请求会在服务器里跑了一段能够造成阻塞的代码了。
因为javascript本身就是事件驱动的脚本语言。你回想一下,在给前端写javascript的时候,更多时候你都是在搞事件处理和回调函数。javascript本身就是给事件处理量身定制的语言。
Node.js还是处于初期阶段。如果你想开发一个基于Node.js的应用,你应该会需要写一些很底层代码。但是下一代浏览器很快就要采用WebSocket技术了,从而长轮询也会消失。在Web开发里,Node.js这种类型的技术只会变得越来越重要。
Ref: WEB通讯技术之短轮询、长轮询(comet)、长连接(SSE)、WebSocket
实现Web端即时通讯的方法:实现即时通讯主要有四种方式,它们分别是短轮询、长轮询(comet)、长连接(SSE)、WebSocket。
①短轮询
服务器端在收到请求后,不论是否有数据更新,都直接进行响应。
实现简单;但链接频繁,洪水攻击的危险。
②comet 长轮询
当服务器收到客户端发来的请求后,不会直接进行响应,而是先将这个请求挂起,然后判断服务器端数据是否有更新。
如果有更新,则进行响应,如果一直没有数据,则到达一定的时间限制(服务器端设置)后关闭连接。
明显减少了很多不必要的http请求次数,相比之下节约了资源。However,连接挂起也会导致资源的浪费。
③SSE
SSE是HTML5新增的功能,全称为Server-Sent Events。允许服务推送数据到客户端。
SSE在本质上就与之前的长轮询、短轮询不同,虽然都是基于http协议的,但是轮询需要客户端先发送请求。而SSE最大的特点就是不需要客户端发送请求。
④WebSocket
WebSocket是Html5定义的一个新协议,与传统的http协议不同,该协议可以实现服务器与客户端之间全双工通信。
简单来说,首先需要在客户端和服务器端建立起一个连接,这部分需要http。连接一旦建立,客户端和服务器端就处于平等的地位,可以相互发送数据,不存在请求和响应的区别。
四种Web即时通信技术比较
从兼容性角度考虑,短轮询 > 长轮询 > 长连接SSE > WebSocket;
从性能方面考虑,WebSocket > 长连接SSE > 长轮询 > 短轮询。
Ref: 七种WebSocket框架的性能比较【实验数据比较】
测试结果分析
- Netty, Node.js, Undertow, Vert.x都能正常建立百万连接。 Jetty, Grizzly 和 Spray未能完成百万连接
- Netty表现最好。内存占用非常的少, CPU使用率也不高。 尤其内存占用,远远小于其它框架
- Jetty, Grizzly和Spray会产生大量的中间对象,导致垃圾回收频繁。Jetty表现最差
- Node.js表现非常好。 尤其是测试中使用单实例单线程,建立速度非常快,消息的latency也很好。 内存占用也不错
- Undertow表现也不错,内存占用比Netty高一些,其它差不多
- 这里还未测到Spray另一个不好的地方。 在大量连接的情况小,即使没有消息发送,Spray也会占用40% CPU 时间
- 来自:http://www.tuicool.com/articles/veIvue
“下一代浏览器很快就要采用WebSocket技术,长轮询(long polling)也就会消失掉”
- 什么是WebSocket?
- WebSocket跟普通的HTTP有什么不一样?
- WebSocket是否会取代HTTP?
(1).
关于第一个问题,简单来说,WebSocket主要提供了一个全双工(Full-Duplex)和持久化(Long lived)的连接,它跟HTTP一样工作在TCP层上,它主要解决了长轮询的问题,那什么是长轮询问题呢?
首先看一个应用场景:浏览器(客户端)想从服务器端接收实时数据。
在以前,客户端就会采取长轮询(long polling)机制:客户端发送请求以后,如果没有等到服务器的响应,该请求就阻塞等待服务器返回消息,直到服务器返回响应以后,客户端才会发起新的HTTP轮询请求 (注意:如果是HTTP1.1,那么新的HTTP请求会复用前一个HTTP请求的TCP链接),由于是阻塞等待模式,如果这时候,客户端还需要再一次发送消息,就需要建立新的TCP连接到服务器,从而造成资源的浪费;
另一方面,如果服务器想要再次推送新的响应消息给客户端,它需要等待客户端发起新的请求,才能给予。。。(HTTP的Request只能有一个Response,服务器不是想给就能给,不便于"推送")
实际上HTTP Streaming技术是可以满足服务器发送多个消息到客户端的:
HTTP streaming: a variety of techniques (multipart/chunked response) that allow the server to send more than one response to a single client request. The W3C is standardizing this as Server-Sent Events using a text/event-stream MIME type. The browser API (which is fairly similar to the WebSocket API) is called the EventSource API.
但是,并不能解决客户端阻塞等待或者建立新连接的问题。
而WebSocket协议,允许客户端和服务器端可以在任何时间任意发送消息,并且异步处理接收到的消息,很好的解决了长轮询这种类似hacks服务器的简单粗暴行为。
(2).
关于第二个问题,WebSocket在第一次跟服务器握手的时候,会用到HTTP协议,在Upgrade里指明该次握手是用于建立WebSocket (Upgrade: WebSocket),握手之后,客户端和服务器之间的通信,将会按照WebSocket的协议报文来通信。
实际上,HTTP和WebSocket这两种协议在初次跟服务器握手时,都采用一样的建连方式,但是WebSocket只需要建立一次TCP连接,随后只需要发送较小的WebSocket报文(该报文只有6个字节的开销,2个字节header,4个字节的mask value)。
顺便插一句,很喜欢stackoverflow上面的一段话:
The latency overhead is not so much from the size of the headers, but form the logic to parse/handle/store those headers. In addtion, the TCP connection setup latency is probably a bigger factor than the size or processing time for each request.
这里讲TCP建连的开销比处理header大小需要的开销更多,其实,这个也适用于其他方面,消息大小长短可能不是造成开销的主要原因,更为重要的是我们用什么方式(逻辑)去解析/处理这些消息?
(3).
关于第三个问题,WebSocket并不会取代HTTP协议,它只是HTTP的一个扩展,它主要应用的场景是,当javascript应用运行的浏览器(客户端)想从服务器端接收实时数据,服务器可以随时推送消息到客户端,客户端也不需要阻塞或者重新建立连接到服务器。
最后说下,为什么WebSocket不直接采用HTTP,那是因为WebSocket需要异步通信模式,如果采用普通HTTP,会让服务器感到困惑,所以,采用了与HTTP完全不一样的报文格式。
另外,我个人很喜欢的一篇很有意思的解读:
HTTP的弊端
请记住 Request = Response , 在HTTP中永远是这样,也就是说一个request只能有一个response。而且这个response也是被动的,不能主动发起。
Client发送请求
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13 Origin: http://example.com
然后,Sec_WebSocket-Protocol 是一个用户定义的字符串,用来区分同URL下,不同的服务所需要的协议。简单理解:今晚我要服务A,别搞错啦~
最后,Sec-WebSocket-Version 是告诉服务器所使用的Websocket Draft(协议版本)
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= Sec-WebSocket-Protocol: chat
同时由客户主动询问,转换为服务器(推送)有信息的时候就发送(当然客户端还是等主动发送信息过来的。。),
【Nginx :一个高性能的HTTP和反向代理服务器】
但是可以通过上面说的 long poll 和 ajax 轮询来 模拟出类似的效果
Node.js 创建第一个应用
Node.js 应用是由哪几部分组成的:
-
引入 required 模块:我们可以使用 require 指令来载入 Node.js 模块。
-
创建服务器:服务器可以监听客户端的请求,类似于 Apache 、Nginx 等 HTTP 服务器。
-
接收请求与响应请求 服务器很容易创建,客户端可以使用浏览器或终端发送 HTTP 请求,服务器接收请求后返回响应数据。
var http = require('http'); http.createServer( function (request, response) { // 发送 HTTP 头部 // HTTP 状态值: 200 : OK // 内容类型: text/plain response.writeHead (200, {'Content-Type': 'text/plain'}); // 发送响应数据 "Hello World" response.end ('Hello World\n'); }).listen(8888); // 终端打印如下信息 console.log('Server running at http://127.0.0.1:8888/');
NPM 使用介绍
From: http://www.runoob.com/nodejs/nodejs-npm.html
随同NodeJS一起安装的包管理工具,能解决NodeJS代码部署上的很多问题,常见的使用场景有以下几种:
- 允许用户从NPM服务器下载别人编写的第三方包到本地使用。【借用他人代码】
- 允许用户从NPM服务器下载并安装别人编写的命令行程序到本地使用。【借用他人命令行程序】
- 允许用户将自己编写的包或命令行程序上传到NPM服务器供别人使用。【分享自己的代码或命令行程序】
unsw@unsw-UX303UB$ node -v v8.9.4
unsw@unsw-UX303UB$ npm -v 5.6.0
╭─────────────────────────────────────╮
│ │
│ Update available 5.6.0 → 5.8.0 │
│ Run npm i -g npm to update │
│ │
╰─────────────────────────────────────╯
$ npm list -g // 查看所有全局安装的模块
package.json文件
From: package.json文件 - from ruanyifeng
-
node,npm 都要用。
1.1 node在调用require的时候去查找模块,会按照一个次序去查找,package.json会是查找中的一个环节。【见阮一峰的require分析 http://www.ruanyifeng.com/blog/2015/05/require.html】
1.2 npm用的就比较多,其中的 "dependencies" 字段就是本模块的依赖的模块清单。每次npm update的时候,npm会自动的把依赖到的模块也下载下来。当npm install 本模块的时候,会把这里提到的模块都一起下载下来。通过package.json,就可以管理好模块的依赖关系。 -
如果是应用,不必编写package.json
(1). 每个项目的根目录下面,一般都有一个package.json
文件,定义了这个项目所需要的各种模块,以及项目的配置信息(比如名称、版本、许可证等元数据)。
$ npm init
// 以使用npm init
命令自动生成。
(2). 有了package.json文件,直接使用npm install命令,就会在当前目录中安装所需要的模块。
$ npm install
// 根据这个配置文件,自动下载所需的模块,也就是配置项目所需的运行和开发环境。
(3). 如果一个模块不在package.json
文件之中,可以单独安装这个模块,并使用相应的参数,将其写入package.json
文件之中。
$ npm install express --save
$ npm install express --save-dev
一个更完整的package.json文件,如下所示:
{
"name": "Hello World",
"version": "0.0.1",
"author": "张三",
"description": "第一个node.js程序",
"keywords":["node.js","javascript"],
"repository": {
"type": "git",
"url": "https://path/to/url"
},
"license":"MIT",
"engines": {"node": "0.10.x"},
"bugs":{"url":"http://path/to/bug","email":"bug@example.com"},
"contributors":[{"name":"李四","email":"lisi@example.com"}],
"scripts": {
"start": "node index.js" // npm run start
},
"dependencies": { // 运行 所依赖模块
"express": "latest",
"mongoose": "~3.8.3",
"handlebars-runtime": "~1.0.12",
"express3-handlebars": "~0.5.0",
"MD5": "~1.2.0"
},
"devDependencies": { // 开发 所需要的模块
"bower": "~1.2.8",
"grunt": "~0.4.1",
"grunt-contrib-concat": "~0.3.0",
"grunt-contrib-jshint": "~0.7.2",
"grunt-contrib-uglify": "~0.2.7",
"grunt-contrib-clean": "~0.5.0",
"browserify": "2.36.1",
"grunt-browserify": "~1.3.0",
}
}
peerDependencies
需要一种机制,在模板安装的时候提醒用户,如果A和B一起安装,那么B必须是2.0模块。
从npm 3.0版开始,peerDependencies
不再会默认安装了。
{ "name": "chai-as-promised", // 安装chai-as-promised
模块时 "peerDependencies": { // 主程序chai
必须一起安装
"chai": "1.x" // 而且chai
的版本必须是1.x
}
}
bin字段
指定各个内部命令对应的可执行文件的位置。
"bin": { "someTool": "./bin/someTool.js" }
main字段
指定了加载的入口文件,require('moduleName')
就会加载这个文件。这个字段的默认值是模块根目录下面的index.js
。
"main": "index.js",
config 字段
{ "name" : "foo", "config" : { "port" : "8080" }, "scripts" : { "start" : "node server.js" } }
get(): 用户执行npm run start
命令时,脚本就可以得到该引用config值,如下:
http
.createServer(...)
.listen(process.env.npm_package_config_port)
set(): 用户也可以改变这个值。
$ npm config set foo:port 80
其他字段,详见链接。
package.json的加载与解析。
require 的源码:在 Node 的 lib/module.js 文件。
模块的加载实质上就是,
- 注入exports、require、module三个全局变量,
- 然后执行模块的源码,
- 然后将模块的 exports 变量的值输出。
require执行逻辑:
当 Node 遇到 require(X) 时,按下面的顺序处理。
(1)如果 X 是内置模块(比如 require('http'))
a. 返回该模块。
b. 不再继续执行。
(2)如果 X 以 "./" 或者 "/" 或者 "../" 开头
a. 根据 X 所在的父模块,确定 X 的绝对路径。
b. 将 X 当成文件,依次查找下面文件,只要其中有一个存在,就返回该文件,不再继续执行。
- X
- X.js
- X.json
- X.node
c. 将 X 当成目录,依次查找下面文件,只要其中有一个存在,就返回该文件,不再继续执行。
- X/package.json(main字段)
- X/index.js
- X/index.json
- X/index.node
(3)如果 X 不带路径
a. 根据 X 所在的父模块,确定 X 可能的安装目录。
b. 依次在每个目录中,将 X 当成文件名或目录名加载。
例子:require('bar')
首先,确定 x 的绝对路径可能是下面这些位置,依次搜索每一个目录。
/home/ry/projects/node_modules/bar /home/ry/node_modules/bar /home/node_modules/bar /node_modules/bar
搜索时,Node 先将 bar 当成文件名,依次尝试加载下面这些文件,只要有一个成功就返回。
bar bar.js bar.json bar.node
如果都不成功,说明 bar 可能是目录名,于是依次尝试加载下面这些文件。
bar/package.json(main字段) bar/index.js bar/index.json bar/index.node
如果在所有目录中,都无法找到 bar 对应的文件或目录,就抛出一个错误。
(4) 抛出 "not found"
以上便是基本的背景和配置内容。