DayNode(Node.js)

503 阅读24分钟

截屏2021-07-15 下午3.21.54.png

截屏2021-07-15 下午3.26.12.png

fs模块

读取文件 readFile()

// 1. 导入 fs 模块,来操作文件
const fs = require('fs')

// 2. 调用 fs.readFile() 方法读取文件
//    参数1:读取文件的存放路径
//    参数2:读取文件时候采用的编码格式,一般默认指定 utf8
//    参数3:回调函数,拿到读取失败和成功的结果  err  dataStr
fs.readFile('./files/11.txt', 'utf8', function(err, dataStr) {
  // 2.1 打印失败的结果
  // 如果读取成功,则 err 的值为 null
  // 如果读取失败,则 err 的值为 错误对象,dataStr 的值为 undefined
  if (err) {
    return console.log('读取文件失败!' + err.message)
  }
  console.log('读取文件成功!' + dataStr)
})

写入文件 writeFile()

字符集 utf8

截屏2021-07-16 上午9.11.17.png

不写utf8(字符串),默认是文件流 buffer(二进制)

// 1. 导入 fs 文件系统模块
const fs = require('fs')

// 2. 调用 fs.writeFile() 方法,写入文件的内容
//    参数1:表示文件的存放路径
//    参数2:表示要写入的内容
//    参数3:回调函数
fs.writeFile('./files/3.txt', 'ok123', function(err) {
  // 2.1 如果文件写入成功,则 err 的值等于 null
  // 2.2 如果文件写入失败,则 err 的值等于一个 错误对象
  // console.log(err)

  if (err) {
    return console.log('文件写入失败!' + err.message)
  }
  console.log('文件写入成功!')
})

__dirname

表示当前文件所处的目录

path 路径模块

path.join()

拼接文件 路径,识别相对路径符号 ../, ./

const path = require('path');

fs.readFile(path.join(__dirname, './files/1.txt'), 'utf8', function(err, dataStr) {
  if (err) {
    return console.log(err.message)
  }
  console.log(dataStr)
})

path.basename()

从文件路径中,拿到文件名称-- index.html | index

const path = require('path')

// 定义文件的存放路径
const fpath = '/a/b/c/index.html';

const fullName = path.basename(fpath);// index.html

const nameWithoutExt = path.basename(fpath, '.html');// index

path.extname()

获取 路径 中的扩展名部分-- .html

const path = require('path')

// 这是文件的存放路径
const fpath = '/a/b/c/index.html'const fext = path.extname(fpath);// .html

http 模块

node提供的,用来创建 web服务器的模块

IP地址

IP是在网络中 找对应的电脑 互联网中 每台web服务器,都有自己 唯一的IP地址

// 终端 查看百度的ip地址
ping www.baidu.com

39.156.66.18

域名和域名服务器

域名是为了 方便记忆 IP

