Vue 常见业务场景以及细节心得

6,175 阅读13分钟

前言

上一年末,公司需要开启新的业务线,并且是需要以新的前端框架技术 Vue,去完成这项工作。我很开心能做为其中的开拓者之一,在此之前,我并没有多少 Vue 的开发经验,也仅仅是停留在学习阶段,而且公司一直采用的前端框架还是 jQuery,所以公司也没 Vue 框架开发经验沉淀。外面的世界日新月异,对于新技术也是一直感兴趣,所以一直希望有机会能用上新的框架去开发项目。很开心能有这个机会,虽然任重而道远,一切从零开始摸索。  

但是,何为“开拓者”!这就是!


Vue 

Vue 是当今最火的前端框架之一,这就不用多说了。这里就只留两点建议给刚上手的童鞋: 

  1. 怎么去学习: 
    我建议可以直接去看 Vue 官方文档,官方文档写得挺好的,而且学习曲线还是挺平滑的。这也是 Vue 的一个优点。 
  2. 要注意什么:
    要理解其框架思想。上一代的以 jQuery 为代表的框架的思想是直接操作 DOM,更新视图。现在主流以 Vue、React 为代表的前端框架,最大的特点就是数据驱动,操作数据,视图更新的工作留给框架去做。两种思想是截然不同的,如果之前是接触 jQuery 比较多,这里就需要注意,不要被 jQuery 的开发思维给影响到,必要时可以将之前的思维抛弃掉,以新的思维方式去接受并且开发。 这里就不多说如何上手 Vue 了,直接去官方文档开启 Vue 的学习旅程吧。在这主要分享一下,在这个项目过程中,所遇到的典型业务场景,解决方案,还有一些心得,希望能对你们有所帮助。


常见的业务场景

1、全局注册与局部注册的选择

基于 Vue 的开发,接触最多的便是组件化开发了,这也是框架解决了的前端开发痛点之一。在 Vue 中,组件注册分为全局注册局部注册,两者的区别以及常见的业务场景有哪些呢:

  • 全局注册

    Vue.component('component-name', {
      // ... 选项 ...
    })

    一个网页,一般由基础功能块,再由盒子模型合理布局之后,便组成了我们常见的网页。这些基础性功能块,再由一些基础元素组成,比如 button 标签、input 标签等等。这些大家应该都了解。 在这基础上,整个页面基本遵守页面设计统一的原则,基础元素一般要设计统一,但又要在多个地方复用,所以此时就是全局组件的发挥之地。我们将多处需要复用的元素,封装成全局组件,功能与设计统一维护,而需要用到的地方,直接调用就好了,无需二次开发。这就是全局组件的便利之处。

  • 局部注册

    var ComponentA = { /* ... */ }
    new Vue({
      el: '#app',
      components: {
        'component-a': ComponentA
      }
    })
    

    一张网页,一般对应着一个 Vue 实例,也就是根组件,最外层的组件。在这基础上,内部我们一般会以功能划分为若干个模块,比如头部,内容区等等。这些模块,都是一个个独立的组件。这就是局部注册的业务场景,虽然牺牲一定复用性(但其在内部组件一样可以多处复用),但是可以独立维护内部功能以及独立的业务逻辑。这样剥离出来,代码以及业务逻辑都很清晰,而不是全部都冗杂在外层组件中。

所以在两者的选择上,全局组件更偏向于搭建基础组件,而局部组件偏向于业务模块,根据业务划分不同的组件。

总结:

  • 全局组件最大的特点就是复用,要发挥这个特点才注册成全局,不然会造成一定的浪费。最常见的业务场景就是基础组件,比如 buttoninput 等等。市面上也有很多不错的组件库,比如 iView
  • 局部组件最大的特点就是相关业务逻辑独立维护,降低耦合度以及提高业务的清晰度。


2、怎么去把握业务边界

上面提到,我们在组件化开发的时候,需要根据功能去划分模块。模块的划分,过细或者不够具体,都会在开发过程中造成一定的影响。所以,如何去划分模块,或者说如何把握好一个模块的功能边界,明确其业务范围,也是需要我们去注意的。通常,我们可以从以下几个问题去思考:  

  • 划分出来的模块,它需要做什么?不需要做什么?
  • 它向外暴露什么?同时向外接受什么?  

