Promise - ES6新对象
Promise能够处理异步程序。
回调地狱
JS中或node中,都大量的使用了回调函数进行异步操作,而异步操作什么时候返回结果是不可控的,如果我们希望几个异步请求按照顺序来执行,那么就需要将这些异步操作嵌套起来,嵌套的层数特别多,就叫做回调地狱。
下面的案例就有回调地狱的意思:
案例:有 a.txt、b.txt、c.txt三个文件,使用fs模板按照顺序来读取里面的内容,代码:
// 将读取的a、b、c里面的内容,按照顺序输出
const fs = require('fs');// 读取a文件
fs.readFile('./a.txt', 'utf-8', (err, data) => {if (err) throw err;console.log(data.length);// 读取b文件fs.readFile('./b.txt', 'utf-8', (err, data) => {if (err) throw err;console.log(data);// 读取c文件fs.readFile('./c.txt', 'utf-8', (err, data) => {if (err) throw err;console.log(data);});});
});
案例中,只有三个文件,试想如果需要按照顺序读取的文件非常多,那么嵌套的代码将会多的可怕,这就是回调地狱的意思。
Promise简介
- Promise对象可以解决回调地狱的问题
- Promise 是异步编程的一种解决方案,比传统的解决方案(回调函数和事件)更合理和更强大
Promise可以理解为一个容器,里面可以编写异步程序的代码
- 从语法上说,Promise 是一个对象,使用的使用需要
new
Promise简单使用
Promise是“承诺”的意思,实例中,它里面的异步操作就相当于一个承诺,而承诺就会有两种结果,要么完成了承诺的内容,要么失败。
所以,使用Promise,分为两大部分,首先是有一个承诺(异步操作),然后再兑现结果。
第一部分:定义“承诺”
// 实例化一个Promise,表示定义一个容器,需要给它传递一个函数作为参数,而该函数又有两个形参,通常用resolve和reject来表示。该函数里面可以写异步请求的代码
// 换个角度,也可以理解为定下了一个承诺
let p = new Promise((resolve, reject) => {// 形参resolve,单词意思是 完成// 形参reject ,单词意思是 失败fs.readFile('./a.txt', 'utf-8', (err, data) => {if (err) {// 失败,就告诉别人,承诺失败了reject(err);} else {// 成功,就告诉别人,承诺实现了resolve(data.length);} });
});
第二部分:获取“承诺”的结果
// 通过调用 p 的then方法,可以获取到上述 “承诺” 的结果
// then方法有两个函数类型的参数,参数1表示承诺成功时调用的函数,参数2可选,表示承诺失败时执行的函数
p.then((data) => {},(err) => {}
);
完整的代码:
const fs = require('fs');
// promise 承诺// 使用Promise分为两大部分// 1. 定义一个承诺
let p = new Promise((resolve, reject) => {// resolve -- 解决,完成了; 是一个函数// reject -- 拒绝,失败了; 是一个函数// 异步操作的代码,它就是一个承诺fs.readFile('./a.txt', 'utf-8', (err, data) => {if (err) {reject(err);} else {resolve(data.length);}});
});// 2. 兑现承诺
// p.then(
// (data) => {}, // 函数类似的参数,用于获取承诺成功后的数据
// (err) => {} // 函数类型的参数,用于或承诺失败后的错误信息
// );
p.then((data) => {console.log(data);},(err) => {console.log(err);}
);
then方法的链式调用
-
前一个then里面返回的字符串,会被下一个then方法接收到。但是没有意义;
-
前一个then里面返回的Promise对象,并且调用resolve的时候传递了数据,数据会被下一个then接收到
-
前一个then里面如果没有调用resolve,则后续的then不会接收到任何值
const fs = require('fs'); // promise 承诺new Promise((resolve, reject) => {fs.readFile('./a.txt', 'utf-8', (err, data) => {err ? reject(err) : resolve(data.length);}); }) .then((a) => {console.log(a);return new Promise((resolve, reject) => {fs.readFile('./a.txt', 'utf-8', (err, data) => {err ? reject(err) : resolve(data.length);});}); }) .then((b) => {console.log(b);return new Promise((resolve, reject) => {fs.readFile('./a.txt', 'utf-8', (err, data) => {err ? reject(err) : resolve(data.length);});}); }) .then((c) => {console.log(c) }) .catch((err) => {console.log(err); });
catch 方法可以统一获取错误信息
封装按顺序异步读取文件的函数
function myReadFile(path) {return new Promise((resolve, reject) => {fs.readFile(path, 'utf-8', (err, data) => {err ? reject(err) : resolve(data.length);})});
}myReadFile('./a.txt')
.then((a) => {console.log(a);return myReadFile('./b.txt');
})
.then((b) => {console.log(b);return myReadFile('./c.txt');
})
.then((c) => {console.log(c)
})
.catch((err) => {console.log(err);
});
async 和 await 修饰符
ES6 — ES2015
async 和 await 是 ES2017 中提出来的。
异步操作是 JavaScript 编程的麻烦事,麻烦到一直有人提出各种各样的方案,试图解决这个问题。
从最早的回调函数,到 Promise 对象,再到 Generator 函数,每次都有所改进,但又让人觉得不彻底。它们都有额外的复杂性,都需要理解抽象的底层运行机制。
异步I/O不就是读取一个文件吗,干嘛要搞得这么复杂?异步编程的最高境界,就是根本不用关心它是不是异步。
async 函数就是隧道尽头的亮光,很多人认为它是异步操作的终极解决方案。
ES2017提供了async和await关键字。await和async关键词能够将异步请求的结果以返回值的方式返回给我们。
- async 用于修饰一个 function
- async 修饰的函数,表示该函数里面有异步操作(Promise的调用)
- await和async需要配合使用,没有async修饰的函数中使用await是没有意义的,会报错
- await需要定义在async函数内部,await后面跟的一般都是一个函数(函数里面包含有Promise)的调用
- await修饰的异步操作,可以使用返回值的方式去接收异步操作的结果
- 如果有哪一个await操作出错了,会中断async函数的执行
总结来说:async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。
const fs = require('fs');
// 将异步读取文件的代码封装
function myReadFile (path) {return new Promise((resolve, reject) => {fs.readFile(path, 'utf-8', (err, data) => {err ? reject(err) : resolve(data.length);});})
}async function abc () {let a = await myReadFile('./a.txt');let b = await myReadFile('./b.txt');let c = await myReadFile('./c.txt');console.log(b);console.log(a);console.log(c);
}abc();
路由
什么是路由
广义上来讲,路由就是映射关系。
程序中的路径也是映射关系:
Express 中的路由
在 Express 中,路由指的是客户端的请求与服务器处理函数之间的映射关系。
Express 中的路由分 3 部分组成,分别是请求的类型、请求的 URL 地址、处理函数,格式如下
// 路径 ,就是我们之前说的接口的处理程序
app.get('/api/getbooks', (req, res) => {});app.post('/api/addbook', (req, res) => {});
每当一个请求到达服务器之后,需要先经过路由的匹配,只有匹配成功之后,才会调用对应的处理函数。
在匹配时,会按照路由的顺序进行匹配,如果请求类型和请求的 URL 同时匹配成功,则 Express 会将这次请求,转 交给对应的 function 函数进行处理。
模块化路由
为了方便对路由进行模块化的管理,Express 不建议将路由直接挂载到 app 上,而是推荐将路由抽离为单独的模块。 将路由抽离为单独模块的步骤如下:
-
创建路由模块对应的 .js 文件
- 创建router/login.js 存放 登录、注册、验证码三个路由
- 创建router/heroes.js 存放 和英雄相关的所有路由
-
调用 express.Router() 函数创建路由对象
const express = require('express'); const router = express.Router();
-
向路由对象上挂载具体的路由
// 把app换成router,比如 router.get('/xxx/xxx', (req, res) => {}); router.post('/xxx/xxx', (req, res) => {});
-
使用 module.exports 向外共享路由对象
module.exports = router;
-
使用 app.use() 函数注册路由模块 – app.js
// app.js 中,将路由导入,注册成中间件 const login = require('./router/logon.js'); app.use(login)// app.use(require('./router/heroes.js')); app.use( require(path.join(__dirname, 'router', 'heores.js')) );
为路由模块添加前缀
我们可以省略路由模块中的 /api
前缀,而是在注册中间件的时候,统一设置。
app.use('/api', router);
具体:
app.js中:
// 导入路由模块,并注册成中间件
app.use('/api', require(path.join(__dirname, 'router', 'login.js')) );
app.use('/my', require(path.join(__dirname, 'router', 'heroes.js')) );
路由文件中,把前缀 /api 和 /my 去掉
使用路由模块的好处
- 分模块管理路径,提高了代码的可读性
- 可维护性更强
- 减少路由的匹配次数
- 权限管理更方便
- etc…