vue2学习笔记2

vue项目预热

Vue CLI 的使用

安装 nodejs
https://cli.vuejs.org/zh/guide/creating-a-project.html

1
2
3
4
5
6
7
8
//安装 cli
npm install -g @vue/cli
yarn global add @vue/cli

//拉取 2.x 模板 (旧版本)
npm install -g @vue/cli-init
# `vue init` 的运行效果将会跟 `vue-cli@2.x` 相同
vue init webpack my-project

1、使用图形化界面
使用 vue ui 命令进行图形界面化创建项目
按照界面提示创建

2、Vue CLI 2.x 模板 (旧版本)
vue init webpack travel
project name travel
……
runtime + compiler 选择第一个
install vue-router Y
esLint
npm 包管理
sudo npm install –verbose 查看详细安装信息

3、新版Vue Cli
vue create –help
vue create hello-world

4、如果 vue init webpack travel卡主
安装的最后一步npm 和 yarn 都不选择,选择手动安装
然后 cd travel,
最后 npm install –registry=https://registry.npm.taobao.org

npm set registry https://registry.npm.taobao.org
npm set registry https://registry.npmjs.org/

npm ERR! code CERT_HAS_EXPIRED:解决证书过期问题
清除npm缓存
npm cache clean –force
取消ssl验证:
npm config set strict-ssl false

5、npm run dev 运行项目
npm run serve

6、浏览器vue调试插件
vue.js devtools

vue项目结构

config 各种配置

  • dev.env.js
  • index.js
  • prod.env.js

index.html 生成的首页面

package.json 项目依赖,基础配置

src 目录

  • main.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import Vue from 'vue'
    import App from './App'
    import router from './router'

    Vue.config.productionTip = false

    /* vue根实例 */
    new Vue({
    el: '#app',
    router, //路由 { 键值对一个名字,只写一个 }
    components: { App }, // { 键值对一个名字,只写一个 }
    template: '<App/>' // app 根组件
    })
  • App.vue 根组件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <template>
    <div id="app">
    <img src="./assets/logo.png">
    <router-view/> //显示路由组件,显示的是当前路由所对应的地址,/
    </div>
    </template>

    <script>
    export default {
    name: 'App'
    }
    </script>

    <style>
    #app {

    }
    </style>
  • router文件夹 路由管理

    • index.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import Vue from 'vue'
    import Router from 'vue-router'
    import HelloWorld from '@/components/HelloWorld'

    Vue.use(Router)

    export default new Router({
    routes: [
    {
    path: '/',
    name: 'HelloWorld',
    component: HelloWorld
    }
    ]
    })
  • components 自定义组件目录

    HelloWorld.vue

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <template>

    </template>

    <script>
    //导出组件
    export default {
    name: 'HelloWorld',
    data () {
    return {
    msg: 'Welcome to Your Vue.js App'
    }
    }
    }
    </script>

    /*样式只对这个组件有用*/
    <style scoped>
    </style>
  • assets

static

项目开发总结

1、路由配置放在 src/router/index.js
路由根路径也就是下面的 HelloWorld
显示的是当前路由所对应的地址

入口文件 main.js 文件中 vue实例中
router, //路由 { 键值对一个名字,只写一个 }
router文件夹中的 index.js 文件

1
2
3
4
5
6
7
8
9
export default new Router({
routes: [
{
path: '/',
name: 'HelloWorld',
component: HelloWorld
}
]
})

2、路由使用
src目录下创建页面
pages/home/Home.vue
pages/list/List.vue
router文件夹中修改 index.js

路由配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/pages/home/Home'
import List from '@/pages/list/List'
// @ 表示src 目录

Vue.use(Router)

export default new Router({
routes: [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/list',
name: 'List',
component: List
}
]
})

vue单页应用与多页应用

项目初始化

  1. 在assets 文件件下创建 styles文件夹
    将reset.css,border.css基础样式拷贝进去。
    main.js 中引入
    import ‘./assets/styles/reset.css’
    import ‘./assets/styles/border.css’

  2. 在index.html中加入如下,适配移动端

