大前端 - nodejs - egg.js 企业级框架实战 - eggJS综合案例

案例介绍

  • 模仿实现 youTube clone项目。
  • 在线体验地址:https://utubeclone.netlify.app
  • 后端:https://github.com/manikandanraji/youtubeclone-frontend
  • 客户端:https://github.com/manikandanraji/youtubeclone-backend

前后端架构分离

  • 先做服务接口,再做客户端应用

  • 后端技术选型:
    web框架:eggjs
    数据库:MongoDB
    ORM框架:mongoose
    身份认证:JWT

  • 客户端选型:vue3系列技术栈

接口设计

https://www.yuque.com/books/share/6eb0a508-d745-4e75-8631-8eb127b7b7ca?#

使用 Yapi 管理接口

项目初始化

npm i create-egg -g

create-egg youtube-clone-eggjs (smaple)

cd youtube-clone-eggjs

npm install

npm run dev

初始化mongoose配置

npm i egg-mongoose --save

/config/config.default.js

/* eslint valid-jsdoc: "off" */

'use strict';

/**
 * @param {Egg.EggAppInfo} appInfo app info
 */
module.exports = appInfo => {
  /**
   * built-in config
   * @type {Egg.EggAppConfig}
   **/
  const config = exports = {};

  // use for cookie sign key, should change to your own and keep security
  config.keys = appInfo.name + '_1649334278876_9943';

  // add your middleware config here
  config.middleware = [];

  // add your user config here
  const userConfig = {
    // myAppName: 'egg',
  };

  // 加入mongoose
+  config.mongoose = {
    client: {
      url: 'mongodb://127.0.0.1/youtube-clone',
      options: {},
      plugins: [],
    },
  };

  return {
    ...config,
    ...userConfig,
  };
};

/config/plugin.js

'use strict';

/** @type Egg.EggPlugin */
// module.exports = {
//   // had enabled by egg
//   // static: {
//   //   enable: true,
//   // }

// };

+ exports.mongoose = {
  enable: true,
  package: 'egg-mongoose'
}

数据模型设计

// app/model/user.js
module.exports = app => {
  const mongoose = app.mongoose
  const Schema = mongoose.Schema

  const userSchema = new Schema({
    username: { // 用户名
      type: String,
      required: true
    },
    email: { // 邮箱
      type: String,
      required: true
    },
    password: { // 密码
      type: String,
      select: false, // 查询中不包含该字段
      required: true
    },
    avatar: { // 头像
      type: String,
      default: null
    },
    cover: {
      type: String, // 封面
      default: null
    },
    channelDescription: { // 频道介绍
      type: String,
      default: null
    },
    subscribersCount: {
      type: Number,
      default: 0
    },
    createdAt: { // 创建时间
      type: Date,
      default: Date.now
    },
    updatedAt: { // 更新时间
      type: Date,
      default: Date.now
    }
  })

  return mongoose.model('User', userSchema)
}

视频:

// app/model/video.js
module.exports = app => {
  const mongoose = app.mongoose
  const Schema = mongoose.Schema

  const videoSchema = new Schema({
    title: { // 视频标题
      type: String,
      required: true
    },
    description: { // 视频介绍
      type: String,
      required: true
    },
    vodVideoId: { // VOD 视频 ID
      type: String,
      required: true
    },
    cover: { // 视频封面
      type: String,
      required: true
    },
    user: {
      type: mongoose.ObjectId, // 视频作者
      required: true,
      ref: 'User'
    },
    commentsCount: { // 评论数量
      type: Number,
      default: 0
    },
    dislikesCount: { // 不喜欢数量
      type: Number,
      default: 0
    },
    likesCount: { // 喜欢数量
      type: Number,
      default: 0
    },
    viewsCount: { // 观看次数
      type: Number,
      default: 0
    },
    createdAt: { // 创建时间
      type: Date,
      default: Date.now
    },
    updatedAt: { // 更新时间
      type: Date,
      default: Date.now
    }
  })

  return mongoose.model('Video', videoSchema)
}

视频点赞:

// app/model/video_like.js
module.exports = app => {
  const mongoose = app.mongoose
  const Schema = mongoose.Schema

  const likeSchema = new Schema({
    like: { // 点赞状态
      type: Number,
      enum: [1, -1], // 喜欢 1,不喜欢 -1
      required: true
    },
    user: { // 点赞用户
      type: mongoose.ObjectId,
      ref: 'User', // 关联到User表
      required: true
    },
    video: { // 点赞视频
      type: mongoose.ObjectId,
      ref: 'Video',
      required: true
    },
    createdAt: { // 创建时间
      type: Date,
      default: Date.now
    },
    updatedAt: { // 更新时间
      type: Date,
      default: Date.now
    }
  })

  return mongoose.model('VideoLike', likeSchema)
}

视频评论:

// app/model/video_comment.js
module.exports = app => {
  const mongoose = app.mongoose
  const Schema = mongoose.Schema

  const commentSchema = new Schema({
    content: { // 评论内容
      type: String,
      required: true
    },
    user: { // 评论用户
      type: mongoose.ObjectId,
      ref: 'User',
      required: true
    },
    video: { // 评论视频
      type: mongoose.ObjectId,
      ref: 'Video',
      required: true
    },
    createdAt: { // 创建时间
      type: Date,
      default: Date.now
    },
    updatedAt: { // 更新时间
      type: Date,
      default: Date.now
    }
  })

  return mongoose.model('Comment', commentSchema)
}

频道(用户)订阅

