做爲當前熱門的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做者端...