Node.js

什么是Node.js?

Node.js是一个基于Chrome V8引擎的JavaScript运行环境。

在Node.js环境中运行JavaScript代码
  1. 打开终端
  2. 输入node要执行的js文件的路径
终端里的快捷键
  • 使用⬆️,可以快速定位到上一次执行的命令。
  • 使用tab键可以快速补全路径。
  • 使用esc,能够快速清空当前已经输入的命令。
  • 输入clear命令,可以清空终端。

fs文件系统模块

什么是fs文件系统模块

fs模块是Node.js官方提供的。用于操作文件的模块。它提供了一系列的方法和属性,用来满足用户对文件的操作需求。

  • fs.readFile()方法,用来读取指定文件的内容
  • fs.writeFile()方法,用来向指定的文件中写入内容

如果要在JavaScript代码中,使用fs模块来操作文件,则需要使用如下的方法导入它:

const fs = require('fs')
读取指定文件里的内容
fs.readFile(path[, options], callback)
  • path:必选参数,字符串,表示文件的路径
  • [, options]:可选参数,表示以什么编码格式来读取文件
  • callback:必选参数,文件读取完成以后,通过回调函数拿到读取的结果

以utf8的编码格式,读取指定文件的内容,并打印err和dataStr的值:

const fs = require('fs')
fs.readFile('11.txt', 'utf8', function(err, dataStr){
    console.log(err);
    console.log('-----');
    console.log(dataStr);
})
  • 如果读取成功,则err的值为null
  • 如果读取失败,则err的值为错误对象, dataStr的值为undefined
判断文件是否读取成功

可以判断err对象是否为null,从而知晓文件读取的结果:

const fs = require('fs')
fs.readFile('11.txt', 'utf8', function(err, result){
    if ( err ) {
        return console.log('文件读取失败!' + err.message);
    }
    return console.log('文件读取成功,内容是:' + result);
})
向指定文件中写入内容
fs.writeFile(file, data[options], callback)
  • file:必选参数,需要指定一个文件路径的字符串,表示文件的存放路径
  • data:必选参数,表示要写入的内容
  • [options]:可选参数,表示以什么格式写入文件内容,默认值是utf8
  • callback:必选参数,文件写入完成后的回调函数
const fs = require('fs')
    fs.writeFile('2.txt', 'hello,lingyuancong', function(err){
        console.log(err);
    })
##### 判断文件是否写入成功
const fs = require('fs')
    fs.writeFile('2.txt', 'hello,lingyuancong', function(err){
        if(err){
            return console.log('文章写入失败!' + err.message);
        }
        console.log('文章写入成功!');
    })
联系–成绩整理案例

使用fs模块,将成绩.txt文件中的考试数据,整理到成绩ok.txt文件中。
整理前:

小红=99 小白=100 小黄=70 小黑=66 小路=88
const fs = require('fs')
    fs.readFile('11.txt', 'utf8', function(err, dataStr){
        if(err){
            return console.log('文章读取失败!' + err.message);
        }
        console.log(dataStr);
        const arrOld = dataStr.split(' ')
        const arrNew = []
        arrOld.forEach(item=>{
            arrNew.push(item.replace('=', ':'))
        })
        const newStr = arrNew.join('\r\n')
        fs.writeFile('2.txt', newStr, function(err){
            if(err){
                return console.log('写入失败!');
            }
            console.log('写入成功!');
        })
    })
路径拼接问题

在使用fs模块操作文件时,如果提供的操作路径是以./或…/开头的相对路径时,很容易出现路径动态拼接错误的问题。
原因:代码在运行时,会以执行node命令所在的目录进行动态拼接出被操作文件的完整目录。
解决方法:使用绝对路径,提供一个完整的文件存放路径。
缺点:移植性差,不利于后期维护。
最终方法:使用 __dirname(当前文件所处路径) 做路径拼接。

path路径模块

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

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

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

const path = require('path')
path.join()的语法格式

使用path.join()方法,可以吧多个路径片段拼接成完整的路径字符串,语法格式如下:

path.join([...paths])
  • …paths路径片段的序列
  • 返回值:字符串
const path = require('path')

const pathStr = path.join('/a', '/b/c', '../', './d', 'e') // ../会抵消前面一个路径
console.log(pathStr);  //输出/a/b/d/e

const pathStr2 = path.join(__dirname, './files/11.txt')
console.log(pathStr2); //输出/Users/lingyuancong/Desktop/files/11.txt

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

path.basename()的语法格式

使用path.basename()方法,可以获取路径的最后一部分,经常通过这个方法获取路径中的文件名,语法格式如下:

path.basename(path[ext])
  • path:必选参数,表示一个路径的字符串
  • ext:可选参数,表示文件拓展名
  • 返回值:表示路径的最后一部分
const fpath = '/a/b/c/d/index.html'

var fullName = path.basename(fpath)
console.log(fullName);  //输出 index.html

var nameWithoutExt = path.basename(fpath, '.html')
console.log(nameWithoutExt); //输出index
获取路径中的文件拓展名
path.extname()的语法格式

使用path.extname()方法,可以获取文件拓展名部分,语法格式如下:

path.extname(path)
  • path:必选参数,表示一个路径的字符串
  • 返回值:返回得到的拓展名字符串
const fpath = '/a/b/c/d/index.html'

const fext = path.extname(fpath)
console.log(fext);  //输出.html

什么是http模块

客户端:在网络节点中,负责消费资源的电脑,叫做客户端
服务器:负责对外提供网络资源的电脑,叫做服务器

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

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

const http = require('http')
进一步理解http模块的作用

服务器和普通电脑的区别在于,服务器上安装了web服务器软件,例如:IIS、Apache等。通过安装这些服务器软件,就能把一台普通的电脑变成一台web服务器。
在Node.js中,我们不需要使用IIs,Apache等这些第三方web服务器软件。因为我们可以基于Node.js提供的http模块,通过几行简单的代码,就能轻松的手写一个服务器软件,从而对外提供web服务。

服务器的相关概念
IP地址

IP地址就是互联网上每台计算机的唯一地址,因此IP地址具有唯一性。如果把“个人电脑”比作“一台电话”,那么“IP地址”就相当于“电话号码”,只有在知道对方“IP地址”的前提下,才能与对应的电脑之间进行数据通信。

IP地址的格式:通常用“点分十进制”表示成(a.b.c.d)的形式,其中,a,b,c,d都是0~255之间的十进制整数。例如:用点分十进表示的IP地址(192.168.1.1)。

⚠️

  1. 互联网中每台web服务器,都有自己的IP地址,例如:大家可以在Windows的终端中运行ping www.baidu.com 命令,即可以查看到百度服务器的IP地址。
  2. 在开发期间,自己的电脑既是一台服务器,也是一个客户端,为了方便测试,可以在自己的浏览器中输入127.0.0.1这个IP地址,就能把自己的电脑当作一台服务器访问了。
域名和域名服务器

尽管IP地址能够唯一的标记网络上的计算机,但IP地址是一长串数字,不直观,而且不便于记忆,于是人们又发明了另一套字符型的地址方案,即所谓的域名(Domain Name)地址。
IP和域名是一一对应的关系,这份对应关系放在一种叫做域名服务器的电脑中。使用者只需要通过好记的域名访问对应的服务器即可,对应的转换工作又域名服务器实现。因此,域名服务器就是提供IP地址和域名之间的转换服务的服务器。

注意:

  1. 单纯使用IP地址,互联网中的电脑也能正常工作。但是有了域名的加持,能让互联网的世界变得更加方便。
  2. 在开发测试掐尖,127.0.0.1对应的域名是localhost,它们都代表我们自己的这台电脑,在使用效果上没有任何区别。
端口号

计算机中的端口号,就好像是现实生活中的门牌号一样。通过门牌号,外卖小哥可以在整栋大楼众多房间里,准确的把外卖送到你的手里。
同样的道理,在一台电脑中,可以运行成百上千个web服务。每个web服务都对应一个唯一的端口号。客户端发送过来的网络请求,通过端口号,可以准确的交给对应的web服务进行处理。
注意:

  • 每个端口号不能被多个web服务占用
  • 在实际应用中,url的80端口可以被省略
创建最基本的web服务器

基本步骤:

  1. 导入http模块
  2. 创建web服务器实例
  3. 为服务器实例绑定request事件,监听客户端的请求
  4. 启动服务器
//导入http模块
const http = require('http')

//创建web服务器实例
const server = http.createServer()

