Vue开发的电影预告webApp介绍

9,014 阅读5分钟

即将迎来的端午小假期,小伙伴们都准备好怎么度过了么😄。我每次出去玩都避免不了去看场电影,这次借此机会向大家介绍下我开发的可以查看电影预告片的小项目,希望大家可以去测试,浏览一波即将上映的电影同时可以帮助我测试一下,指出不足,我都会虚心接受的呦!谢谢大家。

项目演示地址


效果图





项目介绍

前端是通过vue-cli进行构建项目,后端接口是使用Koa进行编写的。电影相关数据是使用puppeteer进行爬取并存在mongoDB数据库中,为减轻带宽压力将预告片上传到七牛云上。其主要功能包括:

  • 电影列表的展示
  • 电影详情信息及预告片播放功能
  • 根据上映情况、分类、评分进行筛选电影
  • 电影热度前十榜单
  • 搜索电影功能
  • 用户的注册与登录。

未来想完善的功能:

  • 对电影的收藏与喜欢
  • 根据所在地推荐购票地点
  • 用户信息相关的操作
  • 电影数据的自动爬取更新
  • 项目web端、小程序端

技术问题

电影上映状态路由切换问题

电影上映状态分为正在热映与即将上映,其中list路由页是通过参数进行转换,1为正在上映,2为即将上映。路由配置如下:

{  
  path: '/movie',  
  name: 'movie',  
  component: Movie, 
  children: [
    {      
      path: 'all/:type',    
      name: 'list',  
      component: List    
    }  
  ]
}

同路由组件参数切换不会再次触发createdmounted生命周期函数,所以要实现参数切换重新请求数据需要在组件内导航守卫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进行爬取得到的,获取电影数据总共分为四步:

  1. 利用puppeteer模拟浏览器访问豆瓣网站获取电影的名字、海报、doubanId、评分存入数据库。爬取网址是:

    const nowUrl = 'https://movie.douban.com/cinema/nowplaying/beijing/'
    const comUrl = 'https://movie.douban.com/coming'

  2. 利用豆瓣提供的开放API,通过循环数据库中电影doubanId来获取到电影详细的信息,例如导演、演员、简介、类型、上映日期等。
  3. 利用puppeteer浏览豆瓣电影详情页,从而跳转到预告片页面爬取预告片的资源,存入数据库。爬取网址是:

    const url = 'https://movie.douban.com/subject/'
    

  4. 使用七牛云提供的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呦!谢谢!

GitHub项目地址