React進(jìn)階(五)模塊化與組件化

R N G

今天要分享的是模塊化與組件化,兩個(gè)非常重要,想要用好React, 就必須要理解的概念。當(dāng)然,許多讀者朋友肯定已經(jīng)在很早以前就已經(jīng)接觸過(guò)這兩個(gè)概念,不過(guò)是否已經(jīng)真正理解了呢,我們可以借助下面兩個(gè)問(wèn)題考驗(yàn)一下自己:

  • 閉包與模塊的關(guān)系是什么
  • 模塊與組件之間的聯(lián)系是什么

第一個(gè)問(wèn)題是我在面試的時(shí)候必問(wèn)的問(wèn)題之一,這個(gè)問(wèn)題在很大程度上能夠反映出來(lái)對(duì)方的JS基礎(chǔ)掌握得是否扎實(shí)。不過(guò)如果大家讀過(guò)我之前寫(xiě)的《前端基礎(chǔ)進(jìn)階》,那么這個(gè)問(wèn)題應(yīng)該是沒(méi)有什么難度的。

當(dāng)我在面試中問(wèn)閉包時(shí),大家或多或少都能夠分享一些閉包的概念,但是進(jìn)一步再問(wèn)閉包在實(shí)踐中的應(yīng)用時(shí),大多數(shù)人都不知道應(yīng)該怎么說(shuō)。

在我以前的文章里有很詳細(xì)的描述,如果你對(duì)閉包的基礎(chǔ)概念還不清楚,建議回過(guò)頭去補(bǔ)充一些知識(shí)

模塊化的概念由來(lái)已久,并且在JS中也有很長(zhǎng)久的使用歷史。通常我們?cè)诰帉?xiě)代碼時(shí),會(huì)將復(fù)雜的問(wèn)題根據(jù)實(shí)際情況進(jìn)行合理的拆分,讓代碼更具備可讀性與可維護(hù)性。因此一個(gè)模塊可以理解為整體的一部分。而且隨著JS應(yīng)用復(fù)雜度的提高,模塊化的應(yīng)用也變成了必須。

在之前的JS中,沒(méi)有專門(mén)為模塊化提供相應(yīng)的語(yǔ)法支持,但好在我們還有閉包。因此以前我們借助自執(zhí)行函數(shù)來(lái)模擬一個(gè)模塊。

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

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

// 訪問(wèn)模塊內(nèi)部的方法
moduleDemo.bar();

bar,foo,map三個(gè)方法在函數(shù)內(nèi)部被定義,但是卻可以在外部使用。所以很簡(jiǎn)單就能看出,我們借助閉包實(shí)現(xiàn)了模塊。借助這樣的思路,我們可以封裝一些工具方法組成一個(gè)單獨(dú)的工具模塊,以避免代碼的重復(fù)編寫(xiě)。這樣的比較出名的實(shí)踐有 lodash, axios等。他們都是在實(shí)踐中用得比較多的工具模塊。

還可以看出,模塊化其實(shí)也是單例模式的一種實(shí)踐應(yīng)用

接下來(lái)我們要思考一個(gè)小小的實(shí)踐。使用原生的JS與html實(shí)現(xiàn)一個(gè)簡(jiǎn)單的選項(xiàng)卡。不知道大家腦袋里是否已經(jīng)有了具體的方案。

在上一章中,我們介紹了create-react-app,借助此工具,我會(huì)把這系列文章中所有涉及到的案例與實(shí)踐都集成在一個(gè)項(xiàng)目中,該項(xiàng)目的地址為 https://github.com/yangbo5207/react-advance

