Node.js学习笔记

141 阅读16分钟

参考 npm官方网站


第一部分 引入

浏览器中为什么能运行JavaScript

浏览器中暴露内置API(例如DOM、BOM、Canvas、XMLHttpRequest、JS内置对象)提供给待执行的JavaScript代码。这些代码会在浏览器引擎中执行。

Node.js运行环境

JS除了做前端开发,也可以做后端开发。做后端开发时,需要使用到Node.js环境。它也是一个基于V8引擎的代码执行环境。同样地,Node.js也提供很多内置API,例如:fs、path、http、JS内置对象、querystring...

快捷键

esc:清空当前命令行的命令

cls: 清空终端(Mac是clear)

node.js的模块化导入和导出

module.exports进行导出:

// math.js
function add(a, b) {
    return a + b
}

function suntract(a, b) {
    return a - b
}

module.exports = {
    add : add,
    substract : substract
}

exports进行导出:

// math.js
function add(a, b) {
    return a + b
}

function suntract(a, b) {
    return a - b
}

exports.add = add
exports.substract = substract

exports只是module.exports的一个引用。

require进行导入

导入时,会执行导入文件,执行完之后,返回一个对象,这个对象是

{
  add: [Function: add],
  subtract: [Function: subtract]
}

require导入时,会首先看是否是Node.js的内置模块;然后看是否是一个文件或者文件夹模块;最后在node_modules文件夹里寻找相应模块。

// app.js

const math = require('./math.js');

const sum = math.add(2, 3);
const difference = math.subtract(5, 2);

console.log('Sum:', sum);
console.log('Difference:', difference);

第二部分 核心模块

fs模块

const fs = require('fs')

// fs.readFile(path[, options], callback)
fs.readFile('./files/1.txt', 'utf8', (err, datastr) => {
    console.log(err)
    console.log(datastr)
})

// 读取成功: err为null; 读取失败: err为错误对象,datastr为undefined

// fs.writeFile(path[, options], callback)

path模块

什么情况下nodeJS文件读取路径会出错

  • 1.js文件内容:
fs.readFile('./files/1.txt', () => {})
  • 执行1.js的方法:
node ./code/1.js

由于1.js中读取文件时,是用执行node命令时的路径拼接读取文件时的相对路径,而非用./code/拼接./files/1.txt,因此,会出现报错。

使用绝对路径

  • 1.js文件内容改为:
fs.readFile('C:\\users\\files\\code\\1.txt', () => {})

使用双反斜杠的原因是,单反斜杠是转义,双反斜杠就可以表示原本的意思。然而,这样会导致较差的可移植性和可维护性。

__dirname

  • 1.js文件内容改为:
fs.readFile(__dirname+'./files/1.txt', () => {})

双下划线__dirname的值不会随着Node命令所处的路径而进行改变。

path方式

const path = require('path')

path.join()

const pathStr = path.join('/a', '/b/c', '../', '/d')
\a\b\d\e

其中,../作为返回上一级,和/c相互抵消。

  • 1.js文件内容改为:
fs.readFile(path.join(__dirname, './files/1.txt'), () => {})
path.basename() 获取文件名
const path = require('path')
const fileName = path.basename('./files/index.html')                      // index.html
const fileNamewithoutExt = path.basename('./files/index.html', 'html')    // index
path.extname() 获取文件扩展名
const fileNameExt = path.extname('./files/index.html')                    // .html             

http模块

基本概念

客户端与服务器

在网络服务中,消耗资源的是客户端,提供资源的是服务器。通过http模块提供的http.createServer()方法,把电脑变为一台服务器。

通常,提供Web服务需要有以下三个步骤:

  • 安装 Apache 服务器软件
  • 网站根目录中放入要提供的资源
  • 在浏览器中输入 127.0.0.1localhost 访问本地服务器资源

在nodeJS中,可以使用http模块提供服务

IP地址

IP地址相当于电话号码,每台电脑的IP地址都不一样,只有知道对方IP地址的前提下,才能和对应的电脑进行数据通信。

  • 互联网每一台Web服务器,都有自己的IP地址。可以 ping 网址 进行访问。
  • 在开发期间,自己的电脑既是客户端,又是服务器。可以用 127.0.0.1localhost 访问。

域名和域名服务器

域名是一种字符型的地址方案。由域名服务器存放域名和服务器之间的转换关系。例如,127.0.0.1对应的域名就是localhost

端口号

端口号和门牌号一样,指明端口号,会准确交给对应Web服务进行处理。 image.png

  • 一个端口只能被一个Web服务所占用
  • 未指明端口号时,http将会发送给80端口,https将会发送给443端口。

创建web服务器

  • 导入http模块
const http = require('http')
  • 创建Web服务器实例
const server = http.createServer()
  • 为服务器实例绑定request事件,监听客户端请求

server.on可以处理多种事件,例如,request、connection、close等。

// 第一个参数是事件名称,第二个参数是回调函数
server.on('request', (req, res) => {
  console.log('Someone visit our webserver')
})
  • 调用服务器实例提供的.listen方法,启动当前服务器实例

