1. 程式人生 > >Vue 重構有贊商城

Vue 重構有贊商城

swipe 依賴 插件 lock tlist tel 更新 asc oci

Vue.js 重構移動端有贊商場

代碼鏈接:GitHub

預覽鏈接:Git Pages

本項目的開發讓我了解並學習到以下幾點:

1.在真實的開發工作環境與流程,一些項目結構的處理,讓其更容易維護

2.數據接口的封裝與切換,與上下遊更好地協作

3.webpack 配置參數的一些原理和技巧

4.在前端開發過程中 mock 數據,更好地進行測試

5.更全面地了解 Vue / vue-router / vuex 等

6.在項目開發過程使用了一些庫:qs / Swiper / mint-ui / ...

7.把靜態頁面使用 Vue 重構

實現功能:

首頁 展示輪播圖和商品列表

分類頁 展示不同商品的推介列表

商品詳情頁

顯示商品信息(包括價格、圖片、詳情等),可增加商品數量並加入購物車

購物車 可增加商品數量,對商品可刪除、批量刪除,價格實時演算

個人頁面 可管理個人收收貨地址(包括刪除、增加、修改、設為默認地址等)

頁面渲染流程:

API 拿到數據 -> 渲染頁面

沒有真實數據的情況下 -> Mock 數據 -> 使用 API 拿到數據 -> 渲染頁面

頁面重構:

把原 HTML 的內容放進對應的 Vue 組件中,引入 CSS,確定樣式,再獲取數據,渲染頁面。

接下來歸納整理一下開發過程中學習到的知識點和踩的坑。


項目構建方面處理:在使用 vue-cli 構建項目後對目錄結構和 webpack 配置做一個調整。

多頁面應用調整

基於 vue-cli 把單頁面應用搭建成多頁面應用:

  • 修改目錄結構

  • 修改 webpack 配置

commit

參考:

基於vue-cli搭建一個多頁面應用

基於vue-cli重構多頁面腳手架


webpack

build/webpack.base.conf.js 中的 resolve 可以設置路徑或模塊的別名:

  ......
  resolve: {
    extensions: [‘.js‘, ‘.vue‘, ‘.json‘],
    alias: {
      ‘vue$‘: ‘vue/dist/vue.esm.js‘,
      ‘@‘: resolve(‘src‘),
      ‘components‘: ‘@/components‘,
      ‘pages‘: ‘@/pages‘,
      ‘js‘: ‘@/modules/js‘,
      ‘css‘: ‘@/modules/css‘,
      ‘sass‘: ‘@/modules/sass‘,
      ‘imgs‘: ‘@/modules/imgs‘
    }
  }
  ......

在其他地方引用:

import Hello from ‘components/Hello‘

參考:[webpack resolve]


首頁

  • DNS預解析
<!-- DNS預解析 -->
<link rel="dns-prefetch" href="https://dn-kdt-img.qbox.me/">
<link rel="dns-prefetch" href="https://img.yzcdn.cn/">
<link rel="dns-prefetch" href="https://b.yzcdn.cn/">
<link rel="dns-prefetch" href="https://su.yzcdn.cn/">
<link rel="dns-prefetch" href="https://h5.youzan.com/v2/">
<link rel="dns-prefetch" href="https://h5.youzan.com/">

能夠減少用戶點擊鏈接時的延遲。

  • mock 數據接口的處理

在真實開發環境中,前端需要通過 API 接口獲取數據,從而把數據渲染在頁面上,那麽可以這樣寫:

// api.js
// 開發環境和真實環境的切換
let url = {
    hotLists:‘/index/hotLists‘,
    banner:‘/index/banner‘
}
let host =  ‘http://rap2api.taobao.org/app/mock/7058‘
for (let key in url){
    if(url.hasOwnProperty(key)){
        url[key] = host + url[key]
    }
}
export default url

先使用 mock 數據的接口獲取數據,進行開發和測試,在與後端對接的時候再替換真實的數據接口。

  • mint-ui

問題 使用命令 npm i mint-ui -S 安裝了 mint-ui 後,在 babelrc 中做了相應的配置,引用後報錯,提示找不到模塊:

技術分享圖片

解決辦法:npm start 重啟服務器。

  • Infinite scroll

使用 mint-ui 的 Infinite scroll,使頁面的推薦商品列表下拉到底部時可以自動獲取並加載數據,實現無限滾動。

