2

课程目标

  • 编写RESTful API
  • 文件上传
  • 表单校验
  • 图形验证码
  • 发送短信
  • 案例:用户注册
  1. 掌握Koa中编写Restful风格API
  2. 掌握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());

    参考文档:理解resful架构、 resful API最佳实践

文件上传

  • 安装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)

曦沙飞舞
673 声望15 粉丝

爱前端,爱生活