开发博客项目之接口(3)

4-8 开发 博客详情路由

controller-blog.js定义一个getDetail()用于返回假数据:

1
2
3
4
5
6
7
8
9
10
const getDetail = () => {
//先返回假数据
return {
id: 1,
title: "标题A",
content: "内容A",
createTime: 1579421639661,
author: "hlz"
}
}

router-blog.js引入getDetail(),修改“获取博客详情”返回的内容:

1
2
3
4
5
6
//获取博客详情
if (method === "GET" && req.path === "/api/blog/detail") {
const id = req.query.id;
const data = getDetail(id);
return new SuccessModel(data);
}

将我们在发送get请求时的id获取并传入getDetail(),得到的对象data传入SucessModel()中去新建一个对象,该对象是格式化后的数据对象,在app.js中该对象会通过res.end()显示在页面上:
效果


nodejs读取文件的简单示例

读取a.json文件内容

新建文件夹promise-test,该文件夹下新建一个index.js用于读取文件。新建一个**files文件夹用于存放3个文件a.jsonb.jsonc.json**。

a.json

1
2
3
4
{
"next": "b.json",
"msg": "this is a"
}

index.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const fs = require("fs");// fs是node自带的模块,用于读取文件
const path = require("path");//path是node自带的模块,用于获取路径

//获取a.json文件的绝对路径fullFileName
//path的resolve()用于拼接路径,__dirname是node的全局变量,可直接使用,表示当前文件的目录
const fullFileName = path.resolve(__dirname, "files", "a.json")
//通过fs.readFile()读取绝对路径的文件
fs.readFile(fullFileName, (err, data) => {
//fs.readFile()读取文件是异步的
if (err) {
// 如果报错就抛出
console.log(err);
return;
}
// 没报错就将读取到的数据data转换为字符串打印(data默认二进制)
console.log(data.toString());
})

运行index.js可以打印出a.json的数据:
运行index.js可以打印出a.json的数据

通过回调函数的方式获取文件内容

修改index.js获取a.json文件的内容:

  • 创建getFileContent()
    • **回调函数(参数函数)callback**:获取文件内容,转换为JSON对象后通过参数传给回调函数callback
    • 参数fileName:需要获取内容的文件名a.json)。
  • 调用getFileContent()时只需要传入需要获取的文件名,(回调函数接受获取到的数据对象作为参数)即可在回调函数中对获取到的内容进行操作。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    const fs = require("fs");// fs是node自带的模块,用于读取文件
    const path = require("path");//path是node自带的模块,用于获取路径


    // callback方式获取文件内容
    function getFileContent(fileName, callback) {
    //path的resolve()用于拼接路径,__dirname是node的全局变量,可直接使用,表示当前文件的目录
    const fullFileName = path.resolve(__dirname, "files", fileName)
    //通过fs.readFile()读取绝对路径的文件
    fs.readFile(fullFileName, (err, data) => {
    //fs.readFile()读取文件是异步的
    if (err) {
    // 如果报错就抛出
    console.log(err);
    return;
    }
    // 读取到的数据data默认二进制
    // 没报错就将data转换为字符串再转换为JSON对象后传给回调函数callback
    callback(JSON.parse(data.toString()));
    })
    }

    // 测试
    getFileContent("a.json", aData => {
    console.log("a data", aData)
    })
    结果

依次获取文件内容(回调地狱)

b.json:

1
2
3
4
{
"next": "c.json",
"msg": "this is b"
}

c.json:

1
2
3
4
{
"next": null,
"msg": "this is c"
}

index.js:

1
2
3
4
5
6
7
8
9
10
// 依次获取三个文件内容,回调地狱
getFileContent("a.json", aData => {
console.log("a data", aData)
getFileContent(aData.next, bData => {
console.log(bData);
getFileContent(bData.next, cData => {
console.log(cData);
})
})
})

结果


