用vue全家桶仿寫豆瓣電影wap版。
最近在公司專案中嘗試使用vue,但奈何自己初學水平有限,上了vue沒有上vuex,開發過程特別難受。
於是玩一玩本專案,算是對相關技術更加熟悉了。
原計劃仿寫完所有頁面,礙於豆瓣的介面API有限,實現頁面也有限。
由於公開的豆瓣介面具有訪問次數限制,克隆到本地體驗效果更加!
web端訪問已設定寬度適配。
進入GitHub檢視本專案原始碼
歡迎issue
,pr
,star
or follow
!我將繼續開源更多有趣的專案!
推薦一些之前寫的新手入門專案
線上版
部分效果截圖
工具&技能
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複製程式碼
實現功能
首頁
- 影院熱映、即將上映、top250、北美票房榜
- 電影條目可橫向滾動
- 預覽電影評分
搜尋頁
輸入搜尋關鍵詞,Enter鍵
搜尋,或者點選搜尋按鈕。
- 搜尋功能
- 熱門搜尋詞條的記錄
檢視更多
- 預覽電影評分
- 滾動動態載入
- 資料快取入vuex
電影詳情
- 電影評分
- 電影條目
- 演員列表
- 劇情簡介
- 資料快取入vuex
搜尋結果頁
- 翻頁功能
- 圖片懶載入
- 預覽電影條目
- 本地快取瀏覽資訊
目錄結構
|
|—— 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的封裝
其實我就是不想用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
!我將繼續開源更多有趣的專案!
你的支援將有助於專案維護以及提高使用者體驗,感謝各位的支援!