项目准备
- 在码云新建仓库travel
- 克隆到本地
- 在本地仓库所在目录执行
vue init webpack travel
选择y
表示继续
样式重置
index.html
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" />
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
<template>
<div><home-header></home-header></div>
</template>
<script>
import HomeHeader from './components/Header'
export default {
name: 'Home',
components: {
//es6中键值相同可以省略值
HomeHeader
}
}
</script>
<style></style>
Vue自动完成HomeHeader和小写的<home-header>的关联
页面元素高度问题
由于移动端一般使用双倍像素,如果指定元素高度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'
使用图标
<span class="iconfont iconfanhui"></span>
使用scss变量
在src/assets/styles
目录下新建_variables.scss
,存放变量
// demo
$bgColor: #00bcd4;
在项目中引入
src/pages/home/components/Header.vue
<style lang="scss" scoped>
@import '~@/assets/styles/_variables';
...
</style>
注意@
表示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
<div class="wrapper">
...
</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 :options="swiperOptions" v-if="swiperList.length">
<swiper-slide v-for="item of swiperList" :key="item.id"><img class="swiper-img" :src="item.imgUrl" alt="" /> </swiper-slide>
<div class="swiper-pagination" slot="pagination"></div>
</swiper>
swiper
创建时数据还没有生成,这个时候需要加上v-if="swiperList.length"
表示获得数据之后才会渲染这个组件
改进:虽然这样可以解决问题,但我们建议模板中尽可能减少逻辑代码的出现。使用computed来解决这个问题
computed: {
showSwiper() {
return this.swiperList.length
}
}
修改tempate
<swiper :options="swiperOptions" v-if="showSwiper">
...
</swiper>
优化滑动
npm install better-scroll --save
使用该组件需要dom结构满足一定条件
<div class="list" ref="wrapper">
<div>
<div></div>
<div></div>
<div></div>
</div>
</div>
import BScroll from 'better-scroll'
mounted() {
this.scroll = new BScroll(this.$refs.wrapper)
}
实现父子组件联动
Alphabet.vue
(子组件)
<template>
<ul class="list">
<li
class="item"
v-for="item of letters"
:key="item"
@click="handleLetterClick"
@touchstart.stop.prevent="handleTouchStart"
@touchmove.stop.prevent="handleTouchMove"
@touchend.stop.prevent="handleTouchEnd"
:ref="item"
>
{{ item }}
</li>
</ul>
</template>
-
@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
(父组件)
<city-list :cities="cities" :hotCities="hotCities" :letter="letter"></city-list>
<city-alphabet :cities="cities" @change="handleLetterChange"></city-alphabet>
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
<template>
<div>
<div class="search">
<input v-model="keyword" class="search-input" type="text" placeholder="输入城市名或拼音" />
</div>
<!-- keyword有值时显示 -->
<div class="search-content" ref="search" v-show="keyword">
<ul>
<li class="serach-item" v-for="(item, index) of list" :key="index">{{ item.name }}</li>
<!-- 没有查询结果时显示 -->
<li class="serach-item" v-show="hasNoData">没有找到匹配数据</li>
</ul>
</div>
</div>
</template>
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: '<App/>'
})
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中的数据
<div class="header-right">
{{ this.$store.state.city }}
<span class="iconfont iconjiantouxia arrow-icon"></span>
</div>
触发改变store中的数据的函数
<div class="button-wrapper" v-for="item of hotCities" :key="item.id" @click="handleCityClick(item.name)">
handleCityClick(city) {
// 派发changeCity的Action
this.$store.dispatch('changeCity', city)
// 不经过action的写法
this.$store.commit('changeCity', city)
}
页面跳转
- 链接式
<router-link to="/"></router-link>
- 编程式
this.$router.push('/')
router-link
<router-link :to="'/detail/' + item.id" tag="li" class="item" v-for="item of recommendList" :key="'recommend' + item.id">
...
</router-link>
<router-link>标签默认情况下渲染为一个a标签,可以通过增加tag属性指定希望渲染成的标签,如tag="li"
会最终渲染为li
标签
路由传递参数
router/index.js
export default new Router({
routes: [
...
{
path: '/detail/:id',
name: 'Detail',
component: Detail
}
]
})
Recommend.vue
<router-link :to="'/detail/' + item.id" tag="li" class="item" v-for="item of recommendList" :key="'recommend' + item.id">
...
</router-link>
全局组件
在src目录下新建common目录,用于存放全局公用组件
修改webpack.base.conf.js
增加common
的别名
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
vue$: 'vue/dist/vue.esm.js',
'@': resolve('src'),
styles: resolve('src/assets/styles'),
common: resolve('src/common')
}
},
样式覆盖
遇到组件不能覆盖另一组件的样式,就需要用样式穿透,在scss中可以用>>>
或者/deep/
.container >>> .swiper-container {
overflow: inherit;
}
props
props里的属性的默认值可以是函数
props: {
imgs: {
type: Array,
default() {
return ['a','b']
}
}
}
解决swiper插件显示问题
如图,swiper不能显示正确的下标,是因为渲染这个组件后其父元素是隐藏的,当它显示时就会出现这个问题,解决办法是在让swiper监听父元素变化
data() {
return {
swiperOptions: {
pagination: {
el: '.swiper-pagination',
type: 'fraction'
},
// 监听父元素变化
observeParents: true,
observer: true,
loop: true
// Some Swiper option/callback...
}
}
}
Vue点击事件
我们要实现点击黑色区域返回首页,而点击图片不做任何响应
<div class="container" @click.self.prevent="handleGalleryClick">
...
</div>
@click.self.prevent
表示只监听元素自身的点击事件,@click.prevent.self
刚好相反,点击自身无效,而点击其子元素会被监听到。
监听屏幕滚动事件
methods: {
handleScroll() {
// 滚动的距离
const top = document.documentElement.scrollTop
// 距离顶部60开始隐藏
if (top > 60) {
let opacity = top / 140
opacity = opacity > 1 ? 1 : opacity
this.showAbs = false
this.opacityStyle = {
opacity
}
} else {
this.showAbs = true
}
}
},
mounted() {
window.addEventListener('scroll', this.handleScroll)
}
监听屏幕事件必须在mounted()
方法中写window.addEventListener('scroll', this.handleScroll)
表示监听屏幕滚动,并指定处理函数
对全局事件的解绑(重要)
在我们编写代码的过程中,如果没有对事件进行及时的解绑,可能影响程序性能或造成错误。如下所示,我们在一个组件监听window对象,当这个组件销毁后,这个监听仍在继续。这显然是不合理的。
mounted() {
window.addEventListener('scroll', this.handleScroll)
}
改进:在组件销毁前解绑事件
mounted() {
window.addEventListener('scroll', this.handleScroll)
},
beforeDestroy() {
window.removeEventListener('scroll', this.handleScroll)
}
使用递归组件
在组件内部调用组件自身
使用递归组件必须满足一个条件,即组件需定义好了name属性,根据name可以调用自身
<template>
<div>
<div class="item" v-for="(item, index) of list" :key="index">
<div class="item-title">
<span class="item-title-icon"></span>
{{ item.title }}
</div>
<div v-if="item.children">
<detail-list :list="item.children"></detail-list>
</div>
</div>
</div>
</template>
export default {
name: 'DetailList',
props: {
list: {
type: Array
}
}
}
组件缓存
如果我们不希望某些页面被缓存下来,而是每次进入都重新获取数据
<template>
<div id="app">
<keep-alive exclude="Detail">
<router-view />
</keep-alive>
</div>
</template>
exclude="Detail"
将name=Detail
的组件排除在缓存之外
小结:组件name的三个用途
- 用于递归组件
- 用于Vue调试工具
- 用于keep-alive排除组件缓存
解决打开页面时页面停留在底部
router/index.js
scrollBehavior
函数会在每次切换页面时执行
export default new Router({
routes: [
{
path: '/',
name: 'Home',
component: Home
},
...
],
// 当进行页面切换时,x、y轴初始是0
scrollBehavior(to, form, savedPosition) {
return { x: 0, y: 0 }
}
})
添加动画
在common
目录下新建fade目录
common/fade/Fade.vue
<template>
<transition>
<slot></slot>
</transition>
</template>
<script>
export default {
name: 'Fade'
}
</script>
<style lang="scss" scoped>
.v-enter,
.v-leave-to {
opacity: 0;
}
.v-enter-active,
.v-leave-active {
transition: opacity 0.5s;
}
</style>
<transition>
包裹需要动画效果的元素,Vue会自动添加一些类名
使用
<fade-animation>
<common-gallery :imgs="bannerImgs" v-show="showGallery" @close="handleGalleryClose"></common-gallery>
</fade-animation>
Vue项目的接口联调
config/index.js
dev: {
// Paths
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {
'/api': {
// target: 'http://localhost:8080',
target: 'http://localhost',
// pathRewrite: {
// 请求以/api开头的转发到/static/mock
// '^/api': '/static/mock'
// }
}
},
...
}
'/api':
开头的请求会被转发到本地80端口,localhost
在上线阶段应更改为服务器地址或域名
Vue项目的测试
ifconfig
inet 192.168.1.103
该地址是本机在内网的ip地址
假设我们的Vue项目运行在8080端口,当访问192.168.1.103:8080
时,会访问失败,因为webpack屏蔽了通过ip地址访问,如果要解除这个限制,修改package.json
文件,加上--host 0.0.0.0
"scripts": {
"dev": "webpack-dev-server --host 0.0.0.0 --inline --progress --config build/webpack.dev.conf.js",
"start": "npm run dev",
"lint": "eslint --ext .js,.vue src",
"build": "node build/build.js"
},
这个时候,如果手机和电脑在同一局域网下,就可以通过192.168.1.103:8080
访问页面了
解决拖动字母屏幕跟着滑动
Alphabet.vue
@touchstart.prevent="handleTouchStart"
.prevent
可以解决这个问题
解决浏览器不支持promise
npm install babel-polyfill --save
main.js
import 'babel-polyfill'
Vue项目的打包上线
- 运行打包命令
npm run build
在项目根目录会自动生成dist文件夹,该文件夹的内容是项目最终上线放到服务器的内容
如果不想生成map文件,修改config/index.js
productionSourceMap: false,
重新打包即可
2 . 将dist文件夹下内容上传至服务器
将dist文件夹下的内容放到服务器网站目录根路径下
-
api
是后端代码 -
static
、index.html
是打包后dist文件夹的内容
这个时候,网站可以通过localhost
(本地配置了php或nginx)或服务器ip
访问
通过ip加访问路径访问
我们希望通过ip地址/project
访问服务器,需要做如下修改
- 修改webpack配置
assetsPublicPath: '/project',
config/index.js
build: {
// Template for index.html
index: path.resolve(__dirname, '../dist/index.html'),
// Paths
assetsRoot: path.resolve(__dirname, '../dist'),
assetsSubDirectory: 'static',
// assetsPublicPath: '/',
assetsPublicPath: '/project',
...
}
- 网站根目录下新建
project
目录 - 重新打包,把dist文件夹下的文件放到刚才新建的
project
目录
异步组件
打包生成的app.js
包含所有页面的业务逻辑代码,默认情况下它会在浏览器第一次请求页面时全部加载,显然这样不是很合理,我们希望访问某个页面时,只加载这个页面的逻辑
router/index.js
export default new Router({
routes: [
{
path: '/',
name: 'Home',
component: () => import('@/pages/home/Home')
},
{
path: '/city',
name: 'City',
component: () => import('@/pages/city/City')
},
{
path: '/detail/:id',
name: 'Detail',
component: () => import('@/pages/detail/Detail')
}
],
// 当进行页面切换时,x、y轴初始是0
scrollBehavior(to, form, savedPosition) {
return { x: 0, y: 0 }
}
})
- 组件中异步加载其他组件
components: {
HomeHeader: () => import('./components/Header'),
...
},
component: () => import('@/pages/home/Home')
解决了按需加载问题
使用异步组件需要考虑的问题:
- 异步组件可以减少首次加载时间,但在app.js文件本身较小时效果不明显
- 异步组件缺点是会增加网络请求
考虑以上因素,在app.js文件较小时不使用异步组件