初识Node

  • 什么是Node.js?

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

  • 注意:

浏览器是JavaScript的前端运行环境

Node.js是JavaScript的后端运行环境

Node.js中无法调用DOM和BOM等浏览器内置API

  • Node.js可以做什么

①基于Express框架,可以快速构建web应用

②基于Electron框架,可以构建跨平台的桌面应用

③基于restify框架,可以快速构建API接口项目

④读写和操作数据库、创建实用的命令行工具辅助前端开发

fs文件系统模块:

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

  • 例如:

fs.readFile()方法:用来读取指定文件中的内容

fs.writeFile()方法:用来向指定文件中写入内容

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

const fs = require('fs');

1、fs.readFile()的语法格式:

fs.readFile(path[,options],callback)

参数一:必选参数,字符串,表示文件的路径

参数二:可选参数:表示以什么编码格式来读取

参数三:必选参数,文件读取完成后,通过回到函数拿到读取的结果

  • 示例代码:
// 导入fs模块
const fs = require("fs");
// 调用
fs.readFile("./files/1.txt", "utf8", function (err, dataStr) {
  console.log(err);
  console.log(dataStr);
});

如果读取成功,err的值为null

如果读取失败,err的值为 错误对象,dataStr的值为undefined

  • 判断文件是否读取成功
const fs = require("fs");
​
fs.readFile("./files/1.txt", "utf8", function (err, dataStr) {
  if (err) {
    return console.log("读取失败" + err.message);
  }
  console.log("读取成功" + dataStr);
});

2、向指定的文件写入内容:

fs.writeFile()的语法格式

fs.writeFile(file,data[,options],callback)

参数一:必选参数,需要指定一个文件路径的字符串,表示文件的存放路径

参数二:必选参数,表示要写入的内容

参数三:可选参数:表示以什么格式写入文件内容,默认值为utf8

参数四:必选参数:文件写入完成后的回调函数

  • 示例代码:
const fs = require("fs");
​
fs.writeFile("./files/2.txt", "abcd", function (err) {
  // 如果写入成功,err为null
  // 如果写入失败,err为错误对象
  console.log(err);
});

判断文件是否写入成功:

const fs = require("fs");
​
fs.writeFile("./files/3.txt", "ok", function (err) {
  // 如果写入成功,err为null
  // 如果写入失败,err为错误对象
  console.log(err);
​
  if (err) {
    return console.log("文件写入失败" + err.message);
  }
  console.log("文件写入成功");
});
​

考试成绩整理:

const fs = require("fs");
​
fs.readFile("./files/成绩.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("=", ":"));
  });
  console.log(arrNew);
  // 将新数组中的每一项,进行合并,得到一个新的字符串
  const newStr = arrNew.join("\r\n");
  console.log(newStr);
  // 存
  fs.writeFile("./files/成绩-ok.txt", newStr, function (err) {
    if (err) {
      console.log("写入文件失败" + err.message);
    }
    console.log("写入成功");
  });
});
​

路径动态拼接的问题:

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

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

  • __dirname 表示当前文件夹的目录

path路径模块:

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

例如:

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

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

const path = require('path')

路径拼接:

  • path.join()语法格式
path.join([...paths])
  • ...paths<string>路径片段序列
  • 返回值:<string>

代码示例:

const path = require("path");
​
const pathStr = path.join("/a", "/b/c", "../", "/d", "/e");
console.log(pathStr);
​
const pathStr2 = path.join(__dirname, "./files/1.txt");
console.log(pathStr2);
​

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

const path = require("path");
const fs = require("fs");
​
const pathStr = path.join("/a", "/b/c", "../", "/d", "/e");
console.log(pathStr);
​
const pathStr2 = path.join(__dirname, "./files/1.txt");
console.log(pathStr2);
​
fs.readFile(
  path.join(__dirname, "/files/1.txt"),
  "utf8",
  function (err, dataStr) {
    if (err) {
      return console.log(err.message);
    }
    console.log(dataStr);
  }
);

获取路径中的文件名:

  • path.basename()的语法格式

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

path.basename(path[,ext])
  • path<string>必选参数,表示一个路径的字符串
  • ext<string>可选参数,表示文件扩展名
  • 返回:<string>表示路径中的最后一部分
const path = require("path");
const fpath = "/a/b/c/index.html";
// 带上文件扩展名
const fullName = path.basename(fpath);
console.log(fullName);
// 只拿文件名称
const nameWithoutExt = path.basename(fpath, ".html");
console.log(nameWithoutExt);

使用path.extname()方法可以获取路径中的扩展名部分,语法如下:

path.extname(path)
  • path<string>必选参数,表示一个路径的字符串
  • 返回:<string>返回得到的扩展名字符串
