1. 程式人生 > >前端-Vuejs2.5開發去哪兒網

前端-Vuejs2.5開發去哪兒網

Vuejs開發去哪兒網


重要說明 -_-||

  • 專案開始日期 2018-07-22,結束日期 2018-07-28
  • 更換開發環境時,記得先npm install一下,再啟動(可能需要安裝新的依賴)
  • 更換開發環境時,切換分支後,記得git pull(只pull分支節點是不夠的)
  • 本專案新元件都在新分支開發,master主幹合併最新分支的的程式碼
  • 開發工具SublimeText3 (調一下右下角的Tab鍵的設定)
  • MarkDown中使用 > (>後啥也不寫)來實現一個格式分割問題(類似清除浮動)–似乎還有問題(使用3個空格可以分開上下連線的問題,有時不能使用>)
  • MarkDown中好像必須在列表下才可能縮排
  • 使用keep-alive元件後,可能導致F5不能載入最新的程式碼,直接重啟dev環境。keep-alive的exclude屬性會導致頁面中不再回調activated (),詳見本文第六節第9點的說明

TODO NEXT _

  • 首頁banner圖下的圖示選單在小屏iPhone上有padding距離不正常的問題
  • 首頁進入城市選擇頁面時,頁面從右向左轉場動畫效

連結 -_-||

1> ES6語法及eslint語法檢查

2> 經驗記錄

效果演示

https://jiangjiesheng.gitee.io/qu-na-er

開發筆記

一、常用指令
  1. git合併到主幹:

    git checkout master
    git merge [分支名稱]    
    git push 
    
