Update 2019/12/12
组件编写规范
前言
随着js编程进入工程化纪元,代码模块化,组件化,成为工程的具体落地方法。
最近使用vue全家桶做了一个类似于iconfont的网站,在做的过程中关于组件有了一些思考和总结,为了巩固,写个小结。
一 组件类型
根据我已知的组件的编写方式,有四种介绍给大家:
1.1 基本组件
最基本的组件由一个.vue文件构成,其中包含了基本的tempplate,script, style三大元素。
可以通过props接收参数,$emit事件bus向父传递参数。
比较懒得我一般会在dev阶段在components文件夹里放置一个Demo.vue。写好基本的元素以及组件注释。
import后在组件的components中声明就可以在模板里标签调用了。
<template>
<!-- 指南中推荐组件名应该始终为多个单词的,根组件App,<transition>、<component>之类除外 -->
<Header></Header>
<shop-cart></shop-cart>
</template>
components: { Header, ShopCart }
这里比较注意的是组件命名规范应该严格参考vue风格指南。让驼峰和短横线在该使用的地方使用。
1.2 构造组件
构造类组件主要是用在想要通过js动态调用组件的场景里,由一个该组件文件夹比如notice,中包含Notice.vue, index.js两个文件组成。
demo-notice
DemoNotice.vue
index.js
vue文件就是编写基本的vue文件
index.js中
- 使用vue的extend方法来返回一个Notice.vue的构造器对象,在对象原型上添加一些常用方法比如close
- 编写Notice函数对象,在函数中将构造器对象进行实例化(最基本的:创造一个div元素挂在el属性上,你也可以根据场景需求为其配置上store,router,通过.$store获取)
- 将该实例的el属性上的元素append到body(或者你想要的添加的元素)中
- 将函数传来的参数数据处理后对实例中数据进行赋值(等于props传值,同时此处相当于做了每次打开组件时的周期。)
- 将这个函数对象作为default返回
/** 项目添加模态框 */
import Vue from 'vue'
import proModal from './ProModal.vue'
import store from '@/store/index'
import router from '@/router/index'
let proModalInstance
export default {
open (modalType, packageInfoDatail) {
if (!proModalInstance) {
proModalInstance = new ProModalCreater({
el: document.createElement('div'),
store: store,
router: router
})
document.body.appendChild(proModalInstance.$el)
}
proModalInstance.$data.modalType = modalType
proModalInstance.$data.options4 = []
proModalInstance.visible = true
modalType === 'modify'
? (() => {
proModalInstance.$data.packageInfoDetailTemp = packageInfoDatail
proModalInstance.$data.headerName = '修改项目'
proModalInstance.$data.value9 = []
packageInfoDatail.coUsers.map((item) => {
proModalInstance.$data.value9.push(item)
proModalInstance.$data.options4.push(item)
})
})()
: proModalInstance.init()
if (modalType === 'create' && packageInfoDatail) {
proModalInstance.fromPath = packageInfoDatail
}
// proModalInstance.$data.callback = callback
}
}
调用:
- 可以通过在main.js中调用然后将其挂载在Vue原型上再去组件this中调用方式调用
Vue.prototype._message = obj => {
obj.duration = messageDurationTime
ElementUI.Message(obj)
}
this._message.info('why so serious?')
- 也可以在不同组件中引入然后再去调用
import ProModal from '@/components/common/proModal'
methods: {
createPro () {
// 打开模态框
ProModal.open('create')
},
}
1.3 全局组件
全局组件主要是用在一些通用组件想要在各处组件里使用标签调用的场景,文件结构与构建组件相同。
vue文件就是编写基本的vue文件。(该注意的点还是多注意下,全局对通用,颗粒的要求更高一些)
index.js中
- 引入组件ButtonComponent并且编写一个组件对象用于导出,该组件对象中有一个install方法
- install方法中核心:Vue.component("Button", ButtonComponent);
- 返回该组件对象
import NoContentComponent from './NoContent.vue'
// 添加install方法 (插件方法)
const NoContent = {
install: function (Vue) {
Vue.component('NoContent', NoContentComponent)
}
}
// 导出Buttonexport
default NoContent
在全局main.js中引入组件对象使用Vue.use()注册该组件
Vue.use(NoContent)
在各个组件中通过标签直接调用
<no-content v-if="!Array.isArray(iconLibNowDetail.icons)">
<template slot="default">
<span>还没有上传图标哦</span>
<upload-btn :toPath="'/upload?type=iconLib&id=' + libId"></upload-btn>
</template>
</no-content>
1.4 指令类组件
指令类主要是使用Vue.directive方法注册一个指令,在指令中bind的方法中去实例组件并添加到响应dom处。这里不多说,因为我没用到。(想看怎么用的去看参考Vue组件的三种调用方式)
其结构类似于全局组件,只是调用是通过在某个元素上的指令属性进行调用。
二 组件设计原则
好了,知道怎么写一个基本的组件了,路程刚刚开始,那么如何写好一个组件呢。如果你写的组件是为了目的而写不考虑设计原则,相信很快就会被你自己所抛弃,然后重构组件。(小伙伴说我这一节写的步骤有些乱不太好,后面会改进)
先说下我的组件设计思路:
- 思考组件类型:是基础组件,还是业务组件
- 思考组件核心:该组件核心是什么,核心代表着极低的变动,甚至不变
- 思考组件颗粒度(复用性): 该组件是基础组件会被多处复用 ?设计时尽量最小化,减少ui数据绑定使其灵活轻巧 : 业务组件则可以适当ui和数据绑定,只通过一部分传参进行update
- 思考组件插槽:插槽的存放位置,后备内容,数据等
- 思考组件数据传递(可配置性): 是否需要数据传递 ?是否会深层传参 ?采用vuex :注意props数据不要被组件内直接改变,$emit向上传递数据 :无数据传递,仅作为ui层复用
- 组件通信这里可以再更多考虑,局部组件处嵌套通信是否可以使用bus来通信,深层传参是否使用inject来传入,包括是否使用this.$refs.parent.xxData来获取数据。这些都需要去考虑,当然一定不要被这些便利的属性触角诱惑而不去使用props/$emit,这样只会使组件数据逻辑变得不易阅读和查找。
- 思考组件数据健康(内存管理,watcher下尽量扁平数据存放,动态请求刷新)
- 思考组件性能(优化,提取,销毁)
Update 2019/12/12
组件编写规范
1. 属性顺序:
一个持久化的健康组件,必然是遵循了统一风格来编写的,所以一个组件的内部编写规范非常的重要,先分享下我的组件内部属性编写顺序
<template>
<header v-show="headerShown">
<header-select
source="demo"
v-if="true"
v-show="true"
v-once="false"
v-modal=""
@change=""
// v-html=""
isDemo="">
</header-select>
</header>
</template>
<script>
import HeaderSelect from '@/components/header-select'
export {
name: 'demo-header',
components: { // 模板依赖(组件内使用的资源)
HeaderSelect
},
extends: {}, //组合
mixins: {}, // 组合
props: { // 组件接口
headerShown: {
type: Boolean,
default: true
}
},
data () {
return {
}
},
computed: {},
watch: {},
methods: {},
// 生命周期
beforeCreated () {
},
created () {},
...
}
</script>
2. 组件通信单向流
为了保持组件间的纯净性,以及对数据流向的清晰,我们尽量避免使用 instance.$parent[xxChildCoponentsName] 来获取其他组件的数据并修改。
从这个角度我们再返回看看前面的组件设计思路, 就可以得知一个顶层模块组件中所拥有的一些子级组件,这个要求我们从源头做起,从一开始就要画好代码设计流程图,一定要清楚模块之间的流向以及数据的流向,将一个需求进行拆分,解析出数据和内容的层级,并将其清晰的做出分配以及指向,并且要考虑到两者的边界情况以及可能拥有的复杂场景。这种在下面的复杂组件设计中尤为重要。
3. 合理且全面的出入口注释设计
假设你要给一个项目写一个公用的 CommonTable.vue ,那在做好了其层级定位及核心内容数据的解析后,对出入口数据的设计就显得极为重要了,别人可以不关注你内部的 data 或是变量,但是对于要调用的组件的自定义特性(Prop),别人一定是需要知道这个组件都有哪些可用的特性的。所以,假如你没有文档(谁会专门为了个项目去写组件文档呢),那么在script最前端的组件注释就是组件最好的文档。(改天放一个组件特性注释模板上来)
三 复杂组件设计
这一节起的有点节题党,这里只是分享一点点点总结,不敢谈设计,后续有好的总结会更新上来,大佬们轻拍哈哈
组件在一些场景中会变得复杂,其复杂性分为组件本身的复杂和被应用场景的复杂。
本身比较复杂的组件如dataTimerPicker,Form之类的拥有复杂的配置项属性以及methods,events,并且其中会包含子组件,子组件的slot,attributes,methods也会很多。
这里仅仅分享三个场景:
1. 我在做一个IconList,当时想要一次性在组件中全部做好,就不想再多抽出子组件,结果发现会写的很冗余。于是很快就抽出了icon组件,IconList接受icons数组数据,在v-for中将item对象再传入icon组件,icon中通过props拿到iconData,最终用来渲染出最终显示的icon。
当然还可以再对icon进行子组件抽取的,我目前还是采用配置参数iconModalType方式+v-if来区分项目中和图标库内在不同权限下,三种modal层的显示。后面可以继续使用mixIn(IconModal.vue中使用)+调用时<icon-modal :modalType="icon.modalType">方式来加载。
组合大于继承,不用一次性去写一个庞然大物,这样会在复用的时候束手束脚失去灵活性。
2. 我在做一个FilterGroup,确定了其核心是InputSearch结果到了后面发现还是会遇到核心不确定场景。于是这个时候FilterGroup只为ui服务,子元素可以通过slot来加载,slot中通过name将子组件放置到对应位置。下面是个demo,我当时就将InputSearch和Sort只作为了FilterGroup中的元素来处理了,导致了后续需要通过很长的配置项在group上去确定groupElement上的属性,使Group本身属性被混淆减弱了自身含义,增加了复用时理解难度。
<!-- group合理设计应该为使用slot插入需要的groupElement而不是直接去集成div,即使是非常固有化的元素,如其中的两个元素:排序和搜索元素 -->
<btn-group inputPlaceVal="请搜索图标" :useSort="true" @sort="proSort" :sortOptions="options" @search="searchIcon" >
<template slot="body">
<div class="radius-btn" v-if="_isProManager() >= 2" @click="createPro">
<img :src="iconAddLib" alt="">
</div>
</template>
</btn-group>
正确的是应该去考衡抽出的必要性,假如该元素不复杂不需要传递数据自身无复杂方法,固定为ui项,就可以通过配置属性来取决其是否显示,比如element的input的icon,使用prefix-icon
和 suffix-icon
属性在 input 组件首部和尾部增加显示图标,当然在句尾说清了:也可以通过 slot 来放置图标 。因为slot伴随着子组件的编写,也是有代价的,所以对于一些默认最小项,个人认为可以内置编写好,通过配置属性来显示。
当然,可以看到,slot对于一个组件是多么的重要,一个复杂的子组件,必定不是一次性在组件内写好的,而是经过提取优化思考得到的一个组合结果,并且经得起复用和灵活扩展。
3. 我在做一个BatchOperate,批量操作组件时发现自己需要通过获得兄弟组件iconlist中被批量选择的icon的id数据,并且将这些icon加入ShopCart,这个时候对于数据以及状态的考量就来了。
最后选用了vuex加LocalStorage方式来解决了这个数据通信的问题。(有点晚了,先不细说...)
这几个场景事最想表达的是:
在模块化思想的今天,这种抽出组合的思维是很灵活的,可以在写组件的时候多去思考下模块化,包括公共方法,公共样式,vue的mixIn,scss的mixIn,公共变量仓库store集中处理,组件最下化,让重复性代码模块化抽出,再通过配置灵活组合。这种做法可以让代码在后续的维护中非常清爽,一键修改专制各种公共改动,再也不用跑到各个地方去做一下下的修改,并且避免了耦合带来的深层代码缠绕,可以说真的很好用了。
今天先写到这,第三节总结的不是很好,只是先罗列出自己之前想到的一些问题和处理,具体的优化想法会和组内小伙伴讨论后再更新的。
Update 2019/12/12
组件模块化到底如何去做,我目前是这样做的,首先你要有清晰度的代码目录层级,比如
page // 特性页
page // 专门放置一级页面
market-demo
compontens
data.js
market-child
components
market-child-tree.vue
market-child.vue
market-select.vue
market-modal.vue
market-demo.vue
components // 公共组件
components
demo-button
demo-button.vue
demo-total
demo-total.vue
hhhh,其他的都下次更新上去,没带写的总结