avatar


4.Node.js

《2.基础语法》《3.DOM和BOM》中,我们说JavaScript,可以被分为三部分:

  1. ECMAScript(JavaScript语法)
  2. DOM(页面文档对象类型)
  3. BOM(浏览器对象模型)

一般语境下的JavaScript确实就是指上述三部分,但其实JavaScript也完全可以在服务端运行。
(例如,Hexo,JavaScript在服务端运行的一个典型例子,关于Hexo,可以参考《基于Hexo的博客搭建》。)

这种在服务端运行的JavaScript,被称为Node.js。

定义

引例

为什么JavaScript可以在浏览器中被执行?
这个问题我们在《2.基础语法》的"浏览器的引擎"部分就讨论过,一个浏览器,通常由两个主要部分组成:渲染引擎和JavaScript引擎。

  • 渲染引擎:用来解析HTML与CSS,俗称内核。
    比如新版本Chrome浏览器的blink,以及旧版本Chrome浏览器的webkit。
  • JavaScript引擎:也称为JavaScript解释器,读取网页中的JavaScript代码,逐行解释每一句JavaScript代码,将其翻译为机器语言,然后由计算机去执行。
    比如Chrome浏览器的V8,Firefox浏览器的OdinMonkey,Safari浏览器的JSCore。其中,Chrome浏览器的V8引擎性能最好。

那么,JavaScript是怎么操作操作DOM和BOM的呢?
首先,浏览器提供了和DOM、BOM相关的API;然后,我们用JavaScript写location.replace('/')这种代码;浏览器才会在解析JavaScript调用相关的API。
即,在浏览器中的JavaScript运行环境如下:

在浏览器中的JavaScript运行环境如下

那么,如果说,内置的API,不仅仅是DOM和BOM,还包括读写文件、提供网络服务,调用操作系统资源在内的诸多API呢?
这就是Node.js。

Node.js

Node.js是一个基于Chrome的V8引擎的JavaScript运行环境。
官网:https://nodejs.org

其运行环境如下:
Node.js

虽然Node.js只是提供了基础的功能和API,但是其拥有非常完善的生态,有诸多的框架和工具。例如:

  • Hexo
    基于Hexo,可以快速构建博客应用。
  • Express
    基于Express,可以快速构建Web应用。
  • Electron
    基于Electron,可以构建跨平台的桌面应用。
  • Robot.js、Auto.js
    基于Robot.js,可以构建自动化工具。

此外,还包括操作数据库、解析HTML、JS混淆等等。
环境,就没有DOM和BOM的API。

环境准备

LTS和Current

正如浏览器需要安装,Node.js的环境也需要安装。
我们会看到两个版本的Node.js。

两个版本的Node.js

  • LTS,长期稳定版,一般推荐安装改版本。
  • Current,新特性版,可能存在隐藏的Bug或安全漏洞。

是不是有一个问题?
有18、有20,19呢?
19作为20的预备版本。在Node.js,是下一个偶数版的预备版本。

Windows

下载node-v18.16.0-x64.msi,然后直接进行安装即可。

MacOS

下载node-v18.16.0.pkg,然后直接进行安装即可。

Linux

Ubuntu

安装命令:apt install nodejs

通过这个命令安装的,不一定是最新的,安装最新版的方法如下:

  1. 升级apt
    1
    sudo apt update && sudo apt upgrade
  2. 删除旧版本
    1
    2
    3
    4
    5
    6
    cd /etc/apt/sources.list.d 
    sudo rm nodesource.list
    sudo apt --fix-broken install
    sudo apt update
    sudo apt remove nodejs
    sudo apt remove nodejs-doc
  3. 添加新版本的源
    1
    curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
  4. 安装新版本
    1
    sudo apt-get install -y nodejs
  5. 在安装过程中,可能有如下告警:
    1
    2
    3
    4
    5
    6
    Unpacking nodejs (18.16.0-deb-1nodesource1) ...
    dpkg: error processing archive /var/cache/apt/archives/nodejs_18.16.0-deb-1nodesource1_amd64.deb (--unpack):
    trying to overwrite '/usr/share/systemtap/tapset/node.stp', which is also in package libnode72:amd64 12.22.9~dfsg-1ubuntu3
    Errors were encountered while processing:
    /var/cache/apt/archives/nodejs_18.16.0-deb-1nodesource1_amd64.deb
    E: Sub-process /usr/bin/dpkg returned an error code (1)
    解决方法:
    1
    sudo dpkg -i --force-overwrite /var/cache/apt/archives/nodejs_18.16.0-deb-1nodesource1_amd64.deb

CentOS

安装命令:yum install nodejs

如果是在CentOS 7系统上,通过这个命令安装的,不是最新的(18版)。

目前18版,不支持CentOS 7。

如果不是CentOS 7,可以通过如下的方法安装

  1. 添加新版本的源
    1
    curl -sL https://rpm.nodesource.com/setup_18.x | sudo bash -
    注意,这里的地址是rpm.nodesource
  2. 安装
    1
    yum install nodejs

18版本的要求

18版要求的系统版本:

  • CentOS,7以上
  • RHEL,7以上
  • Ubuntu,18.04以上
  • MacOS,10.15及以上(包括10.15)
  • Windows 10及以上

虽然网上能找到一些在不符合(较低版本)的操作系统上安装18版的方法;但我本人在实践过程中,发现不可行;而且不建议在不符合的操作系统上安装18版。

版本覆盖

在某些情况下,我们可能会有多版本的场景,而且经常需要切换node的版本,关于这个可以参考用n模型或者nvm模块进行管理。
这里我们不做太多讨论,我们讨论一般不切换node版本,版本覆盖的方式。

我们以12.22.12为例。

下载地址

找到一些大版本中的最后一个小版本。
通过https://nodejs.org/en/download/releases,可以找到大版本的最后一个小版本。
通过https://nodejs.org/download/release/,可以找到所有的历史版本。

Windows

先卸载旧版本,再安装新版本。

MacOS

直接下载旧版本,覆盖安装。

Linux

Ubuntu

通过=【版本号】的方式,指定版本,覆盖安装。

1
apt install nodejs=12.22.12

如果提示找不到12.22.12,可能需要我们手动添加源

1
curl -fsSL https://deb.nodesource.com/setup_12.x | sudo -E bash -

CentOS

  1. 卸载旧版本
    1
    yum remove nodejs
  2. 清除 yum rpm 源
    1
    2
    cd /etc/yum.repos.d
    rm -rf nodesource-el7.repo
  3. 清除 yum rpm 源缓存
    1
    2
    yum clean all
    rm -rf /var/cache/yum
  4. 添加12到源
    1
    curl --silent --location https://rpm.nodesource.com/setup_12.x | bash -
  5. 安装
    1
    yum install -y nodejs

怎么运行

  1. 可以使用node 文件名.js运行。
  2. 可以在VS Code直接运行。
  3. 可以通过Code Runner运行。

对于第一种方法,没什么好讨论的。
我们讨论第二种和第三种方法。

可以在VS Code直接运行

在如图所示之处,运行。
VS Code 1

可以提前停止运行等。
VS Code 2

可以通过Code Runner运行

下载Code Runner

Code Runner 1

右键,点击运行。

Code Runner 2

可以提前停止运行等。
Code Runner 3

模块

分类

Node.js中根据模块来源的不同,将模块分为了三大类,分别是:

  1. 内置模块
    由Node.js官方提供,在Node.js中内置的。
  2. 自定义模块
    用户创建的每个.js文件,就是自定义模块。
  3. 第三方模块
    由第三方开发出来的模块,使用前需要先下载。

其实,不仅仅Node.js如此,对于Java、Python等语言中的"包",也可以分为上述三大类。

加载

require()方法

