案例介绍
- 模仿实现 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