使用Promise避免回调地狱

  • 使用**getFileContent()只需要传入文件名fileName**,(避免回调地狱,不需要使用回调函数)返回的是一个Promise对象
  • then()返回的Promise对象可以通过我们的设置返回新的Promise对象
  • then()中的参数函数(回调函数)获取到的参数是由调用它的Promise对象在决议为成功/失败时(即**resolve()/reject()中作为参数)返回的数据**。

关于Promise和resolve()、reject():

  • getFileContent()中我们返回的是一个Promise对象,这个对象中保存着一个异步操作的结果(成功/失败)。(异步操作放在Promise的参数函数中)
  • 当Promise的参数函数中保存的异步操作成功,则可以调用resolve()将获取到的数据通过参数传递给then()的第一个参数函数
  • 如果异步操作失败,则可以调用reject()将错误信息通过参数传递给then()的第二个参数函数/catch()的参数函数
  • 注意:调用resolve()/reject()都不会终止Promise的参数函数的执行(他们只改变Promise对象的状态并传递参数),所以为了防止resolve()或reject()后面的函数还继续执行,最好在resolve或reject前面加上return语句,这样函数就结束了,后面语句自然也就不会执行了(return resolve();)。

index.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const fs = require("fs");// fs是node自带的模块,用于读取文件
const path = require("path");//path是node自带的模块,用于获取路径

// 用Promise依次获取不同文件的内容
// 使用getFileContent()只需要传入文件名fileName,返回的是promise对象
function getFileContent(fileName) {
const promise = new Promise((resolve, reject) => {
//path的resolve()用于拼接路径,__dirname是node的全局变量,可直接使用,表示当前文件的目录
const fullFileName = path.resolve(__dirname, "files", fileName)
//通过fs.readFile()读取绝对路径的文件
fs.readFile(fullFileName, (err, data) => {
//fs.readFile()读取文件是异步的
if (err) {
// 如果报错就将错误信息传给reject()
reject(err);
return;
}
resolve(
// 读取到的数据data默认二进制
// 没报错就将data转换为字符串再转换为JSON对象后传给resolve()
JSON.parse(data.toString())
)
})
})
return promise;
}

// 测试,getFileContent()会返回一个Promise对象
// 成功获取的数据作为参数传入then()的参数函数中
getFileContent("a.json").then(aData => {
console.log("a data", aData);
// then()可以返回另一个Promise对象
return getFileContent(aData.next);
}).then(bData => {
console.log("b data", bData);
return getFileContent(bData.next);
}).then(cData => {
console.log("c data", cData);
})

补充:使用async await会更加简单,会在后面讲koa2框架时讲解。


4-9 处理post请求

之前我们处理的两个路由都是get请求的,现在我们来处理post请求的路由。

复习:post请求的简单示例

可参考笔记“开发博客项目之接口(1)”post请求的例子
注意:数据流获取数据的方式是异步的,所以我们可以使用Promise来获取数据

通过Promise来解析postData

  • 回到**blog-1文件夹中,在app.js中新建函数getPostData(),在该函数中通过Promise来解析postData**。
    • 其中我们没有用到reject()是因为在这里我们并不把 get请求 或者 请求类型为JSON以外的类型的数据(比如form-data类型)判定为错误的,只需要返回空数据就好。
  • app.js中的serverHandle()中,我们会处理不同的路由,在处理路由前就需要使用getPostData()解析PostData。解析成功以后的数据会作为参数传到then()中,此时我们需要将所有路由的处理都放在then()的回调函数中,让接下来在router中处理路由时都能拿到PostData。

app.js中添加函数getPostData()用于解析PostData:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const getPostData = (req) => {
const promise = new Promise((resolve, reject) => {
if (req.method !== "POST") {
return resolve({});
}
if (req.headers["content-type"] !== "application/json") {
return resolve({});
}
// 数据流获取数据的方式是异步的,可以放在Promise
let postData = "";
req.on("data", chunk => {
postData += chunk.toString();
})
req.on("end", () => {
if (!postData) {
return resolve({});
}
resolve(JSON.parse(postData));
})
})
return promise;
}