//为服务器实例绑定request事件,使用server.on()方法
server.on('request', (req, res) => {
    console.log('Someone visit our web server.');
})

//启动服务器,调用服务器实例.listen()方法,即可启动当前的web服务器
server.listen(80, () => {
    console.log('http server running at http://127.0.0.1');
})
req请求对象

只要服务器接收到了客户端的请求,就会调用通过server.on()为服务器绑定的request事件处理函数。如果想在事件处理函数中,访问与可鹅湖段相关的数据和属性,可以使用如下的方式:

server.on('request', (req, res) => {
    const str = `Your request url is ${req.url}, add request method is ${req.method}`
    console.log(str);
})
res响应对象

在服务器的request事件处理函数中,如果想访问与服务器相关的数据或属性,可以使用如下的方式:

server.on('request', (req, res) => {
    const str = `Your request url is ${req.url}, add request method is ${req.method}`
    
    //调用res.end()方法,向客户端响应一些内容
    res.end(str)
})
解决中文乱码问题

当调用res.end()方法,向客户端发送中文内容的时候,会出现乱码现象,此时,需要手动设置内容的编码格式:

server.on('request', (req, res) => {
    const str = `您请求的url地址是${req.url},请求的method类型是${req.method}`
    
    //为了防止中文乱码,需要设置 Content-Type 的值为 text/html; charset=utf-8
    res.setHeader('Content-Type','text/html; charset=utf-8')

    //调用res.end()方法,向客户端响应一些内容
    res.end(str)
})
根据不同的URL响应不同的html内容
  1. 获取请求的URL地址
  2. 设置默认的响应内容为404 Not fount
  3. 判断用户请求的是否为/或/index.html首页
  4. 判断用户请求的是否为/about.html关于页面
  5. 设置Content-Type响应头,防止中文乱码
  6. 使用res.end()把内容响应给客户端
server.on('request', (req, res) => {
    const str = `您请求的url地址是${req.url},请求的method类型是${req.method}`
    
    //为了防止中文乱码,需要设置 Content-Type 的值为 text/html; charset=utf-8
    res.setHeader('Content-Type','text/html; charset=utf-8')

    //调用res.end()方法,向客户端响应一些内容
    res.end(str)
})
案例 - 实现html文件的web服务器请求

核心思路:
把文件的实际寻访路径,作为每个资源的请求URL地址。

实现步骤:

  1. 导入需要的模块
  2. 创建基本的web服务器
  3. 将资源的请求URL地址映射为文件的存放路径
  4. 读取文件并响应给客户端
  5. 优化资源的请求路径
//导入读取文件模块
const fs = require('fs')

//导入路径模块
const path = require('path')

//导入http模块
const http = require('http')

//创建web服务器实例
const server = http.createServer()

//为服务器实例绑定request事件,使用服务器实例.on()方法
server.on('request', (req, res) => {
    //获取客户端请求的URL地址
    const url = req.url
    

    //把请求的URL地址映射为具体文件的存放路径
    let fpath = ''
    if (url === '/') {
        fpath = path.join(__dirname, '/index.html')
    }else{
    fpath = path.join(__dirname, url)}
    console.log(fpath);

    //根据映射过来的文件路径来读取文件
    fs.readFile(fpath, 'utf8', (err, dataStr) => {
        //读取失败,向客户端响应固定的错误信息
        if(err) return res.end('404 not found.')
        //读取成功,向客户端响应成功的内容


        return res.end(dataStr)

    })
  
})

//启动服务器,调用服务器实例.listen()方法,即可启动当前的web服务器
server.listen(80, () => {
    console.log('http server running at http://127.0.0.1');
})

模块化

模块化的基本概念
什么是模块化

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

编程领域的模块化,就是遵守固定的规则,把一个大文件拆成独立并互相依赖的多个小模块。

  1. 提高了代码的复用性
  2. 提高了代码的可维护性
  3. 可以实现按需加载
模块化的规范

模块化规范就是对代码进行模块化的拆分和组合时,需要遵守的哪些规则。

  • 使用什么样的语法格式来引入模块
  • 在模块中使用什么样的语法格式向外暴露成员

遵循模块化规范的好处:大家都遵守同样的模块化代码规范写代码,降低了沟通的成本,极大方便了各个模块之间的调用,利人利己。

Node.js中的模块化
Node.js中模块的分类

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

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

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

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

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

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

注意:使用require()方法加载其他模块时,会执行被加载模块中的代码。加载用户自定义模块时,可以省略 .js 后缀名。

Node.js的模块作用域

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

模块作用域防止了全局变量污染的问题,模块中的变量只能在模块中访问。

向外共享模块作用域的成员

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

Module {
  id: '.',
  path: '/Users/lingyuancong/Desktop',
  exports: {},
  filename: '/Users/lingyuancong/Desktop/1.js',
  loaded: false,
  children: [],
  paths: [
    '/Users/lingyuancong/Desktop/node_modules',
    '/Users/lingyuancong/node_modules',
    '/Users/node_modules',
    '/node_modules'
  ]
}
module.exports对象

在自定义模块中,可以使用module.exports对象,将模块中的成员共享出去,供外界使用。外界使用require()方法得到的就是module.exports所指向的对象。

//module.exports()默认导出的是个空对象

//向 module.exports 对象上挂载 username 属性
module.exports.username = 'zhangsan'

//向 module.exports 对象上挂载 sayHello 方法
module.exports.sayHello = function(){
    console.log('hello');
}
//在外界使用require导入一个自定义模块的时候,得到的成员就是那个模块中,通过module.exports指向的那个对象。

const m = require('./2')  //导入自定义模块

console.log(m); //得到{ username: 'zhangsan', sayHello: [Function (anonymous)] }
共享成员的注意点

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

//module.exports()默认导出的是个空对象

//向 module.exports 对象上挂载 username 属性
module.exports.username = 'zhangsan'

//向 module.exports 对象上挂载 sayHello 方法
module.exports.sayHello = function(){
    console.log('hello');
}

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

    sayHi : function(){
        console.log('hi');
    },
}

最终接受到的是:
{ nickname: ‘kris’, sayHi: [Function: sayHi] }

exports对象

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

exports 和 module.exports 的使用误区

时刻谨记,使用require()模块时,得到的永远是 module.exports 指向的对象:
为了避免混乱,建议大家不要在一个模块中同时使用 exports 和 module.exports。

Node.js中的模块化规范

Node.js 遵循了 Common.js 模块化规范,CommonJs 规定了模块的特性和个模块之间如何相互依赖。
CommonJs 规定:

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

npm与包

什么是包

Node.js中第三方模块又叫做包。
就像电脑和计算机指的是相同的东西,第三方模块和包指的是同一个概念,只不过叫法不同。

包的来源

不同于Node.js的内置模块和自定义模块,包是有第三方个人或团队开发出来的,免费供所有人使用。
⚠️Node.js中的包都是免费且开源的,不要付费即可免费下载使用。

为什么需要包

由于Node.js的内置模块只提供了一些底层的API,导致在基于内置模块进行项目开发时,效率很低。
包是基于内置模块封装出来的,提供了更高级,更方便的API,极大的提高了开发效率。
包和内置模块之间的关系,类似于jQuery和浏览器内置API之间的关系。

从哪里下载包

国外有一家IT公司,叫做npm,Inc,这家公司旗下有一个非常著名的网站:http://www.npmjs.com/,它是全球最大的包共享平台,你可以从这个网站上搜索到任何你想要的包,只要你有足够的耐心!
nom,Inc.公司提供了一个地址为:http://registry.npmjs.org/的服务器,对外共享所有的包,我们可以从这个服务器上下载自己所需要的包。

注意:

  • 从 http://www.npmjs.com/ 网站上搜索自己所需要的包
  • 从 http://registry.npmjs.org/ 服务器上下载自己要的包
如何下载包

nom,Inc.公司提供了一个包管理工具,我们可以直接使用这个包管理工具,从http://registry.nomjs.org/ 服务器上把需要的包下载到本地使用。
这个包管理工具的名字叫做Node Package Manage(简称npm包管理工具),这个包管理工具随着Node.js的安装包一起被安装到了用户的电脑上。
大家可以在终端中执行 npm -v 命令,来查看自己电脑上所安装的 npm 包管理工具的版本号。

npm初体验

格式化时间的传统做法:

  1. 创建格式化时间的自定义模块
  2. 自定义格式化时间的方法
  3. 创建补零函数
  4. 从自定义模块中到处格式化时间的函数
  5. 导入格式化时间的自定义模块
  6. 调用格式化时间的函数
