今年上半年大概4月的时候,因为开发需要,加上个人兴趣,于是便尝试了使用Vue来编写前端。目前有尝试做过纯前端的小项目、一个浏览器扩展、一个混合渲染的小项目。就着这几个月的经历,来说说Vue开发的体会和踩过的“坑”吧。
Vue:方便的前端框架
和之前经常接触的jQuery、Zepto等不同,Vue是一个“渐进式框架”。按照我的理解,便是达到HTML和JS分离的效果。
模板引擎
使用Vue给我最大的感受便是:你不需要直接操作DOM,也不需要关心DOM的重新渲染等各种细节。你只需要告诉Vue该怎么渲染这个组件,再把数据告诉Vue,剩下的事情便是Vue自己来完成了。
这样一来,其实Vue便与一些“模板引擎”有很多相似之处了,例如前端领域的BaiduTemplate、mustache,后端领域的Smarty等,就像:
<!-- 这一段是BaiduTemplate的Demo代码 -->
<%if(list.length>1) { %>
<div>
<h2>输出list,共有<%=list.length%>个元素</h2>
<ul>
<!-- 循环语句 for-->
<%for(var i=0;i<5;i++){%>
<li><%=list[i]%></li>
<%}%>
</ul>
</div>
<%}else{%>
<h2>没有list数据</h2>
<%}%>
<!-- 用Vue实现类似的效果 -->
<div v-if="list.length>1">
<h2>输出list,共有{{list.length}}个元素</h2>
<ul>
<li v-for="item of list">{{item}}</li>
</ul>
</div>
<h2 v-else>没有list数据</h2>
其实不难看出,单从“模板”的作用来讲,Vue和大部分模板引擎的作用很类似,语法稍微有一些不同。Vue的大部分功能都写为标签的属性,而大部分模板引擎则会独立出各种if、for等语法。个人认为,Vue的学习成本会更低一些。
数据绑定和事件
不过,如果只是把Vue当成模板引擎来用,那也太大材小用了。Vue最擅长的还是数据的绑定。例如:
<p><span>您输入了:{{text}}</span></p>
<p><input type="text" v-modal="text"></p>
<p><input type="button" value="清空" @click="clear"></p>
对应的Vue部分代码:
export default {
data() {
return {
text: ""
}
},
methods: {
clear() {
this.text = "";
}
}
}
这段代码实现了什么效果?我们通过v-modal指令实现了输入框和text变量的双向绑定,修改text变量,效果会直接呈现在输入框中。修改输入框中的内容,也会直接影响text变量。同时,text变量的变化,也会直接影响上面的{{text}}。
总而言之,通过Vue,将会减少大量直接操作DOM的代码(Vue也不推荐直接操作DOM),让JS代码更专注于实际逻辑,而不是各类DOM操作。
组件
这是Vue强大的另一方面。在有一定规模的项目中,常常会有一些通用性的元素,会用在一个项目中各个地方,这个时候,将其抽象为组件便是一个不错的选择。
例如,我们可以定义一种特殊的按钮:
Vue.component('my-button', {
template: '<button><slot></slot></button>'
})
之后,在HTML中,我们可以直接通过<my-button>test</my-button>来使用它。是不是很简单?
那么,我们现在再多做一些事。例如,我们想要让用户指定按钮的颜色,通过“color”标签来传递:
Vue.component('my-button', {
props: ['color'],
template: '<button :style="backgroundColor"><slot></slot></button>',
computed: {
backgroundColor: function() {
return 'background-color: ' + (this.color ? this.color : 'white');
}
}
});
//使用方式:
//<my-button color="green">test</my-button>
可能有些读者已经注意到,我们在介绍“组件”的开头,就写到了一个<slot></slot>。在Vue中,它被称为“插槽”。顾名思义,便是组件给用的人留下的一个“空隙”,使用的时候可以按需要插入不同的内容。
写在<slot>与</slot>之间的内容被称为“默认内容”,例如:
<div id="app">
<my-button></my-button>
<my-button>test</my-button>
</div>
<script>
Vue.component('my-button', {
template: '<button><slot>我是按钮</slot></button>'
});
new Vue({
el: '#app',
data: function() {
return {};
}
});
</script>
运行结果,会生成这样的两个按钮:

一个组件可以有很多个插槽,通过name来进行区分,例如:
<div id="app">
<my-button>
<i slot="icon" class="material-icons">home</i>
<span slot="text">主页</span>
</my-button>
</div>
<script>
Vue.component('my-button', {
template: `<button>
<p><slot name="icon"></slot></p>
<p><small>上面是按钮的图标</small></p>
<p><slot name="text"></slot></p>
</button>`
});
new Vue({
el: '#app',
data: function() {
return {};
}
});
</script>
效果:

一点提示
- 直接修改对象Vue不一定会更新视图,因为Vue是通过getter和setter来实现监听对象的修改,例如:
<div id="app">
<div v-for="(item, index) of count">
<h3>{{index}}</h3>
<p v-for="(item2, index2) of item">{{index2}}: {{item2}}</p>
</div>
<p><button @click="addAudio">add audio</button></p>
<p><button @click="removeImageUpload">remove image upload</button></p>
</div>
<script>
new Vue({
el: '#app',
data: function() {
return {
count: {
image: {
hot: 0,
upload: 0,
explore: 0
}
}
};
},
methods: {
addAudio() {
//无效
this.count.audio = {
hot: 10,
upload: 10,
explore: 10
};
//有效
this.$set(this.count, 'audio', {
hot: 10,
upload: 10,
explore: 10
});
//有效
Vue.set(this.count, 'audio', {
hot: 10,
upload: 10,
explore: 10
});
},
removeImageUpload() {
//无效
delete this.count.image.upload;
//有效
this.$delete(this.count.image, 'upload');
//有效
Vue.delete(this.count.image, 'upload');
}
}
});
</script>
- 另外,上文只是简单介绍了一下Vue的常用功能。若想了解更多,请阅读官方文档
Vuex:全局状态管理
引入
在上文的“组件”中,我们提到了props,它可以给子组件传递属性。但是,它依然存在一些不便之处:
1.在“层层嵌套”的时候会非常不便。
某些情况下我们可能会写出这样的东西:
文件1:
<template> <div> <navbar :cart="cart"></navbar> </div> </template>
文件2:
<template id="navbar">
<div class="navbar">
<div class="container">
<cart :cart="cart" @click="goCart">购物车({{cart.length}})</div>
</div>
</div>
</template>
文件3:
<template id="cart"> <div> <!-- 使用cart做一些事 --> </div> </template>
这边意味着我们需要一层一层的往下嵌套,这样虽然能达到目的,但是非常繁琐,并且也不太合理。
2.组件之间传递内容很繁琐
在Vue中,我们不推荐直接修改,而是需要通过“提交事件”来达到目的,例如:
<comp :foo="bar" @update:foo="val => bar = val"></comp>
当comp组件需要更新foo时,会这样做:
this.$emit('update:foo', newValue)
共用同一个东西的组件比较少还好,一多起来简直是灾难性的。另外,组件之间传递属性也不太方便。
因此,我们有了一个新东西:Vuex
什么时候该用Vuex
并不是所有时候都会用到Vuex,如果只是一个简单的应用,使用Vuex反而会增加整体的复杂度。因此,Vuex推荐用于大型的单页应用。
当然,我觉得,什么时候该用Vuex,这个问题其实很简单:您知道会用到的时候,自然就会用了。
基本介绍
实质上Vuex非常简单,因为它只需要做到维护一个“状态树”就行了。因此,Vuex就像一个全局的“data”一样:
const store = () => new Vuex.Store({
state: {
count: 0
}
});
在组件之中,我们可以像使用data一样,方便的使用store中的数据:
new Vue({
el: '#app',
store,
data: function() {
return {};
},
computed: {
showCount() {
return 'Count: ' + this.$store.state.count;
}
}
});
与Vue自带的功能很类似,例如,Vue的computed功能,在Vuex中也有类似的实现,Vuex把它称作getter:
const store = () => new Vuex.Store({
state: {
count: 0
},
getters: {
halfCount() {
return this.state.count / 2;
}
}
});
如果组件需要修改state中的数值,则需要通过Mutation提交事件进行:
const store = () => new Vuex.Store({
state: {
count: 0
},
mutations: {
increase(state) {
state.count++;
},
increaseWith(state, n) {
state.count += n;
}
}
});
//组件中使用:
methods: {
click() {
this.store.commit('increase');
this.store.commit('increaseWith', 5);
}
}
更多详细内容可以参见官方文档
Vue Router:单页应用的路由组件
一般我们使用Vue不是用来开发一个简单的页面,而是会构建一个有多个页面的网站。这时,如何让它跳转良好,便是需要解决的问题了。Vue也有自己的解决方案:Vue Router
基本使用
首先,我们在基本模板中,放入Vue Router渲染的部分:
<div id="app"> <h1>这是标题</h1> <router-view></router-view> </div>
接下来,我们需要定义好路由,并把它挂载给Vue:
Vue.use(VueRouter)
//创建router实例
//其中,每一条router的component指的就是这一条路由对应的组件,这个组件将会被渲染到<router-view></router-view>上
//组件可以有多种方式定义,下面几种都是合法的形式
const router = new VueRouter({
routes: [
{
path: '/foo',
component: { template: '<div>foo</div>' }
},
{
path: '/bar',
component: require('component/bar')
},
{
path: '/login',
component: () => Promise.resolve(require('component/login.vue'))
}
]
});
//初始化Vue
new Vue({
router
}).$mount('#app')
接下来,在页面里面,就可以使用<router-link to="/bar">Go to Bar</router-link>来生成一个路由跳转的标签。或者,也可以在Javascript中,使用this.$router.push('/')进行路由跳转。
动态路由和子路由
很多网站经常会有诸如这样的路径:
/user/:id/profile /user/:id/feed /archives/:id
这些路由,一般通过子路由和动态路由来实现。例如,/archives/:id可以这样实现:
routes: [
{ path: '/archives/:id', component: Archives }
]
在Archives组件中,我们可以使用this.$route获取路由信息:
data() {
return {
path: this.$route.path, //路由的路径
id: this.$route.params.id, //param中的参数
query: this.$route.query //查询参数,即URL中“?"后的内容
}
}
这就是基本的动态路由。我们接下来看一下,子路由如何实现。以上面的URL为例:
routes: [
{
path: '/user/:id',
component: User,
children: [
{
//对应/user/:id,如果你需要的话
path: '',
component: UserHome
},
{
//对应/user/:id/profile
path: 'profile',
component: UserProfile
},
{
//对应/user/:id/feed
path: 'feed',
component: UserFeed
}
]
}
]
你可以发现,children下每一项都是一条完整的router,这就意味着,你可以继续往下嵌套children
Vue Router的基本介绍就到此为止吧,实际上Vue Router的功能远不止这些,详细了解请移步官方文档
SSR(Server-Side Rendering)
其实服务器端渲染并不是什么新鲜事。在Vue这类前端框架还没有流行开的时候,页面基本上都是服务器端渲染的。在PHP、Java等语言中,已经是一个老东西了。不过,如果能用一套代码,同时实现客户端渲染+服务器端渲染,是不是一件更简单的事呢?
SSR的出现也是解决了一些实际需求的:
- 搜索引擎无法完整收录单页应用
- 减少首屏渲染时间,对于运行缓慢或者网络不佳的设备更加友好
Vue官方提供了vue-server-renderer,不过配置稍显繁琐,我们可以使用Nuxt.js来简化工作。
我们可以从这个模板开始,一步一步构建一个Web应用
在Vue中,页面都是组件。Nuxt则进行了一些细分,将其放入了pages中。在这之前,我们需要手动配置Vue Router,而Nuxt则会根据pages目录结构,自动生成router
实际上,Nuxt只是在Vue、Vue Router、Vuex、Vue Server Renderer等组件的基础上做了包装,方便我们的使用。因此,编写起Nuxt应用,实际上和编写Vue程序没有太多不同。下面我就数数一些常见的做法。
细分组件
Nuxt有一个默认模板,大部分情况下你不需要更改它。如果确实有需要,你可以在跟目录下创建一个名为app.html的文件,放入以下内容:
<!DOCTYPE html>
<html {{ HTML_ATTRS }}>
<head>
{{ HEAD }}
</head>
<body {{ BODY_ATTRS }}>
{{ APP }}
</body>
</html>
你可以在此基础上适当的修改内容。
接下来,将组件分为layouts、pages、components三类,并放入相应的目录中。
layouts是页面的整体布局,将之前的<router-view></router-view>改为<nuxt/>。至少需要有一个名为default的默认布局。
pages则是各种页面,同时,Nuxt也支持子页面(nuxt-child)。
components则是更纯粹的“组件”。
另外,在页面中使用router也需要做一些改变:将router-link改为nuxt-link即可,例如:
<router-link to="/about">关于</router-link> <!-- 改为下面的语句 --> <nuxt-link to="/about">关于</nuxt-link>
另外,Nuxt提供了一种方便的形式来修改<head>部分的内容,你可以在nuxt.config.js中配置:
head: {
title: '标题',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: 'some description' }
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
{ rel: 'stylesheet', href: 'https://example.com/style.css' }
]
}
异步数据的处理
相对于Vue,Nuxt增加了两个方法,来修改数据,它们是asyncData和fetch。asyncData用于在渲染组件之前异步获取数据,它的返回结果将会和data合并。而fetch则只是简单的抓取数据并填充状态树。例如:
export default {
data() {
return {
archive: {
title: "标题",
content: "内容"
}
}
},
async asyncData(context) {
const archive = await getApi('archive/get/' + context.params.id);
return {
archive: archive
};
},
async fetch ({ store, params }) {
const user = await getApi('user/info');
store.commit('setUser', user)
}
}
中间件
你可以定义一些中间件来处理请求,中间件可以作用于三个部分:
- 全局,在nuxe.config.js中配置
- 布局,在layouts中配置
- 页面,在pages中配置
如果有多个中间件,调用顺序为:全局-布局-页面。
例如,只允许登录用户访问:
//nuxt.config.js中配置
router: {
middleware: 'auth'
}
//单个页面或者布局中配置
export default {
middleware: 'auth',
data() {
return {};
}
}
然后,在mimiddlewarem目录下新建auth.js,输入下面内容:
export default function (context) {
return new Promise(resolve => {
//异步验证Header中的token
checkToken(context.req.header['x-token'])
.then(r => JSON.parse(r))
.then(r => {
if (r.success) {
//验证成功,允许访问
resolve();
} else {
//跳转到登录页面
context.redirect('/login');
//注意,下面这一行不能少
resolve();
}
})
})
}
到这里,其实Nuxt最常用的几个功能已经基本上介绍完了,还有资源文件、插件等功能,有兴趣的可以移步官方文档
完
这篇文章就写到这里吧,本文也没有太多高深的东西,主要作为入门使用。