执行过程,洋葱模型

  1. const Koa = require('koa');
  2. const app = new Koa();
  3. app.use(async (ctx, next) => {
  4. console.log(1);
  5. await next()
  6. console.log(2);
  7. })
  8. app.use(async (ctx, next) => {
  9. console.log(3);
  10. await next()
  11. console.log(4);
  12. ctx.body = "hello koa2!!!"
  13. })
  14. app.use(ctx => {
  15. console.log(5);
  16. ctx.body = "hello koa3"
  17. })
  18. app.listen(3000)

打印结果 1 ,3, 5, 4, 2
image.pngimage.png

路由中间件

根据url匹配处理

处理不同的url

通过不同的url路径,跳转不同的页面

  1. app.use(async (ctx, next) => {
  2. if(ctx.url === '/'){
  3. ctx.body = "主页"
  4. }else if(ctx.url === "/users"){
  5. ctx.body = "用户列表"
  6. }else{
  7. ctx.status = 404;
  8. }
  9. })

处理不同的http请求方式

  1. if(ctx.url === "/users"){
  2. if(ctx.method === "GET"){
  3. ctx.body = "用户列表"
  4. }
  5. else if(ctx.method === "POST"){
  6. ctx.body = "创建用户"
  7. }
  8. else{
  9. // 不允许操作
  10. ctx.status = 405;
  11. }
  12. }

解析url上的参数

使用url.match()

  1. if(ctx.url.match(/\/users\/\w+/)){
  2. const userId = ctx.url.match(/\/users\/(\w+)/)[1];
  3. ctx.body = `用户${userId}`
  4. }

通过koa-router中间件

  1. const Router = require('koa-router')
  2. const router = new Router()
  3. router.get('/',(ctx) =>{
  4. ctx.body = "这是主页"
  5. })
  6. router.get('/users', (ctx) =>{
  7. ctx.body = "用户列表"
  8. })
  9. // 不同的http请求方法
  10. router.post('/users', (ctx) =>{
  11. ctx.body = "创建用户"
  12. })
  13. // 获取url路径上的参数
  14. router.get('/users/:id', (ctx) =>{
  15. ctx.body = `这是用户${ctx.params.id}`
  16. })
  17. app.use(router.routes())

路由添加前缀prefix

  1. // 添加前缀,可以分层级管理路由
  2. const userRouter = new Router({prefix: "/users"})
  3. userRouter.get('/', (ctx) =>{
  4. ctx.body = "用户列表"
  5. })
  6. // 不同的http请求方法
  7. userRouter.post('/', (ctx) =>{
  8. ctx.body = "创建用户"
  9. })
  10. // 获取url路径上的参数
  11. userRouter.get('/:id', (ctx) =>{
  12. ctx.body = `这是用户${ctx.params.id}`
  13. })
  14. app.use(userRouter.routes())

使用koa-bodyparser解析body内容

  1. const bodyparser = require("koa-bodyparser");
  2. // 注册到koa实例app上
  3. app.use(bodyparser());

重构路由结构

将以下文件重构放入到routes目录下;

把home和users抽离为单独路由

home.js代码

  1. const Router = require("koa-router");
  2. const router = new Router()
  3. router.get('/', (ctx)=>{
  4. ctx.body = "这里是主页"
  5. })
  6. module.exports = router;

users.js文件

  1. const Router = require("koa-router");
  2. const router = new Router({prefix:"/users"})
  3. router.get('/', (ctx) =>{
  4. ctx.body = "用户列表"
  5. })
  6. // 不同的http请求方法
  7. router.post('/', (ctx) =>{
  8. ctx.body = "创建用户"
  9. })
  10. // 获取url路径上的参数
  11. router.get('/:id', (ctx) =>{
  12. ctx.body = `这是用户${ctx.params.id}`
  13. })
  14. module.exports = router;

index文件收集所有路由文件统一处理

使用到node的fs模块,同步读取目录下的所有文件。将所有文件统一做处理导出。

  1. const fs = require('fs');
  2. const allRoutes = (app)=>{
  3. fs.readdirSync(__dirname).forEach(file=>{
  4. if(!file.includes("index")){
  5. const route = require(`./${file}`)
  6. app.use(route.routes()).use(route.allowedMethods())
  7. }
  8. })
  9. }
  10. module.exports = allRoutes;

然后在入口文件引入routes目录下的index文件

  1. const routes = require("./routes")
  2. routes(app);

控制器control

处理经过路由之后进入的页面,处理不同的业务逻辑。由于业务逻辑比较复杂,实际项目开发中,会把控制器单独抽离出来。
创建controllers目录

home控制器

  1. class HomeCtrl{
  2. index(ctx){
  3. ctx.body = "这是首页page"
  4. }
  5. }
  6. module.exports = new HomeCtrl();

