今天要分享的是模块化与组件化,两个非常重要,想要用好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的发展,让组件化的思维可以实现并在目前的前端开发中大展拳脚,这也正是我们需要学习的开发思维之一。
基础概念的理解我想并不会太难,但是考验一个前端开发的功底的是,你是否能够合理的对一个页面进行组件划分。希望大家能够在以后的学习与实践中,不停的去思考这个问题,锻炼自己这方面的能力,合理的划分意味着你的代码具备更高的可读性,可维护性,以及更高的性能,这些都是评判你的代码是否优秀的重要标准。而这些,都是慢慢沉淀出来的。