小QA学习前端系列之vue实战

##通过一个简单vue项目了解vue整个流程

1
https://github.com/shinygang/Vue-cnodejs.git

大神写的一个高仿cnodejs
直接clone

1
yarn

启动服务(http://localhost:8020)

1
npm run dev

然而我们并不是让你们看如何这个页面如何好看,那没啥用
还是进入code中,才能理解其中的玄学
好的编辑器是成功的一半,所以我选vscode,你呢 哈哈
好了 进入主题 先看项目结构

项目结构

项目结构
采用 vue2 vue-router2 vuex 组件思想构建了整个项目
先来看看 main.js main文件为整个文件的入口

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
import Vue from 'vue';
import $ from 'webpack-zepto';
import VueRouter from 'vue-router';
import filters from './filters';
import routes from './routers';
import Alert from './libs/alert';
import store from './vuex/user';
import FastClick from 'fastclick';
//引入Vuerouter
Vue.use(VueRouter);
//引入alert
Vue.use(Alert);
$.ajaxSettings.crossDomain = true;
// 实例化Vue的filter
Object.keys(filters).forEach(k => Vue.filter(k, filters[k]));
// 实例化VueRouter
const router = new VueRouter({
mode: 'history',
routes
});
//FastClick是一个非常方便的库,在移动浏览器上发生介于轻敲及点击之间的指令时,能够让你//摆脱300毫秒的延迟。FastClick可以让你的应用程序更加灵敏迅捷。
FastClick.attach(document.body);
// 处理刷新的时候vuex被清空但是用户已经登录的情况
if (window.sessionStorage.user) {
store.dispatch('setUserInfo', JSON.parse(window.sessionStorage.user));
}
// 登录中间验证,页面需要登录而没有登录的情况直接跳转登录
router.beforeEach((to, from, next) => {
// 处理左侧滚动不影响右边
// $('html, body, #page').removeClass('scroll-hide');
$('body').css('overflow', 'auto');
if (to.matched.some(record => record.meta.requiresAuth)) {
if (store.state.userInfo.userId) {
next();
} else {
next({
path: '/login',
query: { redirect: to.fullPath }
});
}
} else {
next();
}
});
new Vue({
router,
store
}).$mount('#app');

这就是整个main文件 只是将vuex store 与router 最后都实例化到vue对象中并挂在到id为app的节点下

路由

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
// require.ensure 是 Webpack 的特殊语法,用来设置 code-split point
const Home = resolve => {
require.ensure(['./views/index.vue'], () => {
resolve(require('./views/index.vue'));
});
};
const List = resolve => {
require.ensure(['./views/list.vue'], () => {
resolve(require('./views/list.vue'));
});
};
const routers = [{
path: '/',
name: 'home',
component: Home
}, {
path: '/cnodevue',
name: 'cnodevue',
component: Home
}, {
path: '/list',
name: 'list',
component: List
}, {
path: '/topic/:id',
name: 'topic',
component(resolve) {
require.ensure(['./views/topic.vue'], () => {
resolve(require('./views/topic.vue'));
});
}
}, {
path: '/add',
name: 'add',
component(resolve) {
require.ensure(['./views/new.vue'], () => {
resolve(require('./views/new.vue'));
});
},
meta: { requiresAuth: true }
}, {
path: '/message',
name: 'message',
component(resolve) {
require.ensure(['./views/message.vue'], () => {
resolve(require('./views/message.vue'));
});
},
meta: { requiresAuth: true }
}, {
path: '/user/:loginname',
name: 'user',
component(resolve) {
require.ensure(['./views/user.vue'], () => {
resolve(require('./views/user.vue'));
});
}
}, {
path: '/about',
name: 'about',
component(resolve) {
require.ensure(['./views/about.vue'], () => {
resolve(require('./views/about.vue'));
});
}
}, {
path: '/login',
name: 'login',
component(resolve) {
require.ensure(['./views/login.vue'], () => {
resolve(require('./views/login.vue'));
});
}
}, {
path: '*',
component: Home
}];
export default routers;

上面定义了系统所有的路由路径,以及需要实现懒加载的路由

组件

我们先看看系统由多少组件组合而成
/image/component.png
再来看看 views
/image/view.png
回到我们的router 当我们进入一个应用时,首先呈现在我们眼前的应该是index.
我们看看router的代码

1
2
3
path: '/',
name: 'home',
component: Home

而component 则是来自 index.vue

1
2
3
4
5
const Home = resolve => {
require.ensure(['./views/index.vue'], () => {
resolve(require('./views/index.vue'));
});
};

ok 我们看看 index.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
<template>
<div>
<img class="index" src="../assets/images/index.png">
</div>
</template>
<script>
require('../assets/scss/CV.scss');
require('../assets/scss/iconfont/iconfont.css');
require('../assets/scss/github-markdown.css');
export default {
mounted() {
setTimeout(() => {
this.$router.push({
name: 'list'
});
}, 2000);
}
};
</script>
<style lang="scss">
.index {
width: 100%;
background-color: #fff;
margin-top: 40%;
}
</style>

index.vue 只是利用mount去向$router push一个{ name: ‘list’}对象
设置了一个异步操作 2秒后 跳转到list页面
在来看看 index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Vue.js-Cnodejs社区</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, minimal-ui">
<meta content="yes" name="apple-mobile-web-app-capable">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta content="black" name="apple-mobile-web-app-status-bar-style">
</head>
<body>
<div id="app" v-cloak>
<router-view></router-view>
</div>
<!-- built files will be auto injected -->
</body>
</html>

他是整个SPA的基础页面

将真个应用会挂载的id 为app 这个节点下。

再来看看index跳转的list页面

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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
<template>
<div>
<!-- 全局header -->
<nv-head :page-type="getTitleStr(searchKey.tab)"
ref="head"
:fix-head="true"
:need-add="true">
</nv-head>
<section id="page">
<!-- 首页列表 -->
<ul class="posts-list">
<li v-for="item in topics" :key="item.id">
<router-link :to="{name:'topic',params:{id:item.id}}">
<h3 v-text="item.title"
:class="getTabInfo(item.tab, item.good, item.top, true)"
:title="getTabInfo(item.tab, item.good, item.top, false)">
</h3>
<div class="content">
<img class="avatar" :src="item.author.avatar_url" />
<div class="info">
<p>
<span class="name">
{{item.author.loginname}}
</span>
<span class="status" v-if="item.reply_count > 0">
<b>{{item.reply_count}}</b>
/{{item.visit_count}}
</span>
</p>
<p>
<time>{{item.create_at | getLastTimeStr(true)}}</time>
<time>{{item.last_reply_at | getLastTimeStr(true)}}</time>
</p>
</div>
</div>
</router-link>
</li>
</ul>
</section>
<nv-top></nv-top>
</div>
</template>
<script>
import $ from 'webpack-zepto';
import utils from '../libs/utils.js';
import nvHead from '../components/header.vue';
import nvTop from '../components/backtotop.vue';
export default {
filters: {
getLastTimeStr(time, isFromNow) {
return utils.getLastTimeStr(time, isFromNow);
}
},
data() {
return {
scroll: true,
topics: [],
index: {},
searchKey: {
page: 1,
limit: 20,
tab: 'all',
mdrender: true
},
searchDataStr: ''
};
},
mounted() {
if (this.$route.query && this.$route.query.tab) {
this.searchKey.tab = this.$route.query.tab;
}
// 如果从详情返回并且之前存有对应的查询条件和参数
// 则直接渲染之前的数据
if (window.window.sessionStorage.searchKey && window.window.sessionStorage.tab === this.searchKey.tab) {
this.topics = JSON.parse(window.window.sessionStorage.topics);
this.searchKey = JSON.parse(window.window.sessionStorage.searchKey);
this.$nextTick(() => $(window).scrollTop(window.window.sessionStorage.scrollTop));
} else {
this.getTopics();
}
// 滚动加载
$(window).on('scroll', utils.throttle(this.getScrollData, 300, 1000));
},
beforeRouteLeave(to, from, next) {
// 如果跳转到详情页面,则记录关键数据
// 方便从详情页面返回到该页面的时候继续加载之前位置的数据
if (to.name === 'topic') {
// 当前滚动条位置
window.window.sessionStorage.scrollTop = $(window).scrollTop();
// 当前页面主题数据
window.window.sessionStorage.topics = JSON.stringify(this.topics);
// 查询参数
window.window.sessionStorage.searchKey = JSON.stringify(this.searchKey);
// 当前tab
window.window.sessionStorage.tab = from.query.tab || 'all';
}
$(window).off('scroll');
next();
},
beforeRouteEnter(to, from, next) {
if (from.name !== 'topic') {
// 页面切换移除之前记录的数据集
if (window.window.sessionStorage.tab) {
window.window.sessionStorage.removeItem('topics');
window.window.sessionStorage.removeItem('searchKey');
window.window.sessionStorage.removeItem('tab');
}
}
next();
},
methods: {
// 获取title文字
getTitleStr(tab) {
let str = '';
switch (tab) {
case 'share':
str = '分享';
break;
case 'ask':
str = '问答';
break;
case 'job':
str = '招聘';
break;
case 'good':
str = '精华';
break;
default:
str = '全部';
break;
}
return str;
},
// 获取不同tab的样式或者标题
getTabInfo(tab, good, top, isClass) {
return utils.getTabInfo(tab, good, top, isClass);
},
// 获取主题数据
getTopics() {
let params = $.param(this.searchKey);
$.get('https://cnodejs.org/api/v1/topics?' + params, (d) => {
this.scroll = true;
if (d && d.data) {
d.data.forEach(this.mergeTopics);
}
});
},
mergeTopics(topic) {
if (typeof this.index[topic.id] === 'number') {
const topicsIndex = this.index[topic.id];
this.topics[topicsIndex] = topic;
} else {
this.index[topic.id] = this.topics.length;
this.topics.push(topic);
}
},
// 滚动加载数据
getScrollData() {
if (this.scroll) {
let totalheight = parseInt($(window).height(), 20) + parseInt($(window).scrollTop(), 20);
if ($(document).height() <= totalheight + 200) {
this.scroll = false;
this.searchKey.page += 1;
this.getTopics();
}
}
}
},
watch: {
// 切换页面
'$route' (to, from) {
// 如果是当前页面切换分类的情况
if (to.query && to.query.tab) {
this.searchKey.tab = to.query.tab;
this.topics = [];
this.index = {};
}
this.searchKey.page = 1;
this.getTopics();
// 隐藏导航栏
this.$refs.head.show = false;
}
},
components: {
nvHead,
nvTop
}
};
</script>

利用了2个component nvHEAD nvTop
下一篇 继续讲解 list view