🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
[TOC] # react-router - [React Router文档](http://react-guide.github.io/react-router-cn/) - [https://www.jianshu.com/p/5e8297858ea8](https://www.jianshu.com/p/5e8297858ea8) - [https://www.jianshu.com/p/6a45e2dfc9d9/](https://www.jianshu.com/p/6a45e2dfc9d9/) ## 安装 `npm i react-router-dom --save` ## 主要组件 <span style="font-size: 20px;">\<Router></span> 你可以将各种组件及标签放进 \<Router> 组件中,他的角色很像 Redux 中的 \<Provider>。不同的是 \<Provider> 是用来保持与 store 的更新,而 \<Router> 是用来保持与 location 的同步。示例如下: ```html <Router> <div><!-- <Router> 组件下只允许存在一个子元素!--> <ul> <li><Link to="/">首页</Link></li> <li><Link to="/about">关于</Link></li> <li><Link to="/topics">主题列表</Link></li> </ul> <hr/> <Route exact path="/" component={Home}/> <Route path="/about" component={About}/> <Route path="/topics" component={Topics}/> </div> </Router> ``` Router 是所有路由组件共用的底层接口,一般我们的应用并不会使用这个接口,而是使用高级的路由: `<BrowserRouter>`:使用 HTML5 提供的 history API 来保持 UI 和 URL 的同步; `<HashRouter>`:使用 URL 的 hash (例如:window.location.hash) 来保持 UI 和 URL 的同步; `<MemoryRouter>`:能在内存保存你 “URL” 的历史纪录(并没有对地址栏读写); `<NativeRouter>`:为使用 React Native 提供路由支持; `<StaticRouter>`:从不会改变地址; <span style="font-size: 20px;">\<Route></span> Route 组件主要的作用就是当一个 location 匹配路由的 path 时,渲染某些 UI。示例如下: ```html <Router> <div> <Route exact path="/" component={Home}/> <Route path="/news" component={NewsFeed}/> </div> </Router> // 如果应用的地址是 /,那么相应的 UI 会类似这个样子: <div> <Home/> </div> // 如果应用的地址是 /news,那么相应的 UI 就会成为这个样子: <div> <NewsFeed/> </div> ``` \<Route> 组件有如下属性: - path&emsp;*String*: 路由匹配路径。(没有 path 属性的 Route 总是会匹配); - exact&emsp;*Boolean*:为 true 时,则要求路径与 location.pathname 必须完全匹配; - strict&emsp;*Boolean*:true 的时候,有结尾斜线的路径只能匹配有斜线的 location.pathname; **exact 配置:** | 路径 | location.pathname | exact | 是否匹配 | | --- | --- | --- | --- | | /one | /one/two | true | 否 | | /one | /one/two | false | 是 | **strict 配置:** | 路径 | location.pathname | strict | 是否匹配 | | --- | --- | --- | --- | | /one/ | /one | true | 否 | | /one/ | /one/ | true | 是 | | /one/ | /one/two | true | 是 | 新版的路由为`<Route>`提供了三种渲染内容的方法 * `<Route component>`:在地址匹配的时候 React 的组件才会被渲染,route props 也会随着一起被渲染; * `<Route render>`:这种方式对于内联渲染和包装组件却不引起意料之外的重新挂载特别方便; * `<Route children>`:与 render 属性的工作方式基本一样,除了它是不管地址匹配与否都会被调用; `<Route component>`的优先级要比`<Route render>`高,所以不要在同一个`<Route>`中同时使用这两个属性。 ```html // 行内渲染示例 <Route path="/home" render={() => <div>Home</div>}/> // 包装/合成 const FadingRoute = ({ component: Component, ...rest }) => ( <Route {...rest} render={props => ( <FadeIn> <Component {...props}/> </FadeIn> )}/> ) <FadingRoute path="/cool" component={Something}/> ``` <span style="font-size: 20px;">\<Link></span> 其有以下两个属性: - to(string/object):要跳转的路径或地址; - replace(bool):为 true 时,点击链接后将使用新地址替换掉访问历史记录里面的原地址;为 false 时,点击链接后将在原有访问历史记录的基础上添加一个新的纪录。默认为 false; 示例如下: ```html // to为string <Link to="/about">关于</Link> // to为obj <Link to={{ pathname: '/courses', search: '?sort=name', hash: '#the-hash', state: { fromDashboard: true } }}/> // replace <Link to="/courses" replace /> ``` <span style="font-size: 20px;">\<Switch></span> 推荐快速浏览这篇文章:[https://www.jianshu.com/p/d5173d7b411a](https://www.jianshu.com/p/d5173d7b411a) \<Swich> 组件可以保证其包含的路由组件在路径相同 / 上级路径相同的情况下只匹配一个,避免重复匹配和渲染不需要的组件。 思考下面的代码: ```html <Route path="/about" component={About}/> <Route path="/:user" component={User}/> <Route component={NoMatch}/> ``` 如果现在的 URL 是`/about`,那么`<About>`, `<User>`, 还有`<NoMatch>`都会被渲染,因为它们都与路径(path)匹配。这种设计,允许我们以多种方式将多个`<Route>`组合到我们的应用程序中,例如侧栏(sidebars),面包屑(breadcrumbs),bootstrap tabs 等等。 然而,偶尔我们只想选择一个`<Route>`来渲染。如果我们现在处于`/about`,我们也不希望匹配`/:user`(或者显示我们的 “404” 页面 )。以下是使用 Switch 的方法来实现: ```html <Switch> <Route exact path="/" component={Home}/> <Route path="/about" component={About}/> <Route path="/:user" component={User}/> <Route component={NoMatch}/> </Switch> ``` 现在,如果我们处于`/about`,`<Switch>`将开始寻找匹配的`<Route>`。`<Route path="/about"/>`将被匹配,`<Switch>`将停止寻找匹配并渲染`<About>`。同样,如果我们处于`/michael`,`<User>`将被渲染。 ## 匹配语法 路由路径是匹配一个(或一部分)URL 的`一个字符串模式`。大部分的路由路径都可以直接按照字面量理解,除了以下几个特殊的符号: * `:paramName`– 匹配一段位于`/`、`?`或`#`之后的 URL。 命中的部分将被作为一个参数 * `()`– 在它内部的内容被认为是可选的 * ` *`– 匹配任意字符(非贪婪的)直到命中下一个字符或者整个 URL 的末尾,并创建一个`splat`参数 ```html <Route path="/hello/:name"> // 匹配 /hello/michael 和 /hello/ryan <Route path="/hello(/:name)"> // 匹配 /hello, /hello/michael 和 /hello/ryan <Route path="/files/*.*"> // 匹配 /files/hello.jpg 和 /files/path/to/hello.jpg ``` ## 路由跳转 虽然在组件内部可以使用`this.context.router`来实现导航,但许多应用想要在组件外部使用导航。使用 Router 组件上被赋予的 history 可以在组件外部实现导航。 ```js // your main file that renders a Router import { Router, browserHistory } from 'react-router' import routes from './app/routes' render(<Router history={browserHistory} routes={routes}/>, el) ``` ```js // somewhere like a redux/flux action file: import { browserHistory } from 'react-router' browserHistory.push('/some/path') ``` 使用 Link 或者在 js 方法中实现跳转 ```js import React from 'react' import { Link } from 'react-router-dom' class First extends React.Component { constructor(props) { super(props) } handleBtnClick() { this.props.history.push('/child2') } render() { return ( <h1> <Link to="/child1">this is first route</Link><!--Link 组件相当于<a>标签,会有默认样式--> <button onClick={this.handleBtnClick}>点我跳转</button> </h1> ) } } ``` 有时候组件内部找不到 this.props.history,因为 history 的对象没有由父组件传递过来,这就需要使用 withRouter 做连接,代码如下: ```js import { withRouter } from 'react-router-dom' // 需要对该组件做如下处理(这是与 redux 连接的同时又使用 withRouter 的情况) export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Header)) // js 方法实现路由跳转 this.props.history.push('/') // 简单来说就是通过某种方式(context?)把 history 给传递到这个组件了 ``` ## 参数传递 参考链接:[https://www.cnblogs.com/yky-iris/p/9161907.html](https://www.cnblogs.com/yky-iris/p/9161907.html) 1、params 传参,定义路由时以动态路径匹配的形式定义,跳转到的组件通过 `this.props.match.params`获取匹配到的字符串 ```js // Route 定义形式 <Route path="/path/:name" component={Path} /> // Link 组件 <Link to="/path/jojo">跳转</Link> // 参数获取 this.props.match.params.name ``` 2、query 形式,跳转到的组件中通过`this.props.location.query`获取 ```js // Route 定义方式 <Route path="/user" component={User} /> // Link 组件 <Link to={{ pathname: '/user', query: { name: jack }, state: { value: 123 }, search: '?sort=name', hash: '#the-hash', abc: 'def' }} /> // JS 方式 this.props.history.push({ pathname: '/user', query: { name: jack }, state: { value: 123 }, search: '?sort=name', hash: '#the-hash', abc: 'def' }) // home 页面接收参数 this.props.location.query.name // jack this.props.location.state.value  // 123 this.props.location.search  // ?sort=name this.props.location.hash  // #the-hash this.props.location.abc  // def (自定义参数) ``` # Redux ## 安装 ``` npn install --save redux npm install --save react-redux // react 绑定库 npm install --save-dev redux-devtools // redux 开发者工具 ``` React 只是一个视图层框架,Redux 是数据层框架 Redux = Reducer + Flux ![](https://box.kancloud.cn/311b5e362aedf61d867c6e7b9a89f389_433x251.png =300x200) ![](https://box.kancloud.cn/23200e084e7147c03fd5718d9426a7e2_392x242.png =280x200) 建议将 demo 下载后再阅读后面的内容:[https://github.com/ChenMingK/demos/tree/master/redux-demo](https://github.com/ChenMingK/demos/tree/master/redux-demo) ## 三个基本原则: Ⅰ、单一数据源:整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。 ```js console.log(store.getState()) /* 输出 { visibilityFilter: 'SHOW_ALL', todos: [ { text: 'Consider using Redux', completed: true, }, { text: 'Keep all state in a single tree', completed: false } ] } */ ``` Ⅱ、State 是只读的:唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的 JavaScript 对象,其必须有一个 type 属性表示将要执行的动作,一般会定义为字符串常量。 ```js store.dispatch({ type: 'COMPLETE_TODO', // 一般会将这些字符串常量写在一个单独的文件如 action-type.js 中,定义为字符串常量的好处是书写错误会报错 index: 1 }) store.dispatch({ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_COMPLETED' }) ``` Ⅲ、使用纯函数来进行修改:为了描述 action 是如何改变 state tree,需要编写 reducers;Reducer 只是一些纯函数,它接收先前的 state 和 action,并返回新的 state。刚开始你可以只有一个 reducer,随着应用变大,你可以把它拆成多个小的 reducers,分别独立地操作 state tree 的不同部分。 ``` function visibilityFilter(state = 'SHOW_ALL', action) { switch (action.type) { case 'SET_VISIBILITY_FILTER': return action.filter default: return state } } function todos(state = [], action) { switch (action.type) { case 'ADD_TODO': return [ ...state, { text: action.text, completed: false } ] case 'COMPLETE_TODO': return state.map((todo, index) => { // 一个 reducer 返回的应该是什么?reducer 拆分时各自返回的又是什么? if (index === action.index) { return Object.assign({}, todo, { completed: true }) } return todo }) default: return state } } import { combineReducers, createStore } from 'redux' let reducer = combineReducers({ visibilityFilter, todos }) let store = createStore(reducer) ``` 注意以下几个要点: 1. 不要修改`state`。使用`Object.assign()`新建了一个副本。不能这样使用`Object.assign(state, { visibilityFilter: action.filter })`,因为它会改变第一个参数的值。你**必须**把第一个参数设置为空对象。你也可以开启对 ES7 提案的支持, 从而使用`{ ...state, ...newState }`达到相同的目的。 `Object.assign(target, source1, source2)`:该方法用于将源对象的所有可枚举属性复制到目标对象,第一个参数是目标对象,其余的都是源对象 2. 在`default`情况下返回旧的`state`。遇到未知的 action 时,一定要返回旧的`state`。 使用`combineReducers()`工具类来拆分 reducers,每个 reducer 只负责管理全局 state 中它负责的一部分,每个 reducer 的 state 参数都不同,分别对应它管理的那部分 state 数据 ``` import { combineReducers } from 'redux' const todoApp = combineReducers({ visibilityFilter, todos }) export default todoApp ``` ## 搭配React Redux 和 React 之间没有关系。Redux 支持 React、Angular、Ember、jQuery 甚至纯 JavaScript。只是它俩搭配起来很好用罢了 安装 React Redux:`npm install --save react-redux` 简单来说,这个库让我们很方便地将组件连接到 Redux 并建立映射关系 使用指定的 React Redux 组件`<Provider>`来让所有容器组件都可以访问 store,而不必显示地传递它。只需要在渲染根组件时使用即可。 ```js import React from 'react' import { render } from 'react-dom' import { Provider } from 'react-redux' import { createStore } from 'redux' import todoApp from './reducers' import App from './components/App' let store = createStore(todoApp) render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') ) ``` 组件中使用如下的方式来“建立连接”: ``` import { connect } from 'react-redux' import { setVisibilityFilter } from '../actions' import Link from '../components/Link' // 建立映射关系后可通过 this.props.state 来访问到 Store 中的状态,第二个参数是自身的 props? const mapStateToProps = (state, ownProps) => { return { active: ownProps.filter === state.visibilityFilter } } // 之后我们便可以调用 this.props.onClick 来触发这个 dispatch const mapDispatchToProps = (dispatch, ownProps) => { return { onClick: () => { dispatch(setVisibilityFilter(ownProps.filter)) } } } const FilterLink = connect( mapStateToProps, mapDispatchToProps )(Link) export default FilterLink ``` connect 是 react-redux 提供的一个方法,这个方法接收两个参数,执行结果依然是一个函数,所以才可以在后面又加一个圆括号(柯里化?),把 connect 函数执行的结果立即执行。 这里有两次函数的执行,第一次是 connect 函数的执行,第二次是把 connect 函数返回的函数再次执行,最后产生的就是容器组件。 connect 函数具体做了哪些事呢? * 把 Store 上的状态转化为内层傻瓜组件的 prop * 把内层傻瓜组件中的用户动作转化为派送给 Store 的动作 这两个工作一个是内层傻瓜对象的输入,一个是内层傻瓜对象的输出 `mapStateToProps`(命名是业界习惯)就是把 Store 上的状态转化为内层组件的 props,建立映射关系 `mapDispatchToProps` 把内层傻瓜组件暴露出来的函数类型的 prop 关联上 dispatch 函数的调用 ## 使用 redux-thunk 中间件 安装:`npm install --save redux-thunk` 要使用中间件,都要在创建 store 的时候进行“注册”,创建 store 的文件模板大致如下: ```js import { createStore, compose, applyMiddleware } from 'redux' import thunk from 'redux-thunk' import reducer from './reducer' // combineReducers() 将多个 reducer 合并成一个后导入 // 开发者工具 const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; const store = createStore(reducer, composeEnhancers( applyMiddleware(thunk) // 使用 redux-thunk 中间件 )) export default store ``` `createStore()`的第二个参数是可选的, 用于设置 state 初始状态。这对开发同构应用时非常有用,服务器端 redux 应用的 state 结构可以与客户端保持一致, 那么客户端可以将从网络接收到的服务端 state 直接用于本地数据初始化。 ``` let store = createStore(todoApp, window.STATE_FROM_SERVER) ``` 把状态存放在组件中并不是一个很好的选择, Redux 本身就是用来帮助管理应用状态的,应该尽量把状态存放在 Redux Store 中。 Redux 本身的设计理念是不允许异步操作的,所以就需要中间件如 redux-thunk、redux-saga 在 Redux 架构下,一个 action 对象在通过 store.dispatch 派发,在调用 reducer 函数之前,就会先经过一个中间件的环节,这就是产生异步操作的机会 ![](https://box.kancloud.cn/74a868650bc5c1dbd21501a90f9f371c_959x402.png) redux-thunk 的工作是检查 action 对象是不是函数,如果不是函数就放行,完成普通 action 对象的生命周期,而如果发现 action 对象是函数,那就执行这个函数,并把 Store 的 dispatch 函数和 getState 函数作为参数传递到函数中去,不会让这个异步 action 对象继续往前派发到 reducer 函数 举一个简单的例子来介绍异步 action: ```js const increment = () => ({ type: ActionTypes.INCREMENT }) const incrementAsync = () => { return dispatch => { setTimeout(() => { dispatch(increment()) }, 1000) } } ``` 这个函数被 dispatch 函数派发之后,会被 redux-thunk 中间件执行,于是 setTimeout 函数就会发生作用,在 1s 后利用参数 dispatch 函数派发出同步 action 构造函数 increment 的结果。应用到发送 AJAX 请求中就是:action 对象函数可以通过 fetch 发起一个对服务器的异步请求,当得到服务器结果之后,通过参数 dispatch 把成功或者失败的结果当作 action 对象再派发到 reducer 上(这次发送的是普通的 action 对象),最终驱动 Store 上状态的改变。 虽然大致了解了原理,但使用时还要注意设计异步操作的模式,如设计 action: 一个访问服务器的 action,至少要设计到三个 action 类型: - 表示异步操作已经开始的 action 类型 - 表示异步操作成功的 action 类型 - 表示异步操作失败的 action 类型 当这三种类型的 action 对象被派发时,会让 React 组件进入各自不同的三种状态:(组件根据状态来渲染不同的视图) - 异步操作正在进行中 - 异步操作已经成功完成 - 异步操作已经失败 ```js import {FETCH_STARTED, FETCH_SUCCESS, FETCH_FAILURE} from './actionTypes.js'; // 返回 type 字段以驱动 reducer 函数去改变 Redux Store 上的某个字段的状态,从而驱动对应的 React 组件重新渲染 export const fetchWeatherStarted = () => ({ type: FETCH_STARTED }); export const fetchWeatherSuccess = (result) => ({ type: FETCH_SUCCESS, result }) export const fetchWeatherFailure = (error) => ({ type: FETCH_FAILURE, error }) export const fetchWeather = (cityCode) => { return (dispatch) => { const apiUrl = `/data/cityinfo/${cityCode}.html`; dispatch(fetchWeatherStarted()) // 派发一个普通 action 对象,将视图置于"有异步 action 还未结束"的状态 return fetch(apiUrl).then((response) => { if (response.status !== 200) { throw new Error('Fail to get response with status ' + response.status); } response.json().then((responseJson) => { dispatch(fetchWeatherSuccess(responseJson.weatherinfo)); // 派发一个表示请求成功的普通 action 对象 }).catch((error) => { dispatch(fetchWeatherFailure(error)); // 派发一个表示请求失败的普通 action 对象 }); }).catch((error) => { dispatch(fetchWeatherFailure(error)); }) }; } ``` 再来看下 reducer 函数如何处理 ``` import {FETCH_STARTED, FETCH_SUCCESS, FETCH_FAILURE} from './actionTypes.js'; import * as Status from './status.js'; /* ./status.js export const LOADING = 'loading'; export const SUCCESS = 'success'; export const FAILURE = 'failure'; */ export default (state = {status: Status.LOADING}, action) => { switch(action.type) { case FETCH_STARTED: { return {status: Status.LOADING}; } case FETCH_SUCCESS: { return {...state, status: Status.SUCCESS, ...action.result}; } case FETCH_FAILURE: { return {status: Status.FAILURE}; } default: { return state; } } } ``` 异步 action 构造函数的模板: ``` export const sampleAsyncAction = () => { return (dispatch, getState) => { // 在这个函数里可以调用异步函数,自行决定在合适的时机通过 // 参数派发出新的 action 对象 } } ``` # React-Redux 实现原理浅析 详细分析过程见 [掘金-React-Redux 源码分析](https://juejin.im/post/59cb5eba5188257e84671aca#heading-29) react-redux 库提供`Provider`组件通过 context 方式向应用注入 store,然后可以使用`connect`高阶方法,获取并监听 store,然后根据 store state 和组件自身 props 计算得到新 props,注入该组件,并且可以通过监听 store,比较计算出的新 props 判断是否需要更新组件。 ![](https://img.kancloud.cn/c1/b4/c1b4d14bf8601691dc4cd20882753fe4_827x538.png) - Provider 使用的是官方提供的 Context API? - connect 做了什么?如何实现的? - 如何监听数据变化?