二、執行環境
  1. node -v ==> v8.11.3
    npm -v ==> 5.6.0
    #使用淘寶映象
    npm config set registry https://registry.npm.taobao.org
    [npm install -g cnpm --registry=https://registry.npm.taobao.org]
    
三、專案初始化
  1. 安裝vue

    npm install [email protected]^2.5.2 --save
    
  2. 使用腳手架命令列工具vue-cli建立vue專案

    最好先初始化專案,再建立一些記錄性檔案

    #全域性安裝 vue-cli
    npm install --global vue-cli
    #建立一個基於 webpack 模板的新專案
    vue init webpack qu-na-er  (去哪兒網-GitBash中執行沒有反應)
    
    #輸入專案資訊
    >> Project name qu-na-er
    >> Project description A Vue.js project
    >> Author [email protected]
    >> Vue build (Use arrow keys)
    >> Vue build standalone
    >> Install vue-router? Yes
    >> Use ESLint to lint your code? Yes
    >> Pick an ESLint preset Standard
    >> Set up unit tests No
    >> Setup e2e tests with Nightwatch? No
    >> Should we run `npm install` for you after the project has been created? (recommended) npm
    
       vue-cli · Generated "qu-na-er".
    
    # Installing project dependencies ...
    # ========================
    
    #安裝依賴
    cd qu-na-er 
    (這裡是單獨建立一個資料夾並初始化的,所以把專案複製到原始的建立的專案中)
    npm install 
    npm run dev
    
四、認識專案結構並引入必要檔案
  1. reset.css

    統一不同瀏覽器的預設樣式,檔案為src/assets/styles/reset.css,並在main.js中引用

    【本專案使用的尺寸單位是rem,是相對於html的font-size: 50px的大小來設定的】

    1rem = html font-size = 50px
    

    43px = 0.86rem
    
    // 統一不同瀏覽器的預設樣式
    import './assets/styles/reset.css'
    
  2. 1畫素邊框

    可以顯示一個類似垂直分割線的邊框效果,檔案為src/assets/styles/border.css,並在main.js中引用

    // 1畫素邊框解決方案
    import './assets/styles/border.css'
    
  3. 300毫秒點選延遲(在部分瀏覽器上)

    安裝並在main.js中引入並初始化

    npm install fastclick --save
    
    import fastclick from 'fastclick'
    // 300毫秒點選延遲
    fastclick.attach(document.body)
    
  4. iconfont註冊

    使用微博登入 [email protected]

    選單 >> 圖示管理 >> 我的專案

    下載後注意修改iconfont.css中的引用路徑

五、去哪兒網首頁

參考頁面 http://piao.qunar.com/touch/

  1. Header頭

     首先安裝css外掛
    
     npm install stylus --save
     npm install stylus-loader --save
    

    stylus使用

    //scoped 對區域性樣式有效
    <style lang="stylus" scoped>
      .header //不要有冒號
        display: flex
        height: .86rem
        .header-left //不要有冒號
          width: .64rem
          float: left
        .header-input //不要有冒號
          flex: 1
        .header-right //不要有冒號
          width: 1.24rem
          float: right
    </style>
    

    scoped的樣式穿透

    <style lang="stylus" scoped>
      .wrapper >>> .swiper-pagination-bullet-active
         background: red !important
    </style>
    

    style中引入注意

    @表示src目錄別名

    別名配置在build/webpack.base.conf.js中的alias節點

    注意在style使用要加上~

    @import '[email protected]/assets/styles/varibles.styl'
    
  2. 首頁輪播圖

    首先在線上建立分支index-swiper,分支都相對於前一個分支,然後git pull 同步本地分支,並切換到分支index-swiper

    https://www.npmjs.com/package/vue-awesome-swiper

    https://github.com/surmon-china/vue-awesome-swiper

    https://blog.csdn.net/mrliber/article/details/78819191 [配置參考]

    安裝

    npm install [email protected] --save
    

    html上的冒號是繫結屬性,@是繫結事件
    子元件中的屬性需要在data() 中定義並返回物件

    弱網測試:F12 > Network > 選擇3G網路

    設定輪播圖的佔位

     .wrapper
        overflow: hidden
        width: 100%
        height:0
        padding-bottom: 25%
    

    or, 但是可能有相容問題

     .wrapper
        width: 100%
        height:25vw
    

    push程式碼後切換回master主幹,併合並index-swiper分支程式碼

    git checkout master
    git merge index-swiper
    git push 
    
  3. 首頁圖示選單元件

    建立分支index-icons-3

    設定基本佔位效果

    <template>
      <div class="icons">
        <div class="icon">
        </div>
      </div>
    </template>
    
    <script>
    
    export default {
      name: 'HomeIcons'
    }
    </script>
    
    <style lang="stylus" scoped>
      .icons
        overflow: hidden
        height: 0
        padding-bottom: 50%
        background: green
        .icon
          float:left
          width:25%
          padding-bottom: 25%
          background: red
    </style>
    
    

    安裝 vue dev tools Chrome瀏覽器外掛,方便檢視Vue的結構

    Vuejs的計算屬性

    computed: { // 計算屬性
        pages () {
          const pages = []
          this.iconList.forEach((item, index) => {
            const page = Math.floor(index / 8)
            if (!pages[page]) {
              pages[page] = []
            }
            pages[page].push(item)
          })
          return pages
        }
    }
    

    stylus的樣式封裝(相當於方法)

    // src/assets/styles/mixins.styl
    
    ellipsis()
      overflow: hidden
      white-space: nowrap
      text-overflow: ellipsis
    // 應用
    @import '[email protected]/assets/styles/mixins.styl'
    .icon-desc
      height: 0.44rem
      line-height: 0.44rem
      text-align: center
      ellipsis()
    
  4. 首頁推薦元件

    建立分支index-recommend-4

    flex關鍵作用

    //相當於安卓中的layout_weight:1 ,用於撐開剩餘空間部分
    flex:1 
    

    如果ellipsis的省略效果不出現,可以在父級設定min-width:0

    注意: 子元件中定義的data需要使用return一個物件

    export default {
      name: 'HomeRecommend',
      data () {
        return {
          recommendList: [
            {
              id: '0001',
              imgUrl: 'http://img1.qunarzz.com/sight/p0/201406/04/4f597aad25208a233999238c65af9b06.jpg_200x200_d1ea2bd2.jpg',
              title: '南京珍珠泉水上世界',
              desc: '高品質天然泉水水上樂園'
            }
          ]
        }
      }
    }
    
  5. 使用ajax獲取api資料

    安裝axios依賴

    npm install axios --save
    

    在首頁Home.vue中mounted()生命週期函式(又稱鉤子函式)中獲取整個首頁的多個元件API資料。

    基本用法

    import axios from 'axios'
    
    export default {
      name: 'Home',
      components: {
       ...
      },
      methods: {
        getHomeInfo () {
          axios.get('/api/index.json')
            .then(this.getHomeInfoSucc)
        },
        getHomeInfoSucc (res) {
          console.log(res)
        }
      },
      mounted () {
        this.getHomeInfo()
      }
    }
    

    Mock API資料

    /config/index.js >> dev >> proxyTable

    proxyTable: {
        '/api': {
            target: 'http://localhost:8080',
            pathRewrite: {
                '^/api':'/static/mock'
            }
        }
    }
    

    另外關於資源和API請求在線上環境需要分離的處理見

    《npm-資源路徑-本地除錯-線上環境的api配置-環境隔離-打包》

    首頁的父子元件資料傳遞

    // 父元件
    
    // 關鍵位置1 繫結city屬性
    <home-header :city="city"></home-header>
    
    // 關鍵位置2 在data () 中定義city屬性,並更新值
    export default {
      name: 'Home',
      components: {
        ...
      },
      data () {
        return {
          city: ''
        }
      },
      methods: {
        getHomeInfo () {
          axios.get('/api/index.json')
            .then(this.getHomeInfoSucc)
        },
        getHomeInfoSucc (res) {
          res = res.data
          if (res.ret && res.data) {
            const data.city = res.data
            this.city = data.city
          }
        }
      },
      mounted () {
        this.getHomeInfo()
      }
    }
    
    // 子元件 
    
    // 在props中定義屬性並指定型別
    export default {
      name: 'HomeHeader',
      props: {
        city: String
      }
    }
    
    //取父元件傳過來的值
    {{this.city}}
    
六、去哪兒網選擇城市列表頁

參考頁面 http://piao.qunar.com/touch/toNewCityList.htm

  1. CityHeader元件

    建立分支city-router-6

    新增city路由,/src/router/index.js

    {
    path: '/city',
    name: 'City',
    component: City
    }
    

    Header.vue

  2. 快速搜尋元件

    建立分支city-search-7

    Search.vue

  3. 城市列表元件

    建立分支city-list-8

    List.vue

    修改1畫素預設的偽元素的屬性

    .border-topbottom 
    &:before
      border-color: #ccc
    &:after
      border-color: #ccc
    

    Better-Scroll區域滾動元件

    https://github.com/ustbhuangyi/better-scroll

    https://blog.csdn.net/qq_26632807/article/details/77856950 [引數設定]

    不帶引數的better-scroll初始化會導致在安卓手機上不能點選better-scroll區域中的click事件,配置方法見

    安裝Better-Scroll

    npm install better-scroll --save
    

    DOM結構

    <div class="wrapper">
      <ul class="content">
        <li>...</li>
        <li>...</li>
        ...
      </ul>
      <!-- you can put some other DOMs here, -->
      <!-- it won't affect the scrolling -->
    </div>
    

    初始化

    1> 最簡單的初始化(廢棄):

    import BScroll from 'better-scroll'
    const wrapper = document.querySelector('.wrapper')
    const scroll = new BScroll(wrapper)
    

    Better-Scroll提供一個類,當例項化時,其第一個引數是一個純DOM物件。當然,Better-Scroll內部滾動將嘗試使querySelector選擇器來獲取DOM物件,因此初始化程式碼也可以如下所示:

    2> 帶引數的初始化(必須使用

    import BScroll from 'better-scroll'
    
    mounted () {
       const options = { 
       // 處理在better-scroll在安卓手機上不能點選的問題
       // 更多配置見
       // https://ustbhuangyi.github.io/better-scroll/doc/zh-hans/options.html#click
       click: true,
       tap: true
     }
     this.scroll = new Bscroll(this.$refs.wrapper, options)
    },
    

    特別注意要使Better-Scroll生效,可能還需要目標滾動區域要有效果,即預設上下溢位時卻不能滾動才可以

    .list
      overflow: hidden
      position: absolute
      top: 1.58rem
      left: 0
      right: 0
      bottom: 0
      background: red
    

    獲取DOM節點的方法

    // 指定一個ref (不加s)
    <div class="list" ref="wrapper">
    
    // 獲取dom (不加s)
    this.$refs.wrapper
    
  4. 右側字母表元件

    建立分支city-alphabet-9

    Alphabet.vue

    flex又一用法

    display: flex
    flex-direction: column
    justify-content: center
    

    特別注意在微信或者QQ瀏覽器向下滑動會觸發事件冒泡,導致不能通過字母表上下滑動選擇字母

    <li @touchmove="handleTouchMove">{{item}}</li>
    
    methods: {
      handleTouchMove (e) {
        // 微信中處理 向上滑動時整個頁面跟隨滾動的問題
        e.preventDefault() 
        ...
      }
    },
    
  5. ajax獲取資料

    建立分支 city-ajax-10

  6. 兄弟元件間聯動

    建立分支 city-components-11

    基本過程: Alphabet.vue子元件通過$emit發出自定義事件changeLetter

    methods: {
      handleLetterClick (e) {
         this.$emit('changeLetter', e.target.innerText)
        }
    }
    

    City.vue父元件在子元件dom上繫結子元件發出事件的接收函式changeLetter,並通過父元件中的handleLetterChange接收值letter,

    <template>
     <div class="w">
        <city-alphabet @changeLetter="handleLetterChange"></city-alphabet>
      </div>
    </template>  
    ...
    data () {
      return {
          letter: ''
        }
      },
    methods: {
      handleLetterChange (letter) {
        this.letter = letter
      }
    },
    

    City.vue父元件再通過給List.vue子元件繫結屬性的方式,把值letter傳遞給子元件

    :letter="letter"
    

    List.vue子元件通過定義屬性的方式接收傳遞過來的值,並通過來watch監聽letter變化。

    props: {
       letter: String
    },
    watch: {
       letter () {
         console.log(this.letter)
       }
    }
    
  7. 城市列表頁搜尋邏輯處理

    建立分支 city-search-logic-12

    v-model實現資料的雙向繫結

    <input v-model="keyword" class="search-input" type="text" placeholder="輸入城市名或拼音">
    
    data () {
        return {
          keyword: '',
          list: [],
          timer: null
        }
    },
    watch: { // 特別注意:這裡冒號不是watch () { ... }
      keyword () {
        if (this.timer){
            clearTimeout(this.timer)
        }
        this.timer = setTimeout(() => {
        const result = []
        for (let i in this.cities) {
          this.cities[i].forEach((value) => {
          if (value.spell.indexOf(this.keyword) > -1 || value.name.indexOf(this.keyword) > -1) {
             result.push(value)
             }
            })
          }
        this.list = result
     },100)
    }
    

    控制Dom是否顯示

    v-show="!list.length"
    
    or better
    
    v-show="hasNoData"
    
    computed: {
        hasNoData () {
          return !this.list.length
        }
    },
    
  8. vuex實現資料共享

    建立分支city-vuex-13

    官網文件 https://vuex.vuejs.org/zh/

    vuex

    基本使用步驟:

    1> 安裝vuex

    npm install vuex --save
    

    2> 新建/src/store/index.js, 並在main.js中引入

    import Vue from 'vue'
    import Vuex from 'vuex'
    
    Vue.use(Vuex)
    
    export default new Vuex.Store({
      state: {
        city: '南京'
      }
    })
    

    3> 並在main.js中引入,註冊在Vue例項中

    import store from './store/'
    
    new Vue({
      el: '#app',
      router,
      store, // 看我看我
      components: { App },
      template: '<App/>'
    })
    

    4> 取值

    {{this.$store.state.city}}
    

    5> 給熱門城市繫結點選事件,並在點選事件中分發儲存事件通知,在/src/store/index.js接收事件,並繼續處理資料更新。

    也可以直接跳過dispatch,直接修改commit,即

    // this.$store.dispatch('changeCity', city)
    this.$store.commit('toChangeCity', city)
    
    <div v-for="item of hot" :key="item.id" @click="handleCityClick(item.name)">
    
    methods: {
     handleCityClick (city) {
       this.$store.dispatch('changeCity', city)
       alert(city)
      }
    },
    
    /src/store/index.js
    
    export default new Vuex.Store({
      state: {
        city: '南京'
      },
      actions: { //看我看我
        changeCity (ctx, city) {
          console.log(city)
          ctx.commit('toChangeCity', city)
        }
      },
      mutations: {
        toChangeCity (state, city) {
          state.city = city
        }
      }
    })
    

    vuex還支援欄位對映

    import { mapState } from 'vuex'
    
    computed: {
      ...mapState(['city'])
    }
    

    or

    import { mapMutations } from 'vuex'
    
     handleCityClick (city) {
      ...
      this.toChangeCity(city)
    },
    ...mapMutations(['toChangeCity'])
    

    通過js來開啟路由頁面

    this.$router.push('/')
    
  9. 使用keep-alive優化效能

    建立分支 city-keepalive-14

    修改App.vue

    <template>
      <div id="app">
        <keep-alive> // 看我看我
          <router-view/>
        </keep-alive>
      </div>
    </template>
    

    此時又需要處理資料動態改變時頁面需要重新請求資料,例如當前城市改變時,需求載入當前城市的資訊

    使用keep-alive後,mounted周期函式只會被呼叫一次,但是activated周期函式會每次都會被呼叫。所以…

    在Home.vue中使用vuex,並在computed計算屬性中對映一個city物件,同時定義一個屬性lastCity標記上一個城市。

    然後在activated周期函式中判斷當前選擇城市和上一個城市是否相等,不相等則將當前城市作為引數重新請求一次ajax。

    import { mapState } from 'vuex'
    
    data () {
      return {
        lastCity: '',
       }
    },
    computed: {
      ...mapState(['city'])
    },
    methods: {
      getHomeInfo () {
         axios.get('/api/index.json?city=' + this.city)
            .then(this.getHomeInfoSucc)
        }
    },
    mounted () { // 頁面初始化
      this.lastCity = this.city
      console.log('mounted')
      this.getHomeInfo()
    },
    activated () { // 頁面可見時
      if (this.lastCity !== this.city) {
        this.lastCity = this.city
        this.getHomeInfo()
      }
       console.log('activated')
    }
    

    另外也可以使用來排除一部分頁面,使其不使用快取,例如城市詳情頁,修改App.vue

    <keep-alive exclude="Detail">
      <router-view/>
    </keep-alive>
    

    特別注意: exclude會導致頁面中不再回調activated (), 但是會呼叫created (),所以一些需要重新初始化的方法或者屬性需要在created () 呼叫。

    例如:src/pages/detail/components/Header.vue

七、城市詳情頁面
  1. Banner

    建立分支detail-banner-15

    將li標籤換成router-link:

    解決router-link預設會改成標籤顏色問題

    注意需要同時在router-link標籤上增加tag屬性,指定需要渲染成li標籤,同時指定to屬性。

    <router-link tag="li" v-for="item of list" :key="item.id" :to="'/detail/' + item.id">
      ...
    </router-link>
    

    註冊動態路由,獲取路由引數

     {
      path: '/detail/:id',
      name: 'Detail',
      component: Detail
     }
    

    參考 http://touch.piao.qunar.com/touch/detail.htm?id=33782

    建立DetailBanner元件

    漸變效果

    background-image: linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.8))
    
  2. 共用元件畫廊

    swiper 中文網

    http://3.swiper.com.cn/api/pagination/2016/0126/299.html

    程式碼見 /src/common/gallary/Gallary.vue

  3. Header漸隱漸顯效果

    建立分支 detail-header-16

    特別注意:獲取滾動條滾動的垂直距離scrollTop的相容問題

    通過繫結:style的opacity屬性來動態改變透明度

    <router-link tag="div" to="/" class="header-abs" 
    v-show="showAbs"  :style="opacityAbsStyle">
       <div class="iconfont back-icon-back">&#xe624;</div>
    </router-link>
    
    <div class="header-fixed" v-show="!showAbs" :style="opacityFixedStyle">
       <router-link to="/">
          <div class="iconfont header-fixed-back">&#xe624;</div>
       </router-link>
       景點詳情
    </div>
    
    
    data () {
      return {
          showAbs: true,
          opacityAbsStyle: { // 返回鍵的漸變
            opacity: 1
          },
          opacityFixedStyle: { // 固定標題欄的漸變
            opacity: 0
          }
        }
      },
      methods: {
        handleScroll () {
          // 特別注意,scrollTop中的每一對()都是來自網路的一種取法。已相容安卓瀏覽器和UA為蘋果的瀏覽器
          
          const scrollTop = (window.parent.document.documentElement.scrollTop || window.parent.document.body.scrollTop) 
          || (document.body.scrollTop + document.documentElement.scrollTop) 
          || (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0)
      
          if (top > 40) { // 固定標題欄的漸變
            let opacity = top / 130
            opacity = opacity > 1 ? 1 : opacity
            this.opacityFixedStyle = {
              opacity
            }
            this.showAbs = false
          } else { // 返回鍵的漸變
            this.showAbs = true
            let opacity = top / 40
            opacity = opacity > 1 ? 1 : opacity
            this.opacityAbsStyle = {
              opacity: (1 - opacity)
            }
          }
        }
      },
      activated () {
        window.addEventListener('scroll', this.handleScroll)
      }
    
  4. 對全域性事件解綁

    處理上一節window.addEventListener

    activated () {
        window.addEventListener('scroll', this.handleScroll)
    }
    

    解綁事件:

    deactivated () {
      window.removeEventListener('scroll', this.handleScroll)
    }
    
  5. 使用遞迴元件實現詳情頁列表

    建立分支 detail-list-17

    json資料

    list: [{
        title: '成人票',
        children: [{
          title: '成人三管聯票',
          children: [{
            title: '成人三管聯票 - 某一連鎖店銷售'
          }]
        }, {
          title: '成人五管聯票'
        }]
      }, {
        title: '學生票'
      }, {
        title: '兒童票'
      }, {
        title: '特惠票'
      }]
    

    遞迴呼叫detail-list元件(List.vue)

    <div>
      <div class="item" v-for="(item,index) of list" :key="index">
        <div class="item-title border-bottom">
         <span class="item-title-icon"></span>
             {{item.title}}
        </div>
        
        // 看這裡
        <div v-if="item.children" class="item-children">
              <detail-list :list="item.children"></detail-list>
        </div>
       </div>
    </div>
    
  6. ajax獲取資料

    建立分支 detail-ajax-18

    注意一下,使用屬性繫結時加上冒號

    <img class="banner-img" :src="bannerImg">
    

    顯示文字,使用{{屬性xxx}}即可

    <div class="banner-title">{{this.sightName}}</div>
    // 這裡的this可以去掉,但是大部分js環境的this不能去
    

    使用計算屬性控制是否顯示一個列表部分(避免按空陣列初始化,導致有資料變化時的當前顯示的索引為列表的最後一個)

    <swiper :options="swiperOption" v-if="isShowGallary">
     // 需要特別注意,這個不要使用v-show,而是v-if
       ···
    </swiper>
     
    computed: {
     isShowGallary () {
       return this.imgs.length > 0
     }
    }
    

    路由行為

    開啟新頁面顯示到頂部,不能受到上一頁上下滾動的距離影響

    https://cn.vuejs.org/v2/guide/migration-vue-router.html#saveScrollPosition-替換

    修改router/index.js

    export default new Router({
    routes: [{
        path: '/',
        name: 'HelloWorld',
        component: Home
      }], // 開啟新頁面顯示到頂部,不能受到上一頁上下滾動的距離影響
      scrollBehavior: function (to, from, savedPosition) {
        return savedPosition || { x: 0, y: 0 }
      }
    })
    

    更多關於路由介紹

    https://cn.vuejs.org/v2/guide/routing.html

  7. 在專案中使用基本動畫

    建立分支 detail-animation-19

    程式碼在src/common/fade/FadeAnimation.vue (淡入淡出效果)

    <template>
      <transition>
        <!-- 插槽 呼叫方的包裹的子元件會填充到這裡 -->
        <slot></slot>
      </transition>
    </template>
    
    <script>
    
    export default {
      name: 'FadeAnimation'
    }
    </script>
    
    <style lang="stylus" scoped>
      .v-enter, .v-leave-to
        opacity: 0
      .v-enter-active, .v-leave-active
        transition: opacity .5s
    </style>
    
    

    應用在在src/pages/detail/components/Banner.vue中

     <fade-animation>
      <!-- 這裡會填充到插槽 -->
      <common-gallary :imgs="bannerImgs" v-show="showGallary"></common-gallary>
    </fade-animation>
    

開發結束,非常感謝您閱讀此文!

作者:江節勝
微信:767000122 (歡迎新增好友)
Q Q :596957738
個人網站:www.jiangjiesheng.com
聯絡郵箱:[email protected]