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