vue 仿豆瓣 爬坑之旅

最近一直在学vue,想用vue做点东西,在网上找了很多,最后决定仿豆瓣做一个适合pc和移动端的应用。由于平时工作都是做的PC端而且只用Chrome浏览器,不考虑兼容问题,这次想有所突破,考虑一下兼容移动端,但是浏览器兼容性问题就暂时先忽略了,这里顺便吐槽一下IE,今天看到微信公众号 “前端大全”的推送,IE的市场占有率又下降了,~~囧。 下面开始撸代码吧:

先介绍一下本人用的一些工具:

  • 编辑器:vscode
  • 服务器:nodejs、
  • 技术栈:vue.js、vue-router、axios、less
1. 搭建vue开发环境

1.1 node安装

node下载地址 安装完成后使用 node -v 、 npm -v 检查node和npm版本,若显示出版本号则说明安装成功。

1.2 vue-cli 脚手架搭建

npm install -g vue-cli 
或者使用国内的淘宝镜像 
npm install -g cnpm --registry=https://registry.npm.taobao.org
复制代码

1.3 搭建vue项目,"vue-douban"是我的项目名称

npm init webpack vue-douban
复制代码

1.4 配置需要安装的vue环境

vue-router 路由是项目一定要用到的,所以选择 Yes。 ESLint 代码检查,unit tests 单元测试, e2e tests 端到端测试 暂时可以不用。

1.5 完成以后项目目录结构如下:

1.6 执行 npm run dev 命令,启动项目,当出现如下图时,说明项目启动成功

此时可以通过浏览器访问链接http://localhost:8080来访问项目了。这时页面如下图:

接下来就可以愉快的撸代码了~~~

夜深,关机睡觉,明天再来接着写

======================================================================

2. 项目开发

2.1 jsonp、axios配置、API封装、路由配置
2.1.1 jsonp、axios配置

本项目使用jsonp请求豆瓣API,使用axios请求本地静态数据

jsonp 安装:

npm install --save vue-jsonp
复制代码

jsonp 封装,根据网上的教程,创建jsonp.js

import originJSOP from 'jsonp'  //引入jsonp

export default function jsonp (url, data, option) {
  url += (url.indexOf('?') > 0 ? '&' : '?') + param(data)
  return new Promise((resolve, reject) => {
    originJSOP(url, option, (err, data) => {
      if (!err) {
        resolve(data)
      } else {
        resolve(data)
      }
    })
  })
}

//拼接URL后面的参数
function param (data) {
  let url = ''
  for (var k in data) {
    let value = data[k] !== undefined ? data[k] : ''
    url += `&${k}=${encodeURIComponent(value)}`
  }
  return url ? url.substring(1) : ''
}
复制代码

安装axios

npm install axios -S
复制代码

引入axios:

import axios from 'axios';
//或者在main.js中全局引用
import Axios from 'axios';
Vue.prototype.axios = Axios
//全局引用后,使用时 用 this.axios.xxx
复制代码

查看豆瓣官网获取其API地址如下:

1、推荐列表

2、影院热映

3、免费在线电影

4、新片速递

5、最受关注图书丨虚构类

6、最受关注图书丨非虚构类

7、豆瓣书店

2.1.2 API封装

根据请求参数,创建 config.js

export const recommedParams = {
  alt: 'json',
  next_date: '',
  loc_id: 118172,
  gender: '',
  birthday: '',
  udid: '9fcefbf2acf1dfc991054ac40ca5114be7cd092f',
  for_mobile: 1
}

export const commonMoviesParams = {
  os: 'ios',
  start: 0,
  count: 8,
  loc_id: 108288,
  _: 0
}

export const commonBooksParams = {
  os: 'ios',
  start: 0,
  count: 8,
  loc_id: 0,
  _: 0
}

export const options = {
  param: 'jsonpCallback'
}
复制代码

封装API请求

import jsonp from '@/assets/js/jsonp';
import {
  recommedParams,
  commonMoviesParams,
  commonBooksParams,
  options
} from './config';
import axios from 'axios';