server.listen中的回调函数,只在启动服务器的时候,调用一次。先绑定request事件的原因是,如果先启动了服务器实例,服务器可能已经开始接收请求,而无法对这些请求进行正确的处理。

server.listen(80, () => {
    console.log('http server running at http://127.0.0.1')
})
  • 另外,每次代码改动时,都需要重启服务器。

req请求对象

只要服务器收到了request请求,就会调用server.on()所绑定的回调函数。如果想要访问客户端的数据或者属性,可以用req请求对象。

  • req.url 请求地址(端口号后面/开始的字符串)
  • req.method 请求方式

res响应对象

如果想要访问客户端的数据或者属性,可以用res响应对象。

  • req.end() 响应一些内容,并且结束这次请求。

解决中文乱码问题

使用req.end()方法,向客户端发送请求如果带有中文的话,会出现乱码问题。因此,需要手动设置内容的编码格式。

server.on('request', (req, res) => {
  const str = `您请求的url地址是${req.url}, 请求的method类型是${req.method}`
  res.setHeader('Content-Type', 'text/html; charset=utf-8')
  res.end(str)
})

根据不同url响应不同html内容

在上述例子中我们发现,不论是访问127.0.0.1,还是访问127.0.0.1/index.html,都会有一样的响应内容。

const http = require('http')
const server = http.createServer()