const fpath = "/a/b/c/index.html";
// 获取文件扩展名
const fext = path.extname(fpath);
console.log(fext);

拆分html、css、js合为一体的文件

实现步骤:

  • 创建两个正则表达式,分别用来匹配<style><script>标签
  • 使用fs模块,读取需要被处理的html文件
  • 自定义resolveCSS方法,来写入index.css样式文件
  • 自定义resolveJS方法,来写入index.js脚本文件
  • 自定义resolveHTML方法,来写入index.html文件

匹配正则:

//匹配<style></style>标签的正则
//其中 \s 表示空白字符;\S 表示非空白字符; *表示撇配任意次
const regStyle = /<style>[\s\S]*<\/style>/
//匹配<script></script>标签的正则
const regScript = /<script>[\s\S]*<\/script>/

案例代码:

// 导入fs
const fs = require("fs");
// 导入path
const path = require("path");

const regStyle = /<style>[\s\S]*<\/style>/;
const regScript = /<script>[\s\S]*<\/script>/;

// 调用fs.readFile()
fs.readFile(
  path.join(__dirname, "./files/index.html"),
  "utf8",
  function (err, dataStr) {
    if (err) return console.log("读取失败" + err.message);
    resolveCSS(dataStr);
    resolveJS(dataStr);
    resolveHTML(dataStr);
  }
);

// 处理css
function resolveCSS(htmlStr) {
  // 使用正则提取页面中<style></style>标签
  const r1 = regStyle.exec(htmlStr);
  // console.log(r1);
  // 将提取出来的样式字符串,做进一步处理
  const newCSS = r1[0].replace("<style>", "").replace("</style>", "");
  // console.log(newCSS);
  // // 将提取出来的css样式,写入到index.css文件中
  fs.writeFile(path.join(__dirname, "./files/index.css"), newCSS, (err) => {
    if (err) {
      return console.log("写入CSS样式失败" + err.message);
    }
    console.log("写入css样式成功");
  });
}

// 处理js脚本
function resolveJS(htmlStr) {
  // 使用正则读取页面中的<script></script>标签
  const r2 = regScript.exec(htmlStr);
  // 将提取出来的脚本字符串,做进一步的处理
  const newJS = r2[0].replace("<script>", "").replace("</script>", "");
  // 将提取出来的js脚本,写入到index.js文件中
  fs.writeFile(path.join(__dirname, "./files/index.js"), newJS, (err) => {
    if (err) return console.log("写入JavaScript脚本失败" + err.message);
    console.log("写入JS脚本");
  });
}

// 处理html文件
function resolveHTML(htmlStr) {
  // 使用字符串replace方法,把内嵌<style>和<script>标签,替换为外联的<link>和<script>标签
  const newHTML = htmlStr
    .replace(regStyle, '<link rel="stylesheet" href="./files/index.css">')
    .replace(regScript, '<script src="./files/index.js"></script>');
  // 替换完成后的html代码,写入到index.html文件中
  fs.writeFile(
    path.join(__dirname, "./files/Newindex.html"),
    newHTML,
    (err) => {
      if (err) return console.log("写入html文件失败" + err.message);
      console.log("写入HTML页面成功");
    }
  );
}

注意点:

  • fs.writeFile()方法只能用来创建文件,不能用来创建路径
  • 重复调用fs.writeFile()写入同一个文件,新写入的内容会覆盖之前的旧内容

HTTP模块:

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

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

const http = require('http')

创建web服务器的基本步骤:

①导入http模块

②创建web服务器实例

③为服务器实例绑定request事件,监听客户端的请求

④启动服务器

  • 导入http模块:
const http = require('http')
  • 创建web服务器实例
const server = http.createServer()
  • 为服务器实例绑定request事件
server.on('request',(req,res)=>{
    //只要有客户端来请求我们自己的服务器,就会触发request事件,从而调用这个事件处理函数
    console.log('Someone visit our web server')
})
  • 启动服务器
//调用werver.listen(端口号,cb回调)方法,即可启动web服务器
server.listen(80,()=>{
    console.log('http server running at http://127.0.0.1')
})
  • req请求对象:

只要服务器接收到了客户端的请求,就会通过server.on()为服务器绑定的request事件处理函数

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

server.on('request',(req)=>{
    //req是请求对象,它包含了与客户端相关的数据和属性,例如:
    //req.url 是客户端请求的URL地址
    //req.method 是客户端的method请求类型
    const str = `Your request url is ${req.url},and request method is ${req.method}`
    console.log(str)
})
  • res响应对象:

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

server.on('request',(req,res)=>{
    //res响应对象,它包含了与服务器相关的数据和属性,例如:
    //要发送到客户端的字符串
    const str = `Your request url is ${req.url},and request method is ${req.method}`
    //res.end()方法的作用
    //向客户端发送指定的内容,并结束这次请求的处理过程
    res.end(str)
})