export function getRecommendData () {
  const url = 'https://m.douban.com/rexxar/api/v2/recommend_feed';
  const data = Object.assign({}, recommedParams)
  return jsonp(url, data)
}

export function getHotMovies () {
  const url =
    'https://m.douban.com/rexxar/api/v2/subject_collection/movie_showing/items';
  const data = Object.assign({}, commonMoviesParams)
  return jsonp(url, data)
}

export function getFreeMovies () {
  const url =
    'https://m.douban.com/rexxar/api/v2/subject_collection/movie_free_stream/items';
  const data = Object.assign({}, commonMoviesParams)
  return jsonp(url, data)
}

export function getNewMovies () {
  const url =
    'https://m.douban.com/rexxar/api/v2/subject_collection/movie_latest/items';
  const data = Object.assign({}, commonMoviesParams)
  return jsonp(url, data)
}

export function getMoviesTypes () {
  const url = '/static/movieTypes.json';
  return axios.get(url)
}

export function getGoodMoives () {
  const url = '/static/findGoodMovies.json';
  return axios.get(url)
}

export function getfictionBook () {
  const url =
    'https://m.douban.com/rexxar/api/v2/subject_collection/book_fiction/items';
  const data = Object.assign({}, commonBooksParams)
  return jsonp(url, data)
}

export function getnoFictionBook () {
  const url =
    'https://m.douban.com/rexxar/api/v2/subject_collection/book_nonfiction/items';
  const data = Object.assign({}, commonBooksParams)
  return jsonp(url, data)
}

export function getBookShop () {
  const url =
    'https://m.douban.com/rexxar/api/v2/subject_collection/market_product_book_mobile_web/items';
  const data = Object.assign({}, commonBooksParams)
  return jsonp(url, data)
}

export function getBookTypes () {
  const url = '/static/movieTypes.json';
  return axios.get(url)
}

export function getGoodBook () {
  const url = '/static/findGoodMovies.json';
  return axios.get(url)
}
复制代码
2.1.3路由配置

创建文件夹和文件:

配置路由:

import Vue from 'vue'
import Router from 'vue-router'
import Index from '@/components/views/index'
import Movie from '@/components/views/movie'
import Book from '@/components/views/book'
import Broadcast from '@/components/views/broadcast'
import Group from '@/components/views/group'
import Search from '@/components/views/search'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'Index',
      component: Index
    },
    {
      path: '/movie',
      name: 'Movie',
      component: Movie
    },
    {
      path: '/book',
      name: 'Book',
      component: Book
    },
    {
      path: '/broadcast',
      name: 'Broadcast',
      component: Broadcast
    },
    {
      path: '/group',
      name: 'Group',
      component: Group
    },
    {
      path: '/search',
      name: 'Search',
      component: Search
    }
  ]
})
复制代码
2.2 封装公用组件
  • header
<template>
  <div class="header">
    <router-link to="/">
      <h1 class="title"></h1>
    </router-link>
    <ul class="nav">
      <li>
        <router-link to="/movie"
                     style="color: #2384E8;">电影</router-link>
      </li>
      <li>
        <router-link to="/book"
                     style="color: #9F7860;">图书</router-link>
      </li>
      <li>
        <router-link to="/broadcast"
                     style="color: #E4A813;">广播</router-link>
      </li>
      <li>
        <router-link to="/group"
                     style="color: #2AB8CC;">小组</router-link>
      </li>
      <li>
        <router-link to="/search"
                     style="color: #00b600;">
          搜索
        </router-link>
      </li>
    </ul>
  </div>
</template>

<script>
export default {

}
</script>

