項(xiàng)目介紹
技術(shù)棧
- Vue2.0 (核心框架)
- Vue-CLI 4.0 (Vue腳手架)
- Vue-Router (SPA頁(yè)面路由)
- Vuex (狀態(tài)管理)
- Axios (網(wǎng)絡(luò)請(qǐng)求)
- ES 6 (JavaScript 語(yǔ)言的下一代標(biāo)準(zhǔn))
- Less (CSS 預(yù)處理器)
- Better-Scroll (讓移動(dòng)端的滾動(dòng)更為流暢)
- FastClick (解決移動(dòng)端點(diǎn)擊300ms延遲)
- Vue-Lazyload (懶加載工具)
- PostCss (css代碼轉(zhuǎn)化工具)
初始化項(xiàng)目
通過(guò) Vue-CLI 4.2.3 創(chuàng)建項(xiàng)目
vue create mall
目錄劃分及相關(guān)配置
劃分目錄結(jié)構(gòu) (父級(jí)目錄為 src)
- assets: 創(chuàng)建 img、css 文件夾
- common: 存放一些公共的 JS 文件, 例如公共的常量、方法、工具類
- components: 存放一些公共的組件, 這里還可以分成兩個(gè)文件: common 和 content
- common: 存放一些完全公共的組件, 完全獨(dú)立的組件內(nèi)容, 即使存放在下一個(gè)項(xiàng)目也能用的組件
- content: 對(duì)本項(xiàng)目業(yè)務(wù)來(lái)說(shuō)是公共的, 存放在下一個(gè)項(xiàng)目里時(shí)不能使用的組件
- views: 主要存放一些視圖的相關(guān)業(yè)務(wù)和代碼
- router: 存放一些路由相關(guān)的代碼
- store: 存放一些 Vuex 公共狀態(tài)管理相關(guān)的內(nèi)容
- network: 存放一些網(wǎng)絡(luò)相關(guān)的代碼
引入兩個(gè)初始化 CSS 文件 (父級(jí)目錄為 assets/css)
- 初始化 CSS 文件, 讓樣式在各大瀏覽器顯示統(tǒng)一的樣式
- 創(chuàng)建一個(gè) normalize.css 文件, 這里推薦使用 normalize
- 也可以通過(guò)
npm install normalize.css來(lái)進(jìn)行下載
- 創(chuàng)建一個(gè) base.css 文件用來(lái)對(duì)項(xiàng)目進(jìn)行統(tǒng)一初始化
- 在這個(gè)文件里引用 normalize 文件, 然后再在 App.vue 文件內(nèi)引入這個(gè)文件
base.css 文件
@import './normalize.css';
App.vue 文件
@import './assets/css/base.css';
路徑配置別名
在項(xiàng)目根目錄下創(chuàng)建一個(gè) vue.config.js 配置文件, 到時(shí)候會(huì)將這個(gè)文件和公共配置進(jìn)行一個(gè)合并
module.exports = {
configureWebpack: { // 表明你要配置的是哪個(gè)配置文件
resolve: { // resolve 可以解決一些路徑相關(guān)的問(wèn)題
alias: { // 配置別名
// '@': 'src' 默認(rèn)已經(jīng)配置了這個(gè)別名
'assets': '@/assets',
'common': '@/common',
'components': '@/components',
'network': '@/network',
'store': '@/store',
'views': '@/views'
}
}
}
}
統(tǒng)一代碼風(fēng)格
在項(xiàng)目根目錄下創(chuàng)建一個(gè) .editorconfig 配置文件, 統(tǒng)一代碼風(fēng)格
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
路徑問(wèn)題
上面我們已經(jīng)為路徑配置了別名, 但在使用時(shí)應(yīng)注意以下幾點(diǎn):
- 在 JS 中使用可直接使用別名
import 'components/HelloWorld.vue' - 在含有 src、href 等路徑屬性時(shí)需在其別名前加上 ~
<img src='~asstes/logo.png'>
公共組件
制作前要想好組件是否可復(fù)用, 是完全公共的組件還是僅項(xiàng)目公共組件
完全公共組件
tabbar : 頁(yè)面底部切換組件

navbar : 頂部導(dǎo)航


swiper : 輪播圖

toast : 提示框