// app/model/subscription.js
module.exports = app => {
  const mongoose = app.mongoose
  const Schema = mongoose.Schema

  const subscriptionSchema = new Schema({
    user: { // 订阅用户
      type: mongoose.ObjectId,
      ref: 'User',
      required: true
    },
    channel: { // 订阅频道
      type: mongoose.ObjectId,
      ref: 'User',
      required: true
    },
    createdAt: { // 创建时间
      type: Date,
      default: Date.now
    },
    updatedAt: { // 更新时间
      type: Date,
      default: Date.now
    }
  })

  return mongoose.model('Subscription', subscriptionSchema)
}

观看历史:

// app/model/video_view.js
module.exports = app => {
  const mongoose = app.mongoose
  const Schema = mongoose.Schema

  const viewSchema = new Schema({
    user: { // 用户
      type: mongoose.ObjectId,
      ref: 'User',
      required: true
    },
    video: { // 视频
      type: mongoose.ObjectId,
      ref: 'Video',
      required: true
    },
    createdAt: { // 创建时间
      type: Date,
      default: Date.now
    },
    updatedAt: { // 更新时间
      type: Date,
      default: Date.now
    }
  })

  return mongoose.model('View', viewSchema)
}

用户注册-准备

app/router.js

'use strict';

/**
 * @param {Egg.Application} app - egg application
 */
module.exports = app => {
  const { router, controller } = app;
  router.prefix('/api/v1') // 设置基础路由
  router.get('/users', controller.user.create)
};

app/controller/user.js

'use strict';

const Controller = require('egg').Controller;

class UserController extends Controller {
  async create() {
    const { ctx } = this;
    ctx.body = 'UserController'
  }
}

module.exports = UserController;

config/config.default.js

/* eslint valid-jsdoc: "off" */

'use strict'

/**
 * @param {Egg.EggAppInfo} appInfo app info
 */
module.exports = appInfo => {
  /**
   * built-in config
   * @type {Egg.EggAppConfig}
   **/
  const config = {}

  // use for cookie sign key, should change to your own and keep security
  config.keys = appInfo.name + '_1611716016238_6422'

  // add your middleware config here
  config.middleware = ['errorHandler']

  // add your user config here
  const userConfig = {
    // myAppName: 'egg',
  }

  config.mongoose = {
    client: {
      url: 'mongodb://127.0.0.1/youtube-clone',
      options: {
        useUnifiedTopology: true
      },
      // mongoose global plugins, expected a function or an array of function and options
      plugins: []
    }
  }
	
	// 解决403问题,没有权限问题。关闭权限的验证
+  config.security = {
    csrf: {
      enable: false
    }
  }

  return {
    ...config,
    ...userConfig
  }
}

用户注册-数据验证介绍

参数校验 :https://www.eggjs.org/zh-CN/basics/controller#%E5%8F%82%E6%95%B0%E6%A0%A1%E9%AA%8C

https://github.com/eggjs/egg-validate

用户注册-数据验证

npm i egg-validate --save

// config/plugin.js
exports.validate = {
  enable: true,
  package: 'egg-validate',
};
// controller/user.js
'use strict';

const Controller = require('egg').Controller;

class UserController extends Controller {
  async create() {
    const { ctx } = this;
    // 1.数据验证
+    this.ctx.validate({
      username: { type: string },
      email: { type: email },
      
    })
    // 2.保存用户
    // 3.生成token
    // 4.发送响应
  }
}

module.exports = UserController;

用户注册-自定义异常处理

2种方式
1.try catch
2.框架层统一处理异常:https://www.eggjs.org/zh-CN/core/error-handling
或 https://www.eggjs.org/zh-CN/tutorials/restful#%E7%BB%9F%E4%B8%80%E9%94%99%E8%AF%AF%E5%A4%84%E7%90%86

通一错误处理

// app/middleware/error_handler.js
// 外层函数负责接收参数
module.exports = () => {
	// 返回一个中间件处理函数
  return async function errorHandler(ctx, next) {
    try {
      await next();
    } catch (err) {
      // 所有的异常都在 app 上触发一个 error 事件,框架会记录一条错误日志
      ctx.app.emit('error', err, ctx);

      const status = err.status || 500;
      // 生产环境时 500 错误的详细错误内容不返回给客户端,因为可能包含敏感信息
      const error =
        status === 500 && ctx.app.config.env === 'prod'
          ? 'Internal Server Error'
          : err.message;

      // 从 error 对象上读出各个属性,设置到响应中
      ctx.body = { error };
      if (status === 422) {
        ctx.body.detail = err.errors;
      }
      ctx.status = status;
    }
  };
};

config/config.default.js

/* eslint valid-jsdoc: "off" */

'use strict';

/**
 * @param {Egg.EggAppInfo} appInfo app info
 */
module.exports = appInfo => {
+  // 配置errorHandler中间件
+  config.middleware = ['errorHandler'];

 
  
  return {
    ...config,
    ...userConfig,
  };
};

// controller/user.js
'use strict';

const Controller = require('egg').Controller;

class UserController extends Controller {
  async create() {
    const { ctx } = this;
    // 1.数据验证
      this.ctx.validate({
      username: { type: string },
      email: { type: email },
      password: { type: string },
    })
    // 2.保存用户
    // 3.生成token
    // 4.发送响应
  }
}

module.exports = UserController;

用户注册-将数据保存到数据库

// controller/user.js
'use strict';

const Controller = require('egg').Controller;