IP地址和域名一一对应( 39.156.66.18----www.baidu.com截屏2021-07-16 上午10.51.28.png

端口

端口是在网络中 找电脑里面的 软件

创建web服务器

截屏2021-07-16 上午11.23.15.png

  • 01.js

// 1. 导入 http 模块
const http = require('http')
// 2. 创建 web 服务器实例
const server = http.createServer()

// 3. 为服务器实例绑定 request 事件,监听客户端的请求
// req-require/request请求,res-response响应
server.on('request', (req, res) => {
  // 定义一个字符串,包含中文的内容
  const str = `您请求的 URL 地址是 ${req.url},请求的 method 类型为 ${req.method}`
  // 调用 res.setHeader() 方法,设置 Content-Type 响应头,解决中文乱码的问题
  res.setHeader('Content-Type', 'text/html; charset=utf-8')
  // res.end() 将内容响应给客户端
  res.end(str)
})

// 4. 启动服务器
server.listen(80, () => {
  console.log('server running at http://127.0.0.1')
})
  • 终端输入
// 启动服务器
node 01.js 

// 终止服务器
control+c

模块化

截屏2021-07-16 下午2.23.25.png

模拟require的实现

  • myRequire.js
const fs = require('fs');

function myRequire(filePath) {
    let myDirname = '读取文件中。。。。\n';

    fs.readFile(filePath, 'utf8', function(err, content) {
        if (err) return console.log(err.message);
        // 读取成功,就执行读取的代码
        eval(content);
    })
}

myRequire('./source.js')
  • source.js
console.log(myDirname);

console.log('被读取的文件是我');

模块作用域

  • 概念: 模块内的变量,外界不可访问

  • 目的: 防止全局变量污染

module对象

存储了和当前模块相关的属性

module.exports对象

将模块内的成员共享出去,供外界使用

注意:使用require导入的结果,永远以module.exports 指向的对象为准

// 在一个自定义模块中,默认情况下, module.exports = {}
const age = 20

// 导出方法1: module.exports  【点语法】
module.exports.username = 'zs'
// 向 module.exports 对象上挂载 sayHello 方法
module.exports.sayHello = function() {
  console.log('Hello!')
}
module.exports.age = age

// 导出方法2(推荐): 让 module.exports 【指向一个全新的对象】
// 会覆盖前面 单独挂载的 属性,导出的为该对象
module.exports = {
  nickname: '小黑',
  sayHi() {
    console.log('Hi!')
  }
}

// 导出方法3(不推荐): exports 【点语法】
exports.dogname= 'tomato';

模拟module.exports实现原理

  • 模拟实现module.exports.js
const fs = require('fs');

function myRequire(filePath) {
    let myDirname = '读取文件中。。。。\n';

    // 1.创建module局部变量,并设置 初始值和属性
    let module = {
        // ...
        exports: {} // 用来导出数据和方法的属于
        // ...
    }
    
    // 1.2创建 exports局部变量,用来 方便 访问module.exports
    let exports=module.exports;

    // 2.根据传入的路径,同步读取 文件中的代码字符串
    let content = fs.readFileSync(filePath, 'utf8');

    // 3.读取成功,就执行读取的代码
    eval(content);

    // 4.返回 需要导出的数据和方法
    return module.exports;
}

// 测试 导入js模块 .js后缀名可以省略
let m = myRequire('./source.js');
console.log(m); // {name:'33'}
  • source.js
module.exports.name = '33';

模块化规范

CommonJS规定了 模块的特性各模块之间如何相互依赖

npm与包

包(第三方模块)

包-参考文档 Node Package Manager(npm包管理攻击)

下载包

  • 下载 moment包
//初始化 文件,生成 package-lock.json 文件
npm init -y

// 安装 moment 包
npm i monent// i--install

// 安装指定版本,会自动覆盖已安装的版本
npm i monent@2.29.1
  • 导入包
// 1.导入 下载好的 moment 包(moment 导入 返回的是一个 函数)
const moment = require('moment')

// 2.使用 moment 函数
// 2.1 获取 当前 日期时间 --------------------------------
let nowTime = moment()
console.log(nowTime) // -> Moment<2021-07-16T18:40:23+08:00>


// 2.2 获取 当前 日期时间,并格式化-------------------------
let nowTime2 = moment().format('YYYY-MM-DD HH:mm:ss')
console.log(nowTime2) // '2021-07-16 18:41:43'

// 2.3 获取 指定日期时间,并格式化--------------------------
// a.传入 日期 对象
let dateNow = new Date()
let nowTime3 = moment(dateNow).format('YYYY-MM-DD HH:mm:ss')
console.log(nowTime3) // '2021-07-16 18:41:43'

// b.传入 日期 字符串
let nowTime4 = moment('2021-07-16T10:45:05.942Z').format('YYYY-MM-DD HH:mm:ss')
console.log(nowTime4) // '2021-07-16 18:45:05'

// c.传入 总毫秒数 字符串
let nowTime4 = moment(1626432356541).format('YYYY-MM-DD HH:mm:ss')
console.log(nowTime4) // '2021-07-16 18:45:05'

初始化包

//初始化 文件,生成 package-lock.json 文件
npm init -y

一次性安装所有依赖包

npm i;// install

卸载包

npm uninstall moment

解决下包速度慢的问题 nrm

将国外的官方服务器 切换为 国内的淘宝服务器

# 查看当前的 下包镜像源
npm config get registry

# 将下包的镜像源 切换为 淘宝镜像源
npm config set registry=http://registry.npm.taobao.org/

# 检查镜像源是否下载成功
npm config get registry

nrm工具(更方便的切换包)

# 将nrm安装为全局 可用的 工具
npm i nrm -g

# 查看 所有可用的镜像源
nrm ls

# 将下包的镜像源 切换为 taobao镜像
nrm use taobao

包的分类

项目包

截屏2021-07-18 下午2.23.51.png 被安装到 项目的 node_modules目录中的包

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

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

package.json 与 package-lock.json

  • package.json 只记录项目直接依赖的包
  • package-lock.json 记录项目 直接与间接依赖的所有包(且保存了包的hash值)

全局包

执行npm install 命令时,提供了-g参数,就会将包安装为 全局包

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

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

  • 不会记录到 项目的 package.json文件中

# 全局 安装指定的包
npm install 包名 -g

# 全局 卸载指定的包
npm uninstall包名 -g

# 全局安装的文件 的默认目录
/usr/local/lib/node_modules

i5ting_toc 将md转换为html

# 安装为全局包
npm install -g i5ting_toc

# 调用i5ting_toc,将md文件 转 html
i5ting_toc -f 要转换的md文件路径 -o

规范的包结构

  • 包必须以单独的目录而存在

  • 包的顶级目录下要必须包含 package.json 这个包管理配置文件

  • package.json 中必须包含 name,version,main 这三个属性,分别代表包的名字、版本号、包的入口。

开发自己的包

初始化包的基本结构

  1. 新建 itheima-tools 文件夹,作为包的根目录
  2. 在 itheima-tools 文件夹中,新建如下三个文件:
    • package.json (包管理配置文件)
      • 注意:name不能有大写
    • index.js (包的入口文件)
    • README.md (包的说明文档)

完善包的文件代码

/package.json/

{
    "name": "jean-sayhi",// 注意不能有大写
    "version": "1.0.0",
    "description": "测试jean的打招呼包",
    "main": "index.js",
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
    },
    "keywords": ["sayHi", "jean", "test"],
    "author": "",
    "license": "ISC"
}

/index.js/

// 导入成员
const sayHi = require('./src/sayhi.js');

// 导出成员
module.exports = {
    // 对象中的扩展运算符(...)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中
    ...sayHi
}

/src/sayhi.js/

// 打招呼
function sayHi(username) {
    console.log(`${username},hello~Welcome to use Jean's package!`);
}
module.exports = {
    sayHi
}

/README.md/

## 安装
npm i Jean-sayHi


## 导入
const Jeans = require('Jean-sayHi');


## 打招呼
// 调用 dateFormat 对时间进行格式化
const str = Jean.sayHi('super');
// 结果  2020-04-03 17:20:58
console.log(str)

发布包

注意:在终端执行命令前,必须 先把下包的服务器地址切换为 npm 的官方 服务器。否则会导致发布包失败!

