课程目标
- 编写RESTful API
- 文件上传
- 表单校验
- 图形验证码
- 发送短信
- 案例:用户注册
- 掌握Koa中编写Restful风格API
- 掌握Koa中文件上传、表单验证、图形验证码、发送短信等常见任务
编写RESTful API
-
方法设计 PUT和DELETE ngnix可能会有问题
- GET:读取(Read)
- POST:新建(Create)
- PUT:更新(Update)
- PATCH:更新(Update),部分更新
- DELETE:删除(Delete)
-
对象
- GET /users(通常用复数避免多级)
- GET /users/1 (一样是使用复数)
-
状态码
- 1xx :相关信息
- 2xx :操作成功
- 3xx :重定向
- 4xx :客户端错误
- 5xx :服务器错误
-
返回
//推荐 HTTP/1.1 400 Bad Request Content-Type: application/json { "error": "不合法的附件", "detail": { "uname": "用户名为必填项" } }
-
解决跨域:
npm i koa2-cors
var Koa = require('koa'); var cors = require('koa2-cors'); var app = new Koa(); app.use(cors());
文件上传
- 安装koa-multer: npm i koa-multer -S
-
配置:./routes/users.js
const upload = require("koa-multer")({ dest: "./public/images" }); router.post("/upload", upload.single("file"), ctx => { console.log(ctx.req.file); // 注意数据存储在原始请求中 console.log(ctx.req.body); // 注意数据存储在原始请求中 ctx.body = "上传成功"; });
-
调用接口,./public/upload-avatar.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> <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css" /> <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> <title>文件上传</title> </head> <body> <div id="app"> <!-- ajax方式上传 --> <el-upload class="avatar-uploader" action="/users/upload" :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> var app = new Vue({ el: "#app", data() { return { imageUrl: "" }; }, methods: { handleAvatarSuccess(res, file) { this.$message.success('上传头像成功') 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>
表单校验
- 安装koa-bouncer: npm i -S koa-bouncer
-
配置:app.js
// 为koa上下文扩展一些校验方法 app.use(bouncer.middleware());
-
基本使用:user.js
router.post("/", ctx => { try { // 校验开始 ctx .validateBody("uname") .required("要求提供用户名") .isString() .trim() .isLength(6, 16, "用户名长度为6~16位"); // ctx.validateBody('email') // .optional() // .isString() // .trim() // .isEmail('非法的邮箱格式') ctx .validateBody("pwd1") .required("密码为必填项") .isString() .isLength(6, 16, "密码必须为6~16位字符"); ctx .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` 对象 console.log(ctx.vals); console.log("POST /users"); // const { body: user } = ctx.request; // 请求body const user = ctx.vals; user.id = users.length + 1; users.push(user); ctx.body = { ok: 1 }; } catch (error) { if (error instanceof bouncer.ValidationError) { ctx.body = '校验失败:'+error.message; return; } throw error } });
图形验证码(就为了拿个图片?)
- 安装trek-captcha: npm i trek-captcha -S
-
使用:./routes/api.js
const captcha = require("trek-captcha"); router.get("/captcha", async ctx => { const { token, buffer } = await captcha({ size: 4 }); ctx.body = buffer; });
-
图片显示,upload-avatar.html
<!-- 验证码 --> <img src="/api/captcha" id="captcha" /> <script> document.getElementById('captcha').onclick = function() { captcha.src = "/users/captcha?r=" + Date.now(); }; </script>
发送短信
- 秒滴短信API
- 安装依赖: npm i -S moment md5 axios
-
接口编写,./routes/api.js
router.get("/sms", async function(ctx) { // 生成6位随机数字验证码 let code = ran(6); // 构造参数 const to = ctx.query.to; // 目标手机号码 const accountSid = "3324eab4c1cd456e8cc7246176def24f"; // 账号id const authToken = "b1c4983e2d8e45b9806aeb0a634d79b1"; // 令牌 const templateid = "613227680"; // 短信内容模板id const param = `${code},1`; // 短信参数 const timestamp = moment().format("YYYYMMDDHHmmss"); const sig = md5(accountSid + authToken + timestamp); // 签名 try { // 发送post请求 const resp = await axios.post( "https://api.miaodiyun.com/20150822/industrySMS/sendSMS", qs.stringify({ to, accountSid, timestamp, sig, templateid, param }), { headers: { "Content-Type": "application/x-www-form-urlencoded" } } ); if (resp.data.respCode === "00000") { // 短信发送成功,存储验证码到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.respDesc} } } catch (e) { ctx.body = {ok:0, message: e.message} } });
案例:用户注册
-
前端页面,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> var app = new Vue({ el: "#app", data() { return { regForm: { phone: "", captcha: "", code: "", password: "" }, captchaSrc: "/api/captcha" }; }, methods: { getCaptcha() { this.captchaSrc = "/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("/students", this.regForm) .then(() => alert("注册成功")) .catch(error => alert("注册失败:" + error.response.data.message)); } } }); </script> </body> </html>
- 注册接口编写,./routes/students.js
const Router = require("koa-router");
const router = new Router({ prefix: "/students" });
const bouncer = require("koa-bouncer");
router.post("/", async ctx => {
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, "验证码已过期");
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 };
}
});
module.exports = router;
koa2 中间件
-
路由相关
-
'koa-bodyparser' 【post解析】
app.use(bodyParser({ extendTypes:\['json','form','text'\] }))
-
'koa-router' 【路由】
import Router from 'koa-router'; import axios from './utils/axios' import Cart from '../dbs/models/cart' import md5 from 'crypto-js/md5' //加密 let router \= new Router({prefix: '/cart'}) router.post('/getCart', async ctx \=> { let {id} \= ctx.request.body console.log(id); try { let result \= await Cart.findOne({cartNo: id}) ctx.body \= { code: 0, data: result ? result.detail\[0\] : {} } } catch (e) { ctx.body \= { code: -1, data: {} } } })
-
'koa-multer' 【文件上传】
router.post("/upload", upload.single("file"), ctx \=> { console.log(ctx.req.file); // 注意数据存储在原始请求中 console.log(ctx.req.body); // 注意数据存储在原始请求中 ctx.body \= "上传成功"; });
-
'koa2-cors' 【跨域】
// 跨域 var cors \= require('koa2-cors'); app.use(cors());
-
'koa-bouncer' 【校验】
//配置 const bouncer \= require("koa-bouncer"); app.use(bouncer.middleware()); //使用 router.post("/", ctx => { try { // 校验开始 ctx .validateBody("uname") .required("要求提供用户名") .isString() .trim() .isLength(6, 16, "用户名长度为6~16位"); // ctx.validateBody('email') // .optional() // .isString() // .trim() // .isEmail('非法的邮箱格式') ctx .validateBody("pwd1") .required("密码为必填项") .isString() .isLength(6, 16, "密码必须为6~16位字符"); ctx .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` 对象 console.log(ctx.vals); console.log("POST /users"); // const { body: user } = ctx.request; // 请求body const user = ctx.vals; user.id = users.length + 1; users.push(user); ctx.body = { ok: 1 }; } catch (error) { if (error instanceof bouncer.ValidationError) { ctx.body = '校验失败:'+error.message; return; } throw error } });
-
-
数据库相关
-
'koa-generic-session' 【登录状态鉴权】
app.use(session({key: 'mt', prefix: 'mt:uid', store: new Redis()}))
-
'koa-session' 【登录状态鉴权】
app.keys \= \['some secret'\]; //设置秘钥 // 配置项 const SESS\_CONFIG \= { key: 'kkb:sess', // cookie键名 maxAge: 86400000, // 有效期,默认一天 httpOnly: true, // 仅服务器修改 signed: true, // 签名cookie }; // 注册 ~~~~ app.use(session(SESS\_CONFIG, app));
-
'koa-redis' 【redis】
app.use(session({key: 'mt', prefix: 'mt:uid', store: new Redis()}))
-
'mongoose' 【面向对象操作数据库】
//连接 mongoose.connect(dbConfig.dbs,{ useCreateIndex: true, useNewUrlParser:true }) //操作 import mongoose from 'mongoose' const Schema \= mongoose.Schema const Cart \= new Schema({ id: { type: String, require: true }, detail: { type: Array, require: true }, cartNo: { type: String, require: true }, user: { type: String, require: true }, time: { type: String, require: true } }) export default mongoose.model('Cart', Cart)
-
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。