使用require()方法,可以加载模块。

  1. 内置模块
    1
    require(【内置模块的名称】)
  2. 加载用户自定义模块
    1
    require(【路径】)
  3. 加载第三方模块
    1
    require(【第三方模块的名称】)

示例代码:

1
2
3
4
5
6
7
8
// 加载内置模块
const http = require('http');

// 加载用户自定义模块
const custom = require('./custom.js')

// 加载第三方模块
const custom = require('moment')

被加载会被执行

使用require()方法加载其它模块时,会执行被加载模块中的代码。
我们举一个例子。

假设存在1.js,示例代码:

1
console.log('1.js')

我们在2.js中加载./1.js,示例代码:

1
const m1 = require('./1.js')

执行2.js,运行结果:

1
1.js

执行了1.js中的console.log('1.js'),即被加载的模块中的代码被执行了。

关于模块加载过程,在"模块加载机制"这部分,有更多讨论。

作用域

模块级别的访问限制

在自定义模块中定义的变量,方法等成员,只能在当前模块内被访问,这种模块级别的访问限制,叫做模块作用域。

1.js,示例代码:

1
2
3
4
5
const username = 'Kaka'

function printName(){
console.log(username)
}

2.js,示例代码:

1
2
3
4
5
const m1 = require('./1.js')

console.log(m1.username)

m1.printName()

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
undefined
/Users/kaka/Documents/ns/2.js:5
m1.printName()
^

TypeError: m1.printName is not a function
at Object.<anonymous> (/Users/kaka/Documents/ns/2.js:5:4)
at Module._compile (internal/modules/cjs/loader.js:999:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)
at Module.load (internal/modules/cjs/loader.js:863:32)
at Function.Module._load (internal/modules/cjs/loader.js:708:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)
at internal/main/run_main_module.js:17:47

全局变量污染

这么设计的好处是,防止了变量污染的问题。
什么是全局变量污染?

假设有一个HTML,内容如下:

1
2
3
4
5
6
7
8
9
10
<html>
<script>
var v = 'v3'
</script>
<script src="./v1.js"></script>
<script src="./v2.js"></script>
<script>
console.log(v)
</script>
</html>

v1.js的内容为:

1
var v = 'v1'

v2.js的内容为:

1
var v = 'v2'

我们执行,会发现打印的是v2

全局变量污染

如果声明变量的时候,不用var,用let,则不会有全局变量污染问题。

全局变量污染-let

关于let,我们在《基于JavaScript的前端开发入门:2.基础语法》的"ES6的新特性"部分有过讨论。

外部使用

module对象

在每个.js模块中都有一个module对象,存储了和当前模块有关的信息.

我们可以新建一个.js文件,然后打印module对象看看。示例代码:

1
console.log(module)

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Module {
id: '.',
path: '/Users/kaka/Documents/ns',
exports: {},
parent: null,
filename: '/Users/kaka/Documents/ns/mtest2.js',
loaded: false,
children: [],
paths: [
'/Users/kaka/Documents/ns/node_modules',
'/Users/kaka/Documents/node_modules',
'/Users/kaka/node_modules',
'/Users/node_modules',
'/node_modules'
]
}

module.exports

可以使用module.exports对象,将模块内的成员暴露出去,供外部使用。
外部用require()方法导入自定义模块时,得到的是module.exports所指向的对象。

新建1.js,通过module.exports进行暴露。示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
const username = 'Kaka'

function printName(){
console.log(username)
}

module.exports = {
username: '卡卡',
printName(){
console.log(username)
}
}

2.jsrequire('./1.js'),并打印内容,调用方法。示例代码:

1
2
3
4
5
const m1 = require('./1.js')

console.log(m1.username)

m1.printName()
运行结果:
1
2
卡卡
Kaka

如果我们希望printName()方法打印的是module.exports中的username呢?
那么,得指明。

1.js中的module.exportsprintName()指明。示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
const username = 'Kaka'

function printName(){
console.log(username)
}

module.exports = {
username: '卡卡',
printName(){
console.log(module.exports.username)
}
}

执行2.js,示例代码:

1
2
3
const m1 = require('./1.js')

m1.printName()
运行结果:
1
卡卡

如果我们想在2.jsm1.username,指向的是m1模块自身的username呢?
那么,需要在1.jsmodule.exports中指明,module.exports.username=username

1.jsmodule.exports中指明,module.exports.username=username。示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
const username = 'Kaka'

function printName(){
console.log(username)
}

module.exports = {
username: username,
printName(){
console.log(module.exports.username)
}
}

执行2.js,示例代码:

1
2
3
const m1 = require('./1.js')

console.log(m1.username)
运行结果:
1
Kaka

“exports”

Node.js还提供了另一种写法,exports
exports的作用是什么?
是另一种写法,可以少写一个单词,和module.exports指向的是同一个内存区域。

exportsmodule都是node在执行js文件的时候生成的变量。

module.exports

1.js,示例代码:

1
2
3
4
5
6
7
const username = 'zs'

exports.username = username
exports.age = 20
exports.sayHello = function(){
console.log('hello')
}

2.js,示例代码:

1
2
3
4
const m1 = require('./1.js')

console.log(m1)
m1.sayHello()

运行结果:

1
2
{ username: 'zs', age: 20, sayHello: [Function] }
hello

同时使用的现象

接下来,我们来看一个有趣的现象。
exportsmodule.exports,同时使用。

1.js,示例代码:

1
2
3
4
5
6
exports.username = 'zs'

module.exports = {
gender: '男',
age: 22
}

2.js,示例代码:

1
2
3
4
5
const m1 = require('./1.js')

console.log(m1.username)
console.log(m1.gender)
console.log(m1.age)
运行结果:
1
2
3
undefined

22

解释说明:

  1. 一开始,exportsmodule.exports,指向同一块内存区域。
  2. exports.username = 'zs'
    此时,会申请一块新的内存区域,username = 'zs'
  3. module.exports = {gender: '男',age: 22}
    再申请一块新的内存区域猫,module.exports指向新的内存区域。
  4. 执行2.js,会根据module.exports所指向的内存区域。

同时使用-1

1.js,示例代码:

1
2
3
4
5
module.exports.username = 'zs'
exports = {
gender: '男',
age: 22
}

2.js,示例代码:

1
2
3
4
5
const m1 = require('./1.js')

console.log(m1.username)
console.log(m1.gender)
console.log(m1.age)
运行结果:
1
2
3
zs
undefined
undefined

解释说明:

  1. 一开始,exportsmodule.exports,指向同一块内存区域。
  2. module.exports.username = 'zs'
    在内存区域存储username = 'zs'
  3. exports = {gender: '男',age: 22}
    再申请一块内存区域,exports指向该区域。
  4. 执行2.js,会根据module.exports所指向的内存区域。

同时使用-2

1.js,示例代码:

1
2
exports.username = 'zs'
module.exports.gender = '男'

2.js,示例代码:

1
2
3
4
const m1 = require('./1.js')

console.log(m1.username)
console.log(m1.gender)
运行结果:
1
2
3
zs

22

分析过程略,内存结构如图

同时使用-3

1.js,示例代码:

1
2
3
4
5
6
7
exports = {
username:'zs',
gender:'男'
}

module.exports = exports
module.exports.age = 22

2.js,示例代码:

1
2
3
4
5
const m1 = require('./1.js')

console.log(m1.username)
console.log(m1.gender)
console.log(m1.age)
运行结果:
1
2
3
zs

22

解释说明:

  1. 一开始,exportsmodule.exports,指向同一块内存区域。
  2. exports = {username:'zs',gender:'男'}
    申请一块新的内存区域,exports指向该区域。
  3. module.exports = exports
    module.exports指向exports所指向的区域。
  4. module.exports.age = 22
    在内存区域存储age = 22
  5. 执行2.js,会根据module.exports所指向的内存区域。

