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

4-5 搭建开发环境

  • 从0开始搭建,不使用任何框架
  • 使用nodemon监测文件变化,自动重启node(不需要像之前那样使用node xxx.js来手动重启node)
  • 使用cross-env设置环境变量,兼容mac linux和windows

开始搭建

根据上一篇笔记的“模块化规范”初始npm环境,并在package.json文件中继续以下步骤:

nodemon

安装nodemon
npm install nodemon --save或者npm i nodemon -D
启动nodemon:
nodemon app.js(用nodemon替换node去启动项目的入口文件将项目改变成自动重启服务器)
当你看到每次保存以后终端都自动更新,说明自启动成功
nodemon自动重启成功
后续启动项目:(package.json中的配置写在了下面)
npm run dev

cross-env

安装cross-env
npm install cross-env --save-dev或者npm i cross-env -D

手动补齐package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"name": "blog-1",
"version": "1.0.0",
"description": "",
"main": "bin/www.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "cross-env NODE_ENV=dev nodemon ./bin/www.js",
"prd": "cross-env NODE_ENV=production nodemon ./bin/www.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"cross-env": "^6.0.3",
"nodemon": "^2.0.2"
}
}
  • 在node中,process对象是全局变量,它提供当前node.js的有关信息,以及控制当前node.js的有关进程。因为是全局变量,它对于node应用程序是始终可用的,无需require()。
  • env是process的一个属性,这个属性返回包含用户环境信息的对象。在终端输入node后,在输入process.env可以看到打印出来的信息。
  • NODE_ENV不是process.env对象上原有的属性,它是我们自己添加上去的一个环境变量,用来确定当前所处的开发阶段
    • 一般 生产阶段 设为production,开发阶段 设为develop(在package.json中设置),然后在 脚本 中读取process.env.NODE_ENV即可知道现在是开发阶段还是生产阶段。

app.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const serverHandle = (req, res) => {
//设置返回格式 JSON
res.setHeader("Content-type", "application/json")

const resData = {
name: "Hlz",
site: "imooc",
//这里还是开发阶段,
//所以NODE_ENV读到的是我们`package.json`中设置好的dev
env: process.env.NODE_ENV
}

res.end(
JSON.stringify(resData)
)
}

module.exports = serverHandle

./bin/www.js:

1
2
3
4
5
6
7
const http = require("http")

const PORT = 8000
const serverHandle = require("../app")

const server = http.createServer(serverHandle)
server.listen(PORT)

千万不要忘记**终端执行npm run dev**!!

测试

连通8000成功


4-6 初始化路由 开发接口

初始化路由:根据之前技术方案的设计,做出路由
返回假数据:将路由和数据处理分离,以符合设计原则
之前技术方案的接口设计

注意

  1. 路由组件(函数)写好后需要通过module. exports输出出去,然后再在需要调用的js文件顶部通过require调用后才能使用。**(app.js中的serverHandle不是路由组件,但是注意,函数都需要输出!不输出www.js中就用不了)**
  2. 千万不要忘记在**终端执行npm run dev**!!(否则就不会去监听8000端口)
  3. 设置接口路径时前面前往不要忘记api前面的/!

思路

我们先不管接口内容,去把各个接口跑通。
blog.jsuser.js中存放的是路由组件
app.js用来设置系统比较基础的功能(处理bloguser路由)或者参数(获取path、解析query)还有返回类型(JSON),还是不涉及业务逻辑的处理。
blog.jsuser.js中通过判断以后返回的是对象,所以在app.js中得到的blogData也是一个对象,但res.end返回的需要是字符串,所以需要通过JSON.stringify()来转换一下,此时注意需要return来结束。

复习:处理HTTP请求简单示例

为了帮助理解下面被拆分的代码,我们需要先复习一下一个简单的示例。
处理HTTP请求简单示例

  1. 调用 node的**'http'模块**
  2. 使用'http'模块的createServer()创建一个server服务端
  3. createServer()接收一个函数作为参数,该函数接收2个参数,分别是 浏览器向server服务端**发送的请求req和 服务端返回给浏览器的响应res**。
  4. 其中**res.end()中需要返回字符串**,该字符串会显示在页面上
  5. 让**server服务端监听8000端口**,好让浏览器访问http://localhost:8000/时可以访问到服务端。

在下面的代码中,这一部分的“开启 Server,监听8000端口”写在www.js中,createServer()的回调函数(参数)被写在app.js中。

代码

目录

www.js开启 Server,监听8000端口:

1
2
3
4
5
6
7
8
9
const http = require("http")

const PORT = 8000
const serverHandle = require("../app")

//通过createServer()调用app.js中的serverHandle,创建服务器server
const server = http.createServer(serverHandle)
//启动监听服务,监听8000端口
server.listen(PORT)

app.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
40
41
// 引用路由组件
const handleBlogRouter = require("./src/router/blog")
const handleUserRouter = require("./src/router/user")

