Node学习资料文档

Node.js

FS文件系统模块:

fs.readFile()

  1. 语法格式。

    fs.readFile(path[,options],callback)
    //参数一:必传,字符串,表示文件的路径
    //参数二:可选参数,表示以什么编码格式来读取文件
    //参数三:必传参数,文件读取完成后,通过回调函数拿到读取的结果
    
  2. 实例代码

    console.log("使用fs.readFile()读取文件内容:")
    //导入fs模块,来操作文件
    const fs=require('fs')
    //调用fs.readFile()方法来读取文件
    fs.readFile("a.txt","UTF-8",function(err,data){
    	//如果文件读取失败,err是一个错误对象。读取文件成功返回null
    	if(err){
    		return console.log("读取文件失败:"+err.message)
    	}
    	//如果文件读取成功返回文件内容
    	console.log(data)
    })
    

fs.writeFile()

  1. 语法格式
    使用fs.writeFile()方法,可以向指定的文件中写入内容,语法格式如下:

    fs.writeFile(file,data[,options],callback)
    //参数一:必传参数,需要指定一个文件路径的字符串,表示文件的存放路径
    //参数二:必传参数,表示要写入的内容
    //参数三:可选参数,表示以什么格式写入文件内容,默认是utf8
    //参数四:必传参数,表示文件写入完后都会执行回调函数
    
  2. 实例代码

    console.log("使用fs.writeFile()往文件写入内容:")
    //导入fs模块,来操作文件
    const fs=require('fs')
    //调用fs.writeFile()方法来往文件中写入内容。文件不存在时会创建一个文件,路径不存在出错。只能创建文件不能创建文件夹
    //重复使用fs.writrFile()往一个文件中写入,新写入的会覆盖原来的文件内容
    fs.writeFile("a.txt","哈哈哈哈","UTF-8",function(err){
    	//如果文件文件写入成功,则err的值为null.写入失败err则是一个错误对象
    	if(err){
    		return console.log("往文件写入内容失败:"+err.message)
    	}
    	console.log("文件写入成功!")
    })
    

fs操作文件示例

//引入fs模块,操作文件
const fs=require('fs')
//调用fs.readFile()方法来读取文件
fs.readFile("a.txt","UTF-8",function(err,data){
	//如果文件读取失败,err是一个错误对象。读取文件成功返回null
	if(err){
		return console.log("读取文件失败:"+err.message)
	}
	//如果文件读取成功返回文件内容
	console.log(data)
	//先把读取到的数据按照空格分割
	const arrOld=data.split(" ");
	const arrNew=[];
	arrOld.forEach(item=>{
		//对数组中的=替换为:
		arrNew.push(item.replace("=",":"))
	})
	//对数组中的
	const arrStr=arrNew.join("\r\n")
	//写入文件中
	fs.writeFile("a.txt",arrStr,"UTF-8",function(err){
		//如果文件文件写入成功,则err的值为null.写入失败err则是一个错误对象
		if(err){
			return console.log("往文件写入内容失败:"+err.message)
		}
		console.log("文件写入成功!")
	})
})

fs路径动态拼接问题

在使用fs模块操作文件时,如果提供的操作陆是以./或…/开头的相对路径时,很容易出现路径动态拼接错误的问题。

原因:代码在运行的时候,会以执行node命令时所处的目录,动态拼接出被操作的文件的完整路径。

//出现路径拼接错误问题,使用为提供了./或../开头的相对路径。
//可以提供一个完整的路径,不适用./或../相对路径
//但是移植性非常差,不利于维护
//使用 __dirname 表示当前文件所处的目录
console.log(__dirname)

path路径模块

path模块时Node.js官方提供的,用来处理路径的模块。它提供了一系列的方法和属性,用来满足用户对路径的处理需求。

  • path.join()方法,用来将多个路径片段拼接成一个完整的路径字符串。
  • path.basename()方法,用来从路径字符串中,将文件名解析出来。
  • path.extname()方法,用来从路径字符串中,将文件后缀名解析出来。

如果要在JavaScript代码中,使用path模块来处理路径,则需要使用如下的方式先导入它。

const path = require("path")
//引入path模块,操作路径
const path=require('path')
const pathStr=path.join('/a','/b/c','../','/d/e');
console.log(pathStr)

const pathStr2=path.join(__dirname,"a.txt")
console.log(pathStr2)
//使用path.dasename()方法,可以获取路径中的最后一部分,经常通过这个方法获取路径中的文件名。
const name=path.basename(pathStr2,".txt")
//第一个参数:必传参数,表示一个路径的字符串
//第二个参数:可选参数,表示文件扩展名,如果传了第二个参数,就会在最后得到的String中去掉后缀
//返回:路径字符串中的最后一部分
console.log(name)
//通过path.extname()方法获取文件的后缀名
console.log(path.extname(pathStr2))

今后凡是涉及到路径拼接的操作,都要使用path.join()方法进行处理。不要直接使用+进行字符串的拼接。

Http模块

http模块时Node.js官网提供的,用来创建web服务器的模块。通过http模块提供的http.createServer()方法,就能方便的把一台普通的电脑,变成一台web服务器,从而对外提供web资源服务。

如果要希望使用http模块来创建web服务器,则需要先导入它:

const http = require("http")