同时使用-4

建议不要同时使用

为了代码的可读性,不要同时使用exportsmodule.exports

题外话,在《Linux操作系统使用入门:3.Shell脚本》,有一个关键词export(注意,没有s),作用有些类似,把变量提升为全局环境变量,供其他Shell程序。

内置模块

fs

概述

fs,Node.js官方提供的一个内置模块,用来操作文件。

导入模块

导入模块,示例代码:

1
const fs = require('fs')

读文件

读取文件通过readFile方法。

1
fs.readFile(path [, options] callback)
  • path,必填,字符串,文件路径。
  • options,可选,编码格式,默认是二进制字节,可以指定为utf8,表示UTF-8编码。
  • callback,必填,文件读取完成后的回调函数。

二进制字节,示例代码:

1
2
3
4
5
6
7
const fs = require('fs')

fs.readFile('/Users/kaka/Desktop/main.py',function(err,data){
console.log(err)
console.log('======')
console.log(data)
})

运行结果:

1
2
3
null
======
<Buffer 66 72 6f 6d 20 73 74 6f 63 6b 5f 65 6e 76 20 69 6d 70 6f 72 74 20 53 74 6f 63 6b 45 6e 76 0a 0a 65 6e 76 20 3d 20 53 74 6f 63 6b 45 6e 76 28 29 0a 65 ... 239 more bytes>

UTF-8编码,示例代码:

1
2
3
4
5
6
7
const fs = require('fs')

fs.readFile('/Users/kaka/Desktop/main.py','utf8',function(err,data){
console.log(err)
console.log('======')
console.log(data)
})

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
null
======
from stock_env import StockEnv

env = StockEnv()
env.reset()
for i in range(1000):
env.render()
observation, reward, terminated, truncated, info = env.step(env.action_space.sample())
print(observation)
print(reward)
if terminated or truncated:
break
env.close()

如果err不为空,说明读取失败。示例代码:

1
2
3
4
5
6
7
const fs = require('fs')

fs.readFile('/Users/kaka/Desktop/mainXXX.py','utf8',function(err,data){
console.log(err)
console.log('======')
console.log(data)
})

运行结果:

1
2
3
4
5
6
7
8
[Error: ENOENT: no such file or directory, open '/Users/kaka/Desktop/mainXXX.py'] {
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: '/Users/kaka/Desktop/mainXXX.py'
}
======
undefined

写文件

写文件通过writeFile()方法。

1
fs.writeFile(file, data[, options], callback)
  • file,必选,文件路径。
  • data,必选,写入的内容。
  • options,可选,以什么格式写入文件内容,默认是utf8。
  • callback,必选,文件写入完成后的回调函数。

示例代码:

1
2
3
4
5
const fs = require('fs')

fs.writeFile('/Users/kaka/Desktop/n.txt','Hello',function(err){
console.log(err)
})

运行结果:

1
null

文件内容:
写文件

路径问题

Node.js在运行的时候,如果使用相对路径,会以执行node命令时所处的目录为基础。
在有些时候,在有些时候,可以考虑统一使用绝对路径。

path

path,Node.js官方提供一个内置模型,用来处理路径。

path.join()

path.join(),将多个路径片段拼接成一个完整的路径字符串。
例如,绝对路径/a的子目录/b/c/d的上级目录..的当前目录./d2的内部e.txt文件。示例代码:

1
2
3
const path = require('path')

console.log(path.join(`/a`,`/b/c/d`,`..`,`./d2`,`e.txt`))

运行结果:

1
/a/b/c/d2/e.txt

path.basename()

path.basename(),从路径字符串中,将文件名解析出来。示例代码:

1
2
3
const path = require('path')

console.log(path.basename('/a/b/c/d2/e.txt'))

运行结果:

1
e.txt

path.extname()

path.extname(),获取路径中的扩展名。示例代码:

1
2
3
const path = require('path')

console.log(path.extname('/a/b/c/d2/e.txt'))

运行结果:

1
.txt

__dirname

__dirname表示当前文件所处的目录。示例代码:

1
2
3
4
const path = require('path')

console.log(__dirname)
console.log(path.join(__dirname,`/d2`,`e.txt`))

运行结果:

1
2
/Users/kaka/Desktop
/Users/kaka/Desktop/d2/e.txt

__filename

__filename表示当前文件的完整路径。示例代码:

1
2
3
4
const path = require('path')

console.log(__filename)
console.log(path.basename(__filename))

运行结果:

1
2
/Users/kaka/Desktop/n.js
n.js

http

争议

有些资料说,http,是Node.js官方提供的内置模块,用来创建web服务。

这么说,不够完整。
是Node.js官方提供的内置模块,这句话没有问题。
但是不仅仅用来创建web服务,还可以发送请求。
完整的论述是,提供了通过HTTP(超文本传输协议)传输数据这个功能的模块。
与之相对应的,还有一个模块,https,通过HTTP(TLS/SSL)协议传输数据。

发送请求

发送GET请求,通过https.get方法,示例代码:

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

https.get('https://kakawanyifan.com', (response) => {
let data = '';

// called when a data chunk is received.
response.on('data', (chunk) => {
data += chunk;
});

// called when the complete response is received.
response.on('end', () => {
console.log(data);
});

}).on('error', (error) => {
console.log('Error: ' + error.message);
});
  • response.on('data'...,拼接返回。
  • response.on('end'...,请求完成的时候触发。
  • .on('error'...,处理异常。

发送POST请求。

  1. 定义需要发送的数据,data
  2. 定义发送地址,请求头等,options
  3. 定义回调方法,请求完成的回调,处理异常,req
  4. 发送请求,write()
  5. 结束,end()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const https = require('https');

const data = JSON.stringify({
name: 'John Doe',
job: 'DevOps Specialist'
})

const options = {
protocol: 'https:',
hostname: 'kakawanyifan.com',
port: 443,
path: '/12003',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': data.length
}
}

const req = https
.request(options, res => {
let data = ''

res.on('data', chunk => {
data += chunk
})

res.on('end', () => {
console.log(data)
})
})
.on('error', err => {
console.log('Error: ', err.message)
})

req.write(data)
req.end()

提供服务

导入http模块,示例代码:

1
const http = require('http');

创建web服务器实例,示例代码:

1
const server = http.createServer()

为服务器绑定reqeust事件,示例代码:

1
2
3
4
5
6
server.on('request', (req,res) => {
console.log(`receive reqeust`)
console.log(`url ${req.url}`)
console.log(`method ${req.method}`)
res.end('收到请求')
})
  • req.url,请求地址。
  • req.method,请求方法
  • res.end('收到请求'),返回响应,并结束请求。

启动服务,调用服务器实例的.listen()方法,即可启动当前的web服务器实例,示例代码:

1
2
3
server.listen(8080, () => {
console.log('http server running 8080')
})

完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const http = require('http');

const server = http.createServer()

server.on('request', (req,res) => {
console.log(`receive reqeust`)
console.log(`url ${req.url}`)
console.log(`method ${req.method}`)
res.end('收到请求')
})

server.listen(8080, () => {
console.log('http server running 8080')
})

然后我们发一个请求试一下。
运行结果:

1
2
3
4
http server running 8080
receive reqeust
url /123
method GET

乱码

好,又乱码了。
这个我们已经处理太多次了。
设置响应头,告诉浏览器,要以utf-8',进行解析。

1
res.setHeader('Content-Type','text/html; charset=utf-8')

示例代码:

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

const server = http.createServer()

server.on('request', (req,res) => {
console.log(`receive reqeust`)
console.log(`url ${req.url}`)
console.log(`method ${req.method}`)
res.setHeader('Content-Type','text/html; charset=utf-8')
res.end('收到请求')
})

server.listen(8080, () => {
console.log('http server running 8080')
})

运行结果:

乱码已处理

npm

概述

Node.js中的第三方模块也被称为包。

我们可以通过npm的方式下载包。
npm官网:https://www.npmjs.com/

install

安装包的命令为:npm install 【包】

也可以简写作:npm i 【包】

默认会安装最新版本的,如果我们想制定版本,可以在包名之后,通过@符号指定具体的版本,npm install 【包】@【版本】

npm中,包的版本号是以"点分十进制"形式进行定义的,总共有三个数字,例如2.24.0
第1个数字表示大版本,第2位数字表示功能版本,第3位数字表示Bug修复版本。

package.json

必须要有package.json

npm规定,在项目根目录中,必须提供一个叫做package.json的包管理配置文件,用来记录与项目有关的一些配置信息。
例如,项目的名称、版本号、描述等,项目中都用到了哪些包,哪些包只在开发期间会用到,那些包在开发和部署时都需要用到。

类似于maven中的pom.xml
关于maven,可以参考《基于Java的后端开发入门:11.Maven》

创建package.json

可以通过npm init命令,初始化一个用npm管理的项目,该命令会创建package.json文件。

示例代码:

1
npm init -y

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Wrote to /Users/kaka/Documents/ns/npmns/package.json:

{
"name": "npmns",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
  • -y,在执行npm init命令期间的所有选项,均采用默认。
  • 上述命令只能在英文的目录下成功运行,所以,项目文件夹的名称一定要使用英文命名,不要使用中文,不能出现空格。

dependencies

我们执行npm install hexo,再观察一下package.json,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"name": "npmns",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"hexo": "^7.0.0-rc1"
}
}

npm会自动把包的名称和版本号,记录到package.json中。其中dependencies节点,记录的是安装了哪些包。

devDependencies

如果某些包只在项目开发阶段会用到,在项目上线之后不会用到,则建议把这些包记录到devDependencies节点中。
命令如下:

1
npm install moment --save-dev

或者

1
npm install moment -D

此时package.json的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"name": "npmns",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"hexo": "^7.0.0-rc1"
},
"devDependencies": {
"moment": "^2.29.4"
}
}

node_modules和package-lock.json

node_modules

我们会发现,还多了一个文件夹node_modules和一个文件package-lock.json
node_modules文件夹,用来存放所有已安装到项目中的包。require()导入第三方包时,就是从这个目录中查找并加载包。

package-lock.json配置文件用来记录node_modules目录下的每一个包的下载信息,例如包的名字、版本号、下载地址等。示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"name": "npmns",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"a-sync-waterfall": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz",
"integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA=="
},

【部分运行结果略】

"which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"requires": {
"isexe": "^2.0.0"
}
}
}
}

工作建议

  1. node_modules文件夹,添加到git的.gitignore忽略文件中。
    对于git-clone得到的新项目,通过npm install重新安装配置文件中的所有包。
  2. 不建议手动修改node_modulespackage-lock.json文件中的任何代码。
    如果需要卸载包,通过命令,npm uninstall 【包】

全局包

npm中,安装包的作用范围,可以分为两类包。

  1. 项目包
  2. 全局包

被安装到项目的node_modules目录中的包,都是项目包。上文我们讨论的,都是项目包。
在执行npm install命令时,如果提供了-g参数,则会把包安装为全局包。
通过命令npm root -g,可以查看全局包的安装路径。
一般只有工具性质的包(如npm),才有全局安装的必要性。

切换源

镜像源

下包的地址,就是源。
因为npm的官方源,在某些网络环境下,下包会很慢,甚至连不上。所以可以考虑国内的一些镜像源。

例如:

查看当前源

示例代码:

1
npm config get registry

运行结果:

1
https://registry.npmjs.org/

切换源

示例代码:

1
npm config set registry https://registry.npmmirror.com

发布包

初始化包

  1. 新建文件夹,作为包的目录。
  2. 执行npm init,进行初始化。生成package.json(包管理配置文件)
  3. 在根目录创建index.js,作为包的入口文件。
  4. 在根目录创建README.md,作为包的说明文档。

编写代码

我们以一个"获取当前时间"这个功能为例,代码结构如下。

代码结构

可以只在index.js中定义方法,作为一个例子,本文选择了"模块化"。

s.js,示例代码:

1
2
3
4
5
6
7
function now(){
return new Date()
}

module.exports = {
now
}

index.js,示例代码:

1
2
3
4
5
6
7
8
9
const now = require('./scripts/s.js')

function xianZai(){
return now.now()
}

module.exports = {
xianZai
}

发布

注册npm账号

访问:https://www.npmjs.com
注册账号。

登录npm账号

执行命令npm login命令,进行登录。

需要注意的是,在执行npm login之前,需要先源切换为npm的官方源,否则登录的是镜像源。

如,淘宝源。
淘宝源

切换回官方源:

1
npm config set registry https://registry.npmjs.org

发布到npm上

在包的根目录,执行npm publish,发布。

试一下

我们可以试一下,先执行npm install kaka-package-test,安装。

示例代码:

1
2
3
const kaka = require('kaka-package-test')

console.log(kaka.xianZai())

运行结果:

1
2023-05-05T09:44:11.750Z

删除已发布的包

npm unpublish 【包】 --force,即可从npm删除已发布的包。

注意:

  1. npm unpublish,只能删除72小时以内发布的包。
  2. npm unpublish,删除的包,在24小时内不允许重复发布。

图床

npm命令有一个神奇的用法,图床。虽然这个属于 滥用

所有的操作都一样,只是我们在包里面放了图片。

包里放图片

访问方法:

例如,在本例中,地址为:https://cdn.jsdelivr.net/npm/kaka-package-pic/pic/1.jpg

kaka-package-pic/pic/1.jpg

模块加载机制

优先从缓存中加载

模块在第一次加载后会被缓存,不论是内置模块、用户自定义模块、还是第三方模块,它们都会优先从缓存中加载。
这也意味着多次调用require()不会导致模块的代码被执行多次。

我们可以试一下,假设存在1.js,示例代码:

1
console.log('1')

2.js,示例代码:

1
2
3
const m1 = require('./1.js')

const m2 = require('./1.js')

运行结果:

1
1

内置模块优先级最高

内置模块的加载优先级最高。
例如,fs是node的一个内置模块。假如说,在node_modules目录下有名字相同的包也叫做fsrequire('fs')加载的也是内置的fs模块。

会发现只运行了一次。

自定义模块的加载机制

使用require()加载自定义模块时,必须指定以./..//开头的路径标识符。如果没有指定,则node会把它当作内置模块或第三方模块进行加载。

使用require()导入自定义模块时,如果省略了文件的扩展名,则Node.js会按顺序分别尝试加载以下的文件:

  1. 按照确切的文件名进行加载
  2. 补全.js扩展名进行加载
  3. 补全.json扩展名进行加载
  4. 补全.node扩展名进行加载
  5. 加载失败,终端报错

有些资料说,对于用户自定义模块的加载,必须是./../开头的相对路径,这个是不对的。
在我实际测试中,绝对路径也可以,应该是以./..//开头的路径。

第三方模块的加载机制

如果传递给require()的模块标识符不是一个内置模块,也没有以./..//开头,则Node.js会从当前模块的父目录开始,尝试从/node_modules文件夹中加载第三方模块。
如果没有找到对应的第三方模块,则移动到再上一层父目录中,进行加载,直到文件系统的根目录。
例如,假设在C:\Users\kaka\project\foo.js文件里require('tools'),则Node.js会按以下顺序查找:

  1. C:\Users\kaka\project\node_modules\tools
  2. C:\Users\kaka\node_modules\tools
  3. C:\Users\node_modules\tools
  4. C:\node_modules\tools

