Vue入门指南

Vue入门指南

今年上半年大概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>

运行结果,会生成这样的两个按钮:

1

一个组件可以有很多个插槽,通过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>

效果:

2

一点提示

  • 直接修改对象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最常用的几个功能已经基本上介绍完了,还有资源文件、插件等功能,有兴趣的可以移步官方文档

这篇文章就写到这里吧,本文也没有太多高深的东西,主要作为入门使用。