服务器端基础概念

1 服务器端基础概念

1.1 网站的组成

网站应用程序主要分为两大部分:客户端和服务器端。

客户端:在浏览器中运行的部分,就是用户看到并与之交互的界面程序,使用HTML、CSS、JavaScript构建。

服务器端:在服务器中运行的部分,负责存储数据和处理应用逻辑。

1.2 Node 网站服务器

能够提供网站访问服务的机器就是网站服务器,它能够接受客户端的请求,能够对请求作出响应

1.3 IP地址

互联网中设备的唯一标识。

IP是Internet Protocol Address 的缩写,代码互联网协议地址。

1.4 域名

由于 IP 地址难于记忆,所以就产生了域名的概念,所谓域名就是平时上网所使用的网址。

虽然在地址栏中输入的是网址,但是最终还是会将域名转换为 ip 才能访问到指定的网站服务器。

1.5 端口

端口是计算机与外界通讯交流的出口,用来区分服务器电脑中提供的不同服务。

1.6 URL

统一资源定位符,又叫URL(Uniform Resource Location),是专为标识 Internet 网上资源位置而设的一种编址方式。
我们平时所说的网页地址指的即是URL。

URL 的组成:

传输协议://服务器IP或域名:端口/资源所在位置标识
https://www.cnblogs.com/joe235/p/12745332.html

http: 超文本传输协议,提供了一种发布和接收 HTML 页面的方法。
https: 是以安全为目标的 HTTP 通道,在HTTP的基础上通过传输加密和身份认证保证了传输过程的安全性。

1.7 开发过程中客户端和服务器端说明

本机域名:localhost
本地IP:127.0.0.1

2 创建 web 服务器

示例代码:

// 引用系统模块
const http = require('http');
// 创建 web 服务器
const app = http.createServer();
// 当客户端发送请求的时候
app.on('request', (req, res) => {
  // 响应
  res.end('<h1>hi,user</h1>');
});
// 监听3000端口
app.listen(3000);
console.log('服务器已启动,监听3000端口,请访问locathost:3000')

例子:

1.新建项目 server,并创建 app.js 文件:

// 用于创建网站服务器的模块
const http = require('http');
// 创建 web 服务器,app 对象就是网站服务器对象
const app = http.createServer();
// 当客户端有请求来的时候
app.on('request', (req, res) => {
  // 响应
  res.end('<h2>hello user</h2>');
});
// 监听3000端口
app.listen(3000);
console.log('网站服务器已启动,监听3000端口,请访问localhost:3000')

2.回到命令行工具,切换到 server 目录下,输入:

nodemon app.js

3.打开浏览器,输入 localhost:3000

可以看到浏览器上显示:

3 HTTP 协议

3.1 HTTP 协议的概念

超文本传输协议HTTP(HyPer Text Transfer Protocol)规定了如何从网站服务器传输超文本到本地浏览器。它基于客户端服务器架构工作,是客户端(用户)和服务器端(网站)请求和应答的标准。

3.2 报文

在 HTTP 请求和响应的过程中传递的数据块就叫报文,包括要传送的数据和一些附加信息,并且要遵循规定好的格式。

3.3 请求报文

1、请求方式(Resquest Method):

  GET 请求数据

  POST 发送数据

例子:

在 server 项目下的 app.js 中添加 req 代码:

// 用于创建网站服务器的模块
const http = require('http');
// 创建 web 服务器,app 对象就是网站服务器对象
const app = http.createServer();
// 当客户端有请求来的时候
app.on('request', (req, res) => {
  // 获取请求方式
  // req.method
  console.log(req.method);
  // 响应
  res.end('<h2>hello user</h2>');
});
// 监听3000端口
app.listen(3000);
console.log('网站服务器已启动,监听3000端口,请访问localhost:3000')

在 server 项目根目录下,新建 form.html 文件:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <!-- 
   method:指定当前表单提交的方式 action:指定当前表单提交的地址
--> <form method="POST" action="http://localhost:3000"> <input type="submit" name=""> </form> </body> </html>

右键点击在浏览器运行

点击提交按钮,会跳转到 http://localhost:3000

回到命令行工具中,可以看到:先是POST输出,后是GET(表单的跳转行为默认是get方式)

