组件化思想并非前端独有的,但倒是前端技术的延伸 任何软件开发过程,或多或少都有那么一些组件化的需求css
随着三大框架崛起,前端组件化逐渐成为前端开发的迫切需求,一种主流,一种共识,它不只提升开发效率,同时也下降了维组件内聚原则护成本 开发者们不须要再面对一堆晦涩难懂的代码,转而只须要关注以组件⽅式存在的代码⽚段 这是一场新的挑战!前端
面试官一般会问 写过前端通用组件吗?
复制代码
你可能会自信的表示: sure!vue
emm..是的吗?react
前端工程经历的三个阶段git
解决完开发效率,还须要兼顾运行性能, 故而选择某种构建工具,对代码进行压缩,校验,以后再以页面为单位进行简单的资源合并github
解决了基本开发效率和运行效率以后,开始考虑维护效率了web
分而治之(以分解下降复杂度)是软件工程中的重要思想,是复杂系统开发和维护的基石,模块化就是前端的分治手段面试
所以,模块化强调的是拆分,最大的价值就是分治,意味着无论你未来是否要复用这块儿代码,都有将他们拆成一个模块的理由编程
将一个大问题,不断的拆解为各个小问题进行分析研究,而后再组合到一块儿(分而治之原则)element-ui
无模块化->函数写法->对象写法->自执行函数->CommonJS/AMD/CMD->ES6 Module
复制代码
css模块化是在less,sass等预处理器的支持下实现的
复制代码
固然是不够的
模块化强调的是拆分,不管是从业务角度仍是从架构、技术角度,模块化首先意味着将代码、数据等内容按照其职责不一样分离
单纯的横向拆分业务功能模块有一些问题
随着业务发展,”过程线“也会愈来愈长,其余项目成员根据各自须要,在”过程线“ 加插各自逻辑,最终这个页面的逻辑变得难以维护
咱们须要摆脱【一泻而下】式的代码编写
复制代码
除了JS和CSS,界面也须要拆分,如何让模块化思想融入HTML语言
复制代码
在大肆宣扬组件化开发概念以前,也经历了寻求组件化最佳实践的阶段
落实到实际开发中像这样
咱们能够获取信息
tabContainer
,listContainer
和 imgsContainer
三个模块咦?嗅到一丝组件化的味道
历史总有遗🐖
早在N年前微软提出过一套解决方案,名为HTML Component
是一个比较完整的组件化方案了,但却没可以进入标准,默默地消失了,今天的角度来看,它能够说是生不逢时
当时”所谓的组件“
因而 W3C 按耐不住了,制定一个 WebComponents 标准,为组件化的将来指引了明路
大体四部分功能
<template>
定义组件的 HTML模板能力咱们思考一下,可行的实践化方案须要具有哪些能力
今天的前端生态里面 React,Angular和Vue三分天下,即便它们定位不一样,但核心的共同点就是提供了组件化的能力,算是目前是比较好的组件化实践
import PageContainer from './layout/PageContainer'
import PageFilter from './layout/PageFilter'
export default {
install(Vue) {
Vue.component('PageContainer', PageContainer)
Vue.component('PageFilter', PageFilter)
}
}
复制代码
还提供了SFC(Single File Component,单文件组件)‘.vue’文件格式
<template>
//...
</template>
<script>
export default {
data(){}
}
</script>
<style lang="scss">
//...
</style>
复制代码
class Tabs extends React.Component {
render() {
if (!this.props.items) {
console.error('Tabs中须要传入数据');
return null;
}
const propId = this.props.id;
return (
<ul className={this.props.className}>
<li>测试</li>
</ul>
);
}
}
复制代码
<input type="text" ng-model="firstname">
var app = angular.module('myApp', []);
app.controller('formCtrl', function($scope) {
$scope.firstname = "John";
});
复制代码
template
,style
,script
,script又能够由各类模块组成随着前端开发愈来愈复杂、对效率要求越来高,由项目级模块化开发,进一步提高到通用功能组件化开发,模块化是组件化的前提,组件化是模块化的演进
组件化方案下,咱们须要具备组件化设计思惟,它是一种【整理术】帮助咱们高效开发整合
任何一个组件都应该遵照一套标准,可使得不一样区域的开发人员据此标准开发出一套标准统一的组件
复制代码
描述了组件的细粒度,遵循单一职责原则,保持组件的纯粹性
属性配置等API对外开放,组件内部状态对外封闭,尽量的少与业务耦合
复制代码
UI差别,消化在组件内部(注意并非写一堆if/else)
输入输出友好,易用
复制代码
追求短小精悍
适用SPOT法则
Single Point Of Truth,就是尽可能不要重复代码,出自《The Art of Unix Programming》
复制代码
使用父组件的 state 控制子组件的状态而不是直接经过 ref 操做子组件
复制代码
设计不当致使环形依赖示意图
影响
组件间耦合度高,集成测试难 一处修改,到处影响,交付周期长 由于组件之间存在循环依赖,变成了“先有鸡仍是先有蛋”的问题
那假若咱们真的遇到了这种问题,就要考虑如何处理
消除环形依赖
咱们的追求是沿着逆向的依赖关系便可寻找到全部受影响的组件
建立一个共同依赖的新组件
- 组件的抽象程度与其稳定程度成正比,
- 一个稳定的组件应该是抽象的(逻辑无关的)
- 一个不稳定的组件应该是具体的(逻辑相关的)
- 为下降组件之间的耦合度,咱们要针对抽象组件编程,而不是针对业务实现编程
复制代码
若是一个数据能够由另外一个 state 变换获得,那么这个数据就不是一个 state,只须要写一个变换的处理函数,在 Vue 中可使用计算属性
若是一个数据是固定的,不会变化的常量,那么这个数据就如同 HTML 固定的站点标题同样,写死或做为全局配置属性等,不属于 state
若是兄弟组件拥有相同的 state,那么这个state 应该放到更高的层级,使用 props 传递到两个组件中
复制代码
父组件不依赖子组件,删除某个子组件不会形成功能异常
复制代码
除了数据,避免复杂的对象,尽可能只接收原始类型的值
复制代码
把组件内部能够完成的工做作到极致,虽然提倡拥抱变化,但接口不是越多越好
若是常量变为 props 能应对更多的场景,那么就能够做为 props,原有的常量可做为默认值。
若是须要为了某一调用者编写大量特定需求的代码,那么能够考虑经过扩展等方式构建一个新的组件。
保证组件的属性和事件足够的给大多数的组件使用。
复制代码
那有了组件设计的“API”,就必定能开发出高质量的组件吗?
组件最大的不稳定性来自于展示层,一个组件只作一件事,基于功能作好职责划分
根据经验,我将组件应分为如下几类
为了让开发者更关注业务逻辑,涌现出了不少优秀的UI组件库 好比antd
,element-ui
,咱们只须要调用API便能知足大部分的业务场景,前端角色后置了,开发变得更简单了
一个容器性质的组件,通常看成一个业务子模块的入口,好比一个路由指向的组件
DOM
标签<template>
<div class="purchase-box">
<!-- 面包屑导航 -->
<bread-crumbs />
<div class="scroll-content">
<!-- 搜索区域 -->
<Search v-show="toggleFilter" :form="form"/>
<!--展开收起区域-->
<Toggle :toggleFilter="toggleFilter"/>
<!-- 列表区域-->
<List :data="listData"/>
</div>
</template>
复制代码
主要表现为组件是怎样渲染的,就像一个简单的模版渲染过程
<template>
<div class="purchase-box">
<el-table
:data="data"
:class="{'is-empty': !data || data.length ==0 }"
>
<el-table-column
v-for = "(item, index) in listItemConfig"
:key="item + index"
:prop="item.prop"
:label="item.label"
:width="item.width ? item.width : ''"
:min-width="item.minWidth ? item.minWidth : ''"
:max-width="item.maxWidth ? item.maxWidth : ''">
</el-table-column>
<!-- 操做 -->
<el-table-column label="操做" align="right" width="60">
<template slot-scope="scope">
<slot :data="scope.row" name="listOption"></slot>
</template>
</el-table-column>
<!-- 列表为空 -->
<template slot="empty">
<common-empty />
</template>
</el-table>
</div>
</template>
<script>
export default {
props: {
listItemConfig:{ //列表项配置
type:Array,
default: () => {
return [{
prop:'sku_name',
label:'商品名称',
minWidth:200
},{
prop:'sku_code',
label:'SKU',
minWidth:120
},{
prop:'product_barcode',
label:'条形码',
minWidth:120
}]
}
}}
}
</script>
复制代码
一般是根据最小业务状态抽象而出,有些业务组件也具备必定的复用性,但大多数是一次性组件
能够在一个或多个APP内通用的组件
特色:复用性强,只经过 props、events 和 slots 等组件接口与外部通讯
<template>
<div class="empty">
<img src="/images/empty.png" alt>
<p>暂无数据</p>
</div>
</template>
复制代码
高阶组件能够看作是函数式编程中的组合 能够把高阶组件看作是一个函数,他接收一个组件做为参数,并返回一个功能加强的组件
高阶组件能够抽象组件公共功能的方法而不污染你自己的组件 好比 debounce
与 throttle
用一张图来表示
React中高阶组件是比较经常使用的组件封装形式,Vue官方内置了一个高阶组件keep-alive经过维护一个cache实现数据持久化,但并未推荐使用HOC :(
在 React 中写组件就是在写函数,函数拥有的功能组件都有
Vue更像是高度封装的函数,可以让你轻松的完成一些事情,但与高度的封装相对的就是损失必定的灵活,你须要按照必定规则才能使系统更好的运行
复制代码
品牌车系滑动的动画
对比图
既然如此,那我何时引入容器组件,何时引入展现组件
优先考虑展现组件,当你意识到有一些中间组件不使用它继承的props而是转而传递给他们的子级,每次子级组件须要更多数据时,你都须要从新调整这些中间组件,那么,这时候就要考虑引入容器组件
容器组件和展现组件的区别并无被严格定义,它们的区别不在技术上而是目的性上
用这种方式写组件,你能够更好的理解你的app和你的ui,甚至会逐渐造成你本身的开发套路
复制代码
一个组件只作一件事,解除了组件的耦合带来更高复用性
复制代码
因为展现组件和容器组件是经过prop接口来链接,能够利用props的校验机制来加强代码的可靠性,混合的组件就没有这种好处
举个🌰(Vue)
props: {
editData: Object,
statusConfig: {
type: Object,
default() {
return {
isShowOption: true, //是否有操做栏
isShowSaveBtn: false
};
}
}
}
复制代码
组件作的事情更少了,测试也会变得容易
容器组件不用关心UI的展现,只关心数据和更新
展现组件只是呈现传入的props,写单元测试的时候也很是容易mock数据层
复制代码
长远来看,利大于弊
物极必反,跃跃欲试前,思考如下几个问题以引导完善组件的设计
超过三层以后可见组件的数据传递的过程就会变得越复杂
复制代码
缩减组件依赖能够提升组件的可复用度
较常见的一种状况是:组件运行时对window对象添加resize监听事件以实现组件响应视窗尺寸变化事件,这种需求的更好替代方案是:组件提供刷新方法,由父组件实现调用
次优的方案是,当组件destroy前清理恢复
复制代码
须要考虑须要适用的不一样场景,在组件接口设计时进行必要的兼容
接口设计符合规范和大众习惯,尽可能让别人用起来简单易上手,易上手是指更符合直觉。
各组件以前以组合的关系互相配合,也是对功能需求的模块化抽象,当需求变化时能够将实现以模块粒度进行调整
上文提到的各类准则仅仅描述了一种开发理念,也能够认为是一种开发规范,假若你承认这规范,对它的分治策略产生了共鸣,那咱们就能够继续聊聊它的具体实现了
问本身一个问题
你心中的相对完美的组件是什么样子的?
明确你的组件划分依据,目前是两种
这是最容易想到的方法,当一个组件渲染了不少元素,就须要尝试分离这些组件的渲染逻辑 咱们以掘金页面为例
大致上看,能够分为Part1,Part2,Part3
<template>
<div id="app">
<div class="panel">
<div class="part1 left">
<!--内容-->
</div>
<div class="part1 right">
<!--内容-->
</div>
<div class="part1 right">
<!--内容-->
</div>
</div>
</template>
复制代码
问题:
<template>
<div id="app">
<part1 />
<part2 />
<part3 />
</div>
</template>
复制代码
好处:
问题:
但我看过不少项目的代码,就是这么干的,认为本身作了组件化,抽象的还不错(@_@)
它们有类似的外层,part2和part3更有类似的titlebar,除了业务内容,彻底就是如出一辙
🌰(vue)
<template>
<div class="part">
<header>
<span>{{ title }}</span>
</header>
<slot name="content" />
</div>
</template>
复制代码
咱们将part内能够抽象的数据都作成了props,利用slot去作模版 那么咱们在开发相应Part1,Part2时
🌰(vue)
<template>
<div id="app">
<part title="亦舒">
<div slot="content">----</div>
</part>
<part title="兴隆臻园户型">
<div slot="content">-----</div>
</part>
</div>
</template>
复制代码
更具表明性的示例图
在业务逻辑层处理
首先要明确一点,这些差别并非组件自己形成的,是你本身的业务逻辑形成的,因此容器组件(父组件)应该为此买单
复制代码
结合组件自己和业务上下文将差别合理的消除在内部
好比part3中,其余的part只有一个相似更多>>的link,可是它却有多个(一居,二居...)
这里我推荐将这种差别体如今组件内部,设计方法也不少:
好比能够将link数组化为links;
好比能够将更多>>看做是一个default的link,而多余的部分则是用户自定义的特殊link,这二者合并组成了links。用户自定义的默认是没有的,须要引用组件时进行传入。
复制代码
组件设计初期,就应该拥有不耦合业务的名字
一个通用的或者说将来可能通用的,要有相对合理的命名,好比 Search,List,尽可能不要出现与业务耦合过深的业务名词,通用组件与业务无关,只与自身抽象的组件有关
咱们在设计组件初期,就应该有这种思想,等到真正能够抽出公用组件了,再去苦逼的名更名字?
库一般都想让广大开发者用,咱们在设计组件时,能够下降标准到先作到你的整个APP中通用
复制代码
组件设计规则明明白白写着咱们要遵循单一职责原则,这也带来了上文聊过的过分抽象(组件化)的问题,咱们结合具体的业务聊一下
要实现徽章组件,它有两部分组成
二者都是符合单一职责的,能够将其抽离成一个独立组件,可是一般不要这么作
由于同一个app的风格必将是统一的,除此以外没别的应用场景了,就像上文所说的,抽离组件以前,多问本身为何以及投入/产出比,没有绝对的规则
复制代码
单一职责组件要创建在可复用的基础上,对于不可复用的单⼀职责组件咱们仅仅做为独立组件的内部组件便可
思考,若是让你实现你会如何设计... 我当初是这么设计的
index.js(react)
<div className="select-brand-box" onTouchStart={touchStartHandler} onTouchMove={touchMoveHandler} onTouchEnd={touchEndHandler.bind(this, touchEndCallback)}>
<NavBar></NavBar>
<Brand key="brands-list" {...brandsProps} />
<Series key="series-list" {...seriesProps} >
</div>
export default BrandHoc(index);
复制代码
Brand.js(react)
<div className="brand-box">
<div className="brand-wrap" ref="brandWrap">
<p className="brands-title hot-brands-title">热门品牌</p>
<FlexLayout onClick={hotBrandClick}>
<HotBrands HotBrands={hotBrands} />
</FlexLayout>
{!isHideStar && <UnlimitType {...unlimitProps} />}
<AllBrands {...brandsProps} />
</div>
<AsideLetter {...asideProps} />
{showPop ? <PopTips key="pop-tips" tip={currentLetter} /> : null}
{showBrandLoading ? <Loading /> : null}
</div>
复制代码
FlexLayout.js(react)
这个示例几乎涵盖了全部的规则
<p className="brands-title hot-brands-title">热门品牌</p> 只有一行,直接写就完了
复制代码
组件的形态(UI)永远是变幻无穷的,可是其行为(逻辑)是固定的,所以通用组件的秘诀之⼀就是将DOM 结构的控制权交给开发者,组件只负责⾏为和最基本的DOM结构
这是一个显眼的栗子
某一天,你接到这样儿的需求
开心,简单,三下五除二写完了
忽然有一天又有这样儿的需求
emm..可定制?以前的select无法用了,怎么作?要修改上一个或者再写一个吗? 一旦出现了这种状况,证实以前的组件须要从新设计了
实现通用性设计的关键一点是放弃对Dom的掌控
通用性设计在将Dom结构决定权交给开发者的同时指定默认值
这里是一个新鲜出炉(vue)🌰
List组件
父组件🌰(vue)及slot
模版(伪代码)
<template>
<List :data="tableData[item.type]" :loading="loading" @loadMore="loadMore" :noMore="noMore">
<a v-if="item.type == 0" slot="listOption" slot-scope="childScope" class="edit-btn" @click="edit(childScope.data)" v-bind:key="childScope.data.id">{{Status[childScope.data.status]['text']}}</a>
</List>
</template>
config(伪代码)
export const Status = {
//....
1: {
label: '草稿',
type: '',
text: '编辑',
class: 'note'
}}
//...
复制代码
又有一个栗子(vue)
忍不住放上磐石业务的反面例子
难用无非是两方面的问题
全部的业务逻辑与场景都包含在组件内部,外界只经过变量来控制,初衷是好的,可是随着业务发展,组件愈来愈庞大,开发者也愈来愈力不从心了
恰好现阶段UI改版,咱们的工做量就由只改样式直接转化为推倒重来了,又没有详细的文档,工做量瞬间翻了N倍😭宝宝内心苦宝宝不说
其实一开始,我并无专门去套用设计模式,彻底是业务驱使 你必定见到过这样儿的
一旦这样儿的逻辑多了,那是否是就跟业务耦合了,跟业务耦合多了,那组件天然没有什么通用性了,即便咱们不考虑到通用性,那写的累吧?
考虑下这样写会不会好一点
config(伪代码)
export const Status = {
4: {
label: '部分入库',
type: '',
text: '查看'
}
}
模版(vue)
<a v-if="item.type == 0" slot="listOption" slot-scope="childScope" class="edit-btn" @click="edit(childScope.data)" v-bind:key="childScope.data.id">{{Status[childScope.data.status]['text']}}</a>
复制代码
世界上本没有设计模式,写的人多了,就自成一套脱颖而出进而被历史铭记了!不只如此,一部分看似复杂的业务若是合理设计配置项,能够会为你省去一大篇js
像磐石这种底层的业务支持系统,离不开大量的列表,查询,编辑,详情等,我通常会花30秒搭好架子,像但不限于下面这种
1. Form:表单 通常会被add.vue(编辑) 和edit.vue(详情)引用
2. List:列表
3. Search: 搜索组件
4. 其余业务中有但却没看到的基本上都已经抽离到common了 好比面包屑导航,收起展开功能等
复制代码
采购模块结构图
form
edit
不管有多少种状态,只在edit这层容器维护
最后说一句
组件化没有终点,day day up