scroll : better-scroll 組件
僅項(xiàng)目公共組件
mainTabBar : 使用 tabbar 插槽的組件

tabControl : 分類菜單

backTop : 回到頂部按鈕

goods : 商品展示

tabControl 的下拉吸頂效果
- 獲取到 tabControl 的 offsetTop
- 必須知道滾動(dòng)到多少時(shí), 開(kāi)始有吸頂效果, 這個(gè)時(shí)候就需要獲取距離頂部的距離是多少
- 如果直接獲取到 tabControl 的 offsetTop 的值是不正確的, 因?yàn)?strong>圖片加載比較慢的原因
- 監(jiān)聽(tīng) HomeSwipper(輪播圖) 中的任意一個(gè) img 的加載完成后發(fā)出自定義事件, 在 Home.vue 監(jiān)聽(tīng)事件后獲取正確的值
this.$refs.tabControl.$el.offsetTop
- 判斷滾動(dòng)的距離為元素添加 fixed 樣式
- 但是 better-scroll 是通過(guò)改變 translate 來(lái)實(shí)現(xiàn)滾動(dòng)的, fixed 樣式依然會(huì)被滾到上面, 所以這個(gè)方法不管用
- 通過(guò)復(fù)制一個(gè)相同的組件, 放在 better-scroll 外面, 默認(rèn)隱藏, 當(dāng)組件重疊的時(shí)候顯示, 并設(shè)置 層級(jí)(z-index) 就可以了
- 這里有一個(gè)問(wèn)題, 兩個(gè)組件的點(diǎn)擊事件是不同步的, 要解決這個(gè)問(wèn)題只需要在點(diǎn)擊事件里讓這兩個(gè)組件的當(dāng)前狀態(tài)的值一致就可以了
backTop
點(diǎn)擊回到頂部, 這里設(shè)置整個(gè)組件為點(diǎn)擊事件, 一般情況下直接為組件添加原生事件是不行的, 可以使用修飾符 .native 來(lái)實(shí)現(xiàn)綁定原生事件
<back-top @click="backClick" /> // 這樣是沒(méi)有效果的
<back-top @click.native="backClick" /> // 有效果
使用 better-scroll 對(duì)象里的方法 scrollTo(0,0) 來(lái)實(shí)現(xiàn)回到頁(yè)面的頂部
這里直接在滾動(dòng)組件 Scroll.vue 里封裝了一個(gè) scrollTo 方法
/**
* 設(shè)置跳轉(zhuǎn)位置, 默認(rèn)跳轉(zhuǎn)時(shí)間300ms
*/
scrollTo(x, y, time = 300) {
this.scroll && this.scroll.scrollTo && this.scroll.scrollTo(x, y, time);
},
點(diǎn)擊事件
<scroll ref="scroll">
滾動(dòng)的組件
</scroll>
<back-top @click.native="backTop" v-show="isShowBackTop" />
/**
* 回到頂部
*/
backTop() {
this.$refs.scroll.scrollTo(0, 0);
},
/**
* 監(jiān)聽(tīng) better-scroll 的滾動(dòng)事件
* 1. 顯示/隱藏backTop
* 2. 是否吸頂tabControl
*/
contentScroll(position) {
// 判斷BackTop是否顯示
this.listenerShowBackTop(position.y);
// 決定tabControl是否吸頂(position: fixed)
this.isTabFixed = Math.abs(position.y) >= this.tabOffsetTop;
}
/**
* 顯示/隱藏BackTop
*/
listenerShowBackTop(positionY) {
this.isShowBackTop = Math.abs(positionY) >= BACK_POSITION;
}
this.$refs.scroll 獲取的就是滾動(dòng)組件里的 scroll 對(duì)象, 然后直接調(diào)用里面定義的方法就可以了
better-scroll
入門(mén)
這里使用的原生的滾動(dòng)效果, 在手機(jī)上使用可能會(huì)有延遲感, 卡頓感, 給用戶的體驗(yàn)并不是很好, 所以推薦使用 Better-Scroll

