转载自:https://blog.laisky.com/p/react/

一、简介

1、前言

今年上半年要给公司做个新项目,时间比较充裕,于是前端框架选用了 react,打算一般学习一边完成。

学习加用了几个月的 react,总结了一些经验,做一些分享。

因为已经有很多资料可供参考,可以见文章末尾的链接。

所以我就不过多累述技术细节,而侧重分享一些观念上的理解,从宏观上了解 react 的使用理念,如果理解了理念, 再去学习技术细节就会更有针对性,使用起来也会更加得心应手。

2、概述

react 有一个完整的生态圈,可以让你解决从组件设计到大型网站架构的诸多问题。 可以这样制定自己的学习步骤:

  1. reactjs;
  2. react-router;
  3. flux & react-redux。

reactjs 是一个设计组件的框架,解决的是 MVC 中 View 这一块的问题。 学好 react,就掌握了组件化设计页面的能力,为构建大型应用铺好了基础。

然后就可以学习 react-router,这是一个可以实现单页应用的组件, 让开发者可以更灵活的控制页面切换中的每一个步骤。

最后就可以学习 flux 的设计理念,以及 react-redux 的实现。 理解 flux 的核心理念:单项数据传递。

如果有需要,还可以尝试 react-relay 这样的基于 graphql 的数据驱动方式。

如果与后端数据的异步交互极为复杂,还可以尝试引入 react-saga 的方式进行解耦。

掌握了以上这些后,你就可以通过灵活的运用去组建大型的网页应用了。

3、概念

先针对一些较少前端开发经验的人介绍一些基本的概念。

4、单页应用

最早的网页是纯静态的,任何页面的改动都需要重新刷新页面。后来网景推出了 JavaScript,开发者可以动态的更新页面中的一部分。

不过大部分情况下,我们仅仅使用 JavaScript 更新页面中的一小部分内容,当用户需要切换到另一个不同的版面时,我们仍会刷新整个页面。

这种刷新网页的行为会有几个问题:

  • 额外的网络负担;

每次刷新页面都需要重新加载一遍所有的静态资源,不过可以通过良好的缓存设置来解决,还不算太大的问题。

  • 较难保存连贯的用户状态和数据;

用户加载新页后,JavaScript 都会被重新执行,除了 cookies 外你很难保存连续的用户数据,而 cookie 又不能保存太大的数据。 如果你是使用 session 来保存数据的话,又需要额外的网络通信。

不过现在有 localStorage 和 sessionStorage,可以部分的解决这个问题。

  • 不太好的用户体验;

用户点击链接时,浏览器会变为空白,等待资源的加载,不过你也可以妥善的设置 prefetch 预加载临近的内容。

上述的几个问题,虽然都可以有其他的途径来解决,不过操作起来毕竟比较繁琐,所以单页应用单页应用这一模式逐渐流行。

所谓单页应用,就是指整个应用对于浏览器而言只有一页,所有的页面切换都通过动态加载来实现(就如 jquery 中的 $.load)。 这样做的好处是,用户的整个访问过程,都处在同一个上下文之中,你可以在脚本中维持一个完整的用户上下文, 保存用户的状态、监听用户的所有动作,并触发相应的操作。

其实实现的原理也很简单,当用户要切换页面时,首先确认页面改动的区域,使用 js 动态的替换 DOM 元素, 然后调用 history.pushState 接口更新历史纪录和 URL。

5、语义化

HTML4 到 HTML5 最大的改变不是多了几个功能,而是一种编写网页概念上的转变,这种转变的趋势就是 “语义化”,也可以称为 “组件化”。

形象的说,过去的网页的结构可能是这样的:

而现代的网页设计是这样的:

最大的区别在于,过于倾向于使用拼凑 div 来组织页面,然后通过自定义 class 来定义组织结构。

而现代则更倾向于使用 tag 来设计网页的架构, 比如 HTML5 里就新增了 articlenavheadersection 等一些列的 tag, 而且比起 class,更推荐人们使用 attribute 来定义属性,举个例子:

<!-- 过去的写法 -->
<div class="article readonly"></div>
​
<!-- 现在推荐的写法 -->
<article readonly="true" data-page="5"></article>

组件化的构建网页使得网页内的元件得以解耦,你可以更方便的在不同的页面里复用相似功能的组件。 而且语义化的网页对于设备更为友好,让设备可以更好的识别网页中的内容,比如可以更好的构建无障碍网页 1


二、ReactJs

reactjs 就是为了更好的实现上文提到过的组件化开发而存在的。

具体的使用可以查阅文章开头的文档,这里主要记录一些关键的知识点和理解。

一些相关资源:

Update at: 2016/12/15

其实 react 的意义不仅仅在于组件化,随着 redux 等插件的引入, react 让你可以用一种状态机和函数式的思想去构建大型网页应用。 这和 AngularJS 的 data bindind 的思路是不一样的。

1、组件

reactjs 是一套关于组件的框架,使用 react 可以方便的构建出功能组件(包含部分的页面和逻辑)。

每一个组件都负责实现一小部分的页面和功能,小的组件可以组合和嵌套成更大的组件, 最终的页面就是由数个组件组合而成。

组件是一种抽象的概念,可以理解为一个组件类,使用组件需要对其传参,就如同实例化一个组件类。

组件接收传入的参数(this.props),然后根据参数来初始化自己的状态(this.state), 并且执行一系列的逻辑操作,比如计算、或是向后台请求一些数据,最后根据状态来生成页面(render)。

比如定义一个最简单的组件:

import { Component } from 'react';
​
​
export class SimpleComponent extends Component {
    constructor(props, context) {
        super(props, context)
​
        this.props.xxx // 获取用户传入的参数
        this.setState({
            // 初始化状态
        })
    };
​
    // 渲染页面
    // 每当 state 有变动时,render 都会被触发,重新渲染
    // 所以我们应该通过更新 state 来更新页面,而不要直接操作网页元素
    render() {
        return (
            <article>{this.state.xxx}</article>
        );
    };
}

需要注意的是 render() 返回的并不是一个组件的实例,这里需要了解两个概念,keyref

详细的可以参考这两篇文章:

简单的概括下:

  • ref 可以让你拿到组件的实例;
  • key 用来标记组件的顺序,以及告诉 react 是否需要重绘。

2、JSX

组件通过 JSX 语法编写,细节不累述,只需要记住几个关键的地方。

  • 默认情况下用 js 语法解析;
  • 遇到 < 时用 HTML 语法;
  • 在 HTML 语法里使用 js 要用 {} 包起来。

3、参数

使用组件是可以传入参数,在组件内容果 this.props 来获取传入的参数,比如上述组件可以这么使用:

render() {
    return <SimpleComponent xxx="123" yyy="234" />
};

参数可以是字符串,也可以是函数句柄。

还可以通过设置 propTypes 来对传入的参数做检查,具体可见文档。

4、事件监听

出了属性外,也可以对组件绑定事件监听。

如果需要 this 指向原组件,可以考虑采用闭包:

render() {
    return <SimpleComponent onClick={() => {xxx}} />
};

5、状态 state

需要注意的是,react 里不要直接的去更新页面元素,而是要通过更新组件 state 的方式去间接的更新页面。

每一个组件都会有状态(this.state),当状态有变化时,该组件的 render 函数会自动重新调用, 所以你在定义 render 函数时,应该把需要变化的值用 state 的形式传递进去,如:

render() {
    return (
        <p>剩余时间:<span>{this.state.nSecs}</span></p>
    );
};

然后你可以通过一个计时器不算的更新 state,页面也自然会同步刷新。

更新 state 的方法是调用 this.setState

this.setState({
    a: xxx
    b: yyy
})

6、生命周期

知道了组件的使用方法后,组件的最后一个概念就是生命周期。

一个组件从准备显示到被移除,一共经历了三个状态:

  • Mounting:挂载;
  • Updating:更新;
  • Unmounting:移除。

