浅谈前端组件化

6,546 阅读7分钟

背景

不知道你有没有遇到过以下场景:

  • 页面逻辑越来越多,代码越来越庞大,写到后面难以 hold 住所有逻辑,很容易牵一发而动全身.
  • 你负责的页面好好的突然出现问题,查到最后是别人代码影响.
  • 同样的逻辑在多个地方重复书写,每次一改要改一批文件

随着前端项目复杂度的急剧增加, 上面列出的几种场景就是传统开发中会出现的问题. 也是前端组件化出现的原因.

  • 项目复杂度增加, 一个页面一个文件需要处理的内容过多.
  • 重复性劳动多, 效率低
  • 质量差, 不可控

组件化初探

正是由于出现了这样的问题, 为了在越来越复杂的前端项目中提高开发效率和保证开发质量, 各路大神们开始通过各种方式来尝试解决问题.

曾经非常火的 jQuery 就基于自己建立了 jQuery 插件机制. 你可以将一些常用逻辑进行封装变成 jQuery 插件, 还可以将插件开源进行共享. 比如纯手写会吐血的日期时间选择器,轮播, 多级菜单等等. 你可以在这里浏览更多jQuery插件.

jQuery 插件的用法通常是:

$(".select").pluginName(config)

除了 jQuery 插件模式, 还有一种常见模式是对象模式. 这种模式现在仍然有很多优秀的库在被我们直接或者间接使用. 比如: swiper

对象模式的写法:

<!-- Slider main container -->
<div class="swiper-container">
    <!-- Additional required wrapper -->
    <div class="swiper-wrapper">
        <!-- Slides -->
        <div class="swiper-slide">Slide 1</div>
        <div class="swiper-slide">Slide 2</div>
        <div class="swiper-slide">Slide 3</div>
        ...
    </div>
</div>

<script src="path/to/swiper.min.js"></script>
<script>
  var mySwiper = new Swiper ('.swiper-container', {
    direction: 'vertical',
    loop: true,
  })
  </script>

对象模式通过配置创建对象, 通常创建对象的参数中有一项是元素/元素选择器, 通过js代码将逻辑与这个元素紧密绑定.

除了各路大神提出这这些封装代码的模式, 前端标准也在探索组件化模式, webComponent 是 W3C 提出的一种自定义标签模式, 但是其实现繁琐, 目前支持度并不是很好, 这里就不详细说明. 详细的实现示例参看这里

现代组件结构

经过前端工程师不断的探索, 目前组件化的思想已经在前端项目开发中获得广泛的使用, 最流行的前端框架 Vue React 已经是组件化的集大成者 (虽然二者内涵的东西不止是组件化). 我们先来看一下我们希望组件是一个怎样的存在.

首先, 组件必须是高内聚低耦合的. 当我们开始用组件的思想看待页面时, 页面便不再是 dom 元素的组合, 而是不同组件的组合. 但是由于前端基础技术栈的原因, html css js 运行在一个页面上时是没有隔离的, 也就是说 js 可以根据选择器获取到任意的 dom 节点, 一条 css 规则也会应用在文档中所以满足规则的节点, js 代码中可以随意的创建和使用全局变量. 所以解决隔离是组件的基础要求.

其次, 组件是具有一定意义的功能集合体. 通常组件可以有自己的名字, 比如, 轮播组件, 表格组件, 分页组件, 导航组件. 有名字代表我们把它看成一个个体, 能完成这个个体需要完成的功能. 当然这个个体是可以有层次的, 比如, 表格组件里面可能是表头组件和表主题组件的组合. 这就类似学生, 老师, 学校但是不会有把学生的一半跟老师一半加在一起的东西. 学生是可以完成独立功能的, 这样的抽象就有意义. 看到这里, 你可能发现这跟面向对象有一定的相似性. 我认为二者在思想上确实是相似的.

