前言
上一年末,公司需要开启新的业务线,并且是需要以新的前端框架技术 Vue,去完成这项工作。我很开心能做为其中的开拓者之一,在此之前,我并没有多少 Vue 的开发经验,也仅仅是停留在学习阶段,而且公司一直采用的前端框架还是 jQuery,所以公司也没 Vue 框架开发经验沉淀。外面的世界日新月异,对于新技术也是一直感兴趣,所以一直希望有机会能用上新的框架去开发项目。很开心能有这个机会,虽然任重而道远,一切从零开始摸索。
但是,何为“开拓者”!这就是!
Vue
Vue 是当今最火的前端框架之一,这就不用多说了。这里就只留两点建议给刚上手的童鞋:
- 怎么去学习:
我建议可以直接去看 Vue 官方文档,官方文档写得挺好的,而且学习曲线还是挺平滑的。这也是 Vue 的一个优点。 - 要注意什么:
要理解其框架思想。上一代的以 jQuery 为代表的框架的思想是直接操作 DOM,更新视图。现在主流以 Vue、React 为代表的前端框架,最大的特点就是数据驱动,操作数据,视图更新的工作留给框架去做。两种思想是截然不同的,如果之前是接触 jQuery 比较多,这里就需要注意,不要被 jQuery 的开发思维给影响到,必要时可以将之前的思维抛弃掉,以新的思维方式去接受并且开发。 这里就不多说如何上手 Vue 了,直接去官方文档开启 Vue 的学习旅程吧。在这主要分享一下,在这个项目过程中,所遇到的典型业务场景,解决方案,还有一些心得,希望能对你们有所帮助。
常见的业务场景
1、全局注册与局部注册的选择
基于 Vue 的开发,接触最多的便是组件化开发了,这也是框架解决了的前端开发痛点之一。在 Vue 中,组件注册分为全局注册和局部注册,两者的区别以及常见的业务场景有哪些呢:
- 全局注册
一个网页,一般由基础功能块,再由盒子模型合理布局之后,便组成了我们常见的网页。这些基础性功能块,再由一些基础元素组成,比如Vue.component('component-name', { // ... 选项 ... })复制代码
button
标签、input
标签等等。这些大家应该都了解。 在这基础上,整个页面基本遵守页面设计统一的原则,基础元素一般要设计统一,但又要在多个地方复用,所以此时就是全局组件的发挥之地。我们将多处需要复用的元素,封装成全局组件,功能与设计统一维护,而需要用到的地方,直接调用就好了,无需二次开发。这就是全局组件的便利之处。 - 局部注册
一张网页,一般对应着一个 Vue 实例,也就是根组件,最外层的组件。在这基础上,内部我们一般会以功能划分为若干个模块,比如头部,内容区等等。这些模块,都是一个个独立的组件。这就是局部注册的业务场景,虽然牺牲一定复用性(但其在内部组件一样可以多处复用),但是可以独立维护内部功能以及独立的业务逻辑。这样剥离出来,代码以及业务逻辑都很清晰,而不是全部都冗杂在外层组件中。var ComponentA = { /* ... */ } new Vue({ el: '#app', components: { 'component-a': ComponentA } }) 复制代码
所以在两者的选择上,全局组件更偏向于搭建基础组件,而局部组件偏向于业务模块,根据业务划分不同的组件。
总结:
- 全局组件最大的特点就是复用,要发挥这个特点才注册成全局,不然会造成一定的浪费。最常见的业务场景就是基础组件,比如
button
、input
等等。市面上也有很多不错的组件库,比如 iView 。 - 局部组件最大的特点就是相关业务逻辑独立维护,降低耦合度以及提高业务的清晰度。
2、怎么去把握业务边界
上面提到,我们在组件化开发的时候,需要根据功能去划分模块。模块的划分,过细或者不够具体,都会在开发过程中造成一定的影响。所以,如何去划分模块,或者说如何把握好一个模块的功能边界,明确其业务范围,也是需要我们去注意的。通常,我们可以从以下几个问题去思考:
- 划分出来的模块,它需要做什么?不需要做什么?
- 它向外暴露什么?同时向外接受什么?
考虑好这几个问题之后,模块结构基本也清晰了。
- 这个模块需要做什么 ,不需要做什么
首先,我们从这两个问题入手思考,这两个问题也是为了让我们确定这个模块的功能边界。比如,我们设计一个弹窗组件,我们从这两个问题出发思考,它需要做什么,弹出、显示内容、关闭,没了。显示什么内容呢,它不需要关心,这就是它不需要做的事情。从这两个问题出发,去把握好模块的功能边界,划分出来的模块结构也会比较清晰。 - 模块向外暴露什么,向外接受什么
这其实是考虑如何进行数据通信了。在 Vue 中,最常见的就是父子之间的通信了,父组件通过 props 传递数据给子组件,子组件通过 emit 触发自定义事件发送信息给父组件。这就是父子之间的通信过程。
但是有一个点容易被忽略,当 props 是一个引用类型的时候,这个时候传递就不是单向数据流了。因为子组件拿到的是一个引用,它可以修改这个引用对象上的值,父组件的数据也会随之相应地变化了,所以这就变成了双向数据流。
这是不应该的,子组件不应该直接修改父组件的数据的,而且当你这么做的时候,Vue 也会提示你不要这样操作。所以在 props 中一般只传递基本类型的值,避免传递引用类型的值,同时最好在定义 props 的时候,写好类型和默认值,校验一次。
但有些场景下,传递引用类型会方便一些。我个人分为两种情况:
- 当这个组件是全局复用时,比如 input 、button 等等这些全局复用的自定义组件,就只传递基本类型。
- 当这个组件是某个功能模块内部使用,比如局部注册的组件,因为它局限于某个模块下,并且数据量大的时候,传递引用类型就方便一点。但还是要记得不能直接修改引用对象的值。
若需要有对这个引用某个值进行操作,可以将这个值赋予 data 或者 computed 属性,再去相对应的操作。(这也是为了将数据修改约束在顶层组件,这样做的好处是数据流清晰,只有一处修改数据的地方。出问题追溯数据也比较好追。因为框架是基于数据驱动,要面对的问题都是因为数据,所以要对数据的结构、来源,去向以及修改做好约束,或者管理好数据。)
这是向外接受要思考的方向。 向外暴露呢,或者说向外提供什么呢?
组件化开发,内部高度自治,所以一个逻辑到了某个组件,组件内部处理完之后,需要向外部告知。此时需要注意一个原则:
应该向外告知,我发生了什么,而不能是,你去干什么?
可能有点拗口,但是你仔细思考一下就理解了,后者其实是决定了外部行为,这样就存在了耦合,这样的设计是不合理的。前者只是告知外部它自己发生了什么,它不管外部是根据这个信息,进行什么响应,这样的设计才是合理的。虽然这句话听起来,看似很相似。
3、数据配置
前面也提到,对数据的结构、来源,去向以及修改做好约束,或者管理好数据。因为数据驱动,我们要高度关注数据,合理设计数据结构以及管理数据。数据配置是我在项目中用得最多的一种数据管理方式,其实在很多框架细节上,随处可见。
什么是数据配置?
举个栗子,最常见的业务场景,后台管理。
大致的代码结构如下:
"manage" class="manage">
"header">
"main">
"sidebar">
"menu" v-for="menu in menuDef" :key="menu.type">
"menu-title">{{ menu.name }}
- "sub-menu" v-for="submenu in menu.submenu" :key="submenu.type">{{ submenu.name }}
"content">内容区
复制代码
这是一个简单的后台管理页面,左侧面板有一系列的菜单项,右侧内容区基于左侧菜单切换而变化。左侧菜单结构一致,只不过是数据不同而已。在这个例子也可以看出:
- 页面结构与数据的关系。页面结构只是数据的外层表现而已,并且是基于数据结构去变化的
- 所以再抽象一点,将数据从页面结构抽象出来,便就是这个页面所需要的数据结构了。
- 如果我们将剥离出来的数据结构,单独维护,这便是我所说的数据配置了。
数据配置有什么好处呢?
- 只关注数据结构,不用关注页面结构,即使增删菜单项、或者修改结构层级,我也只需在数据这一层,进行调整就好了,不需要再进入逻辑层或者页面层进行变动。
- 这种方式对于后期维护相当友好。不管是扩展还是维护,都是相当的便利的。记得在我这个项目开发过程中,后台管理页面有过一定的变动,在 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 的方式,错误的方式
'app' @mouseenter='handleMouseEnter' @mouseleave='handleMouseLeave'>
'tip'>
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 的方式,正确的选择
'app' v-tip='{ text: tipText }'>
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!
知乎:陈永森