Skip to content

Latest commit

 

History

History
317 lines (281 loc) · 9.16 KB

第6章:前后端通信.md

File metadata and controls

317 lines (281 loc) · 9.16 KB

第6章:前后端通信

回顾我们的TodoApp,我们在第二、三章完成的App分明是点击后添加立刻出现了响应,但是在第四章时却要进行跳转,数据还没办法展示在主页的列表里,这简直糟透了,我们现在处理这个问题。


Ajax

以前,浏览器发送请求到后端只能通过两种方式:访问新的页面和填写表单。

表单填写完成之后,用户点击发送,浏览器会把数据发送到后端,后端处理过后会发送一个新的页面到前端,反映在用户的电脑上,就是页面发生了跳转。

这一行为导致的最严重的结果就是:如果填写完的表单出现了某种错误,那么只有提交过后才能知道出现了 错误。而且这种行为也极其影响用户体验,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()

Fetch

通过上文可以看出,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)
      })
    })
  })
})

这个让人头痛的怪物就是回调地狱,所以后来出现了很多种异步方案,让异步操作更容易编写,易于维护,如果有兴趣可以阅读我的一篇博客


RESTful风格的路由

当我们设计后端逻辑时,每个后端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