打开 app.js 文件,添加修改代码:

// 用于创建网站服务器的模块
const http = require('http');
// 创建 web 服务器,app 对象就是网站服务器对象
const app = http.createServer();
// 当客户端有请求来的时候
app.on('request', (req, res) => {
  // 获取请求方式
  // req.method
  console.log(req.method);
  if (req.method == 'POST') {
    res.end('post');
  } else if (req.method == 'GET') {
    res.end('get');
  }
  // 响应
  // res.end('<h2>hello user</h2>');
});
// 监听3000端口
app.listen(3000);
console.log('网站服务器已启动,监听3000端口,请访问localhost:3000')

重新打开浏览器,输入:localhost:3000

可以看到页面上显示“get”

在浏览器重新打开 form.html ,点击提交按钮

可以看到页面上显示的是“post”

2、请求地址(Requset URL)

app.on('request', (req, res) => {
  req.headers // 获取请求报文 
  req.url // 获取请求地址
  req.method // 获取请求方法
});

例子:

在 app.js 文件中添加获取请求地址:

// 用于创建网站服务器的模块
const http = require('http');
// 创建 web 服务器,app 对象就是网站服务器对象
const app = http.createServer();
// 当客户端有请求来的时候
app.on('request', (req, res) => {
  // 获取请求方式
  // req.method
  // console.log(req.method);

  // 获取请求地址 req.url
  console.log(req.url);

  if (req.method == 'POST') {
    res.end('post');
  } else if (req.method == 'GET') {
    res.end('get');
  }
  // 响应
  // res.end('<h2>hello user</h2>');
});
// 监听3000端口
app.listen(3000);
console.log('网站服务器已启动,监听3000端口,请访问localhost:3000')

打开浏览器,在地址栏中输入:localhost:3000/index

回到命令行工具,可以看到:

再在浏览器的地址栏中输入: localhost:3000/list

那么在命令行工具就可以看到,打印出:/list

继续修改 app.js 文件:

// 用于创建网站服务器的模块
const http = require('http');
// 创建 web 服务器,app 对象就是网站服务器对象
const app = http.createServer();
// 当客户端有请求来的时候
app.on('request', (req, res) => {
  // 获取请求方式 req.method
  // console.log(req.method);

  // 获取请求地址 req.url
  // console.log(req.url);

  if (req.url == '/index' || req.url == '/') {
    res.end('Welcome to homepage');
  } else if (req.url == '/list') {
    res.end('Welcome to listpage');
  } else {
    res.end('not found');
  }

  if (req.method == 'POST') {
    res.end('post');
  } else if (req.method == 'GET') {
    res.end('get');
  }
  // 响应
  // res.end('<h2>hello user</h2>');
});
// 监听3000端口
app.listen(3000);
console.log('网站服务器已启动,监听3000端口,请访问localhost:3000')

打开浏览器在地址栏中输入:localhost:3000/index,或者输入:localhost:3000

页面上会显示:“Welcome to homepage”。

同理,输入:localhost:3000/list,页面会显示'Welcome to listpage'。

如果输入其他地址,页面会显示“not found”。

3、请求报文信息(Requset headers)

例子:

在 app.js 中添加请求报文信息代码:

// 用于创建网站服务器的模块
const http = require('http');
// 创建 web 服务器,app 对象就是网站服务器对象
const app = http.createServer();
// 当客户端有请求来的时候
app.on('request', (req, res) => {
  // 获取请求方式 req.method
  // console.log(req.method);

  // 获取请求地址 req.url
  // console.log(req.url);

  // 获取请求报文信息 req.headers
  console.log(req.headers);

  if (req.url == '/index' || req.url == '/') {
    res.end('Welcome to homepage');
  } else if (req.url == '/list') {
    res.end('Welcome to listpage');
  } else {
    res.end('not found');
  }

  if (req.method == 'POST') {
    res.end('post');
  } else if (req.method == 'GET') {
    res.end('get');
  }
  // 响应
  // res.end('<h2>hello user</h2>');
});
// 监听3000端口
app.listen(3000);
console.log('网站服务器已启动,监听3000端口,请访问localhost:3000')

刷新页面,打开命令行工具可以看到:

如果只想获取 accept:项的信息,那么可以写为:

console.log(req.headers['accept:']);

刷新页面,回到命令行工具可以看到:

 

3.4 响应报文

1、HTTP 状态码

  200 请求成功
  404 请求的资源没有被找到
  500 服务器错误
  400 客户端请求有语法错误

比如例子:

res.writeHead(200)

2、内容类型

  text/html
  text/css
  application/javascript
  image/jpeg
  application/json

例子:app.js文件添加 content-type:

// 用于创建网站服务器的模块
const http = require('http');
// 创建 web 服务器,app 对象就是网站服务器对象
const app = http.createServer();
// 当客户端有请求来的时候
app.on('request', (req, res) => {
  // 获取请求方式 req.method
  // console.log(req.method);

  // 获取请求地址 req.url
  // console.log(req.url);

  // 获取请求报文信息 req.headers
  // console.log(req.headers['accept']);

  res.writeHead(200, {
    'content-type': 'text/plain' // 纯文本类型
  });

  if (req.url == '/index' || req.url == '/') {
    res.end('<h2>Welcome to homepage</h2>');
  } else if (req.url == '/list') {
    res.end('Welcome to listpage');
  } else {
    res.end('not found');
  }

  if (req.method == 'POST') {
    res.end('post');
  } else if (req.method == 'GET') {
    res.end('get');
  }
  // 响应
  // res.end('<h2>hello user</h2>');
});
// 监听3000端口
app.listen(3000);
console.log('网站服务器已启动,监听3000端口,请访问localhost:3000')

这时刷新页面,显示的是:

因为是纯文本类型,把<h2>也当做文本输出了。

如果想识别 h2 标签,那要把 content-type 类型修改为 html:

res.writeHead(200, {
    'content-type': 'text/html' 
});

刷新页面后显示为:

另外,如果输出内容为中文,比如:

if (req.url == '/index' || req.url == '/') {
    res.end('<h2>欢迎来到首页</h2>');
}

有时候页面会出现乱码,那么我们需要指定编码:

res.writeHead(200, {
    'content-type': 'text/html;charset=utf8'
});

刷新后就没有乱码的问题了。

4 HTTP 请求与相应处理

4.1 请求参数

客户端向服务器端发送请求时,有时候需要携带一些客户信息,客户信息需要通过请求参数的形式传递到服务器,比如登录操作。

4.2 GET 请求参数

参数被放置在浏览器地址中,例如:http://localhost:3000/?name=zhangsan&age=20

node 内置的 url 模块,用于处理 url 地址

// 用于处理 url 地址
const url = require('url');

url.parse(req.url); // 返回对象

例子:在浏览器输入“localhost:3000/index?name=zhangsan&age=20”

修改 app.js 文件:

// 用于创建网站服务器的模块
const http = require('http');
// 用于处理 url 地址
const url = require('url');
// 创建 web 服务器,app 对象就是网站服务器对象
const app = http.createServer();
// 当客户端有请求来的时候
app.on('request', (req, res) => {
  // 获取请求方式 req.method
  // console.log(req.method);

  // 获取请求地址 req.url
  // console.log(req.url);

  // 获取请求报文信息 req.headers
  // console.log(req.headers['accept']);

  res.writeHead(200, {
    'content-type': 'text/html;charset=utf8' // plain 纯文本类型
  });

  console.log(req.url);
  console.log(url.parse(req.url));

  if (req.url == '/index' || req.url == '/') {
    res.end('<h2>欢迎来到首页</h2>');
  } else if (req.url == '/list') {
    res.end('Welcome to listpage');
  } else {
    res.end('not found');
  }

  if (req.method == 'POST') {
    res.end('post');
  } else if (req.method == 'GET') {
    res.end('get');
  }
  // 响应
  // res.end('<h2>hello user</h2>');
});
// 监听3000端口
app.listen(3000);
console.log('网站服务器已启动,监听3000端口,请访问localhost:3000')

刷新页面,页面显示“not found”,回到命令行工具,发现显示:

我们想把以 & 符号分隔的字符串,转换成对象的形式,添加第二个参数:

// 第1个参数:要解析的url地址 
// 第2个参数:将查询参数解析成对象的形式
console.log(url.parse(req.url, true));

