柚木

精读《React Router4.0 进阶概念》

本期精读的文章是:React Router 进阶:嵌套路由,代码分割,转场动画等等

懒得看文章?没关系,稍后会附上文章内容概述,同时,更希望能通过阅读这一期的精读,穿插着深入阅读原文。

1 引言

React Router4.0 出来之前,许多人都对其夸张的变化感到不适,但其实 4.0 说不定真的是一个非常正确的改动。

也许,说 4.0 不好的人,正是另一个消极版的小红点,希望这篇文章可以让大家意识到,React Router4.0 对大多数人真的很棒!

2 内容概要

React Router4.0 正式版发布了,生态也逐渐完善了起来,是时候推一波与其完美结合的实用工具了!

代码分割

通过 react-loadable,可以做到路由级别动态加载,或者更细粒度的模块级别动态加载:

const AsyncHome = Loadable({
    loader: () => import('../components/Home/Home'),
    loading: LoadingPage
})

<Route exact path="/" component={AsyncHome} />

当然上面展示的是 ReactRouter 中的用法,AsyncHome 可以在任何 JSX 中引用,这样就提升到了模块级别的动态加载。

注意,无论是 webpack 的 Tree Shaking,还是动态加载,都只能以 Commonjs 的源码为分析目标,对 node_modules 中代码不起作用,所以 npm 包请先做好拆包。或者类似 antd 按照约定书写组件,并提供一种 webpack-loader 自动完成按需加载。

转场动画

通过 React Router Transition (Ant Motion 也很好用) 可以实现路由级别的动画:

<Router>
  <AnimatedSwitch
    atEnter={{ opacity: 0 }}
    atLeave={{ opacity: 0 }}
    atActive={{ opacity: 1 }}
    className="switch-wrapper"
  >
    <Route exact path="/" component={Home} />
    <Route path="/about/" component={About}/>
    <Route path="/etc/" component={Etc}/>
  </AnimatedSwitch>
</Router>

并提供了一些生命周期的回调,具体可以参考文档。现在动画的思路比较靠谱的也大致是这种:通过添加/移除 class 的方式,利用 css3 做动效。

滚动条复位

当页面回退时,将滚动条恢复到页面最顶部,可以让单页路由看起来更加正常。由于 React Router4.0 中,路由是一种组件,我们可以利用 componentDidUpdate 简单完成滚动条复位的功能:

<Router history={history}>
  <ScrollToTop>
    <div>
      <Switch>
        <Route exact path="/" component={Home} />
        <Route path="/about" component={About} />
        <Route path="*" component={NotFound} />
      </Switch>
    </div>
  </ScrollToTop>
</Router>
@withRouter
class ScrollToTop extends Component {
    componentDidUpdate(prevProps) {
        if (this.props.location !== prevProps.location) {
            window.scrollTo(0, 0)
        }
    }

    render() {
        return this.props.children
    }
}

非通过 Route 渲染的组件,可以通过 withRouter 拿到路由信息,仅当其为 Router 的子元素时有效。

嵌套路由

React Router4.0 嵌套路由与 3.0 不同,是通过组件 Route 的嵌套实现的。

在任何组件,都可以使用如下代码实现嵌套路由:

<Route path={`${this.props.match.url}/:id`} component={NestComponent} />

这样将路由功能切分到各个组件中,我现在的项目甚至已经没有 route.js 文件了,路由由 layout 与各个组件自身承担。这种设计思路与 Nestjs 的描述性路由具有相同的思想 - 在 nodejs 中,我们可以通过装饰器,在任意一个 Action 上描述其访问的 URL:

@POST("/api/service")
async someAction() {}

服务端渲染

浏览器端,需要一个专属的入口文件,使用 BrowserRouterlocation 对接:

<BrowserRouter>
  <App />
</BrowserRouter>

服务器端,BrowserRouter 变成了 StaticRouter

renderToString(
  <StaticRouter
    location={req.url}
    context={context}
  >
  <html>
    <body>
      <App />
    </body>
  </html>
  </StaticRouter>
)

与浏览器不同的是,React Router 无法根据 location 自动判断当前所在页面,而需要你把 req.url 传给 StaticRouter,后续的路由渲染逻辑双端都是通用的。

如果存在跳转 Redirect,会通过 context.url 告诉你,所以后面会跟上跳转处理逻辑:

if (context.url) {
  res.writeHead(301, {
    Location: context.url
  })
  res.end()
} else {
  res.write(markup)
  res.end()
}

3 精读

React Router 从 3.0 到 4.0 的改动,想来想去,认为是对于 URL 这个资源理解的变化。

URL 即浏览器地址,在前端数据化统一的浪潮下,其实 URL 也可以被看作是一种参数,在 React 中即一个 props 属性。

单页应用,如果从传统多页应用角度来思考,可能认为不过是一种体验的优化,或者是一种 “伪单页”,毕竟本质上单页应用只是一个页面而已。但换个角度想想,网站何尝不是一个整体,而网址的变化只是一种状态呢?

当我们做一个 Tabs 组件时,会发觉做得越来越像浏览器原生 tab,当用户给你提需求,在刷新浏览器时,能自动打开上一次打开的 Tab,我们的做法就是将当前打开的 Tab 信息保存在 URL 中,刷新时读取再切换过去。这证明了 URL 表示的就是一种状态。

而页面路由的状态化,是将模拟 Tab 的思路应用到了浏览器级别的 Tab。URL 是一种状态,在前端,可以通过浏览器地址自动获取,在后端,可以通过 req.url 获取,甚至可以手动传入来覆盖。

传统的开发思路:我们为每个 URL 编写独立的页面或者模块。

新的开发思路:URL 是一个状态,代码读取这个状态作出不同展现,展现得完全不同时,可以看作传统模式的页面切换;但还可以做到只有某一块区域展现得不同。

4. 总结

也许 React Router4.0 带给我们的思考是,放下对网页“页面”的刻板印象,其实网站本没有页面,有的只是状态。

讨论地址是:精读《React Router4.0 进阶概念》》 · Issue #43 · dt-fe/weekly

如果你想参与讨论,请点击这里,每周都有新的主题,每周五发布。