<style lang="less" scoped>
.header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  max-width: 650px;
  padding: 0 18px;
  background: #ffffff;
  border-bottom: 1px solid #f3f3f3;
  margin: 0 auto;
  display: flex;
  align-items: center;
  height: 47px;
  justify-content: space-around;
  .title {
    color: #00b600;
    font-size: 20px;
    background: url(logo.png) no-repeat;
    background-size: cover;
    width: 46px;
    height: 22px;
    flex: 1;
    word-break: break-all;
  }
  .nav {
    display: flex;
    flex: 1;
    justify-content: space-around;
    li {
      display: inline-block;
    }
  }
  a {
    color: #494949;
    text-decoration: none;
    font-size: 15px;
  }
}
</style>
复制代码
  • footer
<template>
  <div class="footer">
    <div class="info">
      <img width="48"
           src="https://img3.doubanio.com/f/talion/7837f29dd7deab9416274ae374a59bc17b5f33c6/pics/card/douban-app-logo.png"
           alt="">
      <div class="info-content">
        <strong>豆瓣</strong>
      </div>
    </div>
    <div>
      <a href="https://www.douban.com/doubanapp/card/log?category=book_home&cid=&action=click_download&ref=http%3A//www.douban.com/doubanapp/app%3Fchannel%3Dcard_book_home%26direct_dl%3D1">去 App Store 免费下载 iOS 客户端</a>
    </div>
  </div>
</template>

<script>
export default {

}
</script>

<style lang="less" scoped>
.footer {
  margin-top: 50px;
  margin-bottom: 30px;
  padding: 0 0 20px 0;
  text-align: center;
  font-size: 15px;
  a {
    color: #42bd56;
  }
}
.info {
  display: inline-block;
  color: #111;
  margin-bottom: 15px;
  img {
    vertical-align: middle;
    margin-right: 12px;
    float: left;
  }
  .info-content {
    display: inline-block;
    strong {
      font-size: 24px;
      font-weight: normal;
      line-height: 48px;
    }
  }
}
</style>
复制代码
  • 首页推荐列表组件 recommendList.vue
<template>
  <div>
    <div v-if="hasImg">
      <div class="rightImg">
        <img :src="imgUrl"
             alt="">
      </div>
      <div class="leftContent">
        <h3 class="title">{{listItem.title}}</h3>
        <p class="desc">{{listItem.target.desc}}</p>
      </div>
    </div>
    <div v-else>
      <div class="leftContent"
           style="width:100%;">
        <h3 class="title">{{listItem.title}}</h3>
        <p class="desc">{{listItem.target.desc}}</p>
      </div>
    </div>
    <div class="author">
      <span>by {{listItem.target.author.name}}</span>
    </div>
  </div>
</template>

<script>
export default {
  props: [
    'item'
  ],
  data () {
    return {
      imgUrl: '',
      listItem: this.item,
      hasImg: this.item.target.cover_url ? true : false
    }
  },
  methods: {
    getImgUrl: function() {
      let _u = this.listItem.target.cover_url.substring(7)
      return this.imgUrl = 'https://images.weserv.nl/?url=' + _u
    }
  },
  mounted () {
    this.getImgUrl()
  }
}
</script>

<style lang="less" scoped>
.rightImg {
  float: right;
  width: 26%;
  margin-left: 20px;
  img {
    width: 100%;
  }
}
.leftContent {
  h3 {
    text-align: justify;
    font-size: 17px;
    font-weight: 500;
    line-height: 1.41;
    color: #494949;
    margin-bottom: 6px;
  }
  p {
    text-align: justify;
    color: #aaa;
    font-size: 12px;
    line-height: 1.5;
    display: -webkit-box;
    overflow: hidden;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3;
  }
}
.author {
  color: #ccc;
  padding-top: 10px;
}
</style>
复制代码
  • 电影和图书公用的组件 columnBox.vue