1. 登录npm
npm login

2. 将终端切换到`包的根目录`,发布包
npm publish

删除已发布的包

npm unpublish 包名 --force

模块的加载机制

优先从缓存中加载

截屏2021-07-18 下午7.03.35.png 模块在第一次加载后会被缓存。 这也意味着多次调用 require() 不会导致模块的代码被执行多次。

注意:不论是内置模块、用户自定义模块、还是第三方模块,它们都会优先从缓存中加载,从而提高模块的加载效率截屏2021-07-18 下午7.19.21.png

内置模块的加载机制

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

自定义模块的加载机制

文件

必须指定以 ./ 或 ../ 开头的路径标识符。在加载自定义模块时,否则会当作内置模块或第三方模块进行加载。

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

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

文件夹(目录作为模块)

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

  1. 在被加载的目录下查找一个叫做 package.json 的文件,并寻找 main 属性,作为 require() 加载的入口

  2. 如果目录里没有 package.json 文件,或者 main 入口不存在或无法解析,则 Node.js 将会试图加载目录下的 index.js 文件。

  3. 如果以上两步都失败了,则 Node.js 会在终端打印错误消息,报告模块的缺失:Error: Cannot find module 'xxx'

第三方模块的加载机制

Node.js 会从当前模块的父 目录开始,尝试从 /node_modules 文件夹中加载第三方模块。

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

例如,假设在 'C:\Users\itheima\project\foo.js' 文件里调用了 require('tools'),则 Node.js 会按以下顺序查找:

  1. C:\Users\itheima\project\node_modules\tools
  2. C:\Users\itheima\node_modules\tools
  3. C:\Users\node_modules\tools
  4. C:\node_modules\tools

express

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

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

截屏2021-07-19 上午10.08.10.png

基本使用

// 1. 导入 express
const express = require('express')
// 2. 创建 web 服务器
const app = express()

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

app.post('/user', (req, res) => {
  // 调用 express 提供的 res.send() 方法,向客户端响应一个 `文本字符串`
  res.send('请求成功')
})

// 3. 启动 web 服务器
app.listen(80, () => {
  console.log('express server running at http://127.0.0.1')
})

把内容响应给客户端

res.send()可以向客户端响应一个 JSON 对象文本字符串 截屏2021-07-19 上午10.41.51.png

获取 URL 中携带的查询参数

获取在地址栏中输入的参数 req.query

![截屏2021-07-19 上午11.50.42.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/07c778601bf241bd9ce5d9f72c9abb28~tplv-k3u1fbpfcp-watermark.image)
app.get('/', (req, res) => {
  // 访问根路径
  // 通过 req.query 可以获取到客户端发送过来的 查询参数
  // 注意:默认情况下,req.query 是一个空对象
  console.log(req.query)
  res.send(req.query.id)// 1
})

获取 URL 中的动态参数

req.params 是动态匹配到的 URL 参数 截屏2021-07-19 上午11.47.15.png

// 注意:这里的 :id 是一个动态的参数
// ids--参数名;浏览器地址栏中--参数值
app.get('/user/:ids/:username', (req, res) => {
  // req.params 是动态匹配到的 URL 参数,默认也是一个空对象
  console.log(req.params)
  res.send(req.params.ids)
})

express.static()

方便地创建一个静态资源服务器

注册静态资源文件夹 托管(可以处理 html/css/js/图片等类型文件)

// clock是文件夹,里面有index.html文件
// 在地址栏写入 http://127.0.0.1/index.html 访问该文件
app.use(express.static('./clock'))

截屏2021-07-19 下午2.41.12.png

托管多个静态资源目录

// 在第一个文件夹里找到了 文件,就不再去第二个文件夹找
app.use(express.static('./clock'))
app.use(express.static('./files'))

挂载路径前缀

// 在浏览器中 需要加上前缀/files  
// http://127.0.0.1/files/index.html
app.use('/files', express.static('./files'))

nodemon

它能够监听项目文件 的变动,当代码被修改后,nodemon 会自动帮我们重启项目,极大方便了开发和调试。

终端执行

# 安装包
npm i nodemon -g

# 开启项目
nodemon 文件名

Express路由

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

Express 中的路由分 3 部分组成,分别是请求的类型、请求的 URL 地址、处理函数,格式:app.method(path, handler)

路由匹配:请求方式+url --> 处理函数

模块化路由

router.js

// 这是路由模块
// 1. 导入 express
const express = require('express')
// 2. 创建路由对象
const router = express.Router()

// 3. 挂载具体的路由
router.get('/user/list', (req, res) => {
  res.send('Get user list.')
})
router.post('/user/add', (req, res) => {
  res.send('Add new user.')
})

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

app.js 注意: 导入的路由 ,如果是对象,必须要解构出来,只需要router属性const {router} = require('./router.js')

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

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

// 1. 导入路由模块 `解构`出router 属性
const {router} = require('./router.js')

// 2. 注册路由模块 添加统一的访问前缀 /api
app.use('/api', router)

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

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

在地址栏中 输入 http://127.0.0.1/api/user/list 即可访问路由

Express 中间件

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

截屏2021-07-21 上午9.19.28.png

截屏2021-07-21 上午10.39.23.png

全局生效中间件

app.use(中间件函数)

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


// 这是定义全局中间件的简化形式
app.use((req, res, next) => {
  // 获取到请求到达服务器的时间
  const time = Date.now()
  // 为 req 对象,挂载自定义属性,从而把时间共享给后面的所有路由
  req.startTime = time
  next()
})

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

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

局部生效中间件

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