目录作为模块

如果把目录作为模块标识符,传递给require()进行加载,按照如下步骤加载

  1. 在被加载的目录下查找一个叫做package.json的文件,并寻找main属性,将main属性定义的文件,作为require()加载的入口。
  2. 如果目录里没有package.json文件,或者main入口不存在,或者main入口无法解析,则Node.js将会试图加载目录下的index.js文件。
  3. 如果以上两步都失败了,则Node.js会在终端打印错误消息,报告模块的缺失:Error: Cannot find module 'xxx'

假设代码结构如下。

目录作为模块

package.json

1
2
3
{
"main": "main.js"
}

1.jsrequire('/dir')。示例代码:

1
const d = require('/dir')

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
internal/modules/cjs/loader.js:818
throw err;
^

Error: Cannot find module '/dir'
Require stack:
- /Users/kaka/Documents/ns/npmns/1.js
at Function.Module._resolveFilename (internal/modules/cjs/loader.js:815:15)
at Function.Module._load (internal/modules/cjs/loader.js:667:27)
at Module.require (internal/modules/cjs/loader.js:887:19)
at require (internal/modules/cjs/helpers.js:74:18)
at Object.<anonymous> (/Users/kaka/Documents/ns/npmns/1.js:1:11)
at Module._compile (internal/modules/cjs/loader.js:999:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)
at Module.load (internal/modules/cjs/loader.js:863:32)
at Function.Module._load (internal/modules/cjs/loader.js:708:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12) {
code: 'MODULE_NOT_FOUND',
requireStack: [ '/Users/kaka/Documents/ns/npmns/1.js' ]
}

mysql

安装

mysql,一个第三方的模块,提供了在Node.js中连接和操作MySQL数据库的能力。

安装命令:npm install mysql

配置

createPool

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const mysql = require('mysql')

// 建立连接
const db = mysql.createPool({
host: '10.211.55.19',
user: 'root',
password: 'MySQL@2023',
database: 'my_db_01'
})

db.query('select 1',(error, result) => {
if (error) return console.log(error.message)
console.log(result)
})

运行结果:

1
ER_NOT_SUPPORTED_AUTH_MODE: Client does not support authentication protocol requested by server; consider upgrading MySQL client

上述报错了,这个报错的原因,我们在《MySQL从入门到实践:1.概述和工具准备》的"MySQL安装与配置"的"连接异常的处理"也讨论过。
原因是:MySQL8.0默认采用caching_sha2_password的加密方式,但是有些第三方客户端不支持这种加密方式。解决方法是

1
ALTER USER'root'@'%' IDENTIFIED WITH mysql_native_password BY 'MySQL@2023';

如果运行成功,结果为:

1
[ RowDataPacket { '1': 1 } ]

createConnection

在上文,我们用的是mysql.createPool(),还有一个方法是mysql.createConnection()
mysql.createPool(),建立连接池。
mysql.createConnection(),建立连接。

关于连接和连接池,可以参考《基于Java的后端开发入门:10.JDBC》

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const mysql = require('mysql')

// 建立连接
const db = mysql.createConnection({
host: '10.211.55.19',
user: 'root',
password: 'MySQL@2023',
database: 'my_db_01'
})

db.query('select 1',(error, result) => {
if (error) return console.log(error.message)
console.log(result)
})

db.end()

运行结果:

1
[ RowDataPacket { '1': 1 } ]

mysql.createConnection()中,我们可以执行db.end(),主动释放连接。但是在mysql.createPool(),不要执行db.end()

查询

查询user表中所有的数据。示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const mysql = require('mysql')

// 建立连接
const db = mysql.createPool({
host: '10.211.55.19',
user: 'root',
password: 'MySQL@2023',
database: 'my_db_01'
})

db.query('select * from user',(error, result) => {
if (error) return console.log(error.message)
console.log(result)
})

运行结果:

1
2
3
4
[
RowDataPacket { username: 'u1', password: 'p1' },
RowDataPacket { username: 'u2', password: 'p2' }
]

插入

一般方法

示例代码:

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

// 建立连接
const db = mysql.createPool({
host: '10.211.55.19',
user: 'root',
password: 'MySQL@2023',
database: 'my_db_01'
})

const user = {
username: '姓名',
password: '密码'
}

const sqlStr = 'INSERT INTO user (username,password) VALUES (?,?)'

db.query(sqlStr,[user.username,user.password],(error,result) => {
if (error) return console.log(error.message)
if (result.affectedRows === 1){
console.log('插入成功')
}
})

运行结果:

1
插入成功

快捷方法

如果对象的每个属性和数据表的字段一一对应,可以通过INSERT INTO SET的方式快速插入数据。

1
INSERT INTO user SET ?

示例代码:

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

// 建立连接
const db = mysql.createPool({
host: '10.211.55.19',
user: 'root',
password: 'MySQL@2023',
database: 'my_db_01'
})

const user = {
username: '姓名',
password: '密码'
}

const sqlStr = 'INSERT INTO user SET ?'

db.query(sqlStr,user,(error,result) => {
if (error) return console.log(error.message)
if (result.affectedRows === 1){
console.log('插入成功')
}
})

运行结果:

1
插入成功

更新

一般方法

示例代码:

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

// 建立连接
const db = mysql.createPool({
host: '10.211.55.19',
user: 'root',
password: 'MySQL@2023',
database: 'my_db_01'
})

const user = {
username: '姓名',
password: '新密码'
}

const sqlStr = 'UPDATE user SET password = ? WHERE username = ?'

db.query(sqlStr,[user.password,user.username],(error,result) => {
if (error) return console.log(error.message)
if (result.affectedRows === 1){
console.log('更新成功')
}
})

运行结果:

1
更新成功

快捷方法

如果我们需要更新多个字段,这种方法会很效率很高。

1
UPDATE user SET ? WHERE username = ?

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const mysql = require('mysql')

// 建立连接
const db = mysql.createPool({
host: '10.211.55.19',
user: 'root',
password: 'MySQL@2023',
database: 'my_db_01'
})

const sqlStr = 'UPDATE user SET ? WHERE username = ?'

db.query(sqlStr,[user,user.username],(error,result) => {
if (error) return console.log(error.message)
if (result.affectedRows === 1){
console.log('更新成功')
}
})

运行结果:

1
更新成功

删除

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const mysql = require('mysql')

// 建立连接
const db = mysql.createPool({
host: '10.211.55.19',
user: 'root',
password: 'MySQL@2023',
database: 'my_db_01'
})

const user = {
username: '姓名',
password: '新密码',
address: '新地址'
}

const sqlStr = 'DELETE FROM user WHERE username = ?'

db.query(sqlStr,user.username,(error,result) => {
if (error) return console.log(error.message)
if (result.affectedRows === 1){
console.log('删除成功')
}
})

运行结果:

1
删除成功

express

概述

Express,基于Node.js的Web开发框架。

安装

npm i express

基本操作

创建Web服务器

示例代码:

1
2
3
4
5
6
7
8
9
const express = require('express')

// 创建Web服务器
const app = express()

// 启动,监听8080端口
app.listen(80, () => {
console.log('express server running')
})

监听GET请求

通过app.get()方法,可以监听客户端发出的GET请求。

示例代码:

1
2
3
app.get('/get',function(req,res){
// 处理函数
})
  • 参数一:客户端请求的URL地址
  • 参数二:请求对应的处理函数
    • req:请求对象(包含了与请求相关的属性与方法)
    • res:响应对象(包含了与响应相关的属性与方法)

监听POST请求

通过app.post()方法,可以监听客户端发出的POST请求。

示例代码:

1
2
3
app.post('/post',function(req,res){
// 处理函数
})
  • 参数一:客户端请求的URL地址
  • 参数二:请求对应的处理函数
    • req:请求对象(包含了与请求相关的属性与方法)
    • res:响应对象(包含了与响应相关的属性与方法)