考虑好这几个问题之后,模块结构基本也清晰了。

  1. 这个模块需要做什么 ,不需要做什么
    首先,我们从这两个问题入手思考,这两个问题也是为了让我们确定这个模块的功能边界。比如,我们设计一个弹窗组件,我们从这两个问题出发思考,它需要做什么,弹出、显示内容、关闭,没了。显示什么内容呢,它不需要关心,这就是它不需要做的事情。从这两个问题出发,去把握好模块的功能边界,划分出来的模块结构也会比较清晰。  
  2. 模块向外暴露什么,向外接受什么
    这其实是考虑如何进行数据通信了。在 Vue 中,最常见的就是父子之间的通信了,父组件通过 props 传递数据给子组件,子组件通过 emit 触发自定义事件发送信息给父组件。这就是父子之间的通信过程。 
    但是有一个点容易被忽略,当 props 是一个引用类型的时候,这个时候传递就不是单向数据流了。因为子组件拿到的是一个引用,它可以修改这个引用对象上的值,父组件的数据也会随之相应地变化了,所以这就变成了双向数据流。
    这是不应该的,子组件不应该直接修改父组件的数据的,而且当你这么做的时候,Vue 也会提示你不要这样操作。所以在 props 中一般只传递基本类型的值,避免传递引用类型的值,同时最好在定义 props 的时候,写好类型和默认值,校验一次。
    但有些场景下,传递引用类型会方便一些。我个人分为两种情况:
    1. 当这个组件是全局复用时,比如 input 、button 等等这些全局复用的自定义组件,就只传递基本类型。
    2. 当这个组件是某个功能模块内部使用,比如局部注册的组件,因为它局限于某个模块下,并且数据量大的时候,传递引用类型就方便一点。但还是要记得不能直接修改引用对象的值。
      若需要有对这个引用某个值进行操作,可以将这个值赋予 data 或者 computed 属性,再去相对应的操作。(这也是为了将数据修改约束在顶层组件,这样做的好处是数据流清晰,只有一处修改数据的地方。出问题追溯数据也比较好追。因为框架是基于数据驱动,要面对的问题都是因为数据,所以要对数据的结构、来源,去向以及修改做好约束,或者管理好数据。) 

这是向外接受要思考的方向。 向外暴露呢,或者说向外提供什么呢?

组件化开发,内部高度自治,所以一个逻辑到了某个组件,组件内部处理完之后,需要向外部告知。此时需要注意一个原则:

应该向外告知,我发生了什么,而不能是,你去干什么? 

可能有点拗口,但是你仔细思考一下就理解了,后者其实是决定了外部行为,这样就存在了耦合,这样的设计是不合理的。前者只是告知外部它自己发生了什么,它不管外部是根据这个信息,进行什么响应,这样的设计才是合理的。虽然这句话听起来,看似很相似。


3、数据配置

前面也提到,对数据的结构、来源,去向以及修改做好约束,或者管理好数据。因为数据驱动,我们要高度关注数据,合理设计数据结构以及管理数据。数据配置是我在项目中用得最多的一种数据管理方式,其实在很多框架细节上,随处可见。

什么是数据配置

举个栗子,最常见的业务场景,后台管理。


大致的代码结构如下:

<div id="manage" class="manage">
    <div class="header"></div>
    <div class="main">
        <div class="sidebar">
	    <ul class="menu" v-for="menu in menuDef" :key="menu.type">
	        <div class="menu-title">{{ menu.name }}</div>
		<li class="sub-menu" v-for="submenu in menu.submenu" :key="submenu.type">{{ submenu.name }}</li>
	    </ul>
	</div>
	<div class="content">内容区</div>
    </div>
</div>

<script>
    // 二级菜单
    var subServer = [
        { type: 'manage', name: '管理服务', extClass: 'server-manage' },
        { type: 'list', name: '服务列表', extClass: 'server-list' },
    ];
    // 二级菜单
    var subNews = [
        { type: 'internation', name: '国际新闻', extClass: 'i-news' },
        { type: 'home', name: '国内新闻', extClass: 'home-news' },
    ];
    // 一级菜单
    var menu = [
        { type: 'server', name: '服务', extClass: 'server', submenu: subServer },
        { type: 'news', name: '新闻', extClass: 'news', submenu: subNews }
    ];
    // 根实例
    var manage = new Vue({
        el: '#manage',
        data: {
            menuDef: menu
        }
    });
</script>

这是一个简单的后台管理页面,左侧面板有一系列的菜单项,右侧内容区基于左侧菜单切换而变化。左侧菜单结构一致,只不过是数据不同而已。在这个例子也可以看出:

  • 页面结构与数据的关系。页面结构只是数据的外层表现而已,并且是基于数据结构去变化的
  • 所以再抽象一点,将数据从页面结构抽象出来,便就是这个页面所需要的数据结构了。
  • 如果我们将剥离出来的数据结构,单独维护,这便是我所说的数据配置了。

数据配置有什么好处呢?

  • 只关注数据结构,不用关注页面结构,即使增删菜单项、或者修改结构层级,我也只需在数据这一层,进行调整就好了,不需要再进入逻辑层或者页面层进行变动。 
  • 这种方式对于后期维护相当友好。不管是扩展还是维护,都是相当的便利的。记得在我这个项目开发过程中,后台管理页面有过一定的变动,在 PM 跟我讲完需要变动的地方的时候,我也基本都改好了...因为我只需更换数据层就好了,就比方我修改一个配置文档而已,只要文档标注写清楚了,谁来改都是一样的便利。 