解决中文乱码问题:

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

res.setHeader('Content-Type','text/html;charset=utf-8')

根据不同的url响应不同的内容:

核心实现步骤:

  • 获取请求的url地址
  • 设置默认的响应内容为404 Not found
  • 判断用户请求的是否为/或/index.html首页
  • 判断用户请求的是否为/about.html关于页面
  • 设置Content-Type响应头,防止中文乱码
  • 使用res.end()把内容响应给客户端
const http = require("http");
const server = http.createServer();
server.on("request", (req, res) => {
  // 获取请求的url地址
  const url = req.url;
  // 设置默认的响应内容为404 Not found
  let content = "404 Not Found!";
  // 判断用户请求的是否为/或/index.html首页
  if (url == "/" || url == "/index.html") {
    content = "<h1>首页</h1>";
  }
  // 判断用户请求的是否为/about.html关于页面
  else if (url == "/about") {
    content = "<h1>关于</h1>";
  }
  // 设置Content-Type响应头,防止中文乱码
  res.setHeader("Content-Type", "text/html;charset=utf-8");
  // 使用res.end()把内容响应给客户端
  res.end(content);
});

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

请求本地文件:

实现步骤:

  • 导入需要的模块
  • 创建基本的web服务器
  • 将资源的请求url地址映射为文件的存放路径
  • 读取文件内容并响应给客户端
  • 优化资源的请求路径
// 导入http模块
const http = require("http");
// 导入fs模块
const fs = require("fs");
// 导入path文件
const path = require("path");

// 创建web服务器
const server = http.createServer();
// 监听web服务器
server.on("request", (req, res) => {
  //获取到客户请求的url
  const url = req.url;
  console.log(url);
  //把请求的url地址映射为具体的存放路径
  // const fpath = path.join(__dirname, "../node/files", url);
  let fpath = "";
  if (url == "/") {
    fpath = path.join(__dirname, "../node/files/index.html");
  } else {
    fpath = path.join(__dirname, "../", "/node/files", url);
  }
  console.log(fpath);
  fs.readFile(fpath, "utf8", (err, dataStr) => {
    if (err) return console.log("404 Not Fount");
    res.end(dataStr);
  });
});
// 启动服务器
server.listen(80, () => {
  console.log("server running at http://127.0.0.1");
});

模块化:

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

  • 变成邻域中的模块化:

遵守固定的规则,把一个大文件拆分成独立并互相依赖的多个小模块

好处:

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

模块化规范:

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

例如:

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

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

Node.js中模块的分类:

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

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

加载模块:

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

注意:在使用require()方法,可以省略后缀名

模块作用域:

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

好处:防止全局污染

module对象:

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

Module {
  id: '.',
  path: 'C:\\Users\\admin\\Desktop\\前端知识点\\code\\node\\web服务器',
  exports: {},
  parent: null,
  filename: 'C:\\Users\\admin\\Desktop\\前端知识点\\code\\node\\web服务器\\10.演 
示module对象.js',
  loaded: false,
  children: [],
  paths: [
    'C:\\Users\\admin\\Desktop\\前端知识点\\code\\node\\web服务器\\node_modules',    'C:\\Users\\admin\\Desktop\\前端知识点\\code\\node\\node_modules',
    'C:\\Users\\admin\\Desktop\\前端知识点\\code\\node_modules',
    'C:\\Users\\admin\\Desktop\\前端知识点\\node_modules',
    'C:\\Users\\admin\\Desktop\\node_modules',
    'C:\\Users\\admin\\node_modules',
    'C:\\Users\\node_modules',
    'C:\\node_modules'
  ]
}
  • module.exports对象:

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

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

  • 共享成员时注意点

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

exports对象

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

image-20220306155718379.png

注意:为了防治混乱,建议不要在同一个模块中同时使用exports和module.exports

Node.js中的模块化规范:

Node.js遵循了CommonJS模块化规范,CommonJS规定了模块的特性和各模块之间如何让相互依赖

CommonJS规定:

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

包:

  • 什么是包:

Node.js中的第三方模块又叫做包

  • 包的来源:

不同于Node.js中的内置模块与自定义模块,包是由第三方个人或团队开发出来的,免费供所有人使用

  • 为什么需要包:

由于Node.js的内置模块仅提供了一些底层的API,导致在基于内置模块进行项目开发时,效率低,包是基于内置模块封装出来的,提高了更高级、更方便的API,极大的提高了开发效率

npm初体验:

  • 格式化时间的传统做法:

创建格式化时间的自定义模块

定义格式化时间的方法

创建补零函数