意思是:把查询参数转换成对象的形式。

这时再刷新页面,然后回到命令行工具:

 

拿到对象,修改代码为:

  // 第1个参数:要解析的url地址 
  // 第2个参数:将查询参数解析成对象的形式
  let params = url.parse(req.url, true).query;
  console.log(params.name); 
  console.log(params.age);

刷新页面后,回到命令行工具显示:zhangsan   20

继续修改 app.js 代码:

  // 第1个参数:要解析的url地址 
  // 第2个参数:将查询参数解析成对象的形式
  let {query, pathname} = url.parse(req.url, true);
  console.log(query.name); // zhangsan
  console.log(query.age); // 20

  if (pathname == '/index' || pathname == '/') {
    res.end('<h2>欢迎来到首页</h2>');
  } else if (pathname == '/list') {
    res.end('Welcome to listpage');
  } else {
    res.end('not found');
  }

刷新页面,发现可以显示“欢迎来到首页”了。

4.3 POST 请求参数

参数被放置在请求体中进行传输
获取 POST 参数需要使用 data 事件和 end 事件
使用 querystring 系统模块将参数转换为对象格式

示例代码:

// 导入系统模块 querystring 用于将 HTTP 参数转换为对象格式
const querystring = require('querystring');

app.on('request', (req, res) => {
  let postData = '';
  // 监听参数传输事件
  req.on('data', (chunk) => postData += chunk;);
  // 监听参数传输完毕事件
  req.on('end', () => {
    console.log(querystring.parse(postData));
  }); 
});

例子:

打开 form.html 文件,添加表单项代码:

<form method="POST" action="http://localhost:3000">
    <input type="text" name="infoname" />
    <input type="password" name="password" />
    <input type="submit" name="">
</form>

右键点击在浏览器运行 form.html 

随便输入一些内容点击提交,然后打开 network 选项可以看到:

此时知道如何从客户端发送 POST 请求参数了。

下面要在服务器端接收这些参数:

在根目录下新建一个文件 post.js:

// 用于创建网站服务器的模块
const http = require('http');
// 创建 web 服务器,app 对象就是网站服务器对象
const app = http.createServer();
// 当客户端有请求来的时候
app.on('request', (req, res) => {
  // post 参数是通过事件的方式接收的
  // data 当请求参数传递的时候触发 data 事件
  // end 当参数传递完成的时候触发 end 事件

  let postParams= '';

  req.on('data', params => {
    postParams += params;
  });

  req.on('end', () => {
    console.log(postParams);
  });

  res.end('ok');
});
// 监听3000端口
app.listen(3000);
console.log('网站服务器已启动,监听3000端口,请访问localhost:3000')

打开命令行工具,输入:

nodemon post.js

然后刷新浏览器,输入信息后,点击提交按钮,发现页面地址跳转到:http://localhost:3000,并且显示“ok”。

而命令行工具则显示:infoname=213&password=123345

是刚刚输入的信息内容,表示参数接收成功了。

这个参数依然是字符串类型,我们想要的是对象的形式。

node 提供的内置模块 querystring。

在 post.js 中导入 querystring 模块:

// 处理请求参数模块
const querystring = require('querystring');

使用 parse() 方法:

  req.on('end', () => {
    console.log(querystring.parse(postParams));
  });

再刷新页面重新输入信息提交,然后可以在命令行工具中看到:{ infoname: '213', password: '123345' }

4.4 路由

路由是指客户端请求地址与服务器端程序代码的对应关系。简单的说,就是请求什么响应什么。

核心代码:

// 当客户端发来请求的时候
app.on('request',  (req, res) => {
  // 获取客户端的请求路径
  let {pathname} = url.parse(req.url);
  if (pathname == '/' || pathname == '/index') {
    res.end('欢迎来到首页');
  } else if (pathname == '/list') {
    res.end('欢迎来到列表页面');
  } else {
    res.end('抱歉,您访问的页面出游了');
  }
});

例子:

新建 route 项目文件夹,并新建 app.js 文件:

// 1.引入系统模块 http
const http = require('http');
// 用于处理 url 地址
const url = require('url');
// 2.创建网站服务器
const app = http.createServer();
// 3.为网站服务器对象添加请求事件
app.on('request', (req, res) => {
  // 获取客户端的请求方式 req.method
  const method = req.method.toLowerCase(); // toLowerCase 转为小写
  // 获取客户端的请求地址 req.url
  const pathname = url.parse(req.url).pathname;

  res.writeHead(200, {
    'content-type': 'text/html;charset=utf8'
  });
  // 4.实现路由功能
  if (method == 'get') {
    if (pathname == '/' || pathname == '/index') {
      res.end('欢迎来到首页');
    } else if (pathname == '/list') {
      res.end('欢迎来到列表页');
    } else {
      res.end('您访问的也不存在');
    }
  } else if (method == 'post') {

  }
});

// 监听3000端口
app.listen(3000);
console.log('服务器启动成功')

在命令行工具输入:

nodemon app.js

在浏览器输入:localhost:3000

可以看到页面显示:欢迎来到列表页

4.5 静态资源

服务器端不需要处理,可以直接响应给客户端的资源就是静态资源,例如CSS、JavaSCript、image文件。

例子:

新建项目 static 文件夹,并创建 public 文件夹,把上次 gulp-demo 项目 dist 目录下的文件都拷贝过来。

再新建 app.js 文件:

// 1.引入系统模块 http
const http = require('http');
// 2.创建网站服务器
const app = http.createServer();
// 3.为网站服务器对象添加请求事件
app.on('request', (req, res) => {
  res.end('ok');
});

// 监听3000端口
app.listen(3000);
console.log('服务器启动成功')

在命令行工具输入:

nodemon app.js

此时页面显示 ok,代表服务器启动成功了。

这时我们想在浏览器直接输入:localhost:3000/default.html  就可以访问到 public 目录下的 default.html 文件,需要:

1)先通过 req.url 获取到用户的请求路径,也就是说获取到 /default.html

2)把这个请求路径转换为文件所在服务器上的真实物理路径,然后读取这个文件的内容

3)最终把读取的内容返回给客户端

继续编辑 app.js 文件:

// 引入系统模块 http
const http = require('http');
// 用于处理 url 地址
const url = require('url');
// 导入系统模块 path 模块
const path = require('path');
// 导入系统模块 fs
const fs = require('fs');
// 创建网站服务器
const app = http.createServer();
// 为网站服务器对象添加请求事件
app.on('request', (req, res) => {
  // 1.获取用户的请求路径
  let pathname = url.parse(req.url).pathname;
  // 2.将用户的请求路径转换为实际的服务器硬盘路径
  let realPath = path.join(__dirname, 'public' + pathname);
  // 3.通过模块内部的 readFile 方法,读取文件内容
  fs.readFile(realPath, 'utf8', (err, result) => {
    // 如果文件读取失败
    if (err != null) {
      res.writeHead(404, {
        'content-type': 'text/html;charset=utf8'
      });
      res.end('文件读取失败');
      return;
    }
    res.end(result);
  });
  //res.end('ok');
});

// 监听3000端口
app.listen(3000);
console.log('服务器启动成功')

此时在浏览器输入:localhost:3000/default.html ,可以访问到页面了。

这里图片和样式有点问题,需要修改下代码:

// 这里的 utf8 去掉
fs.readFile(realPath,  (err, result) => {

刷新页面,已经好了:

 

还有个问题,如果输入:localhost:3000 ,页面会显示“文件读取失败”,这里我们也想它访问到 default 页面。

我们需要在路径做个判断,当是‘/’的时候让它也访问‘/default.html’

继续编辑 app.js 文件,添加判断:

// 1.获取用户的请求路径
let pathname = url.parse(req.url).pathname;
pathname = pathname == '/' ? '/default.html' : pathname

这时刷新页面,localhost:3000 也可以访问到页面内容了。

当服务器端向客户端做出响应的时候,要告诉客户端,当前所给的类型是什么。我们只是在错误的时候给了类型,正常的时候没给。

这里需要用到一个第三方模块:mime

功能是:可以根据当前的请求路径分析出这个资源的类型,然后把资源的类型通过返回值返给你。

打开命令行工具先打断服务的运行,然后下载这个 mime:

npm install mime

然后重新运行服务:

nodemon app.js

回到 app.js 文件,先引用模块:

// 导入第三方模块 mime
const mime = require('mime');

app.on('request', (req, res) => {
  。。。

  // 2.将用户的请求路径转换为实际的服务器硬盘路径
  let realPath = path.join(__dirname, 'public' + pathname);

  console.log(mime.getType(realPath));

  。。。
});

刷新页面,然后回到命令行工具,可以看到:

 

这些都是当前请求文件的类型。

新建一个变量,把文件的类型存储下:

  // 请求文件的类型
  let type = mime.getType(realPath);

  // 3.通过模块内部的 readFile 方法,读取文件内容
  fs.readFile(realPath, (err, result) => {
    、、、
    // 成功的报文信息
    res.writeHead(200, {
        'content-type': type
    })
    res.end(result);
  });

此时打开 network 并刷新页面,可以看到样式和图片资源加载出来了。

4.6 动态资源

相同的请求地址不同的响应资源,这种资源就是动态资源。

5 Node.js 异步编程

5.1 同步API,异步API

// 路径拼接
const publib = path.join(__dirname, 'public');

// 请求地址解析
const urlObj = url.parse(req.url);

// 获取文件内容
fs.readFile('./demo.txt', 'utf8', (err, result) => {
  console.log(result);
});

同步API:只有当前 API 执行完成后,才能继续执行下一个 API

例如:

console.log('before');
console.log('after');

结果是:先输出 before,然后再输出 after。

异步API:当前 API 的执行不会阻碍后续代码的执行

例如:

console.log('before');
setTimeout(() => {
  console.log('last');
}, 2000);
console.log('after');

结果是:先输出 before,再输出 after,然后过2秒后输出 last。

5.2 同步API,异步API 的区别(获取返回值)

同步 API 可以从返回值中拿到 API 执行的结果,但是异步 API 是不可以的。

// 同步
function sum (n1, n2) {
  return n1 + n2;
}
const result = sum(10, 20);

可以拿到返回值:30。

例子:

创建项目 async, 新建 getRetrunValue.js 文件:

// 异步
function getMsg () {
  setTimeout(() => {
    return { msg: 'Hello Node.js'}
  }, 2000);
}
const msg = getMsg();
console.log(msg);

在命令行工具中运行:

node getReturnValue.js

结果是:undefined

结论:在异步 API 里,我们是无法通过返回值的方式,去拿到异步 API的执行结果。需要通过回调函数。

5.3 回调函数

自己定义函数让别人去调用。

// getData 函数定义
function getData (callback) {}
// getData 函数调用
getData(() => {});

例子:

新建 callback.js 文件:

function getData (callback) {
  callback();
}

getData(function () {
  console.log('callback 函数被调用了')
});

回到命令行工具中输入:

node callback.js

结果是:callback 函数被调用了

修改下代码:

function getData (callback) {
  callback('123');
}

getData(function (n) {
  console.log('callback 函数被调用了');
  console.log(n);
});

重新在命令行工具中运行

结果是: 

callback 函数被调用了
123

重新修改 getRetrunValue.js 的代码:

// 异步
function getMsg (callback) {
  setTimeout(() => {
    callback({ 
      msg: 'Hello Node.js'
    });
  }, 2000);
}

getMsg(function(data){
  console.log(data);
});

到命令行工具中输入:

node getReturnValue.js

结果是:2秒后显示:{ msg: 'Hello Node.js' }

5.5 同步API,异步API 的区别(代码执行顺序)

同步 API 从上到下依次执行,前面代码会阻塞后面代码的执行。

for(var i = 0; i < 100000; i++) {
  console.log(i)
}
console.log('for 循环后面的代码');

上面的代码: for 循环不执行完,后面的代码是不会执行的。

异步 API 不会等待 API执行完成后再向下执行代码

console.log('代码开始执行');
setTimeout(() => { console.log('2秒后执行的代码') }, 2000);
setTimeout(() => { console.log('0秒后执行的代码') }, 0);
console.log('代码结束执行');

结果是:

代码开始执行
代码结束执行
0秒后执行的代码
2秒后执行的代码

5.6 代码执行顺序分析

1.console.log 是同步 API,会在同步代码执行区执行

2.setTimeout 是异步 API,放到异步代码执行区,紧接着会把异步 API 对应的回调函数,放到回调函数队列中。这时的代码还没有被执行。

3.setTimeout 同上

4.console.log 是同步 API,会在同步代码执行去执行

这时2个同步代码已经执行完成了,然后转到异步代码执行区中去依次执行代码

5.由于第2个定时器是在0秒后执行,系统会在回调函数队列中找到第2个定时器所对应的回调函数,拿到同步代码执行区中去执行

6.等待2秒后,第1个定时器也执行了,系统会在回调函数队列中找到第1个定时器所对应的回调函数,也拿到同步代码执行区中去执行

5.7 Node.js 中的异步 API

fs.readFile('./demo.txt', (err, result) => {]);

fs.readFile 就是异步 API。

var server = http.createServer();
server.on('request', (req, res) => {});

事件监听也是异步 API。

如果异步 API 后面代码的执行依赖当前异步 API 的执行结果,但实际上后续代码在执行的时候异步 API 还没有返回结果,这个问题要怎么解决?

例子:需求:依次读取A文件、B文件、C文件

新建1.txt、2.txt、3.txt:里面分别写入1、2、3

创建 callbakehell.js 文件:

const fs = require('fs');

fs.readFile('./1.txt', 'utf8', (err, result1) => {
  console.log(result1)
  fs.readFile('./2.txt', 'utf8', (err, result2) => {
    console.log(result2)
    fs.readFile('./3.txt', 'utf8', (err, result3) => {
      console.log(result3)
    });  
  });
});

回到命令行工具,输入:

node callbackhell.js

可以看到结果是:

1
2
3

证明确实是依次读取文件内容。

但是这种嵌套形式的话,如果嵌套的层数过多,维护起来会很麻烦。这种回调嵌套回调再嵌套回调,我们比作为:回调地狱。

5.8 Promise 

Promise 出现的目的是解决 Node.js 异步编程中回调地狱的问题。

基础语法:

let promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    if (true) {
      resolve({name: '张三'})
    } else {
      reject('失败了')
    }
  }, 2000);
});

