前言
今年3月初发布了react-router v4,相较之前的v3和v2版本做了一个破坏性的升级。遵循一切皆React Component的理念。静态路由变成了动态路由。这里记录下v3项目如何迁移到v4。
项目地址:github.com/YutHelloWor…
迁移步骤
- 对React-Router和Redux同步进行重构
- 重写路由
- 代码分割
- 琐碎的API替换
详细代码参阅这个PR
React-Router和Redux同步
这里我们仍然不适用
react-router-redux这个库。为了和react-routerv4版本保持一致,react-router-redux发布了v5.0.0版本,你当然也可以使用它来实现这个功能。
1. 替换依赖包
v3我们引入的是react-router包,在v4我们只引入react-router-dom这个包。安装react-router-dom时会同时安装history。
package.json
- "react-router": "^3.0.0",
+ "react-router-dom": "^4.1.2",
2. 改写对browserHistory的创建和当前location的获取
location.js
// v3
import { browserHistory } from 'react-router'
// 获取当前location
const initialState = browserHistory.getCurrentLocation()
==>
// v4
import createHistory from 'history/createBrowserHistory'
export const history = createHistory()
// Get the current location.
const initialState = history.location
这里替换的是history,和当前location的获取方法。在v3,browserHistory存在于react-router中,而v4把history抽离了出来,提供了createBrowserHistory,createHashHistory,createMemoryHistory三种创建history的方法。v4中创建的history导出,在后面会需要用到。
historyAPI详见: github.com/ReactTraini…
3. 对history绑定监听事件,把location的改变同步到Redux的store中
createStore
// v3
import { browserHistory } from 'react-router'
import { updateLocation } from './location'
store.unsubscribeHistory = browserHistory.listen(updateLocation(store))
updateLocation用来把location的更新同步到store中。
export const updateLocation = ({ dispatch }) => {
return (nextLocation) => dispatch(locationChange(nextLocation))
}
一切似乎都很顺利,接着第一个坑来了
根据historyAPI提供的
// Listen for changes to the current location.
const unlisten = history.listen((location, action) => {
// location is an object like window.location
console.log(action, location.pathname, location.state)
})
修改createStore.js
==>
// v4
import { updateLocation, history } from './location'
// 监听浏览器history变化,绑定到store。取消监听直接调用store.unsubscribeHistory()
store.unsubscribeHistory = history.listen(updateLocation(store))
接着修改app.js
// v3
// ...
import { browserHistory, Router } from 'react-router'
// ...
<Router history={browserHistory} children={routes} />
==>
// ...
import { BrowserRouter, Route } from 'react-router-dom'
// ...
<BrowserRouter>
<div>
<Route path='/' component={CoreLayout} />
</div>
</BrowserRouter>
//...
我们到浏览器中查看,发现URL变化并没有触发updateLocation(store),state并没有变化。
What a f**k!
问题出在BrowserRouter在创建的时候在内部已经引入了一个history,updateLocation(store)应该监听的是内部的这个history。这里贴下BrowserRouter.js的代码
import React from 'react'
import PropTypes from 'prop-types'
import createHistory from 'history/createBrowserHistory'
import { Router } from 'react-router'
/**
* The public API for a <Router> that uses HTML5 history.
*/
class BrowserRouter extends React.Component {
static propTypes = {
basename: PropTypes.string,
forceRefresh: PropTypes.bool,
getUserConfirmation: PropTypes.func,
keyLength: PropTypes.number,
children: PropTypes.node
}
history = createHistory(this.props)
render() {
return <Router history={this.history} children={this.props.children}/>
}
}
export default BrowserRouter
于是,我们放弃使用BrowserRouter,而使用Router。
修改app.js
==>
// v4
import { Router, Route } from 'react-router-dom'
//...
<Router history={history}>
<div>
<Route path='/' component={CoreLayout} />
</div>
</Router>
这样,这个坑算是填上了。也就完成了history和store之间的同步。
重写路由
v4取消了PlainRoute 中心化配置路由。
Route是一个react component。
取消了IndexRoute,通过Switch来组件提供了相似的功能,当<Switch>被渲染时,它仅会渲染与当前路径匹配的第一个子<Route>。
routes/index.js
// v3
//..
export const createRoutes = (store) => ({
path : '/',
component : CoreLayout,
indexRoute : Home,
childRoutes : [
CounterRoute(store),
ZenRoute(store),
ElapseRoute(store),
RouteRoute(store),
PageNotFound(),
Redirect
]
})
//...
==>
// ...
const Routes = () => (
<Switch>
<Route exact path='/' component={Home} />
<Route path='/counter' component={AsyncCounter} />
<Route path='/zen' component={AsyncZen} />
<Route path='/elapse' component={AsyncElapse} />
<Route path='/route/:id' component={AsyncRoute} />
<Route path='/404' component={AsyncPageNotFound} />
<Redirect from='*' to='/404' />
</Switch>
)
export default Routes
//
这里路由的定义方式由PlainRoute Object改写成了组件嵌套形式,在PageLayout.js中插入<Routes />。
代码分割
v3版本通过
getComponet和require.ensure实现代码分割和动态路由。在v4版本,我们新增异步高阶组件,并使用import()替代require.ensure()
Counter/index.js
// v3
import { injectReducer } from '../../store/reducers'
export default (store) => ({
path : 'counter',
/* 动态路由 */
getComponent (nextState, cb) {
/* 代码分割 */
require.ensure([], (require) => {
const Counter = require('./containers/CounterContainer').default
const reducer = require('./modules/counter').default
/* 将counterReducer注入rootReducer */
injectReducer(store, { key : 'counter', reducer })
cb(null, Counter)
}, 'counter')
}
})
首先,新增AsyncComponent.js
import React from 'react'
export default function asyncComponent (importComponent) {
class AsyncComponent extends React.Component {
constructor (props) {
super(props)
this.state = {
component: null,
}
}
async componentDidMount () {
const { default : component } = await importComponent()
this.setState({
component: component
})
}
render () {
const C = this.state.component
return C
? <C {...this.props} />
: null
}
}
return AsyncComponent
}
- 这个
asyncComponent函数接受一个importComponent的参数,importComponent调用时候将动态引入给定的组件。- 在
componentDidMount我们只是简单地调用importComponent函数,并将动态加载的组件保存在状态中。- 最后,如果完成渲染,我们有条件地提供组件。在这里我们如果不写null的话,也可提供一个菊花图,代表着组件正在渲染。
接着,改写Counter/index.js
==>
import { injectReducer } from '../../store/reducers'
import { store } from '../../main'
import Counter from './containers/CounterContainer'
import reducer from './modules/counter'
injectReducer(store, { key : 'counter', reducer })
export default Counter
一旦加载
Counter/index.js,就会把counterReducer注入到Rudecer中,并加载Counter组件。
琐碎API的替换
v4 移除了
onEnteronLeave等属性,history替换router属性,新增match
this.props.router.push('/')
==>
this.props.history.push('/') this.props.params.id
==>
this.props.match.params.id
总结
这里可以看出,使用v4替换v3,对于大型项目并不是一件轻松的事情,有许多小坑要踩,这就是社区很多项目仍然使用v2/v3的原因。笔者认为,v4更符合React的组件思想,于是做了一个实践。最后欢迎指正拍砖,捂脸求star 🤣 。