//定义格式化时间的方法
function dateFormat(dtStr){
    const dt = new Date(dtStr)

    const y = dt.getFullYear()
    const m = padZero(dt.getMonth() + 1)
    const d = padZero(dt.getDate())

    const hh = padZero(dt.getHours())
    const mm = padZero(dt.getMinutes())
    const ss = padZero(dt.getSeconds())

    return `${y}-${m}-${d} ${hh}:${mm}:${ss}`
}

//定义补零的函数
function padZero(n){
    return n > 9 ? n : '0' + n
}

module.exports = {
    dateFormat
}
//在外界使用require导入一个自定义模块的时候,得到的成员就是那个模块中,通过module.exports指向的那个对象。

const Time = require('./2')  //导入自定义模块

const dt = new Date()
console.log(dt);

const newDT = Time.dateFormat(dt)
console.log(newDT);
格式化时间的高级做法
  1. 使用npm包管理工具,在项目中安装格式化时间的包 moment
  2. 使用require()导入格式化时间的包
  3. 参考 moment 的官方 API 文档对时间进行格式化

在项目中安装包的命令:npm install 包的完整名称可以简写为npm i 包的完整名称

//先安装moment模块:npm i moment
const moment = require('moment')

//使用moment模块对时间进行格式化
const dt = moment().format('YYYY-MM-DD HH:mm:ss')

console.log(dt);
初次装包后多了哪些文件

初次装包完成后,在项目文件夹下多了一个叫做node_modules的文件夹和pac kage-lock.json的配置文件。
其中:
node_modules 文件夹用来存放所有已安装到项目中的包,require()导入第三方包时,即使从这个目录里查找并加载包。
package-lock.json 配置文件用来记录 node_modules 目录下的每一个包的下载信息,例如包的名字、版本号、下载地址等等。

注意:不要手动修改 node_modules 或 package-lock.json 文件中的任何代码,7pm 包管理工具会自动维护它们。

安装指定版本的包

默认情况下,使用 npm install 命令安装包时会自动安装最新版本的包,如果需要安装指定版本的包,可以在包名后面,通过@符号指定具体的版本。

包的语义化版本规范

包的版本号是以‘点分十进制’形式进行定义的,总共有三位数字,例如2.24.0
其中每一位数字所代表的含义如下:

  1. 大版本
  2. 功能版本
  3. bug修复版本

版本号提升规则:只要前面的版本号增长了,则后面的版本号归零。

包管理配置文件

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

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

多人协作的问题:
由于第三方包体积过大,不方便团队成员之间共享项目源码。

如何记录项目中用到了哪些包

在项目根目录中,创建一个叫做package.json的配置文件,即可记录项目中安装了哪些包,从而方便剔除node_modules目录之后,在团队成员之间共享项目源码。
注意:在项目开发中,一定要把node_modules文件夹,添加到.gitignore忽略文件里。

如何快速的创建package.json

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

npm init -y //项目文件夹创建后即执行

注意:

  • 上述命令只能在英文的目录下运行成功,所以,项目文件夹的名称一定要用英文命名,不要使用中文,不能出现空格。
  • 运行7pm install 命令安装包时,npm包管理攻击会自动把包的名称和版本号记录到package.json中。
dependencies 节点

package.json文件中,有一个dependencies节点,专门用来记录您使用npm install 命令安装了哪些包。

一次性安装所有的包

当我们拿到了一个剔除了node_modules的项目之后,需要把所有的包下载到项目里,才能将项目运行起来,否则就会报类似于下面的错误:

Error: Cannot find module "moment"

我们可以运行npm install (npm I)命令,可以一次性安装所有的依赖包。

卸载包

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

npm uninstall moment 

注意:npm uninstall 命令执行完毕之后,会把卸载的包,自定聪package.json的dependencies中移除掉。

devDependencies节点

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

//安装指定的包到devDependencies节点中
npm i -D
//上述为简写,完整写法为:
npm install 包名 --save-dev
解决下包速度慢的问题

在使用npm下包的时候,默认从国外的https://registry.npmjs.org/服务器进行下载,此时,网络数据的传输需要经过漫长的海底光缆,因此下载速度会很慢。

淘宝npm镜像服务器

淘宝在国内搭建了一个服务器,专门把国外服务器上的包同步到国内的服务器,然后在国内提供下包服务,从而极大的提高了下包的速度。

切换npm的下包镜像源
//查看当前的下包镜像源
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
包的分类
项目包

哪些被安装到项目的node_modules目录中的包,都是项目包。
项目包又被分为两类:

  • 开发依赖包(被记录到devDependenices节点中的包,只会在开发期间会用到)
  • 核心依赖包(被记录到dependencis节点中的包,在开发期间和项目上线后都会用到)
全局包

在执行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这三个属性,分别代表包的名字,版本号,包的入口。
开发属于自己的包
需要实现的功能
  • 格式化日期
  • 转义html中的特殊字符
  • 还原html中的特殊字符
初始化包的基本结构
  1. 新建lingyuancong-tools文件夹,作为包的根目录
  2. 在lingyuancong-tools文件夹中,新建如下三个文件:
package.json(包管理配置文件)
index.js(包的入口文件)
README.md(包的文档说明)
//这是包的入口文件

function dateFormat(dtStr){
   
        const dt = new Date(dtStr)
    
        const y = dt.getFullYear()
        const m = padZero(dt.getMonth() + 1)
        const d = padZero(dt.getDate())
    
        const hh = padZero(dt.getHours())
        const mm = padZero(dt.getMinutes())
        const ss = padZero(dt.getSeconds())
    
        return `${y}-${m}-${d} ${hh}:${mm}:${ss}`
    }
    
    //定义补零的函数
    function padZero(n){
        return n > 9 ? n : '0' + n
    }
    //定义转义html字符的函数
    function htmlEscape(htmlStr){
        return htmlStr.replace(/<|>|“|&/g, (match) => {
            switch (match) {
                case "<":
                    return "&lt;"
                case ">":
                    return "&gt;"
                case '"':
                    return "&quot;"
                case "&":
                    return "&amp;"
            }
        })
    }
    //定义转义html字符的函数
    function htmlUnEscape(str){
        return str.replace(/&lt;|&gt;|&quot;|&amp;/g, (match) => {
            switch (match) {
                case "&lt;":
                    return "<"
                case "&gt;":
                    return ">"
                case "&quot;":
                    return '"'
                case "&amp;":
                    return "&"
            }
        })
    }
   

module.exports = {
    dateFormat, htmlEscape, htmlUnEscape
}
将不同的功能进行模块化拆分
  • 将格式化时间的功能,拆分到src -> dateFormat.js中
  • 将处理html字符串的功能,拆分到src -> htmlEscape.js中
  • 在index.js中,导入两个模块,得到需要向外共享的方法
  • 在index.js中,用module.exports将模块共享出去

function dateFormat(dtStr){
   
    const dt = new Date(dtStr)

    const y = dt.getFullYear()
    const m = padZero(dt.getMonth() + 1)
    const d = padZero(dt.getDate())

    const hh = padZero(dt.getHours())
    const mm = padZero(dt.getMinutes())
    const ss = padZero(dt.getSeconds())

    return `${y}-${m}-${d} ${hh}:${mm}:${ss}`
}

//定义补零的函数
function padZero(n){
    return n > 9 ? n : '0' + n
}

module.exports = {
    dateFormat
}
 //定义转义html字符的函数
 function htmlEscape(htmlStr){
    return htmlStr.replace(/<|>|“|&/g, (match) => {
        switch (match) {
            case "<":
                return "&lt;"
            case ">":
                return "&gt;"
            case '"':
                return "&quot;"
            case "&":
                return "&amp;"
        }
    })
}
//定义转义html字符的函数
function htmlUnEscape(str){
    return str.replace(/&lt;|&gt;|&quot;|&amp;/g, (match) => {
        switch (match) {
            case "&lt;":
                return "<"
            case "&gt;":
                return ">"
            case "&quot;":
                return '"'
            case "&amp;":
                return "&"
        }
    })
}

module.exports = {
    htmlEscape, htmlUnEscape
}
//这是包的入口文件
const date = require('./src/dateFormat')
const excape = require('./src/htmlEscape')


module.exports = {
    ...date,
    ...excape
}

编写包的说明文档