1
2
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,
maximum-scale=1.0,user-scalable=no">
  1. 安装依赖库
    npm set registry https://registry.npm.taobao.org
    npm install fastclik –save
    npm install stylus@0.54.8 –save
    npm install stylus-loader –save
    npm install stylus-loader@3.0.2 –save

也可以修改 package.json
然后 npm install

  1. iconfont 图标使用
    将图标加入购物车
    ttf 与 css 文件放入
    /assets/iconfont/

  2. 页面文件夹中创建 components目录 存放组件
    如:src/pages/home/components/Header.vue
    在 Home.vue 文件中使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template lang="">
<div>
<HomeHeader></HomeHeader>
</div>
</template>

<script>
import HomeHeader from './components/Header'
export default {
name:'Home',
components: {
HomeHeader
}
}
</script>

<style lang="stylus" scoped>

</style>

  1. UI布局, stylus 语法
    先写骨架,再写样式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
       <template>
    <div class="header">
    <div class="header-left">
    <div class="iconfont back-icon">&#xe601;</div>
    </div>
    <div class="header-input">
    <span class="iconfont">&#xe632;</span>
    输入城市/景点/游玩主题
    </div>
    <router-link to="/city">
    <div class="header-right">
    <!-- 从store中取共用数据 -->
    {{this.$store.state.city}}
    <span class="iconfont arrow-icon">&#xe600;</span>
    </div>
    </router-link>
    </div>
    </template>

    <script>
    export default {
    name: 'HomeHeader',
    props: {
    // city: String
    }
    }
    </script>
    <!-- https://www.stylus-lang.cn/docs/css-style.html 语法 -->
    <style lang="stylus" scoped>
    @import '~styles/theme.styl' /*引入全局样式*/
    .header
    display: flex
    line-height: .86rem
    background: $bgColor
    color: #fff
    .header-left
    width: .64rem
    float: left
    .back-icon
    text-align: center
    font-size: .4rem
    .header-input
    flex: 1
    background: #fff
    border-radius: .1rem
    height: .64rem
    line-height: .64rem
    margin-top: .12rem
    margin-left: .2rem
    padding-left: .2rem
    color: #ccc
    .header-right
    min-width: 1.04rem
    padding: 0 .1rem
    float: right
    text-align: center
    color: #fff
    .arrow-icon
    font-size: .24rem
    </style>
  • 配置文件路径
    @import ‘~styles/theme.styl’
    build/webpack.base.conf.js文件中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    resolve: {
    extensions: ['.js', '.vue', '.json'],
    alias: {
    'vue$': 'vue/dist/vue.esm.js',
    '@': resolve('src'),
    'styles': resolve('src/assets/styles'),
    'common': resolve('src/common'),
    }
    },
  1. 使用外部ui库
    比如:
    https://github.com/surmon-china/vue-awesome-swiper
    npm install vue-awesome-swiper@2.6.7 –save
    main.js 文件中引入 全局引入
    import VueAwesomeSwiper from ‘vue-awesome-swiper’
    Vue.use(VueAwesomeSwiper)

Swipe.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
  <template>
<div class="wrapper">
<swiper :options="swiperOption" v-if="showSwiper">
<swiper-slide v-for="item of list" :key="item.id">
<img class="swiper-img" :src="item.imgUrl">
</swiper-slide>
<div class="swiper-pagination" slot="pagination"></div>
</swiper>
</div>
</template>

<script>
export default {
name: 'HomeSwiper',
props: {
list: []
},
data () {
return {
swiperOption: {
autoplay: 3500,
pagination: '.swiper-pagination',
paginationClickable: true,
loop: true
}
}
},
computed: {
showSwiper () {
return this.list.length
}
}
}
</script>

<style lang="stylus" scoped>
//>>>穿透样式
.wrapper >>> .swiper-pagination-bullet-active
background: #fff
.wrapper
overflow: hidden
width: 100%
height: 0
padding-bottom: 55% // padding-bottom: 57.4% 高比宽
background: #ccc
.swiper-img
width: 100%
.swiper-pagination
position: absolute
z-index: 20
bottom: 20%
width: 100%
text-align: center
</style>

