你或许不需要 React Router ― Medium(by Hugo)

smallpanda 发布于1年前 阅读12412次
0 条评论

如何你已经使用Facebook的React.js工作了一段时间,你可能会注意到在React社区中流传着一些错误的概念。其中的一个就是断言React仅仅是MVC架构中的V,如果想在开发web应用时使用它作为框架的话,那么首先需要将它和一堆其他的库混合使用。实际上,你很难见到React开发者使用MVC中的controllers和models。组件化的UI架构已经稳步的接管了前端社区,现在越来越少的人去使用MVC模式了。另一个错误概念就是React Router库(RR)是Facebook官方的路由解决方案。实际上,Facebook中的主要项目都不会使用它。说到路由,大量的web应用项目和实例依靠一个小的自定义的路由就可以做的很好。在你把这种见解当作一个完全的邪说之前,请让我向你展示如何在50行代码内去实现一个满足所有 功能的路由解决方案。

导航

首先,向RR中那样将路由和客户端导航合并在同一组件中是不必要的。这样做你的路由就会实现真正的通用 - 以完全相同的形式,与客户端,服务端环境中相同的API一起工作。有一个非常棒的叫做 history 的npm模块能够处理导航的部分(FYI,它是一种HTML5 History API的封装并且被RR在内部使用)。你只需要在你的项目中初始化这个组件(类)的地方创建history.js文件然后在你的app中把它当作一个单例使用:

import createHistory from 'history/lib/createBrowserHistory';
import useQueries from 'history/lib/useQueries';
export default useQueries(createHistory)();

从现在开始,无论什么时候只要你需要在不刷新页面的情况下为用户重定向,你可以引用这个文件然后调用history.push(‘/new-page’)。在主应用文件中(bootstrap 代码),你可以将所有的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组件中。点击 Link 查看 React静态样板 (RSB)中的组件.这样你的客户端就可以只是像这样去链接:Click。

需要在用户离开页面之前显示一个确认信息?就像在history模块 docs 中描述的那样,仅仅在你的组件的componentDidMount()方法中注册history.listenBefore(..)事件处理函数即可。这个方法也可以用页面之间的动画过渡( demo )。

路由

你可以将路由列表和每一个路由通过JavaScript objects的方式描述出来,不需要使用JSX。例如:

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

顺便说下,如果谁知道为什么这么多人喜欢用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;

这种方式要比嵌套路由实现起来更加容易并且更加灵活,直观,而且解锁了更多的用例(注意你怎样可以把一个面包屑组件传递到Layout中)。

路由本身可以被写成两个函数 - matchURI()和一个内部(私有化)函数来帮助比较一个参数路径字符串和实际URL的区别;resolve()函数在路由列表中查询,找到匹配给定地址的路由,运行路由处理函数返回结果给调用者。下边是(router.js)的举例:

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-regexp 库的文档。这个库棒极了!例如你可以使用这个库将参数路径转化为URLs:

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样例项目:

React Starter Kit — isomorphic web app boilerplate (Node.js, GraphQL, React) React Static Boilerplate — serverless web app (React, Redux, Firebase) ASP.NET Core Starter Kit — single-page app (ASP.NET Core, C#, React)

这些样例在全球范围内被使用在许多真实的项目中,它们是非常流行和成功的。非常值得你去查看:)

需要 登录 后回复方可回复, 如果你还没有账号你可以 注册 一个帐号。