包根目录下的README.md文件,市包的使用说明文档。通过它,我们可以事先把包的使用说明,以markdown的格式写出来,方便用户参考。
README.md文件具体写什么内容,没有强制性的要求;只要能够清晰的将包的作用、用法、注意事项等描述清楚就可以了。
我们创建的包的README.md文档,会包含以下6项内容:

  • 安装方式
  • 导入方式
  • 格式化时间
  • 转义html中的特殊字符
  • 还原html中的特殊字符
  • 开源协议
发布包
  1. 注册npm账号
  2. 登陆npm账号
    npm账号注册完成后,可以在终端中执行npm login命令,依次输入用户名、密码、邮箱后,即可登录成功。
    注意:要确保使用的是npm的官方服务器,不是淘宝镜像。
  3. 把包发布到npm上
    将终端切换到根目录,运行npm publish命令,即可将包发布到npm上(注意:包名不能雷同)
  4. 删除已发布的包
    运行npm unpublish包名 --force命令,即可从npm删除已发布的包。
    ⚠️ npm unpublish只能删除72小时以内发布的包,已经删除的包,在24小时之内不允许重新发布。
模块的加载机制
优先从缓存中加载

模块在第一次加载后会缓存,这也意味着多次调用require()不会导致模块的代码被执行多次。
注意:不论是哪只模块,用户自定义模块,还是第三方模块,都遵循优先缓存加载,从而提高模块的加载效率。

内置模块的加载机制

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

自定义模块的加载机制

使用require()加载自定义模块时,必须指定以./或…/开头的路径标识符。在加载自定义模块时,如果没有指定./或…/这样的路径标识符,则node会把它当作内置模块或者第三方模块进行加载。
同时,在使用require()导入自定义模块时,如果省略了文件的拓展名,则Node.js会按顺序分别尝试加载一下的文件:

  1. 按照确切的文件名进行加载
  2. 补全.js拓展名进行加载
  3. 补全.json拓展名进行加载
  4. 补全.node拓展名进行加载
  5. 加载失败,终端报错
第三方模块的加载机制

如果传递给require()的模块标识符不是一个自定义模块,也没有以 ‘./’ 或 ‘…/’ 开头,则Node.js会从当前模块的父目录开始,尝试从/node_modules文件夹中加载第三方模块。
如果没有找到对应的第三方模块,则移动到上一层父目录中,进行加载,直到文件系统的根目录。

目录作为模块

当把目录作为模块比奥识符,传递给require()进行加载时,有三种加载方式:

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

Express时基于Node.js平台,快速,开放,极简的web开发框架。
通俗解释:Express 的作用和Node.js内置的http模块类似,是用来专门创建Web服务器的。
Express的本质:就是一个npm的第三方包,提供了快速创建web服务器的便捷方法。

Express能够做什么?

对于前端程序员来说,最常见的两种服务器,分别是:

  • Web网站服务器:专门对外提供Web网页资源的服务器
  • API接口服务器:专门对外提供API接口的服务器

使用Express,我们可以方便,快速的创建web网站服务器或API接口的服务器。

Express的基本用法
安装

在项目所处的目录中,运行如下的终端命令,即可将express安装到项目中使用:

npm i express@4.17.1
创建基本的web服务器
// 导入express
const express = require('express')

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

// 监听客户端的GET和POST请求,并向客户端响应具体的内容
app.get('/user', (req, res) => {
    // 调用express 提供的 res.send()方法,向客户端响应一个 json 对象
    res.send({name: 'zs', age: 20,gender: '男'})
})

// 监听客户端的post请求
app.post('/user', (req, res) => {
    res.send('请求成功')
})

// 启动服务器
app.listen(80, ()=>{
    console.log('express server running at http://127.0.0.1');
})	```
###### 监听GET请求
通过app.get()方法,可以监听客户端的GET请求,具体的语法如下:

```javascript
1 // 参数1:客户端请求的url地址
2 // 参数2:请求对应的处理函数
3 //	req:请求对象(包含了与请求相关的属性和方法)
4 //	res:响应对象(包含了与响应相关的属性和方法)
5 app.get('请求url', function(req, res){处理函数})
监听POST请求
1 // 参数1:客户端请求的url地址
2 // 参数2:请求对应的处理函数
3 //	req:请求对象(包含了与请求相关的属性和方法)
4 //	res:响应对象(包含了与响应相关的属性和方法)
5 app.post('请求url', function(req, res){处理函数})
把内容响应给客户端

通过res.send()方法,可以把处理好的内容,发送给客户端:

app.get('/user', (req, res) => {
	//向客户端发送 json 对象
	res.send({ name: 'zs', age: 20, gender: '男'})
})

app.post('/user', (req, res) => {
	//向客户端发送文本内容
	res.send('请求内容')
})
获取URL中携带的查询参数

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

app.get('/', (req, res) => {
	// req.query 默认是一个空对象
	// 客户端通过使用 ?name=zs&age=20 这种查询字符串的方式,发送到服务器的参数
	// 可以通过req.query 对象访问到,例如:
	// req.query.name. req.query.age
	
	console.log(req.query)
})
获取URL中的动态参数

通过req.params对象,可以访问到url中,通过:匹配到的动态参数:

// 获取URL中的动态参数
app.get('/user/:id', (req, res) => {
    // req.params 默认是一个空对象
    // 里面存放着通过 : 匹配到的参数值
    console.log(req.params);
})
托管静态资源
express.static()

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

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

这样就可以访问public目录中的所有文件了:
http://localhost:8000/images/bg.jpg

注意:Express在指定的静态目录里查找文件,并对外提供资源的访问路径,因此,存放静态资源的目录名不会出现在url中。

托管多个静态资源目录

如果要托管多个静态资源目录,请多次调用express.static()函数:

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

注意:访问静态资源文件时,express.static()函数会根据目录的添加顺序查找所需要的文件。

挂载路径前缀

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

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

现在,需要通过带有/public前缀地址来访问public目录里的文件了:
http://localhost:8000/public/images/bg.jpg

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监听到,从而实现自动重启项目的效果。

Express路由
路由

路由就是映射关系。

Express中的路由

在Express中,路由指的是客户端的请求与服务器处理函数之间的映射关系。
Express中的路由分3部分组成,分别是请求的类型,请求的URL地址,处理函数,格式如下:

app.METHOD(PATH, HANDLER)
路由的匹配过程

每当一个请求到达服务器之后,需要先经过路由的匹配,只有匹配成功之后,才会调用对应的处理函数。
在匹配是,会按照路由的顺序进行匹配,如果请求类型和请求的url同时匹配成功,则Express会将这次请求,转交给对应的function函数进行处理。
路由匹配的注意点:

  1. 按照定义的先后顺序进行匹配
  2. 请求类型和请求的URL同时匹配成功才会调用对应的处理函数
路由的使用

在express中使用路由最简单的方式就是把路由挂载到app上,示例代码如下:

app.get('/', (req, res) => { res.send('hello world' )})
app.post('/', (req, res) => { res.send('hello world') })
模块化路由

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

  1. 创建路由模块对应的.js文件
  2. 调用express.Router()函数创建路由对象
  3. 向路由对象上挂载具体的路由
  4. 使用module.exports向外共享路由对象
  5. 使用app.user()函数注册路由模块
// 导入路由模块
var express = require('express')
// 创建路由对象
var router = express.Router()

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

router.post('/user/add', function(req, res){
    res.send('Add new user.')
})

// 向外导出路由
module.exports = router
注册路由模块
const express = require('express')
const app = express()
// 导入路由模块
const router = require('./Router')

// 使用app.use()函数注册路由模块
app.use(router)

app.listen(80, () => {
    console.log('http:/127.0.0.1/');
})

app.use()函数的作用,就是来注册全局中间件的。

为路由模块添加前缀

类似于托管静态资源时,为静态资源统一挂载访问前缀一样,路由模块添加前缀的方式也非常简单:

app.use('/api', router)
中间件

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

Express的中间件,本质上就是一个function处理函数,Express中间件的格式如下:

在这里插入图片描述
注意:中间件函数的形参列表中,必须包含next函数,而路由处理函数中只包含req和res。

next函数的作用

next函数是实现多个中间件连续调用的关键,它表示把流转关系转交给下一个中间件或路由。
在这里插入图片描述

定义中间件函数

可以通过如下的方式,定义一个最简单的中间件函数:

// 常量 mw 所指向的,就是一个中间件函数
const mw = function(req, res, next){
    console.log('这是一个简单的中间件函数');
    // 注意:在当前中间件的业务处理完毕之后,必须要调用next()函数
    // 表示把流转关系交给下一个中间件或路由
    next()
}
全局生效的中间件

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

const express = require('express')
const app = express()