<template>
  <section>
    <header>
      <h2>{{title}}</h2>
      <a href=""
         v-if="type==='movieImg'">更多</a>
    </header>
    <div class="section-content">

      <ul class="row items movieImg"
          v-if="type === 'movieImg'">
        <li class="item item_movie"
            v-for="item in items"
            :key='item.name'>
          <a href="">
            <div class="item-poster">
              <img :src="'https://images.weserv.nl/?url='+item.cover.url.substring(7)"
                   alt="">
            </div>
            <div class="item-title">{{item.title}}</div>
            <div class="item-rank">

              <rate :rate="item.rating"
                    v-if="item.price === null"
                    type="price" />
              <span v-else>¥{{item.price}}</span>
            </div>
          </a>
        </li>
      </ul>
      <ul class="row movieBorder"
          v-else-if="type === 'movieBorder'">
        <li v-for="item in items.slice(0, 4)"
            :key="item.name">
          <a :href="item.url"
             :style="getColor()">{{item.name}}</a>
        </li>
        <li class="line"></li>
        <li v-for="n in items.slice(4)"
            :key="n.name">
          <a :href="n.url"
             :style="getColor()">{{n.name}}</a>
        </li>
      </ul>
      <ul class="row types movieText"
          v-else>
        <li v-for="item in items"
            :key="item.name">
          <a :href="item.url">{{item.name}}
            <span></span>
          </a>
        </li>
        <li v-show="items.length%2 !== 0"></li>
      </ul>
    </div>
  </section>
</template>

<script>
import Rate from '@/components/common/rate'
export default {
  components: {
    Rate
  },
  props: [
    'title',
    'items',
    'type'
  ],
  data () {
    return {
      colors: [
        '#4F9DED',
        '#42BD56',
        '#FFC46C',
        '#FF4055',
        '#CC3344',
        '#2384E8',
        '#3BA94D',
      ]
    }
  },
  methods: {
    getColor () {
      let index = parseInt(Math.random() * 7)
      return {
        color: this.colors[index],
        borderColor: this.colors[index]
      }
    }
  }
}
</script>

<style lang="less" scoped>
section {
  overflow: hidden;
  padding-top: 10px;
  header {
    padding: 0 18px;
    h2 {
      font-size: 18px;
      display: inline-block;
    }
    a {
      color: #42bd56;
      float: right;
      font-size: 14px;
      line-height: 24px;
    }
  }
}
.section-content {
  margin-bottom: -20px;
}
.row {
  overflow-x: auto;
  white-space: nowrap;
  border-bottom: 1px solid #f2f2f2;
  padding: 15px 0 43px 0;
  .item {
    width: 100px;
    display: inline-block;
    margin-left: 8px;
    .item-title {
      overflow: hidden;
      white-space: nowrap;
      text-overflow: ellipsis;
      font-size: 15px;
      padding: 5px;
      text-align: center;
    }
    img {
      width: 100%;
      height: 142px;
    }
  }
  .item:first-child {
    margin-left: 18px;
  }
  .item:last-child {
    margin-right: 18px;
  }
}
.item-rank {
  text-align: center;
}

.movieBorder {
  padding: 15px 15px 45px 15px;
  overflow-x: auto;
  white-space: nowrap;
}
.movieBorder li {
  display: inline-block;
  margin: 3px 5px;
}
.movieBorder .line {
  display: block;
  width: 100%;
}
.movieBorder li:nth-child(4)::after {
  display: block;
}
.movieBorder a {
  border: 1px solid #eee;
  border-radius: 5px;
  display: block;
  padding: 0 20px;
  height: 50px;
  line-height: 50px;
  font-size: 15px;
}

.types {
  padding-left: 18px;
}
.types li {
  width: 50%;
  float: left;
  border-top: 1px solid #eee;
  border-right: 1px solid #eee;
  box-sizing: border-box;
  padding: 0 20px 0 0;
  height: 40px;
  line-height: 40px;
}
.types li:nth-child(even) {
  border-right: none;
  padding-left: 18px;
}
.types li:nth-last-child(2),
.types li:nth-last-child(1) {
  border-bottom: 1px solid #eee;
}
.types li a {
  font-size: 15px;
  color: #42bd56;
  cursor: pointer;
  display: block;
}
.types li a span {
  float: right;
  width: 8px;
  height: 8px;
  border-right: 1px solid #ccc;
  border-bottom: 1px solid #ccc;
  transform: rotate(-45deg);
  margin-top: 15px;
}
</style>
复制代码
  • 评分组件 rate.vue