适合的场景:

  • 相似的结构,只是数据有所不同。 
  • 这适合的场景也是比较广泛的,基本也可以搭配组件复用去实现的。  

上面说到,右侧内容区是基于左侧菜单切换而变化的。这个实现是基于路由实现的,Vue 也提供了 vue-router 官方路由库,具体的文档也是可以直接去看官方文档。在这里想是,对于路由的管理,同样也可以运用数据配置的方式,统一在一处管理路由配置项,还可以与其他数据结构嵌套使用。比如接上上面的例子:

// 路由配置
// compons.serviceManage、compons.serviceList 就是内容区的内容组件
var serviceRoutes = {
    serviceManage: { path: '/serviceManage', name: 'serviceManage', component: compons.serviceManage },
    serviceList: { path: '/serviceList', name: 'serviceList', component: compons.serviceList}
};
// ...

// 菜单项配置
// 二级菜单
var subServer = [
    { type: 'manage', name: '管理服务', extClass: 'server-manage', router: serviceRoutes.serviceManage },
    { type: 'list', name: '服务列表', extClass: 'server-list', router: serviceRoutes.serviceList },
];
// ....

// 如何使用
// 上面配置的路由配置对象全部放进来,下面会循环创建路由对象。
var routerList = [serviceRoutes, articleRoutes, ...];
// 路由配置,循环路由配置
var router = [];
routerList.forEach(router => {
   router.push(router); 
});

// 根实例
var manage = new Vue({
    router,				// 将配置好的路由对象传进来
    el: '#manage',
    data: {
        menuDef: menu
    }
});

后期在维护这一块的时候,只需要对数据结构的配置以及路由的配置维护就好了。


4、如何优雅地搭配 jQuery 使用

在 Vue 的开发过程中,大多时候并不需要你去亲自去操作 DOM,你只要关注数据层,通过数据驱动,由框架去完成视图的更新。

但在某些特殊场景下,你切确需要去进行一些底层的 DOM 操作,比如 hover 某个模块出现 tip,或者 hover 某个结构出现工具栏。这种业务场景,你可能想到的是,在模块内部处理 mouseenter 和 mouseleave 事件的时候,进行这块逻辑的处理。这是比较典型的 JQuery 思维去处理这个问题,这样处理方案有很多缺点:

  • 无法复用,你所做的处理,只是限定在某个模块下的,别的地方无法直接复用。 
  • 耦合高,因为这个业务本身就可以剥离出去的,成为一个单独的组件维护了。如果还夹杂在其他模块内部,会造成模块内部逻辑以及结构不清晰。 
  • 后期维护难。

如果是用 Vue 的方式去实现呢?

Vue 提供了一个自定义指令 (directive) 的接口给我们,这种方式就很好地解决了这个业务场景下的问题。在 Vue 里面,它不建议你直接在实例内部中进行一些 DOM 操作,如果你需要进行一些底层 DOM 操作,你可以将它们抽离到自定义指令中来。而在需要用到的时候,我直接使用就好了,完美解决上面的处理方案存在的问题。

// jQuery 的方式,错误的方式
<div id='app' @mouseenter='handleMouseEnter' @mouseleave='handleMouseLeave'></div>
<div id='tip'></div>
var app = new Vue({
    el: '#app',
    data: {},
    methods: {
        handleMouseEnter: function () {
            document.querySelector('#tip').style.display = 'block';
        },
        handleMouseLeave: function () {
            document.querySelector('#tip').style.display = 'none';
        }
    }
});

// Vue 的方式,正确的选择
<div id='app' v-tip='{ text: tipText }'></div>

Vue.directive('tip', {
    inserted: function(el, binding) {
	var tipDom = document.querySelector('#tip');
	if(tipDom) {
            // 如果已经存在,不需要创建 DOM
            // do something
	} else {
	    // 如果不存在,创建 DOM
            tipDom = document.createElement('div');
            tipDom.setAttribute('id', 'tip');
            document.body.appendChild(tip);
            // do something
	}
	// 为绑定该指令的元素注册事件
	el.addEventListener('mouseenter', function() {
            // do something
	});
	el.addEventListener('mouseleave', function() {
            // do something
	});
    }
});

var app = new Vue({
    el: '#app',
    data: {
        tipText: 'Hello, world!'
    },
});

这便是这种业务场景下,Vue 的优雅实现。很好地将业务逻辑剥离出来成自定义指令,一处实现,多处复用。代码逻辑清晰,耦合度低,后期好维护等等优点铺面而来。简直如浴春风啊~

具体相关的自定义指令的知识点以及运用,大家可以去查看自定义指令的官方文档。


结尾

在开发过程中,还有很多细节一点的场景,比如如何结合 Vue 优雅地实现动画,父子通信,兄弟通信需要注意的细节等等。这些细节就相对琐碎了一点,一一讲起来篇幅也会太长了。后期有读者遇到相关问题,也可以再另起一篇幅,再给大家唠嗑唠嗑~

这篇就到这了。

Hello,world!


知乎:陈永森