从自定义模块中导出格式化时间的函数

导入格式化时间的自定义模块

调用格式化时间的函数

  • dateFormat.js:
// 定义格式化时间的方法
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 = 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,
};
  • test.js:
// 导入自定义格式化时间的模块
const TIME = require("./dateFormat");
// 调用方法,进行时间的格式化
const dt = new Date();
const newDT = TIME.dateFormat(dt);
console.log(newDT);
  • 格式化时间的高级做法:

使用npm包管理工具,在项目中安装格式化时间的包 moment

使用require()导入格式化时间的包

参考moment的官方API文档对时间进行格式化

  • 在项目中安装包命令
npm install 包完整名称
或
npm i 包完整名称
  • 初次装包后多了哪些文件

初次装包完成后,在项目文件夹下多了一个叫做node_modules的文件夹和package-lock.json的配置文件

其中,node_modules文件夹用来存放已安装到项目中的包,require()导入第三方包时,就是从这个目录中查找并加载包

package-lock.json配置文件用来记录node_modules目录下的每一个包的下载信息,例如包的名字、版本号、下载地址等

  • 安装指定版本的包:

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

  • 包的语义化版本规范

包的版本号是以“点分十进制”形式进行定义的,总共有三位数字,例如:2.24.0

  • 第一位数字:大版本
  • 第二位数字:功能版本
  • 第三位数字:Bug修复版本

包管理配置文件:

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

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

如何记录项目中安装了哪些包:

在项目根目录,创建一个叫package.json的配置文件,即可用来记录项目中安装了哪些包。从而方便剔除node_modules目录之后,在团队成员之间共享的源代码

注意:开发中,一定要把node_modules文件夹,添加到.gitigonre忽略文件中

快速创建package.json

npm init -y
  • dependencies节点:

专门用来记录使用npm install 命令安装了哪些包

  • 一次性安装所有的包:
npm install

读取到记录的所有依赖包名称和版本号之后,npm包管理工具会把这些包一次性下载到项目中

  • 卸载包:

可以运行 npm uninstall 命令,来卸载包

npm uninstall moment
  • devDependencies节点:

如果某些包只在项目开发阶段会用到,在项目上线之后就不会用到,则建议把这些包记录到devDependencies节点中

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

npm i 包名 -D
或
npm install 包名 --save-dev
  • 解决下包速度慢的问题:

淘宝NPM镜像服务器:

切换npm的下包镜像源

npm config set registry=https://registry.npm.taobao.org/
  • nrm:

为了更方便的切换下包的镜像源

npm i nrm -g
//查看所有可用镜像源
nrm ls
//切换npm下载镜像
nrm use taobao

包的分类:

  • 项目包:

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

项目包又分为两类,分别是:

  1. 开发依赖包:记录到devDependencied节点中,只在开发时用
  2. 核心依赖包:记录到dependencie节点中的包,开发和上线都用
  • 全局包:

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

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

注意:

  • 只有工具性质的包,才有安装全局的必要性,因为他们提供了好用的终端命令
  • 判断某个包是否需要全局安装后才能使用,可以参考官方提供的使用说明即可

规范的包结构:

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

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

开发属于自己的包:

  • 初始化包的基本脑结构:
  1. 新建itheima-tools文件夹,作为包的根目录
  2. 在itheima-tools文件夹中,新建如下三个文件

    1. package.json(包管理配置文件)
    2. index.js(包的入口文件)
    3. README.md(包的说明文档)
  3. 初始化package.json
{
  "name":"itleima-tools",
  "version": "1.0.0",
  "main": "index.js",
  "description": "提供了格式化时间、HTMLEscape相关的功能",
  "keywords": ["itheima","detaFormat","escape"],
  "license": "ISC"
}

4、在index.js中定义格式化时间的方法

// 这是包的入口文件
function dateFormat(dateStr) {
  const dt = new Date(dateStr);

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

  const hh = 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,
};

5、在index.js中定义转义HTML的方法

// 定义转义HTML字符的函数
function htmlEscape(htmlstr) {
  return htmlstr.replace(/<|>|"|&/g, (match) => {
    switch (match) {
      case "<":
        return "&lt;";
      case ">":
        return "&gt;";
      case '"':
        return "&quot;";
      case "&":
        return "&amp";
    }
  });
}

6、在index.js中定义还原HTML字符串的函数

//定义还原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 "&";
    }
  });
}

7、将不同的功能进行模块化拆分

  • 将格式化时间的功能,拆分到src->dateFormat.js中
  • 将处理HTML字符串的功能,拆分到src->htmlEscape.js中
  • 在index.js中,导入两个模块,得到需要向外共享的方法
  • 在index.js中,使用module.exports把对应的方法共享出去

