Koa项目重构-基础设施搭建
安装项目需要的依赖
javascript
{
"name": "koa-project",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "nodemon app.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@alicloud/pop-core": "^1.7.12",
"@koa/cors": "^3.3.0",
"@koa/router": "^10.1.1",
"ioredis": "^5.0.6",
"joi": "^17.6.0",
"koa": "^2.13.4",
"koa-body": "^5.0.0",
"mongoose": "^6.3.6",
"nodemon": "^2.0.16"
}
}
index.js
应用入口
javascript
const Koa = require('koa');
const cors = require('@koa/cors');
const koaBody = require('koa-body');
const router = require('./router');
const app = new Koa();
// 允许跨域
app.use(cors());
// 解析请求体
app.use(koaBody());
// 注册路由
app.use(router.routes());
app.use(router.allowedMethods());
// 监听错误
app.on('error', (err, ctx) => {
console.log(err);
});
app.listen(3000, () => {
console.log('server is running at http://localhost:3000');
});
router\index.js
路由
javascript
const Router = require('koa-router');
const userController = require('../controller/userController');
const router = new Router();
router.get('/', userController.index);
module.exports = router;
controller\userController.js
控制器
javascript
module.exports.index = async (ctx) => {
ctx.body = 'Hello World';
};
数据库链接模型创建
model\userModel.js
用户模型
javascript
const mongoose = require('mongoose')
const md5 = require('../util/md5')
const baseModel = require('./baseModel')
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true
},
email: {
type: String,
required: true
},
password: {
type: String,
required: true,
set: value => md5(value),
select:false
},
phone: {
type: String,
required: true
},
image: {
type: String,
default: null
},
cover:{
type: String,
default: null
},
channeldes:{
type: String,
default: null
},
subscribeCount:{
type:Number,
default:0
},
...baseModel
})
module.exports = userSchema
model\index.js
javascript
const mongoose = require('mongoose')
const { mongoPath } = require('../config/config.default')
async function mian(){
await mongoose.connect(mongoPath)
}
mian()
.then(res=>{
console.log('mongoDB 连接成功');
})
.catch(err=>{
console.log(err);
})
module.exports = {
User:mongoose.model('User',require('./userModel')),
Video:mongoose.model('Video',require('./videoModel')),
Subscribe:mongoose.model('Subscribe',require('./subscribeModel')),
Videocomment:mongoose.model('Videocomment',require('./videocommentModel')),
Videolike:mongoose.model('Videolike',require('./videolikeModel')),
collectModel:mongoose.model('CollectModel',require('./collectModel'))
}

用户登录与数据验证
安装 Joi 请求数据校验库
javascript
// ^17.6.0
pnpm i joi
https://joi.dev/api/?v=17.13.3
router\index.js
新建注册路由
javascript
// 注册
router.post('/user/register', registerValidate, userController.register);
middleware\userValidate.js
注册路由参数校验中间件
javascript
const Joi = require('joi');
const { User } = require('../model');
// 用户注册参数验证
module.exports.registerValidate = async (ctx, next) => {
const schema = Joi.object({
username: Joi.string().required(),
password: Joi.string().min(6).required(),
email: Joi.string().email().required(),
phone: Joi.string().min(11).max(11).required(),
}).validate(ctx.request.body);
// 验证失败
if (schema.error) {
ctx.body = {
code: 400,
message: schema.error.message,
};
return;
}
// 邮箱是否注册
const user = await User.findOne({
email: ctx.request.body.email,
});
if (user) {
ctx.body = {
code: 400,
message: '邮箱已注册',
};
return;
}
await next();
};
controller\userController.js
实现用户注册逻辑
javascript
const { User } = require('../model');
// 用户注册
module.exports.register = async (ctx) => {
const userModal = new User(ctx.request.body);
const dbBack = await userModal.save();
ctx.body = {
code: 200,
message: '注册成功',
data: dbBack,
};
};