class UserController extends Controller {
  async index() {
    const { ctx } = this;
    // 1.数据验证
    const body = this.ctx.request.body // 获取请求体
    this.ctx.validate({
      username: { type: string },
      email: { type: email },
      password: { type: string },
    })

    if(await this.service.user.findByUsername(body.username)) {
      this.ctx.throw(422, '用户已经存在')
    }
    if(await this.service.user.findByEmail(body.username)) {
      this.ctx.throw(422, '邮箱已经存在')
    }
    // 2.保存用户
    const user = await this.service.user.createUser(body)
    // 3.生成token

    // 4.发送响应
    this.ctx.body = {
      user: {
        // token
        email: user.email,
        username: user.username,
        avator: user.username,
      }
    }

    ctx.body = 'UserController'
  }
}

module.exports = UserController;


service/user.js 专门处理数据库的操作

const Service = require('err').Service

class UserService extends Service {

  get User() {
     return this.app.model.User // 获取User的数据模型
   }

  // 查找username
  findByUsername(username) {
    return this.User.findOne({ username })
  }
  // 查找邮箱
  findByEmail(email) {
    return this.User.findOne({ email })
  }

  // 创建用户
  async createUser(data) {
    data.password = this.ctx.helper.md5(data.password)
    const user = await this.User(data) // 获取User的实例
    await user.save() //  保存到数据库中
    return user
  }
}

app/entend/htlper.js

const crypto = require('crypto')
const _ = require('lodash')

exports.md5 = str => {
  return crypto.createHash('md5').update(str).digest('hex')
}

exports._ = _

用户注册-处理Token

npm i jsonwebtoken

config/config.default.js

/* eslint valid-jsdoc: "off" */

'use strict'

/**
 * @param {Egg.EggAppInfo} appInfo app info
 */
module.exports = appInfo => {
//	添加jwt的secret
+  config.jwt = {
    secret: 'a6e8561e-58df-4715-aa21-b5d1a091e71a',
    expiresIn: '1d'
  }

  return {
    ...config,
    ...userConfig
  }
}

service/user.js

const Service = require('err').Service
+ const jwt = require('jsonwebtoken')

class UserService extends Service {

  get User() {
     return this.app.model.User // 获取User的数据模型
   }

  // 查找username
  findByUsername(username) {
    return this.User.findOne({ username })
  }
  // 查找邮箱
  findByEmail(email) {
    return this.User.findOne({ email })
  }

  // 创建用户
  async createUser(data) {
    data.password = this.ctx.helper.md5(data.password)
    const user = await this.User(data) // 获取User的实例
    await user.save() //  保存到数据库中
    return user
  }

  // 生成token
+  createToken (data) {
    const token = jwt.sign(data, this.app.config.jwt.secret, {
      expiresIn: this.app.config.jwt.expiresIn
    })
    return token
  }
}

controller/user.js

'use strict';

const Controller = require('egg').Controller;

class UserController extends Controller {
// 创建用户
  async create() {
    const { ctx } = this;
    // 1.数据验证
    const body = this.ctx.request.body // 获取请求体
    this.ctx.validate({
      username: { type: string },
      email: { type: email },
      password: { type: string },
    })

    const userService = this.service.user

    if(await userService.findByUsername(body.username)) {
      this.ctx.throw(422, '用户已经存在')
    }
   if(await userService.findByEmail(body.username)) {
      this.ctx.throw(422, '邮箱已经存在')
    }
    // 2.保存用户
    const user = await userService.createUser(body)
    // 3.生成token
+    const token = await userService.createToken({ userId: user._id })

    // 4.发送响应
    this.ctx.body = {
      user: {
+        token,
        email: user.email,
        username: user.username,
        avator: user.username,
      }
    }
  }
}

module.exports = UserController;

用户登录

获取当前登录用户

router.js

'use strict';

/**
 * @param {Egg.Application} app - egg application
 */
module.exports = app => {
  const { router, controller } = app;
 + const auth = app.middleware.auth()

  router.prefix('/api/v1') // 设置基础路由
  // 创建用户
  router.post('/users', controller.user.index)
  // 登录
  router.get('/users/login', controller.user.login)

  // 获取当前用户
+   router.get('/user', auth, controller.user.getCurrentUser)
};

middleware/auth.js

module.exports = () => {
  return async (ctx, next) => {
    // 1. 获取请求头中的token数据
    const token = ctx.headers['authorzation']
    token = token ? ctx.headers['authorzation'].split('Bearer ')[1] : null // Bearer空格token数据
    // 2。验证token,无效401
    if (!token) {
      ctx.throw(401)
    }

    // 3.token有效,根据userId,获取用户数据挂载到ctx对象中,后续中间件都可以使用
    try {
      const data = ctx.service.user.veriftToken(token)
      ctx.user = ctx.module.User.findById(data.user)
    } catch (error) {
      ctx.throw(error)
    }
    

    // 4.next执行后续中间件
    await next()
  }
}

controller/user.js

'use strict';

const Controller = require('egg').Controller;

class UserController extends Controller {

  // 获取当前用户
+  async getCurrentUser() {
    // 1.验证token
    // 2.获取用户
    // 3.发送响应
    const user = this.ctx.user
    this.ctx.body = {
      user: {
        email: user.email,
        token: this.ctx.headers['authorzation'],
        username: user.username,
        avator: user.avator,
      }
    }
  }
}

module.exports = UserController;

service/user.js

const Service = require('err').Service
const jwt = require('jsonwebtoken')

class UserService extends Service {

  get User() {
     return this.app.model.User // 获取User的数据模型
   }

  // 查找username
  findByUsername(username) {
    return this.User.findOne({ username })
  }
  // 查找邮箱
  findByEmail(email) {
    return this.User.findOne({ email })
  }

  // 创建用户
  async createUser(data) {
    data.password = this.ctx.helper.md5(data.password)
    const user = await this.User(data) // 获取User的实例
    await user.save() //  保存到数据库中
    return user
  }

