React进阶(五)模块化与组件化

R N G

今天要分享的是模块化与组件化,两个非常重要,想要用好React, 就必须要理解的概念。当然,许多读者朋友肯定已经在很早以前就已经接触过这两个概念,不过是否已经真正理解了呢,我们可以借助下面两个问题考验一下自己:

  • 闭包与模块的关系是什么
  • 模块与组件之间的联系是什么

第一个问题是我在面试的时候必问的问题之一,这个问题在很大程度上能够反映出来对方的JS基础掌握得是否扎实。不过如果大家读过我之前写的《前端基础进阶》,那么这个问题应该是没有什么难度的。

当我在面试中问闭包时,大家或多或少都能够分享一些闭包的概念,但是进一步再问闭包在实践中的应用时,大多数人都不知道应该怎么说。

在我以前的文章里有很详细的描述,如果你对闭包的基础概念还不清楚,建议回过头去补充一些知识

模块化的概念由来已久,并且在JS中也有很长久的使用历史。通常我们在编写代码时,会将复杂的问题根据实际情况进行合理的拆分,让代码更具备可读性与可维护性。因此一个模块可以理解为整体的一部分。而且随着JS应用复杂度的提高,模块化的应用也变成了必须。

在之前的JS中,没有专门为模块化提供相应的语法支持,但好在我们还有闭包。因此以前我们借助自执行函数来模拟一个模块。

var moduleDemo = (function() {
  function bar() {}
  function foo() {}
  function map() {}

  return {
    bar: bar,
    foo: foo,
    map: map
  }
})();

// 访问模块内部的方法
moduleDemo.bar();

bar,foo,map三个方法在函数内部被定义,但是却可以在外部使用。所以很简单就能看出,我们借助闭包实现了模块。借助这样的思路,我们可以封装一些工具方法组成一个单独的工具模块,以避免代码的重复编写。这样的比较出名的实践有 lodash, axios等。他们都是在实践中用得比较多的工具模块。

还可以看出,模块化其实也是单例模式的一种实践应用

接下来我们要思考一个小小的实践。使用原生的JS与html实现一个简单的选项卡。不知道大家脑袋里是否已经有了具体的方案。

在上一章中,我们介绍了create-react-app,借助此工具,我会把这系列文章中所有涉及到的案例与实践都集成在一个项目中,该项目的地址为 https://github.com/yangbo5207/react-advance

但是很显然,我们这么多的demo,想要组合在一起,还是比较具备很强的复杂度,因此create-react-app提供的默认配置无法满足我们的开发与学习的需要,所以要在默认配置的基础上,进行一些改造与扩展,并且随着学习的深入,这套构建工具将会组件新增更多的能力,这里也无需大家就一定要去深入学习webpack,我会将构建工具的改动历史记录在 https://www.jianshu.com/p/0f56250a5f2b。我不会细说我为什么要这样改动以及各种原理,只会简单记录操作,以供大家在深入学习webpack时做参考使用。不过也建议大家跟着我的改动操作一次,这对于理解组件化会有很大的帮助,后续的文章,也要求大家至少对我做了那些改动有一个了解,不然可能会在某些描述上你会搞不清楚。

OK,多的不说,为了满足这篇文章的需要,我暂时将我们的构建工具新增了多页面构建。以后文章中的每一个demo,都会是一个单独的页面。demo的命名会类似于RA5_01.html, RA5_01.js, RA5_01.css

  • RA 表示react-advance 的缩写
  • 5 表示系列第五章
  • 01, 02, 03 表示该章demo的序列

构建工具初步改造完成之后,目录结构大致如下:

目录结构

所有页面的入口html都放在public目录中。
所有页面的入口js都放在src目录中。

每一个页面的html文件与js文件的命名必须保持一致,例如RA5_01.html, RA5_01.js,这个规则由构建工具制定。

在public中新建RA5_01.html,写入一下简单代码:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <meta name="theme-color" content="#000000">
  <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
  <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
  <title>使用原生方案实现选项卡</title>
</head>