server.on('request', function(req,res) {
    const url = req.url
    let content = '<h1>404 Not found!</h1>'           // 默认内容是404
    if (url === '/' ||  url === '/index.html') {
        content = '<h1>首页</h1>'                     // 用户请求首页
    }else if(url === '/about .html') {
      content = '<h1>关于页面</h1>'                    // 用户请求关于页面
    }
    res.setHeader('Content-Type', 'text/html; charset=utf-8')
    res.end(content)
}

server.listen(80, () => {
    console.log('http server running at http://127.0.0.1')
})

根据不同url,服务器读取不同文件

image.png

const http = require('http')
const fs = require('fs')
const path = require('path')

const server = http.createServer()

server.on('request', function(req,res) {
    const url = req.url
    // 拼接node所在目录和url地址,将请求地址映射为具体存放路径
    // 1. 一般实现方式
    // const fpath = path.join(__dirname, url)
    // 2. 实际应用时,访问/根目录,或者直接/index.html,不会输入clock目录
    let fpath = ''
    if(url === '/') {
        fpath = path.join(__dirname, './clock/index.html')
    }else {
        fpath = path.join(__dirname, './clock', url)
    }
    // 读取文件内容
    fs.readFile(fpath, 'utf8', (err, dataStr) => {
        if(err) return res.end('404 NOT FOUND')
        res.end(dataStr)
    })
    
}

server.listen(80, () => {
    console.log('http server running at http://127.0.0.1')
})

当访问/clock/index.html文件时候,由于外链了./index.css./index.js,会将/clock/index.html./index.css两个路径进行一次拼接。客户端再发送request请求,申请css和js文件。

第三部分 模块化

模块化

模块化是解决复杂问题时,自顶向下把系统拆分成若干模块的过程。提高代码的复用性、可维护性,方便按需加载。

模块根据来源分为三类:

  • 内置模块:Node.js官方提供的,如fs、path、http等
  • 自定义模块:用户创建的每个js文件,都是自定义模块
  • 第三方模块:使用前需下载

require方法

// 加载内置模块
const http = require('http')

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

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

例如,m1.js的文件如下:

console.log('导入m1文件')

1.js文件如下:

const m1 = require('./m1.js')
// 在使用require加在自定义模块的时候,可以省略.js后缀名,会自动补全
// const m1 = require('./m1')
// 此外,在加载模块时,如果只指定文件夹,则会找到文件夹下的package.json文件对应的main属性作为入口文件
console.log(m1)         
// 结果:{}

模块作用域和module.exports对象

函数作用域类似,自定义模块中的成员、变量等,只能在模块内进行访问。防止了全局变量污染的问题。

每个JS文件都内置一个module对象:

image.png

  • path: 当前存放路径
  • filename:文件名称
  • module:向外导出的对象

需要注意的是,require导入的成员,以module.exports指向的对象为准。

module.exports.username = 'zs'
module.exports.sayHi = () => {
    console.log('sayHello')
}

// module.exports指向新对象
module.exports = {
    name : 'ls',
    fn(){
        console.log('111')
    }
}

exports对象

为了简化导出,Node.js提供了一个exports对象。默认情况下, exportsmodule.exports 指向同一个对象。

exports.username = 'zs'
exports.sayHi = () => {
    console.log('sayHello')
}

最终共享结果以 module.exports 为准

  • 当给module.exports赋予新值的时候,exports和module.exports 可能指向不同对象

image.png

  • 当给exports赋予新值的时候,不会影响原本module.exports的值

image.png

  • exports和module.exports可同时作用于一个对象使其生效

image.png

image.png

CommonJS规范

Node.js遵循Common.js规范,规定:

  • 每个模块内部,module变量代表当前模块
  • module变量是一个对象,它的exports属性(即module.exports)是对外暴露的接口
  • 加载某个模块,其实是加载module.exports属性。require()方法用于加载模块。

包与npm

Node.js第三方模块又叫做包。基于内置模块封装,提供更高级、更方便的API。

npm提供全球最大的包共享平台可以检索所需要的包www.npmjs.com/ 并查看相关文档,下载包访问的服务器为registry.npmjs.org/ ,对外提供所有的包。

下载时,可以通过npm包管理工具,从服务器上下载所需要的包。包管理工具(node package management)不需要额外下载,随着Node.js自动安装到用户电脑上。

使用moment格式化时间

当我们新建Date对象时,获取的是一个带有时区和星期的javascript字符串:

const date = new Date()
console.log(date)
// VM210:2 Wed Jan 03 2024 14:25:42 GMT+0800 (中国标准时间)

如果想要以指定格式输出,需要自己编写format和补0函数。可以通过moment包解决:

const moment = require('moment')
// 调用moment方法,获取当前时间,调用.format()方法,对当前时间进行格式化
const dt = moment().format('YY-MM-DD HH:mm:ss')

npm命令

npm包的安装

npm install 完整包名

简写方式为:

npm i 完整包名

初次安装包之后,目录下会出现node_module文件夹,来存放所有已经安装的包,package-lock.json配置文件,记录node_module目录下每个包的下载信息,包括包的名字、版本号、下载地址等。

"node_modules/@babel/code-frame": { 
    "version": "7.23.5", 
    "resolved": "https://mirrors.tencent.com/npm/@babel/code-frame/-/code-frame-7.23.5.tgz", 
    "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", 
    "dev": true, "dependencies": { "@babel/highlight": "^7.23.4", "chalk": "^2.4.2" }, 
    "engines": { "node": ">=6.9.0" } 
},
// `resolved`字段显示了下载路径
// `integrity`字段则提供了一个用于验证包内容是否被篡改的校验和。如果下载的包内容与这个校验和不匹配,npm会报错,防止安装被篡改的包。
// `@babel/code-frame`的版本是`7.23.5`,并且它依赖于`@babel/highlight``chalk`。这些信息都会被记录在`package-lock.json`文件中,以确保未来安装时能够获得相同的依赖树。

安装指定版本的包

npm install 完整包名@版本号

安装新版本包的时候,不需要对原有包进行卸载,只需要下载,在package-lock.json中,新的命令便会覆盖旧的命令。

包的语义化管理规范: 版本号是以点分十进制表示的,例如2.4.22,第一位数字是大版本,涉及到底层的重新设计;第二位数字是功能版本;第三位数字是bug修复版本。只要前面的版本号增长了,后面的版本号都需要归0。

包管理配置文件

npm 规定,在项目根目录中,必须提供一个叫做 package.json 的包管理配置文件。用来记录与项目有关的一些配置信息。例如:

  • 项目的名称 版本号、描述等
  • 项目中都用到了哪些包
  • 哪些包只在开发期间会用到
  • 哪些包在开发和部署时都需要用到

多人协作时,第三方包往往有过大的体积,因此,共享源代码时,需要剔除node_modules文件夹(把node_modules添加到.gitignore忽略文件中)。可以在项目中,创建package.json文件,记录项目中安装了哪些包。

npm init -y

在创建一个新的node项目的时候,想要快速创建package.json文件,可以使用npm init -y命令。需要注意的是:

  • npm init -y只能在英文目录下运行,不能出现中文或者空格。
  • 当下载新包的时候,会自动把新包的名称和版本号记在package.json中。

package.json文件是在项目初始化时执行npm init -y命令会出现的,package-lock.json是第一次安装包执行npm install 包名出现,同时,还会出现node_modules文件夹。每次安装好package.json中dependencies节点还会出现相应的包名和版本号。

npm i

一次性下载所有依赖。

npm uninstall包名

卸载包并从dependencies中移除。

devDependencies节点

有一些包在项目开发的时候会用到,在项目上线的时候不会用到。安装这些包时,可以用下列指令:

npm install 包名 -D
npm install 包名 --save-dev

npm官网中,会给予相应的下载建议:

image.png

(例如,webpack就是开发会用到,但是上线不会用到的包)

下载包速度慢

由于下载包的服务器在国外,因此可以在淘宝的服务器上下载对应的包。淘宝的服务器每隔一段时间就会同步官方服务器上的包。镜像是磁盘映像副本,镜像源指的是服务器地址。

// 查看当前下载包的镜像源
npm config get registry
// 将镜像源设置为淘宝的镜像源
npm config set registry=https://registry.npm.taobao.org/

nrm工具

// 通过npm包管理器,把nrm安装为全局可用的工具
npm i nrm -g
// 查看所有的镜像源
nrm ls
// 将包下的镜像源切换为淘宝镜像
nrm use taobao

包的分类

项目包

被记录在node_module中的包,都是项目包。

  • 开发依赖包:被记录到devDependencies节点中,只会在开发过程中用到 npm install 包名 -D
  • 核心依赖包:被记录到dependencies节点中,开发和上线都会用到 npm install 包名

全局包

  • npm i 包名 -g下载,目录C:\Users\用户目录\VppDatalRoaming\npminode modules中。
  • npm uninstall 包名 -g 卸载

只有有工具性质的的包,才有全局安装的必要性。例如nrmi5ting_toc(将md文件转化为html的包)

包的结构

  • 包必须以单独的目录存在
  • 包的顶级目录下必须有package.json文件
  • package.json文件下有name,version,main三个属性,对应包的名字、包的版本、包的入口。

加载包的时候,会在package.json文件中,找到包的入口,加载入口文件。

发布包

初始化包的基本结构

  • 新建itheima-tools文件夹
  • 在文件夹中,新建
    • package.json
    • index.js
    • README.md
{
   "name": "itheima-tools",  // 真正的包名称,而非文件夹名称。并且不能和npm服务器上其他包名重复
   "version": "1.0.0",
   "main": "index.js",
   "description":“提供了格式化时间,HTMLEscape的功能",   // 在npm官网上搜索时的智能提示
   "keywords": ["itheima", "dateFormat", "escape"],   // 搜索关键字
   "license": "ISC"   // npm官方规定推荐ISC开源许可协议
}

image.png

包的模块化拆分

image.png 拆分成两个放入src文件夹里,在index用require进行导入,再用展开运算符进行导出。

在说明文档里,包含以下内容:安装方式、导入方式、格式化时间方法、转义HTML中的特殊字符方法、还原HTML中的特殊字符方法、开源协议。

包发布上npm

  • 注册npm账号
  • 在终端里面执行npm login命令,依次输入用户名、密码、邮箱(在运行npm login之前,需要把下包服务器地址切换为npm官方服务器,否则会导致发布包失败)
  • 终端切换到包的根目录,运行npm publish命令(包名不能雷同)
  • 在npm官网中登录,在packages中即可看见自己所发布的包
  • 运行npm unpublish 包名 --force,即可以从npm删除(72h内)已发布的包(且24h内不能重复发布)

模块的加载机制

优先从缓存中加载

模块在第一次加载后会被缓存。因此,多次调用require()不会导致模块的代码被执行多次。

// test.js
console.log('ok')
require('./test.js')
require('./test.js')
require('./test.js')
// 只会打印一次ok

内置模块的加载机制

内置模块是node.js官方提供的模块,加载优先级最高。例如,require('fs')始终返回内置的fs模块,即使在node_modules目录下有名字相同的包也叫fs。

自定义模块的加载机制

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

  • 按照确切的文件名进行加载
  • 补全.js的扩展名进行加载
  • 补全.json的扩展名进行加载
  • 补全.node的扩展名进行加载、
  • 报错:加载失败

第三方模块的加载机制

如果传递给require()的既不是内置模块,也不以./或者../开头,

  • Node.js会从当前模块的父级目录开始,尝试从/node_modules文件夹中加载第三方模块;
  • 移动到在上一层父目录中,进行加载,直到文件系统的根目录

目录作为模块的加载机制

把目录作为模块标识符,传递给require()的时候

  • 在被加载的目录下查找package.json,并寻找main属性
  • 如果没有package.json,或者main入口不存在或无法解析,加载目录下的index.js
  • 报错:Error: Cannot find module

第四部分 Express

Express

Express的作用和Node.js中http模块类似,都是专门用来创建Web服务的。本质是npm上第三方包。Express是基于内置的http模块进一步封装出来的,能够极大地提升开发效率。

对于前端程序员来说,服务器包括:

  • Web网站服务器
  • API接口服务器

使用Express创建基本的Web服务器

将express安装到项目中

npm i express@4.17.1
const express = require('express')
// 创建Web服务器
const app = express()
// 监听客户端的GET和POST请求,并向客户端响应具体内容
app.get('/user', (req, res) => {
    res.send({ name: 'zs', age: 20, gender: '男'})
})
app.post('/user', (req, res) => {
    res.send('请求成功')
})
// 启动Web服务器
app.listen(80, () => {
    console.log('express server running at http://127.0.0.1')
})

监听请求和处理参数

路由参数

路由参数分为两种: paramsquery

  1. 查询参数(Query Parameters)

    • 查询参数是附加在URL的查询字符串中的键值对。
    • 它们以问号(?)开始,后面跟随一个或多个键值对,键值对之间用&符号分隔。
    • 查询参数通常用于过滤、排序或指定某种形式的请求的特定数据。
    • 例如:http://www.example.com/api/users?search=张三&page=2
  2. 路由参数(Route Parameters)

    • 路由参数是URL路径中的一部分,用于标识资源的特定实例。
    • 它们在路由定义中以冒号(:)开头,表示该部分是动态的。
    • 路由参数通常用于指定要操作的特定资源的ID或名称。
    • 例如:http://www.example.com/api/users/:id,如果访问http://www.example.com/api/users/123,则id的值为123。:后面的动态参数名称不一定为id,且可以有多个,例如:/post/:year/:month/:day 在Vue.js中,你可以使用Vue Router来处理路由参数,并使用this.$route.params来访问这些参数。对于查询参数,你可以使用this.$route.query来访问。

例如,如果你有一个Vue组件,它通过Vue Router来处理路由/users/:id,并且该路由被访问为/users/123?search=张三&page=2,你可以在组件中这样获取这些参数:

1export default {
2  mounted() {
3    // 路由参数
4    const userId = this.$route.params.id; // "123"
5    
6    // 查询参数
7    const searchQuery = this.$route.query.search; // "张三"
8    const pageNumber = this.$route.query.page; // "2"
9    
10    // 使用这些参数进行后续操作...
11  }
12}

这样的参数设计使得URL能够更加灵活地表示不同的资源和操作,同时也便于前端应用与后端服务器之间的通信。

使用express框架处理参数

  • 监听GET请求
// res是请求对象,req是响应对象
app.get('/user', function(req, res){
    // req.query默认是一个空对象
    // 客户端使用?name=za&age=20这种查询字符串形式,发送到服务器的参数,可以用req.query访问到
    console.log(req.query.name, req.query.age)
})
// res是请求对象,req是响应对象
app.get('/user/:id', function(req, res){
    // req.params默认是一个空对象
    // 客户端使用了/users/:id这种形式,用req.params访问到动态参数
    console.log(req.params.id)
})
  • 监听POST请求
// res是请求对象,req是响应对象
app.post('请求URL', function(req, res){})
  • SEND发送客户端
app.get('/user', (req, res) => {
    // 向客户端发送 JSON 对象
    res.send({ name: 'zs', age: 20, gender: '男'})
})
app.get('/user', (req, res) => {
    // 向客户端发送文本内容
    res.send('请求成功')
})
  • 重启服务器

静态资源处理

托管静态资源

通过express.static(),就可以非常方便地创建一个静态资源服务器,将public目录下的图片、css文件、JS文件向外开放了。

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

这样,public将不会出现在访问路径中,而是直接把public目录下的资源暴露出来。例如,原本访问127.0.0.1/public/index.html,只用访问127.0.0.1/index.html

  • 可以托管多个静态资源文件夹
app.use(express.static('./public'))
app.use(express.static('./files'))

express.static()会通过目录的添加顺序,查找所需要的文件。因此,如果public和files都有index.html文件,会优先访问public中的。

挂载路径前缀

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

如果不使用express.static来托管静态文件,那么客户端(如浏览器)是无法直接通过HTTP请求来访问这些文件的。因为默认情况下,Express不会将你的文件系统中的文件暴露给外部访问,这是出于安全考虑。你需要明确告诉Express哪些文件是可以被外部访问的,这就是使用express.static中间件的原因。

不使用express.static的情况下,如果你想提供静态文件的访问,你将需要手动编写路由和处理函数来读取文件系统中的文件,并将文件内容作为HTTP响应返回给客户端。这样做不仅工作量大,而且容易出错,也不如使用express.static这样的专门设计来得安全和高效。

express.static('public') 是一个内置的中间件函数,用于在 Express 应用中提供静态文件服务。当你使用这个中间件时,它会自动处理对静态资源的请求,并将请求的文件发送给客户端。如果请求的文件在指定的目录中找到,中间件会处理请求并结束响应,不会调用 next() 函数,因为没有必要再继续执行后续的中间件或路由处理器。

然而,如果请求的文件没有在静态目录中找到,express.static 中间件会调用 next() 函数,将控制权传递给下一个中间件或路由处理器。这样做是为了允许请求继续在 Express 的中间件栈中流转,可能是为了处理404错误,或者是为了让其他路由处理器有机会处理该请求。

这里是一个简化的示例来说明这个过程:

1const express = require('express');
2const app = express();
3
4// 静态文件中间件
5app.use(express.static('public'));
6
7// 一个简单的中间件示例
8app.use((req, res, next) => {
9  console.log('这个中间件会在静态文件未找到时执行');
10  next();
11});
12
13// 404处理
14app.use((req, res, next) => {
15  res.status(404).send('未找到资源');
16});
17
18app.listen(3000, () => {
19  console.log('服务器正在监听端口3000');
20});

在这个例子中:

  1. 当请求一个存在于 public 目录中的静态文件时,express.static('public') 中间件会处理请求并直接发送文件,不会调用 next(),请求处理流程在此结束。
  2. 如果请求的静态文件不存在,express.static('public') 中间件会调用 next(),请求会继续流转到下一个中间件。
  3. 如果后续没有其他路由匹配该请求,最后的中间件(404处理)会被执行,返回一个404错误响应给客户端。

nodemon

在编写调试node.js项目的时候,如果有代码更新,需要手动关掉和重启项目。使用nodemon可以帮助我们重启项目。

  • 安装nodemon
npm i -g nodemon
  • 使用nodemon 之前,需要使用node来启动项目,每次更新之后,都需要重启node app.js 使用nodemon自动重启:node nodemon.js

路由

路由,就是映射关系。EXPRESS路由格式为:app.METHOD(URL, HANDLER)

// 精确匹配路径
app.get('/', function(req, res) => {
})

客户端发起的请求,到达服务器之后,首先要经过路由匹配,匹配成功之后,才会调用对应的处理函数。 在匹配时,会按照路由顺序进行匹配,当请求方法和URL都匹配成功之后,就会将这次请求交给function处理函数处理。

使用路由最简单的方式,就是挂载到app上。例如:

const express = require('express')
// 创建Web服务器
const app = express()
// 监听客户端的GET和POST请求,并向客户端响应具体内容
app.get('/user', (req, res) => {
    res.send({ name: 'zs', age: 20, gender: '男'})
})
app.post('/user', (req, res) => {
    res.send('请求成功')
})
// 启动Web服务器
app.listen(80, () => {
    console.log('express server running at http://127.0.0.1')
})

但随着时间的增长,会发现挂载到app实例上的越来越多,文件越来越大。

路由的模块化

通过自定义的路由模块,可以更好地对路由进行管理。

  • 创建路由模块对应的.js文件
  • 调用express.Router()来创建路由对象
  • 向路由对象上挂载具体的路由
  • 使用module.exports共享路由对象
  • 使用app.use()注册路由模块

router.js

const express = require('express')
const router = express.Router()

// 创建Web服务器
const app = express()
// 监听客户端的GET和POST请求,并向客户端响应具体内容
router.get('/user', (req, res) => {
    res.send({ name: 'zs', age: 20, gender: '男'})
})
router.post('/user', (req, res) => {
    res.send('请求成功')
})

module.exports = router

app.js

const userRouter = require('./router/user.js')
// app.use()注册全局中间件
// app.use(userRouter)
// 添加访问前缀
app.use('/api', userRouter)

中间件

当一个请求到达Express服务器之后,可以连续调用多个中间件,对这次请求进行预处理。包含next是中间件函数,路由处理函数只包含req和res。

image.png

全局生效的中间件

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

const express = require('express')

const app = express()

const mw = function(req, res, next){
    console.log('中间件')
    next()
}

app.use(mw)

app.get('/user', (req, res) => {
    res.send({ name: 'zs', age: 20, gender: '男'})
})
app.post('/user', (req, res) => {
    res.send('请求成功')
})

app.listen(80, () => {
    console.log('express server running at http://127.0.0.1')
})

中间件的作用

多个中间件之间,共享一份req和res,因此,可以在上游的中间件中,统一为req或者res对象添加自定义的属性和方法,供下游的中间件或者路由使用。

定义多个全局中间件

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

局部生效的中间件

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

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

app.get('/', mw1, function(req, res) {
    res.send('HomePage')
})

中间的参数是中间件函数。

定义多个局部中间件

app.get('/', mw1, mw2, (req, res) => {
    res.send('HomePage')
})

app.get('/', [mw1, mw2], (req, res) => {
    res.send('HomePage')
})

中间件注意事项

  • 一定要在路由之前注册中间件
  • 客户端发过来的请求,可以连续调用多个中间件进行处理
  • 执行完中间件的业务代码之后,不要忘记调用next()函数
  • 为了防止代码逻辑混乱,调用next()之后不要写额外的代码
  • 连续调用多个中间件时,多个中间件之间,共享req和res对象

中间件分类

应用级别的中间件

绑定到app实例上的中间件,叫做应用级别的中间件。

app.use((req, res, next) => {
    next()
})
app.get('/', mw1, (req, res) => {
})

路由级别的中间件

绑定到路由实例上的中间件,叫做路由级别的中间件。

const app = express()
const router = express.Router()

router.use((req, res, next) => {
    next()
})
app.use('/', router)

一些区别:

  • app.get('/')用于精确匹配根路径的GET请求,而app.use('/')用于匹配所有请求,这是因为在中间件的上下文中,/作为路径前缀实际上是一个通配符,它表示匹配所有路径。
  • app.use()方法的第一个参数确实可以看作是路径匹配和添加前缀的双重角色。与中间件函数使用时候,中间件将会对所有以该路径前缀开始的请求生效。与路由器(Router)实例一起使用时,第一个参数指定的路径前缀实际上是在为该路由器内定义的所有路由添加一个基础路径。
app.use('/', mw); // 对所有请求生效,因为所有路径都以'/'开始
app.use('/api', userRouter); // 所有userRouter的路由现在都有了'/api'前缀
app.get('/') //这是一个路由定义,它会精确匹配根路径`/`的GET请求。只有当请求的路径完全等于`/`时,这个路由处理器才会被调用。它不会匹配任何其他路径,如`/about``/contact`。
app.use('/') //这是一个中间件定义,它会匹配所有以`/`开始的请求路径,实际上就是所有的请求路径,因为所有的路径都以`/`开始。因此,这个中间件会对所有的请求生效

错误级别的中间件

捕获整个项目中发生的异常错误,从而防止项目异常崩溃function(err, req, res, next)。错误级别中间件必须注册在所有路由之后。

app.get('/', function(req, res) => {
    throw new Error('服务器内部发生错误')  // 抛出一个自定义的错误,项目崩溃
    res.send('Home Page')
})
app.use(function (err, req, res, next) { // 错误级别的中间件,使用之后项目不会崩溃,而是正常运行
    console.log('发生了错误' + err.message)
})

Express内置的中间件

  • express.static 快速托管静态资源内置中间件
  • express.json 解析JSON格式的请求体数据
  • express.unlencoded 解析URL-encoded格式的请求体数据
const express = require('express');
const app = express();

// 使用express.json()中间件来解析JSON格式的请求体
app.use(express.json());
// 使用express.urlencoded()中间件来解析urlencoded格式的请求体
app.use(express.urlencoded({ extended: false }));

// 发送JSON格式的body
app.post('/your-endpoint', (req, res) => {
  // 默认情况下,如果不配置解析表单数据的中间件,则req.body默认等于undefined
  console.log(req.body);
  res.send('数据已接收');
});

// 发送x-www-form-urlencoded格式的body
app.post('/book', (req, res) => {
  console.log(req.body)
  res.send('ok');
});

app.listen(3000, () => {
  console.log('服务器正在监听端口3000');
});

第三方中间件

第三方开发出来的,可以按需下载并配置的第三方中间件。

  • 运行npm install body-parser 安装第三方中间件
  • 使用require导入中间件
  • 使用app.use()注册和使用中间件

自定义中间件

  • 需求:手动模拟一个类似于express.urlencoded中间件,解析POST提交到服务器的表单数据
  • 步骤:
    • 定义中间件
    • 监听req的data事件:在中间件中,需要监听req对象的data事件,来获取客户端发送到服务器的数据。如果数据量比较大,无法一次性发送完毕,则客户端会把数据切割之后,分批发送到服务器。所以data事件可能会触发多次,每一次触发data事件时,获取的数据只是完整数据的一部分,需要手动对接收到的数据进行拼接。
    • 监听req的end事件:当请求体数据接收完毕之后,会自动触发end事件,拿到并处理完整的请求体数据。
    • 使用querystring模块解析请求体数据
    • 将解析出来的数据对象挂载为req.body:上游的中间件和下游的中间件和路由之间,共享一份req和res,因此,可以将解析出来的数据,挂载为req的自定义属性,命名为req.body。
    • 将自定义中间件封装为模块
// custom-body-parser.js 
const qs = require('querystring')
mudule.exports = (res, req, next) => {
    let str = ''
    // 监听data事件
    req.on('data', (chunk) => {
        str += chunk
    })
    // 监听end事件
    req.on('end', () => {
        console.log(str)
        // 把字符串形式的请求体数据,解析成对象格式
        const body = qs.parse(str)
        console.log(body)
        req.body = body
        next()
    })
})
const myBodyParser = require('custom-body-parser')
app.use(myBodyParser)

创建接口

  • 创建基本服务器
//导入express 模块
const express = require('express")

//创建 express 的服务器实例
const app = express()

const apiRouter = require('./apiRouter. js')
app.use(express.urlencoded({extended: false))
app.use('/api', apiRouter)

//调用app.1isten 方法,指定端口号井启动web服务器
app.listen(80, function (){
    console. log('Express server running at http: //127.0.0.1')
}
  • 创建API路由模块
// apiRouter.js【路由模块】
const express = require('express')
const apiRouter = express.Router( )

apiRouter.get('/get', (req, res) => {
    const query = req.query
    res.send({
        status: 0,
        msg: 'GET请求成功',
        data: query
    })
})

apiRouter.post('/post', (req, res) => {
    const =body = req.body
    res.send({
        status: 0,
        msg: 'POST请求成功',
        data: body
    })
})

module.exports = apiRouter

基于cors解决接口跨域问题

在本地新建html文件,并且调用https://127.0.0.1/api/get 接口,会发现,存在跨域问题。本地html文件是file://协议,而接口是https协议访问服务器上的资源。当我们只是想要快速查看一个静态的HTML文件,没有与服务器端交互的要求,可以选择直接打开本地文件。

在实际开发过程中,主流的解决方案有CORS和JSONP,其中JSONP只支持GET请求。cors是Express的一个第三方中间件,可以很方便地解决跨域问题。

  • 运行npm i cors安装中间件
  • 使用const cors = require('cors')导入中间件
  • 在路由之前调用app.use(cors())配置中间件
//导入express 模块
const express = require('express")

//创建 express 的服务器实例
const app = express()
const cors = require('cors')
const apiRouter = require('./apiRouter. js')
// 帮助所有的请求都跨域
app.use(cors())
app.use(express.urlencoded({extended: false))
app.use('/api', apiRouter)

//调用app.1isten 方法,指定端口号井启动web服务器
app.listen(80, function (){
    console. log('Express server running at http: //127.0.0.1')
}

经过配置后,就不会产生接口跨域问题

image.png

cors跨域资源共享

CORS(Cross-Origin Resource Sharing, 跨域资源共享)由一系列HTTP响应头组成,这些HTTP响应头决定浏览器是否组织前端JS代码跨域获取资源。
浏览器的同源安全策略默认会阻止网页的"跨域"获取资源。但如果接口服务器配置了CORS相关的HTTP响应头,就可以解除浏览器端的跨域访问限制。

image.png

  • CORS主要是在服务器端进行配置,客户端浏览器无需做任何额外的配置,即可请求开启CORS接口。
  • CORS在浏览器中有兼容性。只有支持XMLHttpRequest2的浏览器,才能正常访问开启了CORS的服务端接口。(IE10+、Chrome4+、FireFox3.5+)

cors相关的三个响应头

Access-Control-Allow-Origin

Access-Control-Allow-Origin: <origin> | *

其中,origin参数的值指定了允许访问该资源的外域URL。下面这行代码中,即指定了只允许来自itcast.cn 的请求。

res.setHeader('Access-Control-Allow-Origin': 'http://itcast.cn')

Access-Control-Allow-Headers

默认情况下,CORS仅支持客户端向服务器发送9个请求头:

Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width、Content-Type(值仅限于text/plain、mutipart、from-data、application/x-www-form-urlencoded三者之一)

如果客户端向服务器发送了额外的请求头信息,则需要在服务器端,通过Access-Control-Allow-Headers 对额外的请求头进行声明,否则这次请求会失败

Access-Control-Allow-Methods

默认情况下,CORS仅支持客户端发起GET、POST、HEAD请求。
如果客户端希望通过PUT、DELETE等方式请求服务器的资源,则需要在服务器端,通过Access-Control-Alow-Methods来指明实际请求所允许使用的HTTP方法。

//只允许POST,GET,DELETE,HEAD请求方式
res.setHeader('Access-Control-Alow-Methods','POST,GET,DELETE,HEAD')
//允许所有的HTTP请求方法
res.setHeader('Access-Control-Alow-Methods','*')

cors的简单请求和预检请求

客户端在请求CORS借口时,根据请求方式和请求头的不同,可以讲CORS的请求分为两大类,分别是:

简单请求

同时满足以下两大条件的请求,就属于简单请求:

  • 请求方式:GET、POST、HEAD三者之一
  • HTTP头部信息不超过以下几种字段: 无自定义头部字段、Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width、Content-Type(只有三个值application/x-www-form-urlencoded、multipart/form-data、text/plain)

预检请求

只要符合以下任何一个条件的请求,都需要进行预检请求:

  • 请求方式为GET、POST、HEAD、之外的请求Method类型
  • 请求头中包含自定义头部字段 = 向服务器发送了application/json格式的数据 在浏览器与服务器正式通信之前,浏览器会先发送OPTION请求进行预检,以获知服务器是否允许该实际请求,所以这一次的OPTION请求成为“预检请求”。服务器成功响应预检请求后,才会发送真正的请求,并且携带真实数据。 image.png

编写jsonp接口

  • JSONP不属于真正的Ajax请求,因为它没有使用XMLHttpRequest这个对象。
  • JSONP仅支持GET请求,不支持POST、PUT、DELETE等请求。

如果服务器端没有特别设计支持JSONP,那么客户端不能使用JSONP来请求这些资源。如果尝试使用JSONP,会遇到问题。

如果项目中已经配置了CORS跨域资源共享,为了防止冲突,必须在配置CORS中间件之前声明JSONP的接口。否则JSONP接口会被处理成开启了CORS的接口。这是因为中间件在Web框架中通常按照声明的顺序执行,而且一旦一个中间件处理了请求并发送了响应,后续的中间件通常不会再执行。

如果你先配置了CORS中间件,它会在所有进入的请求上设置CORS相关的HTTP头部,如Access-Control-Allow-Origin。如果JSONP接口的请求也被CORS中间件处理了,那么即使你的意图是使用JSONP来绕过CORS限制,响应中也会包含CORS头部。

//导入express 模块
const express = require('express")

//创建 express 的服务器实例
const app = express()
const cors = require('cors')

// 优先创建JSONP接口,这个接口不会被处理成Cors
app.get('/api/jsonp', (req, res) => {
    
})
// 帮助所有的请求都跨域
app.use(cors())
app.use(express.urlencoded({extended: false))
// 这是开启了Cors接口
app.get('/api/get', (req, res) => {
})

//调用app.1isten 方法,指定端口号井启动web服务器
app.listen(80, function (){
    console. log('Express server running at http: //127.0.0.1')
}

jsonp接口实现

  • 获取客户端发送的回调函数名字
  • 得到要通过JSONP形式发送给客户端的数据
  • 根据前两步得到的数据,拼接出一个函数调用的字符串
  • 把上一步拼接得到的字符串,响应给客户端的<script>标签进行解析执行
  • 调用ajax函数,提供JSONP的配置选项,从而发起JSONP请求
app.get('/api/jsonp', (req, res) => {
    const func = req.query.callback
    const data = { name:'za' }
    const scriptStr = `${func}(${JSON.stringify(data)})`
    res.send(scriptStr)
})
image.png