Better-Scroll 是作用在外層 wrapper 容器上的, 滾動(dòng)的部分是 content 元素
注意
- wrapper 必須定高, 并且設(shè)置
overflow: hidden - Better-Scroll 只處理容器(wrapper)的第一個(gè)子元素(content)的滾動(dòng), 其它的元素都會(huì)被忽略
某些情況下, 我們希望 wrapper 高度自適應(yīng), 例如本項(xiàng)目中 頂部導(dǎo)航欄和底部導(dǎo)航欄高度固定, 中間可滾動(dòng)區(qū)域的 wrapper 高度自適應(yīng), 那么可以采取以下方案
/* .scroll-content的父元素 */
#home {
position: relative;
height: 100vh;
}
.scroll-content {
position: absolute;
top: 44px;
bottom: 49px;
left: 0;
right: 0;
overflow: hidden;
}
最簡(jiǎn)單的初始化代碼如下
import BScroll from 'better-scroll'
let wrapper = document.querySelector('.wrapper')
let scroll = new BScroll(wrapper)
Better-Scroll 提供了一個(gè)類, 實(shí)例化的第一個(gè)參數(shù)是一個(gè)原生的 DOM 對(duì)象
當(dāng)然, 如果傳遞的是一個(gè)字符串, Better-Scroll 內(nèi)部會(huì)嘗試調(diào)用 querySelector 去獲取這個(gè) DOM 對(duì)象
如果是在 Vue 中使用, 推薦使用 ref 的方式拿到 DOM 對(duì)象, 防止類名相同而拿不到對(duì)象
- ref 如果是綁定在組件中的, 那么通過(guò)
this.$refs.refname獲取到的是一個(gè)組件對(duì)象 - ref 如果是綁定在普通的元素中, 那么通過(guò)
this.$refs.refname獲取到的是一個(gè)元素對(duì)象
監(jiān)聽(tīng)事件
默認(rèn)情況下 BScroll 是不可以實(shí)時(shí)的監(jiān)聽(tīng)滾動(dòng)位置, 如果你想監(jiān)聽(tīng)滾動(dòng), 可以傳遞第二個(gè)參數(shù)
import BScroll from 'better-scroll'
let wrapper = document.querySelector('.wrapper')
let scroll = new BScroll(wrapper, {
probeType: 3,
pullUpLoad: true,
click: true
})
scroll.on('scroll', (position) => {
console.log(position) // 這里就可以打印監(jiān)聽(tīng)的滾動(dòng)的位置了
})
scroll.on('pullingUp', () => {
console.log('上拉加載更多')
//scroll.finishPullUp()
setTimeout(() => {
scroll.finishPullUp()
}, 2000)
})
probeType : 偵測(cè)類型
- 這里可以傳遞的參數(shù)有 0 、1 、2 、3
- 0 和 1 都是不偵測(cè)實(shí)時(shí)的位置
- 2 是在手指滾動(dòng)的過(guò)程中偵測(cè), 手指離開(kāi)后的慣性滾動(dòng)過(guò)程中不偵測(cè)
- 3 是只要是滾動(dòng)都會(huì)偵測(cè)
pullUpLoad : 監(jiān)聽(tīng)滾動(dòng)到底部事件
- 默認(rèn)只會(huì)觸發(fā)一次, 如果想多次觸發(fā), 必須要在每次觸發(fā)事件后調(diào)用
scroll.finishPullUp()來(lái)結(jié)束這次事件, 這樣就可以進(jìn)行多次監(jiān)聽(tīng)滾動(dòng)到底部事件了 - 如果不想太過(guò)頻繁的觸發(fā)事件, 可以將調(diào)用包裹在一個(gè)定時(shí)器中
click : 監(jiān)聽(tīng)點(diǎn)擊事件
- 如果滑動(dòng)區(qū)域內(nèi)有除了 button 按鈕以外的點(diǎn)擊事件, 要加上這個(gè)才能點(diǎn)擊, 否則點(diǎn)擊事件會(huì)失效
- button 按鈕無(wú)論該屬性為 true | false 都會(huì)生效
封裝
這里用的是 @1.13.2 版本的, 如果是 @2.0 版本以上的要參考官方的方式
在 Vue 中使用的封裝
<template>
<div class="wrapper" ref="wrapper">
<div class="content">
<slot></slot>
</div>
</div>
</template>
<script>
import BScroll from 'better-scroll'
export default {
name: 'Scroll',
props: {
// 由使用者決定偵測(cè)類型和是否監(jiān)聽(tīng)滾動(dòng)到底部事件
probeType: {
type: Number,
default: 0
},
pullUpLoad: {
type: Boolean,
default: false
}
},
data() {
return {
scroll: null
}
},
mounted() {
// 創(chuàng)建 BScroll 對(duì)象
this.scroll = new BScroll(this.$refs.wrapper, {
click: true,
probeType: this.probeType,
pullUpLoad: this.pullUpLoad
});
// 監(jiān)聽(tīng)滾動(dòng)的位置
if (this.probeType == 2 || this.probeType == 3) {
this.scroll.on("scroll", position => {
this.$emit("scroll", position);
})
}
// 監(jiān)聽(tīng)scroll滾動(dòng)到底部
if (this.pullUpLoad) {
this.scroll.on("pullingUp", () => {
this.$emit("pullingUp");
})
}
},
methods: {
/**
* 設(shè)置跳轉(zhuǎn)位置
*/
scrollTo(x, y, time = 300) {
this.scroll && this.scroll.scrollTo && this.scroll.scrollTo(x, y, time);
},
/**
* 刷新底部上拉事件
*/
finishPullUp() {
this.scroll && this.scroll.finishPullUp && this.scroll.finishPullUp();
},
/**
* 刷新scroll可滾動(dòng)高度
*/
refresh() {
this.scroll && this.scroll.refresh && this.scroll.refresh();
},
/**
* 獲取當(dāng)前scroll的y值
*/
getScrollY() {
return this.scroll.y ? this.scroll.y : 0;
}
}
}
</script>
使用
使用時(shí)將封裝好的組件導(dǎo)入, 并將要滑動(dòng)的區(qū)域用標(biāo)簽包裹起來(lái)
<scroll
class="scroll-content"
ref="scroll"
:probe-type="3"
:pull-up-load="true"
@scroll="contentScroll"
@pullingUp="loadMore">
<div>
需要包裹的內(nèi)容
</div>
</scroll>
better-scroll 有時(shí)不能滾動(dòng) bug