promise.then(
  result => console.log(resule); // {name: '张三'}
)
.catch(
  error => console.log(error); // 失败了
)

例子:

新建 promise.js 文件:

const fs = require('fs');

let promise = new Promise((resolve, reject) => {
  fs.readFile('./1.txt', 'utf8', (err, result) => {
      if (err != null) {
        reject(err);
      } else {
        resolve(result);
      }
  });
});

promise.then((result) => {
  console.log(result);
})
.catch((err) => {
  console(err);
});

回到命令行工具,输入:

node promise.js

结果是:1

下面就要解决依次读取文件例子的回调地狱的问题:

新建 promise2.js 文件:

const fs = require('fs');

// fs.readFile('./1.txt', 'utf8', (err, result1) => {
//   console.log(result1)
//   fs.readFile('./2.txt', 'utf8', (err, result2) => {
//     console.log(result2)
//     fs.readFile('./3.txt', 'utf8', (err, result3) => {
//       console.log(result3)
//     });  
//   });
// });

function p1 () {
  return new Promise ((resolve, reject) => {
    fs.readFile('./1.txt', 'utf8', (err, result) => {
      resolve(result)
    }); 
  });
}

function p2 () {
  return new Promise ((resolve, reject) => {
    fs.readFile('./2.txt', 'utf8', (err, result) => {
      resolve(result)
    }); 
  });
}

function p3 () {
  return new Promise ((resolve, reject) => {
    fs.readFile('./3.txt', 'utf8', (err, result) => {
      resolve(result)
    }); 
  });
}

p1().then((r1) =>{
  console.log(r1);
  return p2();
})
.then((r2) =>{
  console.log(r2);
  return p3();
})
.then((r3) => {
  console.log(r3);
})

p1.then 里 return 了一个 p2 的调用,p2 的调用返回一个 promise 对象,也就是说实际上 return 了一个 promise 对象。在下一个 then 里就能拿到上一个 then 里面 return 的 promise 对象的结果。

回到命令行工具,输入:

node promise2.js

结果为:

1
2
3

5.9 异步函数

异步函数是异步变成语法的终极解决方案,它可以让我们将异步代码写成同步的形式,让代码不再有回调函数嵌套,使代码变得清晰明了。

