node.js入门笔记

基本名词概念

同步:
当一个同步调用发出后,调用者要一直等待返回消息(或者调用结果)通知后,才能进行后续的执行

异步:
当一个异步过程调用发出后,调用者不能立刻得到返回消息(结果)。在调用结束之后,通过消息回调来通知调用者是否调用成功。

阻塞:
阻塞调用是指调用结果返回之前,当前线程会被挂起,一直处于等待消息通知,不能够执行其他业务,函数只有在得到结果之后才会返回。

非阻塞:
非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。

同步和异步指的是线程之间的关系,阻塞和非阻塞指的是同一线程的状态

I/O:
一般指通过网络或存储介质读取或写入数据。

回调:
指函数在运行完成后的执行部分,用于异步的实现

路由:
URL到函数的映射。接收 HTTP 请求中的参数,然后决定要执行的代码

中间件:
请求与处理的中间过程,比如解析json请求为对象

基础知识

路由

路由直接定义:

1
2
3
4
//app.all()用于响应任何http方法
app.all('/secret', (req, res, next) => { //req指http请求(对象),res指http响应(对象),
console.log('request for secret'); //next提供了next()方法,用于跳过该定义中余下的函数
});
1
2
3
4
//app.get()接收GET方法的http访问
app.get('/', (req, res) => {
res.send('Hello World!');
});

路由外部导入:

1
2
3
//app.use()用于主文件导入路由
const router = require('./router.js');
app.use('/test', router);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//路由文件导出
const express = require('express');
const router = express.Router();

router.get('/', (req, res) => {
res.send('首页'); //映射到完整url应为/test/
});

router.get('/about', (req, res) => {
res.send('关于'); //映射到完整url应为/test/about
});

module.exports = router; //将router对象暴露在模块之外

exports.area = width => { return width * width; }; //将area方法暴露在模块之外,返回给定参数的平方

事件 / 事件监听(EventEmitter)

Node.js 所有的异步 I/O 操作在完成时都会发送一个事件到事件队列。

Node.js 里面的许多对象都会分发事件:一个 net.Server 对象会在每次有新连接时触发一个事件, 一个 fs.readStream 对象会在文件被打开的时候触发一个事件。 所有这些产生事件的对象都是 events.EventEmitter 的实例。

导入 events.Emitter

1
2
3
4
// 引入 events 模块
var events = require('events');
// 创建 eventEmitter 对象
var eventEmitter = new events.EventEmitter();

开启事件监听

1
2
3
event.on('some_event', function() { 
console.log('some_event 事件触发');
});

触发事件监听

1
event.emit('some_event'); 

req 和 res 对象的常用属性和方法

req常用属性

1
2
3
4
5
6
7
8
9
10
req.ip		//请求来源的ip
req.method //返回请求使用的http方法
req.body //body-parser提供的解析,获取并解析POST请求中的json
req.originalUrl //GET /search?q=something console.dir(req.originalUrl)输出'/search?q=something'
req.params
//GET或POST提交的参数列表,优先级1.url中路由参数2.post请求中提交参数3.get请求中的参数
//例如http://127.0.0.1/user/gtg?id=456中,后端定义路由使用路径为'/user/:id',获取到的id键值为gtg
req.query //GET参数列表
req.headers //调用http请求头
req.on //绑定指定事件

res常用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
res.append()		//添加指定内容的响应头,如res.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly');
res.attachment() //将指定文件作为附件发送,如res.attachment('path/to/default.png');
res.cookie() //按照所给参数发送Set-Cookie
res.clearcookie() //清除指定cookie
res.end() //结束响应过程
res.json() //发送json响应
res.render() //用于呈现视图,并将呈现的HTML字符串发送给客户端。必要参数为渲染模板
res.send() //发送http响应
res.sendStatus() //用于将响应HTTP状态代码设置为statusCode并将其字符串表示形式发送为响应主体
res.status() //仅设置状态码
res.set() //此方法用于将响应的HTTP标头字段设置为value。
res.type() //设置响应的Content-type
res.cookie() //设置响应的cookie

