做为当前热门的js框架之一,以数据驱动和组件构建为核心的vue实在是使人着迷。用vue写个项目,是对这段学习时间的一个自我巩固与自我提高。我在写这个项目的过程当中,爬过不少坑,碰过不少壁,把他们分享出来,但愿能让各位看官老爷吸取一些有价值的东西。css
咱们都知道,vue的产生核心之一在于开发大型单页应用,一个大型单页应用归根结底仍是一个html,若是不对项目进行一些优化的话,浏览器会将全部没有设置路由懒加载的组件,webpack打包生成的依赖js,页面样式文件所有加装完毕再将网页渲染出来。这是将致使一个很是可怕的白屏时间,带给用户的体验效果极差!咱们能够从几个方面减轻压力...html
在webpack配置中能够设置externals(外部)参数,不对一些依赖进行打包,而已cdn的形势引入。配置也很简单,代码以下:前端
externals: {
'vue': 'Vue',
'vuex': 'Vuex',
'vue-router': 'VueRouter'
}
复制代码
json的key值为引入资源的名字,value值表示该模块提供给外部引用的名字,由对应的库自定。例如,vue为Vue,vue-router为VueRouter.
别忘了在index.html用script标签引入你以前定义在externals中的依赖哦。vue
vue-router提供的路由懒加载可让组件按需加载,减轻加载压力。配置示例:node
{
path: '/Headlines',
name: 'Headlines',
component (resolve) {
require(['@/page/homeComponents/Headlines'], resolve)
}
},
{
path: '/Joke',
name: 'Joke',
component (resolve) {
require(['@/page/homeComponents/Joke'], resolve)
}
},
{
path: '/City',
name: 'City',
component (resolve) {
require(['@/page/homeComponents/City'], resolve)
}
复制代码
不过不要过分使用路由懒加载,不然在切换的路由的时候可能会出现闪屏的状况哦...webpack
若是你使用了一些vue的ui框架,很是不推荐在main.js中直接将全部组件引入,而是引入的项目中须要的组件,这样不会形成资源浪费,也能减轻浏览器压力。(由于个人项目使用的是vant组件库因此以vant为例)ios
import Vue from 'vue';
import Vant from 'vant';
import 'vant/lib/index.css';
Vue.use(Vant);
复制代码
使用babel-import插件
babel-plugin-import 是一款 babel 插件,它会在编译过程当中将 import 的写法自动转换为按需引入的方式。 在babelrc中加入以下配置:css3
// 在.babelrc 中添加配置
// 注意:webpack 1 无需设置 libraryDirectory
{
"plugins": [
["import", {
"libraryName": "vant",
"libraryDirectory": "es",
"style": true
}]
]
}
复制代码
手动引入git
import Button from 'vant/lib/button';
import 'vant/lib/button/style';
复制代码
由于这是一个先后端分离的项目,因此在访问接口时会出现跨越问题。通常解决跨域问题三种方式:es6
jsonp只能处理get请求,因此在对一些开放形接口会使用jsonp,在前端开发过程当中使用proxy代理解决跨域问题居多。
直接在package.json文件中配置
"proxy":"http://localhost:3000"
复制代码
也能够在webpack中详细配置
module.exports = {
//...
devServer: {
proxy: {
'/api1': { // 若是API中有这个字符串,那么就开始匹配代理,
target: 'http://www.xxx.com/', // 将跨域前往的目标域名或IP地址
pathRewrite: {'^/api' : ''}, // 路径重写,/api会被替换为空
changeOrigin: true, // target是域名的话,须要这个参数,
secure: false, // 设置支持https协议的代理
},
'/api2': {
.....
}
}
}
};
复制代码
三者受制于vuex的一个数组,和数组当前的激活索引,只须要改变数组,数组索引就能完成三者之间的联动
将首页须要的全局变量定义在vuex中,并加上相应的mutations和actions
const moduleHome = {
state: {
navbar: [
{
name: '头条',
component: 'Headlines'
},
{
name: '段子',
component: 'Joke'
},
{
name: '南昌',
component: 'City'
},
{
name: '笑话',
component: 'Easetime'
},
{
name: '图片',
component: 'Picture'
}
],
moreList: [
{
name: '星座',
component: 'Constellation'
},
{
name: '音乐',
component: 'Musi'
},
{
name: '教育',
component: 'Education'
},
{
name: '佛学',
component: 'Buddhism'
}
],
active: 0,
caches: {
headlines: []
},
page: {
headlines: 0
}
},
mutations: {
changeActive (state, index) {
state.active = index
},
resetActive (state) {
state.active = 0
},
changeCache (state, opt) {
let { arr, name } = opt
state.caches[name] = arr
},
changePage (state, opt) {
let { page, name } = opt
state.page[name] = page
},
changeNavbar (state, navbar) {
state.navbar = navbar
},
changeMoreList (state, moreList) {
state.moreList = moreList
}
},
actions: {
changeCache: ({commit}, opt) => commit('changeCache', opt),
changePage: ({commit}, opt) => commit('changePage', opt),
changeActive: ({commit}, index) => commit('changeActive', index),
resetActive: ({commit}) => commit('resetActive')
}
}
复制代码
在使用vuex以前咱们要理解为何要使用它,用bus不行吗,固然使用bus也能够完成组件间的通讯。可是一旦共享状态的组件数量过多,使用bus经过传递的参数的办法未免过于繁琐,组件之间的状态同步须要用多个函数来维持,增大了维护代码的压力。
我把首页的状态抽成了一个module,官方文档对在module中怎么使用action,state,没有详细描述,它的用法以下:
// 导出方式
export default new Vuex.Store({
modules: {
home: moduleHome,
global: moduleGlobal
}
})
// 使用state
$store.state.home // 能够看出就算将状态模块化,它其实也被隐
性包含与一个更大的state中
// 使用actions
对于actions的使用其实不管action在哪一个module中均可以被mapActions映射出来
因此用法和不用module是同样的(没想到吧)
import { mapActions } from 'vuex'
...mapActions([
'changeCache', // home中的action
'changeLogin' // global中的action
])
复制代码
顶部导航
active: function (newVal, oldVal) {
// 向右滑动事件导航跟踪处理
if (newVal % 2 === 0 && newVal - oldVal === 1) {
let interval = setInterval(() => {
this.$refs['scroll'].scrollLeft += this.$refs['scroll'].offsetWidth * 0.2 / 10
setTimeout(() => {
clearInterval(interval)
}, 100)
}, 10)
return
}
// 向左滑动事件导航跟踪处理
if (newVal % 2 === 0 && newVal - oldVal === -1) {
let interval = setInterval(() => {
this.$refs['scroll'].scrollLeft -= this.$refs['scroll'].offsetWidth * 0.2 / 10
setTimeout(() => {
clearInterval(interval)
}, 100)
}, 10)
return
}
// 点击事件处理
if (Math.abs(newVal - oldVal) !== 1) {
let interval = setInterval(() => {
this.$refs['scroll'].scrollLeft += this.$refs['scroll'].offsetWidth * 0.2 * (newVal - oldVal) / 10
setTimeout(() => {
clearInterval(interval)
}, 100)
}, 10)
}
}
// 使用interval模仿滚动动画
复制代码
swiper
mounted () {
const that = this
this.myswiper = new Swiper('.swiper-container',
{
// touchRatio: 0.8,
watchSlidesProgress: true,
observer: true,
on: {
slideChangeTransitionEnd: function () {
that.changeActive(this.activeIndex)
that.$router.push(that.contentArr[this.activeIndex].component)
that.pushRoute(that.contentArr[this.activeIndex].component)
that.shiftRoute()
}
}
})
const sWidth = this.$refs['s-con'].offsetWidth
this.myswiper.setTranslate(-sWidth * this.active)
}
watch: {
active: function (newVal) {
const sWidth = this.$refs['s-con'].offsetWidth
this.myswiper.setTranslate(-sWidth * newVal)
}
}
复制代码
在swiper滑动结束后,路由会进行跳转,在这里,有对路由变换的栈入,队列出的操做,是由于我对路由History的功能还不够知足,自我定义了一个栈来描述路由变换,咱们会在后面提到它的用法。
隐藏设置
dragStart (el) {
this.timeout = setTimeout(() => {
let index = el.target.dataset.index
if (this.edited && index !== '0') {
this.dragged = true
this.moveText = this.list[index]
this.falseDom.index = index
this.falseDom.oldX = el.target.offsetLeft
this.falseDom.oldY = el.target.offsetTop - 10
this.falseDom.width = el.target.offsetWidth
this.falseDom.height = el.target.offsetHeight
}
}, 300)
},
dragMove (el) {
if (this.dragged) {
this.dragIndex = el.target.dataset.index
let draged = document.querySelector('.changed')
if (draged) {
this.trueDom = false
this.list[this.dragIndex] = ''
}
let falseDom = document.querySelector('.falseDom')
falseDom.style.left = el.changedTouches[0].pageX - el.target.offsetWidth / 2 + 'px'
falseDom.style.top = el.changedTouches[0].pageY - el.target.offsetHeight / 2 + 'px'
this.ready = true
}
},
dragEnd (el) {
clearTimeout(this.timeout)
if (this.dragged) {
this.dragged = false
this.ready = false
this.trueDom = true
let falseDom = document.querySelector('.falseDom')
let newX = parseFloat(falseDom.style.left)
let newY = parseFloat(falseDom.style.top)
let goX = parseInt((newX - this.falseDom.oldX) / (this.falseDom.width))
let goY = parseInt((newY - this.falseDom.oldY) / this.falseDom.height)
let newIndex = parseInt(this.dragIndex) + goY * 4 + goX
if (newIndex <= 0) {
this.list[this.dragIndex] = this.moveText
return
}
console.log(goX, goY, newIndex)
this.list = this.list.filter(text => text !== '')
this.list.splice(newIndex, 0, this.moveText)
return
// console.log(this.list)
}
if (this.edited && el.target.dataset.index !== '0') {
let index = el.target.dataset.index
let deleteText = this.list[index].name
let component = this.list[index].component
this.list.splice(index, 1)
this.moreList.push({name: deleteText, component})
return
}
let index = el.target.dataset.index
this.changeActive(index)
this.$router.push(this.list[index].component)
this.closeColumn()
}
复制代码
这里的计算位置算法原理以下: 由于item每4个排一行,用新的left减去初始left除以item宽度就能算出,item在x轴位移的增量,同理算出y轴的增量,由于一行排4个,因此新的Index = oldIndex + goY * 4 + goX
在这里我在vuex中的home模块中定义了一个page状态用来存储分页状态,不过我在数据库里只存了两页,因此拿数据时一直交替page伪造无限滚动。我在项目中使用axios发送ajax请求,对于axios的使用,文档中讲解的十分详细,我就很少谈了。
在下拉刷新中,我使用并改造了vant的Notify组件。
<!--js-->
Notify({
message: '成功为您推荐5条新闻',
className: 'notify',
duration: 800
})
<!--css-->
.notify
top 2.4rem /* 90/37.5 */
left 50%
transform translateX(-50%)
animation show .1s linear
opacity 1
@keyframes show {
0% {
width 70%
top 0
}
10%{
width 100%
top 2.4rem /* 90/37.5 */
}
}
复制代码
细心的看官老爷们会发现个人css代码(这里使用的是stylus)中有一些不一样之处,我定义了一个opacity:1。由于这是一个暗示开启GPU加速的江湖黑话。
为何要使用硬件加速
咱们常常会发现一些css3的动画效果在移动端(甚至pc端)会有些卡顿,这是由于CSS的 animations, transforms以及transitions不会自动开启GPU加速,而是由浏览器的缓慢的软件渲染引擎来执行。为了让动画效果更佳流畅,咱们能够开启GPU加速来达到目的。
不过只有个别css属性能够启动硬件加速:
(使用transform开启硬件加速须要使用3d去小骗一下,如transform: translate3d(0, 0, 0),transform: translateZ(0), transform: rotateZ(360deg))
不过不要去迷恋硬件加速,过分使用它会给你的应用带来一些隐患
在这个项目中我使用了移动端适应插件flexible.js,它对移动端不一样屏幕分辨率的适应提供很好的帮助。它的做用原理是把屏幕宽度1/10定为1rem,我在谷歌浏览器预览效果时用的是iphone6/7/8因此在上面有个
top 2.4rem /* 90/37.5 */
复制代码
flexible的使用也很是简单只要在index.html中用cdn引入就好了...
在图片没有加载完以前,使用一个灰色的div代替,图片Img,onload事件以后再显示出来。
<div src="" alt="" v-if="!includes(0) && !cache" class="img"></div>
<div src="" alt="" v-if="!includes(1) && !cache" class="img"></div>
<div src="" id='imgLast' alt="" v-if="!includes(2) && !cache" class="img"></div>
<img :src="item.img[0]" alt="" @load="load(0)" :data-src='item.img[0]' v-show="includes(0) || cache">
<img :src="item.img[1]" alt="" @load="load(1)" :data-src='item.img[1]' v-show="includes(1) || cache">
<img :src="item.img[2]" alt="" id="imgLast" @load="load(2)" :data-src='item.img[2]' v-show='includes(2) || cache'>
<!--js-->
load (num) {
this.img.push(num)
},
includes (num) {
return this.img.includes(num)
}
复制代码
在template里面不能使用es6方法因此我在method中调用,思路就是,有一个图片就绪数组,当有图片onload时就把它的index传进数组,只有判断数组里是否有它,就能肯定遮挡层的去留。
link (index) {
this.linked = index
let item = this.feedio[index]
item.read = 1
setTimeout(() => {
this.$router.push({name: 'Arcticle', params: { item }})
this.pushRoute('Arcticle')
this.shiftRoute()
// 只能传一个参数
this.changeCache({
arr: this.feedio,
name: 'headlines'
})
}, 300)
复制代码
在这里咱们又看到了我以前提到的本身定义的路由栈了,这个路由栈只能保存两个路由,一个是上一次的路由,一个是当前路由。若是判断你的下一次路由和上一次路由名称相同,那就会使用缓存来代替get请求,这里传入了一个index表示阅读的新闻的位置,并把它的read属性标记为1,因此就能出现如上的已读效果。
这里我还对路由设置了0.3秒延迟,这是为了展现一个路由切换动画,若是直接用routerlink就不能看到那个像水同样像外弥漫的效果了,那个效果的原理以下:
.active
animation link .3s ease forwards
@keyframes link {
0% {
width 60%
border-radius 0
}
20% {
width 100%
border-radius 1.066667rem /* 40/37.5 */
}
100%{
width 100%
border-radius 0
}
}
复制代码
这里的速度曲线用的是ease表示先加速后减速,就像一颗石子落入池塘,水纹扩散的速度也是先加速后减速的,至于那个曲线填满效果,是经过改变盒子的border-radius来实现的
在这个项目中,点击item进入的文章页面是动态渲染出来的,咱们用postman测试一下接口,看下返回的数据(postman是一个测试接口工具)
使用v-html就能把html直接渲染出来
<div id="article" v-html="article.html"></div>
复制代码
在这里有两个滚动需求,一个是当滚动超过做者后,做者信息上跳到头部,跟帖按钮变色拉长,在文章底部有个关闭限制,当上拉超过某个高度时能够关闭页面。
this.myscroll.on('scroll', pos => {
this.scrollY = pos.y
this.tipShow = true
if (this.scrollY < -100) {
this.show = true
} else {
this.show = false
}
})
this.myscroll = new BScroll(this.$refs['bscrll'], {
probeType: 3,
pullUpLoad: true,
click: true
})
复制代码
头部那个变化的实现比较容易,bscroll的scroll事件能够不断监听当前滚动位置,不过为了实时监测,一点别忘了在初始化bscroll时定义probeType:3,至于bscroll的这些属性我就不细说了...
在实现底部的上拉关闭时,bscroll的事件就没有想象中的那么给力了。在这里我使用了他的pullingUp事件
this.myscroll.on('pullingUp', () => {
this.maxY = this.scrollY
})
复制代码
可是这个事件是否是设计的有点毒,它这个pullingUp只能触发一次,若是要再次触发,须要人为去调整他的finsh状态,而后,滚动一到底部它就会瞬间触发,说好的上拉呢,我还没拉它就触发了!真是气死噶人。
还有,能够滚动的高度居然不等于被包裹元素的offsetHeight,致使我须要在滚动到底部时,定义一个最大高度来获取它.而后上拉关闭原理以下:
if (this.maxY - this.scrollY >= 60 && this.maxY) {
this.tipMsg = '释放关闭此页'
this.tipIndex = 1
this.close = true
} else {
this.tipMsg = '上拉关闭此页'
this.tipIndex = 0
this.close = false
}
复制代码
这里bscroll的滚动高度是负值,因此我设置当上拉高度超过底部的60像素时,就能够实现关闭页面
这里的19天前,使用了moment的fromNow方法。fromNow方法会返回一个与今日的时间差,它会经过时间间隔的大小来肯定使用什么时间单位。不过,它返回的是英文,好比''一天前''返回的是''a day ago'',须要咱们人为去修改它,我这里比较直接用的是if,else判断...
<!--translate.js-->
const translate = (time) => {
if (time.includes('a ') || time.includes('an ')) {
time = time.replace(/a |an /, '1')
}
if (time.includes('hour ') || time.includes('hours ')) {
time = time.replace(/hour |hours /, '小时')
}
if (time.includes('day ') || time.includes('days ')) {
time = time.replace(/day |days /, '天')
}
if (time.includes('month ') || time.includes('months ')) {
time = time.replace(/month |months /, '月')
}
if (time.includes('years ') || time.includes('year ')) {
time = time.replace(/years |year /, '年')
}
if (time.includes('ago')) {
time = time.replace(/ago/, '前')
}
if (time.includes(' ')) {
time = time.replace(/ /, '')
}
return time
}
module.exports = translate
<!--sunTime.js-->
const moment = require('moment')
const translate = require('./translate')
const sumTime = (time) => translate(moment(time, 'YYYY-MM-DD HH:mm:ss').fromNow())
module.exports = sumTime
复制代码
由于这些图片是v-html中渲染出来的,不是虚拟Dom,要取到图片的src须要使用到事件委托。
事件委托是利用事件冒泡实现一对多监听的方式,常见如ul,li,若是要给li绑定事件,则全部li都须要进行绑定,不利于代码的稳定性,而事件委托则是在ul中绑定事件,判断触发的target是否是须要绑定的元素,就能够完成一对多监听。
imageView (e) {
let source = e.target
if (source.nodeName === 'IMG') {
if (!this.img.imgs) {
let imgs = []
let imgArr = this.$refs['article'].querySelectorAll(`img`)
imgArr.forEach((img, index) => {
img.setAttribute('index', index)
imgs.push(img.src)
})
this.img.imgs = imgs
}
this.img.index = source.getAttribute('index')
this.changeImg(this.img)
this.viewImg()
}
},
复制代码
咱们在App.vue中事先准备好了一个imgview的组件,而后它的渲染条件是判断vuex中的的一个imgView数组有没有值,先前我在事件委托时,将img的url赋值给了这个数组,因此它在事件发生后就会显示出来,至于里面的swiper效果,比较简单,这里就很少提了。
只有登陆后才能跟帖,未登陆状态点击跟帖会自动弹出登陆界面。由于vue是单页应用,我结合LocalStorage,SessionStorage和vuex存储登陆状态。登陆完成后,在LocalStorage中存储userId,SessionStorage中存储md5加密后的密码,vuex中存一些头像,昵称等信息。
这个输入框既能够发跟帖,也能对跟帖进行留言。
这里在textarea中,给placeholder绑定了一个数据,只要判断是否有这个数据就能区分是发跟帖仍是回复跟帖。
关于点赞,在这里一些vue的初学者,可能会对一列用v-for动态渲染的元素,只改变其中某一个的样式,感到困扰,我这里提供我解决的方法。
<img :src="includes(index)?praisehover:praise" alt="" @click="praised(index)">
复制代码
这里的index是v-for的第二个参数,表明数组索引,只要在事件中把这个做为参数传入就能解决问题。
视频这里的页面结构以下:
<div class="video" :style="'background-image:url('+item.bgImg+')'" v-if='index!=active'>
<div class="title">{{item.title}}</div>
<div class="play-times">{{item.playTimes}}播放</div>
<div class="img">
<img src="../../assets/video/play.svg" alt="" class="icon" @touchend="play(index)">
</div>
<div class="time">{{item.time}}</div>
</div>
<div class="video" v-else>
<video :src='item.video' ref='video' controls='controls' id='video'
poster="../../assets/img/loading.gif"></video>
</div>
复制代码
视频和封面盒子用的是一个类名,直接用v-if,v-else判断,完成替换,但这里要提到的一个知识点就是nextTick。
定义: 在下次 DOM更新循环结束以后执行延迟回调。在修改数据以后当即使用这个方法,获取更新后的 DOM
nextTick涉及到vue的异步加载操做,会用在由于数据更新而变化的dom结构上,常见如v-if。由于数据驱动页面须要必定的时间,若是刚改变数据,就去操做与之新生成的dom,可能会获取不到,而没法完成操做。
只须要将操做放在nextTick的回调函数中,vue就会在dom完成渲染后执行...
play (index) {
this.active = index
this.$nextTick(() => {
this.$refs.video[0].play()
})
}
复制代码
这里,ref取的是一个数组,由于video在虚拟DOM中用v-for渲染,而我定义的ref都是video,因此,全部ref命名同样的DOM结构会存在一个数组中。可是实际上,用v-if控制,数组中只会出现被激活的video,因此是数组索引是0。
细心的看官老爷,可能会发现,我使用的是touchend来播放视频,为啥没用click呢,为啥不是touchstart呢。由于click在移动端有些浏览器中,不能首次触发video.play(),点击事后,还要再点击控制条的播放才能播,这可真让人头大。
在查阅过不少资料中,获得的结果是,在触发事件后有延迟,video.play就可能执行不了,而click事件在移动端是有0.3秒延迟的。由于移动端会对,双击事件进行判断,控制页面缩放。
然而,我在meta里面禁用了页面缩放,click事件仍是不能播。终于在一篇资料中获得启发,video在移动端不支持自动播放,就算用js也不行,必定要引导用户在点击,触屏,或触屏滑动时再用js播放。
触屏?用click用习惯了,都忽略touch系列事件了。先改为touchstart,卡住了...
touch事件的执行顺序是touchstart->touchmove->touchend,touchstart触发太快,video还没到canplay阶段,用touchend就能够播放(不过个人视频源都比较小,我不知道换大的视频源会不会出现问题)。
很是感谢看官老爷们能穿越这篇文章沙漠,来到最后。我在文章中用实战穿插知识点的分享方式,不知道有没有给各位在沙漠中见到绿光。最后源码奉上,在这个仓库中还有这个项目的Node后端,和正在开发的React做者端。若是有机会,我会在以后继续发表后端项目和React做者端...