Header.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
  <template>
<div class="header">
<div class="header-left">
<div class="iconfont back-icon">&#xe601;</div>
</div>
<div class="header-input">
<span class="iconfont">&#xe632;</span>
输入城市/景点/游玩主题
</div>
<router-link to="/city">
<div class="header-right">
<!-- 从store中取共用数据 -->
{{this.$store.state.city}}
<span class="iconfont arrow-icon">&#xe600;</span>
</div>
</router-link>
</div>
</template>

<script>
export default {
name: 'HomeHeader',
props: {
// city: String
}
}
</script>
<!-- https://www.stylus-lang.cn/docs/css-style.html 语法 -->
<style lang="stylus" scoped>
@import '~styles/theme.styl'
.header
display: flex
line-height: .86rem
background: $bgColor
color: #fff
.header-left
width: .64rem
float: left
.back-icon
text-align: center
font-size: .4rem
.header-input
flex: 1
background: #fff
border-radius: .1rem
height: .64rem
line-height: .64rem
margin-top: .12rem
margin-left: .2rem
padding-left: .2rem
color: #ccc
.header-right
min-width: 1.04rem
padding: 0 .1rem
float: right
text-align: center
color: #fff
.arrow-icon
font-size: .24rem
</style>

Home.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
  <template>
<div>
<home-header></home-header>
<home-swiper :list="swiperList"></home-swiper>
<home-icons :list="iconList"></home-icons>
<home-recommend :list="recommendList"></home-recommend>
<home-weekend :list="weekendList"></home-weekend>
</div>
</template>

<script>
import HomeHeader from './components/Header.vue'
import HomeSwiper from './components/Swiper.vue'
import HomeIcons from './components/Icons.vue'
import HomeRecommend from './components/Recommend.vue'
import HomeWeekend from './components/Weekend.vue'
import axios from 'axios'
import { mapState } from 'vuex'

// 在父组件中请求数据分发到每一个小组件里面
export default {
name: 'Home',
components: {
HomeHeader,
HomeSwiper,
HomeIcons,
HomeRecommend,
HomeWeekend
},
data () {
return {
lastCity: '',
swiperList: [],
iconList: [],
recommendList: [],
weekendList: []
}
},
methods: {
getHomeInfo () {
axios.get('/api/index.json')
.then(this.getHomeInfoSucc)
},
getHomeInfoSucc (res) {
res = res.data
if (res.ret && res.data) {
const data = res.data
// this.city = data.city
this.swiperList = data.swiperList
this.iconList = data.iconList
this.recommendList = data.recommendList
this.weekendList = data.weekendList
// console.log(this.city)
}
}
},
mounted () {
this.getHomeInfo()
},
computed: {
// ...展开运算符 取本地存储
...mapState(['city'])
},
// 如果使用了keep-alive,则页面显示的时候会执行activated方法
activated () {
if (this.lastCity !== this.city) {
// 当城市数据发生变化时,重新请求数据
this.lastCity = this.city
this.getHomeInfo()
}
}
}
</script>

<style>
.home {
font-size: 50px;
}
</style>

Icons.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
<template>
<div class="icons">
<swiper :options="swiperOption2">
<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-imgcontent" :src="item.imgUrl"/>
</div>
<p class="icon-desc">{{ item.desc }}</p>
</div>
</swiper-slide>
</swiper>
</div>
</template>

<script>

function groupList (list, itemsPerPage) {
var groupedLists = []
var currentPage = []
for (var i = 0; i < list.length; i++) {
currentPage.push(list[i])

if (currentPage.length === itemsPerPage || i === list.length - 1) {
groupedLists.push(currentPage)
currentPage = []
}
}
return groupedLists
}

export default {
name: 'HomeIcons',
props: {
list: Array
},
data () {
return {
swiperOption2: {
loop: false
}
}
},
computed: {
// 将iconlist 按每页8个元素进行分组
pages () {
var pages = groupList(this.list, 8)
return pages
}
}
}
</script>

