• 保存到桌面  加入收藏  设为首页
HTML5

H5-Vue2.5开发去哪儿网App 从零基础入门到实战项目-开发笔记

时间:2018-07-29 14:19:38   作者:江节胜   来源:胜行天下网   阅读:568   评论:0


Vuejs开发去哪儿网
 
MarkDown版本:
 
重要说明 -_-||
项目开始日期 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距离不正常的问题
首页进入城市选择页面时,页面从右向左转场动画效
 

效果演示


https://gitee.com/jiangjiesheng/qu-na-er
 
开发笔记
一、常用指令
git合并到主干:
 
git checkout master
git merge [分支名称]    
git push 
二、运行环境
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]
三、项目初始化
安装vue
 
npm install vue@^2.5.2 --save
使用脚手架命令行工具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 dev@jiangjiesheng.com
>> 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
四、认识项目结构并引入必要文件
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'
1像素边框
 
可以显示一个类似垂直分割线的边框效果,文件为src/assets/styles/border.css,并在main.js中引用
 
// 1像素边框解决方案
import './assets/styles/border.css'
300毫秒点击延迟(在部分浏览器上)
 
安装并在main.js中引入并初始化
 
npm install fastclick --save
import fastclick from 'fastclick'
// 300毫秒点击延迟
fastclick.attach(document.body)
iconfont注册
 
使用微博登录 jiangjsheng@sina.cn
 
菜单 >> 图标管理 >> 我的项目
 
下载后注意修改iconfont.css中的引用路径
 
五、去哪儿网首页
参考页面 http://piao.qunar.com/touch/
 
Header头
 
 首先安装css插件
 npm install stylus --save
 npm install stylus-loader --save
stylus使用
 
//scoped 对局部样式有效
scoped的样式穿透
 
style中引入注意
 
@表示src目录别名
 
别名配置在build/webpack.base.conf.js中的alias节点
 
注意在style使用要加上~
 
@import '~@/assets/styles/varibles.styl'
首页轮播图
 
首先在线上创建分支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 vue-awesome-swiper@2.6.7 --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 
首页图标菜单组件
 
创建分支index-icons-3
 
设置基本占位效果
 
 
 
 
安装 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 '~@/assets/styles/mixins.styl'
.icon-desc
  height: 0.44rem
   -height: 0.44rem
  text-align: center
  ellipsis()
首页推荐组件
 
创建分支index-recommend-4
 
flex关键作用
 
//相当于安卓中的layout_weight:1 ,用于撑开剩余空间部分
flex:1 
如果ellipsis的省略效果不出现,可以在父级设置min-width:0
 
注意: 子组件中定义的data需要使用return一个对象
 
export default {
  name: 'HomeRecommend',
  data () {
    return {
      recommendList: [
        {
          id: '0001',
          title: '南京珍珠泉水上世界',
          desc: '高品质天然泉水水上乐园'
        }
      ]
    }
  }
}
使用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属性
"city">
 
// 关键位置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
 
CityHeader组件
 
创建分支city-router-6
 
新增city路由,/src/router/index.js
 
{
path: '/city',
name: 'City',
component: City
}
Header.vue
 
快速搜索组件
 
创建分支city-search-7
 
Search.vue
 
城市列表组件
 
创建分支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结构
 
