回顾我们的TodoApp,我们在第二、三章完成的App分明是点击后添加立刻出现了响应,但是在第四章时却要进行跳转,数据还没办法展示在主页的列表里,这简直糟透了,我们现在处理这个问题。
以前,浏览器发送请求到后端只能通过两种方式:访问新的页面和填写表单。
表单填写完成之后,用户点击发送,浏览器会把数据发送到后端,后端处理过后会发送一个新的页面到前端,反映在用户的电脑上,就是页面发生了跳转。
这一行为导致的最严重的结果就是:如果填写完的表单出现了某种错误,那么只有提交过后才能知道出现了 错误。而且这种行为也极其影响用户体验,App的流畅度被大大的降低。
于是Ajax应运而生,使浏览器有了发送异步请求的能力。
Ajax全称为"Asynchronous JavaScript and XML"(异步的JavaScript与XML技术),它允许浏览器在不刷新的情况下发送请求到后端,然后使用JavaScript来处理得到的数据或进行之后的操作,可以说Ajax给了Web App与原生的桌面应用和移动端应用竞争的资本。
JavaScript创建一个Ajax请求的流程如下:
//创建一个xhr对象
const xhr = new XMLHttpRequest()
//选择响应数据的类型
xhr.type = type
//监听状态变化
xhr.onreadystatechange = () => {
if(xhr.readyState === 4 &&
(xhr.status === 200 || xhr.status === 201
|| xhr.status === 202 || xhr.status === 204)) {
//如果请求成功就处理响应
handle(xhr.response)
}
}
//发送请求,两个参数分别代表请求的方法和url
xhr.opne(method, url)
//设置请求头
xhr.setRequestHeader()
//发送请求,参数可以是要发送的数据
xhr.send()
通过上文可以看出,XHR其实封装的不是很好,配置和调用都有些乱。
浏览器规范也意识到了这个问题,因此推出了Fetch API来帮助我们进行异步的AJAX请求。
来看一个典型的使用Fetch的请求
fetch(url).then((response) => {
if(response.ok) {
response.json((data) => {
console.info(data)
})
} else {
console.error(`Error with status code ${res.status}`)
}
}, (e) => {
console.error(`Fetch failed: ${e}`)
})
是不是接口友好了很多?
Fetch目前在Chrome、Firefox和Opera上获得了支持,对于safari和Edge,可以使用Github的polyfill来让fetch获得支持。
更多关于Fetch的详细用法,可以阅读这篇文章
在应用Ajax之前,我们先要弄明白究竟什么是异步操作。
我们先来看一个简单的例子
function callback(data) {
console.log(data)
}
setTimeout(() => {
const output = 'response'
callback(output)
}, 2000)
console.log('pipe')
当代码从上到下执行到setTimeout
语句的时候,由于这一语句要将里面的操作延迟2000ms执行,但是代码又不能因此暂停执行,这样就形成了一个异步操作。setTimeout
中的内容会放在浏览器的相关模块中。等到达到执行的条件(例如这里是时间到达规定时间),操作就被推入任务队列,当主线程空闲的时候,就会取出任务队列中的任务执行。
因此这段代码的打印结果是:
'pipe'
'response'
我们之前对于mongoose增删改查数据的操作,全都是通过异步操作来进行的。
异步操作的主要方式是通过回调函数来进行的,来看一个简单的例子
//一个能创建异步操作的函数
function delay(message, callback) {
setTimeout(() => {
callback(message)
}, 20)
}
function callback(message) {
console.log(message)
}
delay('hi', callback)
我们声明的函数callback就是delay的回调函数,用来处理异步操作得到的数据。
一点延伸 如果多个异步操作要按顺序连续进行,使用回调函数就会变成这样:
delay('hello', (message) => {
console.log(message)
delay('world', (message) => {
console.log(message)
delay('good', (message) => {
console.log(message)
delay('day', (message) => {
console.log(message)
})
})
})
})
这个让人头痛的怪物就是回调地狱,所以后来出现了很多种异步方案,让异步操作更容易编写,易于维护,如果有兴趣可以阅读我的一篇博客
当我们设计后端逻辑时,每个后端API所对应的含义需要符合一个统一的规范,否则就会出现后端API混乱的情况。
例如当我们查找一个todo时,请求的是example.com/todo?id=123
,但是请求评论时,却请求example.com/comment/123
,虽然不会导致报错,但是会提高开发和维护的成本。
RESTful API是一套比较成熟的API设计理论。
关于RESTful的详细规范,阮一峰老师已经做过总结,同学们可以点击这里进行阅读,现在我们就根据我们的Todo App来设计需要的API。
我们希望我们的Todo App有如下功能:
- 注册,登陆用户
- 添加Todo
- 读取Todo
- 修改Todo的内容和状态
因此我们对API作出如下设计:
GET /todo 获取指定用户所有todo
GET /todo/:todoId 获取指定用户指定todo
POST /todo 添加todo
PUT /todo/:todoId 更新指定todo
DELETE /todo/:todoId 删除指定todo
下面让我们重新编写todo应用,先根据上一章编写好的三个模块进行后端重构。 新建一个文件夹,命名为api,以后后端程序将存放在这里。
将之前写好的core
模块和static
模块放在api/lib
文件夹中以供调用。
将model
模块放在api/model
文件夹以供调用。
创建model/modelList.js
文件,存放我们的数据库结构以及创建函数:
const list = [
{
name: 'user',
struct: {
username: {type: String, unique: true},
pwd: {type: String}
}
},
{
name: 'todo',
struct: {
todo: {type: String},
finish: {type: Boolean, default: false},
username: {type: String}
}
}
]
module.exports = list
创建api/server.js
文件,打开进行编辑:
const app = require('./lib/core')()
const Model = require('./model/model')
const modelList = require('./model/modelList')
//服务端配置
const SERVER_CONFIG = {
host: process.env.IP || 'localhost',
port: process.env.PORT || 3000
}
//加载数据库
const model = new Model()
model.setConfig({
user: 'user',
pwd: '0000',
db: 'test'
})
model.loadModelFromList(modelList)
const db = model.init()
//页面
app.get('/', (req, res) => {
//TODO: 主页
})
app.get('/login', (req, res) => {
//TODO: 登陆页面
})
app.post('/login', (req, res) => {
//TODO: 检查登陆
})
app.get('/join', (req, res) => {
//TODO: 注册页面
})
app.post('/join', (req, res) => {
//TODO: 检查注册
})
//数据接口
app.get('/todo', (req, res) => {
//TODO: 发送Todo列表
})
app.get('/todo/:id', (req, res, params) => {
//TODO: 发送Todo
})
app.post('/todo', (req, res) => {
//TODO: 创建Todo
})
app.put('/todo/:id', (req, res, params) => {
//TODO: 更新Todo
})
app.delete('/todo/:id', (req, res, params) => {
//TODO: 删除Todo
})
app.listen(SERVER_CONFIG.port, SERVER_CONFIG.host)
我们定义好了API,现在处理后端逻辑,创建router文件夹,与api文件夹平级。再在router文件夹中创建service文件夹。
创建router/service/todo.js
,进行编辑:
const querystring = require('querystring')
// GET /todo
function listTodo(req, res, db) {
db.find({
username: //USER
}, (docs) => {
//TODO: 处理得到的Todo
})
}
// GET /todo/:id
function getTodo(req, res, db) {
db.findOne({
_id: req.params[':id']
}, (doc) => {
//TODO: 处理得到的Todo
}, () => {
//TODO: 未找到Todo
})
}
// POST /todo
function addTodo(req, res, db) {
req.on('data', (chunk) => {
const data = querystring.parse(chunk.toString())
db.add({
username: //USER
todo: todo
}, (doc) => {
//TODO: 成功添加Todo
}, () => {
//TODO: 添加Todo失败
})
})
}
// PUT /todo/:id
function updateTodo(req, res, db) {
req.on('data', (chunk) => {
const data = querystring.parse(chunk.toString())
db.update({
_id: req.params[':id']
}, data, (doc) => {
//TODO: 成功更新Todo
}, () => {
//TODO: 更新Todo失败
})
})
}
// DELETE /todo/:id
function deleteTodo(req, res, db) {
db.remove({
_id: req.params[':id']
}, () => {
//TODO: 成功删除了Todo
}, () => {
//TODO: 删除Todo失败
})
}
于是,我们现在的项目结构为:
|--api
| |--server.js
| |--lib
| | |--core.js
| | |--static.js
| |--model
| | |--model.js
| | |--modelList.js
| |--router
| | |--router.js
| | |--service
| | | |--todo.js
|--test
| |--core.test.js
| |--model.test.js