// 1. 定义中间件函数
const mw1 = (req, res, next) => {
  console.log('调用了第一个局部生效的中间件')
  next()
}

const mw2 = (req, res, next) => {
  console.log('调用了第二个局部生效的中间件')
  next()
}

// 2. 创建路由 中间件只对 该路由生效 可以不写[]
app.get('/', [mw1, mw2], (req, res) => {
  res.send('Home page.')
})
app.get('/user', (req, res) => {
  res.send('User page.')
})

// 调用 app.listen 方法,指定端口号并启动web服务器
app.listen(80, function () {
  console.log('Express server running at http://127.0.0.1')
})

中间件的的分类

  1. 应用级别的中间件

    • 通过 app.use() 或 app.get() 或 app.post(),绑定到 app 实例上的中间件
  2. 路由级别的中间件

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

    • 错误级别中间件的作用:专门用来捕获整个项目中发生的异常错误,从而防止项目异常崩溃的问题。
    • 格式:错误级别中间件的 function 处理函数中,必须有 4 个形参,形参顺序从前到后,分别是 (err, req, res, next)。
    • 注意:错误级别的中间件, 必须注册在所有路由之后!
// 导入 express 模块
const express = require('express')
// 创建 express 的服务器实例
const app = express()

// 1. 定义路由
app.get('/', (req, res) => {
  // 1.1 人为的制造错误
  throw new Error('服务器内部发生了错误!')
  // 抛出错误后,不会执行这行代码
  res.send('Home page.')
})

// 2. 定义错误级别的中间件,捕获整个项目的异常错误,从而防止程序的崩溃
app.use((err, req, res, next) => {
  console.log('发生了错误!' + err.message)// 发生了错误!服务器内部发生了错误!
  res.send('对不起,页面崩溃了!请稍后再试~')
})

// 调用 app.listen 方法,指定端口号并启动web服务器
app.listen(80, function () {
  console.log('Express server running at http://127.0.0.1')
})
  1. Express内置的中间件
    • express.static 快速托管静态资源的内置中间件,例如: HTML 文件、图片、CSS 样式等(无兼容性)

    • express.json 解析 JSON 格式的请求体数据(有兼容性,仅在 4.16.0+ 版本中可用)

      • JSON格式的数据:数组和对象(且属性和字符串都要加"") 例:{"name":"jean","age":17}["jean",17]
    • express.urlencoded 解析 URL-encoded 格式的请求体数据(有兼容性,仅在 4.16.0+ 版本中可用)

      • URL-encoded 格式的数据:键值对 例:name=jean&&age=17
      • form.serilize(),就是键值对 格式
      • 利用中间件,会解析为 对象格式

截屏2021-07-21 下午2.51.02.png

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

// 注意:除了错误级别的中间件,其他的中间件,必须在路由之前进行配置
// 通过 express.json() 这个中间件,解析表单中的 JSON 格式的数据
app.use(express.json())
// 通过 express.urlencoded() 这个中间件,来解析 表单中的 url-encoded 格式的数据
app.use(express.urlencoded({ extended: false }))

app.post('/user', (req, res) => {
  // 在服务器,可以使用 req.body 这个属性,来接收客户端发送过来的请求体  JSON 格式的表单数据和 url-encoded 格式的数据
  // 默认情况下,如果不配置解析表单数据的中间件,则 req.body 默认等于 undefined
  console.log(req.body);
  res.send('ok')
})

// 调用 app.listen 方法,指定端口号并启动web服务器
app.listen(80, function () {
  console.log('Express server running at http://127.0.0.1')
})
  1. 第三方的中间件

需要npm下载,再require导入,再app.use()使用

不同格式数据的接收方式

18【重要】不同格式数据的接收方式.png

使用CORS解决接口的跨域问题

JSONP只支持get请求

1. 安装cors 
npm i cors

2. 导入,并配置中间件
// 一定要在路由之前,配置 cors 这个中间件,从而解决接口跨域的问题
const cors = require('cors');
app.use(cors())

CORS 响应头部

域名限制 - Access-Control-Allow-Origin

响应头部中可以携带一个 Access-Control-Allow-Origin 字

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

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

// 只允许来自 http://itcast.cn 的请求:
res.setHeader('Access-Control-Allow-Origin','http://itcast.cn')

// 示允许来自任何域的请求
res.setHeader('Access-Control-Allow-Origin','*')

字段限制 - Access-Control-Allow-Headers

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

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

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

// 允许 客户端额外向服务器发送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-Alow-Methods 来指明实际请求所允许使用的 HTTP 方法。

res.setHeader('Access-Control-Allow-Methods','POST,GET,DELETE,HEAD');
// 云去所有的 http 请求方式
res.setHeader('Access-Control-Allow-Methods','*')

简单请求和预检请求

  1. 简单请求
  • 请求方式:GET、POST、HEAD 三者之一
  • HTTP 头部信息的字段:无自定义头部字段
  1. 预检请求

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

  • 请求方式为 GET、POST、HEAD 之外的请求 Method 类型
  • 请求头中包含自定义头部字段
  • 向服务器发送了 application/json 格式的数据
  1. 区别
  • 简单请求的特点:客户端与服务器之间只会发生一次请求。

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

JSONP 接口

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

实现 JSONP 接口的步骤:

  1. 获取客户端发送过来的回调函数的名字
  2. 得到要通过 JSONP 形式发送给客户端的数据
  3. 根据前两步得到的数据,拼接出一个函数调用的字符串
  4. 把上一步拼接得到的字符串,响应给客户端的<script>标签进行解析执行

使用express写接口

测试.html