详细参数介绍见Node.js res 对象 - 蝴蝶教程 (jc2182.com)

参考node js中的req.body,req.query,req.params取参数 - Diamond蚊子 - 博客园 (cnblogs.com)

中间件(middleware)

[MDN web doc](Express/Node 入门 - 学习 Web 开发 | MDN (mozilla.org)): 可以使用 app.use()app.add() 将一个中间件函数添加至处理链中,这取决于中间件是应用于所有响应的,还是应用于特定 HTTP 动词(GETPOST等)响应的。可以为两种情况指定相同的路由,但在调用 app.use() 时路由可以省略。

调用中间件

1
2
3
example:
app.use(logger('dev')); //用use()应用于所有响应
app.add('/someroute', a_middleware_function); // 用add()为一个特定的路由添加该函数

错误处理中间件

1
2
3
4
app.use((err, req, res, next) => {		//错误处理的中间件有err, req, res, next四个参数
console.error(err.stack);
res.status(500).send('出错了!');
});

内置中间件和三方中间件

内置中间件(Built-in middleware)指导入模块时自带的中间件

比如express提供了express.static express.jsonexpress.urlencoded 两个中间件,其中express.static用于托管静态html或图像之类的文件内容,express.json用于解析json请求,express用于解析url编码的内容,后两者在express 5.14后添加

三方中间件(Third-party middleware)指第三方编写的中间件

三方中间件需要与主要模块兼容,比如下面的cookie-parser

1
2
3
4
5
6
var express = require('express')
var app = express()
var cookieParser = require('cookie-parser')

// load the cookie-parsing middleware
app.use(cookieParser('key123')) //参数用于设置cookie密钥

模板渲染

设置模板

1
2
3
4
5
// 设置包含模板的文件夹('views')
app.set('views', path.join(__dirname, 'views'));

// 设置视图引擎,比如'some_template_engine_name'
app.set('view engine', 'some_template_engine_name');

使用模板

1
2
3
app.get('/', (req, res) => {
res.render('index', { title: '关于', message: 'test' });
});

文件系统 fs

1
var fs = require("fs")

文件系统(fs 模块)模块中的方法均有异步和同步版本,例如读取文件内容的函数有异步的 fs.readFile() 和同步的 fs.readFileSync()。

异步的方法函数最后一个参数为回调函数,回调函数的第一个参数包含了错误信息(error)。

fs 的常用方法

fs的同步方法名基本都是其异步方法名+Sync

readFile:

1
2
3
4
5
6
fs.readFile('./public/1.html', 'utf8' , (err, data) => {		//异步函数一定要指定回调函数
if (err) {
console.error(err)
return
}
res.send(data) //没有设置渲染模板可以使用readFile读取html文件以快速建站
1
2
3
4
5
6
7
8
9
10
11
其他:
open() //打开文件
close() //关闭文件,与open搭配使用
writeFile() //写入文件
ftruncate() //截取文件的一部分
stat() //获取文件的相关信息,stat提供了多个判断函数如stat.isFile()
unlink() //删除文件
mkdir() //创建目录
readdir() //读取目录
rmdir() //删除目录
rename() //重命名

流Stdin Stdout Stderr

stdout,stdin, 和stderr 是标准的流,当程序执行时,它们在程序和环境之间互连输入和输出通信通道。

每个进程初始化时都有三个开放的文件描述符,分别称为stdinstdoutstderr,也被称作标准流 。

为一个进程启动了一组三个标准流,我们可以通过Node.js中的process 对象访问它们。

  • stdin: 输入流
  • stdout: 输出流
  • stderr: 错误流

控制台与自己初始事件的标准流相绑定,比如console.log()就实现了process.stdout.out()

因而想要创建其他进程输出到控制台时可以使用child.stdout.pipe(process.stdout),将输出流绑定到控制台

stderr 是默认的文件描述符 进程可以在其中写入错误信息,以避免管道末端的命令可能无法理解。

除了建立管道之外,还可以通过子进程和当前进程共用同一stdio的方式来实现

1
2
3
4
const { spawn } = require('child_process');
const child = spawn('pwd', {
stdio: 'inherit'
});

stdio选项用于配置父进程和子进程之间建立的管道,由于stdio管道有三个(stdin, stdout, stderr)因此stdio的三个可能的值其实是数组的一种简写

子进程控制

1
2
3
child.stdin.write()		//向子进程输入内容
child.stdout.on('data',function(data){}) //监听子进程输出
child.stderr.on('err',function(err){}) //监听子进程错误

参考

[Difference between console.log and process.stdout.write in NodeJS (tutorialspoint.com)](https://www.tutorialspoint.com/difference-between-console-log-and-process-stdout-write-in-nodejs#:~:text=Both the methods – console.log and process.stdout.write have,difference in the way they execute these tasks.)
在Node.js中使用stdout、stdin和stderr的方法 - 掘金 (juejin.cn)

子进程 child_proccess

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const cp = require('child_process');

// spawn
cp.spawn('node', ['./dir/test1.js'],
{ stdio: 'inherit' }
);
// exec
cp.exec('node ./dir/test1.js', (err, stdout, stderr) => {
console.log(stdout);
});
// execFile
cp.execFile('node', ['./dir/test1.js'],(err, stdout, stderr) => {
console.log(stdout);
});
// fork
cp.fork('./dir/test1.js',
{ silent: false }
);

// ./dir/test1.js
console.log('test1 输出...');

freecodecamp: 单一进程总是会受到负载限制,子进程是实现高效率的一种办法

child_process提供了几种创建子进程的方式

  • 异步方式:spawn、exec、execFile、fork

  • 同步方式:spawnSync、execSync、execFileSync

.exec().execFile().fork()底层都是通过.spawn()实现的。

.exec()execFile()额外提供了回调,当子进程停止的时候执行。

child_process.spawn(command[, args][, options])
child_process.exec(command[, options][, callback])
child_process.execFile(file[, args][, options][, callback])
child_process.fork(modulePath[, args][, options])

exec与execFile

exec

创建一个shell,在其中执行命令,提供stdio回调

1
2
3
4
5
6
7
8
exec('ls -al', function(error, stdout, stderr){
if(error) {
console.error('error: ' + error);
return;
}
console.log('stdout: ' + stdout);
console.log('stderr: ' + typeof stderr);
});

execFile

不创建新的shell,与exec相似

调用python解释器执行文件并输出结构:

1
2
3
4
5
6
7
8
9
10
11
12
const { execFile, execFileSync } = require('child_process');

execFile('C:/xxx/python.exe', ['./example.py'], (err, stdout, stderr) => {
if(err) {
console.log(err);
return;
}
console.log(`stdout: ${stdout}`);
});

const stdout = execFileSync('node', ['-v']);
console.log(stdout);

exec与execFile之间的差异

[nodejs.cn: ](在 Windows 上衍生 .bat 和 .cmd 文件 | Node.js API 文档 (nodejs.cn))child_process.exec()child_process.execFile()之间区别的重要性可能因平台而异。 在 Unix 类型的操作系统(Unix、Linux、macOS)上,child_process.execFile() 可以更高效,因为它默认不衍生 shell。 但是,在 Windows 上,.bat.cmd 文件在没有终端的情况下无法自行执行,因此无法使用 child_process.execFile()启动。

fork

modulePath:子进程运行的模块。

参数说明:

  • execPath: 用来创建子进程的可执行文件,Linux下默认是/usr/local/bin/node。可通过execPath来指定具体的node可执行文件路径。
  • execArgv: 传给可执行文件的字符串的参数列表。默认是process.execArgv,跟父进程保持一致。
  • silent: 默认是false,即子进程的stdio从父进程继承。如果是true,则直接pipe向子进程的child.stdinchild.stdout等。
  • stdio: 如果声明了stdio,则会覆盖silent选项的设置。

例子

1
2
3
4
5
6
7
8
var child_process = require('child_process');
var child = child_process.fork('./SilentChild.js', {
silent: true
});
child.stdout.setEncoding('utf8');
child.stdout.on('data', function(data){
console.log(data);
});

控制台成功可以输出子进程的打印内容

spawn

特点:spawn 与 exec、execFile 不同的是,后两者创建时可以指定 timeout 属性设置超时时间,一旦创建的进程运行超过设定的时间将会被杀死;exec 与 execFile 不同的是,exec 适合执行已有的命令,execFile 适合执行文件;exec、execFile、fork 都是 spawn 的延伸应用,底层都是通过 spawn 实现的;

功能纯粹的方法,用法类似如下

1
2
3
4
5
6
7
8
var ls = spawn('ls', ['-al'], {
stdio: 'inherit'
});

ls.on('close', function(code){
console.log('child exists with code: ' + code);
});

参考Nodejs进阶:如何玩转子进程(child_process) - 程序猿小卡 - 博客园 (cnblogs.com)玩转 node 子进程 — child_process - 掘金 (juejin.cn)

express框架学习

Express 是一个非常轻量的 web 应用框架,这是有意为之的,它巨大的裨益和无尽的潜能都来自第三方的库和功能。

模块导入:

1
2
const express = require('express');		//导入express模块
const app = express(); //调用返回对象创建Web应用
1
2
3
app.listen(3000, () => {		//创建3000端口监听,即开启服务
console.log('listen at port 3000');
});

express快速建站示例,命令行node +文件名运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var express = require('express');
var app = express();

app.get('/', function (req, res) {
res.send('Hello World');
})

var server = app.listen(8081, function () {

var host = server.address().address
var port = server.address().port

console.log("应用实例,访问地址为 http://%s:%s", host, port)

})

express内置中间件

express.static

express.static用来托管静态文件,包括图片、CSS 以及 JavaScript 文件

1
app.use(express.static('public'));

然后public文件夹下的所有文件均可通过url+文件名的方式访问了,比如访问/public/imgaes/default.png的访问方式即为http:ip:port/images/default.png

express.json

这是Express中的一个内置中间件函数。它使用JSON有效载荷解析传入的请求,并且基于body-parser。

返回只解析JSON并且只查看Content-Type头与type选项匹配的请求的中间件。这个解析器接受主体的任何Unicode编码,并支持gzip和deflate编码的自动膨胀(automatic inflation)。

在中间件(例如req.body)之后,一个包含解析数据的新body对象被填充到请求对象中,或者如果没有要解析的body,内容类型不匹配,或者发生了错误,则为一个空对象({})。

express.urlencode

基于body-parser对url编码后的内容解析

express兼容中间件

MDN: Express 是高度包容的。几乎可以将任何兼容的中间件以任意顺序插入到请求处理链中,只要你喜欢。可以用单一文件或多个文件构造应用,怎样的目录结构都可以。有时候你自己都会觉得眼花缭乱!

cookie-parser用于cookie的解析,即从中获取访问者相关认证信息

导入cookie-parser后设置res.cookie配置信息

初始化cookie-parser

1
app.use(cookie-parser('secret'));		//设置加密密钥

设置cookie

1
res.cookie("add",adds,{ signed: true, maxAge: 900000, httpOnly: true});		//由express完成
1
2
3
4
5
6
7
8
9
10
11
res.cookie的配置选项
参数一表示,cookie名称
参数二表示,cookie的值
参数三表示,cookie的配置选项
domain 域名
path 路径
expires 过期时间
maxAge 有效时间(以毫秒为单位)
httpOnly 只能由web服务器访问,不允许前端js获取
secure 是否与https一起使用
signed 是否签名,签名后可以使用signedCookie访问

然后在访问中就可以通过req.cookies.<key>访问该键值,如

1
2
3
4
app.use(function (req, res, next) {
console.log(req.cookies.nick); // chyingp
next();
});

body-parser

解析请求的body部分,提供req.body的直接访问

Reference:

node.js中express使用cookie-parser 和 cookie-session处理会话 - 怀素真 - 博客园 (cnblogs.com)

nodejs-learning-guide/cookie-parser-deep-in.md at master · chyingp/nodejs-learning-guide (github.com)