<style lang="stylus" scoped>
@import '~styles/mixins.styl';
//>>>:这是Stylus中的一个深度选择器,用于选择当前元素下的所有后代元素。
.icons >>> .swiper-container
//overflow hidden //隐藏溢出:当子元素的尺寸超出容器的尺寸时,超出的部分将被隐藏,并不会在页面上显示
height 0
padding-bottom: 50% //高宽比 1/2 底部填充为50%
.icon
position relative //相对于自己在正常文档流中的位置进行微调,并不会影响其他元素的布局
overflow hidden
float left //元素在容器内的水平对齐方式
width: 25%
height: 0
padding-bottom: 25% //底部填充为25%
.icon-img
position: absolute //相当于根布局的位置 相对于其最近的已定位(非static)祖先元素进行定位
top: 0 //将该元素的上边缘与其父元素的上边缘对齐
left: 0
right: 0
bottom: .44rem //该元素的下边缘与其父元素的底部边缘保持一定的距离
box-sizing: border-box //元素的盒模型为"border-box"模式,这意味着元素的尺寸包括了内边距和边框
padding: .1rem
.icon-imgcontent
height 100%
display: block //元素显示为块级元素。块级元素会独占一行,并且可以设置宽度、高度以及外边距等属性
margin: 0 auto //左右外边距设置为自动,即水平居中对齐。这样设置后,该元素的左右外边距会自动调整,使其在父元素中水平居中显示
.icon-desc
position: absolute //相当于根布局的位置 相对于其最近的已定位(非static)祖先元素进行定位
left: 0
right: 0
bottom: 0
height: .44rem
line-height: .44rem //行高是指元素中文本行的高度,用于控制行间距和垂直居中等效果
text-align: center
color: #333
ellipsis() //文字超出显示三个点
</style>

Recommend.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<template>
<div>
<div class="title">热销推荐</div>
<ul>
<!-- 把router-link变成li标签,解决了a标签文字颜色问题 -->
<router-link
class="item border-bottom"
v-for="item of list"
:key="item.id"
tag="li"
:to="'/detail/' + item.id"
>
<img class="item-img" :src="item.imgUrl"/>
<div class="item-info">
<p class="item-title">{{ item.title }}</p>
<p class="item-desc">{{ item.desc }}</p>
<button class="item-button">查看详情</button>
</div>
</router-link>
</ul>
</div>
</template>

<script>
export default {
name: 'HomeRecommend',
props: {
list: Array
}
}
</script>

<style lang="stylus" scoped>
@import '~styles/mixins.styl';
.title
margin-top: .2rem
line-height: .8rem
background: #eee
text-indent: .2rem //文本的首行缩进
.item
overflow hidden
display: flex
height: 1.9rem
.item-img
width: 1.7rem
height: 1.7rem
padding: .1rem
.item-info
flex: 1
padding: .1rem
min-width 0
.item-title
line-height: .54rem
font-size: .32rem
.item-desc
line-height: .4rem
color: #ccc
ellipsis() //文字超出显示三个点
.item-button
line-height: .44rem
margin-top: .16rem
background: #ff9300
padding: 0 .2rem
border-radius: .06rem
color: #fff
</style>

Weekend.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<template>
<div>
<div class="title">周末去哪儿</div>
<ul>
<li class="item border-bottom" v-for="item of list" :key="item.id">
<div class="item-img-wrapper">
<img class="item-img" :src="item.imgUrl"/>
</div>
<div class="item-info">
<p class="item-title">{{ item.title }}</p>
<p class="item-desc">{{ item.desc }}</p>
</div>
</li>
</ul>
</div>
</template>

<script>
export default {
name: 'HomeWeekend',
props: {
list: Array
}
}
</script>

<style lang="stylus" scoped>
@import '~styles/mixins.styl';
.title
line-height: .8rem
background: #eee
text-indent: .2rem //文本的首行缩进
.item-img-wrapper
overflow hidden
height: 0
padding-bottom: 33.9%
.item-img
width: 100%
.item-info
padding: .1rem
.item-title
line-height: .54rem
font-size: .32rem
ellipsis() //文字超出显示三个点
.item-desc
line-height: .4rem
color: #ccc
ellipsis() //文字超出显示三个点
</style>

