Node.js+Express+Koa2开发接口学习笔记(二)
搭建开发环境
- 从0开始搭建,不适用任何框架
- 使用nodemon监测文件变化,自动重启node
- 使用cross-env设置环境变量,兼容max linux和windows
创建项目文件夹blog-1
,在终端输入命令
npm init -y
在根目录下创建bin => www.js文件,将初次运行的文件www.js存放在bin目录下。同时需要修改package.json
主程序入口文件为www.js。
在根目录下创建app.js,用于定义处理请求的函数。
//app.js
const serverHandle = (req, res) => {};
module.exports = serverHandle;
//bin->www.js
const http = require("http");
const PORT = 8000;
const serverHandle = require("../app");
const server = http.createServer(serverHandle);
server.listen(PORT);
回头简单完善下serverHandle的内容
const serverHandle = (req, res) => {
// 设置返回格式 JSON
res.setHeader("Content-type", "application/json");
const resData = {
name: "小风车吱呀转",
site: "博客园",
};
res.end(JSON.stringify(resData));
};
module.exports = serverHandle;
运行命令node bin/www.js
,访问8000端口。在页面可以看到返回接口,按F12点击Network
可以看到请求详情。
接下来安装nodemon
和cross-env
npm install nodemon cross-env --save-dev
有了nodemon
可以监测文件变化
,自动重启node
,我们每次都无需修改文件后重新执行node XXX文件。
在package.json
添加运行命令,并且通过cross-env
设置环境变量
"scripts": {
"dev": "cross-env NODE_ENV=dev nodemon ./bin/www.js",
...
},
这样我们只要运行npm run dev
就可以启动服务器了。
为了验证是否生效,我们修改代码,返回当前项目的环境
const serverHandle = (req, res) => {
// 设置返回格式 JSON
res.setHeader("Content-type", "application/json");
const resData = {
name: "小风车吱呀转",
site: "博客园",
env: process.env.NODE_ENV, //获取当前项目环境
};
res.end(JSON.stringify(resData));
};
module.exports = serverHandle;
此时服务器重新启动,在浏览器显示出结果。
{"name":"小风车吱呀转","site":"博客园","env":"dev"}
初始化路由
- 初始化路由:根据之前技术方案的设计,做出路由
- 返回假数据:将路由和数据处理分离,以符合设计原则
在根目录下创建src目录,在里面创建2个文件夹,也就是创建2个路由,blog和user。在blob路由中需要实现5个接口,user路由中需要实现1个接口。
在blog =>blog.js中写
const handleBlogRouter = (req, res) => {
const method = req.method; // GET POST
const url = req.url;
const path = url.split("?")[0];
// 获取博客列表
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 => user.js中写
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;
在app.js中处理不同路由的请求,如果未命中路由则返回404
const handleBlogRouter = require("./src/router/blog");
const handleUserRouter = require("./src/router/user");
const serverHandle = (req, res) => {
// 设置返回格式 JSON
res.setHeader("Content-type", "application/json");
// 处理blob路由
const blogData = handleBlogRouter(req, res);
if (blogData) {
res.end(JSON.stringify(blogData));
return;
}
// 处理user路由
const userData = handleUserRouter(req, res);
if (userData) {
res.end(JSON.stringify(userData));
return;
}
// 未命中路由,返回404,同时将返回格式修改为纯文本
res.writeHead(404, { "Content-type": "text/plain" });
res.write("404 NOT FOUND\n");
res.end();
};
module.exports = serverHandle;
// process.env.NODE_ENV
分别访问相关路由,验证是否正确,例如访问获取博客列表接口http://localhost:8000/api/blog/list?keyword=xxx
最后可以优化下,在app.js中获取path,存放到req中,这样就不需要在每个路由文件重新获取一遍
//app.js
// 获取path
const url = req.url;
req.path = url.split("?")[0];
//blog.js、user.js 将所有path改为req.path
// 获取博客列表
if (method === "GET" && req.path === "/api/blog/list") {
return {
msg: "这是获取博客列表的接口",
};
}
数据模型
首先建立接口返回的内容结构,返回的格式大致为
{
errno:0, // 0代表没有错误,-1代表有错误
data:{...} // 数据,如果失败则没有data
message:'xxx' // 返回失败或成功的信息
}
在src目录创建model => resModel.js,编写代码
class BaseModel {
constructor(data, message) {
if (typeof data === "string") {
this.message = data;
data = null;
message = null;
}
if (data) {
this.data = data;
}
if (message) {
this.message = message;
}
}
}
class SuccessModel extends BaseModel {
constructor(data, message) {
super(data, message);
this.errno = 0;
}
}
class ErrorModel extends BaseModel {
constructor(data, message) {
super(data, message);
this.errno = -1;
}
}
module.exports = {
SuccessModel,
ErrorModel,
};
在GET请求和POST请求,有时候也需要在query中获取客户端发出的请求参数。所以在app.js中还需要借助querystring
将query参数存起来。
const querystring = require("querystring");
...
const serverHandle = (req, res) => {
...
// 获取path
const url = req.url;
req.path = url.split("?")[0];
// 解析query
req.query = querystring.parse(url.split("?")[1]);
...
}
创建controller/blog.js,controller文件夹主要存放接口处理数据的方法,在blog.js中专门处理与博客相关数据的处理。
在blog.js中简单编写一个处理获取博客列表的函数,接收作者author和关键词keyword2个参数,由于还没有连接数据库,暂时用不上。
const getList = (author, keyword) => {
// 先返回假数据(格式是正确的)
return [
{
id: 1,
title: "标题A",
content: "内容A",
createTime: 1694317992475,
author: "小米",
},
{
id: 2,
title: "标题B",
content: "内容B",
createTime: 1694318048011,
author: "小华",
},
];
};
module.exports = {
getList,
};
在router => blog.js中使用定义好的内容结构模型和获取博客列表数据
const { getList } = require("../controller/blog");
const { SuccessModel, ErrorModel } = require("../model/resModel");
// 获取博客列表
if (method === "GET" && req.path === "/api/blog/list") {
const author = req.query.author || "";
const keyword = req.query.keyword || "";
const listData = getList(author, keyword);
return new SuccessModel(listData);
}
测试一下该接口,可以看到返回我们想要的内容。
目前,项目的目录结构如下
- www.js,项目入口文件,主要是创建httpServer,并监听端口
- app.js,serverHandler,主要对req,res进行处理,获取path用于api匹配,获取query参数,获取post数据,并调用router中的相关处理方法
- router部分,主要根据不同模块处理不同的api入口,根据req.path进行匹配分发,调用api的实现方法。获取查询参数与post数据,并将这些作为controller层的各个业务处理函数的入参。
- controller部分,这部分主要实现各个api对应的业务逻辑实现,输入为router中的获取的参数。
- model层,公用的model,如返回数据模型,含有data,message,errorno等信息
到目前为止,controller中的业务实现逻辑并未涉及到数据库的内容,目前还是静态数据。
获取博客详情
通过前面的处理获取博客列表的函数,可以依次类推写出同样是GET方法的 获取博客详情函数。
在controller => blog.js中编写getDetail函数,它接收一个id参数
const getDetail = (id) => {
// 先返回假数据
return {
id: 1,
title: "标题A",
content: "内容A",
createTime: 1694317992475,
author: "小米",
};
};
module.exports = {
getList,
getDetail,
};
在router => blog.js中使用它
const { getList, getDetail } = require("../controller/blog");
// 获取博客详情
if (method === "GET" && req.path === "/api/blog/detail") {
const id = req.query.id;
const detailData = getDetail(id);
return new SuccessModel(detailData);
}
处理POSTData
当用户添加一条博客或者更新某条博客时,需要通过post方法向服务器发送数据,由于服务端接收data数据的过程是异步的,同步代码优先执行,异步代码等同步代码之行结束后再根据规则执行, 这就造成服务端接收不到传送的数据。
所以需要借助Promise解决异步问题:将异步代码放入Promise中。
在app.js中封装一个用户处理post请求发送data数据的函数,在主函数serverHandle里调用它,确保接收完数据,存放到req.body里后才执行后续代码
// 用于处理post data
const getPostData = (req) => {
const promise = new Promise((resolve, reject) => {
if (req.method !== "POST") {
resolve({});
return;
}
if (req.headers["content-type"] !== "application/json") {
resolve({});
return;
}
let postData = "";
req.on("data", (chunk) => {
postData += chunk.toString();
});
req.on("end", () => {
if (!postData) {
resolve({});
return;
}
resolve(JSON.parse(postData));
});
});
return promise;
};
const serverHandle = (req, res) => {
// 设置返回格式 JSON
res.setHeader("Content-type", "application/json");
// 获取path
const url = req.url;
req.path = url.split("?")[0];
// 解析query
req.query = querystring.parse(url.split("?")[1]);
//处理post data
getPostData(req).then((postData) => {
req.body = postData;
// 处理blob路由
const blogData = handleBlogRouter(req, res);
if (blogData) {
res.end(JSON.stringify(blogData));
return;
}
// 处理user路由
const userData = handleUserRouter(req, res);
if (userData) {
res.end(JSON.stringify(userData));
return;
}
// 未命中路由,返回404,同时将返回格式修改为纯文本
res.writeHead(404, { "Content-type": "text/plain" });
res.write("404 NOT FOUND\n");
res.end();
});
};
完善下controller => blog.js里对POST接口的处理
const newBlog = (blogData = {}) => {
// blogData 是一个博客对象,包含title content属性
console.log("newBlog blogData...", blogData);
return {
id: 3, // 表示新建博客,插入到数据表里面的id
};
};
const updateBlog = (id, blogData = {}) => {
// id就是要更新博客的id
// blogData是一个博客对象,包含title content属性
console.log("update blog", id, blogData);
return false;
};
const delBlog = (id) => {
// id 就是要删除博客的id
return true;
};
在路由blog.js里调用实现方法,返回POST接口的响应数据
const { getList, getDetail, newBlog } = require("../controller/blog");
// 新建一篇博客
if (method === "POST" && req.path === "/api/blog/new") {
const data = newBlog(req.body);
return new SuccessModel(data);
}
// 更新一篇博客
if (method === "POST" && req.path === "/api/blog/update") {
const result = updateBlog(id, req.body);
if (result) {
return new SuccessModel();
} else {
return new ErrorModel("更新博客失败");
}
}
// 删除博客
if (method === "POST" && req.path === "/api/blog/del") {
const result = delBlog(id);
if (result) {
return new SuccessModel();
} else {
return new ErrorModel("删除博客失败");
}
}
对user路由的登录接口也是同理,在controller => user.js里对登录接口进行校验账户密码
const loginCheck = (loginData) => {
const { username, password } = loginData;
// 先使用假数据
if (username === "zhangsan" && password === "123456") {
return true;
}
return false;
};
module.exports = {
loginCheck,
};
在路由user.js里调用实现方法,返回登录结果
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") {
const result = loginCheck(req.body);
if (result) {
return new SuccessModel();
} else {
return new ErrorModel("登录失败");
}
}
};
module.exports = handleUserRouter;
源码地址:https://github.com/DaneOvO/Nodejs-Express-Koa2-Learning