mall 項(xiàng)目隨筆

項(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)化工具)

在線預(yù)覽

GitHub 地址

初始化項(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è)面底部切換組件

image

navbar : 頂部導(dǎo)航

image
image

swiper : 輪播圖

image

toast : 提示框

image

scroll : better-scroll 組件

僅項(xiàng)目公共組件

mainTabBar : 使用 tabbar 插槽的組件

image

tabControl : 分類菜單

image

backTop : 回到頂部按鈕

image

goods : 商品展示

image

tabControl 的下拉吸頂效果

  1. 獲取到 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
  2. 判斷滾動(dòng)的距離為元素添加 fixed 樣式
    • 但是 better-scroll 是通過(guò)改變 translate 來(lái)實(shí)現(xiàn)滾動(dòng)的, fixed 樣式依然會(huì)被滾到上面, 所以這個(gè)方法不管用
  3. 通過(guò)復(fù)制一個(gè)相同的組件, 放在 better-scroll 外面, 默認(rèn)隱藏, 當(dāng)組件重疊的時(shí)候顯示, 并設(shè)置 層級(jí)(z-index) 就可以了
  4. 這里有一個(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

image

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

image

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, 這里是非父子組件通信
    1. 通過(guò) Vuex 傳遞方法
    2. 通過(guò) 事件總線 $bus 的方式
      • 因?yàn)橛卸鄠€(gè)頁(yè)面都用到 better-scroll, 為了方便管理, 這里使用事件總線 $bus 的方式傳遞方法
  1. 在 (main.js) Vue 原型上添加 $bus
    Vue.prototype.$bus = new Vue()
    
  2. 將方法發(fā)送到 $bus 中
    imageLoad() { this.$bus.$emit('itemImageLoad') }
    
  3. 通過(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)題

image

當(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)有加載完

image

一定要將詳情頁(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 ?
    1. created 肯定不行, DOM 還沒(méi)渲染
    2. mounted 也不行, 圖片數(shù)據(jù)還沒(méi)有加載完
    3. 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)式原理

  1. 當(dāng)數(shù)據(jù)發(fā)生修改時(shí), Vue 內(nèi)部是如何監(jiān)聽(tīng) message 數(shù)據(jù)的改變
    • Object.defineProperty -> 監(jiān)聽(tīng)對(duì)象屬性的改變
  2. 當(dāng)數(shù)據(jù)發(fā)生改變, Vue 是如何知道要通知那些人, 界面發(fā)生刷新
    • 發(fā)布訂閱者模式
image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容