node.js接口开发(二)

247 阅读9分钟

一个项目文件不同于测试功能用的demo,一个项目因为代码量很多,所以需要把一些代码拆分开来,下图就是这次项目的文件目录(不过这并是不最终版,但是也差不多了)

这一块的内容是我练习node的时候写的文件,后续还需要重构,附上可能用到的github地址 github.com/wukaoyu/nod…

一、环境配置

先看packgae.json

{
  "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": "^5.2.0",
    "nodemon": "^1.19.1"
  },
  "dependencies": {
    "mysql": "^2.17.1"
  }
}
  1. 需要关注的几点:main的值是bin/www.js,这代表bin文件夹中www.js文件就是入口文件
  2. 然后再看devDependencies和dependencies
    • 需要特别注意devDependencies和dependencies的区别,前者是用于开发运行的依赖库,主要用于辅助我们开发,而后者是项目中用到的依赖库,比如react中的antd这些。
    • 在安装的时候如果安装的是devDependencies中的依赖输入命令 npm install 要安装的库 --save -dev
    • dependencies的命令则是npm install 要安装的库 --save
    • devDependencies中安装了两个依赖库:cross-env和nodemon
    • cross-env的作用是让你知道运行的环境是什么,是开发环境还是线上环境,因为有时候开发环境和线上环境运行的代码会有些不同,比如说开发环境用的数据库和线上的数据库肯定不能是同一个
    • nodemon的作用是让你每次代码进行更改的时候可以立刻更新一次,不然每次更新代码都要重启会非常的麻烦
  3. 继续看scripts
    • test是原本就有的不管他
    • dev代表这是开发环境,那么怎么给他这个‘开发环境’的标识呢,就是之前下载的cross-env,dev后面的值是"cross-env NODE_ENV=dev nodemon ./bin/www.js"需要识别环境的地方取出NODE_ENV的值,在这里定义了是dev,那么就知道是开发环境了,之后输入 npm run dev(是不是很眼熟)就可以用刚刚下载的依赖包nodemon来运行bin文件夹下面的www.js文件了
    • prd的值和dev差不多,只是修改了NODE_ENV的值,是prd,这样如果是线上跑线上环境的话就npm run prd,那么利用cross-env就会知道是线上环境了

二、代码结构

1、www.js

现在输入npm run dev或者npm run prd都是先从www.js文件开始运行,先给上www.js的代码

const http = require('http');
const serverHandle = require('../app');

const PORT = 8000;

const server = http.createServer(serverHandle);

server.listen(PORT);

console.log("ok");

这里代码很少,只有六行代码,在这个文件配置的主要是端口号,创建HTTP服务,而服务的内容在二行const serverHandle = require('../app');他是卸载app.js文件中的 这里这样写好直接基本就不用去改了,除非需要更改一下端口号。

2、app.js

先附上app.js的代码,里面内容比较多,先不用仔细看

const querystring = require("querystring")
const handBlogRouter = require("./src/router/blog");
const handUserRouter = require("./src/router/user");

const getPostData = (req) => {
    const promise = new Promise((resolve, reject) => {
        if (req.method !== "POST") {
            resolve({});
            return
        }
        if (req.header["content-type"] !== "application/json") {
            resolve({});
            return
        }
        let postData = '';
        req.on('data',chunk => {
            postData += chunk;
        })
        if (!postData) {
            resolve({});
            return
        }
        resolve(
            JSON.parse(postData)
        )
    })
    return promise;
}