  // 生成token
  createToken (data) {
    const token = jwt.sign(data, this.app.config.jwt.secret, {
      expiresIn: this.app.config.jwt.expiresIn
    })
    return token
  }

  // 验证token
+  veriftToken(token) {
    return jwt.verify(token, this.app.config.jwt.secret)
  }
}

更新当前登录用户资料

router.js

'use strict';

/**
 * @param {Egg.Application} app - egg application
 */
module.exports = app => {
  const { router, controller } = app;
  const auth = app.middleware.auth()

  router.prefix('/api/v1') // 设置基础路由
  // 创建用户
  router.post('/users', controller.user.index)
  // 登录
  router.get('/users/login', controller.user.login)

  // 获取当前用户
  router.get('/user', auth, controller.user.getCurrentUser)

  // 更新用户
+  router.patch('/user', auth, controller.user.update)
};

service/user.js

const Service = require('err').Service
const jwt = require('jsonwebtoken')

class UserService extends Service {

  // 更新用户信息
+  updaetUser(data) {
    // findByIdAndUpdate: 默认返回更新之前的信息,需要配置 { new: true },返回更新之后的数据
   return this.User.findByIdAndUpdate(this.ctx.user._id, data, { new: true })
  }

}

controller/user.js

'use strict';

const Controller = require('egg').Controller;

class UserController extends Controller {
 
  // 更新用户信息
+  async update() {
    // 1.基本数据验证
    // 1.数据验证
    const body = this.ctx.request.body // 获取请求体
    this.ctx.validate({
      username: { type: 'string', required: false }, // required:flase:传了就验证,不传就不验证。
      email: { type: 'email', required: false },
      password: { type: 'string', required: false},
    })

    // 2.校验用户是否已经存在
    this.userService = this.service.user
    if (body.username) {
      if (body.username !== this.ctx.user.username && await userService.findByUsername(body.username)) {
        this.ctx.throw(422, '用户已经存在')
      }
    }
    if (body.password) {
      body.password = this.ctx.header.md5(password)
    }

    // 3.校验邮箱是否已经存在
    if (body.email) {
      if (body.email !== this.ctx.user.email && await userService.findByUsername(body.email)) {
        this.ctx.throw(422, '邮箱已经存在')
      }
    }
    // 4.更新用户信息
    const user = await userService.updaetUser(body)
    // 5.返回更新之后的用户信息
    this.ctx.body = {
      user: {
        email: user.email,
        username: user.username,
        avator: user.avator,
      }
    }
  }
}

module.exports = UserController;

订阅频道

router.js

'use strict';

/**
 * @param {Egg.Application} app - egg application
 */
module.exports = app => {
  const { router, controller } = app;
  const auth = app.middleware.auth()

  router.prefix('/api/v1') // 设置基础路由
  // 创建用户
  router.post('/users', controller.user.index)
  // 登录
  router.get('/users/login', controller.user.login)

  // 获取当前用户
  router.get('/user', auth, controller.user.getCurrentUser)

  // 更新用户
  router.patch('/user', auth, controller.user.update)

  // 用户订阅
+  router.post('/users/:userId/subscribe', auth, controller.user.subscribe)
};

service/user.js

const Service = require('err').Service
const jwt = require('jsonwebtoken')

class UserService extends Service {


+  async subscribe(channelId, userId) {
    const { Subscription } = this.app.model
    // 1.检查是否已经订阅
    const record = Subscription.findOne({ user: userId, channelId: channelId })
    // 2。没有订阅添加订阅
    const user = User.findById(channelId)
    if (!record) {
      await new Subscription({ user: userId, channelId: channelId }).save()
    }
    user.subscribesCount++
    await user.save() //  更新到数据库中。
    // 3.返回用户信息
    return user
  }
}

controller/user.js

'use strict';

const Controller = require('egg').Controller;

class UserController extends Controller {

  // 用户订阅
+  async subscribe() {
    const userId = this.ctx.user._id
    const channelId = this.ctx.params.userId // 获取路径参数的userId

    // 1.用户不能订阅自己
    if(userId.equals(channelId)) {
      this.ctx.throw(422, '用户不能订阅自己')
    }

    // 2.添加订阅
    const user = await this.service.user.subscribe(userId, channelId)
    // 3.发送响应
    this.ctx.body = {
      user: {
        ...user.toJson()
        isSubscribe: true,
      }
    }
  }
}

module.exports = UserController;

使用lodash-pick处理返回的数据

npm install lodash

extend/helper.js

const crypto = require('crypto')
 + const _ = require('lodash')

exports.md5 = str => {
  return crypto.createHash('md5').update(str).digest('hex')
}
 //直接挂载到helper上,可以直接在ctx上使用
+ exports._ = _

controller/user.js

'use strict';

const Controller = require('egg').Controller;

class UserController extends Controller {
 

  // 用户订阅
  async subscribe() {
    const userId = this.ctx.user._id
    const channelId = this.ctx.params.userId // 获取路径参数的userId

    // 1.用户不能订阅自己
    if(userId.equals(channelId)) {
      this.ctx.throw(422, '用户不能订阅自己')
    }

    // 2.添加订阅
    const user = await this.service.user.subscribe(userId, channelId)
    // 3.发送响应
    this.ctx.body = {
      user: {
+        ...this.ctx.helper._.pick(user, ['username', 'email', 'avator', 'cover', 'channelDescription', 'subscribersCount']),
        isSubscribe: true,
      }
    }
  }
}

module.exports = UserController;

消订阅频道

router.js

'use strict';

