你可能并不需要React router

emt

你可能并不需要 React Router

如果你现在恰好在工作中使用了 Facebook 的类库 React.js,你可能会注意到浮现在 React 社区中的一些误解。其中之一就是断言 React 只是 MVC 框架中的 V(视图)部分。为了让你在开发网页应用中像一个框架那样使用它,你需要将 React 混合其他类库一同使用。但实践上来说,你很少机会看见 React 开发者会使用 MVC 中的控制器与模型。因为基于组件的 ui 架构正在稳步占领前端社区的主流,现在已经越来越少有人会去使用 MVC 结构了。

另一个误解是 React Router 库是来自于 Facebook 的官方路由解决方案。而事实上 Facebook 里的绝大多数的项目甚至都没有使用它。说到路由,实际上在相当庞大的一部分网页应用的用例中,使用一个小而定制的路由会让整个应用变得更好。在你明确地将这个主张归类为异端邪说之前,请允许我向你展示,如何在不到50行代码的情况下实现一个功能完备的路由解决方案。

导航

首先,这个解决方案没必要像 RR 所做的那样,将路由与客户端导航都写到同一个组件当中。能让你的路由真正的可通用的方法就是在客户端和服务器端环境都使用同样的路由 api。这里有一个优秀的 npm 模块叫 history ,它可以控制导航部分(仅供参考,history是一个精简的 html5 History API 的封装。RR 里面同样也使用了它)。你只需要在你初始化导航组件的地方引入这个 history.js 文件,然后就可以将它作为应用中的一个单元去使用。

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

现在,只需要引用这个文件,然后在任何你需要重定向用户到新地址(url)的地方, 只需要调用 history.push('/new-page') 就可以达到重定向的作用,而且无需刷新整个页面。你可以像下面这样在主应用文件中(配置代码)订阅全部的路由变化。

1
import history from './history';
1
function render(location) { /* Render React app, read on */ }
1
2
render(history.getCurrentLocation()); // render the current URL
history.listen(render); // render subsequent URLs

一个带有链接的 React 组件在客户端可能会像这样的:

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 模块文档里描述的那样,注册 history.listenBefore(...) 事件方法到你组件的 cjomponentDidMount() 方法即可。同样的方法也适用于在页面之间的过渡动画的控制。

路由

你可以用详细的原生JavaScript 对象来描述整个路由列表和其中的每一个路由。没有必要在这里使用 JSX,例如:

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

话说回来,如果谁知道为什么那么多人倾向于在和 UI 渲染无关的地方也使用 JSX,请留下评论

你可以使用 ES2015+ async/await 语法来写你的路由方法。就像在 RR 里做的那样,没有必要在这里使用回调,举个例子:

1
2
3
4
5
6
7
8
{
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 他们这样的用户规模也没在应用中使用嵌套路由(至少没有在他们的所有项目中使用)

作为嵌套路由的替代方案,你完全可以使用嵌套组件,例如:

1
2
import React from 'react';
import Layout from '../components/Layout';
1
2
3
4
5
6
7
8
9
10
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>
);
}
}
1
export default AboutPage;

与嵌套路由相比,这个方式实现起来更简单。同时又更加灵活,直观,带来更多的使用方法(注意你是如何传递一个面包屑导航到这个 Layout里的)

这个路由可以被设计为一对功能函数—matchURI(),一个用于比较参数化路径字符串与实际 URL的内部(私有)方法;一个通过路由列表找到匹配结果的路由,并执行相应的路由处理方法返回结果给调用者的 reslove() 方法。
这是一个可能的例子:

1
import toRegex from 'path-to-regexp';

1
2
3
4
5
6
7
8
9
10
11
12
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;
}
1
2
3
4
5
6
7
8
9
10
11
12
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;
}
1
export default { resolve };

强烈推荐你们去看看 path-to-regexp 这个库的文档,因为这个类库非常不错!在下面这个例子中你可以使用同一个类库来将一个参数化的路径字符串转化到 URL 当中:

1
2
3
4
const toUrlPath = pathToRegexp.compile('/tasks/:id(\\d+)')
toUrlPath({ id: 123 }) //=> "/user/123"
toUrlPath({ id: 'abc' }) //=> error, doesn't match the \d+ constraint

现在你可以更新主要的应用文件(入口文件)来使用这个路由了

1
2
3
4
import ReactDOM from 'react-dom';
import history from './history';
import router from './router';
import routes from './routes';
1
2
3
4
const container = document.getElementById('root');
function renderComponent(component) {
ReactDOM.render(component, container);
}
1
2
3
4
5
6
7
function render(location) {
router.resolve(routes, location)
.then(renderComponent)
.catch(error => router.resolve(routes, { ...location, error })
.then(renderComponent));
}
1
2
render(history.getCurrentLocation()); // render the current URL
history.listen(render); // render subsequent URLs

就是这样,你或许也想要参考我的项目中使用了这个路由方案的 React 样例项目

React Starter Kit— 同构网页应用模板((Node.js, GraphQL, Reac)

React Static Boilerplate— 无服务器网页应用 (React, Redux, Firebase)

ASP.NET Core Starter Kit— 单页应用(ASP.NET Core, C#, React)

这几个模板项目都是在全球范围内相当流行并成功的应用在多个真实项目中,毫无疑问是有参考价值的