基础语法:

const fn = async () => ();
async function fn () {}

在普通函数定义的前面加上 async 关键字,普通函数就变成了异步函数。

异步函数默认的返回值是 promise 对象,不是 undefined。

例子:

新建 asyncFunction.js 文件:

// 1.在普通函数定义的前面加上 async 关键字,普通函数就变成了异步函数
// 2.异步函数默认的返回值是 promise 对象,不是 undefined
async function fn () {
  return 123;
}

console.log(fn ())

回到命令行工具,输入:

node asycnFunction.js

结果是:

修改下代码:

// 1.在普通函数定义的前面加上 async 关键字,普通函数就变成了异步函数
// 2.异步函数默认的返回值是 promise 对象,不是 undefined
async function fn () {
  return 123;
}

// console.log(fn ())
fn ().then(function (data) {
  console.log(data)
})

重新执行,结果是:

而错误信息用 throw 来抛出返回,throw 一旦执行以后,后面的代码就不会再执行了。用 .catch 来捕获 throw 抛出的错误信息。

// 1.在普通函数定义的前面加上 async 关键字,普通函数就变成了异步函数
// 2.异步函数默认的返回值是 promise 对象,不是 undefined
// 3.在异步函数内部使用 throw 关键字进行错误的抛出
async function fn () {
  throw '发生了一些错误';
  return 123;
}

// console.log(fn ())
fn ().then(function (data) {
  console.log(data)
}).catch(function (err) {
  console.log(err)
})

执行结果是:

await 关键字

1.它只能出现在异步函数中

2.await promise 它可以暂停异步函数的执行,等待 promise 对象返回结果后,再向下执行函数

例子:还是依次读取文件

// await 关键字
// 1.它只能出现在异步函数中
// 2.await promise 它可以暂停异步函数的执行,等待 promise 对象返回结果后,再向下执行函数

async function p1 () {
  return 'p1';
}
async function p2 () {
  return 'p2';
}
async function p3 () {
  return 'p3';
}

async function run () {
  let r1 = await p1()
  let r2 = await p2()
  let r3 = await p3()
  console.log(r1)
  console.log(r2)
  console.log(r3)
}
run();

运行后结果为:

可以看出是顺序输出的:p1、p2、p3

总结:

async 关键字:

1、普通函数定义前加 async 关键字,普通函数变成异步函数
2、异步函数默认的返回 promise 对象
3、在异步函数内部使用 return 关键字进行结果返回,结果会被包裹的 promise 对象中 return 关键字代替了 resolve 方法
4、在异步函数内部使用 throw 关键字抛出错误异常
5、使用异步函数再链式调用 then 方法获取异步函数执行的结果
6、调用异步函数再链式调用 catch 方法获取异步函数执行的错误信息

await 关键字:

1、await 关键字只能出现在异步函数中
2、await promise :await 后面只能写 promise 对象,写其他类型的 API 是不可以的
3、await 关键字可以暂停异步函数向下执行,直到 promise 对象返回结果

例子:通过 await 关键字,改造依次读取三个文件的例子

fs.readFile() 方法是通过返回值的方式,来获取文件的读取结果,也就是说它不返回 promise 对象。node 提供了一个 promisify 方法,这个方法存储在 util 模块中,然后用这个 promisify 方法,对 readFile 进行包装,让它返回 promise 对象。

新建 asyncFunctionReadFile.js 文件:

const fs = require('fs');
// promisify 方法是用来改造现有异步函数 API,让其返回 promise 对象,从而支持异步函数语法
const promisify = require('util').promisify;
// 调用 promisify 方法改造现有异步 API,让其返回 promise 对象
const readFile = promisify(fs.readFile);

async function run () {
  let r1 = await readFile('./1.txt', 'utf8')
  let r2 = await readFile('./2.txt', 'utf8')
  let r3 = await readFile('./3.txt', 'utf8')
  console.log(r1);
  console.log(r2);
  console.log(r3);
}
run();

回到命令工具中,输入:

node asyncFunctionReadFile.js

执行结果是:

 

posted on 2020-07-15 09:57  JoeYoung  阅读(1931)  评论(0编辑  收藏  举报