一、简介

在了解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文件,并写下代码

  1. // test.js
  2. console.log('test node');

然后打开控制台,在该文件的目录中输入命令

  1. node test.js

可以在控制台看到打印了”test node”字符串。

模块化

在Nodejs中每个文件是一个模块,通过commonjs规范导出/引入模块。

比如我们现在目录中有两个文件

  1. .
  2. ├── log.js
  3. └── test.js
  1. // test.js
  2. const log = require('./log');
  3. log.log('test commonjs');
  4. // log.js
  5. module.exports = {
  6. log: (...args) => console.log(...args)
  7. };

在控制台中输入

  1. node test.js

可以在控制台看到打印了”test commonjs”字符串。

虽然执行test.js之后,test.js通过require引入了其他模块,但是这些模块都是运行在同一个进程内的。

常见全局变量

process

脚本及依赖的子模块中(它们都在同一个进程中),可以获取process对象,代表进程对象,可以获取脚本参数。

process一个很常用的属性是argv,它用于获取执行脚本时候的参数。

执行脚本时候输入命令 node [ script.js ][arguments] 其中arguments是用空格分隔的字符串。

process.argv是一个数组,它的第一个元素是Node所在的路径,第二个是运行的脚本所在的路径,从第三个往后都是上面的arguments。

看下面的例子:

控制台输入命令:

  1. node test.js hello world a=b c

脚本中获取参数

  1. console.log(process.argv);

打印结果为

  1. [
  2. '/usr/local/bin/node',
  3. '/Users/liuxin/temp/demo-node/test.js',
  4. 'hello',
  5. 'world',
  6. 'a=b',
  7. 'c'
  8. ]

__dirname

这个变量是当前文件的绝对路径。在项目中有多级目录时候,这个变量会比较有用。

五、常用内置模块

path

Node.js path 模块提供了一些用于处理文件路径的小工具

path.resolve是用来解析路径,得到绝对路径,默认以执行命令所在的路径为当前路径。

比如在路径”/Users/liuxin/temp/demo-node/“有如下目录结构

  1. .
  2. ├── test.js
  3. └── util
  4. └── log.js

其中js文件中代码如下

  1. // test.js
  2. const path = require('path');
  3. // 执行命令时候所在的目录
  4. console.log(path.resolve('./'));
  5. // 相对当前脚本的目录
  6. console.log(path.resolve(__dirname, './util/log.js'));
  7. // log.js
  8. const path = require('path');
  9. // 执行命令时候所在的目录
  10. console.log(path.resolve('./'));
  11. // 相对当前脚本的目录
  12. console.log(path.resolve(__dirname, '../test.js'));

在”/Users/liuxin/temp/demo-node/“路径下,输入命令

  1. node test.js

打印结果为

  1. /Users/liuxin/temp/demo-node
  2. /Users/liuxin/temp/demo-node/util/log.js

输入命令

  1. node util/log.js

打印结果

  1. /Users/liuxin/temp/demo-node
  2. /Users/liuxin/temp/demo-node/test.js

path还有很多用来处理路径的方法。

Node.js Path模块

fs

fs是文件处理模块,最常用的是读方法和写方法,读方法和写方法都有同步版本和异步版本。

同步读取/写入会阻塞js执行,但代码书写更简洁。异步读取/写入不会阻塞js执行,代码更复杂一些。

读方法:

  1. var fs = require("fs");
  2. // 异步读取
  3. fs.readFile('input.txt', function (err, data) {
  4. if (err) {
  5. return console.error(err);
  6. }
  7. console.log("异步读取: " + data.toString());
  8. });
  9. // 同步读取
  10. var data = fs.readFileSync('input.txt');
  11. console.log("同步读取: " + data.toString());
  12. console.log("程序执行完毕。");

写方法:

  1. var fs = require("fs");
  2. // 异步写入
  3. console.log('准备写入');
  4. fs.writeFile('input.txt', 'test', function(err) {
  5. if (err) {
  6. return console.error(err);
  7. }
  8. console.log("写入成功!");
  9. });
  10. // 同步写入
  11. console.log('准备写入');
  12. fs.writeFileSync('input.txt, 'test');
  13. console.log("写入成功!");

http

http模块的使用

http是网络模块,可以用来快速创建一个http web server。

  1. var http = require('http');
  2. // 创建服务器
  3. http.createServer(function (request, response) {
  4. // 设置响应
  5. response.write('Hello, Node');
  6. // 发送响应数据
  7. response.end();
  8. }).listen(8080);
  9. // 控制台会输出以下信息
  10. 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 可以快速地搭建一个完整功能的网站。
安装
  1. npm install express

使用express实现一个web server:

  1. // express-demo.js
  2. var express = require('express');
  3. var app = express();
  4. app.get('/', function (req, res) {
  5. res.send('Hello, Node');
  6. });

除了express还有比较流行的koa框架,它们都是基于node的http模块封装的上层框架,可以让开发者根据请求返回响应的代码更简洁。

实际的大型项目中,一般会选择企业级的框架,如Egg.jsNest.jsMidwayhapi。这些框架对代码组织做了一些约束,虽不如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发布到远程仓库供他人使用。

  1. 初始化npm项目
  2. npm注册:官网注册https://www.npmjs.com/
  3. npm login登录
  4. npm publish

发布的包可以在官网看到,并可以在https://registry.npmjs.org/中下载

nrm的使用

nrm是远程仓库管理器。除了官网的远程仓库,还有一些镜像源,不同的源的包都一致,只是地址不同。因为官网的包仓库地址在国外,因此有时候访问会比较慢,所以可以使用国内的镜像源。

如何指定npm的仓库地址呢?可以使用nrm这个工具。

安装 npm install nrm -g

使用 nrm ls列出当前的镜像

nrm add <镜像别名> <镜像地址>

nrm use <镜像名>

官网

发布的第三方包都可以在官网 npmjs.com查到,并且可以看到安装和使用的方法,还能看到项目的github地址和第三方包的文档主页(如果有的话)。非常方便让我学习第三方的工具使用。