user控制器

  1. class UsersCtrl{
  2. findUser(ctx){
  3. ctx.body = "用户列表"
  4. }
  5. findUserById(ctx){
  6. ctx.body = `这是用户${ctx.params.id}`
  7. }
  8. createUser(ctx){
  9. ctx.body = "创建用户"
  10. }
  11. }
  12. module.exports = new UsersCtrl();

错误处理

koa内部处理

  • 404,客户端请求路径错误
  • 412,获取特定数据不存在,即:先决条件失败412Precondition Failed
  • 500,服务器内部错误,运行时错误(不是语法错误)。

    1. findUserById(ctx){
    2. if(ctx.params.id >100){
    3. ctx.throw(412, "先决条件失败,查询数据不存在。")
    4. }
    5. ctx.body = `这是用户${ctx.params.id}`
    6. }

    编写中间件处理错误

    由于koa内部处理错误返回的数据不是json格式,需要通过中间件处理。

    1. app.use(async (ctx,next) => {
    2. try {
    3. await next();
    4. }catch (err){
    5. ctx.status = err.status || err.statusCode ||500;
    6. ctx.body = {
    7. message: err.message
    8. };
    9. }
    10. })

    错误处理第三方中间件

  • koa-json-error : 处理报错信息为json,生产环境设置取消stack的打印

  • koa-parameter :校验参数

    用户认证JWT

    JWT介绍

    JSON WEB TOKEN的简写,定义了将各方之间的信息作为json对象进行安全传输,该信息可以验证和信任,信息都是经过数字签名的。
    JWT的构成:头部header、有效载荷payload、签名signature。
    image.png
    红色为header,紫色为payload,青色为signature
    https://www.jianshu.com/p/576dbf44b2ae

    jwt的头部header承载两部分信息

  • 声明类型,这里是jwt

  • 声明加密的算法 通常直接使用 HMAC SHA256

完整的头部就像下面这样的JSON:

  1. {
  2. 'typ': 'JWT',
  3. 'alg': 'HS256'
  4. }

将头部进行base64加密,构成了第一部分header

  1. eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

payload载荷

载荷就是存放有效信息的地方。有效信息包含三个部分

  • 标准中注册的声明
  • 公共的声明
  • 私有的声明

定义一个payload:

  1. {
  2. "sub": "1234567890",
  3. "name": "John Doe",
  4. "admin": true
  5. }

然后将其进行base64加密,得到Jwt的第二部分。

  1. eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

signature算法

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

  • header (base64后的)
  • payload (base64后的)
  • secret

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

  1. var signature = HMACSHA256( base64UrlEncode(header) + '.' + base64UrlEncode(payload)
  2. , 'secret');

将这三部分用 . 连接成一个完整的字符串,构成了最终的jwt:

  1. eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt。