// 常量 mw 所指向的,就是一个中间件函数
const mw = function(req, res, next){
    console.log('这是一个简单的中间件函数');
    // 注意:在当前中间件的业务处理完毕之后,必须要调用next()函数
    // 表示把流转关系交给下一个中间件或路由
    next()
}

// 将 mw 注册为全局生效的中间件
app.use(mw)

app.get('/', (req, res) => {
    res.send('Home page.')
})
app.get('/user', (req, res) => {
    res.send('User page.')
})

app.listen(80, () => {
    console.log('http://127.0.0.1');
})
定义全局中间件的简化形式
// 将 mw 注册为全局生效的中间件
app.use(function(req, res, next){
    console.log('这是一个简单的中间件函数');
    // 注意:在当前中间件的业务处理完毕之后,必须要调用next()函数
    // 表示把流转关系交给下一个中间件或路由
    next()
})
中间件的作用

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

定义多个全局中间件

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

app.use(function(req, res, next){
    console.log('第一个全局中间件');
    next()
})
app.use(function(req, res, next){
    console.log('第二个全局中间件');
    next()
})
局部生效的中间件

不实用app.use()定义的中间件,叫做局部生效的中间件,示例代码如下:

// 不使用app.use()函数定义的局部中间件
const mw1 = function(req, res, next){
    console.log('这是中间件函数');
    next()
}

// 在这个路由下,局部中间件生效
app.get('/', mw1, (req, res) => {
    res.send('Home page.' + req.startTime)
})
// 在这个路由下,局部中间件不生效
app.get('/user', (req, res) => {
    res.send('User page.' + req.startTime)
})
定义多个局部生效的中间件

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

app.get('/', mw1, mw2, (req, res) => {
    res.send('Home page.' + req.startTime)
})
app.get('/user', [mw1, mw2],(req, res) => {
    res.send('User page.' + req.startTime)
})
了解中间件的5个使用注意事项
  1. 一定要在路由之前注册中间件
  2. 客户端发送过来的请求,可以连续调用多个中间件进行处理
  3. 执行完中间件的业务代码之后,不要忘记调用next()函数
  4. 为了防止带啊逻辑混乱,调用next()函数后不要再写额外的代码
  5. 连续调用多个中间件时,多个中间件之间,共享req和res对象
中间件的分类

为了方便大家理解和记忆中间件的实用化,Express官方把常见的中间件用法分成了5大类,分别是:

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

通过app.use()或app.get()或app.post(),绑定到app实例上的中间件,叫做应用级别的中间件,示例代码如下:

// 应用级别的中间件(全局中间件)
app.use((req, res, next) => {
	next()
})
// 应用级别的中间件(局部中间件)
app.get('/', mw1), (req, res) => {
	res.send('Home page.')
}
路由级别的中间件

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

var app = express()
var router = Router()

// 路由级别的中间件
router.use(function(req, res, next){
	console.log('Time', Date.now())
	next()
})
app.use('/', router)
错误级别的中间件

错误级别中间件的作用:专门用来捕获整个项目中发生的异常错误,从而防止项目异常奔溃的问题。
格式:错误级别的中间件的function处理函数中,必须有4个形参,参数顺序从前到后,分别是(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); //在服务器打印错误消息
    res.send('Error!' + err.message) //向客户端响应相关的错误内容
})

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

Express内置的中间件

自Express 4.16.0 版本开始,Express内置了3个常用的中间件,极大的提高了Express项目的开发效率和体验:

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

非express官方内置的,而是由第三方开发出来的中间件,叫做第三方中间件。在项目中,大家可以按需下载并配置第三方中间件,从而提高项目的开发效率。
例如:在express@4.16.0之前的版本中,经常使用body-parser这个第三方中间件,来解析请求体数据。使用步骤如下:

  1. 运行npm install body-parser安装中间件
  2. 使用 require 导入中间件
  3. 调用 app.use() 注册并使用中间件
// 导入解析表单数据的中间件 body-parser
const parser = require('body-parser')
// 使用app.user()注册中间件
app.use(parser.urlencoded({ extended: false }))

app.post('/user', (req, res) => {
    console.log(req.body);
    res.send('ok')
})

注意:Express内置的express.urlencoded中间件,就是基于body-parser这个第三方中间件进一步封装出来的。

自定义中间件

手动模拟一个类似于express.urlencoded这样的中间件,来解析POST提交到服务器的表单数据。
实现步骤:

  1. 定义中间件
  2. 监听req的data事件
  3. 监听req的end事件
  4. 使用querystring模块解析请求体数据
  5. 将解析出来的数据对象挂载为req.body
  6. 将自定义中间件封装为模块
const express = require('express')
const app = express()

// 导入处理querystring的Node.js模块
const querystring = require('querystring')

// 解析表单数据解析的中间件
app.use(function(req, res, next){
    // 中间件的业务逻辑
    // 在中间件里,需要监听req对象的data事件,来获取客户端发送到服务器的数据
    // 如果数据量比较大,无法一次性发送完毕,则客户端会把数据切割后,分批发送到服务器。
    // data事件可能会出发多次,每一次触发data事件,获取到的数据只是完整数据的一小部分,需要手动对数据进行拼接。
    
    // 定义变量,用来储存客户端发送过来的请求体数据
    let str = ''
    // 监听req对象的data是阿金(客户端发送过来的新的请求体数据)
    req.on('data', (chunk) => {
        // 拼接请求体数据,隐式转换成字符串
        str += chunk
    })

    // 监听req的end事件
    // 当请求体数据接收完毕之后,会自动触发req的end事件
    // 在req的end事件中,拿到并处理完整的请求体数据

    // 监听req对象的end事件(请求体发送完成后自动发布)
    req.on('end', () => {
        // 打印完整的请求体数据
        // console.log(str);
        // TODO: 把字符串格式的请求体数据,解析成对象格式
        // Node.js内置了一个querystring模块,专门用来处理查询字符串。通过这个模块提供的parse()函数,可以轻松把查询的字符串,解析成对象的格式。
        const body = querystring.parse(str)
        // console.log(body);
        // 将解析出来的数据挂载到req.body
        req.body = body
        next()
    })
})

app.post('/test', (req, res) => {
    res.send(req.body)
})

app.listen(80, function(){
    console.log('http://127.0.0.1');
})
将自定义的中间件封装为模块

为了优化代码的结构,我们可以把自定义的中间件函数,分装为独立的模块:


// 导入处理querystring的Node.js模块
const querystring = require('querystring')

// 解析表单数据解析的中间件
const bodyParser = function(req, res, next){
    // 中间件的业务逻辑
    // 在中间件里,需要监听req对象的data事件,来获取客户端发送到服务器的数据
    // 如果数据量比较大,无法一次性发送完毕,则客户端会把数据切割后,分批发送到服务器。
    // data事件可能会出发多次,每一次触发data事件,获取到的数据只是完整数据的一小部分,需要手动对数据进行拼接。
    
    // 定义变量,用来储存客户端发送过来的请求体数据
    let str = ''
    // 监听req对象的data是阿金(客户端发送过来的新的请求体数据)
    req.on('data', (chunk) => {
        // 拼接请求体数据,隐式转换成字符串
        str += chunk
    })

    // 监听req的end事件
    // 当请求体数据接收完毕之后,会自动触发req的end事件
    // 在req的end事件中,拿到并处理完整的请求体数据

    // 监听req对象的end事件(请求体发送完成后自动发布)
    req.on('end', () => {
        // 打印完整的请求体数据
        // console.log(str);
        // TODO: 把字符串格式的请求体数据,解析成对象格式
        // Node.js内置了一个querystring模块,专门用来处理查询字符串。通过这个模块提供的parse()函数,可以轻松把查询的字符串,解析成对象的格式。
        const body = querystring.parse(str)
        // console.log(body);
        // 将解析出来的数据挂载到req.body
        req.body = body
        next()
    })
}

module.exports = bodyParser
const express = require('express')
const app = express()

const bodyParser = require('./test')
// 将自定义的中间件函数注册成为全局可用的中间件
app.use(bodyParser)

app.post('/test', (req, res) => {
    res.send(req.body)
})

app.listen(80, function(){
    console.log('http://127.0.0.1');
})
使用Express写接口
创建基本的服务器
// 导入express模块
const express = require('express')
// 创建服务器实例
const app = express()


// 启动服务器
app.listen(80, function(){
    console.log('http://127.0.0.1');
})
创建API路由模块
// apiRouter.js(路由模块)
const express = require('express')
const apiRouter = express.Router()

