浅浅聊一下基于koa2 0-1的node服务搭建

207 阅读6分钟

注:本文章浅浅聊一下基于Koa2 0-1手动搭建node服务,不使用脚手架,适用于快速上手

1.Koa2安装

# 项目初始化
npm init -y

# 安装koa2
npm i koa2 -S

2.入口文件

在项目根目录创建 app.js 文件,并在上一步操作中生成的 package.json 里配置:

{
  "scripts": {
    "test": "echo "Error: no test specified" && exit 1",
    "start": "node app.js"
  },
}

在 app.js 中:

const Koa = require('koa2');
const app = new Koa();
const port = 8000;

/* 
	解释下面这段代码:
	app.use()方法是:将给定的中间件方法添加到此应用程序。简单说就是调用中间件
	app.use() 返回 this, 因此可以链式表达
*/
app.use(async (ctx)=>{
    ctx.body = "Hello, Koa";
  	// ctx.body是ctx.response.body的简写
})

app.listen(port, ()=>{
    console.log('Server is running at http://localhost:'+port);
})

 

然后运行 npm start ,并在浏览器输入 http://localhost:8000/ 即可看到页面效果。

3.Koa模型(洋葱模型)

为了让人了解的更加明白,我百度借鉴了别人的图例 由于是快速上手,这里就浅浅解释一下,深入了解详见此篇文章

008eGmZEgy1gplrwjyczwj30da0c30w0.jpg

说到Koa的模型我们就要谈一下Koa 和 Express 的中间件执行顺序 Koa 和 Express 都会使用到中间件,Express的中间件是顺序执行,从第一个中间件执行到最后一个中间件,发出响应:

WeChat549393dd4551aa97cb7542470992da5e.png Koa是从第一个中间件开始执行,遇到 next 进入下一个中间件,一直执行到最后一个中间件,在逆序,执行上一个中间件 next 之后的代码,一直到第一个中间件执行结束才发出响应。

WeChat1a8680ff4302fa780f14eabd6d284a2e.png 代码列子:

const Koa = require('koa2');
const app = new Koa();
const port = 9000;

app.use(async (ctx, next)=>{
    console.log('输出1-1')
    await next();
    console.log('输出1-2'))
})

app.use(async (ctx, next)=>{
    console.log('输出2-1'))
    await next();
    console.log('输出2-2'))
})

app.use(async (ctx)=>{
    console.log('输出3-1'))
})

app.listen(port, ()=>{
    console.log('Server is running at http://localhost:'+port);
})

执行后打印出来的是的

输出1-1
输出2-1
输出3-1
输出2-2
输出1-2

显而易见,通过调用 next可以运行下个中间件,等中间件结束后,会再继续运行当前 next() 之后的代码

4.路由安装

当需要匹配不同路由时,可以安装:

npm i koa-router
const Koa = require('koa2');
const Router = require('koa-router');
const app = new Koa();
const router = new Router();
const port = 8000;

router.get('/', async (ctx)=>{
    ctx.body = "首页";
})

router.get('/login', async (ctx)=>{
    ctx.body = "登录页";
})


app.use(router.routes(), router.allowedMethods());

app.listen(port, ()=>{
    console.log('Server is running at http://localhost:'+port);
})

运行之后,可以在浏览器地址栏里访问/login 即可得到首页和登录页

//router.route()作用是启动路由
//router.allowedMethods()作用是允许任何请求`

5.路由拆分

当我们想把不同页面的子路由拆分处理,那就需要路由拆分,指在不同文件下的前端API请求

  • 创建 router 文件夹

创建router文件夹,并在其中创建:index.js (路由总入口文件)、login.js (登录总路由文件)、list.js (列表页总路由文件):

WeChatf39d85d0f37c7d535cc1f1a0e45dda67.png

//router入口文件
const Router = require("koa-router"); //引入路由
const router = new Router();
const list = require("./list");
const login = require("./login");


router.use("/list", list.routes(), list.allowedMethods());
router.use("/login", login.routes(), login.allowedMethods());
# list.js
const Router = require('koa-router');
const list = new Router();

这里的 '/' 就是指向 index.js 中的 /list

list.get('/', async (ctx)=>{
    ctx.body = "列表页";
})

