Open xingbofeng opened 7 years ago
利用业余时间写了个用vue全家桶仿写豆瓣电影wap版。
原计划仿写完所有页面,碍于豆瓣的接口API有限,实现页面也有限。
由于公开的豆瓣接口具有访问次数限制,克隆到本地体验效果更加!
web端访问已设置宽度适配。
进入GitHub查看本项目源码
欢迎issue,pr,star or follow!我将继续开源更多有趣的项目!
issue
pr
star
follow
点击进入
本地体验线上版本:
git clone https://github.com/xingbofeng/douban-movie.git cd douban-movie npm install node server/index.js
打开浏览器访问http://localhost:3000/ 体验!
git clone https://github.com/xingbofeng/douban-movie.git cd douban-movie npm install npm run dev
打开浏览器访问http://localhost:8080/ 体验!
vue
vuex
vue-router
webpack
webpack-dev-server
http-proxy-middleware
express
iView
vue-lazyload
rem
flex
grid
postman
输入搜索关键词,回车键搜索,或者点击搜索按钮。
回车键
| |—— build |—— config |—— server 服务端 | |—— index.js 服务端启动入口文件 | |—— static 打包后的资源文件 | |__ index.html 网页入口 | |——src 资源文件 | |—— assets 组件静态资源库 | |—— components/ | | |____ Common/ 可复用的组件 | | |____ ... 其它不可复用的单独组件 | | | |—— router/ | | |____ index.js 路由入口 | | |____ server.js 封装`Ajax`函数 | | |____ serverConfig.js 服务端接口配置 | | |____ routes/ 每个页面的路由, 我们在其生命周期钩子函数中改变`vuex`中的状态 | | | |—— 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下列相关文件
/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的状态,然后根据这个状态来渲染页面。
currentPage
具体代码可见/src/containers/Tag.vue。
/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。
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。未超过频率则直接在函数中返回。
flag
具体代码可见src/containers/More.vue
scrolling() { // scrolling函数用于作函数节流 if (this.scrollFlag) { return; } this.scrollFlag = true; setTimeout(() => { this.handleScroll(); this.scrollFlag = false; }, 20); }
这里主要是在vuex中设定两个状态。根据这两个状态返回不同的页面。
具体代码可见src/App.vue
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,做到移动端的适配。
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);
利用业余时间写了个用vue全家桶仿写豆瓣电影wap版。
原计划仿写完所有页面,碍于豆瓣的接口API有限,实现页面也有限。
由于公开的豆瓣接口具有访问次数限制,克隆到本地体验效果更加!
web端访问已设置宽度适配。
进入GitHub查看本项目源码
欢迎
issue
,pr
,star
orfollow
!我将继续开源更多有趣的项目!在线版
点击进入
本地体验线上版本:
打开浏览器访问http://localhost:3000/ 体验!
使用
打开浏览器访问http://localhost:8080/ 体验!
部分效果截图
工具&技能
vue
+vuex
+vue-router
全家桶webpack
+webpack-dev-server
+http-proxy-middleware
进行本地开发环境http请求转发,实现跨域请求express
的http-proxy-middleware
实现请求转发iView
一款vue的组件库vue-lazyload
实现图片懒加载rem
+flex
+grid
实现移动端适配postman
接口测试工具实现功能
首页
搜索页
输入搜索关键词,
回车键
搜索,或者点击搜索按钮。查看更多
电影详情
搜索结果页
目录结构
开发心得
如何缓存数据
这个问题在我之前的的项目总结已经总结过。
加入我们有电影条目A、B、C三个电影条目详情。进入A加载A,进入B加载B。此时也要把A缓存入vuex中。
可以类似于下面的写法。
具体代码可见
/src/router/routes
下列相关文件翻页加载
其实这个在之前的React项目中也有做过,设置一个
currentPage
的状态,然后根据这个状态来渲染页面。具体代码可见
/src/containers/Tag.vue
。滚动加载
类似于瀑布流布局的实现方式,当用户滚动到距离页面底部一定范围的时候去请求后端接口。
具体代码可见
src/containers/More.vue
。滚动节流
滚动节流主要作用是控制滚动事件的频率,设置一个
flag
。未超过频率则直接在函数中返回。具体代码可见
src/containers/More.vue
这里主要是在
vuex
中设定两个状态。根据这两个状态返回不同的页面。具体代码可见
src/App.vue
在路由钩子函数中改变状态
之前在公司做React项目的时候运用了universal-router,当时我们可以在进入路由的时候dispatch一个action改变状态,并且使用async/await函数实现异步。
贴一段之前的React代码:
类似的,在vue中我们也可以这么做!
具体代码可见
/src/router/routes
下的相关代码Ajax的封装
其实我就是不想用Ajax操作的相关库罢了……
代理的配置
为了解决浏览器跨域问题,需要在本地服务端配合实现请求转发。
实际环境中,服务器端配置
移动端的适配
我们使用
rem
作单位,本项目中标准为1rem = 100px,适配750px设备。浏览器执行下列代码,改变根元素的
font-size
,做到移动端的适配。