commit

  • 輪播組件

使用 Swiper 實現首頁輪播組件:

1.在首頁組件中,在 created 階段獲取 banner 的數據

2.通過 props 傳遞數據給 swipe 組件

3.swiper 接收數據,渲染到模板中,完成輪播

但是其中要註意數據獲取和生命周期的問題:

因為 swipe 組件中的 Swiper 插件依賴於 dom 節點,而 dom 節點是在 mounted 時被掛載的,這也就要求了在 swipe 組件中,當生命周期來到 mounted 的時候,他必須拿到數據,才能使 Swiper 組件拿到 dom 節點,操作輪播;當父組件中通過(異步)獲取到 banner 的數據並傳遞給 swipe 組件時,可以在父組件中做如下設置:

<!-- index.html -->
<swipe :lists=‘bannerLists‘ v-if=‘bannerLists‘></swipe>

只有在 bannerLists 數據不為 null 的時候,這個 swipe 的組件才可以顯示,這也就保證了數據可以正常傳遞, Swiper 也可以在 mounted 的時候拿到 dom 節點。

問題 使用 npm run dev 打開 http://localhost:8080/#/ 調試代碼時,總是一刷新就進入 debugger 狀態:

解決辦法:

1.打開 source 面板,把 Any XHR 勾選去掉

2.paused on exception


URL跳轉

從分類頁跳轉到列表頁:

1.傳遞參數及跳轉

// category.js
toSearch(list){
    location.href = `search.html?keyword=${list.name}$id=${list.id}`
}

2.使用 qs 讀取url參數:

// search.js
import qs from ‘qs‘

let {keyword,id} = qs.parse(location.search.substr(1))

mixin

混入 (mixins) 是一種分發 Vue 組件中可復用功能的非常靈活的方式。混入對象可以包含任意組件選項。當組件使用混入對象時,所有混入對象的選項將被混入該組件本身的選項。

把一些公用的函數/方法抽離出來,放進 mixin.js:

// mixin.js
import Foot from ‘components/Foot.vue‘
let mixin = {
    filters:{
        number(price){
            return price = price.toFixed(2)
        }
    },
    components:{
        Foot,
    },
}
export default mixin

在組件中引用 mixin:

import mixin from ‘js/mixin‘
new Vue({
    ...
    mixins:[mixin]
    ...
})

這樣就可以直接在組件中對函數/方法進行復用了。


velocity

使用 velocity 實現「回到頂部」動畫過渡:

安裝:npm i velocity-animate

引用:import Velocity from ‘velocity-animate‘

使用:

new Vue({
    ...
    methods:{
        toTop(){
            // 第一個參數:動作元素 第二個參數:動作事件
            Velocity(document.body,‘scroll‘,{duration:1000})
        }
    }
})

touchmove

問題:使用 touchmove 監聽頁面:

<div class="container with-top-search" style="min-height: 667px;" @touchmove=‘move‘>...</div>

根據距離頁面頂部距離的大小,確定某個元素是否展現:

data:{
    toShow:false
},
move(){
    if(document.documentElement.scrollTop > 100){
        console.log(1)
        this.toShow = true
    }else{
        console.log(2)
        this.toShow = false
    }
},

頁面劃動是有效的,但是結果一直取不到 document.body.scrollTop 的值。

解決方法:使用 document.documentElement.scrollTop

由於在不同情況下,document.body.scrollTop與document.documentElement.scrollTop都有可能取不到值

參考文章:https://segmentfault.com/a/1190000008065472


詳情頁

  • 輪播組件共用

在項目首頁中,有一個圖片輪播組件,用於展示一個具體商品,點擊會跳轉到不同的頁面;

而在詳情頁中,也有一個商品圖片輪播,項目需要這個組件繼續沿用首頁的輪播組件,但是他的圖片、點擊後跳轉、通過 API 所獲取的數據結構均和首頁輪播組件不同,這時候該怎麽處理傳入輪播組件的數據:

1.首先應該分析一下輪播組件需要接收的數據:一個數組,數組裏包含 N 個對象,包含鍵 clickUrl(值為點擊圖片後跳轉的的url)和鍵 img(值為圖片url)

2.對 API 獲取的將要傳入的數據做一層處理,讓輪播組件只接收一種統一的格式:

new Vue({
    el:‘#app‘,
    data:{
        details:null,
        detailTab,
        currentTab:0,
        dealList:null,
        bannerLists:null
    },
    created(){
        this.getDetails()
    },
    methods:{
        getDetails(){
            axios.get(url.details,{id}).then(res=>{
                // 通過API獲取的原數據 details
                this.details = res.data.data
                // 需要傳入組件的數據 bannerLists
                this.bannerLists = []
                this.details.imgs.forEach(item => {

                    // 把 bannerLists 數組中的值改為對象
                    this.bannerLists.push({
                        clickUrl:‘‘,
                        img:item
                    })
                })
            })
        },
    },
})

最後再把數據傳遞給輪播組件:<swipe :lists=‘bannerLists‘ v-if=‘bannerLists‘></swipe>


購物車

  • mockjs

當線上接口平臺連接不穩定的時候,可以使用 mockjs 模擬 mock 數據。

安裝:npm i mockjs

引入:

import Mock from ‘mockjs‘

let Random = Mock.Random

let data = Mock.mock({
    ‘cartList|3‘:[{
        ‘goodsList|1-5‘:[{
            id:Random.int(10000,100000),
            img:Mock.mock(‘@Img(90x90,@color)‘)
        }]
    }]
})

console.log(data)

技術分享圖片

  • $refs

場景:在購物車頁面,向左劃動商品欄時出現相關操作按鈕(增減商品數量,刪除);向右劃動恢復原狀。

在元素上綁定 touchstart 和 touchend 事件,並設置 ref 值用於獲取需要操作的商品節點:

<li class="block-item block-item-cart "
    v-for="(good,goodIndex) in shop.goodsList"
    :class="{editing:shop.editing}"
    :ref="‘goods-‘+ shopIndex + ‘-‘ + goodIndex"
    @touchstart="start($event,good)"
    @touchend="end($event,shopIndex,good,goodIndex)">...</li>

配合 velocity ,根據劃動距離操作節點:

methods:{
    ...
    start(e,good){
        // 拿到初始值的坐標
        good.startX = e.changedTouches[0].clientX
    },
    end(e,shopIndex,good,goodIndex){
        // 拿到結束值的坐標
        let endX = e.changedTouches[0].clientX
        let left = ‘0‘
        if(good.startX - endX > 100){
            left = ‘-60px‘
        }
        if(endX - good.startX > 100){
            left = ‘0px‘
        }
        // 使用 velocity 操作節點
        Velocity(this.$refs[`goods-${shopIndex}-${goodIndex}`],
            {left})
    }
    ...
}

問題:當商品列表中的某款商品被刪除後,某些樣式會繼續殘留在該列表的下一款商品中,如:

技術分享圖片

技術分享圖片

問題原因: 商品列表使用了 v-for 來渲染,而v-for 模式使用“就地復用”策略,簡單理解就是會復用原有的dom結構,盡量減少dom重排來提高性能,當商品刪除後,列表中的剩余商品就會復用被刪除商品的 dom 結構,所以會產生這種現象。

當 Vue.js 用 v-for 正在更新已渲染過的元素列表時,它默認用“就地復用”策略。如果數據項的順序被改變,Vue 將不會移動 DOM 元素來匹配數據項的順序, 而是簡單復用此處每個元素,並且確保它在特定索引下顯示已被渲染過的每個元素。

解決方法:

1.在刪除了商品後,重新操作節點,返回原來的位置(還原dom)。this.$refs[`goods-${shopIndex}-${goodIndex}`][0].style.left = ‘0px‘

2.給遍歷的節點設置一個唯一的 key 屬性:

<li v-for="(good,goodIndex) in shop.goodsList" :key="good.id"></li>

為了給 Vue 一個提示,以便它能跟蹤每個節點的身份,從而重用和重新排序現有元素,你需要為每項提供一個唯一 key 屬性。理想的 key 值是每項都有的唯一 id。

  • 封裝請求接口

在真實開發過程中,對請求接口進行封裝,方便調用。

// fetch.js
import url from ‘js/api.js‘
import axios from ‘axios‘

function fetch(method=‘get‘,url, data) {
    return new Promise((resolve, reject) => {
        axios({method, url, data}).then(res => {
            let status = res.data.status
            if (status === 200) {
                resolve(res)
            }
            if (status === 300) {
                location.href = ‘login.html‘
                resolve(res)
            }
        }).catch(err => {
            reject(err)
        })
    })
}
export default fetch
  • 封裝購物車操作

