Node.js+Express+Koa2开发接口学习笔记(二)

发布时间 2023-09-10 17:39:31作者: 小风车吱呀转

搭建开发环境

  • 从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可以看到请求详情。

接下来安装nodemoncross-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"}

初始化路由

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

pPcYR3D.png

在根目录下创建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);
  }

测试一下该接口,可以看到返回我们想要的内容。

pPca3xU.png

目前,项目的目录结构如下

pPcanVs.png

  1. www.js,项目入口文件,主要是创建httpServer,并监听端口
  2. app.js,serverHandler,主要对req,res进行处理,获取path用于api匹配,获取query参数,获取post数据,并调用router中的相关处理方法
  3. router部分,主要根据不同模块处理不同的api入口,根据req.path进行匹配分发,调用api的实现方法。获取查询参数与post数据,并将这些作为controller层的各个业务处理函数的入参。
  4. controller部分,这部分主要实现各个api对应的业务逻辑实现,输入为router中的获取的参数。
  5. 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