/**
 * @param {Egg.Application} app - egg application
 */
module.exports = app => {
  const { router, controller } = app;
  const auth = app.middleware.auth()

  router.prefix('/api/v1') // 设置基础路由
  // 创建用户
  router.post('/users', controller.user.index)
  // 登录
  router.get('/users/login', controller.user.login)

  // 获取当前用户
  router.get('/user', auth, controller.user.getCurrentUser)

  // 更新用户
  router.patch('/user', auth, controller.user.update)

  // 用户订阅
  router.post('/users/:userId/subscribe', auth, controller.user.subscribe)

  // 取消订阅
+  router.delete('/users/:userId/subscribe', auth, controller.user.unsubscribe)
};

service/user.js

const Service = require('err').Service
const jwt = require('jsonwebtoken')

class UserService extends Service {

  // 取消订阅
+  async unsubscribe(channelId, userId) {
    const { Subscription } = this.app.model
    // 1.检查是否已经订阅
    const record = Subscription.findOne({ user: userId, channelId: channelId })
    // 2。没有订阅添加订阅
    const user = User.findById(channelId)
    if (record) {
      await record.remove() // 删除用户订阅
    }
    user.subscribesCount--
    await user.save() //  更新到数据库中。
    // 3.返回用户信息
    return user
  }
}

controller/user.js

'use strict';

const Controller = require('egg').Controller;

class UserController extends Controller {
  // 取消用户订阅
+  async unsubscribe() {
    const userId = this.ctx.user._id
    const channelId = this.ctx.params.userId // 获取路径参数的userId

    // 1.用户不能取消订阅自己
    if(userId.equals(channelId)) {
      this.ctx.throw(422, '用户不能订阅自己')
    }

    // 2.取消订阅
    const user = await this.service.user.unsubscribe(userId, channelId)
    // 3.发送响应
    this.ctx.body = {
      user: {
        ...this.ctx.helper._.pick(user, ['username', 'email', 'avator', 'cover', 'channelDescription', 'subscribersCount']),
        isSubscribe: false,
      }
    }
  }
}

module.exports = UserController;

获取用户资料

router.js

'use strict';

/**
 * @param {Egg.Application} app - egg application
 */
module.exports = app => {
  const { router, controller } = app;
  const auth = app.middleware.auth()

  router.prefix('/api/v1') // 设置基础路由
  // 创建用户
  router.post('/users', controller.user.index)
  // 登录
  router.get('/users/login', controller.user.login)

  // 获取当前用户
  router.get('/user', auth, controller.user.getCurrentUser)

  // 更新用户
  router.patch('/user', auth, controller.user.update)

  // 用户订阅
  router.post('/users/:userId/subscribe', auth, controller.user.subscribe)

  // 取消订阅
  router.delete('/users/:userId/subscribe', auth, controller.user.unsubscribe)

  // 获取用户资料
+  router.get('/user/:userid', app.middleware.auth({ require: true }), controller.user.getUser)
};

middleware/auth.js

+ module.exports = (options = { require: true }) => {
  return async (ctx, next) => {
    // 1. 获取请求头中的token数据
    const token = ctx.headers['authorzation']
    token = token ? ctx.headers['authorzation'].split('Bearer ')[1] : null// Bearer空格token数据
    // 2。验证token,无效401
    if (!token) {
      ctx.throw(401)
    }
 +   if (token) {
      // 3.token有效,根据userId,获取用户数据挂载到ctx对象中,后续中间件都可以使用
      try {
        const data = ctx.service.user.veriftToken(token)
        ctx.user = ctx.module.User.findById(data.user)
      } catch (error) {
        ctx.throw(error)
      }
+    } else if (options.required) {
      ctx.throw(401)
    }

    // 4.next执行后续中间件
    await next()
  }
}

controller/user.js

// 获取用户资料
async getUser() {
    // 1.获取订阅状态
    let isSubscribe = false
    if (this.ctx.user) {
      // 获取订阅记录
      const record = await this.app.model.Subscription({
        user: this.ctx.user._id,
        cancel: this.ctx.params.userId
      })

      if (record) {
        isSubscribe = true
      }
    }
    // 2.获取用户信息
   const user = await this.app.model.User.findById(this.ctx.params.userId)
    // 3.发送响应
    this.ctx.body = {
      ...this.ctx.helper._.pick(user, ['username', 'email', 'avator', 'cover', 'channelDescription', 'subscribersCount']),
    }
  }

获取用户订阅的频道列表

router.js

// 获取用户订阅的频道列表
  router.get('/users/:userId/subscriptions', auth, controller.user.getSubscriptions)

controller/user.js

// 获取用户订阅的频道列表
  async getSubscriptions() {
    const Subscription = this.app.model.Subscription
    const subscription = await Subscription.find({ user: this.ctx.params.userId }).populate('channel')
    subscription = subscription.map (item => {
      return this.ctx.helper._.pick(item.channel, ['_id', 'username', 'avtor'])
    })
    this.ctx.body = {
      subscription
    }
  }

阿里云视频点播服务介绍

视频上传-获取上传地址和凭证

npm i @alicloud/pop-core --save

router.js

// 阿里云 VOD
router.get('/vod/CreateUploadVideo', auth, controller.vod.createUploadvideo)

controller/vod.js

'use strict';

const Controller = require('egg').Controller;
const RPCClient = require('@alicloud/pop-core').RPCClient


function initVodClient (accessKeyId, accessKeySecret) {
  const regionId = 'cn-shanghai' // 点播服务接入区域
  const client = new RPCClient({
    accessKeyId: accessKeyId,
    accessKeySecret: accessKeySecret,
    endpoint: 'http://vod.' + regionId + '.aliyuncs.com',
    apiVersion: '2017-03-21'
  })


class VodController extends Controller {
  
  // 获取上传地址和凭证
  async createUploadvideo() {
    const query = this.ctx.query

    this.ctx.validate({ title: { type: 'string' }, FilName: { type: 'string' } })

    const vodClient = initVodClient('LTAI4FzgRRRN2MjwBzc3xQtp', 'xAllGuORtBDVcrTQpTOWu4HfjYgN1p')

    // 发送响应
   this.ctx.body = await vodClient.request('CreateUploadVideo',query, {})
  }
}

module.exports = VodController;

视频上传-上传完成

先解决跨域的问题。
npm i egg-cors --save

plugin.js

+ exports.cors = {
  enable: true,
  package: 'egg-cors'
}

config.default.js

+ config.cors = {
    origin: '*'
    // {string|Function} origin: '*',
    // {string|Array} allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH'
  }

视频上传-刷新视频上传凭证

extend/application.js

const RPCClient = require('@alicloud/pop-core').RPCClient

function initVodClient (accessKeyId, accessKeySecret) {
  const regionId = 'cn-shanghai' // 点播服务接入区域
  const client = new RPCClient({
    accessKeyId: accessKeyId,
    accessKeySecret: accessKeySecret,
    endpoint: 'http://vod.' + regionId + '.aliyuncs.com',
    apiVersion: '2017-03-21'
  })

  return client
}

let vodClient = null

module.exports = {
  // get访问器的方法。
  get vodClient () {
    if (!vodClient) {
      vodClient = initVodClient('LTAI4FzgRRRN2MjwBzc3xQtp', 'xAllGuORtBDVcrTQpTOWu4HfjYgN1p')
    }
    return vodClient
  }
}

conroller/vod.js

'use strict';

const Controller = require('egg').Controller;

class VodController extends Controller {
  
  // 获取上传地址和凭证
  async createUploadVideo() {
    const query = this.ctx.query

    this.ctx.validate({ title: { type: 'string' }, FilName: { type: 'string' } }, query)

    // 发送响应
+   this.ctx.body = await this.app.vodClient.request('CreateUploadVideo', query, {})
  }

  // 视频上传-刷新视频上传凭证
+  async refreshUploadvideo() {
    const query = this.ctx.query

    this.ctx.validate({ VideoId: { type: 'string' } }, query)

    // 发送响应
    this.ctx.body = await this.app.vodClient.request('RefreshUploadvideo', query, {})

  }
}

module.exports = VodController;

视频上传-优化配置信息

config/config.local.js 只针对本地开发的文件

/**
 * 本地开发的配置文件 
 */
 // 本地如果还是根据环境变量读取有点麻烦,直接硬编码写死。
const secret = require('./secret')
exports.vod = {
  ...secret.vod
}

ignore.js

exports.vod = {
  accessKeyId: 'LTAI4FzgRRRN2MjwBzc3xQtp',
  accessKeySecret: 'xAllGuORtBDVcrTQpTOWu4HfjYgN1p'
}

config/config.prod.js 只针对生产环境的文件

/**
 * 生产环境的配置文件
 */
exports.vod = {
  // 通过环境变量设置
  accessKeyId: process.env.accessKeyId,
  accessKeySecret: process.env.accessKeySecret
}

extend/application.js

const RPCClient = require('@alicloud/pop-core').RPCClient

function initVodClient (accessKeyId, accessKeySecret) {
  const regionId = 'cn-shanghai' // 点播服务接入区域
  const client = new RPCClient({
    accessKeyId: accessKeyId,
    accessKeySecret: accessKeySecret,
    endpoint: 'http://vod.' + regionId + '.aliyuncs.com',
    apiVersion: '2017-03-21'
  })

  return client
}

let vodClient = null

module.exports = {
  // get访问器的方法。
  get vodClient () {
    if (!vodClient) {
 +     const { accessKeyId, accessKeySecret } = this.config.vod
 +     vodClient = initVodClient(accessKeyId, accessKeySecret)
    }
    return vodClient
  }
}

创建视频-接口实现

// 创建视频
router.post('/videos', auth, controller.vod.createVideo)

controller/video.js

const Controller = require('egg').Controller

class VideoController extends Controller {

  async createVideo () {
    const body = this.ctx.request.body
    const { Video } = this.app.model
    this.ctx.validate({
      title: { type: 'string' },
      description: { type: 'string' },
      vodVideoId: { type: 'string' }
      // cover: { type: 'string' }
    }, body)

    // 默认视频封面
    body.cover = 'http://vod.lipengzhou.com/image/default/A806D6D6B0FD4D118F1C824748826104-6-2.png'

    body.user = this.ctx.user._id
    const video = await new Video(body).save()
    this.ctx.status = 201
    this.ctx.body = {
      video
    }

    const setVideoCover = async (video) => {
      // 获取视频信息
      const vodVideoInfo = await this.app.vodClient.request('GetVideoInfo', {
        VideoId: video.vodVideoId
      })

      if (vodVideoInfo.Video.CoverURL) {
        // 使用自动生成的封面
        video.cover = vodVideoInfo.Video.CoverURL
        // 将修改保存到数据库中
        await video.save()
      } else {
        await new Promise(resolve => {
          setTimeout(() => {
            resolve()
          }, 3000)
        })
        await setVideoCover(video)
      }
    }

    setVideoCover(video)
  }
}

module.exports = VideoController

获取视频详情-接口实现

 // 获取视频详情
  router.get('/videos/:videoId', auth, controller.vod.getVideo)

controller/Video.js

// 获取视频详情
async getVideo() {
    // 获取数据模型
    const { Video, VideoLike, Subscription } = this.app.model
    const { videoId } = this.ctx.params // 获取路径中的videoId

    // 在数据库的Video表中根据videoId查询记录
    let video = await Video.findById(videoId).populate('user', '_id username avatar subscribersCount')

    // 没有查到抛出异常
    if (!video) {
      this.ctx.throw(404, 'Video Not Found')
    }

    // 转换成普通的js对象
    video = video.toJSON()

    // 在video中添加自定义数据
    video.isLiked = false // 是否喜欢
    video.isDisliked = false // 是否不喜欢
    video.user.isSubscribed = false // 是否已订阅视频作者

    // 如果用户登录
    if(this.ctx.user) {
      // 喜欢: 1, 不喜欢: -1

      // 查询记录
      const userId = this.ctx.user._id
      if (await VideoLike.findOne({ user: userId, video: videoId, like: 1 })) {
        video.isLiked = true
      }
      if (await VideoLike.findOne({ user: userId, video: videoId, like: -1 })) {
        video.isDisliked = true
      }
      if (await Subscription.findOne({ user: userId, channel: video.user._id })) {
        video.user.isSubscribed = true
      }
    }
    
    // 查询的结果video直接发送到客户端
    this.ctx.body = {
      video
    }
  }

获取视频列表-接口实现

router.js

  // 获取视频列表
  router.get('/videos', controller.vod.getVideos)

controller/video.js

async getVideos() {
    const { Video } this.app.model
    const { pageNum = 1, pageSize = 10 } = this.ctx.query
    pageNum = Number.parseInt(pageNum)
    pageSize = Number.parseInt(pageSize)

    // 按条件在数据库中查询
    const getVideos = Video.find().popupate('user')
    .sort({ createdAt: -1 })
    .skip((pageNum -1)) * pageSize)
    .limit(pageSize)
    
    const getVideosCount = Video.countDocuments() // 获取视频总数量
    const [videos, videosCount] = await Promise.all([
      getVideos,
      getVideosCount
    ])
    // 查询到的数据返回给客户端。
    this.ctx.body = {
      videos,
      videosCount
    }
  }