发送响应

通过res.send()方法,发送响应。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
app.get('/get',function(req,res){
res.send({
name:'zs',
age:20,
gender:'男'
})
})

app.post('/post',function(req,res){
res.send('请求成功')
})

参数处理

获取Get请求中的参数

客户端通过?name=姓名&age=22这种形式传输参数。
服务端通过req.query.namereq.query.age的形式接收参数。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const express = require('express')

// 创建Web服务器
const app = express()

// 启动,监听8080端口
app.listen(8080, () => {
console.log('express server running')
})

app.get('/get',function(req,res){
console.log(req.query.name)
console.log(req.query.age)
})

app.post('/post',function(req,res){
res.send('请求成功')
})
1
curl --location --request GET 'http://localhost:8080/get?name=姓名&age=22'

运行结果:

1
2
姓名
22

获取RESTful中的参数

有些资料称这个方法为获取动态参数,但是我认为更准确的说法,应该是RESTful
关于RESTful,可以参考《基于Java的后端开发入门:17.SpringMVC》

服务端通过/user/:id的形式定义要接收的参数,并通过req.params获取。

示例代码:

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

// 创建Web服务器
const app = express()

// 启动,监听8080端口
app.listen(8080, () => {
console.log('express server running')
})


app.get('/user/:id', function(req,res){
console.log(req.params)
})
1
curl --location --request GET 'http://localhost:8080/user/1'

运行结果:

1
{ id: '1' }

获取POST请求的表单参数

通过req.body接收,注意其中一行,app.use(express.urlencoded());。示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const express = require('express')

// 创建Web服务器
const app = express()

// 启动,监听8080端口
app.listen(8080, () => {
console.log('express server running')
})

app.use(express.urlencoded());

//这样就可以使用 req.body 来获取 post 传递来的值了
app.post('/user', (req, res) => {
console.log(req.body);
res.send('完成');
});
1
2
3
4
5
curl --location --request POST 'http://localhost:8080/user' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'h1=抽烟' \
--data-urlencode 'h2=喝酒' \
--data-urlencode 'h3=烫头'

运行结果:

1
{ h1: '抽烟', h2: '喝酒', h3: '烫头' }

获取POST请求的JSON参数

也是通过req.body接收,注意其中一行app.use(express.json());。示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const express = require('express')

// 创建Web服务器
const app = express()

// 启动,监听8080端口
app.listen(8080, () => {
console.log('express server running')
})

app.use(express.json());

//这样就可以使用 req.body 来获取 post 传递来的值了
app.post('/user', (req, res) => {
console.log(req.body);
res.send('完成');
});
1
2
3
4
5
6
curl --location --request POST 'http://localhost:8080/user' \
--header 'Content-Type: application/json' \
--data-raw '{
"u1":1,
"u2":2
}'

运行结果:

1
{ u1: 1, u2: 2 }

获取POST请求上传的文件

首先,需要安装multer

然后,配置上传文件临时存储位置:

1
const upload=multer({dest:"uploads/"});

最后,通过req.file获取单个文件,对于多个文件,通过req.files获取。示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
const express = require('express')
const path = require('path')
const fs = require('fs')

// 创建Web服务器
const app = express()

// 启动,监听8080端口
app.listen(8080, () => {
console.log('express server running')
})

// 引入包
const multer = require('multer');

// 配置上传文件临时存储位置
const upload = multer({dest:"uploads/"});


app.post("/postfile", upload.single('cover'), function (req, res) {

// 单一文件 是 req.file 多个文件是 req.files 记住这点区别哈
// 获取上传文件的信息对象
let file=req.file;

// 获取文件的临时存放位置
let imgPath=file.path;

// 获取文件名 后缀
let extname=path.extname(file.originalname);

// 创建一个文件读取流
const fileReader=fs.createReadStream(imgPath);

// 定义文件存储路径 当前文件夹下创建一个 public/upload文件
var fileDir=path.join(__dirname,'/public/upload');
const filePath=`${fileDir}/temp${extname}`;

//创建文件输出流
const fileWrite=fs.createWriteStream(filePath);

// 写入文件数据
fileReader.pipe(fileWrite);

// 返回
res.send({'code':200,msg:'success'})
})

注意,upload.single('cover')

  1. 上传单个文件:upload.single('myfile');
  2. 上传多个文件:upload.array('myfile');
  3. 传多个文件 限制文件的个数:
    upload.array('myfile',1);
    upload.fields([name:'myfile',maxCount:2,{name:'myfile2'}]);

我们可以通过Postman发送文件进行测试,发送方法参考《基于Java的后端开发入门:17.SpringMVC》的"接收请求参数"的"文件"部分。

这里还有一个特点,没有用app.use(),关于该部分,我们会在下文讨论中间件的时候进行讨论。

body-parser

还有一种方法是依赖中间件body-parser,这种方法实际上已经被废弃了,我们不讨论。

静态资源

express.static()

通过app.use(express.static(【指定目录】)),可以非常方便地创建一个静态资源服务器,然后可以将指定目录下的图片、CSS文件、JavaScript文件等对外开放。

示例代码:
代码结构如下,publicapp.js位于同一层级
public和app.js位于同一层级

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

访问地址为:http://localhost:8080/Tokyo_Stock_Exchange.jpeg

http://localhost:8080/Tokyo_Stock_Exchange.jpeg

多个静态资源目录

如果我们有多个静态资源目录,只需要多次调用express.static()函数即可。

示例代码:

1
2
app.use(express.static('public'))
app.use(express.static('files'))

路径前缀

如果希望在静态资源访问路径上加上前缀,可以通过如下的方式。

示例代码:

1
app.use('/img',express.static('public'))

此时,访问地址为:http://localhost:8080/img/Tokyo_Stock_Exchange.jpeg

路由

什么是路由

Express中的路由,指的是,我以某种方式请求某个路径,最终会交给哪个函数处理。

格式如下:

1
app.METHOD(PATH,HANDLER)

我们上文的getpost的例子,其实就是路由。

1
2
3
4
5
6
7
8
9
10
11
app.get('/get',function(req,res){
res.send({
name:'zs',
age:20,
gender:'男'
})
})

app.post('/post',function(req,res){
res.send('请求成功')
})

路由匹配

在Express中,当一个请求到达服务器之后,先经过路由的匹配,只有匹配成功之后,才会调用对应的处理函数。
在匹配时,会按照路由注册的顺序进行匹配,如果请求方式和请求的URL同时匹配成功,则Express会将这次请求,转交给对应的函数进行处理。

路由的匹配过程

模块化路由

为了方便对路由进行模块化的管理,我们推荐将路由抽离为单独的模块。

代码结构:
模块化路由

第一步:

  • 创建路由模块对应的.js文件。
  • 调用express.Router()函数创建路由对象。
  • 向路由对象上挂载具体的路由。
  • 使用module.exports向外共享路由对象。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 调用`express.Router()`函数创建路由对象
const router = require('express').Router()

// 向路由对象上挂载具体的路由
router.get('/user/list',function(req,res){
res.send('Get User List')
})

// 向路由对象上挂载具体的路由
router.post('/user/add',function(req,res){
res.send('Add New User')
})

// 使用`module.exports`向外共享路由对象
module.exports = router

第二步:使用app.use()函数注册路由模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
const express = require('express')

// 创建Web服务器
const app = express()

// 启动,监听8080端口
app.listen(8080, () => {
console.log('express server running')
})

const userRouter = require('./router/user.js')

app.use(userRouter)

中间件

概述

中间件,Middleware,当一个请求到达Express的服务器之后,可以连续调用多个中间件,从而对这次请求进行预处理。

Express