better-scroll 對(duì)象的 scrollerHeight 方法里面記錄了可滾動(dòng)內(nèi)容的高度, 這個(gè)屬性是根據(jù)放在 content 中的子組件的高度來(lái)決定的, 但是在剛開(kāi)始計(jì)算 scrollerHeight 屬性時(shí), 由于圖片加載比較慢, 所以沒(méi)有將圖片高度計(jì)算在內(nèi), 所以得到的可滾動(dòng)高度是錯(cuò)誤的, 后面圖片加載進(jìn)來(lái)之后高度被撐開(kāi)了, 但是 scrollerHeight 屬性并沒(méi)有進(jìn)行更新, 所以滾動(dòng)出現(xiàn)了問(wèn)題
解決方案:
監(jiān)聽(tīng)每一張圖片是否加載完成, 只要有一張圖片加載完成, 就執(zhí)行一次 refresh()
- 原生的 JS 監(jiān)聽(tīng)圖片加載完成的方式:
img.onload = function() {} - Vue 中監(jiān)聽(tīng):
@load=imageLoad, 這里是非父子組件通信- 通過(guò) Vuex 傳遞方法
- 通過(guò) 事件總線 $bus 的方式
- 因?yàn)橛卸鄠€(gè)頁(yè)面都用到 better-scroll, 為了方便管理, 這里使用事件總線 $bus 的方式傳遞方法
- 在 (main.js) Vue 原型上添加 $bus
Vue.prototype.$bus = new Vue() - 將方法發(fā)送到 $bus 中
imageLoad() { this.$bus.$emit('itemImageLoad') } - 通過(guò) $bus 監(jiān)聽(tīng)圖片加載完成, 并調(diào)用 refresh
this.$bus.$on('itemImageLoad', () => { 調(diào)用refresh })
$bus 取消事件監(jiān)聽(tīng)
this.$bus.$off('方法名', '對(duì)應(yīng)的處理函數(shù)')
防抖
每張圖片加載完之后都會(huì)立刻調(diào)用一次 refresh, 這對(duì)于性能上來(lái)說(shuō)無(wú)異于是負(fù)擔(dān), 所以, 通過(guò)防抖對(duì)性能進(jìn)行優(yōu)化
/**
* 防抖
*/
function debounce(func, delay = 100) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
func && func.apply(this, args);
}, delay);
}
}
解決移動(dòng)端 URL 欄 和 底部工具欄 顯示/隱藏 時(shí)高度 Bug
Bug 原因
移動(dòng)端下瀏覽器對(duì) 100vh 的定義不考慮 URL 欄 和 底部工具欄 的高度(無(wú)論顯示還是隱藏), 可以用下面這張圖直觀地體現(xiàn)問(wèn)題