main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
import store from './store'
import VueAwesomeSwiper from 'vue-awesome-swiper'
import 'styles/reset.css'
import 'styles/border.css'
import 'styles/iconfont.css'
import 'swiper/dist/css/swiper.css'

// npm install babel-polyfill --save 浏览器兼容问题
// import 'babel-polyfill'

// package.json 中 "dev": "webpack-dev-server --host 0.0.0.0 使项目支持ip 192.168.0.2

// vue create hello-world
// npm run dev
// npm install stylus --save 样式快速编写
// npm install stylus-loader --save 样式快速编写
// npm install vue-awesome-swiper@2.6.7 --save
// 项目/build/webpack.base.conf.js 文件中配置项目文件路径别名 styles
// https://stylus.xiniushu.com/ stylus 文档

// swiper文档:
// https://2.swiper.com.cn/api/pagination/2014/1217/68.html
// https://www.swiper.com.cn/api/index.html

// axios 文档 https://www.axios-http.cn/docs/intro
// api转发配置 config/index.js proxyTable

// css 文档 https://juejin.cn/post/6941206439624966152

// better-scroll 使用 https://better-scroll.github.io/docs/zh-CN/guide/
// https://better-scroll.github.io/docs-v1/doc/zh-hans/#better-scroll%20%E6%98%AF%E4%BB%80%E4%B9%88
// npm install better-scroll@1.8.1 --save

// vuex数据共享组件
// npm install vuex --save

// 打包上线 npm run build
// https://blog.csdn.net/u010775335/article/details/121870499
// https://blog.csdn.net/SunMoon_444/article/details/130727816

// 异步组件,按需加载js,不会一次加载 修改router下的index.js

// https://core-player.github.io/vue-core-video-player/zh/
// 资源库 https://github.com/vuejs/awesome-vue?tab=readme-ov-file

/**
* package.json文件中
* "devDependencies": {}
*"stylus-loader": "^3.0.2",
*/

Vue.config.productionTip = false
Vue.use(VueAwesomeSwiper)

/* eslint-disable no-new */
new Vue({
el: '#app',
router,
store,
components: { App },
template: '<App/>'
})

项目开发要点

静态后端数据

static目录下创建mock目录,json放里面
项目的config/index.js文件 配置

1
2
3
4
5
6
7
8
9
10
11
// Paths
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {
'/api': {
target: 'http://localhost:8081',
pathRewrite: {
'^/api':'/static/mock'
}
}
},
axios 访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
methods: {
getDetailInfo () {
// 获取路由参数 this.$route.params.id
axios.get('/api/detail.json?id=', {
params: {
id: this.$route.params.id
}
}).then(this.detailInfoSucc)
},
detailInfoSucc (res) {
res = res.data
if (res.ret && res.data) {
const data = res.data
this.sightName = data.sightName
this.bannerImg = data.bannerImg
this.gallaryImgs = data.gallaryImgs
this.categoryList = data.categoryList
console.log(data)
}
}
}
异步组件配置

router/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

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')
}
],
scrollBehavior (to, from, savedPosition) {
// return 每次进入新页面期望滚动到哪个的位置
return {x: 0, y: 0}
}
})

路由跳转
1
this.$router.push('/')
1
2
3
4
5
6
<div class="header">
<router-link to="/">
<div class="iconfont header-back">&#xe601;</div>
</router-link>
城市选择
</div>
better-scroll 使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
<template>
<div class="list" ref="wrapper">
<div>
<div class="area">
<div class="title border-topbottom">当前城市</div>
<div class="button-list">
<div class="button-wrapper">
<div class="button">
{{ city }}
</div>
</div>
</div>
</div>
<div class="area">
<div class="title border-topbottom">热门城市</div>
<div class="button-list">
<div class="button-wrapper"
v-for="city of hotCities"
:key="city.id"
@click="handleCityClick(city.name)"
>
<div class="button">{{ city.name }}</div>
</div>
</div>
</div>
<div class="area"
v-for="(item,key) of cities"
:key="key"
:ref="key"
>
<div class="title border-topbottom">{{ key }}</div>
<div class="item-list">
<div class="item border-topbottom"
v-for="innerItem of item"
:key="innerItem.id"
@click="handleCityClick(innerItem.name)"
>
{{ innerItem.name }}
</div>
</div>
</div>
</div>
</div>
</template>