了解前端组件需要做到怎样的要求, 现在我们具体看下一个组件的结构:

UI

前端的工作是不可能与 UI 分开的, 绝大部分的组件是带着 UI 的. 当然也有一部分容器类组件并不需要 UI. 我们把 UI 主要分成两个部分, 内容和样式. 这样似乎与 HTML 和 CSS 分别对应上了, 那与传统的开发有什么区别呢? 区别就在于我们在上文提到的隔离. 受限于前端基础技术栈的原因, 并不能做到完全的隔离, (web component 是打算从基础的部分解决这个问题) 但是使用各种方式, 比如在 webpack 中做一些额外的处理, 帮助我们做到隔离, 通常按照标准的写法是不会对外部产生不可控的影响的.

举例来说: vue 的单文件组件, template 和 style 部分就对应了内容和样式, 当然描述为渲染和样式更为准确. template 限制了只能书写组件的内容, style 加上 scoped 配置. 如下图, 在渲染成具体的 dom 的时候, vue 会给对应元素加上唯一标志, 并在选择器中带上这个标志. 以这种方式来保证组件间样式的隔离.

attribute, property, status, method

这四个部分便于组件的功能逻辑紧密相关. 对于学习过面向对象设计的同学应该会很熟悉几个内容, attribute 与 property 是初始化时的配置部分, status 是运行时的内部状态变量, method 是提供的方法.

组件配置: 为什么会有 attribute 与 property 两个部分用来描述配置呢? 首先组件是对 HTML 元素的扩展, attribute 便是承接与 HTML 元素的设计. HTML 的元素在书写的时候可以添加 attribute, 但是受限于形式 attribute 仅是字符串格式, 因为我们是写在 html 文件中的. 但是我们都应该有经验, 对一个组件的配置不可能只有字符串的, 比如, 我们在使用 swiper 轮播组件时, 我们可以配置轮播动画的时长, 是否自动轮播等. 这时候我们往往会使用 js 的形式, 并传一个对象进去. 这种通过 js 来配置组件时传的值, 叫做 property. attribute 与 property 通常是有对应关系的, 我的建议是不需要太关心他们的区别, 比如, 我们现在使用的 vue 和 react 实际就抹平了二者的差别, 你可以说都是 property. 比如 vue 的模板语法中, :attr="object" 加了冒号的属性内容就可以被自动解析. jsx 语法中 attr={js} 也是不再限于字符串.

内部状态 status: 内部状态就是组件运行时, 不需要暴露给外部但是内部需要的变量. 这一定程度上可以看成我们写代码中的局部变量.

方法 method: 顾名思义方法就是组件提供的功能. 比如, 对于一个轮播组件会有的方法有: 停止/开始自动轮播, 轮播到某项等.

生命周期

生命周期通常有, 创建, 挂载, 销毁前等, vue 和 react 设计了更细粒度的生命周期.

为什么要有生命周期, 因为我们在设计好一个组件已经组件的功能时, 我们需要在一些特定的时候执行一些代码, 比如初始化动作, 获取数据动作等. 我们把做这些动作的时机整理后发现, 我们往往需要在创建的时候需要做一些动作, 在构建好组件的 dom 挂载到页面的时候需要做一些动作, 在销毁前需要做一些动作比如内存释放等. 因此这些时机也在现代框架中得到了标准的支持.

组件化对当前前端工作的影响

上面内容我们讨论了前端为什么会进行组件化, 已经现代的组件抽象模型是什么. 那学习和理解组件化对我们工作有什么影响呢?

首先, 组件化成为工作必备技能之一, 现代的前端开发工作是必须要求你会一种框架的, 三大框架之一. 而三大框架都深深的蕴含了组件化的思想.

其次, 理解组件化对于可以帮助我们更好的使用框架进行工作内容的拆分和维护. 如果你是团队的 leader, 如何更好的拆分项目的工作如何让团队成员更好的合作, 组件化能力是必须的.