react-router 4 升級攻略
react-router 從 2 / 3 升級到 4.x.x
1. 說明
react-router 版本更新到 4.x.x 已經有一段時間了,但是由於 API 改動較大,為了專案的穩定,一直沒有更新,最近有了空閒時間,整體對 react 的專案進行了一次升級,這裡記錄一下 如何從 react-router 2.x.x/3.x.x 遷移到 react-router 4.x.x
本次使用 react-router 版本為 4.1.0
導航
- 說明
- 重要檔案版本
- 新的API說明
- 從 2 / 3 遷移到 4.x.x
2. 重要檔案版本
{
"postcss-loader": "^1.3.3",
"react-hot-loader": "^3.0.0-beta.7",
"webpack": "^2.6.1",
"webpack-dev-server": "^2.5.0",
"webpack-merge": "^4.1.0",
"es6-promise": "^4.1.0",
"history": "^4.6.3",
"isomorphic-fetch": "^2.2.1",
"pure-render-decorator ": "^1.2.1",
"react": "^15.6.1",
"react-addons-css-transition-group": "^15.6.0",
"react-dom": "^15.6.1",
"react-redux": "^5.0.5",
"react-router-dom": "^4.1.1",
"react-router-redux": "^5.0.0-alpha.6",
"react-tap-event-plugin": "^2.0.1",
"redux ": "^3.7.0",
"redux-thunk": "^2.2.0"
}
注意:有很多 webpack 配置需要的外掛沒有列出,因為改動不是很大,對專案執行沒有很大的影響,具體的 package.json 可以看原始碼,這個是舊版的模板,新版本會在近期提交(新版原始碼)
postcss-loader
外掛 最新版本已經到了 2.x.x ,但是最新版與 1.x.x配置使用上有出入,如果使用新版本遇到報錯,可以檢視 官方文件 或者 這篇部落格, 大概意思就是不能像之前一樣使用webpack.LoaderOptionsPlugin
配置 這個外掛 例如 配置autoprefixer
,可以新增一個postcss.config.js
配置檔案,或者 在 寫成下邊這樣,我這裡還是使用的 1.x.x 版本
{
loader:"postcss-loader",
options: {
sourceMap: true,
plugins: (loader) => [
require('autoprefixer')(),
]
}
}
webpack
使用的是 2.x.x 的版本,因為測試的時候出了點 BUG ,所以沒有升級到最新的 3.x.xreact
相關元件都升級到最新版本,其中 為配合 react-router 4 的使用react-router-redux
使用的是5.0.0-alpha.6
, 而且增加了history
元件react-router-dom
程式碼是基於react-router
的 而且做了升級,只用下載react-router-dom
就可以了
3. 新的 API 說明
在這裡檢視官方文件
1. <Router>
Router 是所有路由元件共用的底層介面,用來與Redux狀態管理庫的history 保持同步,但是在
4.x.x
版本中一般不使用這個元件,而是使用<BrowserRouter> <HashRouter>
等
<Router history={history}>
<App/>
</Router>
2. <BrowserRouter>
使用 HTML5 提供的 history API 來保持 UI 和 URL 的同步,一下是他的屬性
basename: 設定基準的 URL,使用場景:應用部署在 伺服器的二級目錄,將其設定為目錄名稱,不能以
/
結尾,設定之後跳轉的頁面都會加上 basename的字首forceRefresh: 是否強制重新整理頁面,用於適配不支援 h5 history 的瀏覽器
getConfirmation: 彈出瀏覽器的確認框,進行確認
3. <Route>
與 2 / 3 版本的 Route 作用一致,都是在 location 匹配 path 的路徑的時候,渲染指定元件,但是寫法上有變化,而且增加了一些設定
注意:4.x.x 版本的
<Route>
不在有onEnter onLeave
這樣的路由鉤子函式,如果需要這個功能,要在 Route 對應元件中 寫在componentWillMount
或者componentWillUnmount
中渲染內容方法-1:
component
類似 2 / 3 中的 component 屬性,值為一個 react 元件,只有地址匹配的時候才會渲染元件
// 定義元件
class App extends React.Component {
constructor(props) {
super(props);
}
render() {
return <span>App</span>;
}
}
// Route
<Route component={App}/>
- 渲染內容方法-2:
render
值可以選擇傳一個在地址匹配時被呼叫的函式,而不是建立一個元件,但是需要一個返回值,返回一個元件或者null
<Route render={(props) => (
<App>
<SomeCom>
</App>
)/>
渲染內容方法-3:
children
與render
一樣,但是不會匹配地址,路徑不匹配時 URL的match 值為 null,可以用來根據路由是否匹配動態調整UI繫結在 Route 上的元件或者函式都會獲得以下的屬性
component
繫結的元件可以通過 this.props.[history, location, match] 獲取;render, children
下的函式;會得到 param = {match, location, history} 的引數
// 虛擬碼
history: {
goBack(), // 瀏覽器回退
goForward(), // 前進
push(), // 新增跳轉
replace(), // 覆蓋跳轉
}
location: {
hash, // hash (#123)
pathname, // 路徑 (/home)
search, // (?id=123)
state, // ({name: 'name', id: 'id'})
// query, // 4.x.x 沒有 query 了,要從 search 中解析
}
match: {
isExact, // 是否整個URL都需要匹配
params, // 路徑引數,通過解析URL中動態的部分獲得的鍵值對
path, // 路徑格式
url, // URL匹配的部分
params, //
}
path
屬性, 用於匹配 location 路徑 如果沒有 path 屬性則總是匹配exact
屬性 是否需要完全匹配 ,不會判斷末尾的/
/*
以下兩個就是不完全匹配,只有前半部分 一致 /one
如果沒有設定 exact=true,則可以進入到 path 對應的元件,
如果設定 exact 則 不能渲染元件
*/
path: /one
location.pathname : /one/two
strict
屬性 用來強制判斷路徑結尾是否含有/
, 只有path 和 loation.pathname 的結尾都含有或者都不含有/
才會匹配關於路由巢狀,4.x.x 不能像以前的版本那樣嵌套了,需要新的方式,會在下邊的 從 2 / 3 遷移到 4.x.x 中有演示
4. <Redirect>
與 2 / 3 版本一樣都是用來重定向到新的地址,預設會覆蓋訪問記錄中的原地址,但是多了一些屬性
to={string|object}
重定向的目標,可以是字串,也可以是一個地址的物件from={string}
匹配需要重定向的地址push
bool 表示是否需要不替換地址,值為true的時候,不會把訪問記錄中的地址覆蓋
<Redirect push to={{pathname: '/login', search: '?id=123'}}>
5. <Switch/>
用來渲染匹配地址的第一個 或者 , 可以用來配置過度動畫,更多介紹看這裡
6. <Link>
與 2 / 3 相同,增加了屬性 replace 表示是否替換掉原地址
7. withRouter
很重要的一個功能,將普通元件用
withRouter
包裹後,元件就會獲得 location history match 三個屬性,可以用來直接為子元件提供 歷史相關的功能
export default withRouter(App);
// App.js
const {history, location, match} = this.props;
4. 從 2 / 3 遷移到 4.x.x
由於API 改動比較大,使用方式也有變化,因此
1. 構建工具
webpack 的配置檔案方面沒有什麼需要改動的,如果你的外掛
postcss-loader
更新了,則需要修改一下配置部分
// webpack.config.js
rules: [
{
test: /\.(scss|sass|css)$/,
use: [
'style-loader',
'css-loader',
{
loader: 'postcss-loader',
options: {
sourceMap: false,
plugins: (loader) => [
autoprefixer(config.autoConfig) // autoConfig 為 autoprefixer 的配置
]
}
},
'sass-loader' + config.sassLoaderSuffix // sass的配置
]
}
]
...
// 取消掉之前的 postcss 外掛配置
plugins: [
// new webpack.LoaderOptionsPlugin({
// options: {
// context: '/',
// minimize: true,
// postcss: [autoprefixer(config.autoConfig)]
// }
// }),
]
2. app 入口檔案 app/js/index.js
改動不大
/*
app/js/index.js
入口檔案, 配置 webpack 熱載入模組
*/
import '../scss/index.scss'; // 引入樣式
import React from 'react';
import ReactDOM from 'react-dom';
import {AppContainer} from 'react-hot-loader';
import injectTapEventPlugin from 'react-tap-event-plugin';
// 引入路由配置模組
import Root from './routes.js';
const mountNode = document.getElementById('app'); // 設定要掛在的點
// react 的外掛,提供onTouchTap() // 更新後沒有測試是否好使
injectTapEventPlugin();
// 封裝 render
const render = (Component) => {
ReactDOM.render((
<AppContainer>
<Component/>
</AppContainer>
), mountNode);
};
render(Root); // 初始化
console.log(process.env.NODE_ENV);
if (module.hot && process.env.NODE_ENV !== 'production') {
module.hot.accept('./routes.js', (err) => {
if (err) {
console.log(err);
}
/*
解除安裝 react 模組後 重灌
*/
ReactDOM.unmountComponentAtNode(mountNode);
render(Root);
});
}
3. Root 基礎的路由配置檔案 app/js/routes.js
這部分變動很大,上一個版本還有一個叫Root.js 的檔案 用來協助配置 路由,這個版本合併在一起
/*
Root, Router 配置
*/
import React from 'react';
import {Provider} from 'react-redux';
import {
/*
注意:這裡也可以使用 BrowserRouter ,但是為了配合 redux 使用,引入了 react-router-redux,提供了一個用來關聯的 ConnectedRouter
*/
// BrowserRouter as Router,
Route,
Switch,
Redirect
} from 'react-router-dom';
import {ConnectedRouter} from 'react-router-redux'; // 版本需要 5.0.0-alpha.6
import createHistory from 'history/createBrowserHistory';
import store from './store/index';
import {App, Home, Test} from './containers/index';
const history = createHistory();
// Router 下邊只能有一個節點
const Root = () => (
<Provider store={store}>
<ConnectedRouter history={history} >
<div>
<Switch>
<Route path="/" render={(props) => (
<App>
<Switch>
<Route path="/" exact component={Home}/>
<Route path="/home" component={Home}/>
<Route path="/test" component={Test}/>
<Redirect from="/undefined" to={{pathname: '/', search: '?mold=redirect'}}/>
</Switch>
</App>
)}/>
<Route render={() => (<Redirect to="/"/>)}/>
</Switch>
</div>
</ConnectedRouter>
</Provider>
);
export default Root;
4. 路由巢狀的寫法
4.x.x 定義每一個 Route 都是普通的 react 元件,所有使用的時候也要像普通的元件一樣,巢狀的時候,可以按照下邊的方式
// 定義外層路由
const Root = () => {
<Provider store={store}>
<ConnectedRouter history={history} >
<Route path="/" exact component={App}/>
</ConnectedRouter>
</Provider>
}
// App.js 元件中定義在App下的路由
import {Route, Link} from 'react-router-dom';
class App extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div id="app-container">
<header className="app-header">成員列表</header>
<div className="app-body">
<Link to="/test">點選進入 Test 頁面</Link>
<Route path="/test" component={Test}/>
</div>
</div>
);
}
}
上邊這樣的寫法,與沒有升級前的寫法變化很大,但是更加靈活了,不過如果用來版本遷移,工作量會很大,下邊是一個用於相容上個版本程式碼的寫法,可以把所有路由集中在一個檔案中管理
// 方法就是在需要有子級路由的地方使用 render 方法,方法中使用父級路由對應的元件將其他子級路由包裹住
const Root = () => (
<Provider store={store}>
<ConnectedRouter history={history}>
<div>
<Switch>
<Route path="/" render={(props) => (
<App>
<Switch>
<Route path="/" exact component={Login}/>
<Route path="/login" component={Login}/>
<Route path="/unable" component={Unable}/>
<Route path="/show" render={(props) => (
<Show>
<Switch>
<Route path="/show" exact component={Home}/>
<Route path="/show/test" component={Test}/>
</Switch>
</Show>
)}/>
<Route path="/404" component={NotFoundPage}/>
</Switch>
</App>
)}/>
</Switch>
</div>
</ConnectedRouter>
</Provider>
);
5. store 的配置 app/js/store
增加了 router 的中介軟體
// store/configureStore.js
import { compose, createStore, applyMiddleware } from 'redux';
import { routerMiddleware } from 'react-router-redux'; // 新增 route 中介軟體
// 引入thunk 中介軟體,處理非同步操作
import thunk from 'redux-thunk';
import createHistory from 'history/createBrowserHistory'; // 引入 history
export const history = createHistory();
const routeMiddleware = routerMiddleware(history);
const middleware = [routeMiddleware, thunk];
/*
輔助使用chrome瀏覽器進行redux除錯
*/
const composeEnhancers =
process.env.NODE_ENV !== 'production' &&
typeof window === 'object' &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
// Specify here name, actionsBlacklist, actionsCreators and other options
}) : compose;
/*
呼叫 applyMiddleware ,使用 middleware 來增強 createStore
*/
const configureStore = composeEnhancers(
applyMiddleware(...middleware)
)(createStore);
export default configureStore;
// store/index.js
import configureStore from './configureStore';
import reducer from '../reducers';
// 給增強後的store傳入reducer
const store = configureStore(reducer);
export default store;
6. reducer 配置 app/js/reducers/index.js
與升級前沒有區別
// 引入reducer
import {combineReducers} from 'redux';
import {routerReducer} from 'react-router-redux';
import rootReducer from './rootReduer';
// 合併到主reducer
const reducer = combineReducers({
rootReducer,
routing: routerReducer
});
export default reducer;
7. 元件中使用
寫在
<Router component={Component}>
中的元件自動獲得,location,history,match 三個屬性普通元件使用 需要通過父元件傳遞或者通過withRouter
import React, { Component } from 'react';
import {withRouter} from 'react-router-dom';
import '../../../scss/app.scss';
class AppCom extends Component {
constructor(props) {
super(props);
}
render() {
// 獲得的 location 屬性
const path = this.props.location.pathname;
return (
<div id="app-container">
<header className="app-header">地址:{path}</header>
<div className="app-body">
{this.props.children}
</div>
</div>
);
}
}
export default withRouter(AppCom)
8. 手動跳轉頁面與路由鉤子函式onEnter onLeave
上一個版本中大量使用了
browserHistory
進行頁面跳轉,這個版本中需要使用 傳入的 history 來進行跳轉新版本中路由的onEnter onLeave 方法取消,可以通過元件的 componentWillMount 和 componentWillUnmount 實現
import React, { Component } from 'react';
import {withRouter} from 'react-router-dom';
import confPut from './utils/confPut';
import '../../../scss/home.scss';
class TestCom extends Component {
constructor(props) {
super(props);
}
componentWillMount() {
// 代替 原 Route 元件的 onEnter()
}
componentWillUnmount() {
// 代替 原 Route 元件的 onLeave()
}
handleClick = () => {
confPut('add', 'will');
this.props.history.push({
pathname: '/home',
search: '?name=testname'
});
}
render() {
console.log(this.props);
return (
<div className="test-container">
this is Test Page
<button onClick={this.handleClick}>點選回到</button>
</div>
);
}
}
export default withRouter(TestCom);
5. 總結
版本升級工作還在進行中,應該還會有其他的問題出現,這裡只是一個階段的記錄,還有一些像 code splitting 還沒有嘗試,留著下一階段試驗。