<script>
import BScroll from 'better-scroll'
// vuex 高级用法
import { mapState } from 'vuex'

export default {
name: 'CityList',
props: {
cities: Object,
hotCities: Array,
letter: String
},
methods: {
handleCityClick (city) {
// alert(city)
// 改变共用数据
// this.$store.dispatch('changeCity', city)
// 也可以直接commit 不用去store实现action
this.$store.commit('changeCity', city)
// 跳转到首页
this.$router.push('/')
}
},
mounted () {
// click: true 列表里面项目可点击
this.scroll = new BScroll(this.$refs.wrapper, {click: true})
},
// 监听器
watch: {
letter () {
console.log('变化了' + this.letter)
const element = this.$refs[this.letter][0]
this.scroll.scrollToElement(element)
}
},
computed: {
// ...展开运算符
...mapState(['city'])
}
}
</script>

<style lang="stylus" scoped>
@import '~styles/theme.styl'
.border-topbottom
&:before
border-color: #ccc
&:after
border-color: #ccc
.border-bottom
&:before
border-color: #ccc
.list
overflow hidden
position: absolute
top: 1.58rem
left 0
right: 0
bottom: 0
.title
line-height: .54rem
background: #eee
padding-left: .2rem
color #666
font-size: .26rem
.button-list
padding: .1rem .6rem .1rem .2rem //上右下左
overflow hidden
.button-wrapper
width 33.33%
float left
.button
text-align: center
margin: .1rem
padding .1rem 0
border: .02rem solid #ccc
border-radius: .06rem
.item-list
.item
line-height: .76rem
padding-left: .2rem
</style>

兄弟组件传值

借助父组件

节流函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
<template>
<div>
<div class="search">
<input type="text"
class="search-input"
placeholder="输入城市名或拼音" v-model="keyWord"/>
</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"
@click="handleCityClick(item.name)"
>
{{ 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
}
},
methods: {
handleCityClick (city) {
// alert(city)
// 改变共用数据
// this.$store.dispatch('changeCity', city)
// 也可以直接commit 不用去store实现action
this.$store.commit('changeCity', city)
// 跳转到首页
this.$router.push('/')
}
},
computed: {
hasNoData () {
return !this.list.length
}
},
watch: {
// 监听keyWord 的改变
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(value => {
if (value.spell.indexOf(this.keyWord) > -1 ||
value.name.indexOf(this.keyWord) > -1) {
result.push(value)
}
})
}
this.list = result
}, 100)
}
},
mounted () {
this.scroll = new BScroll(this.$refs.search)
}
}
</script>

<style lang="stylus" scoped>
@import '~styles/theme.styl'
.search
background: $bgColor
height: .72rem
padding: 0 .2rem
.search-input
width 100%
height .62rem
line-height: .62rem
text-align: center
border-radius: .06rem
color: #666
box-sizing: border-box
padding 0 .1rem
.search-content
z-index 1
overflow hidden
position: absolute
top: 1.58rem
left 0
right 0
bottom 0
background: #eee
.search-item
line-height: .62rem
padding-left: .2rem
color: #666
background: #fff
</style>

数据共享 VueX

npm install vuex –save
https://vuex.vuejs.org/zh/

新建store文件夹
创建index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

let defaultCity = '北京'
try {
// 本地存储
if (localStorage.city) {
defaultCity = localStorage.city
}
} catch (e) {}

export default new Vuex.Store({
state: {
city: defaultCity
},
// actions: {
// changeCity (ctx, city) {
// // console.log(city)
// ctx.commit('changeCity', city)
// }
// },
mutations: {
changeCity (state, city) {
state.city = city
try {
// 本地存储
localStorage.city = city
} catch (e) {
}
}
}
})

main.js

1
2
3
4
5
6
7
8
import store from './store'
new Vue({
el: '#app',
router,
store,
components: { App },
template: '<App/>'
})
  • 在组件中使用