<body>
    <button id="btnGET">GET</button>
    <button id="btnPOST">POST</button>
    <button id="btnDelete">DELETE</button>
    <button id="btnJSONP">JSONP</button>

    <div></div>
    <script>
        $(function() {
            // 1. 测试GET接口
            $('#btnGET').on('click', function() {
                    $.ajax({
                        type: 'GET',
                        url: 'http://127.0.0.1/api/get',
                        data: {
                            name: 'zs',
                            age: 20
                        },
                        success: function(res) {
                            $('div').html(JSON.stringify(res));

                            console.log(res)
                        },
                    })
                })
                // 2. 测试POST接口
            $('#btnPOST').on('click', function() {
                $.ajax({
                    type: 'POST',
                    url: 'http://127.0.0.1/api/post',
                    data: {
                        bookname: '水浒传',
                        author: '施耐庵'
                    },
                    success: function(res) {
                        $('div').html(res);
                        console.log(res)
                    },
                })
            })

            // 3. 为删除按钮绑定点击事件处理函数
            $('#btnDelete').on('click', function() {
                $.ajax({
                    type: 'DELETE',
                    url: 'http://127.0.0.1/api/delete',
                    success: function(res) {
                        $('div').text(res);
                        console.log(res)
                    },
                })
            })

            // 4. 为 JSONP 按钮绑定点击事件处理函数
            $('#btnJSONP').on('click', function() {
                $.ajax({
                    type: 'GET',
                    url: 'http://127.0.0.1/api/jsonp',
                    dataType: 'jsonp',
                    success: function(res) {
                        $('div').html(res);
                        console.log(res)
                    },
                })
            })
        })
    </script>
</body>

app.js

// 导入 express
const express = require('express')
// 创建服务器实例
const app = express()

// 配置解析表单数据的中间件
app.use(express.urlencoded({ extended: false }))

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

// 一定要在路由之前,配置 cors 这个中间件,从而解决接口跨域的问题
const cors = require('cors')
app.use(cors())

// 导入路由模块
const router = require('./router.js')
// 把路由模块,注册到 app 上
app.use('/api', router)

// 启动服务器
app.listen(80, () => {
  console.log('express server running at http://127.0.0.1')
})

router.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, // 需要响应给客户端的数据
  })
})

// 定义 POST 接口
router.post('/post', (req, res) => {
  // 通过 req.body 获取请求体中包含的 url-encoded 格式的数据
  const body = req.body
  // 调用 res.send() 方法,向客户端响应结果
  res.send({
    status: 0,
    msg: 'POST 请求成功!',
    data: body,
  })
})

// 定义 DELETE 接口
router.delete('/delete', (req, res) => {
  res.send({
    status: 0,
    msg: 'DELETE请求成功',
  })
})

module.exports = router

MySQL数据库

username: root password: admin123

port:3306默认

创建数据表 table

截屏2021-07-22 下午3.20.20.png

SQL的语句

注意:每一句 语句结束要写 ;

SELECT 语句

SELECT 语句用于从表中查询数据。执行的结果被存储在一个结果表中(称为结果集)。 注意:SQL的关键字对大小写不敏感,如select等价于SELECT

-- 从指定表中 查询 所有列的数据
SELECT * FROM 表名称

-- 从指定表中 查询 指定列名称(多个列用 , 隔开)的数据
SELECT 列名称 FROM 表名称

select username,password from users;

INSERT INTO 语句

向数据表中插入新的数据行

-- 列与值要一一对应
insert into 表名称 (列1,列2,...) values(值1,值2,...);

insert into users (username,password) values('jean','123456');

UPDATE 语句

修改表中的数据。

update 表名称 set 列名称 = 新值 where 列名称 = 某值

-- 1.修改某一行的某一列
-- 把 users 表中 id 为 2 的用户密码,更新为 000000
update users set password='000000' where id=2;


-- 2.更新某一行中的若干列
-- 把 users 表中 id 为 1 的password和status,分别更新为 admin123 和 1。
update users set password='admin123',status=1 where id=1

DELETE 语句

删除表中的行(一般是根据id-主键去删除)

delete form 表名称 where 列名称 = 值

-- 从 users 表中,删除 id 为 3 的用户
delete form users where id=3;

WHERE 子句

限定选择的标准。在 SELECT、UPDATE、DELETE 语句中,皆可使用 WHERE 子句来限定选择的标准。

-- 查询语句中的 where条件
select 列名称 from 表名称 where 列 运算符 值

-- 更新语句中的 where条件
update 表名称 set 列=新值 where 列 运算符 值

-- 删除语句中的 where条件
delete from 表名称 where 列 运算符 值
  • where子句中使用的 运算符
操作符描述
=等于
<>或!=不等于
大于
<小于
>=大于等于
<=小于等于
BETWEEN在某个范围内
LIKE搜索某种模式
in选择多个
  • where子句 示例
-- 通过 WHERE 子句来限定 SELECT 的查询条件:
 
-- 1.查询status为1的所有用户
select * from users where status=1

-- 2.查询id>1的所有用户
select * from users where id>1

-- 3.查询username不为zs的所有用户
select * from users where username<>'zs'

-- 将id为12的密码 都修改为000000
update users set password='000000' where id in (1,2)

AND 和 OR 运算符

示例

-- 删除id>=1且status=1的行
delete from users where id>=1 and status=1

ORDER BY 子句

ORDER BY 语句用于根据指定的列对结果集进行排序

ORDER BY 语句默认按照升序对记录进行排序。

如果您希望按照降序对记录进行排序,可以使用 DESC 关键字。

  1. **升序排序 **
-- 按照status按照升序排序
select * from users order by status;
select * from users order by status ASC;
  1. **降序排序 **
-- 按照id进行降序排序
select * from users order by id DESC;
  1. 多重排序
-- 先按照 status 字段进行降序排序,再按照 username 的字母顺序,进行升序排序
select * from users order by status desc,username asc;

COUNT(*) 函数

返回查询结果的总数据条数

select count(*) from 表名称

-- 查询users表中status为0 的总数据条数
select count(*) from users where status=0

-- 查询users表中gender`列不为空` 的总数据条数
select count(gender) from users
-- 等价于如下
select count(*) as gender from users where gender<>'';

使用 AS 为列设置别名

-- 将列名称从 count(*) 修改为total
select count(*) as total from users where status=0

在项目中操作mysql模块

下载模块

npm i mysql

配置与测试模块

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

// 测试 mysql 模块能否正常工作
db.query('select 1', (err, results) => {
  // mysql 模块工作期间报错了
  if(err) return console.log(err.message)
  // 能够成功的执行 SQL 语句
  console.log(results)// [ RowDataPacket { '1': 1 } ]
})

查询数据

// 查询 users 表中 gender="女"所有的数据
const sqlStr = 'select * from users where gender="女"';
db.query(sqlStr, (err, results) => {
    // 查询数据失败
    if (err) return console.log(err.message)
        // 查询数据成功
        // 注意:如果执行的是 select 查询语句,则执行的结果是数组
    console.log(results)// 返回的是数组
})

插入数据

使用?占位符,是为了避免spq注入攻击

// 向 users 表中,新增一条数据,其中 username 的值为 Spider-Man,password 的值为 pcc123
const user = { username: 'Spider-Man', password: 'pcc123' };
// 定义待执行的 SQL 语句 ??是占位,可以通过数据为其指定值
const sqlStr = 'insert into users (username, password) values (?, ?)';
// 执行 SQL 语句
db.query(sqlStr, [user.username, user.password], (err, results) => {
    // 执行 SQL 语句失败了
    if (err) return console.log(err.message);
    // 成功了
    // 注意:如果执行的是 insert into 插入语句,则 results 是一个对象
    // 可以通过 affectedRows 属性,来判断是否插入数据成功
    if (results.affectedRows === 1) {
        console.log('插入数据成功!')
    }
})

插入数据的便捷方式

const user = { username: 'Spider-Man2', password: 'pcc4321' };
// 定义待执行的 SQL 语句
const sqlStr = 'insert into users set ?';
// 执行 SQL 语句
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=?';
// 执行 SQL 语句
db.query(sqlStr, [user.username, user.password, user.id], (err, results) => {
    if (err) return console.log(err.message);
    // 注意:执行了 update 语句之后,执行的结果,也是一个对象,可以通过 affectedRows 判断是否更新成功
    if (results.affectedRows === 1) {
        console.log('更新成功')
    }
})

更新数据的便捷方式

// 演示更新数据的便捷方式
const user = { id: 6, username: 'aaaa', password: '0000' };
// 定义 SQL 语句
const sqlStr = 'update users set ? where id=?';
// 执行 SQL 语句
db.query(sqlStr, [user, user.id], (err, results) => {
    if (err) return console.log(err.message)
    if (results.affectedRows === 1) {
        console.log('更新数据成功')
    }
})

删除用户

// 删除 id 为 5 的用户
const sqlStr = 'delete from users where id=?';
db.query(sqlStr, 5, (err, results) => {
    if (err) return console.log(err.message);
    // 注意:执行 delete 语句之后,结果也是一个对象,也会包含 affectedRows 属性
    if (results.affectedRows === 1) {
        console.log('删除数据成功')
    }
})

标记删除

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

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

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

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

前后端的身份验证

Web 开发模式

  1. 基于服务端渲染的传统 Web 开发模式
  2. 基于前后端分离的新型 Web 开发模式 截屏2021-07-24 下午2.50.44.png

服务端渲染的 Web 开发模式

服务器发送给客户端的 HTML 页面,是在服务器通过字符串的拼接,动态生成的。因此,客户端不 需要使用 Ajax 这样的技术额外请求页面的数据。

优点:

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

缺点:

  • 占用服务器端资源。即服务器端完成 HTML 页面内容的拼接,如果请求较多,会对服务器造成一定的访问压力。
  • 不利于前后端分离,开发效率低。使用服务器端渲染,则无法进行分工合作,尤其对于前端复杂度高的项目,不利于 项目高效开发。

前后端分离的 Web 开发模式

前后端分离的开发模式,依赖于 Ajax 技术的广泛应用。

即后端不提供完整的 HTML 页面内容,而 是提供一些 API 接口,使得前端可以获取到 json 数据;然后前端通过 Ajax 调用后端提供的 API 接口,拿到 json 数据 之后再在前端进行 HTML 页面的拼接,最终展示在浏览器上。

简而言之,前后端分离的 Web 开发模式,就是后端只负责提供 API 接口,前端使用 Ajax 调用接口的开发模式。

优点:

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

缺点:

  • 不利于 SEO。因为完整的 HTML 页面需要在客户端动态拼接完成,所以爬虫对无法爬取页面的有效信息。(解决方 案:利用 Vue、React 等前端框架的 SSR (server side render)技术能够很好的解决 SEO 问题!)

如何选择开发模式

  • 比如企业级网站,主要功能是展示而没有复杂的交互,并且需要良好的 SEO,则这时我们就需要使用服务器端渲染;

  • 而类似后台管理页面,交互性比较强,不需要 SEO 的考虑,那么就可以使用前后端分离的开发模式。

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

