为了账号安全,请及时绑定邮箱和手机立即绑定

浅读React技术栈

标签:
Html5 React

一、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 : 表现行为就是该组件会尝试重新渲染。

注意点:

  1. setState是一个异步的方法。
  2. 一个生命周期内所有的setState方法会合并操作。
    // 这里认识到了一些新的写法
    const currProps = this.props
    
    let activeIndex = 0
    
    if ('activeIndex' in currProups) {
        activeIndex = currProps.activeIndex;
    } 
  • Props :
    props是properties的缩写。
  1. props 的传递过程。对于React组件来说是非常直观的。React的单项数据流,主要的流动管道就是props。props本身是不可变的。
  2. 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。

  1. shouldComponentUpdate 它接收需要更新的props和state,让开发者增加必要的条件判断,让其在需要时跟新,不需要时不更新。因此,当方法返回false的时候,组件不再向下执行生命周期方法。
shouldComponentUpdate 的本质时用来进行正确的组件渲染。默认情况下React会渲染所有的节点,因为shouldComponentUpdate默认返回true。正确的组件渲染从另一个意义上说,也是性能优化的手段之一。

无状态组件是没有生命周期的,所以他在渲染时,每次哦度会渲染,当然,我们可以选择引用Recompose库的pure方法:

    const OptimizedComponent = pure(ExpensiveComponent);
    // 事实上pure方法做的事就是将无状态组件转换成class语法加上PureRender后的组件。
1.5.3 整体流程
  • 生命周期
    qXmJ4clVT5.gif

  • createClass 和ES6 classes的区别
    LOvyo2C8yL.gif

1.6 React 与DOM

从React 0.14 版本开始,React将React中涉及DOM操作的部分剥离开了,目的是为了抽象React, 同时适用于Web端和移动端。ReactDOM的关注点在DOM上,因此只适用于Web端。
  • ReactDOM
  1. 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 只对已经挂载的组 件有效。
}
  1. 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 最大的亮点之一!

  1. 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 的绑定。

  1. bind方法 :
  • 传参,这里就不多谢了,和平常用到的bind绑定是一样的
  • 不传参,之前stage0草案中提供了一个便捷的方法—— 双冒号语法,它的作用和this.handleClick.bind(this)一致,而且Babel已经实现了提案:
    <button onClick={::this.handleClick}>Test</button>
  1. 构造器内声明
<!----> 也是常用的方法,在这就简单的提一下。
constructor(props) {

    super(props);
    this.handleClick = this.handleClick.bind(this);
    
}
  1. 箭头函数
  • 箭头函数不仅是函数的“语法糖”,它还自动绑定了定义此函数作用域的 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原生事件
  1. 事件传播与阻止事件传播

原生DOM 事件的传播可以分为 3 个阶段:

  • 事件捕获阶段、目标对象本身的事件处理 程序调用以及事件冒泡。
  • 事件捕获会优先调用结构树最外层的元素上绑定的事件监听器,然后依次向内调用,一直调用到目标元素上的事件监听器为止。可以在将 e.addEventListener() 的第三 个参数设置为 true 时,为元素e注册捕获事件处理程序,并且在事件传播的第一个阶段调用。
  • 事件捕获并不是一个通用的技术,在低于 IE9 版本的浏览器中无法使用。而事件冒泡则与 事件捕获的表现相反,它会从目标元素向外传播事件,由内而外直到最外层。

由上得出:事件捕获在程序开发中的意义并不大,更致命的是它的兼容性问题。所以,React 的合成事件则并没有实现事件捕获,仅仅支持了事件冒泡机制。这种 API 设计方式统一而简洁, 符合“二八原则”。

阻止原生事件传播需要使用 e.preventDefault(),不过对于不支持该方法的浏览器(IE9 以 下),只能使用 e.cancelBubble = true 来阻止。而在 React 合成事件中,只需要使用 e.prevent- Default() 即可。
  1. 时间类型

React 合成事件的事件类型是 JavaScript 原生事件类型的一个子集。

  1. 事件绑定方式

收到DOM标准的影响。绑定浏览器原生事件的方式也有很多,例如:

  • 直接在DOM元素中绑定;
<button onclick="alert(1);">Test</button>
  • 在JavaScript中,通过为元素的时间属性赋值的方式事先绑定:
el.onclick = e => { console.log(e); }
  • 通过事件监听函数来实现绑定:
el.addEventListener('click', () => {}, false);
el.attachEvent('onclick', () => {});
  1. 事件对象

2.2 表单

2.2.1 应用表单组件
  1. 文本框
  2. 单选按钮与复选框

input 的radio类型是单选;input 的checkbox类型表示复选。

  1. Select 组件
  1. select 元素中设置multiple={true} 可以实现一个多选下拉表。多选的时候onChange返回的是个数组,单选的时候是e.target.value(值);
  2. 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 表单组件的几个重要属性
  1. 状态属性
  • value
  • checked
  • selected: 该属性可作用于 select 组件下面的 option 上,React 并不建议使用这种方式表 示状态,而推荐在 select 组件上使用 value 的方式。
  1. 事件属性

以上两种属性在状态属性发生变化时,会触发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} />;

  1. 样式中的像素值
  2. 使用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

  1. 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,但是这对非常长的 类名却无能为力。

  1. 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 变量共存
  1. 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';

目录结构如下:
8LHzWrnQTO.gif

  1. 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 子组件向父组件通信

这个常用的两种方式:

  1. 利用回调函数
  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
  1. 使用mixin的缘由: 广泛应用于各种面向对象语言中,大多有原生支持,如: Perl、Ruby、Python,甚至连sass也支持。作用:多重继承。
  2. 封装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特别重要的功能,它能够合并生命周期方 法。
  1. 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的合并。
  1. 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
点击查看更多内容
5人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
Web前端工程师
手记
粉丝
5
获赞与收藏
14

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消