用户登录与Token验证
安装 jsonwebtoken 库
javascript
// jsonwebtoken ^9.0.3
pnpm i jsonwebtoken
util\jwt.js
定义生成token和校验token 工具函数。
javascript
const jwt = require('jsonwebtoken')
const { promisify } = require('util')
const tojwt = promisify(jwt.sign)
const verify = promisify(jwt.verify)
// 验证token
module.exports.verifyToken = function (required = true) {
return async (ctx, next) => {
var token = ctx.headers.authorization
token = token ? token.split("Bearer ")[1] : null
if(token){
try {
var userInfo = await verify(token,'koa-viode')
ctx.user = userInfo
await next()
} catch (error) {
ctx.throw(402,error)
}
}else if(required){
ctx.throw(402,'无效的token')
}else{
await next()
}
}
}
// 生成token
module.exports.createToken = async userInfo => {
var token = await tojwt({ userInfo }, 'koa-viode', {
expiresIn: 60 * 60 * 24
})
return token
}
router\index.js
创建用户登录路由
javascript
// 登录
router.post('/user/login', userController.login);
middleware\userValidate.js
自定义用户登录参数校验中间件
javascript
// 用户登录参数校验
module.exports.loginValidate = async (ctx, next) => {
const schema = Joi.object({
password: Joi.string().min(6).required(),
email: Joi.string().email().required(),
}).validate(ctx.request.body);
// 验证失败
if (schema.error) {
ctx.body = {
code: 400,
message: schema.error.message,
};
return;
}
// 邮箱是否注册
const user = await User.findOne({
email: ctx.request.body.email,
});
if (!user) {
ctx.body = {
code: 400,
message: '邮箱未注册',
};
return;
}
await next();
};
controller\userController.js
实现用户登录逻辑
javascript
// 用户登录
module.exports.login = async (ctx) => {
const { email, password } = ctx.request.body;
const user = await User.findOne({
email,
password,
});
if (!user) {
ctx.body = {
code: 400,
message: '用户名或密码错误',
};
return;
}
// 生成 token
const token = await createToken(user._doc);
user._doc.token = token;
ctx.body = {
code: 200,
message: '登录成功',
data: user._doc,
};
};

用户频道模块-获取频道详情
router\index.js
创建获取频道详情路由
javascript
// 获取频道信息
router.get('/user/getuser/:userid', verifyToken(true), userController.getuser);
controller\userController.js
实现获取频道详情逻辑
javascript
// 获取频道信息
module.exports.getuser = async (ctx) => {
const userid = ctx.request.params.userid;
console.log('ctx.user', ctx.user);
const loginUserId = ctx.user ? ctx.user.userInfo._id : null;
let isSubscribe = false;
if (loginUserId) {
// 是否订阅此频道
const subscribe = await Subscribe.findOne({
user: loginUserId,
channel: userid,
});
if (subscribe) {
isSubscribe = true;
}
}
// 获取 userid 频道信息
const userInfoDb = await User.findOne(
{
_id: userid,
},
['username', 'image', 'cover', 'channeldes']
);
const userinfo = userInfoDb._doc;
userinfo.isSubscribe = isSubscribe;
ctx.body = {
code: 200,
message: '获取频道信息成功',
data: userinfo,
};
};

用户频道模块-关注频道
router\index.js
创建关注频道路由
javascript
// 关注频道
router.get(
'/user/subscribe/:subscribeid',
verifyToken(true),
userController.subscribe
);
controller\userController.js
实现关注频道逻辑
javascript
module.exports.subscribe = async (ctx) => {
const subscribeid = ctx.request.params.subscribeid;
const userid = ctx.user.userInfo._id;
// 是否关注自己
if (subscribeid === userid) {
ctx.body = {
code: 400,
message: '不能关注自己',
};
return;
}
const subscribe = await Subscribe.findOne({
user: userid,
channel: subscribeid,
});
// 是否已关注
if (subscribe) {
ctx.body = {
code: 400,
message: '已关注',
};
return;
}
// 关注频道
const subscribeModal = new Subscribe({
user: userid,
channel: subscribeid,
});
const dbBack = await subscribeModal.save();
if (dbBack) {
// 粉丝数+1
const subcribeUser = await User.findById(subscribeid, [
'username',
'image',
'cover',
'channeldes',
]);
subcribeUser.followCount += 1;
await subcribeUser.save();
ctx.body = {
code: 200,
message: '关注成功',
data: subcribeUser,
};
} else {
ctx.body = {
code: 400,
message: '关注失败',
};
}
};

