即將迎來的端午小假期,小夥伴們都準備好怎麼度過了麼?。我每次出去玩都避免不了去看場電影,這次藉此機會向大家介紹下我開發的可以檢視電影預告片的小專案,希望大家可以去測試,瀏覽一波即將上映的電影同時可以幫助我測試一下,指出不足,我都會虛心接受的呦!謝謝大家。
專案演示地址
效果圖
專案介紹
前端是通過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呦!謝謝!