在具體場景中,把對於數據請求的操作放在 Service 中,在別的地方調用的時候傳參即可:

// cartService.js
import url from ‘js/api.js‘
import fetch from ‘./fetch.js‘

class Cart {
    // 增加商品數量
    static add(id){
        return fetch(‘post‘,url.cartAdd,{
            id,
            number:1
        })
    }
    // 減少商品數量
    static reduce(id){
        return fetch(‘post‘,url.cartReduce,{
            id,
            number:1
        })
    }
    // 刪除商品
    static remove(id){
        return fetch(‘post‘,url.cartRemove,{id})
    }
}
export default Cart

這樣就可以省略很多步驟,也讓流程更為清晰:

import Cart from ‘js/cartService.js‘
add(good){
    // axios.post(url.cartAdd,{
    //     id:good.id,
    //     number:1
    // }).then(res=>{
    //     good.number++
    // })
    Cart.add(good.id).then(res=>{
        good.number++
    })
},

個人頁面

路由管理 / 嵌套路由:

在「會員頁面」下有「我的設置」和「收貨地址管理」,「收貨地址管理」下有子路由「地址列表」和「新增/編輯地址」,進入「收貨地址管理」默認重定向到「收貨地址列表」:

import Vue from ‘vue‘
import Router from ‘vue-router‘
Vue.use(Router)
let routes = [
    {
        // 默認顯示頁面
        path:‘/‘,
        components:require(‘./components/member.vue‘)
    },
    {
        // 收貨地址管理
        path:‘/address‘,
        components:require(‘./components/address.vue‘),
        children:[
            {
                path:‘‘,
                redirect:‘all‘
            },
            {
                // 地址列表
                path:‘all‘,
                components:require(‘./components/all.vue‘)
            },
            {
                // 新增/編輯地址
                path:‘form‘,
                components:require(‘./components/form.vue‘)
            }
        ]
    }
]
let router = new Router({
    routes
})
new Vue({
    el:‘#app‘,
    router
})
  • 組件共用

因為「新增地址」和「編輯地址」所用的組件時同一個,所以就要在進入組件的路由參數上做一些設置,讓組件可以區分用戶是需要「新增地址」還是「編輯地址」。

1.首先完善路由信息,增加 name 字段:

{
    path:‘form‘,
    name:‘form‘,
    components:require(‘./components/form.vue‘)
}

2.根據不同的需求,路由跳轉攜帶不同的參數:

// 新增地址 type 為 add
<router-link :to="{name:‘form‘,query:{type:‘add‘}}" >新增地址</router-link>

// 編輯地址 type 為 edit,同時接收一個實例參數:選擇需要修改的地址信息
<a @click="toEdit(list)"></a>
toEdit(list){
    this.$router.push({name:‘form‘,query:{
        type:‘edit‘,
        instance:list
    }})
}

3.同時給組件設置一些初始值,用於 v-model 綁定數據,提交修改:

export default {
    data(){
        return {
            name:‘‘,
            tel:‘‘,
            provinceValue:-1,
            cityValue:-1,
            districtValue:-1,
            address:‘‘,
            id:‘‘,
            type:‘‘,
            instance:‘‘
        }
    },
    created() {
        let query = this.$route.query
        this.type = query.type  
        this.instance = query.instance
        if(this.type === ‘edit‘){
            let ad = this.instance
            this.provinceValue = parseInt(ad.provinceValue)
            this.name = ad.name
            this.tel = ad.tel
            this.address = ad.address
            this.id = ad.id
        }
    },
}

接著根據需求渲染數據即可。


狀態管理(Vuex)

在「個人地址管理頁面」中使用 vuex 管理狀態和數據:

1.首先創建 store,其中包含一些初始值的設置、獲取數據的方法、更改狀態和數據的方法

// vuex/index.js
import Vue from ‘vue‘
import Vuex from ‘vuex‘
import Address from ‘js/addressService.js‘
Vue.use(Vuex)
const store = new Vuex.Store({
    state:{
        lists:null
    },
    mutations:{
        init(state,lists){
            state.lists = lists
        }
    },
    actions:{
        getLists({commit}){
            Address.list().then(res=>{
                // this.lists = res.data.lists
                store.commit(‘init‘,res.data.lists)
              })
        }
    }
})
export default store