创建web服务器

  • 导入http模块
  • 创建web服务器实例
  • 为服务器实例绑定request事件,监听客户端的请求
  • 启动服务器

代码示例:

//导入http模块
const http = require("http")
//创建web服务实例
const server=http.createServer();

//为服务器实例绑定request事件,监听客户端的请求
server.on("request",function(req,res){
	console.log("Someone visit our web server.")
	//req是请求对象,包含了与客户端相关的数据和属性
	//req.url 是客户端请求的url地址
	const url=req.url;
	console.log("请求的url地址为:"+url)
	//req.method 是客户端请求的 method 类型
	const method=req.method;
	console.log("请求的method类型为:"+method)
	//res是响应对象,它包含了与服务器相关的数据和属性
	const str="你请求的地址是"+url+",你请求的method类型为:"+method;
	res.end(str)
})

//调用server.listen(端口号,cb回调)方法,即可启动web服务
server.listen(8080,()=>{
    console.log("WEB Server is runing onLine 127.0.0.1:8080")
})

http模块中解决中文乱码问题:

//设置响应头,解决中文乱码问题
res.setHeader("Content-Type","text/html;charset=utf-8")

模块化

模块化是指解决一个复杂问题时,自顶向下逐层把系统划分成若干模块的过程。对于整个系统来说,模块是可组合、分解和更换的单元。

把代码进行模块化拆分的好处:

  • 提高了代码的复用性
  • 提高了代码的可维护性
  • 可以实现按需加载

Node.js中的模块化:

Node.js中根据模块的来源不同,将模块分为了3打雷,分别是:

  • 内置模块(内置模块是由Node.js 官方提供的,例如 fs、path、http等)
  • 自定义模块(用户创建的每个.js文件,都是自定义模块)
  • 第三方模块(由第三方开发出来的模块,嫔妃官方提供的内置模块,也不是用户创建的自定义模块,使用前需要先下载)
加载模块:

使用强大的require()方法,可以加载需要的内置模块、用户自定义模块、第三方模块进行使用,例如:

//加载内置fs模块
const fs=require("fs")
//加载用户自定义模块,不写.js后缀名也能加载用户自定义模块
const custom=require("./custom.js")
//加载第三方模块
const moment=require("moment")

注意:使用require()方法加载其他模块时,会执行被加载模块中的代码。

模块作用域:

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

模块作用域的好处:防止全局变量污染的问题。

module对象

在每个.js自定义模块中都有一个module对象,它里面存储了和当前模块有关的信息。

Module {
  id: '.',
  path: 'D:\\deskBook',
  exports: {},
  filename: 'D:\\deskBook\\a.js',
  loaded: false,
  children: [],
  paths: [ 'D:\\deskBook\\node_modules', 'D:\\node_modules' ]
}

在自定义模块中,可以使用module.exports对象,将模块内的成员共享出去,供外界使用。

外界用require()方法导入自定义模块时,得到的就是module.exports所指向的对象。

//向module.exports对象上挂载username属性
module.exports.username="zs"
//向module.exports对象上挂载hello方法
module.exports.hello=function(){
    consloe.log("hello方法打印")
}

module.exports={
    name="ls"
    ha()=>{
        consloe.log("he方法打印")
    }
}

使用require()方法导入模块时,导入的结果,永远以module.exports指向的对象为准。

由于module.exprots单词写起来比较复杂,为了简化向外共享成员的代码,Node提供了exports对象,默认情况下,exporte和module.exports指向同一个对象。最终共享的结果,还是以module.exports指向的对象为准。

npm包可以通过网站:www.npmjs.com 来搜索任何需要的包。还提供了一个下载地址 https://registry.npmjs.org/ 可以通过这个服务器下载所有需要的包。

在项目中安装npm包

npm install 包的完整名称
rem 可以使用简写
npm i 包的完整名称
rem 默认使用该命令下载的包是最新的包,可以在命令后面追加@版本号
  • 使用npm包管理工具,在项目中安装格式化时间的包 moment
  • 使用 require() 导入格式化时间的包
  • 参考 moment 的官方API 文档对时间进行格式化
const moment = require("moment")
moment().formant("YYYY-MM-DD HH-mm-SS")
快速创建package.json

npm包管理工具提供了一个快捷命令,可以在执行命令时所处的目录中,快速创建package.json这个包管理配置文件。

//作用:在执行命令所在处的目录中,快速新建 package.json 文件,目录路径只能在英文路径下
npm init -y

运行npm install命令安装包的时候,npm包管理工具会自动把包的名称和版本号,记录到package.json中。

一次性安装所有的包

可以运行 npm install命令(或npm i)一次性安装所有的依赖包:

//执行 npm install 命令时,npm包管理会先读取 package.json 中的 dependencies 节点。
//读取到记录的所有依赖包名称和版本号之后,npm 包管理工具会把这些包一次性下载到项目中
npm install
从项目中卸载包

可以运行 npm uninstall 命令,来卸载指定的包:

//使用 npm uninstall 具体包名 来卸载包
npm uninstall moment

注意:npm uninstall 命令执行成功后,会把卸载的包,自动从package.json的dependencies 中移除掉

如果某些包只是在项目开发阶段会用到,在项目上线之后不会用到,则建议把这些包记录到devDependencies节点中。与之对应的,如果某些包在开发和项目上线之后都需要用到,则建议把这些包记录到dependencies节点中。

你可以使用如下命令,将包记录到devDependencies节点中:

//安装指定的包,并记录到devDependencies节点中
npm i 包名 -D
//注意:上述命令是简写形式,等价于下面完整的写法
npm install 包名 --save-dev
解决下包速度慢的问题

使用淘宝npm镜像服务器。

切换npm下包镜像源

# 默认下载镜像源:https://registry.npmjs.org/
# 查看当前的下包镜像源
npm config get registry
# 将下包的镜像源切换为淘宝镜像源
npm config set registry=https://registry.npm.taobao.org/
# 检查镜像源是否下载成功
npm config get registry
nrm工具

为了更方便的切换下包的镜像,我们可以安装nrm这个小工具,利用nrm提供的终端命令,可以快速查看和切换下包的镜像源。

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

使用npm安装的包可用分为两大类:项目包和全局包

那些被安装到项目的 node_modules 目录中的包,都是项目包。

项目包又分为两类:开发依赖包和核心依赖包。

开发依赖包(被记录到devDependencies 节点中的包,只在开发期间会用到)

核心依赖包(被记录到dependencies 节点中的包,在开发期间和项目上线之后都会用到)

在执行npm install 命令是,如果提供了 -g 参数,则会把包安装为全局包

# 安装全局包
npm i 包名 -g
# 卸载全局安装包
npm uninstall 包名 -g

只有工具性质的包,才有全局安装的必要性。因为他们提供了好用的终端命令。

判断某个包是否需要全局安装后才能使用,可用参考官方提供的使用说明即可。

i5ting_toc

i5ting_toc是一个可用包md文档转为html页面的小工具:

# 全局安装i5ting_toc
npm install -g i5ting_toc
# 调用 i5ting_toc,轻松使用 md 转 html 的功能
i5ting_toc -f 要转换的md文件路径 -o
规范的包结构

在清楚了包的概念,以及如何下载和使用包之后,接下来我们深入了解一下包的内部结构。

一个规范的包,它的组成结构,必须符合一下3个要求:

  • 包必须以单独的目录而存在
  • 包的顶级目录下要必须包含 package.json 这个包管理配置文件
  • package.json 中必包含 name、version、main 这三个属性,分别代表包的名称、版本号、包的入口。

更多规范和约束,可以参考如下网址:

https://yarnpkg.com/zh-Hans/docs/package-json

开发属于自己的包

  • 新建Tool文件夹,作为包的根目录
  • 在Tool文件夹中,新建如下三个文件:
    • package.json(包管理配置文件)
    • index.js(包的入口文件)
    • README.md(包的说明文档)

初始化package.json文件

{
    "name":"Tools",//包名称
    "version":"1.0.0",//包的版本号
    "main":"index.js",//包的入口文件
    "description":"提供了格式化时间,HTMLEscape的功能",//包的简介
    "Keywords":["itheima","dateFormat","escape"],//搜索的关键字
    "license":"ISC"//包遵循的开源许可协议
}

转义HTML的方法。在index.js文件中定义

//转义HTML的方法
function htmlEscape(htmlStr){
	return htmlStr.replace(/<|>|"|&/g,(match)=>{
		switch(match){
			case '<':
			 return '<'
			case '>':
			 return '>'
			case '"'
			 return '"'
			case '&':
			 return '&'
		}
	})
}

//还原HTML的方法
function htmlUnEscape(str){
	return str.replace(/<|>|"|&/g,(match)=>{
		switch(match){
			case '<':
			 return '<'
			case '>':
			 return '>'
			case '"'
			 return '"'
			case '&':
			 return '&'
		}
	})
}

//向外暴露需要的成员
module.exports={
	htmlEscape,htmlUnEscape
}

ES6新特性:...对象名 在一个对象中使用 ...另一个对象 表示把另一个对象的属性 全部添加到该对象中。

注册登录npm
//建议先检查当前下载npm下载包的服务器地址是否是npm官方服务器
npm login 登录名 密码 邮箱

在运行npm login 命令之前,必须先把下包的服务器地址切换为npm的官方服务器。否则会导致发布包失败!

发布包

在登录了用户时,将终端切换到包的根目录之后,运行 npm publish 命令,即可将包发布到npm上(注意:包名不能重复)。

删除已发布的包

运行 npm unpublish 包名 --force 命令,即可从npm删除已发布的包。

通过该命令只能删除72小时以内发布的包,使用该命令删除的包,在24小时内不允许重复发布。发布包的时候要慎重,尽量不要往npm上发布没有意义的包。

模块加载机制

模块在第一次加载后会被缓存。这也意味着多次的require() 不会导致模块的代码被执行多次。

注意:不论是内置模块、用户自定义模块、还是第三方模块,他们都会优先从缓存中加载,从而提高模块的加载效率。

内置模块的加载机制:

内置模块是由Node.js 官方提供的模块,内置模块的加载优先级最高。

例如:require(“fs”) 始终放回内置的fs模块,即使在node_modules目录下有名字相同的包也叫做fs。

自定义模块的加载机制:

使用require() 加载自定义模块是,必须指定义 ./ 或 …/这样的路径标识符,则 node 会把它当作内置模块或第三方模块进行加载。

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

  • 按照确切的文件名进行加载
  • 补全 .js 扩展名进行加载
  • 补全 .json 扩展名进行加载
  • 补全 .node扩展名进行加载
  • 加载失败,终端报错