const serverHandle = (req, res) => {
    res.setHeader('Content-type', 'application/json');

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

    //解析query
    req.query = querystring.parse(url.split('?')[1]);

    //解析cookie
    req.cookie = {};
    const cookieStr = req.headers.cookie || '';
    cookieStr.split(";").forEach(item => {
        if (!item) {
            return
        }
        const arr = item.split("=");
        const key = arr[0].trim();
        const val = arr[1].trim();
        req.cookie[key] = val;
    });

    getPostData(req).then(postData => {
        req.body = postData
            //博客路由
        const blogResult = handBlogRouter(req, res);
        if(blogResult) {
            blogResult.then(blogData => {
                res.end(
                    JSON.stringify(blogData)
                )
            })
            return
        }

        //登录路由
        const userResult = handUserRouter(req, res);
        if(userResult) {
            userResult.then(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

仔细看的话其实也不难里面,定义了一个serverHandle函数,把他导出出去,在www.js中结束,他接收两个参数,req和res,req用于接收前端传来的数据,res用于返回数据给前端。至于这些数据怎么来的,先往下看

3、src/conf/db.js

  • 数据从哪来?当然是从数据库里来,这个文件是用来配置数据库的参数的,代码如下:
const env = process.env.NODE_ENV;

let MYSQL_CONF;

//环境为dev时候的配置(线下服务)mysql配置
if (env === 'dev') {
    MYSQL_CONF = {
        host: "localhost",
        user: "root",
        password: "7777",
        port: "3306",
        database: "node_blog"
    }
}

//环境为production(线上服务器)mysql的配置
if (env === 'production') {
    MYSQL_CONF = {
        host: "localhost",
        user: "root",
        password: "7777",
        port: "3306",
        database: "node_blog"
    }
}

module.exports = {
    MYSQL_CONF
};
  • 第一行就是用来读取运行环境的,通过process.env然后是自己定义的NODE_ENV的值来判断是dev环境还是prd环境下运行的,这里因为都是在自己电脑上运行,并不是,不是在服务器上,多云线上环境和dev环境都是一样的参数,但是还是先预留了这个功能,因为产品上线的话是肯定不可能用localhost这个本地地址的。
  • 定义好MYSQL_CONF这个参数后将他导出,在src/db/mysql.js文件接收这个参数

4、src/db/mysql.js

代码如下

const mysql = require("mysql");
const { MYSQL_CONF } = require("../conf/db");

//连接数据库
const con = mysql.createConnection(MYSQL_CONF);

con.connect();

function exec(sql) {
    const promise = new Promise((resolve, reject) => {
        con.query(sql, (err, result) => {
            if  (err) {
                reject(err);
                return
            }
            resolve(result)
        })
    })
    return promise;
}

module.exports = { exec }

连接上配置好的mysql数据,然后通过sql语句来对数据库进行操作,这文件最重要的就是exec这个函数,他接收一个sql参数,之前有需要操作数据库的地方,写好sql语句之后调用这个这个函数,然后他会以promise的方式返回执行完sql语句后返回的数据

5、src/model/resModel.js

  • 先了解下这个函数存在的意义:有时候sql语句输入后得到的预期效果不是用户想要的,比如说登录失败这类错误,前端接收到这个错误之后可以以更友好的方式来提醒一些错误信息,代码如下
    class BeseModel {
        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 BeseModel {
        constructor (data, message) {
            super(data, message);
            this.errno = 0;
        }
    }

    class ErrorModel extends BeseModel {
        constructor (data, message){
            super  (data, message);
            this.errno = -1;
        }
    }

    module.exports = {
        SuccessModel,
        ErrorModel
    }
  • 如果和预想的一样,则调用SuccessModel函数,并且返回对应的数据,errno返回0,否则调用ErrorModel函数,给出错误信息,并且errno返回1,前端利用errno来判断该给出错误提示还是成功之后的操作。

6、src/controller/xxx(user)

为什么这里打xxx了呢,因为之前的都是固定的文件,就是一个封装好的js调用这些功能,每个项目都是这样写的,而从这里开始,就要根据项目所需要的功能来了,比如说我这里取名叫user.js,那么所有与用户有关的操作都在这里:

const { exec } = require("../db/mysql");

const login = (username, passoword) => {
    let sql = `select realname,username from users where username='${username}' and password='${passoword}'`;

    return exec(sql).then(rows => {
        return rows[0] || {}
    })
}

module.exports = login
  • 比如说这个文件我只写了一个login函数,是关于用户登录的,把sql语句传到exec函数中去,然后返回结果到login函数中,最后把他导出。
  • 这个文件夹下放的主要用来写sql语句并且返回结果,但是不用来处理返回结果之后的操作

7、src/router/xxx(user)

同理,router文件夹下的js文件要根据项目需要的功能来,controller文件夹下每新建一个js文件都要在router下新建一个对应的js文件,代码如下:

const login = require("../controller/user")
const { SuccessModel, ErrorModel } = require("../model/resModel")

const handleUserRouter = (req, res) => {
    const method = req.method;

    const getCookieTime = () => {
        let d = new Date();
        d.setTime(d.getTime() + (24*60*60*1000));
        return d.toGMTString();
    }

    //登录
    if(method === 'GET' && req.path === '/api/user/login'){
        // const { username, password } = req.body;
        const { username, password } = req.query;
        const result = login(username, password);
        return result.then(data => {
            if (data.username) {
                //存入cookie
                res.setHeader("Set-Cookie", `username=${data.username};path=/; httpOnly; expires=${getCookieTime()}`);
                return new SuccessModel()
            }
            return new ErrorModel("用户名或密码错误")
        })
    }

    //登录测试
    if (method === "GET" && req.path === '/api/user/login-test') {
        if (req.cookie.username) {
            return Promise.resolve(new SuccessModel({
                username:req.cookie.username
            }))
        }
        return Promise.resolve(new ErrorModel('尚未登录'))
    }
}

module.exports = handleUserRouter;

router文件夹看名字也知道他主要的用处是什么,他是用来定义路由的,比如这段写了两个路由,一个是登录路由'/api/user/login'和一个检测是否登录的路由/api/user/login-test请求方式都是get请求。当然登录请求应该用post请求,这里为了方便测试,通过URL来传递数据,所以先这样写。

7、app.js

现在再回过头去看看app.js中的内容

const querystring = require("querystring")
const handBlogRouter = require("./src/router/blog");
const handUserRouter = require("./src/router/user");

const getPostData = (req) => {
    const promise = new Promise((resolve, reject) => {
        if (req.method !== "POST") {
            resolve({});
            return
        }
        if (req.header["content-type"] !== "application/json") {
            resolve({});
            return
        }
        let postData = '';
        req.on('data',chunk => {
            postData += chunk;
        })
        if (!postData) {
            resolve({});
            return
        }
        resolve(
            JSON.parse(postData)
        )
    })
    return promise;
}

const serverHandle = (req, res) => {
    res.setHeader('Content-type', 'application/json');

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

    //解析query
    req.query = querystring.parse(url.split('?')[1]);

    //解析cookie
    req.cookie = {};
    const cookieStr = req.headers.cookie || '';
    cookieStr.split(";").forEach(item => {
        if (!item) {
            return
        }
        const arr = item.split("=");
        const key = arr[0].trim();
        const val = arr[1].trim();
        req.cookie[key] = val;
    });

    getPostData(req).then(postData => {
        req.body = postData
            //博客路由
        const blogResult = handBlogRouter(req, res);
        if(blogResult) {
            blogResult.then(blogData => {
                res.end(
                    JSON.stringify(blogData)
                )
            })
            return
        }

        //登录路由
        const userResult = handUserRouter(req, res);
        if(userResult) {
            userResult.then(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
  • router中路由参数哪里来,是从app这里接收的,这里的req是可以通用的,比如这里定义了req.path = url.split("?")[0];那么其他地方只要接收到req参数,都可以通过req.path来拿到解析之后的路由;
  • getPostData用来解析post请求发送过来的比较多的json数据
  • res.setHeader('Content-type', 'application/json');用来定义返回的参数都都是json格式的数据
res.end(
                    JSON.stringify(userData)
                )

用来返回数据登录后返回的数据,当然也有可能是登录失败的数据

三、总结

  • 环境配置,先下载好依赖包,然后在package.json里面配置运行环境,用cross-env读出运行环境,用nodemon方便更新node代码,不然要重启才能看到效果
  • 在bin/www.js文件中创建服务并配置端口号
  • app.js中解析路由,解析post数据流,还有cookie的一些参数统一放到req中,方便其他函数中调用参数,最后通过res把解析后的json数据发送到给前端
  • 在conf/db.js文件中配置MySQL参数,根据不同的运行环境连接不同的数据库
  • 在db/mysql.js文件中连接数据,并且把sql语句传过去操作数据库最后返回操作完数据库后返回的数据库
  • 在model/resModel.js文件中设置操作成功和失败的返回结果,用errno来表示是否操作成功
  • 在controller文件中编写sql语句,传入到mysql.js中,最后返回数据库中的数据;每一类功能新建一个文件,比如user文件中可以写关于用户登录,管理员对用户表的增删改查sql语句。
  • 在router文件下定义路由,在这里决定是返回成功还是失败的操作(调用SuccessModel还是调用ErrorModel)。controller和r文件夹下的js文件一一对应
到这里为止项目就可以用了,这里并没有用到框架,和react和vue一样,其实nodejs也有脚手架工具方便我们开发后面我会介绍用express脚手架来开发接口,相比与这个,他会好开发很多,而这里的练习主要是让我更加了解nodejs以及服务端开发的模式。