2.註入 Vue 實例:

import Vue from ‘vue‘
import router from ‘./router/index.js‘
import store from ‘./vuex‘
import ‘./member.css‘

new Vue({
    el:‘#app‘,
    router,
    store
})

3.先在 created 階段執行this.$store.dispatch(‘getLists‘),更新數據到 state,然後通過 computed 拿到 state 中的 數據,在組件中渲染數據渲染:

created() {
    // Address.list().then(res=>{
    //   this.lists = res.data.lists
    // })
    this.$store.dispatch(‘getLists‘)
},
computed:{
    lists(){
    return this.$store.state.lists
    }
}

深度監聽/深拷貝

需求:在使用 vuex 管理狀態和數據的過程中,有一些對於數據列表的增刪改的操作,每當完成這些操作後頁面需要跳轉到某個頁面。

方法:使用 watch 監聽數據列表,一旦監測到數據列表增減,則跳轉。

在實際過程中,數據的增減確實是可以引發跳轉行為,但是列表中(列表項是對象)某個屬性的更改則不會引發跳轉。

解決方法:

1.對數據列表進行深度監聽

為了發現對象內部值的變化,可以在選項參數中指定 deep: true 。註意監聽數組的變動不需要這麽做。

watch:{
    lists:{
        handle(){
            this.$router.go(-1)
        },
        deep:true
    },
}

在設置了深度監聽後,發現問題還是沒有得到解決,那是因為監聽對象是從 state 得到的 lists,當在 mutations 裏對這個 lists 的成員進行其屬性的某些操作的時候,依然沒有監聽到屬性值的改變。

所以,需要對這個 lists 進行深拷貝,當拷貝對象完成對數據的處理後,再把他賦值給 state.lists:

2.對監聽對象進行深拷貝

// vuex/index.js
update(state,instance){
    // 通過 instance 的 id 找到
    let lists = JSON.parse(JSON.stringify(state.lists))
    let index = lists.findIndex(item =>{
        return item.id === instance.id
    })
    lists[index] = instance
    state.lists = lists
},

熱重載

vuex 配合 webpack 實現熱重載功能,提高開發效率(前提:state/mutations/actions 被做為模塊引入 store):

比如配置了 mutations 的熱重載,你添加新的 mutations 方法的時候就不會刷新頁面,而是加載一段新的js,不配頁面就會刷新

/ store.js
import Vue from ‘vue‘
import Vuex from ‘vuex‘
import mutations from ‘./mutations‘
import moduleA from ‘./modules/a‘

Vue.use(Vuex)

const state = { ... }

const store = new Vuex.Store({
  state,
  mutations,
  modules: {
    a: moduleA
  }
})

if (module.hot) {
  // 使 action 和 mutation 成為可熱重載模塊
  module.hot.accept([‘./mutations‘, ‘./modules/a‘], () => {
    // 獲取更新後的模塊
    // 因為 babel 6 的模塊編譯格式問題,這裏需要加上 `.default`
    const newMutations = require(‘./mutations‘).default
    const newModuleA = require(‘./modules/a‘).default
    // 加載新模塊
    store.hotUpdate({
      mutations: newMutations,
      modules: {
        a: newModuleA
      }
    })
  })
}

部署

在將項目部署到 Git Pages 的時候,出現了一個問題:

技術分享圖片

技術分享圖片

原因是 GitPages 是 HTTPS 頁面的,而調用接口獲取數據的 API 是 HTTP 的,HTTPS 頁面裏動態的引入 HTTP 資源,比如引入一個js文件,會被直接block掉的.在 HTTPS 頁面裏通過 AJAX 的方式請求 HTTP 資源,也會被直接block掉的。

搜索了一下資料,按照 stackoverflow 的答案,給 index.html 的 head 加上了一個 meta 標簽,意思是自動將http的不安全請求升級為https:

<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">

產生了兩個結果:

1.本地調試獲取不到 index.js:

技術分享圖片

2.GitPages 中的接口轉換成了 HTTPS,但是接口沒有對應的 https 資源,於事無補:

技術分享圖片

所以只能買一個域名,然後配置 http 的協議,再解析到 Git Pages 上。

  • Git Pages 配置 http 域名

參考:

GitHub 綁定域名

segmentfault 綁定域名

GitHub 綁定域名

Vue 重構有贊商城