8、编写包的说明文档

  • 安装方式
  • 导入方式
  • 格式化时间、
  • 转义HTML中的特殊字符
  • 还原HTML中的特殊字符
  • 开源协议

发布npm包

npm账号注册完成后,可以在终端中执行npm login命令,依次输入用户名、密码、邮箱后,即可登录成功

  • 注意:在运行npm login 之前,必须先把下包的服务器地址切换为npm的官方服务器。否则会导致发布包失败
  • 把包发布到npm上

将终端切换到包的跟目录之后,运行npm publish 命令,即可将包发布到npm上(包不能雷同)

  • 删除已发布的包

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

注意:

  1. npm unpublish命令只能删除72小时以内发布的包
  2. npm unpublish删除的包,在24小时内不允许重复发布
  3. 发包的时候要慎重,尽量不要往npm上发布没有意义的包

模块的加载机制:

优先从缓存中加载:

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

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

内置模块的加载机制:

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

自定义模块的加载机制:

使用require()加载自定义模块时,必须指定以./或../开头的路径标识符,在加载自定义模块时,如果没有指定./或../这样的路径标识符,则node会把它当作内置模块或第三方模块进行加载

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

  1. 按照确切的文件名进行加载
  2. 补全.js扩展名进行加载
  3. 补全.json扩展名进行加载
  4. 补全.node扩展名进行加载
  5. 加载失败,终端报错

第三方模块的加载机制:

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

目录作为模块:

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

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

Express:

  • 什么是Express:

Express的作用和Node.js内置的http模块类似,是专门用来创建web服务器的

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

  • 有了htpp内置模块,为什么还要用Express

http内置模块用起来很复杂,开发效率低,Express是基于内置http模块进一步封装出来的,能够极大的提高开发效率

  • http内置模块与Express是什么关系

类似与浏览器中的webAPI和jQuery的关系

  • Express能做什么

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

  1. web网站服务器:专门对外提供web网页资源的服务器
  2. API接口服务器:专门对外提供API接口的服务器

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

安装:

  • 在项目所处的目录中,运行如下命令:
npm i express @4.17.1
  • 创建基本的web服务器
//导入express
const express = require('express')
//创建web服务器
const app = express()

//调用app.listen(端口号,启动成功后的回调函数),启动服务器
app.listen(80,()=>{
    console.log('express server running at http://127.0.0..1')
})

监听GET请求:

  • 通过app.get()方法,可以监听客户端的GET请求
//参数1:客户端请求的url地址
//参数2:请求对于的处理函数
//req:请求对象(包含了与请求相关的属性与方法)
//res:响应对象(包含了与响应相关的属性与方法)
app.get('请求的URL',function(req,res){处理函数})

监听POST请求:

  • 通过app.post()方法,可以监听客户端的POST请求
//参数1:客户端请求的url地址
//参数2:请求对于的处理函数
//req:请求对象(包含了与请求相关的属性与方法)
//res:响应对象(包含了与响应相关的属性与方法)
app.post('请求的URL',function(req,res){处理函数})

把内容响应给客户端

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

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

获取URL中携带的查询参数

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

app.get("/", (req, res) => {
  // 通过req.query可以获取到客户端发送来的参数 查询参数
  // 默认情况下是空对象
  console.log(req.query);
  res.send(req.query);
});

获取URL中的动态参数

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

app.get("/user/:id/:name", (req, res) => {
  // req.params是动态匹配到的URL参数,默认也是空对象
  console.log(req.params);
  res.send(req.params);
});

托管静态资源:

  • express.static():

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

app.use(express.static('public'))
  • 托管多个静态资源:

使用多次express.static()函数

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

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

挂载路径前缀:

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

nodemon:

当代码修改后,nodemon会自动帮我们重启项目

npm install -g nodemon
  • 使用nodemon
nodemon app.js

路由:

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

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

app.METHOD(PATH,MANDLER)

Express中路由的例子:

//匹配GET请求,且请求URL为 /
app.get('/',function(req,res)=>{
    res.send('hello world')
})
//匹配POST请求,且请求URL为 /
app.post('/',function(req,res)=>{
    res.send('Got a POST request')
})

路由的匹配过程:

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

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

  • 路由匹配规则
  1. 按照定义的先后顺序进行匹配
  2. 请求类型和请求的URL同时匹配成功,才会调用对应的处理函数
const express = require("express");

const app = express();

// 挂载路由
app.get("/", function (req, res) {
  res.send("hello world");
});
app.post("/", function (req, res) {
  res.send("Post Request");
});

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

模块化路由:

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

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

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

为路由模块添加前缀:

//导入
const userRouter = require('./router/user.js')
//添加前缀
app.use('/api',userRouter)

中间件:

中间件:特指业务流程的中间处理环节

image-20220308000746694.png
Express的中间件,本质上就是一个function处理函数,Express中间件的格式如下

image-20220308000941435.png

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

next函数的作用:

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

Express中间件的初体验:

1、定义中间件函数:

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

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

2、全局生效的中间件

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

通过调用app.use(中间件函数),即可定义一个全局生效的中间件

//常量mw所指向的,就是一个中间件函数
const mw = function(req,res,next) {
    console.log('这是一个最简单的中间件函数')
    next()
}
//全局生效的中间件
app.use(mw)

3、定义全局中间件的简化方式

//全局生效的中间件
app.use(function (req,res,next)=>{
    console.log('这是一个最简单的中间件函数')
    next()
})

4、中间件的作用:

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

5、定义多个全局中间件

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

app.use(funciton(req,res,) {//第1个中间件
    console.log('调用了第1个全局中间件')
    next()
})
app.use(funciton(req,res,) {//第2个中间件
    console.log('调用了第2个全局中间件')
    next()
})
app.get('/user',(req,res)=>{//请求这个路由,会依次触发上述两个全局中间件
    res.send('Home page')
})

局部生效的中间件

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

//定义中间件函数mw1
const mw1 = function(req,res,next) {
    console.log('这是中间件函数')
    next()
}
//mw1这个中间件只在“当前路由中生效”,这种用法属于“局部生效的中间件”
app.get('/',mw1,function(req,res){
    res.send('Home page')
})
//mw1这个中间件不会影响下面这个路由
app.get('/user',function(req,res){
    res.send('User page')
})
  • 定义多个局部中间件

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

//以下两种写法是“完全等价”的,可根据自己的喜好,选择任意一种方式进行使用
app.get('/',mw1,mw2,(req,res)=>{res.send('Home page')})
app.get('/',[mw1,mw2],(req,res)=>{res.send('Home page')})
  • 了解中间件的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 = express.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项目的开发效率和体验

  • express.static 快速托管静态资源的内置中间件,例如:HTML文件、图片、CSS样式等(无兼容性)
  • express.json 解析JSON格式的请求体数据(有兼容性,仅在4.16.0+版本可用)
  • 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@4..16.0之前版本中,经常使用body-parser 这个第三方中间件,来解析请求体数据

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

自定义中间件:

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

  • 定义中间件
  • 监听req的data事件
  • 监听res的end事件
  • 使用querystring模块解析请求体数据
  • 将解析出来的数据对象挂载为req.body
  • 将自定义中间件封装
  1. 定义中间件:

使用app.use() 来定义全局生效的中间件

app.use(function(req,res,next){
    //中间件业务逻辑
})

2.监听req的data事件:

在中间件中,需要监听req对象的data事件,来获取客户端发送到服务器的数据

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

// 定义一个str字符串,专门用来存储客户端发来的请求体数据
  let str = "";
  // 监听req的data事件
  req.on("data", (chunk) => {
    str += chunk;
  });

3.监听req的end事件:

当请求体数据接收完毕之后,会自动触发req的end事件

因此,我们可以在req的end事件中,拿到并处理完整的请求体数据

// 监听req的end事件
  req.on("end", () => {
    // 在str中存放的是完整的请求体数据
    console.log(str);
    // 把字符串格式解析为对象格式
  });

4.使用querystring 模块解析请求体数据

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

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

    const body = qs.parse(str);
    console.log(body);

5.将解析出来的数据挂载为req.body

const body = qs.parse(str);
    req.body = body;
    next();

6.将自定义中间件封装为模块

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

const qs = require("querystring");

// 这是解析表单数据的中间件
const bodyParser = (req, res, next) => {
  // 定义中间件具体业务逻辑
  // 定义一个str字符串,专门用来存储客户端发来的请求体数据
  let str = "";
  // 监听req的data事件
  req.on("data", (chunk) => {
    str += chunk;
  });
  // 监听req的end事件
  req.on("end", () => {
    // 在str中存放的是完整的请求体数据
    // console.log(str);
    // 把字符串格式解析为对象格式
    const body = qs.parse(str);
    req.body = body;
    next();
  });
};

module.exports = bodyParser;
const express = require("express");

const app = express();
// 导入内置的querystring模块

// 导入自己封装的中间件模块
const customBodyParser = require("./custom-body-parser");
app.use(customBodyParser);

app.post("/user", (req, res) => {
  res.send(req.body);
});

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

使用Express写接口:

  • 编写express接口:
// 导入express
const express = require("express");
// 创建express实例
const app = express();

// 启动
app.listen(80, () => {
  console.log("http://127.0.0.1");
});
  • 创建API路由:
const express = require("express");
const router = express.Router();
// 在这里挂载对应的路由

module.exports = router;
  • 编写GET接口:
// 在这里挂载对应的路由
router.get("/get", (req, res) => {
  // 通过req.query 获取客户端通过查询字符串,发送到服务器的数据
  const query = req.query;
  // 调用res.send()方法,向客户端响应处理的结果
  res.send({
    status: 0, //0处理成功,1表示处理失败
    mag: "GET请求成功", //状态的描述
    data: query, //需要响应给客户端的数据
  });
});
  • 编写POST接口:
// 配置解析表单的中间件
app.use(express.urlencoded({ extends: false }));
--------------------------------------

router.post("/post", (req, res) => {
  // 获取客户端通过请求体,发送到服务器的URL-encoded数据
  const body = req.body;
  // 调用res.send()方法,把响应数据客户端
  res.send({
    status: 0,
    msg: "POST请求成功!",
    data: body,
  });
});

CORS跨域资源共享:

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

  • CORS(主流的解决方案,推荐使用)
  • JSONP(有缺陷的解决方案,只支持GET请求)

使用cors中间件解决跨域问题:

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

注意: 一定要在路由之前,配置cors这个中间件

CORS响应头:

  • Access-Control-Allow-Origin

响应头部中可以携带一个AccessControl-Allow-Origin字段

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

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

例如:下面的字段值将只允许来自http://itcast.cn的请求

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

星号表示所有网站都可以请求

  • Access-Control-Allow-Headers

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

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

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

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

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

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

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

CORS请求的分类:

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

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

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

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

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

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

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

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

JSONP接口:

概念:浏览器端通过<script>标签的src属性,请求服务器上的数据,同时,服务器返回一个函数的调用。这种请求数据的方式叫做JSONP

特点:

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

项目中操作MySQL:

  • 安装操作MySQL数据库的第三方模块(mysql)
  • 通过mysql模块连接到MySQL数据库
  • 通过mysql模块执行SQL语句

在使用mysql模块操作MySQL数据库之前,必须先对mysql模块进行必要的配置

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

测试连接:

// 检测mysql模块是否正常工作
db.query("SELECT 1", (err, results) => {
  if (err) return console.log(err.message);
  // 只要能打印出[RowDataPacket {'1':1}]的结果,就证明数据库连接正常
  console.log(results);
});

查询数据:

查询users表中的所有用户数据

// 查询users表中所有的数据
const sqlStr = "select * from users";
db.query(sqlStr, (err, results) => {
  if (err) console.log(err.message);
  console.log(results);
});

如果执行的是select 查询语句,则执行的结果是数组

插入数据:

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

// 插入数据
const user = { username: "Spider-Man", password: "pcc321" };
const sqlStr = "insert into users (username,password) values(?,?)";
db.query(sqlStr, [user.username, user.password], (err, results) => {
  if (err) return console.log(err.message);
  if (results.affectedRows == 1) {
    console.log("插入数据成功");
  }
});

可以使用问号?占位符

可以使用results.affectedRows === 1判断是否插入成功

  • 插入数据的便捷方式:

向表中新增数据时,如果数据对象的每个属性和数据表的字段一一对应,则可以通过如下操作:

// 插入数据
const user = { username: "Web-Wss", password: "123456" };
const sqlStr = "insert into users set ?";
db.query(sqlStr, user, (err, results) => {
  if (err) return console.log(err.message);
  if (results.affectedRows == 1) {
    console.log("插入数据成功");
  }
});

更新数据:

// 更新新的数据对象
const user = { id: 6, username: "aaa", password: "000" };
// 要执行的sql语句
const sqlStr = "update users set username=?,password=? where id=?";
// 调用db.query()执行sql语句的同时,使用数组依次为占位符指定具体的值
db.query(sqlStr, [user.username, user.password, user.id], (err, results) => {
  if (err) return console.log(err.message);
  if (results.affectedRows === 1) {
    console.log("更新数据成功");
  }
});
  • 更新数据的便捷方式:
const user = { id: 6, username: "bbb", password: "000" };
// 要执行的sql语句
const sqlStr = "update users set ? where id=?";
// 调用db.query()执行sql语句的同时,使用数组依次为占位符指定具体的值
db.query(sqlStr, [user, user.id], (err, results) => {
  if (err) return console.log(err.message);
  if (results.affectedRows === 1) {
    console.log("更新数据成功");
  }
});

删除数据:

在删除数据时,推荐根据id这样的唯一标识,来删除对应的数据