<template>
  <span>
    <span v-if="rate !== null">
      <span class="star starY"
            v-for="n in starY"
            :key="'key'+n"></span>
      <span class="star starG"
            v-for="n in starG"
            :key="n+'key'"></span>
      <span class="rating">{{rate.value.toFixed(1)}}</span>
    </span>
    <span v-else>暂无评分</span>
  </span>
</template>

<script>
export default {
  props: [
    'rate'
  ],
  data () {
    return {
      starY: 0,
      starG: 0
    }
  },
  created () {
    let value = this.rate !== null && this.rate.value
    this.starY = parseInt(value / 2)
    this.starG = 5 - this.starY
  }
}
</script>

<style lang="less" scoped>
.star {
  display: inline-block;
  width: 10px;
  height: 10px;
  background-size: contain;
  background-repeat: no-repeat;
}
.starY {
  background-image: url("./../../assets/img/star_1.png");
}
.starG {
  background-image: url("./../../assets/img/star_2.png");
}
.rating {
  margin-left: 3px;
}
</style>
复制代码
2.3 搭建页面
  • index.vue
<template>
  <div>
    <ul class="subNav">
      <li class="subNavItem">
        <router-link to="">影院热映</router-link>
      </li>
      <li class="subNavItem">
        <router-link to="">近期值得看的美剧</router-link>
      </li>
      <li class="subNavItem">
        <router-link to="">豆瓣时间</router-link>
      </li>
      <li class="subNavItem">
        <router-link to="">使用豆瓣App</router-link>
      </li>
    </ul>
    <section id="recommend-feed">
      <div>
        <a :href="item.target.uri"
           v-for="item in recommendData"
           :key="item.id"
           class="feed-item">
          <recommend-list :item="item"></recommend-list>
        </a>
      </div>
    </section>
  </div>
</template>

<script>
import { getRecommendData } from '@/assets/api/api.js'
import recommendList from '@/components/common/recommendList'

export default {
  components: {
    recommendList
  },
  data () {
    return {
      recommendData: []
    }
  },
  created () {
    this.startGetRecommendData()
  },
  methods: {
    startGetRecommendData () {
      getRecommendData().then((res) => {
        this.recommendData = res.recommend_feeds
      })
    }
  },
  mounted () {
  }
}
</script>

<style lang="less" scoped>
.subNav {
  padding: 20px 10px;
  overflow: hidden;
  justify-content: space-around;
  .subNavItem {
    float: left;
    width: 50%;
    padding: 5px;
    box-sizing: border-box;
    text-align: center;
    a {
      display: block;
      margin: 0 auto;
      padding: 10px;
      color: #494949;
      background: #f6f6f6;
    }
  }
}
#recommend-feed {
  min-height: 480px;
  color: #494949;
  padding: 0 15px;
  .feed-item {
    display: block;
    padding: 25px 0 25px 0;
    border-bottom: 1px solid #f1f1f1;
  }
}
</style>
复制代码

  • movie.vue
<template>
  <div>
    <div class="cover">
      <column-box title="影院热映"
                  type="movieImg"
                  :items="hotMoviesData"></column-box>
      <column-box title="免费在线观影"
                  type="movieImg"
                  :items="freeMoviesData"></column-box>
      <column-box title="新片速递"
                  type="movieImg"
                  :items="newMoivesData"></column-box>
      <column-box title="发现好电影"
                  type="movieBorder"
                  :items="goodMoviesData"></column-box>
      <column-box title="分类浏览"
                  type="movieText"
                  :items="moviesTypes"></column-box>
    </div>
    <Footer></Footer>
  </div>
</template>

