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按钮、一个输入框,或者一个上传控件都可以封装为一个组件,在使用的时候,可能只用写一行<upload></upload>,就能实现文件上传功能,甚至可以支持拖拽上传、大小和格式限制等。那一个组件具体怎么开发呢,这就是本文后面重点讨论的内容了。

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: '<span>{{ msg }}</span>',
    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节点。

readybeforeDestroy是两个常用的生命周期,ready是在组件准备好时的一个回调,一般在这里我们可以使用获取数据、实例化第三方组件、绑定事件等,beforeDestroy正好相反,是在组件即将被销毁时触发回调,在这里我们销毁自定义的实例、解绑自定义事件、定时器等。

如何使用组件

组件一般是由它的父级来显示调用的,比如上面的child组件,我们就可以在父级中使用:

<child :msg="msg1"></child>
<child :msg.sync="msg2"></child>

new Vue({
    data: {
        msg1: 'Hello,TalkingCoder',
        msg2: '你好,TalkingCoder'
    }
})

上例使用了两次child组件,使用props传递了一个参数msg,并且第二个组件的参数是双向绑定的,在双向绑定后,无论修改父级还是子元素的msg,双方的数据和view都会响应到,而单向绑定后,子组件修改是不会影响到父级的。

在渲染完,<child>的内容就会替换为组件template内的字符串了,虽然使用的是同一个child组件,但是两次使用的作用域是独立的,这也是为什么在组件内data要使用function来返回一个对象的原因。

父子组件间的通信

在Vue.js中,父子之间的通信主要通过事件来完成,这种就是我们熟悉的观察者模式(或叫订阅-发布模式),很多框架也都使用了这种设计模式,比如Angular。父组件通过Vue内置的$broadcast()向下广播事件和传递数据,子组件通过$dispatch()向上派发事件和传递数据,双方都可以在events对象内接收自定义事件,并且处理各自的业务逻辑。

父组件使用了多个相同子组件,如何区分呢?比如我们上面的demo使用了两次child组件,但是如何来区分这两个呢,也就是说如果给child广播事件,如何给其中指定的一个广播呢,因为广播后,它俩都会接收到事件的。我们可以使用v-ref来标识组件:

<child :msg="msg1" v-ref:child1></child>
<child :msg.sync="msg2" v-ref:child2></child>

new Vue({
    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了一个自定义事件,可以这样来区分:

<child :msg="msg1" v-ref:child1 @child-event="handler1"></child>
<child :msg.sync="msg2" v-ref:child2 @child-event="handler2"></child>

new Vue({
    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的内容为:

<div>
    <h1>提示</h1>
    <slot name="content"></slot>
    <span>确定</span>
    <span>取消</span>
</div>

父组件这样调用子组件:

<confirm>
    <p slot="content">欢迎来到TalkingCoder</p>
</confirm>

最终渲染完的内容为:

<div>
    <h1>提示</h1>
    <p>欢迎来到TalkingCoder</p>
    <span>确定</span>
    <span>取消</span>
</div>

编写可复用组件

这里引用一段来自vue.js文档的内容:

在编写组件时,记住是否要复用组件有好处。一次性组件跟其它组件紧密耦合没关系,但是可复用组件应当定义一个清晰的公开接口。

Vue.js 组件 API 来自三部分——prop,事件和 slot:

  • prop 允许外部环境传递数据给组件;
  • 事件 允许组件触发外部环境的 action;
  • slot 允许外部环境插入内容到组件的视图结构内。

使用 v-bindv-on 的简写语法,模板的缩进清楚且简洁:

<my-component
  :foo="baz"
  :bar="qux"
  @event-a="doThis"
  @event-b="doThat">
  <!-- content -->
  <img slot="icon" src="">
  <p slot="main-text">Hello!</p>
</my-component>

路由、组件和组件化

上文说了那么多,现在终于到重点了。在上一篇文章中,我们简单的提到了组件化,这也是将Vue使用到极致的必经之路。我们先看一下src/main.js文件。

Vue有点像Express的用法,也有中间件的概念,比如我们用到的vue-router,还有vuex,它们都是vue的中间件,当然我们自己也可以开发基于vue的中间件。

import Vue from 'vue';
import VueRouter from 'vue-router';
import App from 'components/app.vue';
import Env from './config/env';

Vue.use(VueRouter);

// 开启debug模式
Vue.config.debug = true;

// 路由配置
var router = new VueRouter({
    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内只有一个挂载节点<div id="app"></div>,当我们通过执行router.start(App, '#app')后,app.vue组件就会挂载到#app内了,所以app.vue组件也是我们工程起来后,第一个被调用的组件,可以在它里面完成一些全局性的操作,比如获取登录信息啊,统计日活啊等等。

在app.vue内,有一个<router-view></router-view>的自定义组件,它就是整个网站的路由挂载节点了,切换路由时,它的内容会动态的切换,其实是在动态的切换不同的组件,得益于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里的<router-view>是我们的根路由挂载,如果需要,可以在某个具体的路由组件里面再使用一个<router-view>来分发二级路由。具体使用方法可查看文档

路径跳转。vue-router使用v-link指令来跳转,它会隐式的在DOM上绑定点击事件:

<a v-link="{path: '/index'}">首页</a>
<!--或者其他标签页可以-->
<p v-link="{path: '/index'}">首页</p>

如果是在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,结合文档来学习使用。在下一篇中,将介绍一些开发中沉淀的技巧或使用经验。

讨论区 (请尽量讨论和主题相关的话题)
##comment.user.name## (作者) ##comment.user.company + comment.user.job##
  • 删除
  • ##comment.comment.content##
    ##reply.reply.content##
  • 删除
  • 回复##comment.add_reply.user_name## (作者)
    发表回复

    添加标签,便于归类
    取消
    收藏
    写点推荐理由,选填
    取消
    推荐

    TalkingCoder

    寻找灵感 发现精彩

    登录 · 注册 · 忘记密码
    意见反馈