身份认证

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

  • 服务端渲染推荐使用 Session 认证机制

  • 前后端分离推荐使用 JWT 认证机制

Session 认证机制

HTTP 协议的无状态性

每次的 HTTP 请求都是独立的,连续多个 HTTP 请求之间没有直接的关系,服务器不会 主动保留每次 HTTP 请求的状态。

cookie

医院病例,病例拿在自己手里(cookie存在 浏览器端

Cookie 是存储在用户浏览器中的一段不超过 4 KB 的字符串。它由一个名称(Name)、一个值(Value)和其它几个用 于控制 Cookie 有效期、安全性、使用范围的可选属性组成。

不同域名下的 Cookie 各自独立,每当客户端发起请求时,会自动把当前域名下所有未过期的 Cookie 一同发送到服务器。

Cookie的几大特性:

  • 自动发送
  • 域名独立
  • 过期时限
  • 4KB限制 截屏2021-07-24 下午3.34.16.png

截屏2021-07-24 下午3.37.39.png

Session 的工作原理

银行机制,钱存在银行里(sesssion存在 服务器端

截屏2021-07-24 下午3.56.55.png

Session 认证的局限性

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

客户端的app或小程序 是不支持cookie的!

在Express 中使用 Session 认证

1681627118534_.pic_hd.jpg

npm i express-session
// 导入 express 模块
const express = require('express')
    // 创建 express 的服务器实例
const app = express()

// TODO_01:请配置 Session 中间件
const session = require('express-session')
app.use(
    session({
        secret: 'itheima', // 盐值(用来增加 加密的复杂度--注意:解密也需要用到相同的盐值)
        resave: false,
        saveUninitialized: true,
    })
)

// 托管静态页面
app.use(express.static('./pages'));
// 解析 POST 提交过来的表单数据
app.use(express.urlencoded({ extended: false }));

// 登录的 API 接口
app.post('/api/login', (req, res) => {
    // 判断用户提交的登录信息是否正确
    if (req.body.username !== 'admin' || req.body.password !== '000000') {
        return res.send({ status: 1, msg: '登录失败' })
    }

    // TODO_02:请将登录成功后的用户信息,保存到 Session 中
    // 注意:只有成功配置了 express-session 这个中间件之后,才能够通过 req 点出来 session 这个属性
    req.session.user = req.body // 用户的信息
    req.session.islogin = true // 用户的登录状态

    res.send({ status: 0, msg: '登录成功' })
})

// 获取用户姓名的接口
app.get('/api/username', (req, res) => {
    // TODO_03:请从 Session 中获取用户的名称,响应给客户端
    if (!req.session.islogin) {
        return res.send({ status: 1, msg: 'fail' })
    }
    res.send({
        status: 0,
        msg: 'success',
        username: req.session.user.username,
    })
})

// 退出登录的接口
app.post('/api/logout', (req, res) => {
    // TODO_04:清空 Session 信息
    // 注意:销毁的是当前 浏览器ip的session 不会影响其他域名的session
    req.session.destroy()
    res.send({
        status: 0,
        msg: '退出登录成功',
    })
})

// 调用 app.listen 方法,指定端口号并启动web服务器
app.listen(80, function() {
    console.log('Express server running at http://127.0.0.1:80')
})

JWT 认证机制

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

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

截屏2021-07-25 上午9.48.23.png

JWT 的组成部分

JWT 通常由三部分组成,分别是 Header(头部)、Payload(有效荷载)加密、Signature(签名)。三者之间使用英文的“.”分隔

  • Payload 部分才是真正的用户信息,它是用户信息经过加密之后生成的字符串。

  • Header 和 -Signature 是安全性相关的部分,只是为了保证 Token 的安全性。

JWT 的使用方式

客户端收到服务器返回的 JWT 之后,通常会将它储存在 localStorage 或 sessionStorage 中。 此后,客户端每次与服务器通信,都要带上这个 JWT。推荐的做法是把 JWT 放在 HTTP 请求头Authorization 字段 中

Authorization: Bearer <token>

在 Express 中使用 JWT

截屏2021-07-25 上午10.45.51.png

# 安装两个包
npm i jsonwebtoken express-jwt

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

app.js

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

// TODO_01:安装并导入 JWT 相关的两个包,分别是 jsonwebtoken 和 express-jwt
const jwt = require('jsonwebtoken')
const expressJWT = require('express-jwt')

// 允许跨域资源共享
const cors = require('cors')
app.use(cors())

// 解析 post 表单数据的中间件
const bodyParser = require('body-parser')
app.use(bodyParser.urlencoded({ extended: false }))

// TODO_02:定义 secret 密钥,建议将密钥命名为 secretKey
const secretKey = 'itheima No1 ^_^'

// TODO_04:注册将 JWT 字符串解析还原成 JSON 对象的中间件
// 注意:只要配置成功了 express-jwt 这个中间件,就可以把解析出来的用户信息,挂载到 req.user 属性上
// .unless()--指定哪些接口 不需要访问权限
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: '登录失败!',
        })
    }
    // 登录成功
    // TODO_03:在登录成功之后,调用 jwt.sign() 方法生成 JWT 字符串。并通过 token 属性发送给客户端
    // 参数1:用户的信息对象
    // 参数2:加密的秘钥
    // 参数3:配置对象,可以配置当前 token 的有效期--- 如果解密的时间超过了30s,这个tokenStr就作废了
    // 记住:千万不要把密码加密到 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) {
    // TODO_05:使用 req.user 获取用户信息,并使用 data 属性将用户信息发送给客户端
    console.log(req.user)
    res.send({
        status: 200,
        message: '获取用户信息成功!',
        data: req.user, // 要发送给客户端的用户信息
    })
})

