一、简介
在了解node是如何诞生的之前,我们需要先了解进程和线程的概念。
进程和线程是比较抽象的概念,有各种角度的解读,我们从开发者角度,可以这样简单理解,进程和线程是操作系统提供给开发者的,让程序可以并发执行的能力。
进程
应用在运行时候,很可能有并发执行程序的情况。比如对于一个播放器程序,要渲染画面时候同时从网络上下载视频,播放器不会等视频都下载完成再开始渲染,因此渲染和下载是两段不同的程序,是并行在运行的。一个服务端的应用,通常会有多个用户请求访问,那么每个请求过来时候,都要对其进行处理,应用不会等前一个程序处理完了再处理后面的,所以每个请求的处理都应该是并行执行的,并且相互之间不干扰。
一段程序在工作时候(例如客户端应用的下载、渲染和服务端应用对请求的处理都是一段程序),会用到CPU、内存等资源。操作系统提供给开发者让应用可以同时做多个事情的载体就是进程,开发者可以创建一个进程,系统会分配给这个进程一定的资源,并执行开发者指定的任务。
对于上面的情况,播放器需要创建两个进程,一个进程执行渲染,一个进程执行网络下载;服务端应用要在每个请求来到时候创建一个进程并服务这个连接。
线程
如果一个进程中有多个任务需要进行,这些只能是任务是串行执行的,而这些子任务可能之间并没有依赖,是可以并行执行的,为了进一步提升实时性,一个进程还可以创建多个线程,每个线程去执行一个子任务。每个线程共享这个进程的代码和数据空间。
线程的切换开销远远小于进程切换。
注意同一个CPU在执行进程和线程时候,同一时间都只执行一个进程,一个线程。
了解了进程的概念后,再了解下服务端程序都做了什么工作。
简单的说服务端程序接收到一个请求后会和客户端建立一个连接,然后根据请求的内容对数据库进行读/写操作,然后把结果返回给客户端。
客户端⇌服务器⇌数据库
服务端处理并发请求是必要的能力,传统的服务器(如Apache)对每个连接都创建一个进程,这样多个连接就可以并行处理了。
但是传统服务器有个问题,虽然不同的进程是并行的,但同一个进程的执行都是串行的,比如处理一个连接时候,如果数据库处理时间比较慢,那服务端应用因为需要等数据库返回才能将结果返回给客户端,因此这段等待时间是很大的损耗。由于传统服务需要等数据库返回才能将结果返回给客户端,这种服务端实现方式称为“阻塞式”。
另外进程创建、销毁、切换也会有性能损耗。
为了解决阻塞式的服务端带来的性能损耗,高性能服务专家Ryan Dahl在2009选择用JavaScript来实现服务器。
为什么选择JavaScript?我们知道JavaScript运行在浏览器,JavaScript在发出请求后不会阻塞住等待请求返回,而是会先执行后面代码,等请求结果返回了再异步处理结果,因此JavaScript是非阻塞的。
JavaScript一开始创造出来是运行在浏览器中,如果是多线程的,不同的线程都对DOM进行操作,那么无法确定执行结果,因此JavaScript是单线程的。
JavaScript和渲染是互斥的,即相互阻塞的,在执行JavaScript时候不能渲染,在渲染时候也不能执行JavaScript,这时因为如果JavaScript操作和渲染同时进行,那么JavaScript获取到的界面样式可能和实际的不一致。
由于JavaScript阻塞渲染,为了不让用户界面产生卡顿的效果,JavaScript支持异步操作,比如发送网络请求时候,在等待网络请求返回的过程中,JavaScript不会阻塞住,而是会继续向下执行,等结果返回再处理返回结果。
JavaScript的这种异步特性是通过事件队列(event loop)实现的。
综上,JavaScript只要在一个进程内就可以实现并发请求,没有了进程损耗,并且因为是非阻塞的,因此性能更高。
简单地说,nodejs初衷是用javascript实现高性能服务端应用。由于非阻塞和单线程,所以性能更好。
Nodejs怎么实现服务端应用呢?
Nodejs调用操作系统提供的文件、网络等能力让JavaScript可以实现服务端应用。
JavaScript ← Node ← 操作系统。
而当JavaScript运行浏览器中时候,浏览器调用了操作系统渲染、网络等能力,让JavaScript可以控制界面(通过DOM API),也能发送网络请求(ajax)。
JavaScript ← 浏览器 ← 操作系统。
JavaScript运行的环境(浏览器/nodejs)称为“宿主环境”,我们看到JavaScript运行在不同环境中时候,语法没有变,不同的是API和能力。
二、应用场景
Nodejs除了可以用来实现服务端应用,由于Nodejs可以调用系统API,因此可以做很多事情,比如可以用在打包构建(webpack就是运行在node环境的工具)、客户端(electron)。
- web服务器
- 工程化(如打包构建、发布过程)
- PC客户端(electron)
三、学习内容
学习JavaScript实现网页应用,除了js语法本身,其实就是学习浏览器API,如何使用js控制渲染、发送请求,来实现一个网页端应用。当然也需要学习封装层次更高的一些框架。
学习Nodejs也是学习它提供给JavaScript的API以及如何使用这些API实现一个服务端应用。
Nodejs有很多内置模块,提供了很多有用的API。
对于初学者,Nodejs的API曲线比较陡峭,可以先从一些工具和框架入手,如webpack、express、koa、egg、hapi。
Node API
- 多进程,为了利用多核能力,nodejs提供了多进程能力,通常创建进程个数和CPU核数一致。由于JavaScript是单线程的,因此在单CPU上面启动多进程并不能有效提升并发能力(实际上,一个CPU核心同一时刻,只能执行一个线程,一旦线程的数量超过了CPU核心数,再增加线程数目,由于存在线程切换损耗,只会让程序变得更慢,而不是更快。)
- 文件,文件读写能力,通过fs模块实现
- 网络,通过http模块可以创建一个web server。
- 数据结构(buffer(二进制)、stream(I/O处理))服务端会更多涉及二进制数据(像I/O、网络数据)和流数据。需要通过相应地数据结构处理。
- events.EventEmitter 事件触发与事件监听器功能的封装。
框架
- 常见web server框架,express、koa、eggjs、nestjs、midway、hapi等。
- PC客户端开发框架:electron。
四、Node执行JavaScript脚本
如何执行在Node中执行JavaScript代码?
REPL
Node自带交互式解释器。使用它可以很好地调试JavaScript代码。
Node脚本
执行Nodejs脚本
执行node <脚本文件>就会启动一个进程运行该脚本。
比如我们创建一个test.js文件,并写下代码
// test.js
console.log('test node');
然后打开控制台,在该文件的目录中输入命令
node test.js
可以在控制台看到打印了”test node”字符串。
模块化
在Nodejs中每个文件是一个模块,通过commonjs规范导出/引入模块。
比如我们现在目录中有两个文件
.
├── log.js
└── test.js
// test.js
const log = require('./log');
log.log('test commonjs');
// log.js
module.exports = {
log: (...args) => console.log(...args)
};
在控制台中输入
node test.js
可以在控制台看到打印了”test commonjs”字符串。
虽然执行test.js之后,test.js通过require引入了其他模块,但是这些模块都是运行在同一个进程内的。
常见全局变量
process
脚本及依赖的子模块中(它们都在同一个进程中),可以获取process对象,代表进程对象,可以获取脚本参数。
process一个很常用的属性是argv,它用于获取执行脚本时候的参数。
执行脚本时候输入命令 node [ script.js ][arguments] 其中arguments是用空格分隔的字符串。
process.argv是一个数组,它的第一个元素是Node所在的路径,第二个是运行的脚本所在的路径,从第三个往后都是上面的arguments。
看下面的例子:
控制台输入命令:
node test.js hello world a=b c
脚本中获取参数
console.log(process.argv);
打印结果为
[
'/usr/local/bin/node',
'/Users/liuxin/temp/demo-node/test.js',
'hello',
'world',
'a=b',
'c'
]
__dirname
这个变量是当前文件的绝对路径。在项目中有多级目录时候,这个变量会比较有用。
五、常用内置模块
path
Node.js path 模块提供了一些用于处理文件路径的小工具
path.resolve是用来解析路径,得到绝对路径,默认以执行命令所在的路径为当前路径。
比如在路径”/Users/liuxin/temp/demo-node/“有如下目录结构
.
├── test.js
└── util
└── log.js
其中js文件中代码如下
// test.js
const path = require('path');
// 执行命令时候所在的目录
console.log(path.resolve('./'));
// 相对当前脚本的目录
console.log(path.resolve(__dirname, './util/log.js'));
// log.js
const path = require('path');
// 执行命令时候所在的目录
console.log(path.resolve('./'));
// 相对当前脚本的目录
console.log(path.resolve(__dirname, '../test.js'));
在”/Users/liuxin/temp/demo-node/“路径下,输入命令
node test.js
打印结果为
/Users/liuxin/temp/demo-node
/Users/liuxin/temp/demo-node/util/log.js
输入命令
node util/log.js
打印结果
/Users/liuxin/temp/demo-node
/Users/liuxin/temp/demo-node/test.js
path还有很多用来处理路径的方法。
fs
fs是文件处理模块,最常用的是读方法和写方法,读方法和写方法都有同步版本和异步版本。
同步读取/写入会阻塞js执行,但代码书写更简洁。异步读取/写入不会阻塞js执行,代码更复杂一些。
读方法:
var fs = require("fs");
// 异步读取
fs.readFile('input.txt', function (err, data) {
if (err) {
return console.error(err);
}
console.log("异步读取: " + data.toString());
});
// 同步读取
var data = fs.readFileSync('input.txt');
console.log("同步读取: " + data.toString());
console.log("程序执行完毕。");
写方法:
var fs = require("fs");
// 异步写入
console.log('准备写入');
fs.writeFile('input.txt', 'test', function(err) {
if (err) {
return console.error(err);
}
console.log("写入成功!");
});
// 同步写入
console.log('准备写入');
fs.writeFileSync('input.txt, 'test');
console.log("写入成功!");
http
http模块的使用
http是网络模块,可以用来快速创建一个http web server。
var http = require('http');
// 创建服务器
http.createServer(function (request, response) {
// 设置响应
response.write('Hello, Node');
// 发送响应数据
response.end();
}).listen(8080);
// 控制台会输出以下信息
console.log('Server running at http://127.0.0.1:8080/');
执行这个脚本,就可以访问http://127.0.0.1:8080/了。
express
Express 是一个简洁而灵活的 node.js Web应用框架, 提供了一系列强大特性帮助你创建各种 Web 应用,和丰富的 HTTP 工具。 使用 Express 可以快速地搭建一个完整功能的网站。安装
npm install express
使用express实现一个web server:
// express-demo.js
var express = require('express');
var app = express();
app.get('/', function (req, res) {
res.send('Hello, Node');
});
除了express还有比较流行的koa框架,它们都是基于node的http模块封装的上层框架,可以让开发者根据请求返回响应的代码更简洁。
实际的大型项目中,一般会选择企业级的框架,如Egg.js、Nest.js、Midway、hapi。这些框架对代码组织做了一些约束,虽不如express和koa简洁灵活,但在开发大型项目时候拥有更好的可维护性。
六、npm
npm 最初它只是被称为 Node Package Manager,用来作为Node.js的包管理器。但是随着其它构建工具(webpack、browserify)的发展,npm已经变成了 “the package manager for JavaScript”,它用来安装、管理和分享JavaScript包,同时会自动处理多个包之间的依赖。
为什么需要npm
同一个平台(比如node、网页应用、安卓、ios)很多应用都有相同的通用模块,比如组件库、一些工具方法等等。我们开发应用时候不需要重复轮子,使用其他人开发好的模块就可以。我们可以从github上下载他人的模块然后放到自己的项目中。这样做有些问题:
- 由于引用了很多第三方模块,项目会比较臃肿
- 当第三方模块升级版本时候,会比较麻烦
这时候就需要用到第三方包管理器。管理器可以支持:
- 记录项目依赖的第三方包
- 安装指定版本第三方包(将第三方包引入项目)
- 卸载包
- 发布包,提供给其他人使用
使用包管理器,不需要把第三方包完全放入项目中,只需要记录依赖的包的版本和地址即可,只是在使用时候(开发调试及打包时候)将第三方包拉取到本地就可以。项目发布到git时候只要带着依赖的记录即可。
如何使用npm
安装node会自带npm工具。
初始化npm项目
npm init [-y]
执行命令后将会初始化项目,生成package.json。
package.json
是npm对项目进行依赖包管理的配置文件,其中有几个重要字段。
name,description,author是一些基础信息。
version是当前的项目的版本。
dependencies、devDependencies是依赖的第三方包信息。
scripts是定义的脚本命令。
安装和卸载
安装包到项目中
npm install <包名>[@version]
安装后会将第三方包下载到node_modules目录中,并在dependencies记录。
npm install <包名> —save会将在dependencies记录,—save可以省略。
npm install <包名> —save-dev,会记录在devDependencies中。
安装所有依赖
npm install会安装所有依赖(包括dependencies和devDependencies中的依赖包)。
npm install —production只会安装dependencies的包。
全局安装
npm install <包名> -g
例如npm install create-react-app -g
卸载
npm uninstall <包名>
npm scripts
npm支持在package.json中使用scripts字段定义脚本命令。
它有以下优点:
- 运行脚本更简洁。
- 项目的相关脚本,可以集中在一个地方。
- 不同项目的脚本命令,只要功能相同,就可以有同样的对外接口。用户不需要知道怎么测试你的项目,只要运行npm run test即可。
- 可以利用 npm 提供的很多辅助功能。
发布
npm install下载的包是从指定的第三方包远程仓库中下载的,默认是https://registry.npmjs.org/
我们自己也可以开发通用的模块,并使用npm发布到远程仓库供他人使用。
- 初始化npm项目
- npm注册:官网注册https://www.npmjs.com/
- npm login登录
- npm publish
发布的包可以在官网看到,并可以在https://registry.npmjs.org/中下载
nrm的使用
nrm是远程仓库管理器。除了官网的远程仓库,还有一些镜像源,不同的源的包都一致,只是地址不同。因为官网的包仓库地址在国外,因此有时候访问会比较慢,所以可以使用国内的镜像源。
如何指定npm的仓库地址呢?可以使用nrm这个工具。
安装 npm install nrm -g
使用 nrm ls列出当前的镜像
nrm add <镜像别名> <镜像地址>
nrm use <镜像名>
官网
发布的第三方包都可以在官网 npmjs.com查到,并且可以看到安装和使用的方法,还能看到项目的github地址和第三方包的文档主页(如果有的话)。非常方便让我学习第三方的工具使用。