一、React简介
1.1 Virtual DOM
react 把真实DOM树换成JavaScript 对象树,也就是Virtual DOM:
App -change-> Virtual Dom -change-> DOM -事件触发-> Virtual DOM -事件触发-> App;
每次数据更新后,重新计算Virtual DOM, 并和上一次生成的Virtual DOM 作对比,对发生的部分做批量更新。
Tips: react 提供的shouldComponentUpdate生命周期回调来减少数据变化后不必要的Virtual DOM 对比过程,以保证性能。
1.2 JSX语法
即:JavaScript XML——一种在React组建内部构建标签的类XML语法。(增强React程序组件的可读性)
区别:
- 1、浏览器只能识别普通的js,普通的css,并不能识别scss,或者jsx(scss是css的拓展,jsx可以看做是js的拓展),所以webpack的作用是把scss转换为css,把jsx转换为浏览器可以识别的js,然后浏览器才能正常使用;
- 2、js就是本身并不支持react里面的jsx(也就是在js文件里面直接写html那种),现在他们可以直接写是因为编辑器可以选择语言的解析模式了(待会截图给你看),编辑器正确显示是因为 虽然是.js文件,编辑器用了.jsx的解析模式,所以显示正确
- 3…jsx文件会自动触发编辑器以jsx的模式解析当前的文件,所以可以更不会出错
JSX语法,像是在Javascript代码里直接写XML的语法,实质上这只是一个语法糖,每一个XML标签都会被JSX转换工具转换成纯Javascript代码,React 官方推荐使用JSX, 当然你想直接使用纯Javascript代码写也是可以的,只是使用JSX,组件的结构和组件之间的关系看上去更加清晰。
1.2.1 元素属性
- Boolean属性
省略Boolean
属性值会导致JSX认为bool值设为了true。要传false时,必须使用属性表达式。比如:disable、required、checked、和readOnly等。
1.3 无状态函数
使用无状态函数构建的组件称为无状态组件,无状态组件只传入props和context两个参数,它不存在state,也没有生命周期方法。
无状态组件它创建时始终保持了一个实例,避免了不必要的检查和内存分配,做到了内部优化。
1.4 React数据流
React中, 数据时自顶向下单向流动的,即父组件到子组件。
如果顶层组件初始化props,那么React 会向
下遍历整棵组件树,重新尝试渲染所有的子组件。而state只关心每个组件内部的状态,这些状态只能在组件内改变。把组件看成一个函数,那么他接受了props作为参数,内部由state作为函数的内部参数,返回一个Virtual DOM 的实现。
这里不得不提一下,编程中props的使用,需要合理得当,避免一些不必要的渲染或者是覆盖。
1.4.1 state
- MVC框架常见的状态管理:
Backbone: 将View 中与界面交互的状态解耦,一般放到Model中管理
- setState : 表现行为就是该组件会尝试重新渲染。
注意点:
- setState是一个异步的方法。
- 一个生命周期内所有的setState方法会合并操作。
// 这里认识到了一些新的写法
const currProps = this.props
let activeIndex = 0
if ('activeIndex' in currProups) {
activeIndex = currProps.activeIndex;
}
- Props :
props是properties的缩写。
- props 的传递过程。对于React组件来说是非常直观的。React的单项数据流,主要的流动管道就是props。props本身是不可变的。
- propTypes: 用于规范props的类型与必需的状态。如果组件定义了propTypes,那么我们开发环境下,就会对组件的props值的类型作检查,如果传入的props不能与之匹配,React将实时在控制台报warning。在生产环境下,不会进行减产。
- propTypes支持基本类型中,函数式propTypes.func, propTypes.bool, 因为function 和 boolean在JavaScript 里是关键字。
1.5 React生命周期
React生命周期分成两类:
- 当组件在挂载或卸载时;
- 当组件接收新的数据时,即组件更新时。
1.5.1 挂载或卸载过程
- 组件的挂载
import React, { Component, propTypes } from 'react';
class App extends Component {
static propTypes = {
// ...
}
static defaultProps = {
// ...
}
constructor(props) {
super(props);
this.state = {
// ...
}
}
componentWillMount() {
// ...
}
componentDidMount() {
// ...
}
render() {
return <div>this is a demo.</div>
}
}
propTypes 和 defaultProps 分别代表props类型检查和默认类型。这两个属性被声明成静态属性,意味着从类外面也可以访问他们: App.propTypes 和 App.defaultProps。
- 组件的卸载
只有componentWillUnmount 这一个卸载前状态:
在componentWillUnmount 方法中,我们常常会执行一些清理方法,例如事件回收或者清除定时器。
1.5.2 数据更新过程
更新过程指的是父组件乡下传递props或组件自升之星setState方法时发生的一系列更心动动作。
import React, { Component, PropTypes } from 'react';
class App extends Component {
componentWillReceiveProps(nextProps){
// this.setState({})
}
shouldComponentUpdate(nextProps, nextState) {
// return true;
}
componentWillUpdate(nextProps, nextState) {
// ...
}
render() {
return <div>this is a demo.</div>
}
}
如果自身的state更新了,那么会依次执行shouldComponentUpdate、componentWillUpdate、render 和 componentDidUpdate。
- shouldComponentUpdate 它接收需要更新的props和state,让开发者增加必要的条件判断,让其在需要时跟新,不需要时不更新。因此,当方法返回false的时候,组件不再向下执行生命周期方法。
shouldComponentUpdate 的本质时用来进行正确的组件渲染。默认情况下React会渲染所有的节点,因为shouldComponentUpdate默认返回true。正确的组件渲染从另一个意义上说,也是性能优化的手段之一。
无状态组件是没有生命周期的,所以他在渲染时,每次哦度会渲染,当然,我们可以选择引用Recompose库的pure方法:
const OptimizedComponent = pure(ExpensiveComponent);
// 事实上pure方法做的事就是将无状态组件转换成class语法加上PureRender后的组件。
1.5.3 整体流程
-
生命周期
-
createClass 和ES6 classes的区别
1.6 React 与DOM
从React 0.14 版本开始,React将React中涉及DOM操作的部分剥离开了,目的是为了抽象React, 同时适用于Web端和移动端。ReactDOM的关注点在DOM上,因此只适用于Web端。
- ReactDOM
- findDOMNode
DOMElement findDOMNode(ReactComponent component)
import React, { Component } from 'react'; import ReactDOM from 'react-dom';
class App extends Component {
componentDidMount() {
// this 为当前组件的实例
const dom = ReactDOM.findDOMNode(this);
}
render() {}
// 如果在 render 中返回 null,那么 findDOMNode 也返回 null。findDOMNode 只对已经挂载的组 件有效。
}
- render
为什么说只有在顶层组件我们才不得不使用 ReactDOM 呢?这是因为要把 React 渲染的
Virtual DOM 渲染到浏览器的 DOM 当中,就要使用 render 方法了:
ReactComponent render(
ReactElement element,
DOMElement container,
[function callback]
)
该方法把元素挂载到 container 中,并且返回 element 的实例(即 refs 引用)。当然,如果 是无状态组件,render 会返回 null。当组件装载完毕时,callback 就会被调用。
当组件在初次渲染之后再次更新时,React 不会把整个组件重新渲染一次,而会用它高效的 DOM diff 算法做局部的更新。这也是 React 最大的亮点之一!
- ReactDOM 的不稳定方法unmountComponentAtNode 方法来进行写在操作。
- Dialog组件、Portal组件
render: ReactMount._renderSubtreeIntoContainer(null, nextElement, container, callback)。
unstable_renderSubtreeIntoContainer: ReactMount._renderSubtreeIntoContainer(parentComponent,
nextElement, container, callback)。
这也说明了两者的区别在于是否传入父节点。
- refs
它是React组件中非常特殊的prop,可以附加到任何一个组件上,组件被调用时会新建一个该组件的实例,而refs就会指向这个实例。它可以是一个回调函数,这个回调函数会在组件挂载后立即执行。
import React, { Component } from 'react';
class App extends Component {
constructor(props){
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
if (this.myTextInput !== null) {
this.myTextInput.focus();
}
}
render() {
return (
<div>
<input type="text"
ref={(ref) => this.myTextInput = ref}
/>
<input type="button"
value="Focus the text input"
onClick={this.handleClick}
/>
</div>
);
}
}
- refs同样支持字符串。对DOM操作,不仅可以使用findDOMNode获得该组件DOM,还可以使用refs获得组件内部的DOM。
import React, { Component } from 'react'; import ReactDOM from 'react-dom';
class App extends Component {
componentDidMount() {
// myComp 是 Comp 的一个实例,因此需要用 findDOMNode 转换为相应的 DOM
const myComp = this.refs.myComp;
const dom = findDOMNode(myComp);
}
render() {
return (
<div>
<Comp ref="myComp" />
</div>
);
} }
findDOMNode 和 refs 都无法用于无状态组件中,原因在前面已经说过。无状
态组件挂载时只是方法调用,没有新建实例。
对于 React 组件来说,refs 会指向一个组件类的实例,所以可以调用该类定义的任何方法。 如果需要访问该组件的真实 DOM,可以用 ReactDOM.findDOMNode 来找到 DOM 节点,但我们并 不推荐这样做。因为这在大部分情况下都打破了封装性,而且通常都能用更清晰的办法在 React 中构建代码。
- React之外的DOM操作
例如Popup组件等
componentDidUpdate(prevProps, prevState) {
if (!this.state.isActive && prevState.isActive) {
document.removeEventListener('click', this.hidePopup);
}
if (this.state.isActive && !prevState.isActive) {
document.addEventListener('click', this.hidePopup);
}
}
componentWillUnmount() {
document.removeEventListener('click', this.hidePopup);
}
hidePopup(e) {
if (!this.isMounted()) {
return false;
}
const node = ReactDOM.findDOMNode(this);
const target = e.target || e.srcElement;
const isInside = node.contains(target);
if (this.state.isActive && !isInside) {
this.setState({
isActive: false,
});
}
}
1.7 组件化实例: Tabs组件
笔记:propTypes
class Tabs extends Component {
static propTypes = {
// 在主节点上增加可选 class
className: PropTypes.string,
// class 前缀
classPrefix: PropTypes.string,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]),
// 默认激活索引,组件内更新
defaultActiveIndex: PropTypes.number,
// 默认激活索引,组件外更新
activeIndex: PropTypes.number,
// 切换时回调函数
onChange: PropTypes.func,
};
}
// classnames 用于合并 class
const classes = classnames(className, 'ui-tabs');
// 利用 class 控制显示和隐藏
let classes = classnames({
[`${classPrefix}-tab`]: true,
[`${classPrefix}-active`]: activeIndex === order,
[`${classPrefix}-disabled`]: child.props.disabled,
});
二、漫谈React
2.1 事件系统
Virtual DOM 在内存中是以对象的行使存在,React基于Virtual DOM实现了一个SyntheticEvent(合成事件)
层,我们所定义的事件处理器会接收到一个SyntheticEvent对象的实例它完全符合 W3C 标准,不会存在任何 IE 标 准的兼容性问题。并且与原生的浏览器事件一样拥有同样的接口,同样支持事件的冒泡机制,我 们可以使用 stopPropagation()
和 preventDefault()
来中断它。
所有事件都自动绑定到最外层上。如果需要访问原生事件对象,可以使用 nativeEvent 属性。
2.1.1 合成事件的绑定方式
// JSX
<button onClick={this.handleClick}>Test</button>
// DOM0写法
<button onclick="handleClick()">Test</button>
2.1.2 合成事件的实现机制
- 事件委派
在使用 React 事件前,一定要熟悉它的事件代理机制。它并不会把事件处理函数直接绑定到 真实的节点上,而是把所有事件绑定到结构的最外层,使用一个统一的事件监听器,这个事件监 听器上维持了一个映射来保存所有组件内部的事件监听和处理函数。当组件挂载或卸载时,只是 在这个统一的事件监听器上插入或删除一些对象;当事件发生时,首先被这个统一的事件监听器 处理,然后在映射里找到真正的事件处理函数并调用。这样做简化了事件处理和回收机制,效率 也有很大提升。
- 自动绑定
在 React 组件中,每个方法的上下文都会指向该组件的实例,即自动绑定 this 为当前组件。 而且 React 还会对这种引用进行缓存,以达到 CPU 和内存的最优化。在使用 ES6 classes 或者纯 函数时,这种自动绑定就不复存在了,我们需要手动实现 this 的绑定。
- bind方法 :
- 传参,这里就不多谢了,和平常用到的bind绑定是一样的
- 不传参,之前stage0草案中提供了一个便捷的方法—— 双冒号语法,它的作用和
this.handleClick.bind(this)
一致,而且Babel已经实现了提案:
<button onClick={::this.handleClick}>Test</button>
- 构造器内声明
<!----> 也是常用的方法,在这就简单的提一下。
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
- 箭头函数
- 箭头函数不仅是函数的“语法糖”,它还自动绑定了定义此函数作用域的 this, 因此我们不需要再对它使用 bind 方法。
2.1.3 在React中使用原生事件
在 React 中使用 DOM 原生事件时,一定要在组件卸载时手动移除,否则很 可能出现内存泄漏的问题。而使用合成事件系统时则不需要,因为 React 内部已经帮你妥善地处理了。
import React, { Component } from 'react';
class NativeEventDemo extends Component {
componentDidMount() {
this.refs.button.addEventListener('click', e => { this.handleClick(e);
});
}
handleClick(e) {
console.log(e);
}
componentWillUnmount() {
this.refs.button.removeEventListener('click');
}
render() {
return <button ref="button">Test</button>;
}
}
2.1.4 合成事件与原生事件混用
- 不要讲合成事件与原生事件混用。
比如:
componentDidMount() {
document.body.addEventListener('click', e => {
this.setState({ active: false});
});
document.querySelector('.code').addEventListener('click',
e => { e.stopPropagation(); })
}
componentWillUnmount() {
document.body.removeEventListener('click');
document.querySelector('.code').removeEventListener('click');
}
- 通过e.target判断来避免。
比如:
componentDidMount() {
document.body.addEventListener('click', e => {
if (e.target && e.target.matches('div.code')) {
return;
}
this.setState({ active: false });
});
}
这里了以得出一些结论:避免在 React 中混用合成事件和原生 DOM 事件。另外,用 reactEvent.nativeEvent. stopPropagation() 来阻止冒泡是不行的。阻止 React 事件冒泡的行为只能用于 React 合成事件系统 中,且没办法阻止原生事件的冒泡。反之,在原生事件中的阻止冒泡行为,却可以阻止 React 合成 事件的传播。
2.1.5 对比React 合成事件与 JavaScript原生事件
- 事件传播与阻止事件传播
原生DOM 事件的传播可以分为 3 个阶段:
- 事件捕获阶段、目标对象本身的事件处理 程序调用以及事件冒泡。
- 事件捕获会优先调用结构树最外层的元素上绑定的事件监听器,然后依次向内调用,一直调用到目标元素上的事件监听器为止。可以在将 e.addEventListener() 的第三 个参数设置为 true 时,为元素e注册捕获事件处理程序,并且在事件传播的第一个阶段调用。
- 事件捕获并不是一个通用的技术,在低于 IE9 版本的浏览器中无法使用。而事件冒泡则与 事件捕获的表现相反,它会从目标元素向外传播事件,由内而外直到最外层。
由上得出:事件捕获在程序开发中的意义并不大,更致命的是它的兼容性问题。所以,React 的合成事件则并没有实现事件捕获,仅仅支持了事件冒泡机制。这种 API 设计方式统一而简洁, 符合“二八原则”。
阻止原生事件传播需要使用 e.preventDefault(),不过对于不支持该方法的浏览器(IE9 以 下),只能使用 e.cancelBubble = true 来阻止。而在 React 合成事件中,只需要使用 e.prevent- Default() 即可。
- 时间类型
React 合成事件的事件类型是 JavaScript 原生事件类型的一个子集。
- 事件绑定方式
收到DOM标准的影响。绑定浏览器原生事件的方式也有很多,例如:
- 直接在DOM元素中绑定;
<button onclick="alert(1);">Test</button>
- 在JavaScript中,通过为元素的时间属性赋值的方式事先绑定:
el.onclick = e => { console.log(e); }
- 通过事件监听函数来实现绑定:
el.addEventListener('click', () => {}, false);
el.attachEvent('onclick', () => {});
- 事件对象
2.2 表单
2.2.1 应用表单组件
- 文本框
- 单选按钮与复选框
input 的radio类型是单选;input 的checkbox类型表示复选。
- Select 组件
- select 元素中设置multiple={true} 可以实现一个多选下拉表。多选的时候onChange返回的是个数组,单选的时候是e.target.value(值);
- HTML的 option组件中需要一个selected属性来表示默认选中的列表项。
2.2.2 受控组件
每当表单的状态发生变化时,都会被写入到组件的 state 中,这种组件在 React 中被称为受控组件(controlled component)。在受控组件中,组件渲染出的状态与它的 value 或 checked prop 相对应。React 通过这种方式消除了组件的局部状态,使得应用的整个状态更加可控。
React 受控组件更新 state 的流程总结:
- 可以通过在初始state中设置表单的默认值。
- 每当表单的值发生变化时,调用onChange事件处理器。
- 事件处理器通过合成事件对象e拿到改变后的状态,并更新应用的state。
- setState 触发视图的重新渲染,完成表单组件值的更新。
在 React 中,数据是单向流动的。从示例中,我们能看出来表单的数据源于组件的 state,并 通过 props 传入,这也称为单向数据绑定。然后,我们又通过 onChange 事件处理器将新的表单数 据写回到组件的 state,完成了双向数据绑定。
非受控组件
如果一个表单组件没有 value props(单选按钮和复选框对应的是 checked prop) 时,就可以称为非受控组件。相应地,你可以使用 defaultValue 和 defaultChecked prop 来表示 组件的默认状态。
案例:
import React, { Component } from 'react';
class App extends Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit(e) {
e.preventDefault();
// 这里使用 React 提供的 ref prop 来操作 DOM
// 当然,也可以使用原生的接口,如 document.querySelector const { value } = this.refs.name;
console.log(value);
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<input ref="name" type="text" defaultValue="Hangzhou" /> <button type="submit">Submit</button>
</form>
);
}
}
在 React 中,非受控组件是一种反模式,它的值不受组件自身的 state 或 props 控制。通常, 需要通过为其添加 ref prop 来访问渲染后的底层 DOM 元素。
说白了就是自己能掌控自己的是可以控制的,需要借用其他手段的为不可控的。
2.2.4 对比受控组件和非受控组件
这两者平常没有可以的区分,记几个例子来描述下他们的应用场景和区别:
<input value={this.state.value} onChange={e => {
this.setState({
value: e.target.value.toUpperCase()
})
}}
/>
直接展示输入的字母:
<input defaultValue={this.state.value} onChange={e => {
this.setState({
value: e.target.value.toUpperCase()
})
}}
/>
在受控组件中,可以将用户输入的英文字母转化为大写后输出展示,而在非受控组件中则不会。而如果不对受控组件绑定 change 事件,我们在文本框中输入任何值都不会起作用。多数情 况下,对于非受控组件,我们并不需要提供 change 事件。
受控组件 和非受控组件的最大区别是:非受控组件的状态并不会受应用状态的控制,应用中也多了局部组件状态,而受控组件的值来自于组件的 state。
- 性能上的问题
在受控组件中,每次表单的值发生改变就会调用一次onChange事件。非受控组件就不会出现这样的问题(React中不提倡用非受控组件)。- 是否需要事件绑定
受控组件每个组件需要绑定一个change事件,并且定义一个事件处理器来同步表单值和组件的状态,这是一个必要条件。
例如:
import React, { Component } from 'react';
class FormApp extends Component {
constructor(props) {
super(props);
this.state = { name: '', age: 18,
};
}
handleChange(name, e) {
const { value } = e.target;
// 这里只能处理直接赋值这种简单的情况,复杂的处理建议使用 switch(name) 语句
this.setState({
[name]: value
});
}
render () {
const { name, age} = this.state;
return (
<div>
<input value={name} onChange={this.handleChange.bind(this, 'name')} />
<input value={age} onChange={this.handleChange.bind(this, 'age')} />
</div>
);
}
}
2.2.5 表单组件的几个重要属性
- 状态属性
- value
- checked
- selected: 该属性可作用于 select 组件下面的 option 上,React 并不建议使用这种方式表 示状态,而推荐在 select 组件上使用 value 的方式。
- 事件属性
以上两种属性在状态属性发生变化时,会触发onChange事件属性。
2.3 样式处理
提到了业解火的CSS Modules
2.3.1 基本样式设置
- 自定义组件建议支持 className prop,以让用户使用时添加自定义样式;
- 设置行内样式时要使用对象。
const style = {
color: 'white',
backgroundImage: `url(${imgUrl})`,
// 注意这里大写的 W,会转换成
-webkit-transition WebkitTransition: 'all',
// ms 是唯一小写的浏览器前缀
msTransition: 'all',
};
const component = <Component style={style} />;
- 样式中的像素值
- 使用classnames库
React0.13版本前是提供了React.addons.classSet插件来给组件动态设置classname,后续移除了。
这里提到了一个classnames库,动态处理类名:例如
import React, { Component } from 'react';
class Button extends Component {
// ...
render() {
let btnClass = 'btn';
if (this.state.isPressed) {
btnClass += ' btn-pressed';
} else if (this.state.isHovered) {
btnClass += ' btn-over';
}
return <button className={btnClass}>{this.props.label}</button>;
}
};
使用了classnames库,代码就变得简单了:
import React, { Component } from 'react';
import classNames from 'classnames';
class Button extends Component {
// ...
render() {
const btnClass = classNames({
'btn': true,
'btn-pressed': this.state.isPressed,
'btn-over': !this.state.isPressed && this.state.isHovered,
});
return <button className={btnClass}>{this.props.label}</button>; }
}
);
2.3.2 CSS Modules
css 模块化的解决方案很多,但是主要有两类:
- Inline Style
这种方案彻底抛弃CSS,使用javascript或者JSON来写样式,能给 CSS 提供 JavaScript 同样强大的模块化能力。但缺点同样明显,Inline Style 几乎不能利用 CSS 本身 的特性,比如级联、媒体查询(media query)等,:hover 和 :active 等伪类处理起来比较 复杂。另外,这种方案需要依赖框架实现,其中与 React 相关的有 Radium、jsxstyle 和 react-style。
- CSS Modules
依旧使用 CSS,但使用 JavaScript 来管理样式依赖。CSS Modules 能最大 化地结合现有 CSS 生态和 JavaScript 模块化能力,其 API 非常简洁,学习成本几乎为零。 发布时依旧编译出单独的 JavaScript 和 CSS 文件。现在,webpack css-loader 内置 CSS Modules 功能。
下面我们详细介绍一下 CSS Modules
- CSS模块化遇到的哪些问题?
首先CSS模块化重要的事实解决好了两个问题:CSS样式的导入导出。灵活按需导入以便复用 代码,导出时要能够隐藏内部作用域,以免造成全局污染。
- 全局污染
Web Components 标准中的 Shadow DOM 能彻底解决这个问题,但它把样式彻底局部化,造成 外部无法重写样式,损失了灵活性。
- 命名混乱
- 依赖管理不彻底
组件应该相互独立引入一个组件时,应该只引入它所需要的CSS样式。现在的做法是除了要引入JavaScript,还要再引入它的 CSS,而且 Saas/Less 很难实现对每个组件都编译出单独的 CSS,引入所有模块的CSS又造成浪费。JavaScript 的模块化 已经非常成熟,如果能让 JavaScript来管理CSS依赖是很好的解决办法,而 webpack 的css-loader提供了这种能力。
- 无法共享变量
复杂组件要使用 JavaScript 和 CSS 来共同处理样式,就会造成有些变量 在 JavaScript 和 CSS 中冗余,而预编译语言不能提供跨 JavaScript 和 CSS 共享变量的这种 能力。
- 代码压缩不彻底
由于移动端网络的不确定性,现代工程项目对 CSS 压缩的要求已经到 了变态的程度。很多压缩工具为了节省一个字节,会把 16px 转成 1pc,但是这对非常长的 类名却无能为力。
- CSS Modules 模块化方案
CSS Modules 内部通过ICSS来解决样式导入和导出这两个问题, 分别对应:import 和 :export 这两个伪类。
:import("path/to/dep.css") {
localAlias: keyFromDep;
/* ... */
}
:export {
exportedKey: exportedValue;
/* ... */
}
- 启用CSS Modules
首先这个需要在webpack里面进行配置。启用CSS Modules的配置代码如下:
// webpack.config.js
css?modules&localIdentName=[name]__[local]-[hash:base64:5]
// 加上modules即为启用,其中localIdentName是设置生成样式的命名规则。
// 如果我们看到的如果我们看到的如果我们看到的HTML是这样的:
<button class="button--normal-abc5436"> Processing... </button>
<!--那我们那我们要注意到的是:-->
<!-- button--normal-abc5436 是 -->
<!-- CSS Modules按照localIdentName-->
<!--自动生成的class名称,其中base5436是-->
<!--是按照算发生成的序列码-->
// 经过这样的处理之后,class名基本是唯一的了,同样的修改class名称的长短。可以提高CSS的压缩率。
CSS Modules 实现了以下几点:
- 所有的样式都是局部化的,解决了命名冲突和全局污染问题;
- class名的生成规则配置灵活,可以以此来压缩class名;
- 只需要引用组件的JavaScript,就能搞定组件所有的JavaScript 和CSS;
- 依然是CSS,学习成本几乎为零。
- 样式默认局部
使用了 CSS Modules 后,就相当于给每个 class 名外加了 :local,以此来实现样式的局部化。
如果我们想切换到全局模式,可以使用 :global 包裹。示例代码如下:
.normal {
color: green;
}
/* 以上与下面等价 */
:local(.normal) {
color: green;
}
/* 定义全局样式 */
:global(.btn) {
color: red;
}
/* 定义多个全局样式 */
:global {
.link {
color: green;
}
.box {
color: yellow;
}
}
- 使用composes来组合样式
对于样式复用,CSS Modules 只提供了唯一的方式来处理——composes 组合。例如:
/* components/Button.css */
.base { /* 所有通用的样式 */ }
.normal {
composes: base;
/* normal 其他样式 */
}
.disable {
composes: base;
/* disable 其他样式 */
}
import styles from './Button.css';
buttonElem.outerHTML = `<button class=${styles.normal}>Submit</button>`
生成的 HTML 变为:
<button class="button--base-abc53 button--normal-abc53"> Processing... </button>
由于在 .normal 中组合了 .base,所以编译后的 normal 会变成两个 class。
此外,使用composes还可以组合外部文件中的样式:
/* settings.css */
.primary-color {
color: #f40;
}
/* components/Button.css */
.base { /* 所有通用的样式 */ }
.primary {
composes: base;
composes: $primary-color from './settings.css';
/* primary 其他样式 */
}
对于大多数项目,有了 composes 后,已经不再需要预编译处理器了。但如果想用的话,由 于 composes 不是标准的 CSS 语法,编译时会报错,此时就只能使用预处理器自己的语法来做样式复用了。
- class 命名技巧
CSS Modules 的命名规范是从 BEM 扩展而来的。BEM 把样式名分为 3 个级别,具体如下所示。
- Block: 对应模块名, 如: Dialog;
BEM 最终得到的class 名为 dialog__confirm-button–highlight。使用双符号 __ 和 – 是为 了与区块内单词间的分隔符区分开来。
- Element: 对应模块中的节点名Confirm Button;
- Modifier:对应节点相关的状态,如disabled 和 highlight。
- 实现CSS与JavaScript 变量共存
- CSS Modules使用技巧
建议用它需要注意的原则:
- 不适用选择器,只是用class名来定义样式;
- 不层叠多个class,只使用一个class把所有的样式定义好;
- 所有的样式通过composes组合来实现复用;
- 不嵌套。
如何与全局样式共存
平时在项目中,不可避免的会引入一些全局CSS文件,使用webpack可以让全局样式和CSS Modules的局部样式和谐共存。
下面讲述的是webpack部分配置代码:
module: {
loaders: [{
test: /\.jsx?$/,
loader: 'babel',
},{
test: /\.scss$/,
exclude: path.resolve(__dirname, 'src/styles'),
loader: 'style!css?modules$localIdentName=[name]__[local]!sass?sourceMap=true',
}, {
test: /\.scss$/,
include: path.resolve(__dirname, 'src/styles'),
loader: 'style!css!sass?sourceMap=true',
}]
}
/* src/app.js */
import './styles/app.scss';
import Component from './view/Component'
/* src/views/Component.js */
import './Component.scss';
目录结构如下:
- CSS Modules 结合React实践
一般把组件最外层节点对应的 class 名称为 root。
import React, { Component } from 'react';
import classNames from 'classnames';
import styles from './dialog.css';
class Dialog extends Component {
render() {
const cx = classNames({
confirm: !this.state.disabled,
disabledConfirm: this.state.disabled,
});
return (
<div className={styles.root}>test</div>
)
}
}
当然如果不想频繁地输入styles.,可以使用 react-css-modules库。它通过高阶组件的形式来 避免重复输入 styles.。
例如:
import React, { Component } from 'react';
import classNames from 'classnames';
import CSSModules from 'react-css-modules';
import styles from './dialog.css';
class Dialog extends Component {
render() {
const cx = classNames({
confirm: !this.state.disabled,
disabledConfirm: this.state.disabled,
});
return (
<div styleName="root">
<a styleName={cx}>Confirm</a>
);
}
}
export default CSSModules(Dialog, styles);
2.4 组件间通信
结合实际运用,组件间的通信大致分为三种:
- 父组件向子组件通信
- 子组件向父组件通信
- 没有嵌套关系的组件之间通信
2.4.1 父组件向子组件通信
React是单向数据流,而父组件向子组件通信是最常见的,一般都是通过props通信
2.4.2 子组件向父组件通信
这个常用的两种方式:
- 利用回调函数
- 利用自定义事件机制
2.4.3 跨级组件通信
平常用的props传递通信,代码不太优雅,而且会造成代码冗余。在React中,我们还可以通过context来实现跨级父组件件之间的通信
class ListItem extends Component {
static contextTypes = {
color: PropTypes.string,
}
render() {
return (
<li style={{ background: this.context.color }}>
<span>test</span>
</li>
)
}
}
class List extends Component {
static childContextTypes = {
color: PropTypes.string,
};
getChildContext() {
return {
color: 'red',
};
}
render() {
const { list } = this.props;
return (
<div>
<ListTitle title={title} />
<ul>
{list.map((entry, index) => (
<ListItem key={`list-${index}`} value={entry.text} />
))}
</ul>
</div>
);
}
}
可以看到,我们并没有给 ListItem 传递 props,而是在父组件中定义了 ChildContext,这样从 这一层开始的子组件都可以拿到定义的 context,例如这里的 color。
context它可以减少逐层传递,但当组件结 构复杂的时候,我们并不知道 context 是从哪里传过来的。Context就像一个全局变量一样,而全局变量正是导致应用走向混乱的罪魁祸首之一,给组件带来了外部依赖的副作用。在大部分情 况下,我们并不推荐使用 context 。使用 context比较好的场景是真正意义上的全局信息且不会更改,例如界面主题、用户信息等。
2.4.4 没有嵌套关系的组件通信
这里借用Node.js Events 简介一下
import { EventEmitter } from 'events';
export default new EventEmitter();
// 然后把 EventEmitter 实例输出到各组件中使用:
import ReactDOM from 'react-dom';
import React, { Component, PropTypes } from 'react';
import emitter from './events';
class ListItem extends Component {
static defaultProps = {
checked: false,
}
constructor(props) {
super(props);
}
render() {
return (
<li>
<input type="checkbox" checked={this.props.checked} onChange={this.props.onChange} />
<span>{this.props.value}</span>
</li>
);
}
}
class List extends Component {
constructor(props) {
super(props);
this.state = {
list: this.props.list.map(entry => ({
text: entry.text,
checked: entry.checked || false,
})),
};
onItemChange(entry) {
const { list } = this.state;
this.setState({
list: list.map(prevEntry => ({
text: prevEntry.text,
checked: prevEntry.text === entry.text ?
!prevEntry.checked : prevEntry.checked,
}))
});
emitter.emit('ItemChange', entry);
}
render() {
return (
<div>
<ul>
{this.state.list.map((entry, index) => ( <ListItem
key={`list-${index}`}
value={entry.text}
checked={entry.checked} onChange={this.onItemChange.bind(this, entry)}
/> ))}
</ul>
</div>
);
}
}
class App extends Component {
componentDidMount() {
this.itemChange = emitter.on('ItemChange',
(data) => { console.log(data);
}); }
componentWillUnmount() {
emitter.removeListener(this.itemChange);
}
render() {
return (
<List list={[{text: 1}, {text: 2}]} /> );
}
}
以上只为了更容易理解。在项目应用中Pub/Sub 插件用起来也挺容易的,主要是利用全局对象来保存事件,用广播的方式去处理事件。
2.5 组件间抽象
2.5.1 mixin
- 使用mixin的缘由: 广泛应用于各种面向对象语言中,大多有原生支持,如: Perl、Ruby、Python,甚至连sass也支持。作用:多重继承。
- 封装mixin方法,案例:
const mixin = function (obj, mixins) {
const newObj = obj;
newObj.prototype = Object.create(obj.prototype);
for (let prop in mixins) {
if (mixins.hasOwnProperty(prop)) {
newObj.prototype[prop] = mixins[prop];
}
return newObj;
}
const BigMixin = {
fly: () => {
console.log('I can fly');
}
};
const Big = function() {
console.log('new big');
};
const FlyBig = mixin(Big, BigMixin);
const flyBig = new FlyBig(); // => 'new big'
flyBig.fly(); // => 'I can fly'
判断一个属性是定义在对象本身而不是继承自原型链,我们需要使用从 Object.prototype 继承而来的 hasOwnProperty 方法。
【hasOwnProperty】介绍
对于广义的mixin方法,就是用赋值的方式将 mixin 对象里的方法都挂载到原对象上,来实现对对象的混入。
3. 在React 中使用mixin
React 在使用 createClass 构建组件时提供了 mixin 属性,比如官方封装的 PureRenderMixin:
import React from 'react';
import PureRenderMixin from 'react-addons-pure-render-misin';
React.createClass({
mixins: [PureRenderMixin],
render () {
return <div>foo</div>;
}
})
在 createClass 对象参数中传入数组 mixins,里面封装了我们所需要的模块。mixins 数组也 可以增加多个 mixin,其每一个 mixin 方法之间的有重合,对于普通方法和生命周期方法是有所 区分的。
在React中mixin李名字相同,不会后者覆盖前者,但是会报错ReactClassInterface。
mixin做了哪些事情?
- 工具方法,主要是共享一些类方法;
- 生命周期继承, props与 state 合并,这是 mixin特别重要的功能,它能够合并生命周期方 法。
- ES6 Classes 与 decorator
这里需要说明一下,前面讲到的mixin在我们推荐的ES6 classes中是不支持的。但是讲到语法糖decorator,可以实现class上的mixin。
案例:(core-decorators)
import { getOmnPropertyDesciptors } from './private/utils';
const { defineProperty } = Object
function handleClass(target, mixins) {
if (!mixins.length) {
throw new SyntaxError(`@mixin() class ${target.name} requires at least one mixin as an argument`);
}
for (let i = 0, l = mixins.length; i < l; i++) {
// 获取 mixins 的 attributes 对象
const descs = getOwnPropertyDescriptors(mixins[i]);
// 批量定义 mixins 的 attributes 对象 for (const key in descs) {
if (!(key in target.prototype)) {
defineProperty(target.prototype, key, descs[key]);
}
}
}
}
export default function mixin(...mixins){
if (typeof mixins[0] === 'function'){
return handleClass(mixins[0], []);
} else {
return target => {
return handleClass(target, mixins);
};
}
}
两个mixin相比较有一些不一样的地方, 比如这个class里面的defineProperty,定义是对已有的定义,赋值则是覆盖已有的定义。和之前讲到的官方的mixin不一样,因为官方的会报错,不会覆盖。所以本质上,两者方法很不一样,除了定义方法级别不能覆盖外,还有生命周期方法的继承,以及对state的合并。
- mixin的问题
- 破坏了原有属性的封装
mixin方法会混入方法,给原组件带来新的特性。- 命名冲突
我们知道 mixin是平面结构,在不同的两个mixin钟可能有同一个名字的方法,我们改动其中一个可能会影响到另外的,虽然这种问题 我们可以提前约定,但是平面结构中不能做得很好。- 增加复杂性
2.5.2 高阶组件
高阶函数(higher-order function): 这种函数接受函数作为输入,或者输出一个函数。如: map、reduce、sort等都是高阶函数。
高阶组件(higher-order component):他接受React组件作为输入。输出一个新的React组件。
// Haskell
hocFactory:: w: React.Component => E: React.Component
共同学习,写下你的评论
评论加载中...
作者其他优质文章