node中使用JWT

  • 安装jsonwebtoken
  • 签名
  • 验证
    1. const jwt =require("jsonwebtoken")
    2. const token = jwt.sign({name:"sam"}, "secret")
    3. jwt.verify(token, "secret")

    用jwt实现用户注册和登录

    1:当用户登录时,生产token信息,并返回给前端

    1. // index首页,使用koa-parameter对客户端输入的数据进行校验
    2. const parameter = require('koa-parameter');
    3. parameter(app);
    1. const jsonwebtoken = require("jsonwebtoken");
    2. async login(ctx){
    3. // ctx.request.body name和password必须填写,parameter对参数进行的校验
    4. ctx.verifyParams({
    5. name: {type: "string", required: true},
    6. password: {type: "string", required: true}
    7. })
    8. // 查询输入的用户是否存在
    9. const user = await User.findOne(ctx.request.body);
    10. if(!user) {ctx.throw(401, '用户名或密码有误');}
    11. //解析出用户名和_id信息
    12. const {_id, name} = user;
    13. // 设置jsonwebtoken,使用sign签名,第一个参数要校验的数据,第二个secret密码,第三有效时长
    14. const token = jsonwebtoken.sign({_id, name}, "secret_zidingyi_y8e42938", {expiresIn:"1d"});
    15. // 将生成后的token返回前端,前端以后进行接口请求时,需要携带此信息。
    16. ctx.body = {token}
    17. }

    2:编写一个认证的中间件

    认证的作用,用户进行相关操作时进行的身份认证,看当前作用的身份是否正确,是否能调用当前接口。
    1. // 自定义认证中间件
    2. const auth = async (ctx, next) =>{
    3. const {authorization = ""} = ctx.request.header;
    4. const token = authorization.replace("Bearer ", "")
    5. try{
    6. const user = jsonwebtoken.verify(token, "secret_zidingyi_y8e42938");
    7. // 将用户信息存储
    8. ctx.state.user = user;
    9. }catch(err){
    10. ctx.throw(401, err.message)
    11. }
    12. await next();
    13. }
    通常需要对用户的更新进行认证操作,如果登录的身份和更新的身份不一致,则不能进行更新操作。
    1. // 更新用户信息,新进行认证操作,成功后在进行更新操作
    2. router.put('/:id', auth, userCtrl.updateUser)

    3:授权中间件,判断登录用户的权限

    上述第二步中,如果登录的认证可以通过,如果需要更新其他用户的数据,就需要权限设置,如果登录用户是管理员,权限比较大,就可以更新别的用户信息,此时用到认证后的授权操作。
    示例演示,仅设置最简单的权限,通过判断登录id和token的id是否一致。
    1. // 授权,设置权限
    2. // 检查是否有操作数据的权限
    3. const checkOwner=async (ctx, next)=>{
    4. if(ctx.params.id !== ctx.state.user._id){
    5. ctx.throw(403, '没有权限')
    6. }
    7. await next();
    8. }

    使用koa-jwt

    上面使用了自定义的中间件,也可以使用现成的中间件完成上述过程。 ```javascript const jwt = require(“koa-jwt”);

// 设置jwt的认证,koa-jwt可以简化代码 const jwtAuth = jwt({secret:”secret_zidingyi_y8e42938”}) // 更新用户信息,先进行认证,在进行授权 router.put(‘/:id’, jwtAuth, checkOwner, userCtrl.updateUser)

  1. <a name="HJE83"></a>
  2. ## 图片上传及设置静态路径
  3. <a name="FSVPb"></a>
  4. ### koa-body中间件设置上传
  5. koa-body可以解析上传的文件,也可以解析form及json格式。
  6. ```javascript
  7. const koaBody = require('koa-body')
  8. // 使用koa-body替换bodyParser,koa-body可以解析文件格式
  9. app.use(koaBody({
  10. multipart: true,
  11. formidable:{
  12. uploadDir: path.join(__dirname, "/public/uploads"), //设置上传路径
  13. keepExtensions: true, // 保留扩展名
  14. }
  15. }))

koa-static设置静态目录

可以将目录设置为通过路径就能访问的静态目录

  1. // 设置静态目录z中间件
  2. const koaStatic = require('koa-static');
  3. // 设置静态目录
  4. app.use(koaStatic(path.join(__dirname, '/public')))

修改上传接口返回的路径

  1. upload(ctx){
  2. const file = ctx.request.files.file;
  3. const basename = path.basename(file.path)
  4. ctx.body = {
  5. url: `${ctx.origin}/uploads/${basename}`
  6. }
  7. }

使用mongoose连接数据库

使用mongoose的connect连接数据库

  1. const mongoose = require('mongoose')
  2. const connectionPath="mongodb://test:123456@0.0.0.0:27017/test"
  3. mongoose.connect(connectionPath, { useNewUrlParser: true, useUnifiedTopology: true}, () =>{
  4. console.log('connect success')
  5. })
  6. mongoose.connection.on('error',(err)=>{
  7. console.error("link error",err)
  8. })

定义schema

定义user的模型model

  1. const userSchema = new Schema({
  2. "__v": {
  3. type: Number,
  4. // select:false,查询的时候可以不显示字段
  5. select: false
  6. },
  7. "name": {
  8. type: String,
  9. required: true
  10. },
  11. "password": {
  12. type: String,
  13. required: true,
  14. select: false
  15. },
  16. "gender": {
  17. type: String,
  18. // 可以枚举的类型
  19. enum: ['male', 'female'],
  20. // 默认的类型
  21. default: 'male',
  22. // 必填项
  23. required: true
  24. },
  25. // 兴趣爱好,是个字符串类型的数组
  26. "hobbies": {
  27. type: [{
  28. type: String,
  29. select: false
  30. }]
  31. },
  32. "employments": {
  33. // 工作信息,是个包含多个信息对象的数组列表,包含公司/职业
  34. type: [{
  35. company: {
  36. type: String
  37. },
  38. job: {
  39. type: String
  40. }
  41. }],
  42. select: false
  43. },
  44. // 粉丝列表
  45. following:{
  46. // 引用,设置关联到用户,使用了mongoose的数据类型,schema.Types.ObjectId
  47. // ref,设置关联,在控制器中,可以使用populate()查询完整对象
  48. type: [{ type: Schema.Types.ObjectId, ref: "User"}],
  49. select: false
  50. }
  51. })

控制器

  1. // 查询关注者列表
  2. async followingList(ctx) {
  3. //使用populate方法,需要在定义schema时设置ref关联对象。如果不设置populate,查询处理的只是id
  4. const user = await User.findById(ctx.params.id).select("+following").populate("following")
  5. if(!user){
  6. ctx.throw(404)
  7. }else{
  8. ctx.body = user.following;
  9. }
  10. }