自学用——node.js(要点1)

一、文件


//引入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 之间的关系。

注意:

初次装包完成后,在项目文件夹下多一个叫做 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 的错误中间件,捕获这个错误并进行相关的处理,

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值