當(dāng)?shù)刂窓诳梢?jiàn)時(shí), 由于移動(dòng)瀏覽器不正確地將 100vh 設(shè)置為屏幕高度而沒(méi)有顯示地址欄, 因此屏幕底部被切斷
在上圖中, 應(yīng)該在屏幕底部的按鈕被隱藏了
更糟糕的是, 當(dāng)用戶第一次使用手機(jī)訪問(wèn)網(wǎng)站時(shí), 地址欄會(huì)顯示在頁(yè)面頂部, 因此用戶體驗(yàn)是很糟糕的
設(shè)置 home 高度也不能直接使用 100%, 因?yàn)?100% 是相對(duì)與父元素, 而 home 的父元素的高度又沒(méi)有固定, 而是依賴與 home 的高度撐開(kāi), 所以百分比無(wú)效
解決方案 (window.innerHeight)
解決這個(gè)問(wèn)題的一種方法是依賴 JavaScript 而不是 CSS, 當(dāng)頁(yè)面加載時(shí), 將高度設(shè)置為 window.innerHeight 將正確地將高度設(shè)置為窗口的可見(jiàn)部分
使用 window.innerHeight 動(dòng)態(tài)設(shè)置高度
當(dāng)窗口大小改變時(shí)重新設(shè)置高度為 window.innerHeight, 因?yàn)?window.innerHeight 的高度不包括地址欄和工具欄
- 如果地址欄是可見(jiàn)的, 那么
window.innerHeight將是屏幕可見(jiàn)部分的高度, 正如你所期望的那樣 - 如果地址欄是隱藏的, 那么
window.innerHeight是全屏的高度
<template>
<div id="home" :style="{ height: homeHeight }"><div>
</template>
<script>
export default {
data() {
return {
homeHeight: window.innerHeight + 'px'
}
}
mounted() {
window.addEventListener("resize", () => {
this.homeHeight = window.innerHeight + "px"
})
}
}
</script>
<style>
#home {
position: relative;
/* height: 100vh */
}
</style>
讓 Home 不銷毀(destroyed), 并在路由來(lái)回切換后回到離開(kāi)時(shí)的位置
讓 home 不要隨意銷毀掉
添加 keep-alive 就可以了
讓 home 中的內(nèi)容保持原來(lái)的位置
data() {
return {
saveY: 0
}
},
activated() {
// 當(dāng)路由處于活躍狀態(tài)時(shí), 將頁(yè)面回到離開(kāi)時(shí)的位置, 且刷新一次 scroll 的高度
this.$refs.scroll.scrollTo(0, this.saveY, 0)
this.$refs.scroll.refresh()
},
deactivated() {
// 當(dāng)路由處于不活躍狀態(tài)時(shí), 保存 scroll 的 y 值
this.saveY = this.$refs.scroll.getScrollY()
// 取消該路由的圖片加載事件監(jiān)聽(tīng)
this.$bus.$off("itemImageLoad", this.itemImageListener);
}
詳情頁(yè)
this.$nextTick(() => {}) 在 created 中這個(gè)函數(shù)意思是: 等模板渲染完后就執(zhí)行這個(gè)函數(shù), 從這里就可以拿到一些數(shù)據(jù), 這個(gè)時(shí)候?qū)?yīng)的 DOM 已經(jīng)報(bào)備渲染出來(lái)了, 但是圖片依然是沒(méi)有加載完

