注:本文章浅浅聊一下基于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模型(洋葱模型)
为了让人了解的更加明白,我百度借鉴了别人的图例 由于是快速上手,这里就浅浅解释一下,深入了解详见此篇文章
说到Koa的模型我们就要谈一下Koa 和 Express 的中间件执行顺序
Koa 和 Express 都会使用到中间件,Express的中间件是顺序执行,从第一个中间件执行到最后一个中间件,发出响应:
Koa是从第一个中间件开始执行,遇到
next 进入下一个中间件,一直执行到最后一个中间件,在逆序,执行上一个中间件 next 之后的代码,一直到第一个中间件执行结束才发出响应。
代码列子:
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 (列表页总路由文件):
//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,对于写文档也是第一次,如果错误问题请大家及时指出,避免误导他人