<body>
  <div id="root">
    <div class="titles">
      <div class="item active" data-index="0">标题1</div>
      <div class="item" data-index="1">标题2</div>
      <div class="item" data-index="2">标题3</div>
    </div>
    <div class="contents">
      <div class="item active">内容1</div>
      <div class="item">内容2</div>
      <div class="item">内容3</div>
    </div>
  </div>
</body>

</html>

暂时先找个目录src/styles用以存放css文件,新建一个RA5_01.css文件,将布局写好。

body {
  margin: 0;
}

html, body {
  height: 100%;
}

#root {
  width: 300px;
  margin: 20px auto;
  border: 1px solid #CCC;
  height: 400px;
}

.titles {
  display: flex;
  height: 44px;
  border-bottom: 1px solid #CCC;
}

.titles .item {
  flex: 1;
  height: 100%;
  text-align: center;
  line-height: 44px;
  font-size: 12px;
}

.titles .item.active {
  background-color: orange;
  color: #FFF;
}

.contents {
  position: relative;
  height: 100%;
}

.contents .item {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  display: none;
  align-items: center;
  justify-content: center;
}

.contents .item.active {
  display: flex;
}
样式

最后在src目录中新建RA5_01.js

import './styles/RA5_01.css';

const titles = document.querySelector('.titles');
const contents = document.querySelector('.contents');

let index = 0;

titles.onclick = (event) => {
  const activeTitle =  event.target;
  const aindex = Number(activeTitle.dataset.index);
  
  if (aindex !== index) {
    titles.children[index].classList.remove('active');
    contents.children[index].classList.remove('active');
    
    activeTitle.classList.add('active');
    contents.children[aindex].classList.add('active');
    index = aindex;
  }
}

这样,一个简单的选项卡功能就实现了。

相信这对于大家来说没有任何难度。不过我们来接着思考一个问题。我们知道通常在一个页面,选项卡的应用其实很广泛,如果每次用都这样写一次,就感觉很麻烦,因此我们最好能够将选项卡封装成为一个模块,那么我们下次用到的时候,就直接引入模块就OK了可不可以呢?

我们用import引入的css文件,其实就已经被构建工具当成了一个模块来处理。

src/utils目录下创建模块RA05_tab.js

export const createTab = (containerElement) => {
  const titles = containerElement.querySelector('.titles');
  const contents = containerElement.querySelector('.contents');

  let index = 0;

  titles.onclick = (event) => {
    const activeTitle = event.target;
    const aindex = Number(activeTitle.dataset.index);

    if (aindex !== index) {
      titles.children[index].classList.remove('active');
      contents.children[index].classList.remove('active');

      activeTitle.classList.add('active');
      contents.children[aindex].classList.add('active');
      index = aindex;
    }
  }
}

我们可以看出,该模块提供了一个创建tab的方法createTab。这样,我们就可以修改RA5_01.js,直接引入该方法创建tab了。

import './styles/RA5_01.css';

// 引入模块时可省略.js后缀
// utils是在构建工具中配置了别名,因此不用使用相对路径来引入,构建工具会自动识别
import { createTab } from 'utils/RA5_tab';

createTab(document.querySelector('#root'));

请一定确保自己对ES6的语法已经基本掌握,否则后续的文章可能会有一些难度。

是不是使用起来就简单了很多。

那么现在大家应该对模块的概念应该比较清晰了。在webpack创建的构建工具中,认为一切文件都可以是一个单独的模块。一个js文件,一个css文件,甚至一张图片,只需要进行对应的配置,都是模块,可以使用ES6的 Modules语法引入。

当然思考并没有结束。想一想当我们继续要创建第二个新的tab时,我们要做一些什么操作?

  • html中要新增一段符合要求的html
  • 引入对应的css模块
  • 引入对应的js模块

麻烦的地方就在这里,每次创建新的tab需要执行很多操作,特别是html要整一段新的,就很容易出错,时间久了忘记了就不知道应该怎么用了。如果我们还需要引入图片什么的就更麻烦了。

那么我们能不能只引入一个完整的东西,就能够直接创建新的tab呢?当然是可以的,这就是我们接下来要明白的另一个概念:组件。

