Vue+Webpack开发可复用的单页面富应用教程(组件篇)

本文首发于TalkingCoder,一个有逼格的程序员社区。转载请注明出处和作者。

写在前面

本文为系列文章,总共分四节,建议按顺序阅读:

《Vue+Webpack使用规范》

《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的网站以及基于ElectronIonic的桌面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,结合文档来学习使用。在下一篇中,将介绍一些开发中沉淀的技巧或使用经验。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,907评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,546评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,705评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,624评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,940评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,371评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,672评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,396评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,069评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,350评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,876评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,243评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,847评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,004评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,755评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,378评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,266评论 2 259

推荐阅读更多精彩内容