// TODO_06:使用全局错误处理中间件,捕获解析 JWT 失败后产生的错误
app.use((err, req, res, next) => {
    // 这次错误是由 token 解析失败导致的
    if (err.name === 'UnauthorizedError') {
        return res.send({
            status: 401,
            message: '无效的token',
        })
    }
    res.send({
        status: 500,
        message: '未知的错误',
    })
})

// 调用 app.listen 方法,指定端口号并启动web服务器
app.listen(8888, function() {
    console.log('Express server running at http://127.0.0.1:8888')
})

ES6 模块化

需要在 package.json 中 加上 "type": "module"

导出与导入

默认

注意:

  • 只能默认导出一次

  • 默认导入的命名,遵循变量/常量声明的规则,本质是常量

  • 导入时使用解构语法获取特定的属性,需要配合挨个按需导出使用

# 01.js
// 默认导出
let name='jean';
let age=17;
function run(){};

export default {
    name,
    age,
    run
}

# 02.js
// 默认导入
import s1 from '文件相对路径'

s1是一个常量对象,不能改其引用

按需

注意:

  • 按需导出可以使用多次

  • 导出与导出的名称必须一致

# 01.js
export let name='jean';
export let age=17;
export function say(){};

export default {
    a:20
}

# 02.js
import info, {name, age as ageNum, say} from './01.js';
// 将age重命名为ageNum

info---{a:20}

直接导入 并执行模块中的代码

#01.js
for(let i=0;i<5;i++){
    console.log(i);
}

#02.js
import './01.js'

Promise(ES6新)

Promise是一个构造函数---const p = new Promise()---实例是一个异步操作

作用:解决 由于回调函数 嵌套出现的“回调地狱”

本质是 解耦

Promise.prototype 有一个.then方法,then默认 会返回一个新的promise对象

截屏2021-07-27 下午7.47.18.png

let p = new Promise(function(successCB, errorCB) {
    let age=19;
    if (i > 18) successCB('可以喝酒');
    else errorCB('不能喝酒');
});

//p.then(
//    success => { console.log(success); },
//    err => { console.log(err); }
//);

p.then(
    success => { console.log(success); },
).catch(
    err => { console.log(err); });

// 结果:可以喝酒

Promise.then().catch

安装

npm i then-fs

注意:

  • 如果then中直接return 非promise对象,会自动创建一个promise对象

  • 如果多个promise执行,最后添加的唯一一个catch,前面所有的promise对象共用该错误回调函数

    • 一旦前面的promise对象出现错误,会直接进入到最后catch中运行,中间的then将不再执行
  • 如果不希望后面的代码无法执行,可以把.catch往前移

import fs from 'then-fs';

fs.readFile('./files/01.js', 'utf8')
    .catch(err=>{
        console.log('找不到文件');
    })
    .then(r1 => {
        console.log(r1);
        return fs.readFile('./files/02.txt', 'utf8'); // 返回一个新的promise实例对象
    }).then(r2 => {
        console.log(r2);
        return fs.readFile('./files/03.txt', 'utf8');
    }).then(r3 => {
        console.log(r3);
        return '自动生成一个promise对象';
    }).then(msg => {
        console.log(msg);
    })
    
// 结果: 找不到文件 undefined 2 3 自动生成一个promise对象

Promise.all() | .race()

import fs from 'then-fs';

let p1 = fs.readFile('./files/1.txt','utf8');
let p2 = fs.readFile('./files/2.txt','utf8');
let p3 = fs.readFile('./files/3.txt','utf8');

# 1. all()
// 3个任务都按顺序成功执行,才调用then 中的回调函数
Promise.all([p1, p2, p3])
    .then(function(res) {
        console.log(res);// 输出的是数组['1','2','3']
    }).catch(function(err) {
        console.log(err);
    })

# 2. race()
// 只要有一个任务成功执行了,就调用then函数
Promise.race([p1, p2, p3])
    .then(function(res) {
        console.log(res);// 1
    }).catch(function(err) {
        console.log(err);
    })

基于promise封装读文件的方法

import fs from 'fs';

const myfs = {}

myfs.readFile = function(filepath, charset) {
    return new Promise(function(resolve, reject) {
        // 使用 内置 fs模块读取文件
        fs.readFile(filepath, charset, (err, data) => {
            // 读取失败,调用失败回调函数,并传入错误消息
            if (err) return reject(err);
            // 读取成功,调用成功回调函数,并传入文件内容
            resolve(data);
        })
    });
}

myfs.readFile('./files/01.txt', 'utf8').then(data => {
    console.log('文件读取成功------');
    console.log(data);
}).catch(err => {
    console.log('文件读取失败------');
    console.log(err);
})

async/await(ES8新)

注意:第一个 await 前的代码是同步执行,后面的代码都会异步按顺序执行

import fs from 'then-fs';

console.log('a');
async function ss() {
    console.log('b');
    const r1 = await fs.readFile('./files/01.txt', 'utf8');
    const r2 = await fs.readFile('./files/02.txt', 'utf8');
    const r3 = await fs.readFile('./files/03.txt', 'utf8');
    console.log('c');
}
ss();
console.log('d');

// a b d c

EventLoop

参考文章

同步任务(synchronous)

  • 由JavaScript主线程(js引擎)次序执行

异步任务(asynchronous)

  • 委托给宿主环境(操作系统--浏览器)执行

截屏2021-07-28 上午10.47.09.png