// 在这里挂载对应的路由

module.exports = apiRouter
// 导入express模块
const express = require('express')
// 创建服务器实例
const app = express()

// 导入并注册路由模块
const router = require('./Router')
app.use('/user', router)

// 启动服务器
app.listen(80, function(){
    console.log('http://127.0.0.1');
})
编写get接口和post接口
// apiRouter.js(路由模块)
const express = require('express')
const router = express.Router()

// 在这里挂载对应的路由
router.get('/get', (req, res) => {
    // 通过req.query获取客户端通过查询字符串,发送到服务器的数据
    const query = req.query
    // 调用res.send()方法,向客户端响应处理的结果
    res.send({
        status: 0, //0表示处理成功,1表示处理失败
        msg: 'get请求成功', // 状态的描述
        data: query // 需要响应给客户端的数据
    })
})
// 要获取url-encoded格式体的请求体数据,必须配置中间件app.use(express.urlencoded({ extended:false }))
router.post('/post', (req, res) => {
    // 通过req.query获取客户端通过查询字符串,发送到服务器的数据
    const body = req.body
    // 调用res.send()方法,向客户端响应处理的结果
    res.send({
        status: 0, //0表示处理成功,1表示处理失败
        msg: 'post请求成功', // 状态的描述
        data: body // 需要响应给客户端的数据
    })
})

module.exports = router
// 导入express模块
const express = require('express')
// 创建服务器实例
const app = express()

// 导入路由模块
const router = require('./apiRouter')
// 配置url-encoded中间件
app.use(express.urlencoded({ extended:false }))
// 注册路由模块
app.use('/api', router)

// 启动服务器
app.listen(80, function(){
    console.log('http://127.0.0.1');
})
CORS跨域资源共享
接口的跨域问题

刚才编写的GET和POST接口,存在一个很严重的问题:不支持跨域请求。

CORS响应头部-Access-Control-Allow-Origin

响应头中可以携带一个Access-Control-Allow-Origin字段,其语法如下:

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

其中,origin参数的值制定了允许访问该资源的外域URL,如果指定了字段的值为通配符*,表示允许来自任何域的请求。
例如,下面的字段将只允许来自http://itcast.cn的请求:

res.setHeader('Access-Control-Allow-Origin', 'http://itcast.cn')
CORS响应头部-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')
CORS响应头部-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')
// 允许所有方法
res.setHeader('Access-Control-Allow-Methods', '*')
CORS请求的分类

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

  1. 简单请求
  2. 预检请求
简单请求

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

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

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

  1. 请求方式:GET,POST,HEAD之外的请求METHOD类型
  2. 请求头中包含自定义头部字段
  3. 向服务器发送了application/json格式的数据

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

简单请求和预检请求的区别

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

JSONP接口

浏览器端通过

  1. JSONP不属于真正的Ajax请求,因为它没有使用XMLHttpRequest这个对象。
  2. JSONP仅支持GET请求,不支持POST,PUT,DELETE等请求。
创建JSONP接口的注意事项

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

实现JSONP接口的步骤
  1. 获取客户端发送过来的回调函数的名字
  2. 得到要通过JSONP形式发送给客户端的数据
  3. 根据前两步得到的数据,拼接成一个函数调用的字符串
  4. 把上一步拼接得到的字符串,响应给客户端的
// 必须在配置CORS中间件之前,配置JSONP接口
app.get('/api/jsonp', (req, res) => {
    // TODO:定义JSONP接口的具体实现过程
    // 得到函数的名称
    const funcName = req.query.callback
    // 定义要发送给客户端的数据对象
    const data = { name: 'zs', age: 22 }
    // 拼接出一个函数的调用
    const scriptStr = `${funcName}(${JSON.stringify(data)})`
    // 把拼接好的字符串响应给客户端
    res.send(scriptStr)
})

数据库

什么是数据库

数据库(database)是用来组织、存储和管理数据的仓库。
为了方便管理互联网世界中的数据,就有了数据管理系统的概念(简称:数据库)。用户可以对数据库中的数据进行增删改查等操作。

常见的数据库及分类

市面上最常见的数据库有很多种,最常见的有以下几种:

  • Mysql数据库(目前使用最广泛、流行度最高的开源免费数据库;Community + Enterprise)
  • Oracle数据库(收费)
  • SQL Server数据库(收费)
  • Mongodb数据库(Community + Enterprise)

其中,Mysql,Oracle,Sql Server属于传统型数据库(又叫做:关系型数据库或SQL数据库),这三者的设计理念相同,用法比较类似。
而Mongodb属于新型数据库(又叫做:非关系型数据库或NoSQL数据库),他在一定程度上弥补了传统数据库的缺陷。

传统型数据库的数据组织结构

数据的组织结构:指的就是数据以什么样的结构进行存储。
传统数据库的数据组织结构,与Excel中数据的组织结构比较类似。
因此,我们可以对比着Excel来了解和学习传统数据库的数据组织结构。

Excel的数据组织结构

每个Excel中,数据的组织结构分别为工作簿,工作表,数据行,列这4大部分组成。

传统型数据库的数据组织结构

在传统型数据库中,数据的组织结构分为数据库(database)、数据表(table)、数据行(row)、字段(field)这4大部分组成。

实际开发中库、表、行、字段的关系
  1. 在实际项目开发中,一般情况下,每个项目都对应独立的数据库
  2. 不同的数据,要储存到数据库的不同表中,例如:用户数据存储到user表里,图书数据存储到books表里。
  3. 每个表里具体存储哪些信息,由字段来决定,例如:我们可以为user表设计id、username、password这三个字段。
  4. 表中的行,代表每一条具体的数据。
安装并配置Mysql数据库

对于开发人员来说,只需要安装Mysql Server 和 Mysql Workbench这两个软件,就能满足开发的需要了。

  • Mysql server:专门用来提供数据存储和服务的软件
  • Mysql Workbench:可视化的Mysql管理工具,通过它,可以方便的操作存储在MySQL Server中的数据。
使用sql管理数据库
什么是sql

SQL是结构化查询语言,专门用来访问和处理数据库的编程语言。能够让我们以编程的形式,操作数据库里的数据。
三个关键点:

  1. SQL是一门数据库编程语言
  2. 使用SQL语言编写出来的代码,叫做SQL语句
  3. SQL只能在关系型数据库中使用
SQL能够做什么
  1. 从数据库里查询数据
  2. 向数据库里插入新数据
  3. 更新数据库里的数据
  4. 从数据库中删除数据
  5. 可以创建新数据库
  6. 可在数据库中创建新表
  7. 可在数据库中创建储存过程、视图
  8. etc…
SQL的学习目标

重点掌握如何使用SQL从数据表中:

  • 查询数据(select)
  • 插入数据(insert into)
  • 更新数据(update)
  • 删除数据(delete)

额外需要掌握的4种SQL语法:

  • where条件
  • and和or运算符
  • order by排序
  • count(*)函数
语法

SELECT语句用于从表中查询数据。执行的结果被存储在一个结果表中(称为结果表)。语法格式如下:

-- 这是注释
-- 从FROM指定的表中,查询出【所有的】数据。*表示【所有列】
SELECT * FROM 表的名称

-- 从FROM指定的【表中】,查询出指定列名称(字段)的数据
SELECT 列名称 FROM 表名称

注意:SQL语句中的关键字对大小写不敏感。SELECT等效于select,FROM等效于from。

select * 示例

我们希望从users表中选取所有的列,可以用符号 * 取代列的名称,示例如下:

select * from my_db_01.users
select 列名称示例

如需获取名为“username”和“password”的列的内容,请启用下面的select语句:

select username, password from my_db_01.users
insert into语句

insert into语句用于向数据表中插入新的数据行,语法格式如下:

-- 语法解读:向指定的表中,插入如下几行数据,列的值通过values 一一指定
-- 注意:列和值要一一对应,多个列和多个值之间,使用英文的逗号分隔
insert into table_name (列一,列二...) values (值一,值二...)
insert into users (username, password) values ('ling', 123456789)
update语句

update语句用于修改表中的数据。语法数据如下:

-- 用update指定要更新那个表中的数据
-- 用set指定列对应的新值
-- 用where指定更新的条件

update 表名称 set 列名称 = 新值 where 列名称 = 某值
update users set password = 88888888 where id = 6
update users set password = 88888888, status = 1 where id = 6
delete语句
-- 指定的表中,根据where条件,删除对应的数据行
delete from 表名称 where 列名称 =
delete from users where id = 6
SQL的where语句