第三方模块加载机制:

如果传递给require()的模块标识符不是一个内置模块,也没有以 ‘./’ 或 ‘…/’ 开头,则Node.js 会从当前模块的父目录开始,尝试从 /node_modules 文件夹中加载第三方模块。

如果没有找到对应的第三方模块,则移动到再上一层父目录中,进行加载,直到文件系统的根目录。

目录作为模块:

当把目录作为模块标识符,传递给require()进行加载的时候,有三种加载方式。

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

Express

什么是Express:

官方给出的概念:Express是基于Node.js 平台,快速、开放、极简的Web开发框架。

通俗的理解:Express 的作用和Node.js 内置的 http模块类似,是专门用来创建Web 服务器的。

Express 的本质:就是一个 npm 上的第三方包,提供了快速创建Web服务器的便捷方法。

Express的中文官网:http://www.expressjs.com.cn/

创建服务器

//导入 express
const express = require("express")
//创建web服务器
const app = express()
//启动服务器
app.listen(80, () => {
    console.log("express server is running port 80")
})

监听请求

通过 app.get()方法,可以监听客户端GET 请求,具体的语法格式如下:

//监听客户端的GET请求
//参数一:客户端请求的url地址
//参数二:请求对应的处理函数
//       req:请求对象(包含了于请求相关的属性和方法)
//       res:响应对象(包含了于响应相关的属性和方法)
app.get("/", function (req, res) {
    //通过res.send() 方法可以把数据发送给客户端
    res.send("")
})

通过 app.post()方法,可以监听客户端POST 请求,具体的语法格式如下:

//监听客户端的GET请求
//参数一:客户端请求的url地址
//参数二:请求对应的处理函数
//       req:请求对象(包含了于请求相关的属性和方法)
//       res:响应对象(包含了于响应相关的属性和方法)
app.post("/login", function (req, res) {
    //通过res.send() 方法可以把数据发送给客户端
    res.send("")
})

获取参数:

获取Url中携带的查询参数

通过req.query对象,可以访问到客户端通过查询字符串的形式,发送到服务器的参数。

app.get("/", function (req, res) {
    //通过res.send() 方法可以把数据发送给客户端
    res.send("通过Get方法,请求了/地址")
    //通过req.query对象获取到地址栏中传递的参数:?name=zs&age=20
    //默认req.query是空对象
    console.log(req.query)
})
获取URL中的动态参数

通过req.params 对象,可以访问到URL中,通过:匹配的动态参数。(rest接口)

app.get("/user/:id", function (req, res) {
    //通过res.send() 方法可以把数据发送给客户端
    res.send("通过Get方法,请求了/user/:id地址")
    //req.params 是动态匹配到的 URL 参数,默认是一个空对象。rest请求风格
    console.log(req.params)
})

//请求地址:http://localhost/user/12
//打印出结果为:{ id: '12' }

托管静态资源

express.static()

express 提供了一个非常好用的函数,叫做 express.static(),通过它,我们可以非常方便地创建一个静态资源服务器,例如:通过如下代码就可以将 public 目录下的图片,css文件,javascript文件对外开发访问了。

//静态资源托管
//该静态资源的Tool 需要在Node 执行的js文件同级目录,请求Tool中的静态资源时不需要带Tool,只需在地址栏路径中写:http:ip:prot/{Tool中的资源名称}
app.use(express.static("Tool"))

//托管多个静态资源目录,如果两个目录都有共同的文件,请求时会根据先执行的app.use中查找对应的静态资源
app.use(express.static("files"))
挂载路径的前缀

如果希望在托管的静态资源访问路径之前,挂载路径前缀,则可以使用如下方式:

//第一个参数代表请求这个静态资源时要携带的地址栏前缀
app.use("/Tool",express.static("Tool"))

nodeMon

在编写调试 Node.js 项目的时候,如果修改了项目代码,则需要频繁的手动 close 掉,然后再重新启动,非常繁琐。