app.js中使用函数getPostData()获取PostData,原本req.body是空的,现在把获取到的postData放进去。
把所有 路由的处理 都放在then()的回调函数中,让接下来在router中处理路由时都能拿到PostData:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const serverHandle = (req, res) => {
//设置返回格式 JSON
res.setHeader("Content-type", "application/json")

//获取path
const url = req.url
req.path = url.split("?")[0]

//解析query,parse方法用于将一个 JSON 字符串转换为JS对象
req.query = querystring.parse(url.split("?")[1])

// 处理postData
getPostData(req).then(postData => {
// 原本req.body是空的,现在把获取到的postData放进去
req.body = postData;

//处理blog路由,blogData是通过调用函数handleBlogRouter得到的对象
const blogData = handleBlogRouter(req, res)
if (blogData) {
res.end(
//记住end是一句字符串,故必须把JS对象转换为JSON字符串
JSON.stringify(blogData)
)
//记住要使用return来结束
return
}

//处理user路由(当路由命中handleUserRouter时)
const userData = handleUserRouter(req, res)
if (userData) {
res.end(
//记住end是一句字符串,故必须把对象转换为字符串
JSON.stringify(userData)
)
//记住要使用return来结束
return
}

//未命中路由,返回404(这个了解即可,使用不多)
res.writeHead(404, { "Content-type": "text/plain" })
res.write("404 Not Found\n")
res.end()
})

}

4-10 开发 新建和更新博客路由

新建博客路由

  • controller-blog.js中新建一个函数newBlog(),先通过它返回假数据。
  • router-blog.js中引入newBlog(),开发新建博客的路由"/api/blog/new"

controller-blog.js中新建一个函数newBlog(),先通过它返回假数据:

1
2
3
4
5
6
7
// 兼容:(ES6)如果没有blogData就给一个空对象
const newBlog = (blogData = {}) => {
// blogData是一个博客对象,包含title content 属性
return {
id: 3//表示新建博客,插入到数据表里面的id
}
}

router-blog.js中引入newBlog(),将controller中处理好的数据经过SuccessModel格式化后显示在页面上:

1
2
3
4
5
6
//新建一篇博客
if (method === "POST" && req.path === "/api/blog/new") {
// req.body.js中获取到的postData
const data = newBlog(req.body);
return new SuccessModel(data);
}

启动项目,使用postman测试一下效果:
postman测试

可以在控制台看到postData:
控制台


更新博客路由

  • 和新建博客路由的思路类似,主要区别在更新博客我们还需要从get请求中获取一个id,以此判断更新哪一篇博客。
  • 要更新就需要获取id,在“获取博客详情”时我们使用了id,现在“更新博客路由”也要使用id,那我们就可以把获取id的表达式提取到路由处理的外面。

controller-blog.js中处理数据:

1
2
3
4
5
6
7
8
9
// 更新一篇博客
const updateBlog = (id, blogData = {}) => {
// id是要更新的博客的id
// blogData是一个博客对象,包含title content 属性
console.log("update blog...", id, blogData);

// 返回true说明更新成功
return true;
}

router-blog.js中获取数据并以正确格式显示在页面上

1
2
3
4
5
6
7
8
9
10
11
//更新一篇博客
if (method === "POST" && req.path === "/api/blog/update") {
// id是从req.query中获取到的
const result = updateBlog(id, req.body);
// result是updateBlog()返回的布尔值,true则更新成功
if (result) {
return new SuccessModel();
} else {
return new ErrorModel("更新博客失败");
}
}

通过postman测试updateBlog()返回true时:
postman测试updateBlog()返回true
控制台:
控制台

通过postman测试updateBlog()返回false时:
postman测试updateBlog()返回false


4-11 开发 删除博客路由和登录路由

删除博客路由

  • 删除博客就很简单,只需要获取一个id即可。
  • controller-blog.js中新建方法delBlog(),根据传入的id删除博客。
  • router-blog.js中引用delBlog()