这个概念也不陌生,类似的有

中间件的本质,就是一个函数。

中间件使用方式

  • 在中间件函数的形参列表中,包含next参数。但是,在路由处理函数中只包含reqres
  • next()这一行,表示把流转关系转交给下一个中间件或路由。

定义中间件

示例代码:

1
2
3
4
const mw = function(req,res,next){
console.log('这是一个中间件函数')
next()
}

全局生效的中间价

app.use(中间件函数)

客户端发起的任何请求,到达服务器之后,都会触发的中间件,叫做全局生效的中间件。
通过调用app.use(中间件函数),即可定义一个全局生效的中间件。

示例代码:

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

// 创建Web服务器
const app = express()

// 启动,监听8080端口
app.listen(8080, () => {
console.log('express server running')
})

const mw = function(req,res,next){
console.log('这是一个中间件函数')
next()
}

app.use(mw)

我们在上文的app.use(express.urlencoded())app.use(express.json()),就是全局生效的中间件。

快捷方法

还有一种方法,这个是不是中间件?

1
2
3
4
app.use(function (req,res,next){
console.log('这也是一个中间件')
next()
})

也是中间件,中间件的本质就是一个函数。

多个中间件的顺序

可以使用app.use()定义多个全局中间件。
客户端请求到达服务器之后,会按照中间件定义的先后顺序依次进行调用。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const express = require('express')

// 创建Web服务器
const app = express()

// 启动,监听8080端口
app.listen(8080, () => {
console.log('express server running')
})

const mw = function(req,res,next){
console.log('这是一个中间件函数')
next()
}

app.use(mw)

app.use(function (req,res,next){
console.log('这也是一个中间件')
next()
})

app.get('/',function(req,res){
console.log('get')
})

运行结果:

1
2
3
这是一个中间件函数
这也是一个中间件
get

局部生效的中间件

不使用app.use()定义的中间件,叫做局部生效的中间件。

比如我们在上文的POST接收文件定义的中间件。

