Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
YutHelloWorld opened this issue Aug 7, 2017 · 8 comments
Open

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

YutHelloWorld opened this issue Aug 7, 2017 · 8 comments

Comments

@YutHelloWorld
Copy link
Owner

YutHelloWorld commented Aug 7, 2017

前言

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

迁移步骤

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

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
Copy link

yumo-mt commented Aug 8, 2017

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

@YutHelloWorld
Copy link
Owner Author

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

@YutHelloWorld
Copy link
Owner Author

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

@baiyunshenghaishang
Copy link

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

@YutHelloWorld
Copy link
Owner Author

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

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
Copy link

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

@liuxiaojiu
Copy link

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

@JustinXu0223
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants