YutHelloWorld / Blog

🌎 技术自留地
132 stars 17 forks source link

项目实践:从react-router v3迁移到v4 #5

Open YutHelloWorld opened 7 years ago

YutHelloWorld commented 7 years ago

前言

今年3月初发布了react-router v4,相较之前的v3和v2版本做了一个破坏性的升级。遵循一切皆React Component的理念。静态路由变成了动态路由。这里记录下v3项目如何迁移到v4。 项目地址:https://github.com/YutHelloWorld/vortex-react

迁移步骤

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导出,在后面会需要用到。

history API详见: https://github.com/ReactTraining/history

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在创建的时候在内部已经引入了一个historyupdateLocation(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版本通过getComponetrequire.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
}
  1. 这个asyncComponent 函数接受一个importComponent 的参数,importComponent调用时候将动态引入给定的组件。
  2. componentDidMount 我们只是简单地调用importComponent 函数,并将动态加载的组件保存在状态中。
  3. 最后,如果完成渲染,我们有条件地提供组件。在这里我们如果不写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 移除了onEnter onLeave等属性,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 🤣 。

参考

yumo-mt commented 7 years ago

感觉v2/v3 相当于静态路由,必须在一个配置文件中,作为entry来打包,v4 相当于动态路由,可以和JSX随意结合,比较好。

YutHelloWorld commented 7 years ago

用过express router的同学应该会对配置路由比较熟悉。react-router v4就是遵循组件化的思想,对于react入门的新手而言会相对容易理解些。个人认为以后的趋势还是会慢慢迁移到v4的。 @rongchanghai

YutHelloWorld commented 7 years ago

这里补充点:state.location中在V4会缺少个action字段,这个action的值有'PUSH','REPLACE','POP'三种。详细看这个commit: YutHelloWorld/vortex-react@b9a354e

baiyunshenghaishang commented 6 years ago

将路由写在组件里面真的好嘛,有什么优势?个人觉得反而将将路由配置写在一个文件里,整个应用的页面架构一目了然,更清晰。

YutHelloWorld commented 6 years ago

写在组件里也能够看到整个应用的页面结构啊,例如:

function Layout() {
  return (
    <div className="layout-c">
      <Navbar />
      <Switch>
        <Route exact path="/" component={OrderList} />
        <Route exact path="/admin" component={ProductList} />
        <Route exact path="/competition" component={AsyncCst} />
        <Route path="/admin/add" component={AsyncAdd} />
        <Route
          path="/admin/detail/:goodsType/:goodsId"
          component={ProductDetail}
        />
        <Route path="/admin/:type" component={AdminType} />
        <Route path="/detail/:orderId" component={OrderDetailContent} />
        <Route path="/competition/orderPool" component={CmpOrderList} />
        <Route render={() => <div className="layout-content" />} />
      </Switch>
      <Footer />
    </div>
  );
}
fengyun2 commented 6 years ago

@YutHelloWorld ,在管理后台中想要根据不同的角色从后台返回的菜单,动态生成侧边栏菜单该如何配置呢?望指教。

liuxiaojiu commented 6 years ago

谢谢,在写V2的时候遇到问题搜到这个了,不过还是很不错,可以考虑自己玩耍下V4

JustinXu0223 commented 6 years ago

在v3中 我可以将相同的layout布局放在一个顶层组件中,然后通过this.props.children,内部组件加载可以通过content得到布局样式。但是v4让我很不适应。可能不论是vue目前还是ng依然是路由集中配置。