今年上半年大概 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, //路由的路径
slug: 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' },
{ hslug: '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 最常用的几个功能已经基本上介绍完了,还有资源文件、插件等功能,有兴趣的可以移步官方文档
完
这篇文章就写到这里吧,本文也没有太多高深的东西,主要作为入门使用。