我的关注列表
router\index.js
创建关注频道列表路由
javascript
// 关注列表
router.get(
'/user/subscribe/subscribelist',
verifyToken(false),
userController.subscribeList
);
controller\userController.js
实现获取频道列表
javascript
// 关注频道列表
module.exports.subscribeList = async (ctx) => {
const userid = ctx.user.userInfo._id;
const subscribeList = await Subscribe.find({
user: userid,
}).populate('channel', [
'username',
'image',
'cover',
'channeldes',
'followCount',
]);
console.log('subscribeList', subscribeList);
ctx.body = {
code: 200,
message: '获取关注频道列表成功',
data: subscribeList,
};
};

视频模块-获取视频凭证与客户端视频上传
安装阿里云视频上传库
javascript
pnpm i @alicloud/pop-core
router\index.js
创建获取视频上传凭证路由
javascript
// 视频模块 - 获取视频上传凭证
router.get('/video/getvod', verifyToken(true), vodController.getVod);
controller\vodController.js
实现获取视频上传凭证
javascript
var RPCClient = require('@alicloud/pop-core').RPCClient;
// 初始化点播客户端
function initVodClient(accessKeyId, accessKeySecret) {
const regionId = 'cn-shanghai'; // 点播服务接入区域
var client = new RPCClient({
accessKeyId: accessKeyId,
accessKeySecret: accessKeySecret,
endpoint: 'vod.' + regionId + '.aliyuncs.com',
apiVersion: '2017-03-21',
});
return client;
}
// 获取点播视频上传地址和凭证
exports.getVod = async (req, res) => {
var client = initVodClient('LTAI5t3Yxxxxxx', 'xxxxxxxxxxxxxxxxxxxxxx');
const vodBack = await client.request('CreateUploadVideo', {
Title: 'xxxxxxxxxxxxxx',
FileName: 'xxxxxxxxxxxxxx.mp4',
});
res.status(200).json({ vod: vodBack });
};
视频模块-创建视频入库
router\index.js
创建创建视频路由
javascript
// 视频模块 - 创建视频
router.post(
'/video/createvideo',
verifyToken(true),
videoController.createVideo
);
controller\videoController.js
实现创建视频信息入库
javascript
// 上传视频
exports.createVideo = async (ctx) => {
var body = ctx.request.body;
body.user = ctx.user.userInfo._id;
const videoModel = new Video(body);
try {
var dbBack = await videoModel.save();
ctx.body = {
code: 201,
message: '视频上传成功',
data: dbBack,
};
} catch (error) {
ctx.body = {
code: 500,
message: '视频上传失败',
data: error,
};
}
};

视频模块-获取播放地址
router\index.js
创建获取视频播放地址路由
javascript
// 视频模块 - 获取视频播放地址
router.get(
'/video/getPlay/:videoId',
verifyToken(true),
vodController.getPlayUrl
);
controller\vodController.js
实现获取视频播放地址
javascript
// 获取视频播放地址
exports.getPlayUrl = async (ctx) => {
var client = initVodClient('LTAI5t3Yxxxxxx', 'xxxxxxxxxxxxxxxxxxxxxx');
const { videoId } = ctx.request.params;
const playUrlBack = await client.request('GetPlayInfo', {
VideoId: videoId,
});
ctx.body = playUrlBack;
};
视频模块-获取用户视频列表
router\index.js
创建获取用户发布的视频列表路由
javascript
// 视频模块 - 获取用户发布的视频列表
router.get(
'/video/videolist/:userid',
verifyToken(false),
videoController.videolist
);
controller\videoController.js
实现获取用户发布的视频列表
javascript
// 获取指定用户发布的视频分页列表
exports.videolist = async (ctx) => {
const userid = ctx.request.params.userid;
var { page = 1, size = 10 } = ctx.request.query;
page = parseInt(page);
size = parseInt(size);
try {
var dbBack = await Video.find({ user: userid })
.skip((page - 1) * size)
.limit(size)
.sort({
createdAt: -1,
})
.populate('user', [
'username',
'image',
'cover',
'channeldes',
'followCount',
]);
// 视频总条数
var total = await Video.countDocuments({ user: userid });
ctx.body = {
code: 200,
message: '视频列表获取成功',
total: total,
data: dbBack,
};
} catch (error) {
ctx.body = {
code: 500,
message: '视频列表获取失败',
data: error,
};
}
};