<script>
import Footer from '@/components/footer/footer'
import ColumnBox from '@/components/common/columnBox'
import { getHotMovies, getFreeMovies, getNewMovies, getMoviesTypes, getGoodMoives } from '@/assets/api/api.js'
export default {
  components: {
    Footer,
    ColumnBox
  },
  data () {
    return {
      hotMoviesData: [],
      newMoivesData: [],
      freeMoviesData: [],
      goodMoviesData: [],
      moviesTypes: []
    }
  },
  created () {
    this._getHotMovies()
    this._getNewMovies()
    this._getFreeMovies()
    this._getGoodMovies()
    this._getMoviesTypes()
  },
  methods: {
    _getHotMovies () {
      getHotMovies().then(res => {
        this.hotMoviesData = res.subject_collection_items;
      })
    },
    _getNewMovies () {
      getNewMovies().then(res => {
        this.newMoivesData = res.subject_collection_items;
      })
    },
    _getFreeMovies () {
      getFreeMovies().then(res => {
        this.freeMoviesData = res.subject_collection_items;
      })
    },
    _getGoodMovies () {
      getGoodMoives().then(res => {
        this.goodMoviesData = res.data
      })
    },
    _getMoviesTypes () {
      getMoviesTypes().then(res => {
        this.moviesTypes = res.data
      })
    }
  }
}
</script>

<style lang="less" scoped>
</style>
复制代码

  • book.vue
<template>
  <div>
    <column-box title="最受关注图书丨虚构类"
                type="movieImg"
                :items="fictionBookData"></column-box>
    <column-box title="最受关注图书丨非虚构类"
                type="movieImg"
                :items="noFictionBookData"></column-box>
    <column-box title="豆瓣书店"
                type="movieImg"
                :items="bookShopData"></column-box>
    <column-box title="发现好图书"
                type="movieBorder"
                :items="goodBookData"></column-box>
    <column-box title="分类浏览"
                type="movieText"
                :items="bookTypes"></column-box>
    <Footer></Footer>
  </div>
</template>

<script>
import Footer from '@/components/footer/footer'
import ColumnBox from '@/components/common/columnBox'
import { getfictionBook, getnoFictionBook, getBookShop, getBookTypes, getGoodBook } from '@/assets/api/api.js'
export default {
  components: {
    ColumnBox,
    Footer
  },
  data () {
    return {
      fictionBookData: [],
      noFictionBookData: [],
      bookShopData: [],
      goodBookData: [],
      bookTypes: []
    }
  },
  created () {
    this._getfictionBook()
    this._getnoFictionBook()
    this._getBookShop()
    this._getBookTypes()
    this._getGoodBook()
  },
  methods: {
    _getfictionBook: function() {
      getfictionBook().then(res => {
        this.fictionBookData = res.subject_collection_items
      })
    },
    _getnoFictionBook () {
      getnoFictionBook().then(res => {
        this.noFictionBookData = res.subject_collection_items
      })
    },
    _getBookShop () {
      getBookShop().then(res => {
        this.bookShopData = res.subject_collection_items
      })
    },
    _getBookTypes () {
      getBookTypes().then(res => {
        this.bookTypes = res.data
      })
    },
    _getGoodBook () {
      getGoodBook().then(res => {
        this.goodBookData = res.data
      })
    }
  }
}
</script>

<style lang="less" scoped>
</style>
复制代码

问题:豆瓣API请求到的图片,在页面上显示时出现403的错误,百度谷歌了一个上午得出的结论如下:

豆瓣API是有请求次数限制的,这会引发图片在加载的时候出现403问题,视图表现为“图片加载不出来”,控制台表现为报错403。

解决方法如下:

在请求到的图片链接前面加上 images.weserv.nl/?url= (这是一个专门缓存图片的网址),但是访问速度真的很感人(强迫症患者慎用~~)。

data () {
    return {
      imgUrl: ''
    }
  },
  methods: {
    getImgUrl: function() {
      let _u = this.item.target.cover_url.substring(7)
      console.log(_u)
      return this.imgUrl = 'https://images.weserv.nl/?url=' + _u
    }
  },
  mounted () {
    this.getImgUrl()
  }
复制代码

下面是睡意来袭的分割线

=====================================================================

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值