即将迎来的端午小假期,小伙伴们都准备好怎么度过了么。我每次出去玩都避免不了去看场电影,这次借此机会向大家介绍下我开发的可以查看电影预告片的小项目,希望大家可以去测试,浏览一波即将上映的电影同时可以帮助我测试一下,指出不足,我都会虚心接受的呦!谢谢大家。
项目演示地址
效果图
项目介绍
前端是通过vue-cli进行构建项目,后端接口是使用Koa进行编写的。电影相关数据是使用puppeteer进行爬取并存在mongoDB数据库中,为减轻带宽压力将预告片上传到七牛云上。其主要功能包括:
- 电影列表的展示
- 电影详情信息及预告片播放功能
- 根据上映情况、分类、评分进行筛选电影
- 电影热度前十榜单
- 搜索电影功能
- 用户的注册与登录。
未来想完善的功能:
- 对电影的收藏与喜欢
- 根据所在地推荐购票地点
- 用户信息相关的操作
- 电影数据的自动爬取更新
- 项目web端、小程序端
技术问题
电影上映状态路由切换问题
电影上映状态分为正在热映与即将上映,其中list路由页是通过参数进行转换,1为正在上映,2为即将上映。路由配置如下:
{
path: '/movie',
name: 'movie',
component: Movie,
children: [
{
path: 'all/:type',
name: 'list',
component: List
}
]
}复制代码
同路由组件参数切换不会再次触发created
、mounted
生命周期函数,所以要实现参数切换重新请求数据需要在组件内导航守卫中beforeRouteUpdate
进行操作。其核心代码如下:
beforeRouteUpdate (to, from, next) {
this.page = 1 this.max_page = 0 this.movies = []
this._getMovies(to.params.type)
next()
}复制代码
应对不同场合的Card组件
本项目页面中大量用自己写的Card组件,在list页面、搜索页面、筛选页面、榜单页面等均有使用到。其主要效果如下图:
但当在榜单页面时所有Card组件前都需要有排名,所以可以通过扩展组件的props
实现,新增一个rank属性,当为true时则将排名展示出来,其代码如下:
<p class="text" v-if="rank" :class="'rank-' + index">{{index}}</p>复制代码
props: {
movie: Object,
index: Number,
rank: {
type: Boolean,
default: false
}
}复制代码
电影数据爬取
电影相关数据信息是使用doubanApi结合puppeteer进行爬取得到的,获取电影数据总共分为四步:
- 利用puppeteer模拟浏览器访问豆瓣网站获取电影的名字、海报、doubanId、评分存入数据库。爬取网址是:
const nowUrl = 'https://movie.douban.com/cinema/nowplaying/beijing/' const comUrl = 'https://movie.douban.com/coming'复制代码
- 利用豆瓣提供的开放API,通过循环数据库中电影doubanId来获取到电影详细的信息,例如导演、演员、简介、类型、上映日期等。
- 利用puppeteer浏览豆瓣电影详情页,从而跳转到预告片页面爬取预告片的资源,存入数据库。爬取网址是:
const url = 'https://movie.douban.com/subject/' 复制代码
- 使用七牛云提供的NodeSDK将视频资源上传到七牛云床上,并将返回的key值存在数据库中,通过服务器CNAME可以访问七牛云上的短片。其核心代码如下:
// 上传函数 const uploadToQiniu = async (url, key) => { return new Promise((resolve, reject) => { bucketManager.fetch(url, bucket, key, function (err, respBody, respInfo) { if (err) { reject(err) } else { if (respInfo.statusCode == 200) { resolve({key}) } else { reject(respBody) } } }) }) } // 循环数据库中数据将上传后返回的keuy值存在数据库 ;(async () => { const movies = await Movie.find({ $or: [ {videoKey: {$exists: false}}, {videoKey: null}, {videoKey: ''} ] }) for (let i = 0; i < movies.length; i++) { let movie = movies[i] if (movie.video && !movie.videoKey) { try { let videoData = await uploadToQiniu(movie.video, nanoid() + '.mp4') let posterData = await uploadToQiniu(movie.poster, nanoid() + '.jpg') let coverData = await uploadToQiniu(movie.cover, nanoid() + '.jpg') const arr = [] for (let i = 0; i < movie.images.length; i++) { let { key } = await uploadToQiniu(movie.images[i], nanoid() + '.jpg') if (key) { arr.push(key) } } movie.images = arr for (let j = 0; j < movie.casts.length; j++) { if (!movie.casts[j].avatar) continue; let { key } = await uploadToQiniu(movie.casts[j].avatar, nanoid() + '.jpg') if (key) { movie.casts[j].avatar = key } } if (videoData.key) { movie.videoKey = videoData.key } if (posterData.key) { movie.posterKey = posterData.key } if (coverData.key) { movie.coverKey = coverData.key } await movie.save() } catch (error) { console.log(error) } } } })()复制代码
利用Decorator修饰器定义Route路由类
本项目是通过koa-router进行拦截请求,并进行数据库相关操作,由于接口数量较多,所以可以采用Decorator方式去定义路由,更利于开发与维护。例如:
// 利用Decorator修饰类的行为
@controller('api/client/movie')export class movieController {
@get('/get_all') // 获取符合条件的电影条数
@required({
query: ['page_size', 'page']
})
async getAll (ctx, next) {
const { page_size, page, type } = ctx.query
const data = await getAllMovies(page_size, page, type)
ctx.body = {
code: 0,
errmsg: '',
data
}
}
......
}复制代码
如果想让上述代码有效,需要在项目运行时将修饰器函数定义好,并且载入koa-router中间件,符合修饰器参数的路由则执行相关类实例的方法,其Route类实现代码如下:
export class Route {
constructor (app, apiPath) {
this.app = app
this.apiPath = apiPath
this.router = new Router()
}
/**
* 遍历routerMap,得到请求路径和方法,路径和controller装饰器的参数拼接
* 通过koa-router实例调用请求方法(请求路径, 对应的路由中间件)
* 通过koa实例载入router中间件
*/
init () {
glob.sync(path.resolve(__dirname, this.apiPath, './**/*.js')).forEach(require)
for (let [conf, controllers] of routerMap) {
controllers = toArray(controllers)
const prefixPath = conf.target[symbolPrefix]
prefixPath && (prefixPath = normalizePath(prefixPath))
const routerPath = prefixPath + conf.path
this.router[conf.method](routerPath, ...controllers)
}
this.app.use(this.router.routes()).use(this.router.allowedMethods())
}
}
// 将path统一成 '/xxx' const normalizePath = path => path.startsWith('/')? path : `/${path}` // 将路由类,请求路径以及方法,装饰器对应的方法存入routerMap中 export const router = conf => (target, key, desc) => {
conf.path = normalizePath(conf.path)
routerMap.set({
target,
...conf
}, target[key])
}
// 将path挂载到路由类的prototyp上,实例上可以访问 export const controller = path => target => (target.prototype[symbolPrefix] = path)
export const get = path => router({ path, method: 'get'})复制代码
总结
项目总体来说较为简单,而且有很多不足的地方,之后我也会一直完善项目,希望小伙伴们可以提出不足,以及自己的建议。还有这是我第一次写文章,水平有限,写不出深层次的知识,只好拿自己项目作为处女作。希望各位小伙伴多多包涵。最后,如果感觉项目还不错的,不要吝啬你的star呦!谢谢!