在React中提供了组件化的思路,结合webpack,我们可以很完美的创建一个组件,并且在使用时只需要引入一次就可以了,和上面的方式相比,想一想都觉得方便了很多。

我们来试一下:

在public中创建 RA5_02.html

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <meta name="theme-color" content="#000000">
  <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
  <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
  <title>选项卡组件</title>
</head>

<body>
  <div id="root"></div>
</body>

</html>

src/pages中创建一个Tab组件,该组件将会由css和一段包含html模板(jsx)的js文件组件,因为我们暂时还没有学习到React的具体语法,因此暂时只需要感受一些这种方式即可。

/* src/pages/RA5_02/style.css */
body {
  margin: 0;
}

html, body {
  height: 100%;
}

#root {
  width: 300px;
  margin: 20px auto;
  border: 1px solid #CCC;
  height: 400px;
}

.titles {
  display: flex;
  height: 44px;
  border-bottom: 1px solid #CCC;
}

.titles .item {
  flex: 1;
  height: 100%;
  text-align: center;
  line-height: 44px;
  font-size: 12px;
}

.titles .item.active {
  background-color: orange;
  color: #FFF;
}

.contents {
  position: relative;
  height: 350px;
}

.contents .item {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  display: none;
  align-items: center;
  justify-content: center;
}

.contents .item.active {
  display: flex;
}
/* src/pages/RA5_02/index.js */
import React, { Component } from 'react';
import './style.css';

const defaultTabs = [{
  title: 'tab1',
  content: 'tab1'
}, {
  title: 'tab2',
  content: 'tab2'
}, {
  title: 'tab3',
  content: 'tab3'
}]

class Tab extends Component {
  state = {
    index: 0
  }

  static defaultProps = {
    tabs: defaultTabs
  }

  switchTab = (index) => {
    this.setState({
      index
    })
  }

  render() {
    const { tabs } = this.props;
    const { index } = this.state;

    return (
      <div className="tab_container">
        <div className="titles">
          {tabs.map((tab, m) => (
            <div 
              className={m === index ? 'item active' : 'item'} 
              key={m}
              onClick={() => this.switchTab(m)}
            >
              {tab.title}
            </div>
          ))}
        </div>

        <div className="contents">
          {tabs.map((tab, n) => (
            <div className={n === index ? 'item active' : 'item'} key={n}>{tab.content}</div>
          ))}
        </div>
      </div>
    );
  }
}

export default Tab;

这里我们自定义了一个Tab组件,在使用时,只需要引入该组件即可。

/* src/RA5_02.js */
import React from 'react';
import ReactDOM from 'react-dom';
import Tab from 'pages/RA5_02';

ReactDOM.render(<Tab />, document.getElementById('root'));

使用React相关的APIReactDOM.render将Tab组件渲染进DOM结构中。

  • 这里的Tab就是一个Tab组件
  • 引入之后,就可以在jsx模板中直接跟html标签一样使用,<Tab />
  • 在使用时,我们不再去关注html,css,js逻辑具体怎么实现,只需要关注如何引入,需要传入什么参数即可

由于前端页面的特殊性,页面上的一个元素,往往并不是由一个单一体,往往至少包含了html片段,css样式,js逻辑,或者图片等更多元素。因此组件化的概念非常适合前端开发,这也是React提倡的开发思路之一。

如果善于总结的同学,读到这里,就可以看出,组件化其实是模块化思路的一个延伸,一个组件由许多不同的模块组成。得益于React与webpack的发展,让组件化的思维可以实现并在目前的前端开发中大展拳脚,这也正是我们需要学习的开发思维之一。

基础概念的理解我想并不会太难,但是考验一个前端开发的功底的是,你是否能够合理的对一个页面进行组件划分。希望大家能够在以后的学习与实践中,不停的去思考这个问题,锻炼自己这方面的能力,合理的划分意味着你的代码具备更高的可读性,可维护性,以及更高的性能,这些都是评判你的代码是否优秀的重要标准。而这些,都是慢慢沉淀出来的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容