但是很顯然,我們這么多的demo,想要組合在一起,還是比較具備很強(qiáng)的復(fù)雜度,因此create-react-app提供的默認(rèn)配置無(wú)法滿足我們的開(kāi)發(fā)與學(xué)習(xí)的需要,所以要在默認(rèn)配置的基礎(chǔ)上,進(jìn)行一些改造與擴(kuò)展,并且隨著學(xué)習(xí)的深入,這套構(gòu)建工具將會(huì)組件新增更多的能力,這里也無(wú)需大家就一定要去深入學(xué)習(xí)webpack,我會(huì)將構(gòu)建工具的改動(dòng)歷史記錄在 http://www.itdecent.cn/p/0f56250a5f2b。我不會(huì)細(xì)說(shuō)我為什么要這樣改動(dòng)以及各種原理,只會(huì)簡(jiǎn)單記錄操作,以供大家在深入學(xué)習(xí)webpack時(shí)做參考使用。不過(guò)也建議大家跟著我的改動(dòng)操作一次,這對(duì)于理解組件化會(huì)有很大的幫助,后續(xù)的文章,也要求大家至少對(duì)我做了那些改動(dòng)有一個(gè)了解,不然可能會(huì)在某些描述上你會(huì)搞不清楚。

OK,多的不說(shuō),為了滿足這篇文章的需要,我暫時(shí)將我們的構(gòu)建工具新增了多頁(yè)面構(gòu)建。以后文章中的每一個(gè)demo,都會(huì)是一個(gè)單獨(dú)的頁(yè)面。demo的命名會(huì)類似于RA5_01.html, RA5_01.js, RA5_01.css。

  • RA 表示react-advance 的縮寫(xiě)
  • 5 表示系列第五章
  • 01, 02, 03 表示該章demo的序列

構(gòu)建工具初步改造完成之后,目錄結(jié)構(gòu)大致如下:

目錄結(jié)構(gòu)

所有頁(yè)面的入口html都放在public目錄中。
所有頁(yè)面的入口js都放在src目錄中。

每一個(gè)頁(yè)面的html文件與js文件的命名必須保持一致,例如RA5_01.html, RA5_01.js,這個(gè)規(guī)則由構(gòu)建工具制定。

在public中新建RA5_01.html,寫(xiě)入一下簡(jiǎn)單代碼:

<!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>使用原生方案實(shí)現(xiàn)選項(xiàng)卡</title>
</head>

<body>
  <div id="root">
    <div class="titles">
      <div class="item active" data-index="0">標(biāo)題1</div>
      <div class="item" data-index="1">標(biāo)題2</div>
      <div class="item" data-index="2">標(biāo)題3</div>
    </div>
    <div class="contents">
      <div class="item active">內(nèi)容1</div>
      <div class="item">內(nèi)容2</div>
      <div class="item">內(nèi)容3</div>
    </div>
  </div>
</body>

</html>

暫時(shí)先找個(gè)目錄src/styles用以存放css文件,新建一個(gè)RA5_01.css文件,將布局寫(xiě)好。

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;
  }
}

這樣,一個(gè)簡(jiǎn)單的選項(xiàng)卡功能就實(shí)現(xiàn)了。

相信這對(duì)于大家來(lái)說(shuō)沒(méi)有任何難度。不過(guò)我們來(lái)接著思考一個(gè)問(wèn)題。我們知道通常在一個(gè)頁(yè)面,選項(xiàng)卡的應(yīng)用其實(shí)很廣泛,如果每次用都這樣寫(xiě)一次,就感覺(jué)很麻煩,因此我們最好能夠?qū)⑦x項(xiàng)卡封裝成為一個(gè)模塊,那么我們下次用到的時(shí)候,就直接引入模塊就OK了可不可以呢?

我們用import引入的css文件,其實(shí)就已經(jīng)被構(gòu)建工具當(dāng)成了一個(gè)模塊來(lái)處理。

src/utils目錄下創(chuàng)建模塊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;
    }
  }
}

我們可以看出,該模塊提供了一個(gè)創(chuàng)建tab的方法createTab。這樣,我們就可以修改RA5_01.js,直接引入該方法創(chuàng)建tab了。

import './styles/RA5_01.css';

// 引入模塊時(shí)可省略.js后綴
// utils是在構(gòu)建工具中配置了別名,因此不用使用相對(duì)路徑來(lái)引入,構(gòu)建工具會(huì)自動(dòng)識(shí)別
import { createTab } from 'utils/RA5_tab';

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

請(qǐng)一定確保自己對(duì)ES6的語(yǔ)法已經(jīng)基本掌握,否則后續(xù)的文章可能會(huì)有一些難度。

是不是使用起來(lái)就簡(jiǎn)單了很多。