一定要將詳情頁(yè)銷毀
<keep-alive exclude="Detail">
<router-view />
</keep-alive>
如何判斷一個(gè)對(duì)象是不是一個(gè)空的對(duì)象
const obj = {}
Object.keys(obj).length === 0
混入(mixin)的使用
創(chuàng)建混入對(duì)象: const mixin = {}
組件中導(dǎo)入: mixins: [mixin]
點(diǎn)擊標(biāo)題,滾動(dòng)到對(duì)應(yīng)的主題
- 獲取標(biāo)題的 offsetTop
- 在哪里才能獲取到正確的 offsetTop ?
- created 肯定不行, DOM 還沒(méi)渲染
- mounted 也不行, 圖片數(shù)據(jù)還沒(méi)有加載完
- nextTick 也不行, 雖然 DOM 改變觸發(fā) nextTick 鉤子, 但圖片不一定加載完, 導(dǎo)致offsetTop是錯(cuò)誤的值
方案一
在 created 中事先通過(guò)防抖獲得處理函數(shù), 等待圖片加載完畢之后再調(diào)用該函數(shù)
created() {
/**
* 通過(guò)防抖獲得 getThemeTopY 函數(shù), 等待圖片加載完之后再調(diào)用
*/
this.getThemeTopY = debounce(() => {
this.$nextTick(() => {
this.themeTopYs = [];
this.themeTopYs.push(0);
this.themeTopYs.push(this.$refs.params.$el.offsetTop);
this.themeTopYs.push(this.$refs.comment.$el.offsetTop);
this.themeTopYs.push(this.$refs.recommend.$el.offsetTop);
})
}, 100);
},
methods: {
/**
* 刷新scroll高度, 且獲得各個(gè)標(biāo)題的 offsetTop
*/
detailImageLoad() {
this.refresh();
this.getThemeTopY();
}
}
方案二
等待所有圖片加載完畢
methods: {
detailImageLoad() {
// 判斷所有的圖片都加載完了, 進(jìn)行一次回調(diào)
if (++this.counter === this.imageLength) {
this.refresh();
this.$emit("detailImageLoad");
}
}
}
vuex
mutations 唯一的目的就是修改 state 中狀態(tài), 最好是其中的每個(gè)方法盡可能完成得事件比較單一一點(diǎn), 否則每次執(zhí)行的時(shí)候執(zhí)行的方法名字一樣, 不知道到底執(zhí)行的是哪個(gè)
如果有邏輯判斷推薦放到 actions 里, 執(zhí)行的方法可以放到 mutations 里, 這樣就可以跟蹤每個(gè)想要調(diào)試的點(diǎn)
const store = new Vuex.Store({
state: {
cartList: []
},
mutations: {
addCount(state, payload) {
payload.count++
},
addToCart(state, payload) {
state.cartList.unshift(payload)
}
},
actions: {
addToCart({ state, commit }, payload) {
return new Promise((resolve, reject) => {
let oldProduct = state.cartList.find(item => item.iid === payload.iid)
if (oldProduct) {
commit('addCount', oldProduct)
resolve("當(dāng)前商品已被添加到購(gòu)物車+1")
} else {
payload.count = 1
payload.checked = true
commit('addToCart', payload)
resolve("已添加至購(gòu)物車")
}
})
}
}
})
目錄結(jié)構(gòu)
建議分類成一個(gè)一個(gè)的文件, 這樣方便管理, 還可以封裝常量文件
index.js
import Vue from "vue";
import Vuex from "vuex";
import mutations from "./mutations";
import actions from "./actions";
import getters from "./getters"
Vue.use(Vuex);
const state = {
cartList: []
}
export default new Vuex.Store({
state,
mutations,
actions,
getters
})
toast 插件封裝
在 components/common/toast 文件夾下新建兩個(gè)文件
- index.js
- Toast.vue
index.js
import Toast from "./Toast.vue"
export default {
install(Vue) {
const toastConstructor = Vue.extend(Toast);
const toast = new toastConstructor();
toast.$mount(document.createElement("div"));
document.body.appendChild(toast.$el);
Vue.prototype.$toast = toast;
}
}
Toast.vue
<template>
<div v-show="isShow" class="toast">
<div>{{message}}</div>
</div>
</template>
<script>
export default {
name: "Toast",
data() {
return {
message: "",
isShow: false
}
},
methods: {
show(message, duration = 2000) {
this.isShow = true;
this.message = message;
setTimeout(() => {
this.isShow = false;
this.message = "";
}, duration);
}
}
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.toast {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, .7);
padding: 8px 10px;
color: #fff;
text-align: center;
border-radius: 8px;
z-index: 9999;
}
</style>
main.js
import toast from './components/common/toast/index';
Vue.use(toast) // 這里會(huì)去執(zhí)行 index.js 里的 install 方法
使用的時(shí)候, 只需要: this.$toast.show("需要顯示的文字", 2000) 就可以了
細(xì)節(jié)處理
FastClick
使用 FastClick 解決移動(dòng)端點(diǎn)擊 300ms 的延遲
安裝
npm install fastclick --save
使用 (在 main.js 中安裝插件)
import FastClick from 'fastclick'
FastClick.attach(document.body)
圖片懶加載
圖片需要顯示在屏幕上時(shí)再加載
安裝
npm install vue-lazyload --save
使用 (在 main.js 中安裝插件)
import VueLazyLoad from 'vue-lazyload'
Vue.use(VueLazyLoad, {
// 顯示占位圖
loading: require('./assets/img/common/placeholder.jpg')
})
// 修改組件中 img 的屬性 :src => v-lazy
快捷修改 CSS 單位(適配不同設(shè)備)
項(xiàng)目直接是使用的 px 單位進(jìn)行開(kāi)發(fā)的, 這里改成 vm 單位
使用插件, 有很多類似的插件, 這里使用的 postcss-px-to-viewport, 這是開(kāi)發(fā)時(shí)依賴
安裝
npm install postcss-px-to-viewport --save-dev
配置(在項(xiàng)目根目錄下創(chuàng)建 postcss.config.js 配置文件)
module.exports = {
plugins: {
autoprefixer: {},
"postcss-px-to-viewport": {
viewportWidth: 375, // 視口寬度, 對(duì)應(yīng)的是設(shè)計(jì)稿寬度
viewportHeight: 667, // 視口高度, 對(duì)應(yīng)的是設(shè)計(jì)稿的高度
unitPrecision: 5, // 指定'px'轉(zhuǎn)換為視口單位值的小數(shù)位數(shù)(保留5位小數(shù))
viewportUnit: "vw", // 指定需要轉(zhuǎn)換成的視口單位, 建議使用vw
selectorBlackList: ["ignore"], // 指定不需要轉(zhuǎn)換的類
minPixelValue: 1, // 小于或等于'1px'不轉(zhuǎn)換為視口單位
mediaQuery: false, // 允許在媒體查詢中轉(zhuǎn)換'px'
exclude: [/TabMenu\.vue/] // 排除文件名包含 TabBar 的文件,必須是正則來(lái)匹配文件
}
}
}
這樣項(xiàng)目中所有的 px 單位就會(huì)變成 vm 單位
項(xiàng)目部署到遠(yuǎn)程服務(wù)器
使用 webpack 打包項(xiàng)目
npm run build
使用服務(wù)器軟件: tomcat、nginx, 這里使用 nginx
將 build 文件中的所有文件、文件夾、圖片拷貝到站點(diǎn)根目錄下
刷新頁(yè)面 404
問(wèn)題
將項(xiàng)目部署到遠(yuǎn)程服務(wù)器上后, 在頁(yè)面中一旦刷新, 會(huì)出現(xiàn) 404
原因
使用 history 模式時(shí), 還需要后臺(tái)配置支持
因?yàn)槲覀兊膽?yīng)用是個(gè)單頁(yè)客戶端應(yīng)用, 如果后臺(tái)沒(méi)有正確的配置, 當(dāng)直接訪問(wèn) http://mall.coderlion.com/home 就會(huì)報(bào) 404 的錯(cuò)誤
所以需要在服務(wù)端增加一個(gè)覆蓋所有情況的候選資源: 如果 URL 匹配不到任何靜態(tài)資源, 則應(yīng)該返回同一個(gè) index.html 頁(yè)面, 這個(gè)頁(yè)面就是 home 頁(yè)面
解決方案
為 nginx 服務(wù)器添加重定向配置
location / {
try_files $uri $uri/ /index.html;
}
其他服務(wù)器配置參照官方文檔
Vue 響應(yīng)式原理
- 當(dāng)數(shù)據(jù)發(fā)生修改時(shí), Vue 內(nèi)部是如何監(jiān)聽(tīng) message 數(shù)據(jù)的改變
- Object.defineProperty -> 監(jiān)聽(tīng)對(duì)象屬性的改變
- 當(dāng)數(shù)據(jù)發(fā)生改變, Vue 是如何知道要通知那些人, 界面發(fā)生刷新
- 發(fā)布訂閱者模式
