Vue+Webpack开发可复用的单页面富应用教程(组件篇)
本文首发于TalkingCoder,一个有逼格的程序员社区。转载请注明出处和作者。
写在前面
本文为系列文章,总共分四节,建议按顺序阅读:
《Vue+Webpack开发可复用的单页面富应用教程(配置篇)》
《Vue+Webpack开发可复用的单页面富应用教程(组件篇)》
《Vue+Webpack开发可复用的单页面富应用教程(技巧篇)》
在上一节中,我们介绍了在项目https://github.com/icarusion/vue-vueRouter-webpack中关于webpack的一些基础配置,包括开发环境和生产环境,在本节中,我们重点介绍使用Vue.js和vue-router,通过组件化的方式来开发单页面富应用的相关内容。读者可以clone或下载这个项目,结合具体代码来看本文。
基础知识扫盲
本段主要介绍一些前端的基础概念,老司机可以直接跳过。
单页面富应用(SPA)和前端路由
单页面富应用(即Single Page Web Application,以下简称SPA)应该是最近几年火起来的,尤其是在Angular框架诞生后,很多SPA的网站以及基于Electron或Ionic的桌面App和移动App层出不穷,比如Teambition。
SPA的核心即是前端路由。何为路由呢?说的通俗点就是网址,比如www.talkingcoder.com/article/list;专业点就是每次GET或者POST等请求,在服务端有一个专门的正则配置列表,然后匹配到具体的一条路径后,分发到不同的Controller,然后进行各种操作后,最终将html或数据返回给前端,这就完成了一次IO。当然,目前绝大多数的网站都是这种后端路由,也就是多页面的,这样的好处有很多,比如页面可以在服务端渲染好直接返回给浏览器,不用等待前端加载任何js和css就可以直接显示网页内容,再比如对SEO的友好等。那SPA的缺点也是很明显的,就是模板是由后端来维护或改写。前端开发者需要安装整套的后端服务,必要还得学习像PHP或Java这些非前端语言来改写html结构,所以html和数据、逻辑混为一谈,维护起来即臃肿也麻烦。然后就有了前后端分离的开发模式,后端只提供API来返回数据,前端通过Ajax获取到数据后,再用一定的方式渲染到页面里,这么做的优点就是前后端做的事情分的很清楚,后端专注在数据上,前端专注在交互和可视化上,从此前后搭配,干活不累,如果今后再开发移动App,那就正好能使用一套API了,当然缺点也很明显,就是首屏渲染需要时间来加载css和js。这种开发模式被很多公司认同,也出现了很多前端技术栈,比如以jQuery+artTemplate+Seajs(requirejs)+gulp为主的开发模式所谓是万金油了。在Node.js出现后,这种现象有了改善,就是所谓的大前端,得益于Node.js和JavaScript的语言特性,html模板可以完全由前端来控制,同步或异步渲染完全由前端自由决定,并且由前端维护一套模板,这就是为什么在服务端使用artTemplate、React以及即将推出的Vue2.0原因了。那说了这么多,到底怎样算是SPA呢,其实就是在前后端分离的基础上,加一层前端路由。
前端路由,即由前端来维护一个路由规则。实现有两种,一种是利用url的hash,就是常说的锚点(#),JS通过hashChange事件来监听url的改变,IE7及以下需要用轮询;另一种就是HTML5的History模式,它使url看起来像普通网站那样,以"/"分割,没有#,但页面并没有跳转,不过使用这种模式需要服务端支持,服务端在接收到所有的请求后,都指向同一个html文件,不然会出现404。所以,SPA只有一个html,整个网站所有的内容都在这一个html里,通过js来处理。
前端路由的优点有很多,比如页面持久性,像大部分音乐网站,你都可以在播放歌曲的同时,跳转到别的页面而音乐没有中断,再比如前后端彻底分离。前端路由的框架,通用的有Director,更多还是结合具体框架来用,比如Angular的ngRouter,React的ReactRouter,以及我们后面用到的Vue的vue-router。这也带来了新的开发模式:MVC和MVVM。如今前端也可以MVC了,这也是为什么那么多搞Java的钟爱于Angular。
开发一个前端路由,主要考虑到页面的可插拔、页面的生命周期、内存管理等。
编写可复用的代码、模块化、组件
编写可复用的代码是对编程质量的一个体现。写一个通用工具函数、维护一个对象,这些都可以说是可复用的,不过我们这里讨论的,主要是利用CommonJS规范来进行模块化开发。那代码复用和模块化有什么关系呢,其实模块化的一个原因就是可以使代码复用,你开发的模块可以提供给其他人用,一个模块可以是小到一个配置文件,也可以大到一个日历组件。把一个页面拆分成不同的模块,然后来组装,这样既能提高开发效率,又方便维护。那组件又是什么呢?如果说模块化是一种开发模式,那组件就是这种模式的具体实现。比如一个Button按钮、一个输入框,或者一个上传控件都可以封装为一个组件,在使用的时候,可能只用写一行,就能实现文件上传功能,甚至可以支持拖拽上传、大小和格式限制等。那一个组件具体怎么开发呢,这就是本文后面重点讨论的内容了。
Vue的路由和它的组件化
在项目https://github.com/icarusion/vue-vueRouter-webpack中,我们使用的技术栈是vue.js+vue-router+webpack,其中webpack的作用已经在上篇文章中详细介绍了。在说vue-router之前,我们先聊聊Vue的组件。
组件的构造
Vue的组件可以说是Vue中最神奇也是最难懂的部分了,这部分懂了,vue也就懂了。vue组件的特点是可插拔、独立作用域、观察者模式、完整的生命周期。我们来看一个组件的基本构成:
Vue.component('child', { props: ['msg'], template:'{{ msg }}', data:function(){return{ title:'TalkingCoder'} }, methods: {// ...}, ready:function(){ }, beforeDestroy:function(){ }, events: {// ...}});
一个组件基本跟一个vue实例是类似的,也有自己的methods和data,只不过data是通过一个function来返回了一个对象,具体原因可以查看vue的文档。
props是从父级通过html特性传递来的数据,它可以是字符串、数字、布尔、数组、对象,默认是单向的,也可以设置为双向绑定的。props里的参数可以直接通过像this.msg这种方式调用,这与data的里的数据是一样的。
template是这个组件使用的html片段,可以直接是字符串,也可以像'#child'这样标识一个dom节点。
ready和beforeDestroy是两个常用的生命周期,ready是在组件准备好时的一个回调,一般在这里我们可以使用获取数据、实例化第三方组件、绑定事件等,beforeDestroy正好相反,是在组件即将被销毁时触发回调,在这里我们销毁自定义的实例、解绑自定义事件、定时器等。
如何使用组件
组件一般是由它的父级来显示调用的,比如上面的child组件,我们就可以在父级中使用:
newVue({ data: { msg1:'Hello,TalkingCoder', msg2:'你好,TalkingCoder'}})
上例使用了两次child组件,使用props传递了一个参数msg,并且第二个组件的参数是双向绑定的,在双向绑定后,无论修改父级还是子元素的msg,双方的数据和view都会响应到,而单向绑定后,子组件修改是不会影响到父级的。
在渲染完,的内容就会替换为组件template内的字符串了,虽然使用的是同一个child组件,但是两次使用的作用域是独立的,这也是为什么在组件内data要使用function来返回一个对象的原因。
父子组件间的通信
在Vue.js中,父子之间的通信主要通过事件来完成,这种就是我们熟悉的观察者模式(或叫订阅-发布模式),很多框架也都使用了这种设计模式,比如Angular。父组件通过Vue内置的$broadcast()向下广播事件和传递数据,子组件通过$dispatch()向上派发事件和传递数据,双方都可以在events对象内接收自定义事件,并且处理各自的业务逻辑。
父组件使用了多个相同子组件,如何区分呢?比如我们上面的demo使用了两次child组件,但是如何来区分这两个呢,也就是说如果给child广播事件,如何给其中指定的一个广播呢,因为广播后,它俩都会接收到事件的。我们可以使用v-ref来标识组件:
newVue({ data: { msg1:'Hello,TalkingCoder', msg2:'你好,TalkingCoder'}, methods: { sendData:function(){this.$refs.child1.$emit('set-data', {});this.$refs.child2.$emit('set-data', {}); } }})
通过$refs就可以给指定的组件触发事件了,事实上,通过$refs是可以获取到子组件的整个实例的。
子组件派发事件,而父组件仍然使用了多个相同子组件,如何区分是哪个组件派发的呢?还是上面的demo,比如我们的child组件$dispatch了一个自定义事件,可以这样来区分:
newVue({ data: { msg1:'Hello,TalkingCoder', msg2:'你好,TalkingCoder'}, methods: { sendData:function(){this.$refs.child1.$emit('set-data', {});this.$refs.child2.$emit('set-data', {}); }, handler1:function(){// ...}, handler2:function(){// ...} }})
像绑定DOM2事件一样,使用@xxx或v-bind:xxx来绑定自定义事件,来执行不同的方法。
内容分发slot
有时候我们编写一个可复用的组件时,比如下面的一个confirm确认框:
标题、关闭按钮是统一的,但是中间正文的内容(包括样式)是想自定义的,这时候就会用到Vue组件的slot来分发内容。比如子组件的template的内容为:
提示
确定取消父组件这样调用子组件:
欢迎来到TalkingCoder
最终渲染完的内容为:
提示
欢迎来到TalkingCoder
确定取消编写可复用组件
这里引用一段来自vue.js文档的内容:
在编写组件时,记住是否要复用组件有好处。一次性组件跟其它组件紧密耦合没关系,但是可复用组件应当定义一个清晰的公开接口。
Vue.js 组件 API 来自三部分——prop,事件和 slot:
prop允许外部环境传递数据给组件;
事件允许组件触发外部环境的 action;
slot允许外部环境插入内容到组件的视图结构内。
使用v-bind和v-on的简写语法,模板的缩进清楚且简洁:
Hello!
路由、组件和组件化
上文说了那么多,现在终于到重点了。在上一篇文章中,我们简单的提到了组件化,这也是将Vue使用到极致的必经之路。我们先看一下src/main.js文件。
Vue有点像Express的用法,也有中间件的概念,比如我们用到的vue-router,还有vuex,它们都是vue的中间件,当然我们自己也可以开发基于vue的中间件。
importVuefrom'vue';importVueRouterfrom'vue-router';importAppfrom'components/app.vue';importEnvfrom'./config/env';Vue.use(VueRouter);// 开启debug模式Vue.config.debug =true;// 路由配置varrouter =newVueRouter({ history: Env !='production'});router.map({'/index': { name:'index', component:function(resolve){require(['./routers/index.vue'], resolve); } }});router.beforeEach(function(){window.scrollTo(0,0);});router.afterEach(function(transition){});router.redirect({'*':"/index"});router.start(App,'#app');
以上代码就是main.js的内容,这也是我们项目跑起来后第一个执行的js文件。在导入了Vue和VueRouter模块后,使用Vue.use(VueRouter)安装路由模块。路由可以做一些全局配置,具体可以查看文档,这里只说一个就是history,上文已经介绍了关于HTML5的History,它用history.pushState()和history.replaceState()来管理历史记录,服务器需要正确的配置,否则可能会404。开启后地址栏会像一般网站那样使用“/”来分割,比“#”要优雅很多,可以看到我们通过环境模块env.js默认给开发环境开启了History模式路由,生产环境没有开启,为的是可以让大家来体验到这两者的差异性,使用者可以自己来修改配置。
导入的app.vue模块就是我们的入口组件了,上篇文章已经介绍过,我们通过webpack生成的index.html里,body内只有一个挂载节点
,当我们通过执行router.start(App, '#app')后,app.vue组件就会挂载到#app内了,所以app.vue组件也是我们工程起来后,第一个被调用的组件,可以在它里面完成一些全局性的操作,比如获取登录信息啊,统计日活啊等等。在app.vue内,有一个的自定义组件,它就是整个网站的路由挂载节点了,切换路由时,它的内容会动态的切换,其实是在动态的切换不同的组件,得益于webpack,路由间的切换可以是异步按需加载。
router.map()就是设置路由匹配规则,比如访问127.0.0.1:8080/index,就会匹配到"/index",然后通过component,在回调里使用require()异步加载组件,这个过程是可以改为同步的,不过应该没有人会这么做。vue-router支持匹配带参数的路由,比如'/user/:id'可以匹配到'/user/123',或者'/user/*any/bar'可以匹配到'/user/a/b/bar',:id是参数,*any是全匹配,不过vue-router支持的路由规则还是比较弱的,一般后端框架,比如Python的Tornado或者Node.js的Express是支持正则的。
vue的路由只是动态的调用组件,根本上还是MVVM,而Angular的路由是MVC的,在ng的controller里,可以使用templateURL来使用一个html片段,而vue的组件是不支持这种模式的,必须把html字符串写(或编译)在template里,因为在Vue的设计里,一个组件(.vue文件)是应该把它的样式、html和js紧耦合的,这正是组件化的魅力所在。
嵌套路由。vue-router是支持嵌套路由的,在app.vue里的是我们的根路由挂载,如果需要,可以在某个具体的路由组件里面再使用一个来分发二级路由。具体使用方法可查看文档。
路径跳转。vue-router使用v-link指令来跳转,它会隐式的在DOM上绑定点击事件:
首页首页
如果是在js里跳转,可以这样:
module.exports = { data:function(){return{ } }, methods: { go:function(){console.log(this.$route);console.log(this.$router);this.$router.go('/index'); } }}
使用vue内置的$router方法也可以跳转,如果感兴趣,可以试试上面$route和$router打印出什么内容,通过$route是可以得到当前路由的一些状态信息的,比如路径和参数。
vue-router还有一些钩子函数,通俗讲就是在发生一次路由时某个状态的一些回调。我们的项目main.js中使用了:
router.beforeEach(function(){window.scrollTo(0,0);});router.afterEach(function(transition){console.log(transition);});
beforeEach()是在路由切换开始时调用,这里我们将页面返回了顶端。
afterEach()是在路由成功切换到激活状态时调用,可以打印出transition看看里面都有什么。一般在这里可以做像自动导航、自动面包屑的一些全局工作。
router.redirect()很简单,就是重定向了,找不到路由时可以跳转到指定的路由。
小结
跟vue相关的组件化内容大概就是这么多了,说到底,vue的路由也是个组件,与普通组件并没有任何差异化,只是概念的不同。vue还有一些知识,比如自定义指令,自定义过滤器,这些原理也很类似,使用也很简单,大家可以参考项目中的demo,结合文档来学习使用。在下一篇中,将介绍一些开发中沉淀的技巧或使用经验。