现在,我们可以使用 nodemon (https://www.npmjs.com/package/nodemon) 这个工具,它能够监听项目文件的变动,当代码被修改后,nodemon 会自动帮我们重启项目,极大方便了开发和调试心

安装nodemon
//全局安装 nodemon
npm install -g nodemon
使用nodemon

当基于Node.js编写了一个网站应用的时候,传统的方式,是运行node app.js 命令,来启动项目。这样做的坏处是:代码被修改之后,需要手动重启项目。

现在,我们可以将node命令替换为nodemon命令,使用 nodemon app.js 来启动项目。这样做的好处是:代码被修改之后,会被 nodemon 监听到,从而实现自动重启项目的效果。

//使用nodemon启动项目,在更改项目中的文件时,保存会自动重启项目
nodemon index.js

路由

在Express 中,路由指的是客户端的请求与服务器处理函数之间的映射关系。

Express 中的路由分3部分组成,分别是请求的类型、请求的URL地址、处理函数,格式如下:

//METHOD:请求类型
//PATH:请求的URL地址
//HANDLER:处理函数
app.METHOD(PATH,HANDLER)
路由匹配过程

每当一个请求到达服务器之后,需要先经过路由的匹配,只有匹配成功之后,才会调用对应的处理函数。

在匹配时,会按照路由的顺序进行匹配,如果请求类型请求的URL同时匹配成功,则 Express 会将这次请求,转交给对应的function函数进行处理。

路由最简单的使用方法:
app.get("/", (req, res) => {res.send("通过Get方法,请求了/地址")})

app.post("/login", (req, res) => {res.send("通过Post方法,请求了/login地址")})
模块化路由:

为了方便对路由进行模块化的管理,Express 不建议将路由直接挂载到app上,而是推荐将路由抽离为单独的模块。

将路由抽离为单独模块的步骤如下:

  • 创建路由模块对应的.js文件
  • 调用 express.Router() 函数创建路由对象
  • 向路由对象上挂载具体的路由
  • 使用module.exports 向外共享路由对象
  • 使用app.use() 函数注册路由模块
创建路由模块
//这是路由模块
const express = require("express")
//创建路由对象
const router = express.Router();

//挂载具体的路由
router.get("/", ((req, res) => {
    res.send("Get 请求/")
}))

router.post("/login", ((req, res) => {
    res.send("post 请求/login")
}))

//向外导出路由对象
module.exports = router
使用路由模块
//导入 express
const express = require("express")
//导入自定义的路由模块
const router = require("./router");

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

//app.use()函数的作用,就是用来注册全局中间件
//注册路由模块,使它生效
app.use(router)

//启动服务器
app.listen(80, () => {
    console.log("express server is running port 80")
})
路由模块添加统一的访问前缀:
//app.use()函数的作用,就是用来注册全局中间件
//注册路由模块,使它生效
//第一个参数:/api 代表给路由模块中的所有接口都添加统一的请求前缀
app.use("/api", router)

中间件

中间件(Middleware ) ,特指业务流程的中间处理环节。

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

Express的中间件,本质就是一个 function 处理函数。

app.get("/",function(req,res,next){ next() })

注意:中间件函数的形参列表中,必须包含next 参数。而路由处理函数只包含req和res。

中间件的作用:

多个中间件之间,共享同一份req和res。基于这样的特性,我们可以在上游的中间件中,统一为req或res对象添加自定义的属性或方法,供下游的中间件或路由进行使用。

next 函数的作用:

next函数是实现多个中间件连续调用的关键,它表示把流转关系转交给下一个中间件或路由。

全局中间件:

客户端发起的任何请求,到达服务器之后,都会触发的中间件,叫做全局生效的中间件。

使用app.use(中间件函数),即可定义一个全局生效的中间件。实例代码。

const middleware = function (req, res, next) {
    console.log("我是一个全局中间件")
    //把流转关系,转交给下一个中间件或路由
    next()
}

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

//注册全局中间件
app.use(middleware)
简化版
//创建web服务器
const app = express()

//注册全局中间件
app.use(function (req, res, next) {
    console.log("我是一个全局中间件")
     //获取到请求到服务器的时间
    //为req对象挂载,自定义属性,从而把时间共享给后面的所有路由
    req.startTime = Date.now();
    //把流转关系,转交给下一个中间件或路由
    next()
});
定义多个全局中间件

定义多个全局中间件,只需要多次使用app.use()方法即可,他会根据调用的顺序来依次调用。

//注册全局中间件
app.use(function (req, res, next) {
    console.log("我是第一个全局中间件件")
    //把流转关系,转交给下一个中间件或路由
    next()
});

//注册全局中间件
app.use(function (req, res, next) {
    console.log("我是第二个全局中间件件")
    //获取到请求到服务器的时间
    //为req对象挂载,自定义属性,从而把时间共享给后面的所有路由
    req.startTime = Date.now();
    //把流转关系,转交给下一个中间件或路由
    next()
});
局部生效中间件

不适用 app.use() 定义中间件,叫做局部生效中间件。局部生效的中间,在全局中间件之后调用。

//定义中间件函数
const mw = (req, res, next) => {
    console.log("我是局部中间件函数")
    next()
}

//挂载具体的路由
//使用三个参数的时候,第一个参数代表请求地址,第二个参数代表局部中间件函数,第三个参数代表回调函数
router.get("/", mw, ((req, res) => {
    const error = new Error();
    error.message = "发生错误了"
    error.code = 404
    error.stack = "stack"
    throw error
    res.send("Get 请求/, time:" + req.startTime)
}))

可以在路由中,通过如下两种等价的方式,使用多个局部中间件。

router.get("/", mw, mw1, ((req, res) => {}))
router.get("/", [mw, mw1], ((req, res) => {}))
中间件使用事项
  • 一定要在在路由之前注册中间件,否则中间件不生效
  • 客户端发送过来的请求,可以连续调用多个中间件进行处理
  • 执行完中间件代码后,不要忘记调用next() 函数
  • 为了防止代码逻辑混乱,在调用next() 函数后,不要在next() 函数后面写额外的代码了
  • 连续调用多个中间件时,多个中间件之间,共享req和res对象
中间件分类:

为了方便大家的理解和记忆中间件的使用,Express 官方把常用的中间件用法,分为了5大类:

  • 应用级别的中间件
  • 路由级别的中间件
  • 错误级别的中间件
  • Express 内置的中间件
  • 第三方的中间件
应用级别中间件

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

路由级别中间件

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

//创建路由对象,必须时express.Router() 获取到的router对象设置路由级别的中间件才有效
const router = express.Router();

router.use(function (req, res, next) {
    console.log("我是路由级别中间件")
    //把流转关系,转交给下一个中间件或路由
    next()
})
错误级别的中间件

错误级别中间的作用,专门用来捕获整个项目中发生的异常错误,从而防止项目异常崩溃的问题。

格式:错误级别中间件的 function 处理函数中,必须有4个形参,形参顺序从前到后,分别时(err,req,res,next)。

//app.use()函数的作用,就是用来注册全局中间件
//注册路由模块,使它生效
//第一个参数:/api 代表给路由模块中的所有接口都添加统一的请求前缀
app.use("/api", router)

//注册全局错误级别中间件,捕获整个项目的异常
app.use(function (err, req, res, next) {
    console.log("我是一个全局错误级别的中间件")
    console.log(err)
    res.send("发生错误:" + err.message)
});

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

Express内置的中间件

Express内置了3个常用的中间件,极大提高了Express项目的开发效率和体育:

  • express.static 快速托管静态资源的内置中间件,例如:HTML,图片,CSS样式等(无兼容性)
  • express.json 解析JSON格式的请求体数据(有兼容性,仅在4.16.0+版本可用)
  • express.urlebcoded 解析URL-encoded 格式的请求体数据(有兼容性,仅在4.16.0+版本可用)
//配置解析 application/json 格式数据的内置中间件
app.use(express.json())
//配置解析 application/x-www-from-urlencoded 格式数据的内置中间件
app.use(express.urlencoded({extended: false}))

router.post("/login", ((req, res) => {
    //在服务端可以通过req.body 获取json格式的表单数据和url-encoded格式的数据
    //默认情况下不配置解析表单数据的中间件,则 req.body 默认为:undefined
    console.log(req.body)
    res.send("post 请求/login, time:" + req.startTime)
}))

注意:在配置路由之前配置,除了错误级别的中间件在路由配置之后。

第三方中间件

非Express 官方内置的,而是由第三方开发出来的中间件,叫做第三方中间件。在项目中,大家可以按需下载并配置第三方中间件,从而提高项目的开发效率。

使用第三方中间件步骤:

  • 运行 npm install 第三发中间件名称 安装中间件
  • 使用 require() 导入中间件
  • 调用 app.use() 注册并使用中间件
自定义中间件

定义全局中间件:

//这是解析表单数据的中间件
app.use((req, res, next) => {
    //具体业务处理
   ……
})

监听req的data事件:

在中间件中,需要监听req对象的data 事件,来获取客户端发送到服务器的数据。如果数据量比较大,无法一次性发送完毕,则客户端会把数据切割后,分批发送到服务器。所以data事件可能会触发多次,每一次触发data事件时,获取到数据只是完整数据的一部分,需要手动对接收到的数据进行拼接。

//定义变量,用来存储客户端发送过来的请求数据
let data = "";
//监听req对象的data事件(客户端发送过来的新的请求体数据)
req.on("data", (chunk) => {
    //拼接请求体数据,隐式转换为字符串
    data += chunk
})

监听req的end事件:

当请求体数据接收完毕之后,会自动触发req的end事件。因此,我们可以在req的end 事件中,拿到并处理完整的请求体数据。示例代码如下:

//监听req的end事件
req.on("end", () => {
    //在data中存放的是完整的请求体数据
    console.log(data)
    //TODO: 把字符串格式的请求体数据,解析成对象格式
})
使用querystring 模块解析请求体数据

Node.js 内置了一个querystring模块,专门用来处理查询字符串。通过这个模块提供的 parse()函数,可以轻松把查询字符串,解析成对象的格式。示例代码如下:

//导入 Node.js 内置的querystring 模块
const querystring = require("querystring");
//调用 querystring.parse() 方法,把查询字符串解析为对象
const body = querystring.parse(data);
console.log(body)
将解析出来的数据挂载为req.body

上游的中间件和下游的中间件及路由之间,共享同一份req和res。因此,我们可以将解析出来的数据,挂载为 req的自定义属性,命名为req.body,供下游使用。示例代码如下:

//导入 Node.js 内置的querystring 模块
const querystring = require("querystring");

//监听req的end事件
req.on("end", () => {
    //在data中存放的是完整的请求体数据
    console.log(data)
    //TODO: 把字符串格式的请求体数据,解析成对象格式
    //调用 querystring.parse() 方法,把查询字符串解析为对象
    const body = querystring.parse(data);
    // console.log(body)
    req.body = body
    //把流转关系,转交给下一个中间件或路由
    next()
})
将自定义中间件封装为模块
//这是中间件模块
//导入 Node.js 内置的querystring 模块
const querystring = require("querystring");

const bodyParser = function (req, res, next) {
    //定义变量,用来存储客户端发送过来的请求数据
    let data = "";
    //监听req对象的data事件(客户端发送过来的新的请求体数据)
    req.on("data", (chunk) => {
        //拼接请求体数据,隐式转换为字符串
        data += chunk
    })

    //监听req的end事件
    req.on("end", () => {
        //在data中存放的是完整的请求体数据
        console.log(data)
        //TODO: 把字符串格式的请求体数据,解析成对象格式
        //调用 querystring.parse() 方法,把查询字符串解析为对象
        req.body = querystring.parse(data)
        //把流转关系,转交给下一个中间件或路由
        next()
    })
}

//向外暴露需要的成员
module.exports = bodyParser
使用自定义中间件
//这是路由模块
const express = require("express")
//创建路由对象
const router = express.Router();

router.post("/login", ((req, res) => {
    //在服务端可以通过req.body 获取json格式的表单数据和url-encoded格式的数据
    //默认情况下不配置解析表单数据的中间件,则 req.body 默认为:undefined
    console.log(req.body)
    res.send("post 请求/login")
}))

//向外导出路由对象
module.exports = router

//-----------------------------------------------------

//导入 express
const express = require("express")
//导入自定义的路由模块
const router = require("./router");
//导入自定义中间件模块
const mw = require("./mw");


//创建web服务器
const app = express()
//app.use()函数的作用,就是用来注册全局中间件
app.use(mw)

//注册路由模块,使它生效
//第一个参数:/api 代表给路由模块中的所有接口都添加统一的请求前缀
app.use("/api", router)


//启动服务器
app.listen(80, () => {
    console.log("express server is running port 80 in IP:127.0.0.1")
})

跨域问题

解决接口跨域问题的方案主要有2种:

  • CORS(主流从解决方案,推荐使用)
  • JSONP(有缺陷的解决方案:只支持GET请求)
CORS接口
CORS跨域资源共享

CORS 是Express的一个第三方中间件。通过安装和配置 cors 中间件,跨域很方便地解决跨域问题。

使用步骤:

  • 运行 npm install cors 安装中间件
  • 使用 require(“cors”) 导入中间件
  • 在路由之前调用 app.use(cors()) 配置中间件
//导入cors
const cors = require("cors");
//一定要在路由之前,配置 cors 这个中间件,从而解决接口跨域的问题
app.use(cors())

CORS(Cross-Origin Resource Sharing,跨域资源共享)由一系列HTTP 响应头组成,这些HTTP 响应头决定浏览器是否阻止前端JS代码跨域获取资源。

浏览器的同源安全策略默认会阻止网页“跨域”获取资源。但如果接口服务器配置了 CORS 相关的HTTP 响应托,就跨域解除浏览器端的跨域访问限制。

CORS主要在服务器端进行配置客户端浏览器无须做任何额外的配置,即可请求开启了CORS的接口。

CORS在浏览器中有兼容性。只有支持XMLHttpRequest Level2的浏览器,才能正常访问开启了CORS的服务端接口(例如:IE10+、Chrome4+、FireFox3.5+)。

响应头:
Access-Control-Allow-Origin: | *

其中,origin 参数的值指定了允许访问该资源的外域URL。

例如:

//只允许来自 http://baidu.com 的请求
res.setHeader('Access-Control-Allow-Origin','http://baidu.com')
//如果指定了 Access-Control-Allow-Origin 字段的值为通配符 * ,表示允许来自任何域的请求。
res.setHeader('Access-Control-Allow-Origin','*')
Access-Control-Allow-Headers

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

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

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

//允许客户端额外向服务端发送 Content-Type 请求头和 X-Custom-Header 请求头
//注意:多个请求头之间使用英文的逗号进行分割
res.setHeader('Access-Control-Allow-Headers','Content-Type,X-Custom-Header')
Access-Control-Allow-Methods

默认情况下,CORS仅支持客户端发起GET、POST、HEAD请求。

如果客户端希望通过PUT、DELETE等方式请求服务器的资源,则需要在服务器端,通过Access-Control-Allow-Methods来指明实际请求所允许使用的HTTP方法。

//只允许 POST、GET、DELETE、HEAD 请求方法
res.setHeader('Access-Control-Allow-Methods','POST,GET,DELETE,HEAD')
//允许所有的HTTP请求方式
res.setHeader('Access-Control-Allow-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、HAND 之外的请求 Method 类型
  • 请求头中包含自定义头部字段
  • 向服务器发送了application/json格式的数据

在浏览器与服务器正式通信之前,浏览器会先发送ОPTION请求进行预检,以获知服务器是否允许该实际请求,所以这一次的OPTION请求称为“预检请求”。服务器成功响应预检请求后,才会发送真正的请求,并且携带真实数据。

简单请求与预检请求的区别:

简单请求的特点:客户端与服务器之间只会发生一次请求。

预检请求的特点:客户端与服务器之间会发生两次请求,OPTION预检请求成功之后,才会发起真正的请求。

JSONP接口

概念:浏览器端通过

特点:

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

如果项目中已经配置了CORS 跨域资源共享,为了防止冲突,**必须在配置CORS中间件之前声明JSONP的接口。**否则JSONP接口会被处理成开启了CORS的接口。

JSONP接口实现步骤
  • 获取客户端发送过来的回调函数的名字
  • 得到要通过JSONP形式发送给客户端的数据
  • 根据前两步得到的数据,拼接出一个函数调用的字符串
  • 把上一步拼接得到的字符串,响应给客户喘的
//前端发送jsonp请求
$("#btnJSONP").click(() => {
    $.ajax({
        type: "GET",
        url: "http://127.0.0.1/api/JSONP",
        //表示要发起一个JSONP的请求
        dataType:"JSONP",
        success: function (res) {
            console.log(res)
        }
    })
})

//node响应请求
//必须在CORS中间件之前,配置JSONP的接口
app.get("/api/JSONP",(req, res) => {
    //TODO: 定义JSONP接口具体的实现过程
    //得到函数的名称
    const callback = req.query.callback;
    //定义要发送到客户端的数据对象
    const data = {name: 'zs', age: 20}
    //拼接出一个函数的调用
    const scriptStr = `${callback}(${JSON.stringify(data)})`
    //把拼接的字符串响应给客户端
    res.send(scriptStr)
})

mysql

使用Node操作数据库步骤:

  • 运行 npm install mysql安装中间件
  • 使用 const mysql = require(“mysql”) 导入中间件
  • 使用 const connection = mysql.createConnection({}) 设置连接数据库的信息
  • connection.connect(); 开连接
  • 使用 connection.query() 操作数据库

增删查改

查询语句
//导入mysql模块
const mysql = require("mysql");

//设置数据库信息
const connection = mysql.createConnection({
    host: 'localhost',
    user: 'root',
    password: 'fanzc131',
    database: 'cloud'
});
//开连接
connection.connect();

//第一个参数是要执行的sql语句
//第二个参数是回调函数
connection.query('SELECT * from payment', function (error, results, fields) {
    //error:默认为null,即没有出错时为null
    //如果出错就抛出错误
    if (error) throw error;
    //results:代表执行sql语句之后的返回结果
    console.log('The solution is: ', results);
});

//关闭连接
connection.end();
插入操作
//导入mysql模块
const mysql = require("mysql");

//设置数据库信息
const connection = mysql.createConnection({
    host: 'localhost',
    user: 'root',
    password: 'fanzc131',
    database: 'cloud'
});

//开连接
connection.connect();

//插入数据
const sql = "insert into payment(id,serial) values(?,?)"
const data = {id: 4, serial: "我是通过Node插入的数据"}
//第一个参数是要执行的sql语句
//第二个参数是,执行sql语句中? 占位符中具体的值
//第三个参数是执行sql语句后的返回结果
connection.query(sql, [data.id, data.serial], function (error, results, fields) {
    //如果出错就抛出错误
    if (error) throw error;
    //results.affectedRows 代表影响的行数
    if (results.affectedRows === 1) console.log("插入数据成功")
});

//使用便捷的方式插入数据
const sqlL = "insert into payment set ?"
const dataA = {id: 5, serial: "我是通过Node插入的数据"}
//第一个参数是要执行的sql语句:insert into XX set ?
//第二个参数是,跟表中字段相同的一个对象
//第三个参数是执行sql语句后的返回结果
connection.query(sqlL, dataA, function (error, results, fields) {
    //如果出错就抛出错误
    if (error) throw error.sqlMessage;
    //results.affectedRows 代表影响的行数
    if (results.affectedRows === 1) console.log("插入数据成功")
});

//关闭连接
connection.end();
更新数据库
//导入mysql模块
const mysql = require("mysql");

//设置数据库信息
const connection = mysql.createConnection({
    host: 'localhost',
    user: 'root',
    password: 'fanzc131',
    database: 'cloud'
});

//开连接
connection.connect();

const sql = "update payment set serial=? where id = ?"
const data = {id: 4, serial: "我是通过Node插入的数据!"}
//第一个参数是要执行的sql语句
//第二个参数是,sql中 ? 占位符的具体值的一个数组
//第三个参数是执行sql语句后的返回结果
connection.query(sql, [data.serial, data.id], function (error, results, fields) {
    //如果出错就抛出错误
    if (error) throw error;
    //results.affectedRows 代表影响的行数
    if (results.affectedRows === 1) console.log("更新数据成功")
});

//更新数据的便捷方式
const sqlL = "update payment set ? where id = ?"
const dataA = {id: 4, serial: "我是通过Node插入的数据?"}
//第一个参数是要执行的sql语句:set 后面的? 不直接全部写出所有的键值对,而是使用?占位符
//第二个参数是,sql中 ? 占位符的具体值的一个数组,对应set后面的占位符,使用与数据库表相对应的对象
//第三个参数是执行sql语句后的返回结果
connection.query(sqlL, [dataA, dataA.id], function (error, results, fields) {
    //如果出错就抛出错误
    if (error) throw error;
    //results.affectedRows 代表影响的行数
    if (results.affectedRows === 1) console.log("更新数据成功")
});

//关闭连接
connection.end();
删除数据
//导入mysql模块
const mysql = require("mysql");

//设置数据库信息
const connection = mysql.createConnection({
    host: 'localhost',
    user: 'root',
    password: 'fanzc131',
    database: 'cloud'
});

//开连接
connection.connect();

const sql = "delete from payment where id = ?"
//第一个参数是要执行的sql语句
//第二个参数是,sql中 ? 占位符的具体值,如果SQL语句中有多个占位符,则必须使用数组,若只有一个占位符,则可省略数组
//第三个参数是执行sql语句后的返回结果
connection.query(sql, 4, function (error, results, fields) {
    //如果出错就抛出错误
    if (error) throw error;
    //results.affectedRows 代表影响的行数
    if (results.affectedRows === 1) console.log("删除数据成功")
});

//关闭连接
connection.end();

你可能感兴趣的:(javascript,node.js)