1
app.post("/postfile", upload.single('cover'), function (req, res) {

定义多个局部中间件,有两种方式,是等价的。
方式一:app.get('/' , mwi, mw2, (req, res) => {res.send('Home page.')})
方式二:app.get('/', [mw1, mw2], (req, res) => {res.send('Home page.')})

五类中间件

Express中的间件可以分成5类:

  1. 应用级别的中间件
  2. 路由级别的中间件
  3. 错误级别的中间件
  4. Express内置的中间件
  5. 第三方的中间件

应用级别的中间件

通过app.use()app.get()app.post()等,绑定到app实例上的中间件,叫做应用级别的中间件。

我们上文的定义的所有中间件,都属于应用级别的中间件。

路由级别的中间件

绑定到express.Router()实例上的中间件,叫做路由级别的中间件。
其作用和应用级别中间件没有任何区别。
只不过,应用级别中间件是绑定到app实例上,路由级别中间件绑定到router实例上。

示例代码:

1
2
3
4
router.use(function(req,res,next){
console.log('Time',Date.now())
next()
})

错误级别的中间件

专门用来捕获整个项目中发生的异常错误。

在错误级别中间件的处理函数中,必须有4个形参,形参顺序从前到后,分别是errreqresnext

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const express = require('express')

// 创建Web服务器
const app = express()

// 启动,监听8080端口
app.listen(8080, () => {
console.log('express server running')
})



app.get('/',function(req,res){
throw new Error('制造错误')
console.log('get')
})


const em = function (err,req,res,next){
console.log('发生了:' + err.message)
res.send('Error')
}

app.use(em)

运行结果:

1
发生了:制造错误

注意:错误级别的中间件,必须注册在所有路由之后!

我们来看一个没有定义在路由之后的现象。示例代码:

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

// 创建Web服务器
const app = express()

// 启动,监听8080端口
app.listen(8080, () => {
console.log('express server running')
})


const em = function (err,req,res,next){
console.log('发生了:' + err.message)
res.send('Error')
next()
}

app.use(em)

app.get('/',function(req,res){
throw new Error('制造错误')
console.log('get')
})

运行结果:

1
2
3
4
5
6
7
8
9
10
11
Error: 制造错误
at /Users/kaka/Documents/ns/npmns/app.js:20:11
at Layer.handle [as handle_request] (/Users/kaka/Documents/ns/npmns/node_modules/express/lib/router/layer.js:95:5)
at next (/Users/kaka/Documents/ns/npmns/node_modules/express/lib/router/route.js:144:13)
at Route.dispatch (/Users/kaka/Documents/ns/npmns/node_modules/express/lib/router/route.js:114:3)
at Layer.handle [as handle_request] (/Users/kaka/Documents/ns/npmns/node_modules/express/lib/router/layer.js:95:5)
at /Users/kaka/Documents/ns/npmns/node_modules/express/lib/router/index.js:284:15
at Function.process_params (/Users/kaka/Documents/ns/npmns/node_modules/express/lib/router/index.js:346:12)
at next (/Users/kaka/Documents/ns/npmns/node_modules/express/lib/router/index.js:280:10)
at Layer.handle [as handle_request] (/Users/kaka/Documents/ns/npmns/node_modules/express/lib/router/layer.js:91:12)
at trim_prefix (/Users/kaka/Documents/ns/npmns/node_modules/express/lib/router/index.js:328:13)

Express内置的中间件

Express内置了3个常用的中间件,这3个常用的中间件,我们在上文都讨论过。

  1. express.static
    快速托管静态资源的内置中间件。
  2. express.json
    解析JSON格式的请求体数据,仅在4.16.0以上的版本中可用。
  3. express.urlencoded
    解析URL-encoded格式的请求体数据,仅在4.16.0以上的版本中可用。

第三方的中间件

上文提到的body-parser,就是第三方的中间件。

在下文,我们还会提到好几个第三方的中间件。

注意事项

  1. 除了,错误级别的中间件,其他中间件一定要在路由之前注册
    因为请求在到达后,按照从上到下进行匹配。如果对于其他中间件,如果路由之后注册中间件,会先匹配到路由,不会先进入中间件。
  2. 执行完中间件的业务代码之后,不要忘记调用next()函数。
  3. 为了防止代码逻辑混乱,调用next()函数后不要再写额外的代码。

跨域

现象

我们通过Chrome浏览器访问https://kakawanyifan.com/,进入开发者模式,调用http://127.0.0.1:8080/

示例代码:

1
2
3
4
var xhr = new XMLHttpRequest();
var url = "http://127.0.0.1:8080/"
xhr.open("GET", url);
xhr.send();

运行结果:
跨域

跨域了!

cors中间件

在Express中可以使用cors中间件解决跨域。

  1. 运行npm install cors安装中间件。
  2. 使用const cors = require('cors')导入中间件。
  3. 在路由之前调用app.use(cors())配置中间件。

那么,为什么这样就可以了?
因为在app.use(cors())后,响应头多如下红框标示的一行。

响应头多了这个

即,这种方法,是被请求的一方主动允许跨域。
那么,如果我们只想允许指定的域名可以跨域呢?

示例代码:

1
2
3
4
5
const corsOptions = {
origin: 'https://kakawanyifan.com'
}

app.use(cors(corsOptions))

关于cors中间件的更多用法,可以参考:https://expressjs.com/en/resources/middleware/cors.html

身份认证

身份认证有两种方式:

  1. Session
  2. JWT

Session

什么是Session

关于什么是Session,可以参考《基于Java的后端开发入门:13.Servlet、Filter和Listener》的"Session"部分。

配置express-session中间件

在Express项目中,可以通过express-session中间件,在项目中使用Session认证。

安装:npm install express-session

使用,示例代码:

1
2
3
4
5
6
7
const session = require('express-session')

app.use(session({
secret: 'kaka',
resave: false,
saveUninitialized: true
}))
  • secret:密钥,可以为任意字符串。
  • resave: falsesaveUninitialized: true,是固定写法。

访问Session对象

通过req.session来访问Session对象,通过req.session.destroy(),清空服务器保存的Session信息。

代码结构:
访问Session对象

app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const express = require('express')

// 创建Web服务器
const app = express()

// 启动,监听8080端口
app.listen(8080, () => {
console.log('express server running')
})

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

const session = require('express-session')

app.use(session({
secret: 'kaka',
resave: false,
saveUninitialized: true
}))

app.post('/login', (req, res) => {
if (req.body.username !== 'admin' || req.body.password !== '000000') {
return res.send({ status: 1, msg:"登录失败了"})
}

// 将用户的信息,存储到Session中
req.session.user =req.body
// 將用户的登录状态,存储到Session中
req.session.islogin = true

res.send({status: 0, msg:"登录成功"})
})

访问http://127.0.0.1:8080/login.html,登录后,会看到如下内容。

Session

JWT

工作原理

JWT,JSON Web Token,跨域认证解决方案。

用户的信息通过Token字符串的形式,保存在客户端浏览器中,服务器通过还原Token字符串的形式来认证用户的身份。
具体工作原理如下:

JWT跨域

组成部分

JWT由三部分组成,分别是:

  • Header,头部
  • Payload,有效荷载
  • Signature,签名

其中Payload是用户信息经过加密之后生成的字符串。Header和Signature是安全性相关的部分,用以保证Token的安全性。

三者之间使用英文的.分隔。

例如,这就是一个JWT:

1
2
3
4
5
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9
.
eyJzdWIiOiJ7XCJmcm9tT3BlbklkXCI6XCJcIixcInRvT3BlbklkXCI6XCJcIixcImJsb2dJZFwiOlwiNzEzODMtOTg4MDczODE3Njc0Ny0yNTVcIn0iLCJpYXQiOjE2ODI5OTMwMzQsImV4cCI6MTc2OTM5MzAzNH0
.
pJiGEkH3GOyseCaDgZGCEKa5PTtLkzZqOH4-zAKFJDWT97mrLbzZ8bUTCpKAq4zH20ZO5QNWRLegI1-gjpfjXw

使用方式

客户端收到服务器返回的JWT之后,会将其保存在本地(localStorage、sessionStorage或者cookie)。
此后,客户端每次与服务器通信,都要带上JWT的字符串,进行身份认证。
推荐的做法是把JWT放在HTTP请求头的Authorization字段中,形式如下:

1
Authorization: Bearer <token>

在Express中使用JWT

安装JWT相关的包

  • jsonwebtoken,用于生成JWT字符串。
  • express-jwt,用于将JWT字符串解析还原成JSON对象

示例代码:

1
npm install jsonwebtoken express-jwt

导入JWT相关的包

使用require(),导入JWT相关的两个包。示例代码:

1
2
const jwt = require('jsonwebtoken')
const expressJWT = require('express-jwt')

定义secret密钥

为了保证JWT字符串的安全性,防止JWT字符串在网络传输过程中被破解,我们需要专门定义一个用于加密和解密的secret密钥。

  • 当生成JWT字符串的时候,需要使用secret密钥对用户的信息进行加密,最终得到加密后的JWT字符串。
  • 当把JWT字符串解析还原成JSON对象的时候,需要使用secret密钥进行解密。

示例代码:

1
const secretKey = 'Kaka'

在登录成功后生成JWT字符串

调用jsonwebtoken包提供的sign()方法,将用户的信息加密成JWT字符串,发送给客户端。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 登录接口
app.post('/api/login', function (req, res) {
// 将 req.body 请求体中的数据,转存为 userinfo 常量
const userinfo = req.body
// 登录失败
if (userinfo.username !== 'admin' || userinfo.password !== '000000') {
return res.send({
status: 400,
message: '登录失败!',
})
}
// 登录成功
// TODO_03:在登录成功之后,调用 jwt.sign() 方法生成 JWT 字符串。并通过 token 属性发送给客户端
// 参数1:用户的信息对象
// 参数2:加密的秘钥
// 参数3:配置对象,可以配置当前 token 的有效期
// 记住:千万不要把密码加密到 token 字符中
const tokenStr = jwt.sign({ username: userinfo.username }, secretKey, { expiresIn: '30s' })
res.send({
status: 200,
message: '登录成功!',
token: tokenStr, // 要发送给客户端的 token 字符串
})
})

将JWT字符串还原为JSON对象

客户端每次在访问那些有权限接口的时候,都需要主动通过请求头中的Authorization字段,将Token字符串发送到服务器进行身份认证。
此时,服务器可以通过express-jwt,自动将客户端发送过来的Token解析还原成JSON对象。

示例代码:

1
app.use(expressJWT({ secret: secretKey }).unless({ path: [/^\/api\//] }))
  • 只要配置成功了express-jwt这个中间件,就可以把解析出来的用户信息,挂载到req.user属性上。
  • unless用于配置哪些接口不需要访问权限。

通过req.user获取用户信息

express-jwt这个中间件配置成功之后,即可在那些有权限的接口中,使用req.user对象,来访问从JWT字符串中解析出来的用户信息。

示例代码:

1
2
3
4
5
6
7
8
9
10
// 这是一个有权限的 API 接口
app.get('/admin/getinfo', function (req, res) {
// 使用 req.user 获取用户信息,并使用 data 属性将用户信息发送给客户端
console.log(req.user)
res.send({
status: 200,
message: '获取用户信息成功!',
data: req.user,
})
})

捕获解析JWT失败后产生的错误

当使用express-jwt解析Token字符串时,如果客户端发送过来的Token字符串过期或不合法,会产生一个解析失败的错误。
我们可以通过Express的错误级别的中间件,捕获这个错误并进行相关的处理。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
app.use((err, req, res, next) => {
// 这次错误是由 token 解析失败导致的
if (err.name === 'UnauthorizedError') {
return res.send({
status: 401,
message: '无效的token',
})
}
res.send({
status: 500,
message: '未知的错误',
})
})

Serverless

Serverless,需要依赖云服务商的函数计算功能。
例如,阿里云的:https://www.aliyun.com/product/fc

安装插件@serverless-devs/s,在部署方面会更便捷。

安装

安装命令:

1
npm install @serverless-devs/s -g

安装完成后,可以通过s -v查看是否安装成功。示例代码:

1
s -v

运行结果:

1
@serverless-devs/s: 2.1.14, s-home: /Users/kaka/.s, darwin-x64, node-v12.22.12

注意,如果在安装的时候,没有-g,无法通过s -v查看是否安装成功。

配置

配置密钥

通过s config add,选择云服务厂商。示例代码:

1
s config add

运行结果:

1
2
3
4
5
6
7
8
9
? Please select a provider: (Use arrow keys)
❯ Alibaba Cloud (alibaba)
AWS (aws)
Azure (azure)
Baidu Cloud (baidu)
Google Cloud (google)
Huawei Cloud (huawei)
Tencent Cloud (tencent)
(Move up and down to reveal more choices)

通过上下方向键选择云服务厂商,回车键确认。
然后安装提示配置密钥等,在配置过程会,提示配置别名,默认是default

查看密钥

1
s config get -a default
  • -a--access,指定别名名称。
    如果不加上-a--access,即查看所有。

删除密钥

1
s config delete -a aliasName

初始化

1
s init devsapp/start-express -d start-express
  • -d,指定项目名称。

部署

1
s deploy
文章作者: Kaka Wan Yifan
文章链接: https://kakawanyifan.com/12004
版权声明: 本博客所有文章版权为文章作者所有,未经书面许可,任何机构和个人不得以任何形式转载、摘编或复制。

留言板