class="wrapper">
  
    class="content">
    
  • ...
  •     
  • ...
  •     ...
      
      
      
    初始化
     
    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在安卓手机上不能点击的问题
       // 更多配置见
       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)
    class="list" ref="wrapper">
     
    // 获取dom (不加s)
    this.$refs.wrapper
    右侧字母表组件
     
    创建分支city-alphabet-9
     
    Alphabet.vue
     
    flex又一用法
     
    display: flex
    flex-direction: column
    justify-content: center
    特别注意在微信或者QQ浏览器向下滑动会触发事件冒泡,导致不能通过字母表上下滑动选择字母
     
  • @touchmove="handleTouchMove">{{item}}
  •  
    methods: {
      handleTouchMove (e) {
        // 微信中处理 向上滑动时整个页面跟随滚动的问题
        e.preventDefault() 
        ...
      }
    },
    ajax获取数据
     
    创建分支 city-ajax-10
     
    兄弟组件间联动
     
    创建分支 city-components-11
     
    基本过程: Alphabet.vue子组件通过$emit发出自定义事件changeLetter
     
    methods: {
      handleLetterClick (e) {
         this.$emit('changeLetter', e.target.innerText)
        }
    }
    City.vue父组件在子组件dom上绑定子组件发出事件的接收函数changeLetter,并通过父组件中的handleLetterChange接收值letter,
     
      
    ...
    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)
       }
    }
    城市列表页搜索逻辑处理
     
    创建分支 city-search-logic-12
     
    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
        }
    },
    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: ''
    })
    4> 取值
     
    {{this.$store.state.city}}
    5> 给热门城市绑定点击事件,并在点击事件中分发存储事件通知,在/src/store/index.js接收事件,并继续处理数据更新。
     
    也可以直接跳过dispatch,直接修改commit,即
     
    // this.$store.dispatch('changeCity', city)
    this.$store.commit('toChangeCity', city)
    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('/')
    使用keep-alive优化性能
     
    创建分支 city-keepalive-14
     
    修改App.vue
     
    此时又需要处理数据动态改变时页面需要重新请求数据,例如当前城市改变时,需求加载当前城市的信息
     
    使用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
     
    "Detail">
      
    特别注意: exclude会导致页面中不再回调activated (), 但是会调用created (),所以一些需要重新初始化的方法或者属性需要在created () 调用。
     
    例如:src/pages/detail/components/Header.vue
     
    七、城市详情页面
    Banner
     
    创建分支detail-banner-15
     
    将li标签换成router-link:
     
    解决router-link默认会改成标签颜色问题
     
    注意需要同时在router-link标签上增加tag属性,指定需要渲染成li标签,同时指定to属性。
     
    "li" v-for="item of list" :key="item.id" :to="'/detail/' + item.id">
      ...
    注册动态路由,获取路由参数
     
     {
      path: '/detail/:id',
      name: 'Detail',
      component: Detail
     }
    参考 http://touch.piao.qunar.com/touch/detail.htm?id=33782
     
    创建DetailBanner组件
     
    渐变效果
     
    background-image:  ar-gradient(top, rgba(0000), rgba(0000.8))
    共用组件画廊
     
    swiper 中文网
     
    http://3.swiper.com.cn/api/pagination/2016/0126/299.html
     
    代码见 /src/common/gallary/Gallary.vue
     
    Header渐隐渐显效果
     
    创建分支 detail-header-16
     
    特别注意:获取滚动条滚动的垂直距离scrollTop的兼容问题
     
    通过绑定:style的opacity属性来动态改变透明度
     
    "div" to="/" class="header-abs" 
    v-show="showAbs"  :style="opacityAbsStyle">
       
    class="iconfont back-icon-back">
     
    class="header-fixed" v-show="!showAbs" :style="opacityFixedStyle">
       "/">
          
    class="iconfont header-fixed-back">
       
       景点详情
     
    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)
      }
    对全局事件解绑
     
    处理上一节window.addEventListener
     
    activated () {
        window.addEventListener('scroll'this.handleScroll)
    }
    解绑事件:
     
    deactivated () {
      window.removeEventListener('scroll'this.handleScroll)
    }
    使用递归组件实现详情页列表
     
    创建分支 detail-list-17
     
    json数据
     
    list: [{
        title: '成人票',
        children: [{
          title: '成人三管联票',
          children: [{
            title: '成人三管联票 - 某一连锁店销售'
          }]
        }, {
          title: '成人五管联票'
        }]
      }, {
        title: '学生票'
      }, {
        title: '儿童票'
      }, {
        title: '特惠票'
      }]
    递归调用detail-list组件(List.vue)
     
      
    class="item" v-for="(item,index) of list" :key="index">
        
    class="item-title border-bottom">
         class="item-title-icon">
             {{item.title}}
        
         
        // 看这里
        
    if="item.children" class="item-children">
              "item.children">
        
       
    ajax获取数据
     
    创建分支 detail-ajax-18
     
    注意一下,使用属性绑定时加上冒号
     
    H5-Vue2.5开发去哪儿网App_从零基础入门到实战项目-开发笔记class="banner-img" :src="bannerImg">
    显示文本,使用{{属性xxx}}即可
     
    class="banner-title">{{this.sightName}}
    // 这里的this可以去掉,但是大部分js环境的this不能去
    使用计算属性控制是否显示一个列表部分(避免按空数组初始化,导致有数据变化时的当前显示的索引为列表的最后一个)
     
    "swiperOption" v-if="isShowGallary">
     // 需要特别注意,这个不要使用v-show,而是v-if
       ···
      
    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
     
    在项目中使用基本动画
     
    创建分支 detail-animation-19
     
    代码在src/common/fade/FadeAnimation.vue (淡入淡出效果)
     
     
     
     
    应用在在src/pages/detail/components/Banner.vue中
     
     
      
      "bannerImgs" v-show="showGallary">
    开发结束,非常感谢您阅读此文!
    个人网站 tech.jiangjiesheng.cn
     
    QQ:596957738
     
    微信:596957738
     
    EMail: dev@jiangjiesheng.com


    有任何疑问或技术合作都可联系我

    微信:767000122 【推荐】

    QQ:596957738


    相关评论

    加我微信 596957738 (QQ同号)加我微信   QQ联系:596957738   地址:江苏省南京市雨花台区

    苏公网安备32011402010305号

    皖ICP备16019989号

    江节胜的Gitee,江节胜的Git地址