1
2
<!-- 从store中取共用数据 -->
{{this.$store.state.city}}
  • 改变共用数据
1
2
3
// this.$store.dispatch('changeCity', city)
// 也可以直接commit 不用去store实现action
this.$store.commit('changeCity', city)

可以将 state 和 mutations的代码拆分成
state.js 和 mutations.js

  • vuex 的高级用法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//   this.$store.commit('changeCity', city) 替换
在组件中:
import { mapState } from 'vuex'
......
computed: {
// ...展开运算符 取本地存储
...mapState(['city'])
},
......
this.city 直接调用

this.$store.commit('changeCity', city) 替换
在组件中:
import { mapState,mapMutations } from 'vuex'
......
methods: {
...mapMutations([changeCity])
}
this. changeCity(city) 直接调用

使用 keep-alive 优化页面

路由发生切换的时候,页面都会重新获取数据并重新渲染。

App.vue 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div id="app">
<!-- router-view表示当前路由地址所对应的内容 -->
<!-- keep-alive 把页面保存到内存里面,从内存中取、避免不必要的请求 -->
<!-- exclude 不缓存Detail页面 -->
<keep-alive exclude="Detail">
<router-view/>
</keep-alive>
</div>
</template>

<script>
export default {
name: 'App'
}
</script>

<style>
</style>

  • 如果使用了keep-alive,则页面重新显示的时候会执行activated方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
activated () {
if (this.lastCity !== this.city) {
// 当城市数据发生变化时,重新请求数据
this.lastCity = this.city
this.getHomeInfo()
}
}

//页面隐藏
deactivated() {
}

//只有使用了keep-alive的时候才有上面两个函数
//其他两个生命周期钩子
mounted () {
window.addEventListener('scroll', this.handleScroll)
},
destroyed () {
window.removeEventListener('scroll', this.handleScroll)
}
动态路由

Recommend.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div class="title">热销推荐</div>
<ul>
<!-- 把router-link变成li标签,解决了a标签文字颜色问题 -->
<router-link
class="item border-bottom"
v-for="item of list"
:key="item.id"
tag="li"
:to="'/detail/' + item.id" //跳转传参
>
<img class="item-img" :src="item.imgUrl"/>
<div class="item-info">
<p class="item-title">{{ item.title }}</p>
<p class="item-desc">{{ item.desc }}</p>
<button class="item-button">查看详情</button>
</div>
</router-link>
</ul>
</div>

接受参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
methods: {
getDetailInfo () {
// 获取路由参数 this.$route.params.id
axios.get('/api/detail.json?id=', {
params: {
id: this.$route.params.id //接受参数
}
}).then(this.detailInfoSucc)
},
detailInfoSucc (res) {
res = res.data
if (res.ret && res.data) {
const data = res.data
this.sightName = data.sightName
this.bannerImg = data.bannerImg
this.gallaryImgs = data.gallaryImgs
this.categoryList = data.categoryList
console.log(data)
}
}
},
mounted () {
this.getDetailInfo()
}

路由路径

1
2
3
4
5
{
path: '/detail/:id',
name: 'Detail',
component: () => import('@/pages/detail/Detail')
}
动态修改样式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
<template>
<div>
<router-link
tag="div" to="/"
class="header-abs"
v-show="showAbs"
>
<div class="iconfont header-abs-back">&#xe601;</div>
</router-link>
<div
class="header-fixed"
v-show="!showAbs"
:style="opacityStyle"
>
景点详情
<router-link to="/">
<div class="iconfont header-fixed-back">&#xe601;</div>
</router-link>
</div>
</div>
</template>