// serverHandle接收2个参数:浏览器的请求req,服务端的响应res
const serverHandle = (req, res) => {
//设置返回格式 JSON
res.setHeader("Content-type", "application/json")

//处理blog路由,blogData是通过调用函数handleBlogRouter得到的对象
//将req和res传过去
const blogData = handleBlogRouter(req, res)
if (blogData) {
res.end(
//记住end是一句字符串,故必须把对象转换为字符串
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()
}
//函数也需要输出
module.exports = serverHandle

//process.env.NODE_ENV

blog.js5个路由接口:

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
//接收 浏览器的请求req,服务端的响应res
const handleBlogRouter = (req, res) => {
const method = req.method//GET POST
const url = req.url
const path = url.split("?")[0]

//获取博客列表,api前面千万不要漏掉 / !!!
if (method === "GET" && path === "/api/blog/list") {
return {
msg: "这是获取博客列表的接口"
}
}

//获取博客详情
if (method === "GET" && path === "/api/blog/detail") {
return {
msg: "这是获取博客详情的接口"
}
}

//新建一篇博客
if (method === "POST" && path === "/api/blog/new") {
return {
msg: "这是新建一篇博客的接口"
}
}

//更新一篇博客
if (method === "POST" && path === "/api/blog/update") {
return {
msg: "这是更新一篇博客的接口"
}
}


//删除一篇博客
if (method === "POST" && path === "/api/blog/del") {
return {
msg: "这是删除一篇博客的接口"
}
}
}

/* 不要忘记输出函数 */
module.exports = handleBlogRouter

user.js一个登录接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
const handleUserRouter = (req, res) => {
const method = req.method//GET POST
const url = req.url
const path = url.split("?")[0]

//登录
if (method === "POST" && path === "/api/user/login") {
return {
msg: "这是登陆的接口"
}
}
}
module.exports = handleUserRouter

测试结果

直接输入http://localhost:8000/,未命中设定好的任何一个路由,触发设定好的404:
直接输入`http://localhost:8000/`

输入http://localhost:8000/api/blog/list?author=hlz&keyword=hahaha,命中其中一个接口:
直接输入`http://localhost:8000/api/blog/list?author=hlz&keyword=hahaha`

打开postman测试post请求,如下图所示:
postman测试post请求
再测试一个user的登录接口:
测试一个user的登录接口

简化

把两个路由blog.jsuser.js中都用得到的path参数放到app.js中一次性获取。(因为req和res都会传给路由组件,所以可以在app.js中获取path参数以后挂载在req/res上传给用的到的路由组件)

原始代码:
原始代码
简化后:
简化后1
简化后2


补充:constructor构造方法

constructor 是一种用于创建和初始化class创建的对象的特殊方法。
在一个构造方法中可以使用super关键字来调用一个父类的构造方法
MDN中关于constructor构造方法的解释和例子
例子


补充:JSON.parse()

JSON.parse() 方法用于将一个 JSON 字符串转换为对象
可参考笔记“JSON方法”


4-7 开发路由(博客列表路由)

resModel.js建数据模型

src新建model文件夹,在该文件夹下新建resModel.js文件,在该文件**新建一个基类BaseModel**。
通过基类BaseModel 建两个模型SuccessModelErrorModel,希望通过这两个模型实现返回格式的统一

resModel.js代码解析:

  • 4 BaseModel中,constructor()传入的**data是一个对象类型,message是一个字符串**类型的消息。
  • 6-13 兼容,使得constructor()可以只传一个字符串,也可以传一个对象和一个字符串。
    • 假设传入的第一个参数data是字符串类型且没有传入第二个参数message,为使其兼容则需将参数data赋给BaseModelmessage,且datamessage都不要了。
    • 注意:this.message才是BaseModelmessagedatamessage都只是传入的参数。(不清楚可以看MDN中的例子)
  • 14-19 如果传入constructor()的是data对象,那就赋值给BaseModeldata(this.data)。message同理。
  • 23 我们需要**设立两个模型SuccessModelErrorModel(他们继承BaseModel**),用于app.js中 的res.end
    • 26 使用super继承父类BaseModeldatamessage。super相当于执行了BaseModelconstructor,统一datamessage传过去放在父类BaseModel中处理
  • 最后记得输出两个模块(SuccessModel和ErrorModel)。
  • 注意:BaseModel中的this指向使用SuccessModel/ErrorModel的对象
    • 比如:router-blog.js-handleBlogRouter()里new了一个SuccessModel,那么this就指向在app.js中调用了handleBlogRouter()的**,对象blogData**。所以对象blogData中就会有SuccessModel中的datamessageerrno
  • 接下来就可以**通过new SuccessModel()new ErrorModel()来new一个对象了。(数据等就可以通过参数传入)**(可参考MDN理解

resModel.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
40
41
class BaseModel {
//constructor 是一种用于创建和初始化class创建的对象的特殊方法。
//要求传入的data为对象,message为字符串
constructor(data, message) {
//兼容,如果只传入了 字符串date,没有传入message
if (typeof data === "string") {
// 则将传入的date赋值给BaseModel的message,并清空data、message
this.message = data
//注意,this.message才是BaseModel的message
//data和message都只是传入的参数
data = null
message = null
}
if (data) {
this.data = data
}
if (message) {
this.message = message
}
}
}

class SuccessModel extends BaseModel {
constructor(data, message) {
//super相当于执行了BaseModel的constructor,统一将data和message传过去放在父类BaseModel中处理
super(data, message)
this.errno = 0
}
}
class ErrorModel extends BaseModel {
constructor(data, message) {
super(data, message)
this.errno = -1
}
}

//记得输出
module.exports = {
SuccessModel,
ErrorModel
}

借助这2个模型我们希望实现返回格式的统一(非实际使用,只是在app.js中做效果展示):
使用范例

app.js解析公共参数

所有公共的参数都放到app.js中进行解析(比如query的解析):
req.query得到的效果可以参考这个例子

1.app.js中顶部增添:

1
2
//引用js原生模块querystring
const querystring = require("querystring")

2.app.jsserverHandle增添:

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

新建controller文件夹

  • src下新建controller文件夹,在该文件夹下新建blog.js文件。
    • controller文件夹中的js文件类似于router中的文件
  • blog.js文件中先返回假数据(格式是正确的),此时虽然author和keyword没有使用,但我们假装他使用了并返回了很多的元素(博客)。
    • 没连接数据库,先返回假数据
  • 由于接下来还要建很多个函数,所以返回(输出)的是对象(方便增加返回的函数)。

controller文件夹内的blog.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
const getList = (author, keyword) => {
//虽然author,keyword没有使用,但先返回假数据(格式是正确的)
//返回的是数组,数组元素为博客文章
return [
{
id: 1,
title: "标题A",
content: "内容A",
createTime: 1579421639661,
author: "hlz"
},
{
id: 2,
title: "标题B",
content: "内容B",
createTime: 1579421789492,
author: "lv"
}
]
}

//接下来还会有函数需要输出,所以先使用对象的方式进行返回
module.exports = {
getList
}

其中,createTime可以在网页上获取一个来使用:
createTime获取方法

router中引用controller

  • 可以在router文件夹内的blog.js文件中通过解构赋值的方式引用controller文件夹内的blog.js文件中定义的**getList函数**。(至于为什么使用解构赋值可参考笔记“Node.js函数引用与解构赋值”
  • 想要调用getList就必须传入参数author, keyword,而author, keyword是从queryString中来的,所以我们可以app.js中统一获取到req.query
    • 注意:req.query得到的是一个JSON对象,如果我们访问http://localhost:8000/api/blog/list?author=hlz&keyword=hahaha,则这个对象中author属性和keyword属性可参考这个例子
  • 通过getList()拿到的数据listData 作为参数data **传入SuccessModel**。
    • SuccessModel中的**this指向blogData对象。经过SuccessModel相当于blogData对象添加属性data以及属性值listData和属性errno **。
      • 注意:BaseModel中的this指向使用SuccessModel/ErrorModel的对象
      • 也就是说:router-blog.js-handleBlogRouter()里new了一个SuccessModel对象,那么this就指向在app.js中调用了handleBlogRouter()对象blogData 。所以**对象blogData中就会有SuccessModel中的datamessageerrno**。
  • 通过SuccessModel合成后的数据在app.js中作为blogData对象的属性 通过res.end()显示在页面上

router文件夹内的blog.js顶部:

1
2
3
4
//通过解构赋值的方式引用controller/blog.js中的getList函数
const { getList } = require("../controller/blog")
//引入resModel.js中的两个模块
const { SuccessModel, ErrorModel } = require("../model/resModel")

router文件夹内的blog.js“获取博客列表”的部分进行修改

1
2
3
4
5
6
7
8
9
10
11
12
//获取博客列表,api前面千万不要漏掉 / !!!
if (method === "GET" && req.path === "/api/blog/list") {
//从app.js中解析的query中读取author、keyword,没有则空
const author = req.query.author || " "
const keyword = req.query.keyword || " "
//将读取到的author, keyword传到controller/blog.js中的getList函数,
//获取返回的博客列表数据listData
const listData = getList(author, keyword)
//将博客列表数据listData传入SuccessModel,
//此时我们只传入了data参数,没有传入message参数
return new SuccessModel(listData)
}

测试获取博客列表的接口通过假数据显示已经跑通了:
image.png

捋清思路(系统架构设计的四层)

系统架构设计的四层抽象
第一层: www.js [开启 Server]
第二层:app.js [通信设置层]
第三层:router文件夹 [业务逻辑层]
第四层:controller文件夹 [数据层]

一开始进入项目执行的是bin文件夹下的**www.js,这里面只是createServer的逻辑,端口连通什么的,和我们的业务逻辑没有关系。
第二层app.js ,用来设置系统比较基础的功能(处理bloguser路由)或者参数(获取path、解析query)还有返回类型(JSON),还是
不涉及业务逻辑的处理。
第三层:router文件夹下的两个
路由文件** blog.jsuser.js,稍微涉及逻辑层,但只管路由。匹配到路由(接口)以后会去处理一些数据,然后(通过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对数据进行计算处理再返回。