# Koa实战 - Restful API - 及常见任务
# 目标
# 编写RESTful API
Respresentational State Transfer翻译过来就是“表现层状态转化”,是一种互联网软件的架构原则,因此符合REST风格的Web API设计,就成它为RESTful API
RESTful特征:
- 每一个URL代表一种资源(Resources),比如:http:localhost:8000/courses
- 客户端与服务器之间,传递这种资源的某种表现层,比如:localhost:8000/courses/web
- 客户端通过HTTP动词,对服务端资源进行操作,实现“表现层状态的转化”
![image-20191210175705528](/Users/shangjiawei/Library/Application Support/typora-user-images/image-20191210175705528.png)
// restful.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>restful</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<meta http-equiv="Access-Control-Allow-Origin" content="*" />
<link
rel="stylesheet"
href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"
/>
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
</head>
<body>
<div id="app">
<div style="display:flex;flex-direction:column">
<el-input
v-model="form.name"
placeholder="姓名"
autocomplete="off"
></el-input>
<el-button @click="get">GET</el-button>
<el-button @click="post">POST</el-button>
<el-button @click="del">DELETE</el-button>
<el-button @click="put">PUT</el-button>
<el-button @click="logs=[]">clearLog</el-button>
</div>
<!--日志-->
<ul>
<li v-for="(log,idx) in logs" :key="idx">
{{ log }}
</li>
</ul>
</div>
<script>
axios.defaults.baseURL = "http://localhost:3001/api";
axios.interceptors.response.use(
response => {
app.logs.push(JSON.stringify(response.data));
return response;
},
err => {
app.log.push(JSON.stringify(response.data));
return Promise.reject(err);
}
);
</script>
<script>
const app = new Vue({
el: "#app",
data: {
form: {
name: ""
},
logs: []
},
created() {},
methods: {
async post() {
let res = await axios.post("/member", this.form);
},
get: async function() {
let res = await axios.get("/member?name=" + this.form.name);
},
async put() {
await axios.put("/member/2", this.form);
},
async del() {
let res = await axios.delete("/member/" + this.form.name);
}
},
mounted: function() {}
});
</script>
</body>
</html>
// member.js
const Router = require("koa-router");
const router = new Router({ prefix: "/api/member" });
const mongoose = require("mongoose");
const schema = mongoose.Schema({
name: String
});
const Model = mongoose.model("member", schema);
router.get("/", async ctx => {
const { name } = ctx.query;
let result = name ? await Model.find({ name: name }) : await Model.find();
ctx.body = {
ok: 1,
data: result
};
});
router.post("/", async ctx => {
const { name } = ctx.request.body;
const r = await Model.create({
name: name
});
if (r._id) {
ctx.body = {
ok: 1,
data: "插入成功"
};
} else {
ctx.body = {
ok: 1,
data: "插入失败"
};
}
});
router.put("/:id", async ctx => {
const { name } = ctx.request.body;
const id = ctx.params;
const name1 = await Model.find();
const r = await Model.updateOne(
{ name: name1[0].name },
{ $set: { name: name } }
);
if (r.nModified) {
ctx.body = {
ok: 1,
data: "修改成功"
};
} else {
ctx.body = {
ok: 1,
data: "修改失败"
};
}
});
router.delete("/:name", async ctx => {
const { name } = ctx.params;
const r = await Model.deleteOne({ name: name });
if (r.deletedCount) {
ctx.body = {
ok: 1,
data: "删除成功"
};
} else {
ctx.body = {
ok: 0,
data: "删除失败"
};
}
// const idx = users.findIndex(el=>el.id===id)
// if(idx>-1){ // 修改
// users[idx] = name
// }
// if(idx>-1){ // 删除
// users.splice(idx,1)
// }
});
module.exports = router;
# 解决跨域
npm i koa2-cors
const cors = require('koa2-cors')
app.use(cors())
//替代
// app.all('*', function (req, res, next) {
// res.header('Access-Control-Allow-Origin', '*');
// res.header('Access-Control-Allow-Headers', 'Content-Type');
// res.header('Access-Control-Allow-Methods', '*');
// res.header('Content-Type', 'application/json;charset=utf-8');
// res.header('Access-Control-Allow-Credentials','true');
// next();
// });
![image-20191210181954075](/Users/shangjiawei/Library/Application Support/typora-user-images/image-20191210181954075.png)
# 上传文件
npm i koa-multer
// member.js
const upload = require('koa-multer')({dest:'./public/images'})
router.post('./upload',upload.single('file'),ctx => {
console.log('file',ctx.req.file)
console.log('body',ctx.req.body)
// 地址写入数据库 imageUrl: "http://localhost:3001/images/1fbf4d9c284d8da69e6a648a55e3bf85"
ctx.body = '上传成功'
})
// upload.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>upload</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<link
rel="stylesheet"
href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"
/>
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<meta http-equiv="Access-Control-Allow-Origin" content="*" />
</head>
<style>
.avatar-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader .el-upload:hover {
border-color: #409eff;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
line-height: 178px;
text-align: center;
}
.avatar {
width: 178px;
height: 178px;
display: block;
}
</style>
<body>
<div id="app">
<el-upload
class="avatar-uploader"
action="http://localhost:3001/api/member/posts"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
>
<img v-if="imageUrl" :src="imageUrl" class="avatar" />
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
</div>
<script>
// axios.defaults.baseURL = "http://localhost:3001";
</script>
<script>
const app = new Vue({
el: "#app",
data: {
imageUrl: "http://localhost:3001/images/1fbf4d9c284d8da69e6a648a55e3bf85"
},
methods: {
handleAvatarSuccess(res, file) {
this.imageUrl = URL.createObjectURL(file.raw);
},
beforeAvatarUpload(file) {
const isJPG = file.type === "image/jpeg";
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isJPG) {
this.$message.error("上传头像图片只能是 JPG 格式!");
}
if (!isLt2M) {
this.$message.error("上传头像图片大小不能超过 2MB!");
}
return isJPG && isLt2M;
}
}
});
</script>
</body>
</html>
http://localhost:3001/upload.html
可以通过设置limits、fileFilter、storage等限制文件大小、存储目录和文件名等。
# 表单校验
安装
npm i -S Koa-bouncer
配置:app.js
// index.js // 为koa上下文扩展一些校验方法 const bouncer = require("koa-bouncer"); app.use(bouncer.middleware());
基本使用 member.js
// 一个校验中间件 const bouncer = require("koa-bouncer"); const vali = async (ctx, next) => { const isUser = name => Promise.resolve(name === "abc"); const checkName = name => Model.findOne({ $where: `this.name =='${name}'` }).exec(); // console.log(checkName,'checkName') try { ctx .validateBody("name") .required("要求提供用户名") .isLength(1, 16, "用户长度应该为1-16") .isString() // 变为字符串 才可以用trim() .trim() // 修改后的结果会存在ctx.vals里 .check((await checkName(ctx.vals.name)) == null, "用户名已存在"); // .check(await isUser(ctx.vals.name) , "Check 不 Ok") console.log(ctx.vals, "vals"); // ctx.validateBody('email') // .optional() // .isString() // .trim() // .isEmail('非法的邮箱格式') // .validateBody("pwd2") .required("密码确认为必填项") .isString() // .eq(ctx.vals.pwd1, "两次密码不一致"); // 校验数据库是否存在相同值 // ctx.validateBody('uname') // .check(await db.findUserByUname(ctx.vals.uname), 'Username taken') ctx.validateBody("uname").check("jerry", "用户名已存在"); // 如果走到这里校验通过 // 校验器会用净化后的值填充 `ctx.vals` 对象 await next(); } catch (error) { if (error instanceof bouncer.ValidationError) { ctx.body = { msg: `校验错误${error.message}` }; return; } throw error; } };
# 图形验证码
安装
trek-captcha: npm i trek-captcha -S
使用route/api.js
const Router = require("koa-router"); const router = new Router({ prefix: "/api" }); // 图形验证码 const captcha = require("trek-captcha"); router.get("/captcha", async ctx => { console.log("captcha:", ctx.session.captcha); const { token, buffer } = await captcha({ size: 4 }); ctx.session.captcha = token; ctx.body = buffer; }); router.post("/submit", async ctx => { const { val } = ctx.request.body; const { captcha } = ctx.session; if (val === captcha) { ctx.body = "登录成功"; } else { ctx.body = "登录失败"; } }); module.exports = router;
// Upload.html <img src="http://localhost:3001/api/captcha" id="captcha" /> <input type="text" id="inp" /> <button onclick="sumbit()">提交</button> <span id="show" style="color:#f00"></span> <script> // axios.defaults.baseURL = "http://localhost:3001"; </script> <script> document.getElementById("captcha").onclick = function() { captcha.src = "http://localhost:3001/api/captcha?r=" + Date.now(); }; function sumbit() { const val = document.getElementById("inp").value; axios.post("/api/submit", { val: val }).then(res => { document.getElementById("show").innerText = res.data; }); } </script>
![image-20191212172807145](/Users/shangjiawei/Library/Application Support/typora-user-images/image-20191212172807145.png)
# 发送短信
赛邮
安装依赖: npm i -S moment md5 axios
接口编写,./routes/api.js
const moment = require("moment"); const md5 = require("md5"); const axios = require("axios"); const qs = require("querystring"); const bouncer = require("koa-bouncer"); // 短信验证码 router.get("/sms", async function(ctx) { console.log(ctx.query.to, "ctx.query.to"); // 生成6位随机数字验证码 let code = (Math.random() * 999999).toFixed(); const to = ctx.query.to; // 目标手机号码 // const appid = "44404"; // 账号id // const signature = "73174bb1c9b510580187235769dd0757"; // const project = "5HSjJ2"; const appid = "*****"; // 账号id const signature = "*****"; const project = "*****"; const vars = { code: code, time: "1分钟" }; try { // 发送post请求 const resp = await axios.post( "https://api.mysubmail.com/message/xsend.json", // 赛邮 qs.stringify({ to, appid, signature, project, vars: JSON.stringify(vars) }), { headers: { "Content-Type": "application/x-www-form-urlencoded" } } ); console.log(resp, "resp"); if (resp.data.status === "success") { // 短信发送成功,存储验证码到session,过期时间1分钟 const expires = moment() .add(1, "minutes") .toDate(); ctx.session.smsCode = { to, code, expires }; ctx.body = { ok: 1 }; } else { ctx.body = { ok: 0, message: resp.data.msg }; } } catch (e) { ctx.body = { ok: 0, message: e.message }; } });
// Upload.html <span id="show" style="color:#f00"></span> <!-- 短信验证码 --> <input type="text" id="phone" placeholder="请输入手机号"/> <button onclick="sms()" >发送验证码</button> <script> // 发送验证码 function sms() { const phone = document.getElementById("phone").value; axios.get("/api/sms?to="+phone).then(res => { }); } </script>
# 完成注册
http://localhost:3001/register.html
![image-20191213145556822](/Users/shangjiawei/Library/Application Support/typora-user-images/image-20191213145556822.png)
// register.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<link
rel="stylesheet"
href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"
/>
<style></style>
<title>文件上传</title>
</head>
<body>
<div id="app">
<el-form :model="regForm" ref="regForm">
<el-form-item>
<el-input
type="tel"
v-model="regForm.phone"
autocomplete="off"
placeholder="手机号"
></el-input>
</el-form-item>
<el-form-item>
<el-input
type="text"
v-model="regForm.captcha"
autocomplete="off"
placeholder="图形验证码"
></el-input>
<img :src="captchaSrc" @click="getCaptcha" />
</el-form-item>
<el-form-item>
<el-input
type="text"
v-model="regForm.code"
autocomplete="off"
placeholder="短信验证码"
></el-input>
<el-button type="primary" @click="getSmsCode()"
>获取短信验证码
</el-button>
</el-form-item>
<el-form-item>
<el-input
type="password"
v-model="regForm.password"
autocomplete="off"
></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm()">提交</el-button>
</el-form-item>
</el-form>
</div>
<script>
axios.defaults.baseURL = "http://localhost:3001";
var app = new Vue({
el: "#app",
data() {
return {
regForm: {
phone: "",
captcha: "",
code: "",
password: ""
},
captchaSrc: "http://localhost:3001/api/captcha"
};
},
methods: {
getCaptcha() {
this.captchaSrc = "http://localhost:3001/api/captcha?r=" + Date.now();
},
getSmsCode() {
axios
.get("/api/sms?to=" + this.regForm.phone)
.then(res => res.data)
.then(({ code }) => (this.regForm.code = code));
},
submitForm() {
axios
.post("/api/register", this.regForm)
.then(() => alert("注册成功"))
.catch(error =>
alert("注册失败:" + error.response.data.message)
);
}
}
});
</script>
</body>
</html>
//api.js
const Router = require("koa-router");
const router = new Router({ prefix: "/api" }); // 图形验证码
const captcha = require("trek-captcha");
router.get("/captcha", async ctx => {
console.log("captcha:", ctx.session.captcha);
const { token, buffer } = await captcha({ size: 4 });
ctx.session.captcha = token;
ctx.body = buffer;
});
router.post("/submit", async ctx => {
const { val } = ctx.request.body;
const { captcha } = ctx.session;
if (val === captcha) {
ctx.body = "登录成功";
} else {
ctx.body = "登录失败";
}
});
const moment = require("moment");
const md5 = require("md5");
const axios = require("axios");
const qs = require("querystring");
const bouncer = require("koa-bouncer");
// 短信验证码
router.get("/sms", async function(ctx) {
console.log(ctx.query.to, "ctx.query.to");
// 生成6位随机数字验证码
let code = (Math.random() * 999999).toFixed();
const to = ctx.query.to; // 目标手机号码
// const appid = "44404"; // 账号id
// const signature = "73174bb1c9b510580187235769dd0757";
// const project = "5HSjJ2";
const appid = "*****"; // 账号id
const signature = "*****";
const project = "*****";
const vars = { code: code, time: "1分钟" };
try {
// 发送post请求
const resp = await axios.post(
"https://api.mysubmail.com/message/xsend.json", // 赛邮
qs.stringify({
to,
appid,
signature,
project,
vars: JSON.stringify(vars)
}),
{ headers: { "Content-Type": "application/x-www-form-urlencoded" } }
);
console.log(resp, "resp");
if (resp.data.status === "success") {
// 短信发送成功,存储验证码到session,过期时间1分钟
const expires = moment()
.add(1, "minutes")
.toDate();
ctx.session.smsCode = { to, code, expires };
ctx.body = { ok: 1 };
} else {
ctx.body = { ok: 0, message: resp.data.msg };
}
} catch (e) {
ctx.body = { ok: 0, message: e.message };
}
});
// 注册登录
router.post("/register", async ctx => {
console.log(ctx.request.body.code, "co");
try {
// 输入验证
const { code, to, expires } = ctx.session.smsCode;
ctx
.validateBody("phone")
.required("必须提供手机号")
.isString()
.trim()
.match(/1[3-9]\d{9}/, "手机号不合法")
.eq(to, "请填写接收短信的手机号");
ctx
.validateBody("code")
.required("必须提供短信验证码")
.isString()
.trim()
.isLength(6, 6, "必须是6位验证码")
.eq(code, "验证码填写有误")
.checkPred(() => new Date() - new Date(expires) < 0, "验证码已过期"); // true通过 false错误
ctx
.validateBody("password")
.required("必须提供密码")
.isString()
.trim()
.match(/[a-zA-Z0-9]{6,16}/, "密码不合法");
// 入库, 略
ctx.body = { ok: 1 };
} catch (error) {
if (error instanceof bouncer.ValidationError) {
console.log(error);
ctx.status = 401;
} else {
ctx.status = 500;
}
ctx.body = { ok: 0, message: error.message };
}
});
// 验证吗登录
router.post("/smslogin", async ctx => {
const { code, phone } = ctx.request.body;
const { smsCode } = ctx.session;
if (smsCode.to === phone && smsCode.code === code) {
ctx.body = "登录成功";
} else {
ctx.body = "登录失败";
}
});
module.exports = router;
← Koa实战 - 鉴权 Node 大纲 →