controller-blog.js

1
2
3
4
5
6
const delBlog = (id) => {
// id是要删除的博客的id

// 返回true说明删除成功
return true;
}

router-blog.js

1
2
3
4
5
6
7
8
9
//删除一篇博客
if (method === "POST" && req.path === "/api/blog/del") {
const result = delBlog(id);
if (result) {
return new SuccessModel();
} else {
return new ErrorModel("删除博客失败");
}
}

通过postman测试updateBlog()返回true时:
postman测试
注意:我们虽然没有post数据,但是由于我们规定了method === "POST",所以必须通过postman进行测试。


登录路由

blog.js中的路由就写完了,接下来就要处理user.js相关的路由。

  • controller文件夹中新建user.js文件,创建函数loginCheck用于处理登录路由的逻辑。
  • router-user.js中,引入函数loginCheck,在函数handleUserPouter接收app.js中传入的参数reqres,在该函数中处理登录路由,返回由SuccessModel格式后的数据。

**controller-user.js**:

1
2
3
4
5
6
7
8
9
10
11
const loginCheck = (username, password) => {
// 先使用假数据
if (username === "hlz" && password === "123") {
return true;
}
return false;
}

module.exports = {
loginCheck
}

router-user.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const { loginCheck } = require("../controller/user");
const { SuccessModel, ErrorModel } = require("../model/resModel");

const handleUserRouter = (req, res) => {
const method = req.method//GET POST

//登录
if (method === "POST" && req.path === "/api/user/login") {
// username和password都是从postData中获取的
// 之前我们将password存在了req.body内
const { username, password } = req.body;
const result = loginCheck(username, password);
if (result) {
return new SuccessModel();
} else {
return new ErrorModel("登录失败");
}
}
}
module.exports = handleUserRouter

通过postman测试登录数据正确时:
postman测试登录数据正确

postman测试登录数据错误时


总结系统架构设计的四层

第一层: www.js [开启 Server]
第二层:app.js [通信设置层]
第三层:router文件夹 [路由相关业务逻辑层]
第四层:controller文件夹 [数据处理层]

  • 一开始进入第一层,项目执行的是bin文件夹下的**www.js,这里面只是createServer的逻辑,端口连通**什么的,和我们的业务逻辑没有关系。
  • 第二层app.js ,用来设置系统比较基础的功能(处理bloguser路由)或者定义一些公共参数(可以通过res/req传给路由,比如:获取path、解析query、异步获取PostData)还有设置返回类型(JSON),还是不涉及业务逻辑的处理。
  • 第三层:router文件夹下的两个路由文件 blog.jsuser.js,稍微涉及逻辑层,但只管路由
    • 来了什么路由就分配什么数据,通过SuccessModel/ErrorModel 处理数据格式
    • 匹配到路由(接口)以后会去处理一些数据,然后(通过SuccessModel)会给你返回一个 正确的格式。至于这些数据是怎么去匹配的,怎么去筛选的,是正确的还是错误的他不管,他只管和路由(接口)有关的数据分配。
  • 第四层controller.js最关心数据的层次,他没有resreqpathquery这些东西,只是对传入的数据进行计算处理再返回。(我们目前还没有计算,接下来会有)。至于数据返回后是怎么分配给路由的controller.js是不管的。

一层层拆分是一个由最基础的http服务向逻辑层转变的过程。

我们一开始先www.js创建服务器server并监听8000端口,这个服务器做的事情放到app.js的serverHandle中。
然后app.js将HTTP请求req和响应res传到路由组件(router文件夹),并设置一些 公共参数 通过req/res传给路由组件以供使用。
router文件夹下的路由组件blog.jsuser.js中,根据app.js传过来的req、res匹配对应的接口并将从controller.js中得到的数据进行一些格式的处理。最终返回显示在页面上。
controller.js对获取到的数据进行8计算处理再返回8。