//要执行的sql语句
const sqlStr = 'DELETE FROM users WHERE id = ?'
//调用db.query()执行sql语句的同时,为占位符指定具体的值
//注意:如果sql语句中有多个占位符,则必须使用数组为每个占位符指定具体的值
//如果sql语句中只有一个占位符,则可以省略
db.query(sqlStr,7,(err,results)=>{
    if(err) return console.log(err.message)//失败
    if(results.affectedRows === 1) {
        console.log('删除数据成功')
    }
})
  • 标记删除

使用DELETE语句,会把真正的数据从表中删除,为了保险,推荐使用标记删除,来模拟删除的动作。

所谓的标记删除,就是在表中设置类似status这样的状态字段,来标记当前这条数据是否被删除

当用户执行了删除的动作时,我们并没有执行DELETE语句把数据删除掉,而是执行update语句,将这条数据对应的status字段标记删除即可

const sqlStr = "update users set status=? where id=?";
db.query(sqlStr, [1, 5], (err, results) => {
  if (err) return console.log(err.message);
  if (results.affectedRows === 1) {
    console.log("删除成功");
  }
});

web开发模式:

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

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

优点:

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

缺点:

  1. 占用服务端资源
  2. 不利于前后端分离,开发效率低
  • 前后端分离的web开发模式

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

优点:

  1. 开发体验好
  2. 用户体验好
  3. 减轻了服务器端的渲染压力

缺点:

  1. 不利于SEO

身份认证:

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

  • 为什么需要身份认证:

为了确认当前所声称为某种身份的用户,确实是所声称的用户

  • 不同开发模式下的身份认证:

    • 服务端渲染推荐使用:Session认证机制
    • 前后端分离推荐使用:JWT认证机制

Session认证机制:

  • HTTP协议的无状态性

HTTP协议的无状态性,指的是客户端的每次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的形式发送给浏览器

  • 提高身份认证的安全性:

会员卡+刷卡认证的设计理念,就是Session认证的精髓

  • Session的工作原理:

image-20220310235709476.png

在Express中使用Session:

  • 安装express-session中间件:

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

  • 配置express-session中间件:

通过app.use()来注册session中间件

//导入session中间件
var session = require('express-session')
//配置Session中间件
app.use(session({
    secret:'keyboard cat',//secret属性值可以为任意字符串
    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:1,msg:'登录失败'})
    }
    req.session.user = req.body//将用户信息,存储到session中
    req.session.islogin = true//将用户的登录状态存到session中
    
    res.send({status:0,msg:'登录成功'})
})
  • 从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()
    req.send({
        status:0,
        msg:'退出登录成功'
    })
})

JWT认证机制:

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

注意:

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

什么是JWT:

JWT(英文全称:JSON Web Token)是目前最流行的跨域认证解决方案

JWT的工作原理:

image-20220315231127743.png

总结:用户的信息通过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>

在Express中使用JWT:

安装JWT相关的包

npm install jsonwebtoken express-jwt

其中:

  • jsonwebtoken用于生成JWT字符串
  • express-jwt用于将JWT字符串解析还原成JSON对象

导入:

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

定义secret密钥:

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

①当生成JWT字符串的时候,需要使用secret密钥对用户的信息进行加密,最终得到加密的JWT字符串

②当把JWT字符串解析还原成JSON对象的时候,需要使用secret密钥进行加密

const secretKey = 'itheima No1 ^_^'

在登录成功后生成JWT字符串

调用jsonwebtoken包提供的sign()方法,将用户的信息加密成JWT字符串,响应给客户端

//登录接口
app.post('/api/login',function(req,res) {
    //...省略登录失败情况下的代码
    //用户登录成功之后,生成JWT字符串,通过token属性响应给客户端
    res.send({
        status:200,
        message:'登录成功',
        //调用jwt.sign()生成JWT字符串,三个参数分别是:用户信息对象、加密密钥、配置对象
        token: jwt.sign({username:userinfo.username},secretKey,{expiresIn:'30s'})
    })
})

将JWT字符串还原为JSON对象

客户端每次在访问那些有权限接口的时候,都需要主动通过请求头中的Authorization字段,将Token字符串发送到服务器进行身份验证

此时,服务器可以通过express-jwt这个中间件,自动将客户端发送过过来的Token解析还原成JSON对象

//使用app.use() 来注册中间件
//expressJWT({secret:secretKey})就是用来解析Token的中间件
//.unless({path:[/^\/api\//]})用来指定哪些接口不需要访问权限
app.use(expressJWT({secret:secretKey}).unless({path:[/^\/api\//]}))

使用req.user获取用户信息

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

//这是一个有权限的API接口
app.get('/admin/getinfo',function(req,res) {
    console.log(req.user)
    res.send({
        status:200,
        message:'获取用户信息成功',
        data:req.user
    })
})

捕获解析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,message:'未知错误'})
})
最后修改:2024 年 07 月 26 日
如果觉得我的文章对你有用,请随意赞赏