#login.js也是如此

浏览器地址栏访问/login 与 /list 即可得到登录与列表页。

  • 路由重定向 我们可以在 router/index.js 中做如下配置:
一般用于token校验不通过跳转这里就不过多解释,前端同学经常遇到
router.redirect('/', '/login');
  • 404无效路由 当我们访问到无效路由,那么我们可以统一返回404页面 在 router 下 errPage.js :
const Router = require('koa-router');
const errorPage = new Router();

errorPage.get('/', async (ctx) => {
    ctx.body = "不存在页面";
})

module.exports = errorPage;

 
#app.js
app.use(async (ctx, next) => {
    await next();
    if (parseInt(ctx.status) === 404) {
        ctx.response.redirect("/404")
    }
})
  • 统一异常处理 实际开发中遇到异常响应,不可能每个接口都要做一次返回,所以我们难免会遇到相同返回的情况,以下就是代码,创建 utils/errorHandler.js :
const log = require("./log");
module.exports = (app) => {
    app.use(async (ctx, next) => {
        let status = 0;
        let fileName = "";
        try{
            await next();
            status = ctx.status;
        }catch(err){
            //console.log(err);
            status = 500;
        }
        if(status >= 400){
            switch(status){
                case 400:
                case 404:
                case 500:
                    fileName = status;
                    break;
                default:
                    fileName = "other";
                    break;
            }
        }
        ctx.response.status = status;
        ctx.body = {
            code:ctx.response.status,
            message:"非业务错误,请求失败的status错误状态值为==="+fileName+"接口为==="+ctx.request.url,
            data:{}
        }
        log(("非业务错误,请求失败的status错误状态值为==="+fileName+"接口为==="+ctx.request.url+'!!!'),'error')
        console.log("非业务错误,请求失败的status错误",fileName);
    });
}

注:此处我衔接了错误日志记录,暂时不用管,只需查看统一状体返回结构即可
#app.js

const errorHandler = require('./utils/errorHandler.js');
...
app.use(router.routes(), router.allowedMethods());
...
errorHandler(app);

6.接入mysql数据库

首先,项目内安装 mysql

yarn add mysql

在 utils 目录下创建一个 db.js 文件

var mysql = require('mysql')

var pool = mysql.createPool({
    host: 'xxxxxxx', // 连接的服务器(代码托管到线上后,需改为内网IP,而非外网)
    port: xxxx, // mysql服务运行的端口
    database: 'xxx', // 选择的库
    user: 'admin', // 用户名
    password: '123123' // 用户密码   
})

//对数据库进行增删改查操作的基础
function query(sql,callback){
    pool.getConnection(function(err,connection){
        connection.query(sql, function (err,rows) {
            callback(err,rows)
            connection.release()
        })
    })
}

exports.query = query;

在接口中调用如下:

const db = require('../utils/db.js');
  //访问数据
  let sql = `select * from article`;
  let resposeData = await new Promise((resolve, reject) => {
    return db.query(sql, (err, data) => {
      if (err) throw err;
      resolve(data);
    });
  });
  ctx.body = resposeData;
});

7.常见允许跨域

提供了插件
// 安装koa2-cors
npm i koa2-cors
#app.js

const cors = require("koa2-cors"); //跨域
...
app.use(cors()); //ors()中间件一定要写在路由之前,后断允许跨域
app.use(router.routes(), router.allowedMethods());
...

8.常见请求方式及文件读取依赖

  • post请求
安装koa-body依赖
#app.js
const koaBody = require("koa-body"); //读取前端传来的body
...
app.use(
  koaBody({
    multipart: true,
    jsonLimit: "50000mb",
    formLimit: "50000mb",
    textLimit: "50000mb",
    formidable: {
      maxFileSize: 200 * 1024 * 1024, // 设置上传文件大小最大限制,默认2M
    },
  })
);

例子展示:

login.post("/", async (ctx) => {
  let body = ctx.request.body;
});
  • 本地文件读取
// 安装koa-static
// 引入
#app.js
const path = require('path')
const static = require("koa-static"); //读取静态资源
...
app.use(static(path.join(__dirname + "/assets"))); //读取本地静态资源文件夹

例子:

例子是读取server-log下日志文件内容:
const fs = require("fs");
const path = require("path");
const log = require("./log");
function readFileFn(arg) {
  let myPath = path.join(__dirname, `../server-log/${arg}.log`);
  return new Promise((resolve, rejects) => {
    fs.readFile(myPath, (err, data) => {
      if (err) throw err;
      resolve(data.toString()); //读取的是二进制文件转换为字符串
    });
  });
}

9.练习登录的例子

  • 实际开发中,登录会遇到token的生成,有一个插件jsonwebtoken解决了这个问题
npm install jsonwebtoken --save

#login.js
const jwt = require("jsonwebtoken"); //生产token的东西

//在用户登录的路由中使用 jwt.sign 来生成token,一共三个参数,第一个是存入token的信息,第二个是token的钥匙,第三个是保存的时间,3600即一个小时,最后返回token:

let myToken = jwt.sign({ userName: userName, password: password }, "secret", {
    expiresIn: 3600 * 24,
  });
  
//jsonwebtoken提供了token的生成同时也提供了token的验证,三个参数第一个是要验证的token,第二个参数是加密的密文,第三个参数是回调函数
jwt.verify(ctx.headers["token"], "secret", function (err, decoded) {
    if (err) {
      console.log(err, "err");
    } else {
      console.log(decoded, "decoded");
    }
});

话不多说直接上例子:

#promise.js 封装的请求方法
const logs = require("./log");
const db = require("./db");
function errMsgFn(msg, ctx) {
  ctx.body = {
    code: -1,
    data: {},
    message: msg,
  };
}
let promiseFn = async (sql, ctx) => {
  let response = await new Promise((resolve, reject) => {
    try {
      return db.query(sql, (err, data) => {
        if (err) {
          reject(err);
        } else {
          resolve(data);
        }
      });
    } catch (error) {
      reject(error);
    }
  }).catch((err)=>{
    console.log("异常err====",err)
  });

  return response;
};

let responseCode = (data = {}, msg = "", code = "200",ctx) => {
  ctx.body =  {
    msg:msg,
    data:data,
    code: code,
  };
};

module.exports = {
  promiseFn,
  responseCode
}
#login.js

//写的一个简单的登录

const Router = require("koa-router"); //引入路由
const login = new Router();
const db = require("../utils/db");
const jwt = require("jsonwebtoken"); //生产token的东西
const { promiseFn, responseCode } = require("../utils/promise");
login.post("/", async (ctx) => {
  let body = ctx.request.body;
  if (JSON.stringify(body) == "{}") {
    responseCode({}, "用户信息不能为空", -1, ctx);
    return;
  }
  let userName = ctx.request.body.userName;
  let password = ctx.request.body.password;
  let myToken = jwt.sign({ userName: userName, password: password }, "secret", {
    expiresIn: 3600 * 24,
  });
  let sql = `select * from user where userName = '${userName}' and password = '${password}'`;
  let response = await promiseFn(sql, ctx);
  if (response.length > 0) {
    responseCode(
      {
        userName: response[0].userName,
        token: myToken,
      },
      "登录成功",
      200,
      ctx
    );
  } else {
    responseCode({}, "用户名或密码错误", -1, ctx);
  }
});

module.exports = login;
#app.js

app.use(async (ctx, next) => {
//临时写一个需要检测的路由路径
  let needChecked = ["/login", "/register","/log","/list/detail"];
  let flag = 0;
  if (!needChecked.includes(ctx.request.url)) {
    if (ctx.headers["token"]) {
      jwt.verify(ctx.headers["token"], "secret", function (err, decoded) {
        if (err) {
          console.log(err, "err");
          flag = 1;
        } else {
          console.log(decoded, "decoded");
        }
      });
      if (flag) {
        ctx.body = {
          code: -101,
          message: "token已过期,请重新登录",
          data: {},
        };
        return;
      }
    } else {
      ctx.body = {
        code: -101,
        message: "账号未登录,请先登录",
        data: {},
      };
      return;
    }
    //非登录注册需校验token
  }
  await next();
});

:以上所说的路由对于前端同学第一次接触node来说可以直接理解为接口的地址

声明:本人也是第一次学习使用koa2,对于写文档也是第一次,如果错误问题请大家及时指出,避免误导他人