<script>
export default {
name: 'DetailHeader',
data () {
return {
showAbs: true,
opacityStyle: {
opacity: 0
}
}
},
methods: {
handleScroll () {
console.log('scroll')
// 有些浏览器不兼容
const top = document.documentElement.scrollTop || document.body.scrollTop || window.pageYOffset
if (top > 60) {
let opacity = top / 140
opacity = opacity > 1 ? 1 : opacity
this.opacityStyle = {
opacity
}
this.showAbs = false
} else {
this.showAbs = true
}
// console.log(document.documentElement.scrollTop)
}
},
// keep-live 页面显示执行
// <keep-alive exclude="Detail"> 详情页面不在keep管理范围内 activated、deactivated不会被执行了
// activated () {
// // **** 重要对全局事件的解绑 window
// window.addEventListener('scroll', this.handleScroll)
// },
// // keep-live 页面隐藏执行
// deactivated () {
// window.removeEventListener('scroll', this.handleScroll)
// }
mounted () {
window.addEventListener('scroll', this.handleScroll)
},
destroyed () {
window.removeEventListener('scroll', this.handleScroll)
}
}
</script>

<style lang="stylus" scoped>
@import '~styles/theme.styl'
.header-abs
position: absolute
top: .2rem
left: .2rem
width .8rem
height .8rem
line-height: .8rem
border-radius: .4rem
text-align: center
background: rgba(0,0,0,.8)
.header-abs-back
color #fff
font-size: .4rem
.header-fixed
position fixed
z-index 2
top: 0
left 0
right 0
overflow hidden
height: .86rem
line-height: .86rem
color: #fff
text-align: center
background: $bgColor
font-size: .32rem
.header-fixed-back
position: absolute
top: 0
left: 0
width: .64rem
text-align: center
font-size: .4rem
color: #fff
</style>
递归组件使用

组件自身调用自身

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<template>
<div>
<div
class="item"
v-for="item of list"
:key="item.title">
<div class="item-title border-bottom">
<span class="item-title-icon"></span>
{{ item.title }}
</div>
<div v-if="item.children" class="item-child">
<!-- 组件自己调用自己,递归调用 -->
<detail-list :list="item.children"/>
</div>
</div>
</div>
</template>

<script>
export default {
name: 'DetailList',
props: {
list: {
type: Array
}
}
}
</script>

<style lang="stylus" scoped>
.item-title
line-height: .8rem
font-size: .32rem
padding: 0 .2rem
.item-child
padding: 0 .2rem
</style>
动画组件封装
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<transition>
<slot></slot>
</transition>
</template>

<script>
export default {
name: 'Fade'
}
</script>

<style lang="stylus" scoped>
.v-enter,.v-leave-to
opacity: 0
.v-enter-active,.v-leave-active
transition: opacity .5s
</style>

使用

1
2
3
4
5
6
7
8
9
<fade>
<!-- common-gallary 作为插槽 -->
<common-gallary
:imgs="gallaryImgs"
v-show="showGallary"
@close="gallaryClose"
>
</common-gallary>
</fade>
服务器联调
真机测试

package.json 配置:

1
2
3
"scripts": {
"dev": "webpack-dev-server --host 0.0.0.0 --inline --progress --config build/webpack.dev.conf.js",
"start": "npm run dev",

ip + 端口访问

打包项目

npm run build

生成dist目录,放入服务器

配置打包目录
config/index.js

1
2
3
4
5
6
7
8
build: {
// Template for index.html
index: path.resolve(__dirname, '../dist/index.html'),

// Paths
assetsRoot: path.resolve(__dirname, '../dist'),
assetsSubDirectory: 'static',
assetsPublicPath: './', 配置打包目录
异步组件优化代码

按需加载组件,路由中配置

vue插件资源
vue cli 升级4.0

卸载老版本
npm uninstall vue-cli -g
npm install @vue/cli -g
vue create travel , 手动选择特性

配置路径
vue.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

// const { defineConfig } = require('@vue/cli-service')
// module.exports = defineConfig({
// transpileDependencies: true
// })
const path = require('path');

module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:8081',
pathRewrite: {
'^/api':'/mock'
}
}
}
},
configureWebpack: {
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'), // 将@符号指向src目录
'styles': path.resolve(__dirname, 'src/assets/styles'),
'common': path.resolve(__dirname, 'src/common')
// 'assets': path.resolve(__dirname, 'src/assets'), // 将assets指向src/assets目录
// 'components': path.resolve(__dirname, 'src/components') // 将components指向src/components目录
}
}
}
};

static 换成 public

项目升级3.0 vue-cli 4