环境配置
vue/cli 3.0 以下版本
如果有新的脚手架,先卸载
npm uninstall @vue/cli -g
- 安装脚手架
cnpm i vue-cli -g - 创建项目
vue init webpack my-pro - 项目初始化如下:
4. 进入并启动项目
cd my-pro and cnpm run dev
项目代码初始化
- 完善meta:用户不能通过手指放大缩小页面
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
- 在入口文件main.js 引入 reset 和 border 样式表
// 统一不同手机初始化样式
import './assets/styles/reset.css'
// 1px 物理像素实现
import './assets/styles/border.css'
项目中所用技术
CSS预处理器 stylus
- 安装
// 安装高版本的容易出错,所以:
npm install stylus@0.54.5 --save
npm install stylus-loader@3.0.1 --save
- 使用
// varibles.styl 项目总色调集合
$bgColor = #00bcd4
$darkTextColor = #333
$headerHeight = .86rem
// mixins.styl 通用样式代码:如超出显示省略号
ellipsis()
overflow: hidden
white-space: nowrap
text-overflow: ellipsis
// 所用页面
<style lang="stylus" scoped>
@import '~styles/varibles.styl'
@import '~styles/mixins.styl'
.header
display: flex
line-height: $headerHeight
background: $bgColor
color: #fff
ellipsis()
.box
width: 100%
</style>
// 注意:如果要改变插件中样式,需使用 >>> 将其样式透传到该组件中
.box >>> .swiper-container
引入 iconfont 图标库
- 阿里巴巴矢量图标库
- 资源管理、我的项目、新建项目。
- 寻找所需图标、添加到购物车、在购物车中添加至项目。
- 图标为
Unicode类型,下载至本地。 - 根路径src、assets,新建
stylues文件夹 - 该文件夹下放
reset.css(代码样式重置)、border.css(1px边框问题)、varibles.styl(项目总色调集合)、mixins.styl(通用样式代码:如超出显示省略号)、iconfont.css(iconfont下载的css,注意改变里面src路径) - stylues 文件夹下新建
iconfont,里面放置从iconfont下载的iconfont.ttficonfont.wofficonfont.woff2 - 使用:
8. 1 删除
iconfont.css中定义图标的类。
8. 2 在main.js中引入 iconfont.css。
8. 3 在 iconfont 中复制所需图片的代码,粘贴到代码里。
import './assets/styles/iconfont.css'
<div class="iconfont"></div>
简化文件引入路径
// 单页面vue中引入外部 css
@import '../../../assets/styles/varibles.styl'
// 用 @ 前面一定要加 ~
@import '~@/assets/styles/varibles.styl'
一些路径被反复调用时,可在build、webpack.base.conf.js 中修改路径代码
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
'styles': resolve('src/assets/styles'), // 新增
'common': resolve('src/common') // 新增
}
},
// 简化前
import './assets/styles/reset.css'
import './assets/styles/iconfont.css'
import './assets/styles/varibles.css'
@import '~@/assets/styles/varibles.styl'
// 简化后
import 'styles/reset.css'
import 'styles/iconfont.css'
import 'styles/varibles.css'
@import '~styles/varibles.styl'
1px border 问题
因为之前在 main.js 中引入过 border.css 样式,所以在代码中可直接使用
// border-bottom 该类名已有
<div class="title border-bottom">城市</div>
// 如需改变颜色
.border-bottom
&:before
border-color: #ccc
300ms延迟问题
当我们点击页面的时候移动端浏览器并不是立即作出反应,而是会等上一小会儿才会出现点击的效果。
// 在reset.css中加入
html{touch-action: manipulation}
轮播图
- 安装
cnpm install vue-awesome-swiper@2.6.7 --save
- 全局引用
// main.js 中引用
import VueAwesomeSwiper from 'vue-awesome-swiper'
import 'swiper/css/swiper.css'
Vue.use(VueAwesomeSwiper)
- 在页面中使用
// 有swiperOptions变量的绑定,需在子组件data里绑定
<div class="wrapper">
<swiper :options="swiperOptions">
<swiper-slide>Slide 1</swiper-slide>
<swiper-slide>Slide 2</swiper-slide>
<div class="swiper-pagination" slot="pagination"></div>
</swiper>
</div>
data () {
return {
swiperOption: {
pagination: '.swiper-pagination', // 分页器,fraction 以数字形式展示
loop: true, // 循环轮播
autoplay: false, // 禁止自动轮播
observeParents: true, // 监听到父级元素发生变化时,刷新一次
observer: true
}
}
},
抖动问题
原因:在轮播图未加载完时高度为0,下方内容会自动在轮播图位置,而当轮播图加载完后,刚才下方内容会被挤到下方。造成视觉上抖动。
// 在swiper 父级添加该样式可解决抖动问题
.wrapper
overflow: hidden
width: 100%
height: 0
padding-bottom: 31.25% // 重点 图片宽高比例
默认不是第一张轮播图问题
原因:未加载数据前,list为空,所以出现该问题
// 当数据存在时再显示
<swiper :options="swiperOption" v-if="list.length"></swiper>
数据请求 axios
- 安装
cnpm i axios --save
- 局部使用
import axios from 'axios'
methods: {
async getHomeInfo () {
let res = await axios.get('/api/index.json?city=' + this.city)
...
},
mounted () {
this.getHomeInfo()
}
静态文件放在 static 文件夹下可访问
跨域问题
// config、index.js中
proxyTable: {
'/api': {
target: 'http://localhost:8080',
pathRewrite: {
'^/api': '/static/mock'
}
}
},
Vuex
Vuex: 一个专为 Vue.js 应用程序开发的状态管理模式。
- 安装
cnpm i vuex --save
- 使用
- 根路径src、新建 vuex 文件夹,再新建 index.js。
import Vue from 'vue'
import Vuex from 'vuex'
// 因为 vuex 是个插件,vue中使用插件方法 Vue.use
Vue.use(Vuex)
const store = new Vuex({
state: { // 存放全局公用数据
city: '上海'
},
actions: { // 处理异步操作
// ctx 上下文,city 页面的传值
changeCityA (ctx, city) {
ctx.commit('changeCityM', city)
}
},
mutations: { // 处理同步操作
changeCityM (state, city) {
state.city = city
}
}
})
export default store
- main.js
import store from './store'
new Vue({
el: '#app',
router,
...
})
- 页面中
<div>{{ this.$store.state.city }}</div>
methods: {
handleCityClick (name) {
// 异步操作时:需要派发一个名字为 changeCityA 的action,并把名字传给它
this.$store.dispatch('changeCityA', name)
// 没有异步操作时:
this.$store.commit('changeCityM', name)
// 改变之后跳转到首页,写法1/2
this.$router.push('/')
this.$router.push({name: 'Home'})
},
}
localStorage
注意:当使用 vuex 改变数据后,一旦刷新页面,数据又会变成初始化的数据,所以此时需要结合本地存储 localStorage。
注意:只要使用了 localStorage,就要用 try catch,因为如果用户关闭了本地存储功能或者使用隐身模式,会导致代码运行异常。
import Vue from 'vue'
import Vuex from 'vuex'
// 因为 vuex 是个插件,vue中使用插件方法 Vue.use
Vue.use(Vuex)
let defaultCity = '上海'
try {
defaultCity = localStorage.city ? localStorage.city : defaultCity
} catch(e){}
const store = new Vuex({
state: {
city: localStorage.city || '上海'
},
// 类似于vue的computed,当需要对state的数据做一些尝试的时候放到Getter上
getters: {
doubleCity (state) {
return state.city + ' ' + state.city
}
},
mutations: { // 处理同步操作
changeCityM (state, city) {
state.city = city
try {
localStorage.city = city
} catch(e) {}
}
}
})
export default store
高级用法
<div>{{ city }}</div>
import { mapState, mapMutations } from 'vuex'
computed: {
// 将vuex中的数据city映射到该组件的计算属性computed中
...mapState(['city'])
// 也可以这样写
...mapState({
currentCity: 'city'
})
},
methods: {
handleCityClick (name) {
this.changeCityM(name)
this.$router.push({name: 'Home'})
},
...mapMutations(['changeCityM'])
},
keep-alive
每次切换路由的时候,数据都会请求一次。
<router-view/>当前路由地址所对应内容keep-alive路由中内容被加载一次后,就将其放在内存中。下次再进入该组件时不需要重新渲染该组件exclude除了该页面,其他页面都缓存
// App.vue
<keep-alive exclude="detail">
<router-view/>
</keep-alive>
多出的生命周期函数
注意:当该页面需要重新渲染时,
exclude后,页面中mounted,destroyed...可正常使用。- 使用新增的生命周期函数,对其进行调用。
// 首次进入页面时都会执行
mounted () {
console.log('--- mounted ---')
this.lastCity = this.city
this.getHomeInfo()
},
activated () { // 当选择城市不同时才会执行
console.log('--- activated ---')
if (this.lastCity !== this.city) {
this.lastCity = this.city
this.getHomeInfo()
}
}
// 页面被隐藏的时候执行,相当于 destroyed
deactivated () {}
首页开发
注:750px的UI图,则为 iphone6 设计的 2倍图。
将主页面分成组件开发,利于维护
- 根路径下新建pages文件夹,里面为所有文件的集合。
- 根路径src、pages、home,新建
Home.vue。 - 根路径src、pages、home,新建 components,里面放置
Header.vueSwiper.vueIcons.vueRecommend.vueWeekend.vue。
图标区Icons.vue
注意:轮播图循环的层数,实际为双层数组。
<swiper>
<swiper-slide v-for="(page, index) of pages" :key="index">
<div
class="icon"
v-for="item of page"
:key="item.id"
>
<div class='icon-img'>
<img class='icon-img-content' :src='item.imgUrl' />
</div>
<p class="icon-desc">{{item.desc}}</p>
</div>
</swiper-slide>
</swiper>
// 计算总循环层数,然后将图片分别放在每层里面
computed: {
pages () {
const pages = []
this.list.forEach((item, index) => {
const page = Math.floor(index / 8)
if (!pages[page]) {
pages[page] = []
}
pages[page].push(item)
})
return pages
}
}
home主页面
<template>
<div>
// 页面头部
<home-header :city="city"></home-header>
// 轮播图
<home-swiper :swiperList="swiperList"></home-swiper>
// 图标列表
<home-icons :iconList="iconList"></home-icons>
// 热销推荐
<home-recommend :recommendList="recommendList"></home-recommend>
// 周末去哪
<home-weekend :weekendList="weekendList"></home-weekend>
</div>
</template>
<script>
import HomeHeader from './components/Header'
...
import axios from 'axios'
export default {
name: 'home',
components: {
HomeHeader,
...
},
data () {
return {
city: '',
...
}
},
methods: {
async getHomeInfo () {
let res = await axios.get('/api/index.json')
if (res.data.ret && res.data.data) {
let resData = res.data.data
this.city = resData.city
...
}
}
},
mounted () {
this.getHomeInfo()
}
}
</script>
<style scoped></style>
城市页面开发
- 根路径src、pages、city,新建
City.vue。 - 根路径src、pages、city,新建 components,里面放置
Alphabet.vueHeader.vueList.vueSearch.vue。 跳转到城市页面
// tap 将默认a元素转为div元素,to 跳转链接
<router-link tap='div' to='/city'>城市</router-link>
Bscroll使用
- 安装
cnpm i better-scroll@1.15.2 --save
- 使用
// 注意:需要符合该 DOM 结构
<div class="wrapper" ref='wrapper'>
<ul class="content">
<li>...</li>
<li>...</li>
...
</ul>
</div>
import Bscroll from 'better-scroll'
mounted () {
this.scroll = new Bscroll(this.$refs.wrapper, {
click: true, // 让其可点击
})
}
点击侧边栏字母,让页面滚动到对应位置
this.scroll.scrollToElement(DOM)
// 监听传值到该页面的 letter 是否改变
watch: {
letter () {
console.log(this.letter)
if (this.letter) {
// 获取该DOM元素
let element = this.$refs[this.letter][0]
this.scroll.scrollToElement(element)
}
}
},
滑动侧边栏字母,让页面对应滑动
逻辑:获取首字母距离顶部高度,滑动时候手指距离顶部高度,所以可以得到手指与首字母的距离。再除以每个字母高度,就可以知道滑动到哪个字母了。
<ul class="list">
<li
v-for="item of letters"
:key="item"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
>
{{item}}
</li>
</ul>
data () {
return {
touchStatus: false, // 标识位,touchstart后才会触发
startY: 0,
timer: null // 函数节流,因为手指移动的时候触发函数频率很高,不利于性能优化
}
},
updated () {
// 初始时 cities 为空,后来重新渲染后有值了
console.log('--- alphabet updated ---')
this.startY = this.$refs['A'][0].offsetTop
},
methods: {
handleTouchStart () {
this.touchStatus = true
},
handleTouchMove (e) {
if (!this.touchStatus) return
if (this.timer) {
clearTimeout(this.timer)
}
// 正在做的时候延迟10ms执行,假设在这10ms内又去做手指滚动的话,那么会将上一次的操作给清除掉,重新执行这次要做的事情。
this.timer = setTimeout(() => {
// 79 为城市页面头部和搜索框高度,20 为每个字母高度
const touchY = e.touches[0].clientY - 79
const index = Math.floor((touchY - this.startY) / 20)
if (index >= 0 && index <= this.letters.length) {
// 传值给父页面,父页面再传给子页面
this.$emit('change', this.letters[index])
}
}, 10)
},
handleTouchEnd () {
this.touchStatus = false
}
}
搜索逻辑实现
逻辑:主要是通过该页面数据循环匹配到搜索出的数据,展示对应内容。
做法为通过 indexOf 与 名字 和 拼音简写 相匹配
- cities 中单个城市 结构
{
"id": 1,
"spell": "beijing",
"name": "北京"
}
- 页面代码具体实现
<template>
<div>
<div class="search">
<input v-model="keyword" class="search-input" type="text" placeholder="输入城市名或拼音" />
</div>
<div
class="search-content"
ref="search"
v-show="keyword"
>
<ul>
<li
class="search-item border-bottom"
v-for="item of list"
:key="item.id"
>
{{item.name}}
</li>
<li class="search-item border-bottom" v-show="hasNoData">
没有找到匹配数据
</li>
</ul>
</div>
</div>
</template>
<script>
import Bscroll from 'better-scroll'
export default {
name: 'CitySearch',
props: {
cities: Object
},
data () {
return {
keyword: '', // 有搜索内容时显示列表,没有时隐藏
list: [], // 搜索结果列表
timer: null // 函数节流
}
},
computed: { // 页面上最好不要放置代码逻辑实现
hasNoData () { // 没有搜索到内容时显示:没有找到匹配数据
return !this.list.length
}
},
watch: {
keyword () {
if (this.timer) {
clearTimeout(this.timer)
}
if (!this.keyword) {
this.list = []
return
}
this.timer = setTimeout(() => {
const result = []
for (let i in this.cities) {
this.cities[i].forEach(val => {
if (val.spell.indexOf(this.keyword) !== -1 || val.name.indexOf(this.keyword) !== -1) {
result.push(val)
}
})
}
this.list = result
}, 100)
}
},
mounted () { // 搜索内容多的时候让其可滚动
this.scroll = new Bscroll(this.$refs.search, {
click: true
})
}
}
</script>
城市主页面
<template>
<div>
// 页面头部
<city-header></city-header>
// 页面搜索
<city-search :cities="cities"></city-search>
// 页面列表
<city-list :cities="cities" :hot="hotCities" :letter="letter" </city-list>
// 页面侧边栏字母列表
<city-alphabet :cities="cities" @change="clickLetter" ></city-alphabet>
</div>
</template>
<script>
import axios from 'axios'
import CityHeader from './components/Header'
...
export default {
name: 'City',
components: {
CityHeader,
...
},
data () {
return {
cities: {},
hotCities: [],
letter: ''
}
},
methods: {
async getCityInfo () {
let res = await axios.get('/api/city.json')
if (res.data.ret && res.data.data) {
const data = res.data.data
this.cities = data.cities
...
}
},
clickLetter (e) {
this.letter = e
}
},
mounted () {
this.getCityInfo()
}
}
</script>
<style>
</style>
详情页面
header 渐隐渐现效果
逻辑:通过对页面滑动高度的判断,分别显示不同的头部效果。
<div v-show="showAbs">内容一</div>
<div v-show="!showAbs" :style="opacityStyle">内容二</div>
data () {
return {
showAbs: true,
opacityStyle: {
opacity: 0
}
}
},
methods: {
handleScroll () {
let top = document.documentElement.scrollTop || document.body.scrollTop || window.pageYOffset
if (top > 60) {
let opacity = top / 120 > 1 ? 1 : top / 120
this.opacityStyle.opacity = opacity
this.showAbs = false
} else {
this.showAbs = true
}
}
},
mounted () {
window.addEventListener('scroll', this.handleScroll)
},
// 对全局事件的解绑
destroyed () {
window.removeEventListener('scroll', this.handleScroll)
}
递归组件实现列表部分
循环调用自己
<div class="item" v-for="(item, key) of list" :key="key">
<div class="item-title border-bottom">
<span class="item-title-icon"></span>
{{ item.title }}
</div>
<div v-if="item.children" class="item-chilren">
<detail-list :list="item.children"></detail-list>
</div>
</div>
name: 'DetailList', // script中name属性,很大一个作用就是为了使用递归组件
list: [{
"title": "成人票",
"children": [{
"title": "成人三馆联票",
"children": [{
"title": "成人三馆联票 - 某一连锁店销售"
}]
},{
"title": "成人五馆联票"
}]
}, {
"title": "学生票"
},]
详情主页面
<template>
<div>
// 图片部分
<detail-banner :bannerImg="bannerImg" :sightName="sightName" :gallaryImgs="gallaryImgs"></detail-banner>
// 头部
<detail-header></detail-header>
// 内容列表
<div class="content">
<detail-list :list="categoryList"></detail-list>
</div>
</div>
</template>
<script>
import DetailBanner from './components/Banner.vue'
...
import axios from 'axios'
export default {
name: 'detail',
components: {
DetailBanner,
...
},
data () {
return {
bannerImg: '',
...
}
},
methods: {
async getDetailInfo () {
let upData = {
params: {
id: this.$route.params.id
}
}
let res = await axios.get('/api/detail.json', upData)
if (res.data.ret && res.data.data) {
let resData = res.data.data
this.bannerImg = resData.bannerImg
...
}
}
},
mounted () {
this.getDetailInfo()
}
}
</script>
添加动画效果
全局公用组件,可放置在根路径下文件夹中
- 根路径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>
- 所用页面
<fade-animation>
<div>内容</div>
</fade-animation>
import FadeAnimation from 'common/fade/FadeAnimation'
环境配置
vue/cli 3.0 及以上版本
如果有老的脚手架,先卸载
npm uninstall vue-cli -g当版本升级后,老项目中很多文件已经没有了。具体可见文章末尾出:项目代码结构。
- 安装新的脚手架
cnpm i @vue/cli -g - 创建项目
vue create mypro - 项目初始化如下:
4. 进入并启动项目
cd mypro and cnpm run serve
代码迁移
- 在
package.json中找到安装的第三方依赖包,放置在新项目中对应位置。
"dependencies": {
"vue": "^2.5.2",
"vue-router": "^3.0.1",
"vuex": "^3.4.0",
// 以下三个是需要的第三方包
"axios": "^0.19.2",
"better-scroll": "^1.15.2",
"vue-awesome-swiper": "^2.6.7",
// 不需要安装,脚手架自己带的
"stylus": "^0.54.7",
"stylus-loader": "^3.0.2",
},
- 老项目中 static 文件夹,相当于新项目中 public 文件夹
- 在文件根目录下新建
vue.config.js
const path = require('path');
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:8080',
pathRewrite: {
'^/api': '/mock'
}
}
}
},
// 配置别名
chainWebpack: (config) => {
config.resolve.alias
.set('styles', path.join(__dirname, './src/assets/styles/'))
.set('@', path.join(__dirname, './src/'))
.set('common', path.join(__dirname, './src/common/'))
}
}
- 其他代码基本复制粘贴即可
代码地址
其他
name 名字作用
- 对该页面取消缓存的时候。
- 使用递归组件的时候。
- vue的开发调试工具 vue devtools中各组件名字
- 路由就是根据网址的不同,返回不同的内容给用户
页面切换问题
- 始终回到页面最顶部
routes: [...],
scrollBehavior (to, from, savedPosition) {
return { x: 0, y: 0 }
}
- 当设置文字超出隐藏不生效时,可在其父级添加属性
min-width: 0
项目代码结构
vue/cli 3.0 及以上版本
vue/cli 3.0以下版本
老版本结构描述
- README.md:项目说明文件...第三方模块依赖。
- package:里面有很多依赖包,主要是开发项目时候有第三方模块依赖,都放在这里。
- package-lock.json:是package的一个锁文件,帮我们确定安装的第三方包具体的版本,保持团队编程的统一。
- .gitignore:当我们使用git时,希望把代码传到线上去,但是有一些特殊的代码并不希望传到线上去,当往线上git仓库提交时,它将不会被提交到git代码仓库。
- src:放置的整个项目的源代码。
- main.js:整个项目的入口文件。
- App.vue:项目的最原始根组件。
- router:index.js项目的路由。
- components:项目里用的小组件。
- assets:项目里用的图片类资源。
- node_modules:放置的项目依赖的第三方包。
- config:放置的项目配置文件。
- index.js:放置基础信息。
- dev.env:放置开发环境配置信息。
- prod.env:放置线上环境配置信息。
- index:项目默认的一个首页模板文件。
- .postcssrc:是对postcss的一个配置项。
- .eslintrc:代码规范,可以按照规范检测代码。
- .eslintignore:提示以上格式代码不会被检测/build/,/config/,/dist/,/*.js
- .editorconfig:帮助我们配置了编辑器里面的语法,也可以自己加一些配置,来统一编辑器自动化代码的格式化。
- .babelrc:语法解析器,对语法转换后最终转换成浏览器能够编译执行的代码。
- static:放置的静态资源,如静态图片,json数据等。
- build:放置的项目打包的内容webpack配置。
- LICENSE:是一个开源协议的说明