用vue全家桶仿寫豆瓣電影wap版。javascript
最近在公司項目中嘗試使用vue,但奈何本身初學水平有限,上了vue沒有上vuex,開發過程特別難受。css
因而玩一玩本項目,算是對相關技術更加熟悉了。html
原計劃仿寫完全部頁面,礙於豆瓣的接口API有限,實現頁面也有限。vue
因爲公開的豆瓣接口具備訪問次數限制,克隆到本地體驗效果更加!java
web端訪問已設置寬度適配。webpack
進入GitHub查看本項目源碼git
歡迎issue
,pr
,star
or follow
!我將繼續開源更多有趣的項目!github
推薦一些以前寫的新手入門項目web
vue
+ vuex
+ vue-router
全家桶
webpack
+ webpack-dev-server
+ http-proxy-middleware
進行本地開發環境http請求轉發,實現跨域請求
線上使用express
的http-proxy-middleware
實現請求轉發
iView
一款vue的組件庫
vue-lazyload
實現圖片懶加載
rem
+ flex
+ grid
實現移動端適配
http-proxy-middleware
一個http代理的中間件,進行http請求轉發,實現跨域請求
postman
接口測試工具
git clone https://github.com/xingbofeng/douban-movie.git cd douban-movie npm install npm run dev
[x] 影院熱映、即將上映、top250、北美票房榜
[x] 電影條目可橫向滾動
[x] 預覽電影評分
輸入搜索關鍵詞,回車鍵
搜索,或者點擊搜索按鈕。
[x] 搜索功能
[x] 熱門搜索詞條的記錄
[x] 預覽電影評分
[x] 滾動動態加載
[x] 數據緩存入vuex
[x] 電影評分
[x] 電影條目
[x] 演員列表
[x] 劇情簡介
[x] 數據緩存入vuex
[x] 翻頁功能
[x] 圖片懶加載
[x] 預覽電影條目
[x] 本地緩存瀏覽信息
| |—— build |—— config |—— server 服務端 | |—— app.js 服務端啓動入口文件 | |—— static 打包後的資源文件 | |__ index.html 網頁入口 | |——src 資源文件 | |—— assets 組件靜態資源庫 | |—— components 組件庫 | |—— router 路由配置 | |—— store vuex狀態管理 | |—— App.vue douban-movieSPA | |__ main.js douban-movieSPA入口 | |__ static 靜態資源目錄
這個問題在我以前的的項目總結已經總結過。
加入咱們有電影條目A、B、C三個電影條目詳情。進入A加載A,進入B加載B。此時也要把A緩存入vuex中。
能夠相似於下面的寫法。
{ [`${A.id}`]: A, ...store.state }
具體代碼可見/src/router/routes
下列相關文件
beforeEnter: (to, before, next) => { const currentMovieId = to.params.currentMovieId; if (store.state.moviedetail.currentMovie[`${currentMovieId}`]) { store.commit(types.LOADING_FLAG, false); next(); return; } store.commit(types.LOADING_FLAG, true); currentMovie(currentMovieId).then((currentMovieDetail) => { // 成功則commit後臺接口的數據,並把NET_ERROR的數據置空,並把加載中的狀態置爲false。 const id = currentMovieDetail.id; store.commit(types.CURRENT_MOVIE, { [`${id}`]: currentMovieDetail, ...store.state.moviedetail.currentMovie, }); store.commit(types.LOADING_FLAG, false); store.commit(types.NET_STATUS, ''); document.title = `${currentMovieDetail.title} - 電影 - 豆瓣`; }).catch((error) => { document.title = '出錯啦 Oops… - 豆瓣'; store.commit(types.NET_STATUS, error); store.commit(types.LOADING_FLAG, false); }); next(); }
其實這個在以前的React項目中也有作過,設置一個currentPage
的狀態,而後根據這個狀態來渲染頁面。
具體代碼可見/src/containers/Tag.vue
。
computed: { ...mapState({ tagData(state) { return state.tag.tagData[`${this.$route.params.currentTagId}`]; }, }), subjects() { return this.tagData.subjects.slice( (this.currentPage - 1) * 10, this.currentPage * 10, ); }, }, methods: { ...mapActions(['getMoreTagData']), changePage(flag) { const currentTagId = this.$route.params.currentTagId; const { start, count } = this.tagData; // 第一頁不能往前翻頁,最後一頁不能日後翻頁。 if ((this.currentPage === 1 && flag === 'reduce') || (this.currentPage === Math.ceil(this.tagData.total / 10) && flag === 'add') ) { return; } if (flag === 'add') { this.currentPage = this.currentPage + 1; // 每次請求十條數據 this.getMoreTagData({ tag: currentTagId, count: 10, start: count + start, }); // 須要使用localStorge保存當前的頁碼信息,再次進入能夠有這個頁碼信息。 const doubanMovieCurrentPage = JSON.parse(window.localStorage.doubanMovieCurrentPage); window.localStorage.doubanMovieCurrentPage = JSON.stringify({ ...doubanMovieCurrentPage, [`${currentTagId}`]: this.currentPage, }); } else { this.currentPage = this.currentPage - 1; } window.scrollTo(0, 0); },
相似於瀑布流佈局的實現方式,當用戶滾動到距離頁面底部必定範圍的時候去請求後端接口。
具體代碼可見src/containers/More.vue
。
handleScroll() { // 函數的做用是滾動加載電影詳情信息 // 判斷是否爲請求後臺中的狀態,若是是則返回 const { start, count, total } = this.currentSeeMore; if (!this.requestFlag) { return; } // 不一樣瀏覽器top展示會不一致 let top = window.document.documentElement.scrollTop; if (top === 0) { top = document.body.scrollTop; } const clientHeight = document.getElementById('app').clientHeight; const innerHeight = window.innerHeight; const proportion = top / (clientHeight - innerHeight); // 但若是已把全部數據加載完畢了,則不請求 if (proportion > 0.6 && (start + count) < total) { this.getMoreData({ count, start: start + count, title: this.$route.params.title, }); this.requestFlag = false; } }
滾動節流主要做用是控制滾動事件的頻率,設置一個flag
。未超過頻率則直接在函數中返回。
具體代碼可見src/containers/More.vue
scrolling() { // scrolling函數用於做函數節流 if (this.scrollFlag) { return; } this.scrollFlag = true; setTimeout(() => { this.handleScroll(); this.scrollFlag = false; }, 20); }
404與加載頁面的實現
這裏主要是在vuex
中設定兩個狀態。根據這兩個狀態返回不一樣的頁面。
具體代碼可見src/App.vue
<template> <div id="app"> <net-error v-if="netStatus" :netStatus="netStatus" /> <loading v-else-if="!netStatus && loadingFlag" /> <router-view v-else></router-view> </div> </template>
以前在公司作React項目的時候運用了universal-router,當時咱們能夠在進入路由的時候dispatch一個action改變狀態,而且使用async/await函數實現異步。
貼一段以前的React代碼:
async action({ store, params }) { // 判斷store裏的id和當前id是否一致,若一致,則不請求後臺 console.log("chapter") const chapterInfos = store.getState().home.chapterInfos; if (Object.keys(chapterInfos).length === 0 || chapterInfos.subject.id !== parseInt(params.chapter, 10)) { await store.dispatch(chapter(params.chapter)); } }
相似的,在vue中咱們也能夠這麼作!
具體代碼可見/src/router/routes
下的相關代碼
beforeEnter: (to, before, next) => { document.title = '電影 - 豆瓣'; if (Object.keys(store.state.home.homeData).length !== 0) { store.commit(types.LOADING_FLAG, false); next(); return; } store.commit(types.LOADING_FLAG, true); Promise.all([ hotMovie(8, 0), commingSoon(8, 0), top250(8, 0), usBox(8, 0), ]).then((homeData) => { // 成功則commit後臺接口的數據,並把NET_ERROR的數據置空,並把加載中的狀態置爲false。 store.commit(types.HOME_DATA, homeData); store.commit(types.LOADING_FLAG, false); store.commit(types.NET_STATUS, ''); }).catch((error) => { document.title = '出錯啦 Oops… - 豆瓣'; store.commit(types.NET_STATUS, error); store.commit(types.LOADING_FLAG, false); }); next(); }
其實我就是不想用Ajax操做的相關庫罷了……
import serverConfig from './serverConfig'; const Ajax = url => new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('GET', url); xhr.send(null); xhr.onreadystatechange = () => { if (xhr.readyState === 4) { if (xhr.status === 200) { resolve(JSON.parse(xhr.responseText)); } else { reject(`錯誤: ${xhr.status}`); } } }; }); // 影院熱映 export const hotMovie = (count, start) => Ajax(`${serverConfig}/v2/movie/in_theaters?count=${count}&start=${start}`); // 即將上映 export const commingSoon = (count, start) => Ajax(`${serverConfig}/v2/movie/coming_soon?count=${count}&start=${start}`); // top250 export const top250 = (count, start) => Ajax(`${serverConfig}/v2/movie/top250?count=${count}&start=${start}`); // 北美票房榜 export const usBox = (count, start) => Ajax(`${serverConfig}/v2/movie/us_box?count=${count}&start=${start}`); // 當前電影詳情信息 export const currentMovie = currentMovieId => Ajax(`${serverConfig}/v2/movie/subject/${currentMovieId}`); // 當前標籤詳情信息 export const getTagData = (tag, count, start) => Ajax(`${serverConfig}/v2/movie/search?tag=${tag}&count=${count}&start=${start}`);
爲了解決瀏覽器跨域問題,須要在本地服務端配合實現請求轉發。
proxyTable: { '/v2': { target: 'http://api.douban.com', changeOrigin: true, pathRewrite: { '^/v2': '/v2' } } },
實際環境中,服務器端配置
var express = require('express'); var proxy = require('http-proxy-middleware'); var app = express(); app.use('/static', express.static('static')); app.use('/v2', proxy({ target: 'http://api.douban.com', changeOrigin: true, headers: { Referer: 'http://api.douban.com' } } )); app.get('/', function (req, res) { res.sendFile(__dirname + '/index.html'); }); app.listen(3000);
咱們使用rem
做單位,本項目中標準爲1rem = 100px,適配750px設備。
瀏覽器執行下列代碼,改變根元素的font-size
,作到移動端的適配。
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0,user-scalable=no">
(function (doc, win) { var docEl = doc.documentElement, resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize', recalc = function () { var clientWidth = docEl.clientWidth > 750 ? 360 : docEl.clientWidth ; if (!clientWidth) return; docEl.style.fontSize = clientWidth / 750 * 100 + 'px'; }; if (!doc.addEventListener) return; doc.addEventListener('DOMContentLoaded', recalc, false); if (docEl.clientWidth > 750) return; win.addEventListener(resizeEvt, recalc, false); })(document, window);
文檔借鑑自個人同窗ShanaMaid。
BUG提交請發送郵箱: me@xingbofeng.com
歡迎issue
,pr
,star
or follow
!我將繼續開源更多有趣的項目!
你的支持將有助於項目維護以及提升用戶體驗,感謝各位的支持!