最近一直在学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、新片速递
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()
}
复制代码
下面是睡意来袭的分割线
=====================================================================