where子句用于限定选择的标准。在select、update、delete语句中,皆可使用where子句来限定选择的标准。

-- 查询语句中的where条件
select 列名称 from 表名称 where 列 运算符 值
-- 更新语句中的where条件
update 表名称 set=新值 where 列 运算符 值
-- 删除语句中的where条件
delete from 表名称 where 列 运算符 值
可在where子句中使用的运算符

下面的运算符可在where子句中使用,用来限定选择的标准:

操作符描述
=等于
<>不等于
>大于
<小于
>=大于等于
<=小于等于
between在某个范围内
like搜索某种格式

注意:在某些版本的SQL中,操作<>可以写为 !=

-- 查询status为1的所有用户
select * from users where status =1
-- 查询id大于 2 的所有用户
select * from users where id>2
-- 查询username不等于ling的所有的用户
select * from users where username <> 'ling'
and和or运算符

and和or可在where子语句中把两个或多个条件结合起来。
and必须同时满足多个条件,相当于javascript中的&&运算符。
or表示只要满足任意一个条件即可,相当于javascript中的||运算符。

select * from users where status = 0 and id < 7
select * from users where status = 1 or username = 'zs'
SQL的ORDER BY语句

ORDER BY语句用于根据指定列对结果进行排序。
ORDER BY语句默认按照升序对记录进行排序。
如果您希望按照降序对记录进行排序,可以使用DESC关键字。

-- 默认升序
select * from users order by status
-- 使用desc降序
select * from users order by status desc
-- 使用-降序
select * from users order by - status 
多重排序
-- 对users表中的数据,先按照status字段进行降序排序,再按照username的字母排序,进行升序排序
select * from users order by - status,username 
count(*)函数

count(*)函数用于返回查询结果的总数据条数:

select count(*) from users where status = 1
使用AS为列设置别名

如果希望给查询出来的列名设置别名,可以使用AS关键字,示例如下:

select count(*) as total from users where status = 1
在项目中操作数据库的步骤
  1. 安装操作数据库Mysql数据库的第三方模块(mysql)
  2. 通过mysql模块连接到Mysql数据库
  3. 通过mysql模块执行SQL模块
安装与配置mysql模块

mysql模块是托管于npm上的第三方模块。它提供了再Node.js项目中链接和操作mysql数据库的能力。

npm i mysql
// 导入mysql模块
const mysql = require('mysql')
// 建立与mysql数据库的连接关系
const db = mysql.createConnection({
    host: '127.0.0.1', // 数据库的IP地址
    user: 'root', // 登陆账号
    password: '88888888', // 登陆密码
    database: 'my_db_01', // 指定要操作哪个数据库
})
测试mysql模块能否正常工作

调用dp.query()函数,指定要执行的sql语句,通过回调函数拿到要执行的结果:node.js不支持mysql8.0版本加密方式,连接mysql失败,选择安装mysql2连接npm un mysql && npm i mysql2

// 导入mysql模块
const mysql = require('mysql2')
// 建立与mysql数据库的连接关系
const db = mysql.createPool({
    host: '127.0.0.1', // 数据库的IP地址
    user: 'root', // 登陆账号
    password: '88888888', // 登陆密码
    database: 'my_db_01', // 指定要操作哪个数据库
})

// 测试mysql模块能否正常工作
db.query('select 1', (err, results) => {
    // mysql模块工作期间
    if(err) {
        // 连接失败
        return console.log(err.message);
    }
    // 连接成功
    return console.log(results);
    // 
})
在node.js里用sql语句
查询数据
// 查询users表中所有的数据
const sqlStr = 'select * from users'
db.query(sqlStr, (err, results) => {
    if(err) return console.log(err.message);
    return console.log(results);
})
插入数据

插入数据
向users表中新增数据,其中username为Spider-Man,password为pcc321

// 直接插入数据
const sqlInsert = 'insert into users (username, password) values ("Spider-Man", "pcc321")'
db.query(sqlInsert, (err, results) => {
    if(err) return console.log(err.message);
    return console.log(results);
})
// 使用插入对象方式
// 要插入的对象
const user = { username: 'Spider-Man1', password: 'pcc321' }
// 待执行的sql语句,其中英文的?表示占位符
const sqlInsert = 'insert into users (username, password) values (?, ?)'
// 使用数组的形式,依次为?占位符指定具体的值
db.query(sqlInsert, [user.username, user.password], (err, results) => {
    if(err) return console.log(err.message);
    return console.log(results);
})
// 插入数据的便捷方式
const user = { username: 'Spider-Man2', password: 'pcc321' }
// 待执行的SQL语句,其中英文的?表示占位符
const sqlInsert = 'insert into users set ?'
db.query(sqlInsert, user, (err, results) => {
    if(err) return console.log(err.message);
    return console.log(results);
})
更新数据
// 更新表中的数据
// 要更新的数据对象
const user = { id: 10, username: 'aaa', password: 0000 }
// 要执行SQL语句
const sqlStr = 'update users set username=?, password=? where id=?'
db.query(sqlStr, [user.username, user.password, user.id], (err, results) => {
    if(err) return console.log(err.message);
    return console.log('更新成功');
})
// 更新数据的便捷方式
const user = { id: 10, username: 'Spider', password: 'pcc321' }
// 待执行的SQL语句,其中英文的?表示占位符
const sqlInsert = 'update users set ? where id=?'
db.query(sqlInsert, [user, user.id], (err, results) => {
    if(err) return console.log(err.message);
    return console.log(results);
})
删除数据

在删除数据时,推荐根据id这样的唯一标识,来删除对应的数据。示例如下:

// 删除数据
// 要执行的SQL语句
const sqlStr = 'delete from users where id=?'
// 调用db.query()执行SQL语句的时候,要为占位符指定具体的值
// 注意:如果有多个占位符,则必须使用数组为每个占位符指定具体的值, 如果只有一个占位符,则可以参略数组
db.query(sqlStr, 10, (err, results) => {
    if(err) return console.log(err.message);
    return console.log('删除成功');
})
标记删除

使用delete语句,会真正的把数据从表中删除掉,为了保险起见,推荐使用标记删除的形式,来模拟删除的动作。
所谓的标记删除,就是在表中设置类似于status这样的状态字段,来标记当前这条数据是否被删除。
当用户执行了删除的动作时,我们并没有执行delete语句把数据删除掉,而是执行了update语句,将这条数据对应的status字段标记为删除即可。

// 标记删除
const sqlStr = 'update users set status=? where id=?'
db.query(sqlStr, [1, 9], (err, results) => {
    if(err) return console.log(err.message);
    return console.log('标记删除成功');
})
前后端开发认证
web开发模式

目前主流的开发模式有两种,分别是:

  1. 基于服务端渲染的传统web开发模式
  2. 基于前后端分离的新型web开发模式
服务端渲染的web开发模式

服务器渲染的概念:服务器发送给客户端的html页面,是在服务器通过字符串的拼接,动态生成的。因此,客户端不需要使用ajax这样的技术额外请求页面的数据。示例代码如下:

app.get('/index.html', (req, res) => {
    // 要渲染的数据
    const user = { name: 'zs', age: '20' }
    // 通过服务器动态拼接,动态生成html内容
    const html = `<h1>姓名:${user.name},年龄:${user.age}</h1>`
    // 把生成好的页面响应给客户端,因此,客户端拿到的是真实的html页面
    res.send(html)
})

优点:

  1. 前端耗时少。因为服务器端负责生成html内容,浏览器只需要直接渲染页面即可。尤其是移动端,更省电。
  2. 有利于seo。因为服务端响应的是完整的html页面内容,所以爬虫更容易爬取获得信息,更有利于seo。

缺点:

  1. 占用服务器资源。及服务器完成html页面的拼接,如果请求较多,会给服务器造成一定的访问压力。
  2. 不利于前后端分离,开发效率低。使用服务器渲染,则无法进行分工合作,尤其是前端复杂度高的项目,不利于项目高效开发。
前后端分离的web开发模式

前后端分离的概念:前后端分离的开发模式,依赖于ajax技术的广泛应用。简而言之,前后端分离的web开发模式,就是后端只负责提供api接口,前端使用ajax调用接口的开发模式。
优点:

  1. 开发体验好。前端专注于ui页面的开发,后端专注于api的开发,且前端有更多的选择性。
  2. 用户体验好。ajax技术的广泛应用,极大的提高了用户的体验,可以轻松实现页面的局部刷新。
  3. 减轻了服务器的渲染压力。因为页面最终是在每个用户的浏览器中生成的。

缺点:

  1. 不利于seo。因为完整的html页面需要在客户端动态拼接完成,所以爬虫对无法爬取页面的有效信息。(解决方案:利用vue、react等前端框架的ssr(server side render)技术能够很好的解决seo问题。)
如何选择web开发模式
  • 比如企业级网站,主要功能是展示而没有复杂的用户,并且需要良好的seo,则这时我们就需要使用服务器端渲染。
  • 类似于后台管理系统,交互性比较强,不需要靠路seo,那么就可以使用前后端分离 的开发模式。

另外,具体使用何种开发模式并不是绝对的,为了同时兼顾了首页的渲染速度和前后端分离的开发效率,一些网站采用了首屏服务器渲染+其他页面前后端分离的开发模式。

身份认证

身份认证,又称“身份认证”、“鉴权”,是指通过一定的手段,完成对用户身份的确认。

不同模式下的身份认证

对于服务器渲染和前后端分离这两种开发模式来说,分别有着不同的身份认证方案:

  • 服务器渲染推荐使用session认证机制
  • 前后端分离推荐使用jwt认证机制
session认证机制
http协议的无状态性

了解http协议的无状态性是进一步学习session认证机制的必要前提。
http协议的无状态性,指的是客户端每次的http请求都是独立的,连续多个请求之间没有直接的关系,服务器不会主动保留每次http请求的状态。
客户端向服务器提供身份标识在web中的专业术语叫做Cookie。

什么是Cookie

Cookie是存储在用户浏览器中一段不超过4kb的字符串。它由一个名称、一个值和其他几个用于Cookie有效期、安全性、使用范围的可选属性组成。
不同域名下的Cookie各自独立,每当客户端发起请求时,会自动把当前域名下的所有为过期的Cookie一同发送到服务器。

Cookie的几大特性
  1. 自动发送
  2. 域名独立
  3. 过期时限
  4. 4kb限制
Cookie在身份认证中的作用

客户端在第一次请求服务器的时候,服务器通过响应头的形式,向客户端发送一个身份认证的Cookie,客户端会自动将Cookie保存在浏览器中。
随后,在客户端浏览器每次请求服务器的时候,浏览器护理自动将身份认证相关的Cookie,通过请求头的形式发送给服务器,服务器即可验明客户端的身份。

Cookie不具有安全性

由于Cookie是存储在浏览器中的,而且浏览器也提供了读写Cookie的API,因此Cookie很容易被伪造,不具有安全性。因此,因此不建议服务器将重要的隐私数据,通过Cookie的形式发送给浏览器。
⚠️千万不要使用Cookie存储重要且隐私的数据!比如用户的身份信息、密码等。

提高身份认证的安全性

在这里插入图片描述

在express中使用Session认证
安装express-session中间件

在Express项目中,只需要安装express-session中间件,即可在项目中使用Session认证:

## 安装express-session中间件
npm i express-ssion
// 导入express-session中间件
const session = require('express-session')
app.use(session({
    secret: 'lingyuancong', // 可以是任意值
    resave: false,          // 固定写法
    saveUninitialized: true,// 固定写法
}))
向session中存数据

当express-session中间件配置成功之后,即可通过req.session来访问和使用session对象,从而存储用户的关键信息:

app.post('/api/login', (req, res) => {
    // 判断用户提交的信息是否正确
    if (req.body.username !== 'admin' || req.body.password !== '000000') {
        return res.send({
            status: 0,
            msg: '登录失败!'
        })
    }
    // 将登陆成功后的数据保存到Session中
    // 只有配置了express-session中间件之后,才能通过req点出来session这个属性
    req.session.user = req.body
    req.session.islogin = true
})
从session中取数据

可以直接从req.session中直接获取之前存储的数据:

// 获取用户姓名的接口
app.get('/api/username', (req, res) => {
    // 判断用户是否登陆
    if (!req.session.islogin) {
        return res.send({
            status: 1,
            msg: 'fail'
        })
    }
    res.send({
        status: 0,
        msg: 'success',
        username: req.session.user.username
    })
})
清空session

调用req.session.destroy()函数,就可以清空服务器保存的session信息:

// 退出登陆的接口
app.post('/api/logout', (req, res) => {
    // 清空当前的客户端对应的session信息
    req.session.destroy()
    res.send({
        status: 0,
        msg: '退出成功!'
    })
})
jwt
了解Session认证的局限性

Session认证机制需要配合Cookie才能实现。由于Cookie默认不支持跨域访问,所以,当涉及到前端跨域请求后段接口的时候,需要做很多额外的配置,才能实现跨域Session认证。
注意:

  • 当前端请求后端接口不存在跨域问题的时候,推荐使用Session身份认证机制
  • 当前端需要跨域请求后端接口的时候,不推荐使用Session身份认证机制,推荐使用jwt认证机制。
什么是jwt

jwt是目前最流行的跨域认证解决方案。

jwt的工作原理

在这里插入图片描述

总结:用户的信息通过Token字符串的形式,保存在客户端浏览器中,服务器通过还原Token字符串的形式来认证用户的身份。

jwt的组成部分

jwt通常由三部分组成,分别是Header(头部),Payload(有效荷载),Signature(签名)。
三者之间使用英文的“.”分隔,格式如下:

Header.Payload.Signature

其中:

  • Payload部分才是真正的用户信息,它是用户信息经过加密之后生成的字符串。
  • Header和Signature是安全性相关的部分,只是为了保证Token的安全性。
jwt的使用方式

客户端收到服务器返回的JWT之后,通常会将它储存在localStorage或sessionStorage中。
此后,客户端每次与服务器通信,都要带上这个JWT的字符串,从而进行身份认证,推荐的做法是把JWT放在HTTP请求头的Authorization字段中,格式如下:

Authorization: Bearer <token>
安装JWT相关的包

运行如下命令,安装如下两个JWT相关的包:

npm install jsonwebtoken express-jwt

其中:

  • jsonwebtoken用于生成JWT字符串
  • express-jwt用于将JWT字符串解析还原成JSON字符串
导入JWT相关的包

使用require()函数,分别导入JWT相关的两个包:

// 导入用于生成JWT字符串的包
const jwt = require('jsonwebtoken')
// 导入用于将客户端发送过来的JWT字符串,解析成还原成JSON对象包
const expressJWT = require('express-jwt')
定义secret密钥

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

  1. 当生成JWT字符串的时候,需要使用secret密钥对用户的信息进行加密,最终得到加密好的JWT字符串
  2. 当把JWT字符串解析还原成JSON对象的时候,需要使用secret密钥进行解密。
// 定义secret密钥,建议将密钥命名为secretKey
const secretKey = 'lingyuancong'
在登陆成功后生成JWT字符串

调用jsonwebtoken包提供的sign()方法,将用户的信息加密成JWT字符串,响应给客户端:
获取req.body为undefind,是express版本原因,将body-parser插件抽离出来了,需要在前面配置该插件:

npm i body-parser
var bodyParser = require('body-parser')

app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: false }))
app.post('/api/login', (req, res) => {
    // 判断用户提交的信息是否正确
    if (req.body.username !== 'admin' || req.body.password !== '000000') {
        return res.send({
            status: 0,
            msg: '登录失败!'
        })
    }
    res.send({
        status: 200,
        mes: '登录成功',
        // 调用jwt.sign()生成JWT字符串,三个参数分别是:用户信息对象,加密密钥,配置对象
        token: jwt.sign({ username: userinfo.username }, secretKey, { expiresIn: '30s' }) // 30s有效
    })
})
将JWT字符串还原成JSON对象

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

// 注册将JWT字符串解析还原成JSON对象的中间件
// expressJWT({ secret: secretKey })用来解析Token的中间件
// .unless({ path: [/^\/api\//] }))用来指定哪些接口不需要访问权限
app.use(expressJWT.express({ secret: secretKey,algorithms: ['HS256'] }).unless({ path: [/^\/api\//] }))
使用req.user获取用户信息

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

// 这是一个有权限的api接口
app.get('/admin/getinfo', (req, res) => {
    res.send({
        status: 200,
        msg: '获取用户信息成功',
        data: {
			username: req.auth.username
		}
    })
})
捕获解析JWT失败后产生的错误

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

app.use((err, req, res, next) => {
    // token 解析失败导致的错误
    if(err.name === 'UnauthorizedError'){
        return res.send({
            status: 401,
            message: '无效的token'
        })
    // 其它原因导致的错误
    res.send({
        status: 500,
        msg: '未知错误'
    })
    }
})
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值