视频模块-获取视频详情
router\index.js
创建获取视频详情路由
javascript
// 视频模块 - 视频详情
router.get('/video/:videoId', verifyToken(true), videoController.videodetail);
controller\videoController.js
获取视频详情
javascript
// 获取视频详情
exports.videodetail = async (ctx) => {
var { videoId } = ctx.request.params;
try {
var dbBack = await Video.findById(videoId).populate('user', [
'username',
'image',
'cover',
'channeldes',
'followCount',
]);
const videoinfo = dbBack._doc;
if (videoinfo) {
const { getVodPlay } = require('./vodController');
const vodInfo = await getVodPlay(videoinfo.vodvideoId);
videoInfo.vod = vodInfo;
videoInfo.isLike = false;
videoInfo.isDisLike = false;
videoInfo.isSubscribe = false;
// 是否登录
if (ctx.user) {
const userId = ctx.user.userInfo._id;
// 是否喜欢此视频
videoInfo.isLike = (await Videolike.findOne({
video: videoId,
user: userId,
like: 1,
}))
? 1
: -1;
// 是否踩此视频
videoInfo.isDisLike = (await Videolike.findOne({
video: videoId,
user: userId,
like: -1,
}))
? 1
: -1;
// 是否关注视频作者
videoInfo.isSubscribe = (await Follow.findOne({
user: userId,
channle: videoInfo.user._id,
}))
? 1
: -1;
}
// 热度加 1
// hotInc(videoId, 1);
ctx.body = {
code: 200,
message: '视频详情获取成功',
data: videoInfo,
};
} else {
ctx.body = {
code: 404,
message: '视频不存在',
};
}
} catch (error) {
console.log('error', error);
ctx.body = {
code: 500,
message: '视频详情获取失败',
data: error,
};
}
};
controller\vodController.js
封装获取视频播放详情
javascript
const getVodPlay = async (vodid) => {
var client = initVodClient('LTAI5t3Yxxxxxx', 'xxxxxxxxxxxxxxxxxxxxxx');
const playUrlBack = await client.request('GetPlayInfo', {
VideoId: vodid,
});
return playUrlBack;
};
// 获取视频播放地址
exports.getVodDetail = async (ctx) => {
var { vodid } = ctx.request.params;
const playUrlBack = await getVodPlay(vodid);
res.status(200).json({ playUrl: playUrlBack });
};
module.exports.getVodPlay = getVodPlay;
交互模块-视频评论
router\index.js
创建发布视频评论路由
javascript
// 视频模块 - 发表视频评论
router.post(
'/video/comment/:videoId',
verifyToken(true),
videoController.createComment
);
controller\videoController.js
实现发布视频评论
javascript
// 发表视频评论
exports.createComment = async (ctx) => {
var { videoId } = ctx.request.params;
var { content } = ctx.request.body;
var userId = ctx.user.userInfo._id;
try {
var video = await Video.findById(videoId);
if (video) {
const videocommentModel = new Videocomment({
content,
video: videoId,
user: userId,
});
var dbBack = await videocommentModel.save();
// 评论数加1
video.commentCount += 1;
await video.save();
// 热度加 2
// hotInc(videoId, 2);
ctx.body = {
code: 201,
message: '评论成功',
data: dbBack,
};
} else {
ctx.body = {
code: 404,
message: '视频不存在',
};
}
} catch (error) {
console.log('error', error);
ctx.body = {
code: 500,
message: '评论失败',
data: error,
};
}
};