那么現(xiàn)在大家應(yīng)該對(duì)模塊的概念應(yīng)該比較清晰了。在webpack創(chuàng)建的構(gòu)建工具中,認(rèn)為一切文件都可以是一個(gè)單獨(dú)的模塊。一個(gè)js文件,一個(gè)css文件,甚至一張圖片,只需要進(jìn)行對(duì)應(yīng)的配置,都是模塊,可以使用ES6的 Modules語(yǔ)法引入。

當(dāng)然思考并沒(méi)有結(jié)束。想一想當(dāng)我們繼續(xù)要?jiǎng)?chuàng)建第二個(gè)新的tab時(shí),我們要做一些什么操作?

  • html中要新增一段符合要求的html
  • 引入對(duì)應(yīng)的css模塊
  • 引入對(duì)應(yīng)的js模塊

麻煩的地方就在這里,每次創(chuàng)建新的tab需要執(zhí)行很多操作,特別是html要整一段新的,就很容易出錯(cuò),時(shí)間久了忘記了就不知道應(yīng)該怎么用了。如果我們還需要引入圖片什么的就更麻煩了。

那么我們能不能只引入一個(gè)完整的東西,就能夠直接創(chuàng)建新的tab呢?當(dāng)然是可以的,這就是我們接下來(lái)要明白的另一個(gè)概念:組件。

在React中提供了組件化的思路,結(jié)合webpack,我們可以很完美的創(chuàng)建一個(gè)組件,并且在使用時(shí)只需要引入一次就可以了,和上面的方式相比,想一想都覺(jué)得方便了很多。

我們來(lái)試一下:

在public中創(chuàng)建 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>選項(xiàng)卡組件</title>
</head>

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

</html>

src/pages中創(chuàng)建一個(gè)Tab組件,該組件將會(huì)由css和一段包含html模板(jsx)的js文件組件,因?yàn)槲覀儠簳r(shí)還沒(méi)有學(xué)習(xí)到React的具體語(yǔ)法,因此暫時(shí)只需要感受一些這種方式即可。

/* 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;

這里我們自定義了一個(gè)Tab組件,在使用時(shí),只需要引入該組件即可。

/* 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相關(guān)的APIReactDOM.render將Tab組件渲染進(jìn)DOM結(jié)構(gòu)中。

  • 這里的Tab就是一個(gè)Tab組件
  • 引入之后,就可以在jsx模板中直接跟html標(biāo)簽一樣使用,<Tab />
  • 在使用時(shí),我們不再去關(guān)注html,css,js邏輯具體怎么實(shí)現(xiàn),只需要關(guān)注如何引入,需要傳入什么參數(shù)即可

由于前端頁(yè)面的特殊性,頁(yè)面上的一個(gè)元素,往往并不是由一個(gè)單一體,往往至少包含了html片段,css樣式,js邏輯,或者圖片等更多元素。因此組件化的概念非常適合前端開(kāi)發(fā),這也是React提倡的開(kāi)發(fā)思路之一。

如果善于總結(jié)的同學(xué),讀到這里,就可以看出,組件化其實(shí)是模塊化思路的一個(gè)延伸,一個(gè)組件由許多不同的模塊組成。得益于React與webpack的發(fā)展,讓組件化的思維可以實(shí)現(xiàn)并在目前的前端開(kāi)發(fā)中大展拳腳,這也正是我們需要學(xué)習(xí)的開(kāi)發(fā)思維之一。

基礎(chǔ)概念的理解我想并不會(huì)太難,但是考驗(yàn)一個(gè)前端開(kāi)發(fā)的功底的是,你是否能夠合理的對(duì)一個(gè)頁(yè)面進(jìn)行組件劃分。希望大家能夠在以后的學(xué)習(xí)與實(shí)踐中,不停的去思考這個(gè)問(wèn)題,鍛煉自己這方面的能力,合理的劃分意味著你的代碼具備更高的可讀性,可維護(hù)性,以及更高的性能,這些都是評(píng)判你的代碼是否優(yōu)秀的重要標(biāo)準(zhǔn)。而這些,都是慢慢沉淀出來(lái)的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容