ES6
ES6模块化
模块化的好处
回顾nodejs中模块化的使用
node.js 遵循了 CommonJS 的模块化规范。其中:
- 导入其它模块使用
require()方法 - 模块对外共享成员使用
module.exports对象
ES6模块化中定义
- 每个
js文件都是一个独立的模块 - 导入其它模块成员使用
import关键字 - 向外共享模块成员使用
export关键字
使用方式
node.js 中默认仅支持 CommonJS 模块化规范,若想基于 node.js 体验与学习 ES6 的模块化语法,可以按照 如下两个步骤进行配置:
-
确保安装了
v13.0.0或更高版本的node.js -
在
package.json的根节点中添加"type": "module"节点
ES6模块语法
ES6 的模块化主要包含如下 3 种用法:
- 默认导出与默认导入
- 按需导出与按需导入
- 直接导入并执行模块中的代码
默认导出与默认导入
默认导出的语法: export default 默认导出的成员
const a = 10
const b = 20
const fn = () => {
console.log('这是一个函数')
}
// 默认导出
// export default a // 导出一个值
export default {
a,
b,
fn
}
默认导入的语法: import 接收名称 from '模块路径'
import result from './默认导出.js'
console.log(result)
注意:
每个模块中,只允许使用唯一的一次
export default这里的路径必须补全后缀名,不然会报错。
按需导出与按需导入
按需导出的语法: export const 变量 = 值
export const dao = 'dao'
按需导入的语法: import { 按需导入的名称 } from '模块标识符'
import { name as dao } from "./common/02-按需导出.js";
let love = '刀'
console.log(love, name);
注意:
- 每个模块中可以有多次按需导出
- 按需导入的成员名称必须和按需导出的名称保持一致
- 按需导入时,如果目前模块的变量与导入的变量重名,可以使用
as关键字进行重命名- 按需导入可以和默认导入一起使用
export const that = 'dao' let miss = 'ze' let think = 'tie' export default { love, miss, think, look: 'zhan' }import obj, { that } from "./common/03-按需导出与默认导出.js"; console.log(obj, that); // obj拿到全部的值,that拿到按需导出的值
直接导入模块(无导出)
如果只想单纯地执行某个模块中的代码,并不需要得到模块中向外共享的成员。
//xxx.js
for (let i = 0; i < 10; i++) {
console.log(i)
}
// 导入该模块
import './xxx.js'
Promise
Promise能够处理异步程序。
Promise简介
- Promise对象可以解决回调地狱的问题
- Promise 是异步编程的一种解决方案,比传统的解决方案(回调函数和事件)更合理和更强大
Promise可以理解为一个容器,里面可以编写异步程序的代码`- 从语法上说,
Promise是一个对象,使用的使用需要new
Promise简单使用
- 定义
Promiselet p = new Promise((resolve, reject) => { // 形参resolve,单词意思是 完成 // 形参reject ,单词意思是 失败 fs.readFile('./a.txt', 'utf-8', (err, data) => { if (err) { // 失败,就告诉别人,承诺失败了 reject(err); } else { // 成功,就告诉别人,承诺实现了 resolve(data.length); } }); }); - 获取结果
// promise的成功回调函数触发then,失败回调函数触发catch p.then(res => { console.log(res); }).catch(res => { console.log(res); })
三种状态
- 最初状态:
pending,等待中,此时promise的结果为 undefined; - 当
resolve(value)调用时,达到最终状态之一:fulfilled,(成功的)完成,此时可以获取结果value - 当 reject(error) 调用时,达到最终状态之一:rejected,失败,此时可以获取错误信息
error
当达到最终的
fulfilled或rejected时,promise的状态就不会再改变了。
特点
当调用 resolve 的时候, Promise 将到达最终的状态。 达到最终状态之后, Promise 的状态就不会再改变了。
多次调用 resolve 函数,只有第一次有效,其他的调用都无效。
then方法的链式调用
- 前一个
then里面返回的字符串,会被下一个then方法接收到。但是没有意义; - 前一个
then里面返回的Promise对象,并且调用resolve的时候传递了数据,数据会被下一个then接收到 - 前一个
then里面如果没有调用resolve,则后续的then不会接收到任何值,后续的代码不会执行。
const p = new Promise(function(resolve, reject) {
resolve()
})
p.then(res => {
console.log(11);
return 22
}).then(res => {
console.log(res);
return 33
}).then(res => {
console.log(res);
return 44
}).then(res => {
console.log(res);
return 55
}).then(res => {
console.log(res);
})
结果输出:11 22 33 44 55
错误处理
对于 promise 而言,错误处理的方式为添加 .catch() 方式调用 reject 的方法。
当 then 链式调用中某个 then 触发了 reject ,后续的 then 不会继续执行。
function sleep(num) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (num == 2) {
reject('出错啦')
} else {
resolve(num)
}
}, 1000);
})
}
sleep(1).then(res => {
console.log(res);
return sleep(2)
}).then(res => {
console.log(res);
return sleep(3)
}).then(res => {
console.log(res);
return sleep(4)
}).then(res => {
console.log(res);
}).catch(res => {
console.log(res);
})
结果如下所示:
作业小练
promise 封装 fs 读文件函数
需求:封装一个函数,可以执行
fs读文件的功能,以then的形式返回结果。
思路
- 引入
fs的模块,封装一个函数,形参为文件路径。import fs from "fs"; const readFile = (url) => {} - 返回一个
promise的对象,去调用fs的读文件内置对象。const readFile = (url, code) => { return new Promise((resolve, reject) => { }) } - 失败调用
reject方法;调用成功后触发resolve方法。const readFile = (url, code) => { return new Promise((resolve, reject) => { fs.readFile(url, code, (err, data) => { if (err) { reject(err) } else { resolve(data) } }) }) }
结果只需要调用函数,传递路径,结果通过 then 获取即为成功调用。
readFile('./1.json', 'utf-8').then(res => {
console.log(res);
})
promise 封装 axios 发请求函数
需求:不引用
axios官方包,通过自己结合promise的方式封装一个axios函数,能够实现axios.get()获取到真正的数据。
思路
- 单看
axios.get()这段代码,根据所学知识,我们能判断出只有对象才有obj.key的属性,因此,要设置axios为一个对象。const axios = {} - 该对象中有一个
get()函数,传递两个形参,一个是url请求地址,一个是params传递的数据。const axios = { get(url,params){} } axios.get()是一个异步操作,结果通过.then(res=>{console.log(res)}来返回,而promise方法刚好用于处理异步操作,在执行成功后也是通过触发resolve()方法传递参数给.then()方法返回结果,因此返回一个promise对象是最好的选择。const axios = { get(url,params){ return new Promise((resolve,reject)=>{}) } }- 后续操作就很简单了,用
ajax的知识点即可,创建一个XMLHttpRequest()对象,设置请求方式和地址xhr.open(),设置请求发送xhr.send(),设置请求成功返回的回调函数xhr.onload=function(){}即可。const axios = { get(url, params) { return new Promise((resolve, reject) => { // 创建xhr异步对象 const xhr = new XMLHttpRequest(); // xhr设置请求方式请求地址和发送请求 if (params) { xhr.open('get', url + `?id=${params.id}`) } else { xhr.open('get', url) } xhr.send(); // xhr执行完毕触发其回调函数 xhr.addEventListener('load', function() { resolve(JSON.parse(xhr.responseText)) }) }) } }
使用的时候 axios.get() 是一个 promise 对象,成功后数据通过 then() 输出。
axios.get('http://www.itcbc.com:3006/api/getbooks', {
id: 15949
}).then(res => {
console.log(res);
})
宏任务和微任务、事件循环
宏任务
可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。
| 任务(代码) | 宏任务 | 环境 |
|---|---|---|
| script | 宏任务 | 浏览器 |
| 事件 | 宏任务 | 浏览器 |
| 网络请求(Ajax) | 宏任务 | 浏览器 |
| setTimeout() 定时器 | 宏任务 | 浏览器 / Node |
| fs.readFile() 读取文件 | 宏任务 | Node |
微任务
微任务(microtask)是宏任务中的一个部分,它的执行时机是在同步代码执行之后,下一个宏任务执行之前。
微任务包含:
Promise.then
process.nextTick(Node.js 环境)
运行机制
- 从上往下读取代码,把所有异步任务放到任务队列中,解析所有同步任务。
- 同步任务解读完毕,开始解读异步任务。
- 从上往下执行,先执行异步任务里的同步任务,完毕后执行异步任务中的微任务,最后执行宏任务。
async 与 await
作用
将异步请求的结果以返回值的方式返回给我们,我们不用关心它是不是异步。
-
async 用于修饰一个 function
- async 修饰的函数,总是返回一个 Promise 对象
- 函数内的返回值,将自动包装在 resolved 的 promise 中
async function f1() { const res = 33 console.log(res); // 33 } console.log(f1()); // Promise {<fulfilled>: undefined} -
await 只能出现在 async 函数内
- await 让 JS 引擎等待直到promise完成并返回结果
- 语法:let value = await promise对象; // 要先等待promise对象执行完毕,才能得到结果
- 由于await需要等待promise执行完毕,所以await会暂停函数的执行,但不会影响其他同步任务
async function f1() { console.log(1); await f2(); await f3() console.log(3); } f1() // 结果输出 1 1000 1500 3 function f2() { return new Promise(resolve => { setTimeout(() => { console.log(1000); resolve() }, 1000) }) } function f3() { return new Promise(resolve => { setTimeout(() => { console.log(1500); resolve(1500) }, 1500) }) }注意:
使用await 时会等到 promise 对象执行完毕,状态从等待切换为成功或失败时才会往下执行,否则一直等待。
async function f1() { console.log(1); await f2(); await f3() console.log(3); } f1() // 1 1000 function f2() { return new Promise(resolve => { setTimeout(() => { console.log(1000); }, 1000) }) } function f3() { return new Promise(resolve => { setTimeout(() => { console.log(1500); }, 1500) }) }结果只输出 1 1000,由于 f2 没有调用 resolve 方法,切换不了状态,则一直等待。
错误处理
对于promise而言,错误处理的方式为添加.catch()方式调用reject的方法
当then链式调用中某个then触发了reject,后续的then不会继续执行
function sleep(num) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (num == 2) {
reject('出错啦')
} else {
resolve(num)
}
}, 1000);
})
}
/* 对于async和await而言,错误处理为try catch
正常的代码写在try内,如果报错,则执行catch内的代码
同 then链式调用 一样,一旦出现问题后续不再执行 */
async function init() {
try {
const p1 = await sleep(1)
console.log(p1);
const p2 = await sleep(2)
console.log(p2);
const p3 = await sleep(3)
console.log(p3);
const p4 = await sleep(4)
console.log(p4);
} catch (error) {
console.log(error);
}
}
init()
自制接口并实现数据渲染
前置知识
express:一个基于nodejs的Web开发框架- 引入
import express from "express"; - 设置对象
const app = express()
- 引入
- 获取参数:
get、delete:req.querypost、put、patch:req.body,需要先提前设置好 `app.use(express.urlencoded())
- 回调函数的参数
- req:全称 requset,包含请求信息,如获取参数
- res:全程 respone,包含响应信息,通过 res.send() 发送响应数据
- 第三方读写文件
npm 下载第三方读写文件的包
npm i fs-then,这个包支持 promise,极大方便我们的效率。
搭建服务器
把端口号和每个请求方式的 url 地址搭建好。
import express from "express";
import fs from "fs-then";
const app = express()
app.get('/api/getbooks', async(req, res) => {
})
app.post('/api/addbook', async(req, res) => {
})
app.delete('/api/delbook', async(req, res) => {
})
app.put('/api/updatebook', async(req, res) => {
})
app.listen(3000, () => {
console.log('3000 open success');
})
创建接口
get 接口
一般而言,get 接口用于获取数据,比如全部图书列表等,因此非必须返回参数,只需要读取 json 数据文件并返回响应状态、响应信息和请求好的数据即可。
app.get('/api/getbooks', async(req, res) => {
let data = await fs.readFile('./1.json', 'utf-8')
res.send({
status: 0,
message: '成功~',
data: JSON.parse(data)
})
})
post 接口
由 post 接口获取到的数据通过 req.body 来获得,需要先提前设置好 app.use(express.urlencoded()) ,否则无法获取到数据。
由于要新增数据,因此要先获取到原数据文件内的数据,运用到的知识点为 fs.readFile ,用一个变量 data 来存储。由于是异步操作,因此用到 async 和 await ,拿到其回调函数的返回值,避免回调地狱的产生。
读取到文件后去获取用户输入的信息,拿到的是一个对象形式,并根据数组最后一个元素的id号自增1,赋给获取到的元素。为对象数据类型添加新属性的方法为 obj.key=value ,最后追加到数组 data 内,数组追加元素的方法为 arr.push() 。
追加完数据后还不能结束,现在只是改变了变量内的值,需要我们把追加好数据的数组重新写回文件中。写文件的操作为 fs.writeFile 。
app.post('/api/addbook', async(req, res) => {
let data = JSON.parse(await fs.readFile('./1.json', 'utf-8'))
let row = req.body;
row.id = data[data.length - 1].id + 1
data.push(row)
await fs.writeFile('./1.json', JSON.stringify(data))
res.send({
status: 0,
message: '添加成功'
})
})
delete 接口
delete 方法一般用于删除数据,数据一般由 params 或 url 拼接的方式传递,因此用 req.query 的方式获取到数据。
首先先读取数据文件的内容,然后获取到传递的数据,打印后发现是对象的形式,由于我们需要的是 id ,因此需要 req.query.id 。
获取到 id 值后对数据数组进行遍历,对数组内某个值做删除处理我们之前做过很多次了,使用 filter 方法是较合适的选择,返回所有 id 值不等于获取到的 id 即可,赋值给变量 list 。
最后把新数组 list 写回文件内即可,还可以再做点细节处理。
app.delete('/api/delbook', async(req, res) => {
let data = JSON.parse(await fs.readFile('./1.json', 'utf-8'))
if (data.length != 0) {
let id = req.query.id
let list = data.filter(item => {
return item.id != id
})
fs.writeFile('./1.json', JSON.stringify(list))
res.send({
status: 0,
message: '删除图书成功'
})
} else {
res.send({
status: 1,
message: '没有数据'
})
}
})
put 接口
put 接口用于数据修改,需要拿到 id 号和 用户想要修改的值 name 。由于传递的方式是放在对象 data 内,因此用 req.body 来获取传递的参数。
读取数据文件,分别拿到 id 和 name ,对数据遍历,把数据内的 name 属性的值改为获取到的值,再写文件即可。
app.put('/api/updatebook', async(req, res) => {
let data = JSON.parse(await fs.readFile('./1.json', 'utf-8'))
let id = req.body.id
let newname = req.body.name
data.forEach(item => {
if (item.id == id) {
item.name = newname
}
});
fs.writeFile('./1.json', JSON.stringify(data))
res.send({
status: 0,
message: '修改成功'
})
})
前端数据渲染
页面初始化
首先先初始化页面,设置一个基础的表格并引入 axios 包,后续的数据都拼接在 tbody 内。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>前端页面</title>
</head>
<body>
<div class="main">
<input type="button" class="btnadd" value="添加">
<input type="text" id="add">
<table>
<thead>
<tr>
<th>ID</th>
<th>名字</th>
<th>操作</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<script src="./js/axios.js"></script>
</body>
</html>
获取数据做页面渲染
调用 get 接口发送 get 请求获取数据,并对数据遍历拼接到 tbody 中。由于发送 axios 请求是异步操作,因此用到了 async 和 await 的知识点,等待异步处理结束。
async function getbook() {
let data = await axios('http://127.0.0.1:3000/api/getbooks')
console.log(data);
let res = data.data.data
let html = ''
res.forEach(item => {
html += `
<tr>
<td>${item.id}</td>
<td>${item.name}</td>
<td><button class="btndel" data-id="${item.id}">删除</button></td>
<td><button class="btnedit" data-id="${item.id}"><a href="./11-图书编辑页面.html?id=${item.id}">编辑</a></button></td>
</tr>
`
});
document.querySelector('tbody').innerHTML = html
delbook()
}
getbook()
删除图书
删除图书需要传递 id ,根据前面的知识我们很轻松就能想出方法:在数据渲染的时候为删除按钮绑定自定义属性id即可,点击按钮后获取自定义属性。
由于删除按钮是一个动态元素,一般用事件冒泡来处理,但这次我们换一个方法。声明一个删除函数,获取所有删除按钮,循环遍历添加点击事件,调用函数方法的代码写在拼接成功的代码后面,这样就能确保渲染完后再获取到所有按钮。
async function getbook() {
document.querySelector('tbody').innerHTML = html
delbook()
}
getbook()
把拿到的自定义属性 id 传递给后台, delete 传递数据的方式是 params ,异步处理结束后重新调用一次 getbook() 方法局部刷新数据。
function delbook() {
let btndel = document.querySelectorAll('.btndel')
btndel.forEach((ele, index) => {
ele.addEventListener('click', async function() {
let id = this.dataset.id
let res = await axios.delete('http://127.0.0.1:3000/api/delbook', {
params: {
id
}
})
getbook()
})
})
}
新增图书
新增数据调用 post 接口来发送请求,对象的方式传递数据。
function addbook() {
btnadd.addEventListener('click', async function() {
let name = add.value
let data = await axios.post('http://127.0.0.1:3000/api/addbook', {
name
})
console.log(data);
getbook()
})
}
addbook()
但是我们点击后发现,数据发送成功,但是后台接收不到,显示为空数据对象{} ,返回的是 undefined ,用 APIfox 软件测试发现能够发送数据,点开调试工具查看发送的请求,查看响应标头,如下图所示。
联想之前要获取到 urlencoded 类型数据时,我们设置了一条 app.use(express.urlencoded()); 代码,因此我们可以合理怀疑,我们还需要再加一条类似的代码来识别 json 格式的数据。最后经过百度查询资料,我们确定了要下载 body-parser 第三方包,语法格式如下。
import body_parser from "body-parser";
app.use(body_parser.json())
这样就能接收到用户的数据了。
查询单个图书
想要实现编辑图书,首先需要拿到所有被编辑的图书数据,即获取到单个图书信息,因此需要后台写一个新的接口。
获取数据,一般为 get 请求,params 传递 id 参数,因此用 req.query.id 拿到 id ,读取文件,遍历数组对象,找到 id 号相等的数据,如果没有值数组长度为0的时候返回错误信息,否则输出返回数据。
app.get('/api/getOne', async(req, res) => {
let data = JSON.parse(await fs.readFile('./1.json', 'utf-8'))
let id = req.query.id
let list = data.filter(item => item.id == id)
if (list.length != 0) {
res.send({
status: 0,
message: '成功',
data: list
})
} else {
res.send({
status: 1,
message: '失败'
})
}
})
点击编辑超链接后跳转到编辑页面,此时我们需要在新的页面拿到 id 参数,有两个解决方案:
- 利用本地存储先存储再获取
- 用
url拼接数据的方式在跳转的时候拼接。
我们使用第二种方式,在数据渲染的时候就拼接 id 数据。
<button class="btnedit" data-id="${item.id}"><a href="./11-图书编辑页面.html?id=${item.id}">编辑</a></button>
在新的页面获取数据。
let id = location.search.split('=')[1]
跳转页面后立刻发送查询单个数据的请求,把结果渲染到页面的表单中。
let ipt = document.querySelector('input')
async function editbook() {
let res = await axios('http://127.0.0.1:3000/api/getOne', {
params: {
id
}
})
console.log(res);
ipt.value = res.data.data[0].name
}
editbook()
修改完点击按钮获取最新的表单内容,发送修改请求,传递 id 和 name 即可,最后返回原页面。
btn.addEventListener('click', async function() {
let name = ipt.value
let res = await axios.put('http://127.0.0.1:3000/api/updatebook', {
id,
name
})
location.href = './10-显示图书.html'
})
出现的小问题
自动刷新与跨域
在处理业务实现功能时,每当数据被修改删除添加,页面总会自动刷新,这是因为我们使用了 VS Code 的一个插件,该插件会监听整个文件夹的全部文件,只要有一个文件内容发生改变,则自动刷新一次页面。
因此为了达到局部刷新页面,我们要用最基本的页面打开方式,但是打开控制台发现报错,跨域错误。
同源的条件是:协议、端口号、域名/ip地址一致,只要有一个不符合要求即为跨域。直接打开html页面采用的是 file协议,与http协议不一致,因此跨域。
解决跨域的问题很简单,后端引入第三方包 cors ,设置 app.use(cors()) 即可解决。