获取用户发布的视频列表-接口实现

router.js

// 获取用户发布的视频列表
  router.get('/users/:userId/videos', controller.video.getUserVideos)

controller/video.js

async getUserVideos () {
    const { Video } = this.app.model
    let { pageNum = 1, pageSize = 10 } = this.ctx.query
    const userId = this.ctx.params.userId
    pageNum = Number.parseInt(pageNum)
    pageSize = Number.parseInt(pageSize)
    const getVideos = Video
      .find({
        user: userId
      })
      .populate('user')
      .sort({
        createdAt: -1
      })
      .skip((pageNum - 1) * pageSize)
      .limit(pageSize)
    const getVideosCount = Video.countDocuments({
      user: userId
    })
    const [videos, videosCount] = await Promise.all([
      getVideos,
      getVideosCount
    ])
    this.ctx.body = {
      videos,
      videosCount
    }
  }

获取用户关注的频道视频列表-接口实现

router.js

 //  获取用户关注的频道视频列表-接口实现
  router.get('/user/videos/feed', auth, controller.video.getUserFeedVideos)

controller/video.js

async getUserFeedVideos () {
    const { Video, Subscription } = this.app.model
    let { pageNum = 1, pageSize = 10 } = this.ctx.query
    const userId = this.ctx.user._id
    pageNum = Number.parseInt(pageNum)
    pageSize = Number.parseInt(pageSize)

    const channels = await Subscription.find({ user: userId }).populate('channel')
    const getVideos = Video
      .find({
        user: {
          $in: channels.map(item => item.channel._id) // 用户关注的列表
        }
      })
      .populate('user')
      .sort({
        createdAt: -1
      })
      .skip((pageNum - 1) * pageSize)
      .limit(pageSize)
    const getVideosCount = Video.countDocuments({
      user: {
        $in: channels.map(item => item.channel._id)
      }
    })
    const [videos, videosCount] = await Promise.all([
      getVideos,
      getVideosCount
    ])
    this.ctx.body = {
      videos,
      videosCount
    }
  }

修改视频-接口实现

router.js

// 更新视频
  router.patch('/videos/:videoId', auth, controller.video.updateVideo)

controller/video.js

async updateVideo () {
    const { body } = this.ctx.request
    const { Video } = this.app.model
    const { videoId } = this.ctx.params
    const userId = this.ctx.user._id

    // 数据验证
    this.ctx.validate({
      title: { type: 'string', required: false },
      description: { type: 'string', required: false },
      vodVideoId: { type: 'string', required: false },
      cover: { type: 'string', required: false }
    })

    // 查询视频
    const video = await Video.findById(videoId)

    if (!video) {
      this.ctx.throw(404, 'Video Not Found')
    }

    // 视频作者必须是当前登录用户
    if (!video.user.equals(userId)) {
      this.ctx.throw(403)
    }

    Object.assign(video, this.ctx.helper._.pick(body, ['title', 'description', 'vodVideoId', 'cover']))

    // 把修改保存到数据库中
    await video.save()

    // 发送响应
    this.ctx.body = {
      video
    }
  }

删除视频-接口实现

router.js

 // 删除视频
router.delete('/videos/:videoId', auth, controller.video.deleteVideo)

controller/video.js