这三个状态间的来回切换,衍生出如下的阶段:

  • 挂载;
    • componentWillMount():挂载前;
    • componentDidMount():挂载后;
  • 更新;
    • componentWillReceiveProps(object nextProps):准备更新;
    • shouldComponentUpdate(object nextProps, object nextState):是否需要更新页面;
    • componentWillUpdate(object nextProps, object nextState):更新前;
    • componentDidUpdate(object prevProps, object prevState):更新后;
  • 移除;
    • componentWillUnmount():移除前。

可以在组件内定义上述方法,来实现在各个状态的操作。


三、React-Router

1、简介

ReactJS 其实更倾向于是一个 UI 库,可以让你很方便的构建 UI 组件,并进一步的拼接成页面。

但是如果你是一个多页的网站,又想做成一个单页应用,而且还希望 react 组件可以响应页面变化的事件, 那么你就需要 react-router 了。

react 是一种 URL 路由管理工具,负责监听页面的切换,根据 URL 加载不同的页面组件,并且维护浏览器的历史纪录。

一些相关资源:

2、注册

最简单的用法就是在你使用 react 定义好各页后,按照 url 注册进 react-router 内:

import { Router, Route, browserHistory } from 'react-router';
​
​
/*
 * 下面的例子中,我一共写好了两个页面 App 和 About
 * 然后将 App 注册到 /,将 About 注册到 /about
 */
ReactDOM.render(
    <Router history={browserHistory}>
        <Route name="home" path="/" component={App}>
            <Route name="about" path="about" component={About} />
        </Route>
    </Router>,
    document.getElementById('body')
);

路径匹配

上例中使用了 <Route /> 来注册地址和组件,其实地址有很多的写法:

// matches /hello/michael and /hello/ryan
<Route path="/hello/:name">
​
// matches /hello, /hello/michael, and /hello/ryan
<Route path="/hello(/:name)">
​
// matches /files/hello.jpg and /files/hello.html
<Route path="/files/*.*">
​
// matches /files/hello.jpg and /files/path/to/file.jpg
<Route path="/**/*.jpg">

默认路由 IndexRoute

一半情况下,App 可能是一个是一个 “空组件”,只是用来容纳各页的子组件的。

比如前例中,用户访问 /about,会显示 About 组件的内容,而用户访问 /,则只会显示 App 的空页面。

可以使用默认路由 IndexRoute,指定用户访问 / 时显示的页面内容:

<Router>
    <Route path="/" component={App}>
        {/* 没有 Home 的话,App 内的 this.props.children 就是空的 */}
        <IndexRoute component={Home}/>
        <Route path="accounts" component={Accounts}/>
        <Route path="statements" component={Statements}/>
    </Route>
</Router>

默认跳转 IndexRedirect

和默认路由不同,默认跳转是将用户跳转到另一个页面:

<Route path="/" component={App}>
    <IndexRedirect to="/welcome" />
    <Route path="welcome" component={Welcome} />
    <Route path="about" component={About} />
</Route>

重定向 Redirect

可以用来实现 404,当无法匹配路由的时候,就匹配到 404:

<Router history={ browserHistory }>
    <Route name="home" path="/" component={ App }>
        <Route name="pagenotfound" path="404.html" component={ PageNotFound } />
        <Route name="about" path="about/" component={ About } />
    </Route>
    <Redirect from="*" to="/404.html" />
</Router>

3、生命周期 Lifecycle

除了路由外,生命周期大概是 react-router 最常用的功能。

类似于 react 组件的生命周期,react-router 的生命周期用于追踪页面状态的切换:

  • 页面初次被载入时,触发 componentDidMount
  • 页面切换
    • App 会触发 componentWillReceivePropscomponentDidUpdate
    • 被切出的页触发 componentWillUnmount
    • 切入的页触发 componentDidMount
  • 页内跳转(比如
    /users/:id

    切换 id)

    • App 触发 componentWillReceivePropscomponentDidUpdate
    • 当前页触发 componentWillReceivePropscomponentDidUpdate

4、跳转提醒 routerWillLeave

可以给页面组件设置 routerWillLeave 方法,监听页面离开的事件:

const Home = React.createClass({
​
    contextTypes: {
        router: Router.PropTypes.router
    },
​
    componentDidMount() {
        this.context.router.setRouteLeaveHook(this.props.route, this.routerWillLeave)
    },
​
    routerWillLeave(nextLocation) {
        // return false to prevent a transition w/o prompting the user,
        // or return a string to allow the user to decide:
        if (!this.state.isSaved)
            return 'Your work is not saved! Are you sure you want to leave?'
    },
​
    // ...
​
});

四、Flux

1、介绍

Flux 是 Facebook 对 MVC 架构的优化,将整个网站行为拆分为四个部分,确保数据仅会单向流动。

  • View: 视图层;
  • Action(动作):视图层发出的消息(比如 mouseClick);
  • Dispatcher(派发器):用来接收 Actions、执行回调函数;
  • Store(数据层):用来存放应用的状态,一旦发生变动,就提醒 Views 要更新页面;

一些相关资源:


五、React-Redux

1、简介

一些相关资源:

Redux 是 flux 的一个实现,概念上唯一的差别就是把 Dispatcher 换成了 Reducer。

Redux 三大原则:

  • 单一数据源(整个应用仅有一个 store);
  • state 是只读的(只能通过 action 来修改 state);
  • 使用纯函数来执行修改(使用 reducer(state, action) -> state)。

2、使用

概述

基本是这么个流程:

  1. 编写一系列的 reducer,可以根据 action 修改 state;
  2. 将一系列的 reducer 绑定到根 reducer;
  3. 使用根 reducer 和初始 state 创建 store;
  4. 给每个页面组件 connect 到 state 和 dispatch;
  5. 组件中就可以使用 state,也可以调用 dispatch 抛出 action 给 reducer。

Store

一个应用有且仅有一个 store,这个 store 内以多个 key-value 的形式存放所有重要的 state。

需要注意的两点是:

  • 不建议 store 的借口嵌套太深,而且是并列的形式存放;
  • 建议仅存放重要的 state,深层子组件的 state 没必要存在 store 内。
import { createStore } from 'redux'
​
//  根据 root reducer 创建 store
let store = createStore(Reducer[, initState])
​
// 注册到应用中
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

Reducer

reducer 就是一个根据 action 修改 state 的函数,形如 (previousState, action) => newState

最简单的例子:

function demoReducer(state = {total: 0}, action) {
    switch (action.type) {
        case PLUS:
            return Object.assign({}, state, {
                total: state.total + 1
            });
    }
}

使用 Object.assign 这个方法可以仅替换掉原 state 内的指定键值。

多个 reducers 可以合并为一个根 reducer。

export const rootReducer = combineReducers({
    childReducer1,
    childReducer2
});

Connect

我们已经知道了 store 负责存储 state,reducer 负责处理 action。 接下来需要考虑的就是,我们如何在组件中使用 state 和抛出 action 了。

这就需要借助 connect 函数的帮助了:

connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])

最常用的就是前两个参数:

  • 需要传递的 state;
  • 需要传递的 dispatch(用于抛出 action);
import React, { Component, PropTypes } from 'react'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
import { Provider, connect } from 'react-redux'
​
​
class Counter extends Component {
  render() {
    const { value, onIncreaseClick } = this.props
    return (
      <div>
        <span>{value}</span>
        <button onClick={onIncreaseClick}>Increase</button>
      </div>
    )
  }
}
​
​
// Action
const increaseAction = { type: 'increase' }
​
​
// Map Redux state to component props
// 将 state 和 props 绑定,可以提供任何细粒度的绑定
function mapStateToProps(state) {
  return {
    value: state.count
  }
}
​
​
// Map Redux actions to component props
// 将 dispatch 和 props 绑定,可以在组件内抛出 action
function mapDispatchToProps(dispatch) {
  return {
    onIncreaseClick: () => dispatch(increaseAction)
  }
}
​
​
// Connected Component
const App = connect(
  mapStateToProps,
  mapDispatchToProps
)(Counter)

就酱。