[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 *String*: 路由匹配路徑。(沒有 path 屬性的 Route 總是會匹配);
- exact *Boolean*:為 true 時,則要求路徑與 location.pathname 必須完全匹配;
- strict *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
 
建議將 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 函數之前,就會先經過一個中間件的環節,這就是產生異步操作的機會

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 判斷是否需要更新組件。

- Provider 使用的是官方提供的 Context API?
- connect 做了什么?如何實現的?
- 如何監聽數據變化?
- 序言 & 更新日志
- H5
- Canvas
- 序言
- Part1-直線、矩形、多邊形
- Part2-曲線圖形
- Part3-線條操作
- Part4-文本操作
- Part5-圖像操作
- Part6-變形操作
- Part7-像素操作
- Part8-漸變與陰影
- Part9-路徑與狀態
- Part10-物理動畫
- Part11-邊界檢測
- Part12-碰撞檢測
- Part13-用戶交互
- Part14-高級動畫
- CSS
- SCSS
- codePen
- 速查表
- 面試題
- 《CSS Secrets》
- SVG
- 移動端適配
- 濾鏡(filter)的使用
- JS
- 基礎概念
- 作用域、作用域鏈、閉包
- this
- 原型與繼承
- 數組、字符串、Map、Set方法整理
- 垃圾回收機制
- DOM
- BOM
- 事件循環
- 嚴格模式
- 正則表達式
- ES6部分
- 設計模式
- AJAX
- 模塊化
- 讀冴羽博客筆記
- 第一部分總結-深入JS系列
- 第二部分總結-專題系列
- 第三部分總結-ES6系列
- 網絡請求中的數據類型
- 事件
- 表單
- 函數式編程
- Tips
- JS-Coding
- Framework
- Vue
- 書寫規范
- 基礎
- vue-router & vuex
- 深入淺出 Vue
- 響應式原理及其他
- new Vue 發生了什么
- 組件化
- 編譯流程
- Vue Router
- Vuex
- 前端路由的簡單實現
- React
- 基礎
- 書寫規范
- Redux & react-router
- immutable.js
- CSS 管理
- React 16新特性-Fiber 與 Hook
- 《深入淺出React和Redux》筆記
- 前半部分
- 后半部分
- react-transition-group
- Vue 與 React 的對比
- 工程化與架構
- Hybird
- React Native
- 新手上路
- 內置組件
- 常用插件
- 問題記錄
- Echarts
- 基礎
- Electron
- 序言
- 配置 Electron 開發環境 & 基礎概念
- React + TypeScript 仿 Antd
- TypeScript 基礎
- React + ts
- 樣式設計
- 組件測試
- 圖標解決方案
- Storybook 的使用
- Input 組件
- 在線 mock server
- 打包與發布
- Algorithm
- 排序算法及常見問題
- 劍指 offer
- 動態規劃
- DataStruct
- 概述
- 樹
- 鏈表
- Network
- Performance
- Webpack
- PWA
- Browser
- Safety
- 微信小程序
- mpvue 課程實戰記錄
- 服務器
- 操作系統基礎知識
- Linux
- Nginx
- redis
- node.js
- 基礎及原生模塊
- express框架
- node.js操作數據庫
- 《深入淺出 node.js》筆記
- 前半部分
- 后半部分
- 數據庫
- SQL
- 面試題收集
- 智力題
- 面試題精選1
- 面試題精選2
- 問答篇
- 2025面試題收集
- Other
- markdown 書寫
- Git
- LaTex 常用命令
- Bugs