async deleteVideo () {
    const { Video } = this.app.model
    const { videoId } = this.ctx.params
    const video = await Video.findById(videoId)

    // 视频不存在
    if (!video) {
      this.ctx.throw(404)
    }

    // 视频作者不是当前登录用户
    if (!video.user.equals(this.ctx.user._id)) {
      this.ctx.throw(403)
    }

    await video.remove()

    this.ctx.status = 204
  }

添加视频评论-接口实现

router.js

 // 添加视频评论
router.post('/videos/:videoId/comments', auth, controller.video.createComment)

controller/video.js

async createComment () {
    const body = this.ctx.request.body
    const { Video, VideoComment } = this.app.model
    const { videoId } = this.ctx.params
    // 数据验证
    this.ctx.validate({
      content: 'string'
    }, body)

    // 获取评论所属的视频
    const video = await Video.findById(videoId)

    if (!video) {
      this.ctx.throw(404)
    }

    // 创建评论
    const comment = await new VideoComment({
      content: body.content,
      user: this.ctx.user._id,
      video: videoId
    }).save()

    // 更新视频的评论数量
    video.commentsCount = await VideoComment.countDocuments({
      video: videoId
    })
    await video.save()

    // 映射评论所属用户和视频字段数据
    await comment.populate('user').populate('video').execPopulate()

    this.ctx.body = {
      comment
    }
  }

获取视频评论列表-接口实现

router.js

// 获取视频评论列表
router.get('/videos/:videoId/comments', controller.video.getVideoComments) 

controller/video.js

async getVideoComments () {
    const { videoId } = this.ctx.params
    const { VideoComment } = this.app.model
    let { pageNum = 1, pageSize = 10 } = this.ctx.query
    pageNum = Number.parseInt(pageNum)
    pageSize = Number.parseInt(pageSize)

    const getComments = VideoComment
      .find({
        video: videoId
      })
      .skip((pageNum - 1) * pageSize)
      .limit(pageSize)
      .populate('user')
      .populate('video')

    const getCommentsCount = VideoComment.countDocuments({
      video: videoId
    })

    const [comments, commentsCount] = await Promise.all([
      getComments,
      getCommentsCount
    ])

    this.ctx.body = {
      comments,
      commentsCount
    }
  }

删除视频评论-接口实现

router.js

 //  删除视频评论
router.delete('/videos/:videoId/comments/:commentId', auth, controller.video.deleteVideoComment) 

controller/video.js

async deleteVideoComment () {
    const { Video, VideoComment } = this.app.model
    const { videoId, commentId } = this.ctx.params

    // 校验视频是否存在
    const video = await Video.findById(videoId)
    if (!video) {
      this.ctx.throw(404, 'Video Not Found')
    }

    const comment = await VideoComment.findById(commentId)

    // 校验评论是否存在
    if (!comment) {
      this.ctx.throw(404, 'Comment Not Found')
    }

    // 校验评论作者是否是当前登录用户
    if (!comment.user.equals(this.ctx.user._id)) {
      this.ctx.throw(403)
    }

    // 删除视频评论
    await comment.remove()

    // 更新视频评论数量
    video.commentsCount = await VideoComment.countDocuments({
      video: videoId
    })
    await video.save()

    this.ctx.status = 204
  }

喜欢视频和不喜欢视频-接口实现

router.js

 // 喜欢视频
router.post('/videos/:videoId/like', auth, controller.video.likeVideo)
 
 // 不喜欢视频
router.post('/videos/:videoId/dislike', auth, controller.video.dislikeVideo)

controller/video.js

// 喜欢视频
async likeVideo () {
    const { Video, VideoLike } = this.app.model
    const { videoId } = this.ctx.params
    const userId = this.ctx.user._id
    const video = await Video.findById(videoId)

    if (!video) {
      this.ctx.throw(404, 'Video Not Found')
    }

    const doc = await VideoLike.findOne({
      user: userId,
      video: videoId
    })

    let isLiked = true

    if (doc && doc.like === 1) {
      await doc.remove() // 取消点赞
      isLiked = false
    } else if (doc && doc.like === -1) {
      doc.like = 1
      await doc.save()
    } else {
      await new VideoLike({
        user: userId,
        video: videoId,
        like: 1
      }).save()
    }

    // 更新喜欢视频的数量
    video.likesCount = await VideoLike.countDocuments({
      video: videoId,
      like: 1
    })

    // 更新不喜欢视频的数量
    video.dislikesCount = await VideoLike.countDocuments({
      video: videoId,
      like: -1
    })

    // 将修改保存到数据库中
    await video.save()

    this.ctx.body = {
      video: {
        ...video.toJSON(),
        isLiked
      }
    }
  }
// 不喜欢视频
async dislikeVideo () {
    const { Video, VideoLike } = this.app.model
    const { videoId } = this.ctx.params
    const userId = this.ctx.user._id
    const video = await Video.findById(videoId)

    if (!video) {
      this.ctx.throw(404, `No video found for ID - ${videoId}`)
    }

    const doc = await VideoLike.findOne({
      user: userId,
      video: videoId
    })

    let isDisliked = true

    if (doc && doc.like === -1) {
      await doc.remove()
      isDisliked = false
    } else if (doc && doc.like === 1) {
      doc.like = -1
      await doc.save()
    } else {
      await new VideoLike({
        user: userId,
        video: videoId,
        like: -1
      }).save()
    }

    // 更新视频喜欢和不喜欢的数量
    video.likesCount = await VideoLike.countDocuments({
      video: videoId,
      like: 1
    })
    video.dislikesCount = await VideoLike.countDocuments({
      video: videoId,
      like: -1
    })

    this.ctx.body = {
      video: {
        ...video.toJSON(),
        isDisliked
      }
    }
  }

你可能感兴趣的:(大前端学习,node.js)