项目准备
- 在码云新建仓库travel
- 克隆到本地
- 在本地仓库所在目录执行
vue init webpack travel
选择y
表示继续
样式重置
index.html
reset.css
- 在
src/assets/styles
目录下存放reset.css
@charset "utf-8";
html {
background-color: #fff;
color: #000;
font-size: 12px;
}
body,
ul,
ol,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
figure,
form,
fieldset,
legend,
input,
textarea,
button,
p,
blockquote,
th,
td,
pre,
xmp {
margin: 0;
padding: 0;
}
body,
input,
textarea,
button,
select,
pre,
xmp,
tt,
code,
kbd,
samp {
line-height: 1.5;
font-family: tahoma, arial, 'Hiragino Sans GB', simsun, sans-serif;
}
h1,
h2,
h3,
h4,
h5,
h6,
small,
big,
input,
textarea,
button,
select {
font-size: 100%;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: tahoma, arial, 'Hiragino Sans GB', '微软雅黑', simsun, sans-serif;
}
h1,
h2,
h3,
h4,
h5,
h6,
b,
strong {
font-weight: normal;
}
address,
cite,
dfn,
em,
i,
optgroup,
var {
font-style: normal;
}
table {
border-collapse: collapse;
border-spacing: 0;
text-align: left;
}
caption,
th {
text-align: inherit;
}
ul,
ol,
menu {
list-style: none;
}
fieldset,
img {
border: 0;
}
img,
object,
input,
textarea,
button,
select {
vertical-align: middle;
}
article,
aside,
footer,
header,
section,
nav,
figure,
figcaption,
hgroup,
details,
menu {
display: block;
}
audio,
canvas,
video {
display: inline-block;
*display: inline;
*zoom: 1;
}
blockquote:before,
blockquote:after,
q:before,
q:after {
content: '\0020';
}
textarea {
overflow: auto;
resize: vertical;
}
input,
textarea,
button,
select,
a {
outline: 0 none;
border: none;
}
button::-moz-focus-inner,
input::-moz-focus-inner {
padding: 0;
border: 0;
}
mark {
background-color: transparent;
}
a,
ins,
s,
u,
del {
text-decoration: none;
}
sup,
sub {
vertical-align: baseline;
}
html {
overflow-x: hidden;
height: 100%;
font-size: 50px;
-webkit-tap-highlight-color: transparent;
}
body {
font-family: Arial, 'Microsoft Yahei', 'Helvetica Neue', Helvetica, sans-serif;
color: #333;
font-size: 0.28em;
line-height: 1;
-webkit-text-size-adjust: none;
}
hr {
height: 0.02rem;
margin: 0.1rem 0;
border: medium none;
border-top: 0.02rem solid #cacaca;
}
a {
color: #25a4bb;
text-decoration: none;
}
- 在mian.js文件引入
import './assets/styles/reset.css'
解决移动端1像素边框问题
- 在
src/assets/styles
目录下存放border.css - 在mian.js文件引入
import './assets/styles/border.css'
解决移动端300ms点击延迟
安装fastclick
npm install fastclick --save
main.js
import fastClick from 'fastclick'
fastClick.attach(document.body)
字体图标
在iconfont新建项目
在项目中使用sass
npm install sass-loader node-sass --save-dev
注意sass-loader版本过高可能会报错
页面组件化
将一个页面拆分成多个组件
src/pages/home
目录下新建components
目录,然后新建Header.vue
引入
src/pages/home/home.vue
Vue自动完成HomeHeader和小写的
页面元素高度问题
由于移动端一般使用双倍像素,如果指定元素高度10px,实际显示为20px,所以实际指定高度应为设计图纸中的一半。在实际开发中,一般使用rem作为单位,我们可以指定html的font-size为50px,如果设计图上某个元素高度65px,转化为rem值为0.65rem(css中元素高度应为设计图上该元素高度的一半)
引入字体图标
在styles目录新建iconfont
,把从Iconfont下载的字体图标放进去
iconfont.css
在styles目录下,需要修改字体路径,如
src: url('./iconfont/iconfont.eot?t=1584759696965');
main.js
中
import './assets/styles/iconfont.css'
使用图标
使用scss变量
在src/assets/styles
目录下新建_variables.scss
,存放变量
// demo
$bgColor: #00bcd4;
在项目中引入
src/pages/home/components/Header.vue
注意@
表示src目录,前面的~
必须加上才不会报错
给路径添加别名
给路径添加别名好处是减少路径长度
build/webpack.base.conf.js
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
}
},
alias用于添加别名,如果要让styles指向assets/styles,只需添加
styles: resolve('src/assets/styles')
main.js导入样式文件可以写成
main.js
import 'styles/reset.css'
import 'styles/border.css'
import 'styles/iconfont.css'
//import './assets/styles/iconfont.css'
组件中导入scss文件写成
src/pages/home/components/Header.vue
@import '~styles/_variables';
重启服务器后生效
新建项目分支
在实际项目开发过程中,每开发一个新功能都会创建一个新的分支,功能开发完成之后再合并到主分支
//提交主分支
git add .
git commit -m 'header finished'
git push
//创建新分支
git checkout -b index-swiper
使用轮播插件
npm install swiper vue-awesome-swiper --save
main.js
import VueAwesomeSwiper from 'vue-awesome-swiper'
// import style
import 'swiper/css/swiper.css'
Vue.use(VueAwesomeSwiper, /* { default options with global component } */)
解决自动轮播不生效问题
swiperOptions: {
observer: true, //修改swiper自己或子元素时,自动初始化swiper
observeParents: true, //修改swiper的父元素时,自动初始化swiper
loop: true,
autoplay: {
delay: 3000
},
pagination: {
el: '.pagination-home'
}
// Some Swiper option/callback...
},
解决页面抖动问题
加载页面时,图片在文字之后加载,会造成文字刚开始占用图片的位置,之后又被图片挤开。
解决办法:图片外面加一层div
...
.wrapper {
overflow: hidden;
width: 100%;
height: 0;
//padding 百分比相对于父元素的宽度,这里是屏幕宽度,31.25%是图片实际高度/图片宽度
padding-bottom: 31.25%;
.swiper-img {
width: 100%;
}
}
上传分支代码
git add .
git commit -m 'swiper finished'
//第一次上传分支
git push -u origin index-swiper
//git push
合并到主分支
//切换到主分支
git checkout master
//把线上分支合并到本地分支
git merge origin/index-swiper
//提交master分支
git push
文字超出部分显示省略号
assets/styles
文件夹下新建_mixin.scss
,该文件主要写重复使用的样式
@mixin ellipsis {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
使用
@import '~styles/mixin';
.icon-desc {
@include ellipsis;
}
注意:1. 文件名以下划线开头,但import时不带下划线。
- 如果是flex布局,且元素
flex:1
,加上min-width: 0;
确保该子元素不超过外层容器
.item-info {
flex: 1;
padding: 0.1rem;
min-width: 0;
}
发送ajax请求
npm install axios --save
一个页面可能有多个组件组成,如果每个组件都需要获取服务器的数据,这样效率比较低下,推荐的做法是在父组件发送请求。
Home.vue
import axios from 'axios'
created() {
this.getHomeInfo()
},
methods: {
//模拟请求数据
getHomeInfo() {
axios.get('/api/index.json').then(this.getHomeInfoSucc)
},
getHomeInfoSucc(res) {
console.log(res)
}
}
mock
在没有后端支持情况下,需要axios模拟请求数据,这就要用到mock
在static文件夹下新建mock目录,所有请求的数据放在该文件夹下。
static/mock/index.json
{
"status": "ok"
}
修改Home.vue
getHomeInfo() {
axios.get('/static/mock/index.json').then(this.getHomeInfoSucc)
},
static目录下的文件可以在浏览器通过路径直接访问,一般我们不希望其发布到线上,可以为其添加gitignore
.gitignore
static/mock
配置转发
需求:经过上面的步骤,我们已经可以通过获得数据,但也引入另一个问题,即服务器端api地址和模拟请求的地址不一致,项目上线时需要考虑api地址变更,webpack提供了解决这个问题的办法。
config/index.js
dev: {
// Paths
assetsSubDirectory: 'static',
assetsPublicPath: '/',
//修改部分
proxyTable: {
'/api': {
target: 'http://localhost:8080',
pathRewrite: {
// 请求以/api开头的转发到/static/mock
'^/api': '/static/mock'
}
}
},
...
}
proxyTable
下配置路由转发
现在已经可以通过/api/
访问接口了
getHomeInfo() {
axios.get('/api/index.json').then(this.getHomeInfoSucc)
}
解决组件渲染没有数据问题
swiper
创建时数据还没有生成,这个时候需要加上v-if="swiperList.length"
表示获得数据之后才会渲染这个组件
改进:虽然这样可以解决问题,但我们建议模板中尽可能减少逻辑代码的出现。使用computed来解决这个问题
computed: {
showSwiper() {
return this.swiperList.length
}
}
修改tempate
...
优化滑动
npm install better-scroll --save
使用该组件需要dom结构满足一定条件
import BScroll from 'better-scroll'
mounted() {
this.scroll = new BScroll(this.$refs.wrapper)
}
实现父子组件联动
Alphabet.vue
(子组件)
-
{{ item }}
-
@click="handleLetterClick"
监听点击了哪个letter,通知父组件letter改变,父组件再通知city-list组件letter改变(借助父组件实现兄弟组件通信) - touchstart、touchmove、touchend监听触摸事件
updated() {
// update在获取数据后被执行
// 获取'A'元素距离其父元素上边缘的距离
this.startY = this.$refs['A'][0].offsetTop
},
methods: {
handleLetterClick(e) {
this.$emit('change', e.target.innerText)
},
handleTouchStart() {
this.touchStatus = true
},
handleTouchMove(e) {
if (this.touchStatus) {
// e.touches[0].clientY表示鼠标指针距屏幕顶部的距离
// 79为导航栏的高度
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])
}
}
},
handleTouchEnd() {
this.touchStatus = false
}
}
City.vue
(父组件)
handleLetterChange(letter) {
this.letter = letter
}
List.vue
(子组件)
watch: {
letter(newVal, oldVal) {
if (newVal) {
const element = this.$refs[newVal][0]
this.scroll.scrollToElement(element)
}
}
}
监听父组件传过来的letter的变化,滚动到屏幕相应位置
优化:手指移动的速度很快,可以通过设置定时函数延迟响应滑动事件
搜索
Search.vue
- {{ item.name }}
- 没有找到匹配数据
computed: {
//判断是否有数据
hasNoData() {
return !this.list.length
}
},
watch: {
keyword(newVal, oldVal) {
if (this.timer) {
clearTimeout(this.timer)
}
// 关键字为空,关闭搜索结果页
if (!newVal) {
this.list = []
return
}
// 监听关键字变化,显示查询
const result = []
for (let i in this.cities) {
this.cities[i].forEach(value => {
if (value.spell.indexOf(newVal) > -1 || value.name.indexOf(newVal) > -1) {
result.push(value)
}
})
}
this.list = result
// this.timer = setTimeout(() => {
// const result = []
// for (let i in this.cities) {
// this.cities[i].forEach(value => {
// if (value.spell.indexOf(newVal) > -1 || value.name.indexOf(newVal) > -1) {
// result.push(value)
// }
// })
// }
// this.list = result
// }, 100)
}
},
没有共同父组件的组件通信Vuex
npm install vuex --save
main.js
//store
//import会自动寻找目录下的index.js
import store from './store'
new Vue({
el: '#app',
router,
store,
components: { App },
template: ' '
})
src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
city: '北京'
},
// actions调用mutation改变数据
actions: {
changeCity(ctx, city) {
// 调用mutations中changeCity的方法
ctx.commit('changeCity', city)
}
},
mutations: {
changeCity(state, city) {
state.city = city
}
}
})
获取store中的数据
{{ this.$store.state.city }}
触发改变store中的数据的函数