转载自:https://blog.laisky.com/p/react/
1、前言
今年上半年要给公司做个新项目,时间比较充裕,于是前端框架选用了 react,打算一般学习一边完成。
学习加用了几个月的 react,总结了一些经验,做一些分享。
因为已经有很多资料可供参考,可以见文章末尾的链接。
所以我就不过多累述技术细节,而侧重分享一些观念上的理解,从宏观上了解 react 的使用理念,如果理解了理念, 再去学习技术细节就会更有针对性,使用起来也会更加得心应手。
2、概述
react 有一个完整的生态圈,可以让你解决从组件设计到大型网站架构的诸多问题。 可以这样制定自己的学习步骤:
- reactjs;
- react-router;
- 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 里就新增了 article
、nav
、header
、section
等一些列的 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()
返回的并不是一个组件的实例,这里需要了解两个概念,key
和 ref
。
详细的可以参考这两篇文章:
简单的概括下:
- 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 会触发
componentWillReceiveProps
和componentDidUpdate
- 被切出的页触发
componentWillUnmount
- 切入的页触发
componentDidMount
- App 会触发
- 页内跳转(比如
/users/:id
切换 id)
- App 触发
componentWillReceiveProps
和componentDidUpdate
- 当前页触发
componentWillReceiveProps
和componentDidUpdate
- App 触发
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、使用
概述
基本是这么个流程:
- 编写一系列的 reducer,可以根据 action 修改 state;
- 将一系列的 reducer 绑定到根 reducer;
- 使用根 reducer 和初始 state 创建 store;
- 给每个页面组件 connect 到 state 和 dispatch;
- 组件中就可以使用 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)
就酱。