「译」 你可能不需要 react-router

如果你正好使用 Facebook 的 React.js 工作一段时间,你可能注意到有一些错误的(misconceptions)观点弥漫在 React 的社区。其中有一个观点就是:React 只是 MVC 中的 V,如果进行 Web Application 的开发,需要配合大量的其他类型的库。

实际上,你很少看到 React 开发者使用 MVC 中的 Controller 和 Models。组件化的 UI 架构稳定的掌管着前端社区,如今,越来越少的人在使用 MVC 的模式。

另外一个错误的观点就是认为 React Router(RR) 是 Facebook 的官方路由库。实际上,Facebook 的大部分项目都不会使用 RR。

谈到路由(routing),绝大部分的 Web Application 项目和使用场景,只需要一些小的定制化的库就能做的非常好。在你将这种想法归为异类之前,请让我向你演示一下,如何用不到 50 行的代码实现一个全功能的路由。

首先,像 RR 一样,使用一个组件将路由和客户端的导航绑在一起并不是必须的。这种方式,可以实现真正的 Universal - 在客户端和服务器端,利用同样的 API,使用同样的工作方式。有一个非常赞的 npm 模块叫做「history」,它可以搞定导航这部分(请知悉,这是 HTML5 的 History API的一层封装,被 RR 内置使用)。你只需要在你的项目中初始化这个组件(类)的地方,创建一个 history.js 文件,然后在你的 App 中把它当作一个单例使用:

import createHistory from 'history/lib/createBrowserHistory';  
import useQueries from 'history/lib/useQueries';

export default useQueries(createHistory)();  

从现在开始,你可以引用这个文件,然后在任何你想将一个用户跳转到一个新地址(URL)的时候,使用history.push('/new-page’)就好了,而且还不需要刷新整个页面。在应用的主文件里面(bootstrap code),你可以通过以下方式订阅 URL 改变:

import history from './history';

function render(location) { /* Render React app, read on */ }

render(history.getCurrentLocation()); // render the current URL  
history.listen(render);               // render subsequent URLs  

一个工作在客户端的带有链接的 React 组件可能长这个样:

import React from 'react';  
import history from '../history';

class App extends React.Component {  
  transition = event => {
    event.preventDefault();
    history.push({
      pathname: event.currentTarget.pathname,
      search: event.currentTarget.search
    });
  };

  render() {
    return (
      <ul>
        <li><a href="/" onClick={this.transition}>Home</a></li>
        <li><a href="/one" onClick={this.transition}>One</a></li>
        <li><a href="/two" onClick={this.transition}>Two</a></li>
      </ul>
    );
  }
}

尽管如此,实际上你可能想把 ”transition” 功能单独剥离出来成为一个 React 组件。 观察 React Static Boilerplate (RSB)中的Link 组件。你现在可以将客户端的链接写成

<Link to=”/some-page”>Click</Link>.  

需要在用户离开页面的时候添加一个跳转信息页面?只像 history 模块文档中描述的那样,在组件中 componentDidMount()方法中注册一个 history.listenBefore(..)事件处理器就好了。同样可以给页面切换添加动画。

路由(routing)

你可以直接使用原生 JavaScript 对象来描述路由列表,而不是 JSX,例如:

const routes = [  
  { path: '/', action: () => <HomePage /> },
  { path: '/tasks', action: () => <TaskList /> },
  { path: '/tasks/:id', action: () => <TaskDetails /> }
];

BTW,如果有人知道为什么小伙伴更喜欢使用 JSX 来做一些同 UI 渲染无关的事情,请留个言;

你可以使用 ES2015 + async/await 语法来自己写路由处理器,并没有什么需要像在 RR中一样使用回调函数,比如:

{
  path: '/tasks/:id(\\d+)',
  async action({ params }) {
    const resp = await fetch(`/api/tasks/${params.id}`);
    const data = await resp.json();
    return data && <TaskDetails {...data} />;
  }
}

我所熟悉的绝大部分你情况下,并没有啥需要像 RR 中那样使用嵌套路由。使用嵌套路由会让事情变得比实际上更加复杂,并且会导致非常复杂非常难维护的路由实现问题。据我所知,即使是在 Facebook,在他们非常多的应用中(至少不是所有的项目中),不会在客户端使用嵌套路由。

你可以使用嵌套的 React 组件而不是嵌套路由,比如:

import React from 'react';  
import Layout from '../components/Layout';

class AboutPage extends React.Component {  
  render() {
    return (
      <Layout title="About Us" breadcrumbs="Home > About">
        <h1>Welcome!</h1>
        <p>Here your can learn more about our product.</p>
      </Layout>
    );
  }
}

export default AboutPage;  

这种方式的实现比嵌套路由简单、有更好的灵活性、更偏向直觉并且可以打开更多的使用场景(注意你可以将 Breadcrumbs 的组件传递到 Layout 中)

路由本身可以被写成一对函数(两个) - matchURI(),一个用来辅助比较参数化路径和实际路径字符串的内部(private)的函数;Resolve()函数,用来查询路由列表,找到匹配地址的路由,然后执行对应的路由处理函数,最后将结果返回给调用者。这里是一个例子:

import toRegex from 'path-to-regexp';

function matchURI(path, uri) {  
  const keys = [];
  const pattern = toRegex(path, keys); // TODO: Use caching
  const match = pattern.exec(uri);
  if (!match) return null;
  const params = Object.create(null);
  for (let i = 1; i < match.length; i++) {
    params[keys[i - 1].name] =
      match[i] !== undefined ? match[i] : undefined;
  }
  return params;
}

async function resolve(routes, context) {  
  for (const route of routes) {
    const uri = context.error ? '/error' : context.pathname;
    const params = matchURI(route.path, uri);
    if (!params) continue;
    const result = await route.action({ ...context, params });
    if (result) return result;
  }
  const error = new Error('Not found');
  error.status = 404;
  throw error;
}

export default { resolve };  

查看 path-to-regex 的官方文档。这个库简直屌炸天!比例,你可以使用同样的这个库将一个参数化的路径转化成一个URI的形式:

const toUrlPath = pathToRegexp.compile('/tasks/:id(\\d+)')

toUrlPath({ id: 123 }) //=> "/user/123"  
toUrlPath({ id: 'abc' }) /=> error, doesn't match the \d+ constraint  

现在你可以将应用的主文件(入口文件)使用下面的路由进行升级:

import ReactDOM from 'react-dom';  
import history from './history';  
import router from './router';  
import routes from './routes';

const container = document.getElementById('root');

function renderComponent(component) {  
  ReactDOM.render(component, container);
}

function render(location) {  
  router.resolve(routes, location)
    .then(renderComponent)
    .catch(error => router.resolve(routes, { ...location, error })
    .then(renderComponent));
}

render(history.getCurrentLocation()); // render the current URL  
history.listen(render);               // render subsequent URLs  

就是这样!你们可能想看看我有这些特性的 React 样例项目:

这些样例项目在全球很多的实际场景中也是非常的流行和成功。毫无疑问,他们值得一试!

原文链接:https://medium.com/@tarkus/you-might-not-need-react-router-38673620f3d#.2l0c10mgg

「译」 AirBnB 的 JavaScript 风格指南 →
← 简寻技术沙龙 - Android 专场( ppt / 速记)