一、文件
//引入fs模块
const fs=require("fs")
1、fs模块
(1)、使用readFile读取文件
const fs=require("fs")
fs.readFile("text1.txt", "utf-8", (err, dataStr) => {
if (err) {
// 成功下,err为null
console.log("读取文件失败", err)
} else {
// 失败情况下dataStr为unefined
console.log(dataStr)
}
})
(2)、使用writeFile写入文件
const fs=require("fs")
// 默认utf-8,有文件就会替换文件的所有内容,没有文件则会创建一个新的文件
fs.writeFile("text2.txt", "abcd", "utf-8", (err) => {
// 写入成功err为null
console.log(err)
})
(3)、路径问题
const fs=require('fs')
// 出现路径拼接错误的问题,是因为提供了 ./ 或 ../ 开头的相对路径
// 如果要解决这个问题,可以直接提供一个完整的文件存放路径就行
/* fs.readFile('./files/1.txt', 'utf8', function(err, dataStr) {
if (err) {
return console.log('读取文件失败!' + err.message)
}
console.log('读取文件成功!' + dataStr)
}) */
// 移植性非常差、不利于维护
/* fs.readFile('C:\\Users\\escook\\Desktop\\Node.js基础\\day1\\code\\files\\1.txt', 'utf8', function(err, dataStr) {
if (err) {
return console.log('读取文件失败!' + err.message)
}
console.log('读取文件成功!' + dataStr)
}) */
// __dirname 表示当前文件所处的目录
fs.readFile(__dirname+'/text1.txt', 'utf8', function (err, dataStr) {
if (err) {
return console.log('读取文件失败!'+err.message)
}
console.log('读取文件成功!'+dataStr)
})
2、path模块
//引入path模块
const path = require("path")
(1)、path.join()的使用
const path=require('path')
const fs=require('fs')
// 注意: ../ 会抵消前面的路径
// const pathStr = path.join('/a', '/b/c', '../../', './d', 'e')
// console.log(pathStr) // \a\b\d\e
// fs.readFile(__dirname + '/files/1.txt')
fs.readFile(path.join(__dirname, './text1.txt'), 'utf8', function (err, dataStr) {
if (err) {
return console.log(err.message)
}
console.log(dataStr)
})
(2)、path.basename()的使用
const path=require('path')
// 定义文件的存放路径
const fpath='/a/b/c/index.html'
// const fullName = path.basename(fpath)
// console.log(fullName)
//返回文件名,扩展名为.html的
const nameWithoutExt=path.basename(fpath, '.html')
console.log(nameWithoutExt) //index
(3)、path.extname()的使用
const path=require('path')
// 这是文件的存放路径
const fpath='/a/b/c/index.html'
//返回文件扩展名
const fext=path.extname(fpath)
console.log(fext) //.html
二、服务器
1、创建一个基本的服务器
//引入http模块
const http=require("http")
// 创建一个web服务器实例
const server=http.createServer()
// 为服务器实例绑定request事件
server.on("request", (req, res) => {
// 当客户端访问到此服务器时,触发request事件
// req为请求对象,包含一些客户端的数据和信息
// req.url客户端请求时的url地址,从端口号开始
// req.method客户端请求时所使用的方法
const str=`客户端请求时的url地址:${req.url},客户端请求时所使用的方法:${req.method}`
console.log(str)
// res为响应对象
// 调用 res.end方法,像客户端响应一些内容
// 调用res.end()方法时,想客户端返回数据包含中文时,会产生乱码的情况,需要手动设置编码格式,设置响应头
res.setHeader("Content-Type", "text/html;charset=utf-8")
res.end(str)
})
// 调用服务器实例的listen方法,启动服务器
server.listen(8080, () => {
console.log("http server is running at http://127.0.0.1:8080")
})
2、根据url返回相应内容
const http=require("http")
const server=http.createServer()
server.on("request", (req, res) => {
const url=req.url
let content="<h1>404 Not Found</h1>"
if (url=="/index.html"||url=="/") {
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("8080", () => {
console.log("http server is running at http://127.0.0.1:8080")
})
三、模块化
模块化是指解决一个复杂问题时,自顶向下逐层把系统划分成若干模块的过程。对于整个系统来说,模块是可组合、分解和更换的单元。
Node.js 中根据模块来源的不同,将模块分为了 3 大类,分别是:
内置模块(内置模块是由 Node.js 官方提供的,例如 fs、path、http 等)
自定义模块(用户创建的每个 .js 文件,都是自定义模块)
第三方模块(由第三方开发出来的模块,并非官方提供的内置模块,也不是用户创建的自定义模块,使用前需要先下载)
1、加载模块
require() 方法(可以省略.js),可以加载需要的内置模块、用户自定义模块、第三方模块进行使
//加载内置的模块
const fs=require("fs")
//加载自定义的模块
const test=require("./test,js")
//加载第三方的模块
const moment=require("moment")
注意:使用 require() 方法加载其它模块时,会执行被加载模块中的代码。
2、模块作用域
函数作用域类似,在自定义模块中定义的变量、方法等成员,只能在当前模块内被访问,这种模块级别的访问限制,叫做模块作用域。可以防止了全局变量污染的问题
3、向外共享模块作用域中的成员
(1)、module 对象
在每个 .js 自定义模块中都有一个 module 对象,它里面存储了和当前模块有关的信息。
Module {
id: '.',
path: 'C:\\Users\\25787\\Desktop\\web\\node\\study\\day2',
exports: {},
filename: 'C:\\Users\\25787\\Desktop\\web\\node\\study\\day2\\4、加载模块.js',
loaded: false,
children: [],
paths: [Array]
}
在自定义模块中,可以使用 module.exports 对象,将模块内的成员共享出去,供外界使用。
//导出
const user="张三"
function sayHello () {
console.log("hello")
}
module.exports= { user, sayHello }
//导出
module.exports={
user:"王二",
sayHello(){
console.log("hello")
}
}
//导出
module.exports.user="李四"
module.exports.sayHello=function () {
console.log("hello")
}
//引入
const ob=require("./moduletest")
console.log(ob.user)
外界用 require() 方法导入自定义模块时,得到的就是 module.exports 所指向的对象。
使用 require() 方法导入模块时,导入的结果,永远以 module.exports 指向的对象为准。
(2)、exports对象
由于 module.exports 单词写起来比较复杂,为了简化向外共享成员的代码,Node 提供了 exports 对象。默认情况下,exports 和 module.exports 指向同一个对象。最终共享的结果,还是以 module.exports 指向的对象为准。
(3)、Node.js 中的模块化规范
Node.js 遵循了 CommonJS 模块化规范,CommonJS 规定了模块的特性和各模块之间如何相互依赖。
CommonJS 规定:
每个模块内部,module 变量代表当前模块。
module 变量是一个对象,它的 exports 属性(即 module.exports)是对外的接口。
加载某个模块,其实是加载该模块的 module.exports 属性。require() 方法用于加载模块。
(4)、包
Node.js 中的第三方模块又叫做包,就像电脑和计算机指的是相同的东西,第三方模块和包指的是同一个概念,只不过叫法不同。
不同于 Node.js 中的内置模块与自定义模块,包是由第三方个人或团队开发出来的,免费供所有人使用。
注意:Node.js 中的包都是免费且开源的,不需要付费即可免费下载使用。
由于 Node.js 的内置模块仅提供了一些底层的 API,导致在基于内置模块进行项目开发的时,效率很低。
包是基于内置模块封装出来的,提供了更高级、更方便的 API,极大的提高了开发效率。
包和内置模块之间的关系,类似于 jQuery 和 浏览器内置 API 之间的关系。
注意:
从 https://www.npmjs.com/ 网站上搜索自己所需要的包
从 https://registry.npmjs.org/ 服务器上下载自己需要的包
初次装包完成后,在项目文件夹下多一个叫做 node_modules 的文件夹和 package-lock.json 的配置文件。
其中:
node_modules 文件夹用来存放所有已安装到项目中的包。require() 导入第三方包时,就是从这个目录中查找并加载包。
package-lock.json 配置文件用来记录 node_modules 目录下的每一个包的下载信息,例如包的名字、版本号、下载地址等。
默认情况下,使用 npm install 命令安装包的时候,会自动安装最新版本的包。如果需要安装指定版本的包,可以在包名之后,通过 @ 符号指定具体的版本
//npm 包管理工具提供了一个快捷命令,可以在执行命令时所处的目录中,快速创建 package.json 这个包管理
npm init -y
在项目根目录中,创建一个叫做 package.json 的配置文件,即可用来记录项目中安装了哪些包。从而方便剔除 node_modules 目录之后,在团队成员之间共享项目的源代码。package.json 文件中,有一个 dependencies 节点,专门用来记录您使用 npm install 命令安装了哪些包。
注意:今后在项目开发中,一定要把 node_modules 文件夹,添加到 .gitignore 忽略文件中‘
当拿到一个删除了 node_modules 的项目之后,需要先把所有的包下载到项目中,才能将项目运行起来,可以运行 npm install 命令(或 npm i)一次性安装所有的依赖包
运行 npm uninstall 命令,来卸载指定的包,npm uninstall 命令执行成功后,会把卸载的包,自动从 package.json 的 dependencies 中移除掉。
如果某些包只在项目开发阶段会用到,在项目上线之后不会用到,则建议把这些包记录到 devDependencies 节点中。
npm i 包名 --save-dev
npm i 包名 -D
与之对应的,如果某些包在开发和项目上线之后都需要用到,则建议把这些包记录到 dependencies 节点中。
npm config get registry //查看当前下包镜像源
npm config set registry=https://registry.npm.taobao.org/ //切换下包源为淘宝镜像
为了更方便的切换下包的镜像源,我们可以安装 nrm 这个小工具,利用 nrm 提供的终端命令,可以快速查看和切换下包的镜像源。
npm i nrm -g //通过npm包管理器,将nrm安装为全局可用的工具
nrm ls //查看所有可用的镜像源
nrm use taobao //将下包的镜像源切换为淘宝镜像源
包的分类
那些被安装到项目的 node_modules 目录中的包,都是项目包。
项目包又分为两类,分别是:
开发依赖包(被记录到 devDependencies 节点中的包,只在开发期间会用到)
核心依赖包(被记录到 dependencies 节点中的包,在开发期间和项目上线之后都会用到)
在执行 npm install 命令时,如果提供了 -g 参数,则会把包安装为全局包。
全局包会被安装到 C:\Users\用户目录\AppData\Roaming\npm\node_modules 目录下。
开发自己的包
package.json (包管理配置文件)
index.js (包的入口文件)
README.md (包的说明文档)
一个规范的包,它的组成结构,必须符合以下 3 点要求:
①包必须以单独的目录而存在
②包的顶级目录下要必须包含 package.json 这个包管理配置文件
③package.json 中必须包含 name,version,main 这三个属性,分别代表包的名字、版本号、包的入口。
包根目录中的 README.md 文件,是包的使用说明文档。通过它,我们可以事先把包的使用说明,以 markdown 的格式写出来,方便用户参考。
README 文件中具体写什么内容,没有强制性的要求;只要能够清晰地把包的作用、用法、注意事项等描述清楚即可。
我们所创建的这个包的 README.md 文档中,会包含以下 6 项内容:
安装方式、导入方式、格式化时间、转义 HTML 中的特殊字符、还原 HTML 中的特殊字符、开源协议
发布包到npm与删除包
注:需要将镜像切换到npmjs官方而不是淘宝镜像
将终端切换到包的根目录之后,运行 npm publish 命令,即可将包发布到 npm 上(注意:包名不能雷同)。
npm publish
运行 npm unpublish 包名 --force 命令,即可从 npm 删除已发布的包。
npm unpublish 包名 --force
注意:
①npm unpublish 命令只能删除 72 小时以内发布的包
②npm unpublish 删除的包,在 24 小时内不允许重复发布
③发布包的时候要慎重,尽量不要往 npm 上发布没有意义的包!
模块加载机制
优先从缓存中加载
模块在第一次加载后会被缓存。 这也意味着多次调用 require() 不会导致模块的代码被执行多次。
注意:不论是内置模块、用户自定义模块、还是第三方模块,它们都会优先从缓存中加载,从而提高模块的加载效率。
内置模块的加载机制
内置模块是由 Node.js 官方提供的模块,内置模块的加载优先级最高。
例如,require('fs') 始终返回内置的 fs 模块,即使在 node_modules 目录下有名字相同的包也叫做 fs。
自定义模块的加载机制
使用 require() 加载自定义模块时,必须指定以 ./ 或 ../ 开头的路径标识符。在加载自定义模块时,如果没有指定 ./ 或 ../ 这样的路径标识符,则 node 会把它当作内置模块或第三方模块进行加载。
同时,在使用 require() 导入自定义模块时,如果省略了文件的扩展名,则 Node.js 会按顺序分别尝试加载以下的文件:
①按照确切的文件名进行加载
②补全 .js 扩展名进行加载
③补全 .json 扩展名进行加载
④补全 .node 扩展名进行加载
⑤加载失败,终端报错
第三方模块的加载机制
如果传递给 require() 的模块标识符不是一个内置模块,也没有以 ‘./’ 或 ‘../’ 开头,则 Node.js 会从当前模块的父目录开始,尝试从 /node_modules 文件夹中加载第三方模块。
如果没有找到对应的第三方模块,则移动到再上一层父目录中,进行加载,直到文件系统的根目录。
例如,假设在 'C:\Users\itheima\project\foo.js' 文件里调用了 require('tools'),则 Node.js 会按以下顺序查找:
① C:\Users\itheima\project\node_modules\tools
② C:\Users\itheima\node_modules\tools
③ C:\Users\node_modules\tools
C:\node_modules\tools
目录作为模块
当把目录作为模块标识符,传递给 require() 进行加载的时候,有三种加载方式:
①在被加载的目录下查找一个叫做 package.json 的文件,并寻找 main 属性,作为 require() 加载的入口
②如果目录里没有 package.json 文件,或者 main 入口不存在或无法解析,则 Node.js 将会试图加载目录下的 index.js 文件。
③如果以上两步都失败了,则 Node.js 会在终端打印错误消息,报告模块的缺失:Error: Cannot find module 'xxx'
四、express
1、认识express
Express 是基于 Node.js 平台,快速、开放、极简的 Web 开发框架。
通俗的理解:Express 的作用和 Node.js 内置的 http 模块类似,是专门用来创建 Web 服务器的。
Express 的本质:就是一个 npm 上的第三方包,提供了快速创建 Web 服务器的便捷方法。
不使用 Express 也可以通过nodejs内置模块创建 Web 服务器,但是http 内置模块用起来很复杂,开发效率低;Express 是基于内置的 http 模块进一步封装出来的,能够极大的提高开发效率。
http内置模块和express的关系就好比浏览器WebAPI和jquery的关系,后者是由前者进一步封装出来的
对于前端开发人员来说,常见服务器分为两类
Web 网站服务器:专门对外提供 Web 网页资源的服务器。
API 接口服务器:专门对外提供 API 接口的服务器。
使用 Express,我们可以方便、快速的创建 Web 网站的服务器或 API 接口的服务器。
安装express
npm i express
2、使用express创建服务器
//引入express
constexpress=require("express")
// 创建web服务器。app为服务器实例
constapp=express()
// 启动web服务器
app.listen(8080, () => {
console.log('express server is running at http://127.0.0.1:8080')
})
(1)、监听get请求
通过 app.get() 方法,可以监听客户端的 GET 请求
app.get("/user", (req, res) => {
// req.query默认是一个空对象
// 客户端可使用?name=zs&age=18这种格式,发送参数到服务器
// 可以使用req.query对象访问到客户端带来的参数 req.query.name req.query.age
console.log(req.query)
// 通过 res.send() 方法,可以把处理好的内容,发送给客户端
// 调用express提供的res.send() 方法,给客户端返回一个JSON字符串
res.send({ name: "zs", age: 18 })
})
(2)、监听post请求
通过 app.post() 方法,可以监听客户端的 POST 请求
app.post("/user", (req, res) => {
// 调用express提供的res.send() 方法,给客户端返回一个字符串
res.send("post请求成功")
})
(3)、获取url中的动态参数
通过 req.params 对象,可以访问到 URL 中,通过 : 匹配到的动态参数
app.get("/:id", (req, res) => {
// url中,可以通过:参数名的形式,匹配动态参数值
// req.params默认是一个空对象,里面存放着通过:动态匹配到的参数值
res.send(req.params)
})
(4)、托管静态资源
express 提供了一个非常好用的函数,叫做 express.static(),通过它,我们可以非常方便地创建一个静态资源服务器,例如,通过如下代码就可以将 public 目录下的图片、CSS 文件、JavaScript 文件对外开放访问了
app.use(express.static("public"))
现在就可以访问 public 目录中的所有文件了:
http://localhost:3000/images/bg.jpg
http://localhost:3000/css/style.css
http://localhost:3000/js/login.js
注意:Express 在指定的静态目录中查找文件,并对外提供资源的访问路径。因此,存放静态文件的目录名不会出现在 URL 中。
如果要托管多个静态资源目录,多次调用express.static()就行了,访问静态资源文件时,函数会根据目录添加顺序查找所需要的文件
如果希望在托管的静态资源访问路径之前,挂载路径前缀,则可以使用如下的方式:
app.use("/public",express.static("public"))
现在就可以通过带有 /public 前缀地址来访问 public 目录中的文件了:
http://localhost:3000/public/images/kitten.jpg
http://localhost:3000/public/css/style.css
http://localhost:3000/public/js/app.js
3、nodemon
(1)、为什么要使用nodemon
在编写调试 Node.js 项目的时候,如果修改了项目的代码,则需要频繁的手动 close 掉,然后再重新启动,非常繁琐。
可以使用 nodemon(https://www.npmjs.com/package/nodemon) 这个工具,它能够监听项目文件的变动,当代码被修改后,nodemon 会自动帮我们重启项目,极大方便了开发和调试。
(2)、安装nodemon
安装为全局工具
npm i -g nodemon
(3)、使用nodemon
当基于 Node.js 编写了一个网站应用的时候,传统的方式,是运行 node app.js 命令,来启动项目。这样做的坏处是:代码被修改之后,需要手动重启项目。
现在可以将 node 命令替换为 nodemon 命令,使用 nodemon app.js 来启动项目。这样做的好处是:代码被修改之后,会被 nodemon 监听到,从而实现自动重启项目的效果。
nodemon app.js
4、express路由
(1)、概念
广义上来讲,路由就是映射关系。
在 Express 中,路由指的是客户端的请求与服务器处理函数之间的映射关系。
Express 中的路由分 3 部分组成,分别是请求的类型、请求的 URL 地址、处理函数,格式如下:
app.方法(path,handler)
(2)、路由匹配
每当一个请求到达服务器之后,需要先经过路由的匹配,只有匹配成功之后,才会调用对应的处理函数。
在匹配时,会按照路由的顺序进行匹配,如果请求类型和请求的 URL 同时匹配成功,则 Express 会将这次请求,转交给对应的 function 函数进行处理。
路由匹配的注意点:
①按照定义的先后顺序进行匹配
②请求类型和请求的URL同时匹配成功,才会调用对应的处理函数
在 Express 中使用路由最简单的方式,就是把路由挂载到 app 上
(3)、模块化路由
为了方便对路由进行模块化的管理,Express 不建议将路由直接挂载到 app 上,而是推荐将路由抽离为单独的模块。
将路由抽离为单独模块的步骤如下:
①创建路由模块对应的 .js 文件
②调用 express.Router() 函数创建路由对象
③向路由对象上挂载具体的路由
④使用 module.exports 向外共享路由对象
⑤使用 app.use() 函数注册路由模块
创建路由模块:
const express=require("express")
// 创建路由对象
const router=express.Router()
// 挂载具体的路由
router.get("/user", (req, res) => {
res.send(req.query)
})
router.post("/user", (req, res) => {
res.send(req.query)
})
module.exports=router
注册路由:
const express=require("express")
const app=express()
// 导入路由模块
const router=require("./router")
// 注册路由模块
app.use(router)
app.listen('8080', () => {
console.log('express server is running at http://127.0.0.1:8080')
})
注意:app.use()的作用是注册全局中间件
express.static(‘public’)和router都属于中间件
类似于托管静态资源时,为静态资源统一挂载访问前缀一样,路由模块添加前缀的方式也非常简单
//使用app.use()注册路由模块,并且添加统一的访问前缀
app.use("/api",router)
(4)、中间件
中间件的概念
中间件(Middleware ),特指业务流程的中间处理环节。
当一个请求到达 Express 的服务器之后,可以连续调用多个中间件,从而对这次请求进行预处理。
Express 的中间件,本质上就是一个 function 处理函数,Express 中间件的格式如下:
注意:中间件函数的形参列表中,必须包含 next 参数。而路由处理函数中只包含 req 和 res。
next 函数是实现多个中间件连续调用的关键,它表示把流转关系转交给下一个中间件或路由。
const express=require("express")
const app=express()
//定义中间件函数
const mw= (req, res, next) => {
console.log("这是最简单的中间件")
// 把流转关系交给下一个中间件或路由
next()
}
app.listen('8080', () => {
console.log('express server is running at http://127.0.0.1:8080')
})
客户端发起的任何请求,到达服务器之后,都会触发的中间件,叫做全局生效的中间件。
通过调用 app.use(中间件函数),即可定义一个全局生效的中间件,示例代码如下:
const express=require("express")
const app=express()
//定义中间件函数
const mw1= (req, res, next) => {
console.log("这是中间件1")
// 把流转关系交给下一个中间件或路由
next()
}
//注册全局生效的中间件
app.use(mw1)
app.use((req, res, next) => {
console.log("这是中间件2")
// 把流转关系交给下一个中间件或路由
next()
})
app.get("/", (req, res) => {
console.log("get请求")
res.send("get请求")
})
app.listen('8080', () => {
console.log('express server is running at http://127.0.0.1:8080')
})
可以使用 app.use() 连续定义多个全局中间件。客户端请求到达服务器之后,会按照中间件定义的先后顺序依次进行调用
const mw1= (req, res, next) => {
console.log("这是中间件1")
// 把流转关系交给下一个中间件或路由
next()
}
app.use(mw1)
app.get("/", (req, res, next) => {
console.log("这是路由中间件")
next()
})
app.get("/", (req, res) => {
console.log("get请求")
res.send("get请求")
})
中间件的作用
多个中间件之间,共享同一份 req 和 res。基于这样的特性,可以在上游的中间件中,统一为 req 或 res 对象添加自定义的属性或方法,供下游的中间件或路由进行使用。
局部中间件
不使用 app.use() 定义的中间件,叫做局部生效的中间件
const mw1= (req, res, next) => {
console.log("这是局部中间件")
// 把流转关系交给下一个中间件或路由
next()
}
app.get("/", mw1, (req, res) => {
console.log("这是路由中间件")
next()
})
app.get("/user", (req, res) => {
console.log("get请求")
res.send("get请求")
})
//可同时定义多个中间件,以下两种方式等价:
//app.get("/",mw1,mw2,(req,res)=>{})
//app.get("/",[mw1,mw2],(req,res)=>{})
使用中间件注意事项
①一定要在路由之前注册中间件
②客户端发送过来的请求,可以连续调用多个中间件进行处理
③执行完中间件的业务代码之后,不要忘记调用 next() 函数
④为了防止代码逻辑混乱,调用 next() 函数后不要再写额外的代码
⑤连续调用多个中间件时,多个中间件之间,共享 req 和 res 对象
中间件的分类
① 应用级别的中间件
通过 app.use() 或 app.get() 或 app.post() ,绑定到 app 实例上的中间件,叫做应用级别的中间件
② 路由级别的中间件
绑定到 express.Router() 实例上的中间件,叫做路由级别的中间件。它的用法和应用级别中间件没有任何区别。只不过,应用级别中间件是绑定到 app 实例上,路由级别中间件绑定到 router 实例上
router.use((req, res, next) => {
console.log("路由级别中间件")
next()
})
express().use("/",router)
③ 错误级别的中间件
错误级别中间件的作用:专门用来捕获整个项目中发生的异常错误,从而防止项目异常崩溃的问题。
格式:错误级别中间件的 function 处理函数中,必须有 4 个形参,形参顺序从前到后,分别是 (err, req, res, next)。
app.get("/", (req, res) => {
thrownewError("服务器内部发生错误")
res.send("Home page")
})
// 错误中间件
app.use((err, req, res, next) => {
console.log("发生了错误"+err.message)
res.send("Error!"+err.message)
})
注意:错误级别的中间件,必须注册在所有路由之后!
④ Express 内置的中间件
自 Express 4.16.0 版本开始,Express 内置了 3 个常用的中间件,极大的提高了 Express 项目的开发效率和体验:
express.static 快速托管静态资源的内置中间件,例如: HTML 文件、图片、CSS 样式等(无兼容性)
express.json 解析 JSON 格式的请求体数据(有兼容性,仅在 4.16.0+ 版本中可用)
//配置解析application/json格式数据的内置中间件
//在服务器,可以使用req.body,来接收客户端发送过来的请求体数据
//当客户端请求携带body数据为json字符串时,服务器访问req.body结果为undefined,需要注册内置中间件app.use(express.json)
app.use(express.json)
express.urlencoded 解析 URL-encoded 格式的请求体数据(有兼容性,仅在 4.16.0+ 版本中可用)
//配置解析application/x-www-form-urlencoded格式数据的内置中间件
app.use(express.urlencoded({extended:false}))
//false表示不使用第三方解析方式,使用自身解析的解析方式
⑤ 第三方的中间件
非 Express 官方内置的,而是由第三方开发出来的中间件,叫做第三方中间件。在项目中,大家可以按需下载并配置第三方中间件,从而提高项目的开发效率。
例如:在 express@4.16.0 之前的版本中,经常使用 body-parser 这个第三方中间件,来解析请求体数据。使用步骤如下:
运行 npm install body-parser 安装中间件
使用 require 导入中间件
调用 app.use() 注册并使用中间件
// 内置中间件
// app.use(express.urlencoded({ extended: false }))
// 第三方中间件
const parse=require("body-parser")
app.use(parse.urlencoded({ extended: false }))
app.get("/", (req, res) => {
console.log(req.body)
res.send("OK")
})
注意:Express 内置的 express.urlencoded 中间件,就是基于 body-parser 这个第三方中间件进一步封装出来的。
自定义中间件
在中间件中,需要监听 req 对象的 data 事件,来获取客户端发送到服务器的数据。
如果数据量比较大,无法一次性发送完毕,则客户端会把数据切割后,分批发送到服务器。所以 data 事件可能会触发多次,每一次触发
data 事件时,获取到数据只是完整数据的一部分,需要手动对接收到的数据进行拼接。
当请求体数据接收完毕之后,会自动触发 req 的 end 事件。因此,可以在 req 的 end 事件中,拿到并处理完整的请求体数据。
Node.js 内置了一个 querystring 模块,专门用来处理查询字符串。通过这个模块提供的 parse() 函数,可以轻松把查询字符串,解析成对象的格式
为了优化代码的结构,我们可以把自定义的中间件函数,封装为独立的模块
//middleware.js
// 导入内置模块querystring ,可以将查询字符串转换为对象
const qs=require("querystring")
let str=""
function bodyParse (req, res, next) {
// 监听客户端发送过来的请求体数据,req的data事件
req.on("data", (s) => {
str+=s
})
let obj= {}
req.on("end", () => {
// 使用内置模块querystring的parse方法解析字符串
obj=qs.parse(str)
// 原生
// str.split("&").forEach(item => {
// itemValue = item.split("=")
// obj[itemValue[0]] = itemValue[1]
// })
req.body=obj
next()
})
}
module.exports=bodyParse
//index.js
// 引入express
const express=require('express')
// 创建web服务器。app为服务器实例
const app=express()
// 导入自定义的中间件
const bodyParse=require("./middleware")
app.use(bodyParse)
app.post("/", (req, res) => {
console.log(req.body)
res.send(req.body)
})
// 启动web服务器
app.listen('8080', () => {
console.log('express server is running at http://127.0.0.1:8080')
})
注意事项:
//router可以想app一样,注册中间件或者当路由接口使用
const router=express.Router()
(5)、跨域
所谓跨域就是从a 向 b发请求,如果他们的地址协议、域名、端口都不相同,直接访问就会造成跨域问题
解决接口跨域问题的方案主要有两种:
① CORS(主流的解决方案,推荐使用)
② JSONP(有缺陷的解决方案:只支持 GET 请求)
使用CORS中间件解决跨域问题
cors 是 Express 的一个第三方中间件。通过安装和配置 cors 中间件,可以很方便地解决跨域问题。
使用步骤分为如下 3 步:
①运行 npm install cors 安装中间件
②使用 const cors = require('cors') 导入中间件
③在路由之前调用 app.use(cors()) 配置中间件
CORS跨域资源共享
CORS (Cross-Origin Resource Sharing,跨域资源共享)由一系列 HTTP 响应头组成,这些 HTTP 响应头决定浏览器是否阻止前端 JS 代码跨域获取资源。
浏览器的同源安全策略默认会阻止网页“跨域”获取资源。但如果接口服务器配置了 CORS 相关的HTTP 响应头,就可以解除浏览器端的跨域访问限制。
注意:服务器会响应资源,但是会被浏览器阻止
CORS 主要在服务器端进行配置。客户端浏览器无须做任何额外的配置,即可请求开启了 CORS 的接口。
CORS 在浏览器中有兼容性。只有支持 XMLHttpRequest Level2 的浏览器,才能正常访问开启了 CORS 的服务端接口(例如:IE10+、Chrome4+、FireFox3.5+)。
CORS响应头
Access-Control-Allow-Origin
响应头部中可以携带一个 Access-Control-Allow-Origin 字段其中,origin 参数的值指定了允许访问该资源的外域 URL。
例如,下面的字段值将只允许来自http://itcast.cn的请求,如果指定了 Access-Control-Allow-Origin 字段的值为通配符 *,表示允许来自任何域的请求
//Access-Control-Allow-Origin:<origin>|*
res.setHeader("Access-Control-Allow-Origin","http://itcast.cn")Access-Control-Allow-Headers
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-Cpntrol-Allow-Headers","Content-Type","X-Custom-Header")
Accexx-Control-Allow-Methods
默认情况下,CORS 仅支持客户端发起 GET、POST、HEAD 请求。
如果客户端希望通过 PUT、DELETE 等方式请求服务器的资源,则需要在服务器端,通过 Access-Control-Alow-Methods来指明实际请求所允许使用的 HTTP 方法。
//只允许"POST","GET","DELETE","HEAD"请求方法
res.setHeader("Access-Cpntrol-Allow-Methods","POST","GET","DELETE","HEAD")
//允许所有请求方法
res.setHeader("Access-Cpntrol-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、HEAD 之外的请求 Method 类型
② 请求头中包含自定义头部字段
③ 向服务器发送了 application/json 格式的数据
在浏览器与服务器正式通信之前,浏览器会先发送 OPTION 请求进行预检,以获知服务器是否允许该实际请求,所以这一次的 OPTION 请求称为“预检请求”。服务器成功响应预检请求后,才会发送真正的请求,并且携带真实数据。
简单请求和预检请求的区别:
简单请求的特点:客户端与服务器之间只会发生一次请求。
预检请求的特点:客户端与服务器之间会发生两次请求,OPTION 预检请求成功之后,才会发起真正的请求。
JSONP
概念:浏览器端通过 <script> 标签的 src 属性,请求服务器上的数据,同时,服务器返回一个函数的调用。这种请求数据的方式叫做 JSONP。
特点:
①JSONP 不属于真正的 Ajax 请求,因为它没有使用 XMLHttpRequest 这个对象。
②JSONP 仅支持 GET 请求,不支持 POST、PUT、DELETE 等请求。
如果项目中已经配置了 CORS 跨域资源共享,为了防止冲突,必须在配置 CORS 中间件之前声明 JSONP 的接口。否则 JSONP 接口会被处理成开启了 CORS 的接口
// 优先创建jsonp接口才不会被处理为cors接口
app.get("/api/jsonp", (req, res) => { })
实现步骤 :
①获取客户端发送过来的回调函数的名字
②得到要通过 JSONP 形式发送给客户端的数据
③根据前两步得到的数据,拼接出一个函数调用的字符串
④把上一步拼接得到的字符串,响应给客户端的 <script> 标签进行解析执行
// 优先创建jsonp接口才不会被处理为cors接口
app.get("/api/jsonp", (req, res) => {
// 获取客户端发送过来的回调函数
const fullName=req.query.callback
// 得到要通过jsonp形式发送给客户端的数据
const data= { name: "zs", age: 20 }
//拼接出函数调用的字符串
const scriptStr=`${fullName}(${JSON.stringify(data)})`
// 把拼接的字符串,响应给客户端的script标签进行解析执行
res.send(scriptStr)
})
$('#jsonp').on('click', () => {
$.ajax({
type: 'get',
url: 'http://127.0.0.1:8080/api/jsonp',
dataType: 'jsonp',
success: function (result) {
console.log(result);
},
});
});
五、数据库
1、在项目中操作数据库
①安装操作 MySQL 数据库的第三方模块(mysql)
mysql 模块是托管于 npm 上的第三方模块。它提供了在 Node.js 项目中连接和操作 MySQL 数据库的能力。
想要在项目中使用它,需要先运行如下命令,将 mysql 安装为项目的依赖包
npm i mysql
②通过 mysql 模块连接到 MySQL 数据库
在使用 mysql 模块操作 MySQL 数据库之前,必须先对 mysql 模块进行必要的配置,
//导入mysql模块
const mysql=require('mysql')
// 建立与MySQL数据库的连接
const db=mysql.createPool({
host: '127.0.0.1', //数据库的ip
user: 'root', //登录数据库的账号
password: 'admin123', //登录数据库的密码
database: 'my_db_01' //数据库名
})
③通过 mysql 模块执行 SQL 语句
// 测试mysql
db.query("select * from users", (err, result) => {
if (err) {
console.log(err.message)
} else {
console.log(result)
}
})
const user= { username: "Spider-Man", password: "123456" }
db.query("insert into users(username,password) values(?,?)", [user.username, user.password], (err, result) => {
if (err) {
return console.log(err.message)
}
if (result.affectedRows===1) {
console.log("插入成功")
}
})
向表中新增数据时,如果数据对象的每个属性和数据表的字段一一对应,则可以通过如下方式快速插入数据
const user= { username: "Spider-Man", password: "123456" }
db.query("insert into users set ?", user, (err, result) => {
if (err) {
return console.log(err.message)
}
if (result.affectedRows===1) {
console.log("插入成功")
}
})
const user= { username: "Spider", password: "111111", id: 1 }
db.query("update users set username=?,password=? where id=?", [user.username, user.password, user.id], (err, result) => {
if (err) {
return console.log(err.message)
}
if (result.affectedRows===1) {
console.log("更新成功")
}
})
//简写
db.query("update users set ? where id=?", [user, user.id], (err, result) => {
if (err) {
return console.log(err.message)
}
if (result.affectedRows===1) {
console.log("更新成功")
}
})
注意:sql语句中有多个占位符用数组包裹,只有一个占位符就可以直接写
db.query("delete from users where id=?",4, (err, result) => {
if (err) {
return console.log(err.message)
}
if (result.affectedRows===1) {
console.log("删除成功")
}
})
2、web开发模式
目前主流的 Web 开发模式有两种,分别是:
①基于服务端渲染的传统 Web 开发模式
服务端渲染的概念:服务器发送给客户端的 HTML 页面,是在服务器通过字符串的拼接,动态生成的。因此,客户端不需要使用 Ajax 这样的技术额外请求页面的数据
优点:
前端耗时少。因为服务器端负责动态生成 HTML 内容,浏览器只需要直接渲染页面即可。尤其是移动端,更省电。
有利于SEO。因为服务器端响应的是完整的 HTML 页面内容,所以爬虫更容易爬取获得信息,更有利于 SEO。
缺点:
占用服务器端资源。即服务器端完成 HTML 页面内容的拼接,如果请求较多,会对服务器造成一定的访问压力。
不利于前后端分离,开发效率低。使用服务器端渲染,则无法进行分工合作,尤其对于前端复杂度高的项目,不利于项目高效开发。
②基于前后端分离的新型 Web 开发模式
前后端分离的概念:前后端分离的开发模式,依赖于 Ajax 技术的广泛应用。简而言之,前后端分离的 Web 开发模式,就是后端只负责提供 API 接口,前端使用 Ajax 调用接口的开发模式。
优点:
开发体验好。前端专注于 UI 页面的开发,后端专注于api 的开发,且前端有更多的选择性。
用户体验好。Ajax 技术的广泛应用,极大的提高了用户的体验,可以轻松实现页面的局部刷新。
减轻了服务器端的渲染压力。因为页面最终是在每个用户的浏览器中生成的。
缺点:
不利于 SEO。因为完整的 HTML 页面需要在客户端动态拼接完成,所以爬虫对无法爬取页面的有效信息。(解决方案:利用 Vue、React 等前端框架的 SSR(server side render)技术能够很好的解决 SEO 问题!)
注意:具体使用何种开发模式并不是绝对的,为了同时兼顾了首页的渲染速度和前后端分离的开发效率,一些网站采用了首屏服务器端渲染 + 其他页面前后端分离的开发模式。
六、前后端身份认证
1、HTTP 协议的无状态性
了解 HTTP 协议的无状态性是进一步学习 Session 认证机制的必要前提。
HTTP 协议的无状态性,指的是客户端的每次 HTTP 请求都是独立的,连续多个请求之间没有直接的关系,服务器不会主动保留每次 HTTP 请求的状态。
对于超市来说,为了方便收银员在进行结算时给 VIP 用户打折,超市可以为每个 VIP 用户发放会员卡
注意:现实生活中的会员卡身份认证方式,在 Web 开发中的专业术语叫做 Cookie。
2、cookie
Cookie 是存储在用户浏览器中的一段不超过 4 KB 的字符串。它由一个名称(Name)、一个值(Value)和其它几个用于控制 Cookie 有效期、安全性、使用范围的可选属性组成。
不同域名下的 Cookie 各自独立,每当客户端发起请求时,会自动把当前域名下所有未过期的 Cookie 一同发送到服务器。
Cookie的几大特性:
自动发送
域名独立
过期时限
4KB 限制
客户端第一次请求服务器的时候,服务器通过响应头的形式,向客户端发送一个身份认证的 Cookie,客户端会自动将 Cookie 保存在浏览器中。
随后,当客户端浏览器每次请求服务器的时候,浏览器会自动将身份认证相关的 Cookie,通过请求头的形式发送给服务器,服务器即可验明客户端的身份。
由于 Cookie 是存储在浏览器中的,而且浏览器也提供了读写 Cookie 的 API,因此 Cookie 很容易被伪造,不具有安全性。因此不建议服务器将重要的隐私数据,通过 Cookie 的形式发送给浏览器。
工作原理:
4、express-session中间件
npm i express-session
express-session 中间件安装成功后,需要通过 app.use() 来注册 session 中间件
//导入session中间件
const session=require("express-session")
//配置session中间件
app.use(session({
secret: "keyboard cat", //secret可以是任意字符串
resave: false, //固定写法
saveUninitialized: true, //固定写法
}))
当 express-session 中间件配置成功后,即可通过 req.session 来访问和使用 session 对象,从而存储用户的关键信息:
app.post("/login", (req, res) => {
console.log(req.body)
//判断用户提交的登录信息是否正确
if (req.body.username!=="admin"||req.body.password!=="111111") {
returnres.send({
status: 1,
message: "登陆失败",
data: req.body
})
}
req.session.user=req.body //将用户的信息存储在session中
req.session.isLogin=true //记录用户登陆状态
res.send({
status: 0,
message: "登陆成功",
data: req.session
})
})
可以直接从 req.session 对象上获取之前存储的数据
app.get("/username", (req, res) => {
//判断用户是否登录
console.log(req.session.isLogin)
if (!req.session.isLogin) {
return res.send({
status: 1,
message: "fail",
data: req.session
})
}
res.send({
status: 0,
message: "success",
username: req.session.user.username
})
})
调用 req.session.destroy**()** 函数,即可清空服务器保存的 session 信息。
app.post("/logout", (req, res) => {
// req.session.destroy()
res.send({
status: 0,
message: "退出登陆成功",
data: req.session
})
})
Session 认证机制需要配合 Cookie 才能实现。由于 Cookie 默认不支持跨域访问,所以,当涉及到前端跨域请求后端接口的时候,需要做很多额外的配置,才能实现跨域 Session 认证。
注意:
当前端请求后端接口不存在跨域问题的时候,推荐使用 Session 身份认证机制。
当前端需要跨域请求后端接口的时候,不推荐使用 Session 身份认证机制,推荐使用 JWT 认证机制。
5、JWT
JWT(英文全称:JSON Web Token)是目前最流行的跨域认证解决方案。
用户的信息通过 Token 字符串的形式,保存在客户端浏览器中。服务器通过还原 Token 字符串的形式来认证用户的身份。
JWT 通常由三部分组成,分别是 Header(头部)、Payload(有效荷载)、Signature(签名)。
三者之间使用英文的“.”分隔
其中:
Payload 部分才是真正的用户信息,它是用户信息经过加密之后生成的字符串。
Header 和 Signature 是安全性相关的部分,只是为了保证 Token 的安全性。
(1)、使用方式
客户端收到服务器返回的 JWT 之后,通常会将它储存在 localStorage 或 sessionStorage 中。
此后,客户端每次与服务器通信,都要带上这个 JWT 的字符串,从而进行身份认证。推荐的做法是把 JWT 放在 HTTP 请求头的 Authorization 字段中,
(2)、安装jwt
npm i jsonwebtoken express-jwt
其中:
jsonwebtoken 用于生成 JWT 字符串
express-jwt 用于将 JWT 字符串解析还原成 JSON 对象
(3)、使用jwt
//导入用于生成jwt字符串的包
const jwt=require("jsonwebtoken")
//导入用于将客户端发过来的jwt字符串转换为json对象的包
const expressJWT=require("express-jwt")
secret密钥
为了保证 JWT 字符串的安全性,防止 JWT 字符串在网络传输过程中被别人破解,我们需要专门定义一个用于加密和解密的 secret 密钥:
①当生成 JWT 字符串的时候,需要使用 secret 密钥对用户的信息进行加密,最终得到加密好的 JWT 字符串
②当把 JWT 字符串解析还原成 JSON 对象的时候,需要使用 secret 密钥进行解密
调用 jsonwebtoken 包提供的 sign() 方法,将用户的信息加密成 JWT 字符串,响应给客户端
//用户登录成功后,生成jwt字符串,通过token属性响应给客户端
//参数1:用户信息对象
//参数2:加密的密钥
//参数3:配置对象,可配置当前token有效时间
res.send({
status: 0,
message: "登陆成功",
data: req.session,
token: jwt.sign({ username: req.session.user.username }, secretkey, { expiresIn: "30s" })
})
客户端每次在访问那些有权限接口的时候,都需要主动通过请求头中的 Authorization 字段,将 Token 字符串发送到服务器进行身份认证。
此时,服务器可以通过 express-jwt 这个中间件,自动将客户端发送过来的 Token 解析还原成 JSON 对象
// 允许跨域资源共享
const cors=require('cors')
app.use(cors())
// 解析 post 表单数据的中间件
const bodyParser=require('body-parser')
app.use(bodyParser.urlencoded({ extended: false }))
const secretKey='itheima jwt'
// 注意:只要配置成功了 express-jwt 这个中间件,就可以把解析出来的用户信息,挂载到 req.user 属性上,最新文档变为req.auth
app.use(expressJWT({ secret: secretKey }).unless({ path: [/^\/api\//] }))
// 登录接口
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: '登录失败!',
})
}
// 登录成功
// 在登录成功之后,调用 jwt.sign() 方法生成 JWT 字符串。并通过 token 属性发送给客户端
// 记住:千万不要把密码加密到 token 字符中
const tokenStr=jwt.sign({ username: userinfo.username }, secretKey, { expiresIn: '30s' })
res.send({
status: 200,
message: '登录成功!',
token: tokenStr, // 要发送给客户端的 token 字符串
})
})
// 这是一个有权限的 API 接口
app.get('/admin/getinfo', function (req, res) {
// 使用 req.user 获取用户信息,并使用 data 属性将用户信息发送给客户端
console.log(req.user)
res.send({
status: 200,
message: '获取用户信息成功!',
data: req.user, // 要发送给客户端的用户信息
})
})
// 使用全局错误处理中间件,捕获解析 JWT 失败后产生的错误
app.use((err, req, res, next) => {
// 这次错误是由 token 解析失败导致的
if (err.name==='UnauthorizedError') {
return res.send({
status: 401,
message: '无效的token',
})
}
res.send({
status: 500,
message: '未知的错误',
})
})
当使用 express-jwt 解析 Token 字符串时,如果客户端发送过来的 Token 字符串过期或不合法,会产生一个解析失败的错误,影响项目的正常运行。我们可以通过 Express 的错误中间件,捕获这个错误并进行相关的处理,