Vue全家桶實現還原豆瓣電影wap版

counterxing發表於2017-04-23

用vue全家桶仿寫豆瓣電影wap版。

最近在公司專案中嘗試使用vue,但奈何自己初學水平有限,上了vue沒有上vuex,開發過程特別難受。

於是玩一玩本專案,算是對相關技術更加熟悉了。

原計劃仿寫完所有頁面,礙於豆瓣的介面API有限,實現頁面也有限。

由於公開的豆瓣介面具有訪問次數限制,克隆到本地體驗效果更加!

web端訪問已設定寬度適配。

進入GitHub檢視本專案原始碼

歡迎issueprstar or follow!我將繼續開源更多有趣的專案!

推薦一些之前寫的新手入門專案

線上版

點選進入

部分效果截圖

Vue全家桶實現還原豆瓣電影wap版

工具&技能

  • vue + vuex+ vue-router全家桶
  • webpack + webpack-dev-server + http-proxy-middleware進行本地開發環境http請求轉發,實現跨域請求
  • 線上使用expresshttp-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

歡迎issueprstar or follow!我將繼續開源更多有趣的專案!

你的支援將有助於專案維護以及提高使用者體驗,感謝各位的支援!

相關文章