02-使用路由器
8.1 使用路由器
上一章用React构建了一个可以工作的路由器。当致力于开发生产环境的React应用时,使用者也许想选择React Router这样的东西。幸好,React Router遵循非常类似的API,而且它还有更高级的功能,让使用者可以用路由做更多事情。使用者也许并不需要全部这些功能,类似之前构建的路由器就已经够用了。这非常好——选择最适合要解决的问题的工具,而不是选择拥有最多GitHub星的或Hacker News支持的工具。我们的需求会在第12章处理服务器端渲染时改变,因此我们会在第12章切换到React Router。
让我们开始使用那个亮闪闪的新路由器。首先需要将路由器与HTML5 History API连接起来,以便可以利用导航而又无须重新加载整个页面。因为不需要每次都连接服务来刷新整个页面,所以我们将使用pushState导航。不过也可以使用hash-based路由。
我们不会花太多时间探索HTML5 API,因为它们值得另花时间专门学习。我们将使用知名的 history 库,这个库将让使用者以可靠和可预测的方式跨浏览器使用History API。运行 npm install--save history 来确保它已经安装。一旦安装好,需要对当前作为整个应用根源的index.js文件进行一些更改。到现在为止,这个文件是React DOM将整个应用渲染到一个DOM元素的地方。不过我们已经启用了路由,而Router组件需要位置(参见第7章)。需要找到办法为Router组件提供位置并通过 history 库来使用HTML5 History API,而index.js就是做这些的绝佳位置。
练习8-1 客户端路由与服务器端路由对比
花点儿时间思考一下客户端路由和基于URL的客户-服务器端路由有什么不同之处。客户端路由和服务器端路由的主要区别是什么?
除利用 history 之外,还需要设置路由。为了做到这一点,需要重构一些组件,这会让使用者对React的可组合性与模块化的好处有所认识。虽然会移动一些东西,但不必从根本上改变组件的工作方式。让我们看看首先如何修改App组件。它需要成为装载子路由的容器,因为想让每个页面拥有相同的侧边栏和导航栏,只改变传递给 children 属性的东西。图8-1中的示例展示了这看起来是什么样子。
如代码清单8-1所示,为了实现这类嵌套,需要重构App组件来动态地展示 children 。幸好,最终不会删除太多已经完成的工作——只是将其移动一下。随着重构,将会重新组织应用程序的文件。在src目录中创建一个叫作pages的新目录。我们将会把那些仅包含其他组件并为其提供数据的组件放在这个目录中。在后续章节中开始探索React应用的架构时,我将更多地讨论这个想法。

代码清单8-1 重构App组件(src/app.js)
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ErrorMessage from './components/error/Error';
import Nav from './components/nav/navbar';
import Loader from './components/Loader';
class App extends Component {
constructor(props) {
super(props);
this.state = {
error: null,
loading: false
};
}
static propTypes = {
children: PropTypes.node
};
componentDidCatch(err, info) { ⇽--- 使用componentDidCatch设置顶级错误边界,以便于有东西出错时能显示错误
console.error(err);
console.error(info);
this.setState(() => ({
error: err
}));
}
render() {
if (this.state.error) {
return ( ⇽--- 如果有任何错误就展示错误
<div className="app">
<ErrorMessage error={this.state.error} />
</div>
);
}
return (
<div className="app">
<Nav user={this.props.user} /> ⇽--- 传入user属性—在集成Firebase的时候会用到
{this.state.loading ? ( ⇽--- 如果应用正处于加载状态,则展示一个加载器
<div className="loading">
<Loader />
</div>
) : (
this.props.children ⇽--- 使用props.children输出当前活跃的路由
)}
</div>
);
}
}
export default App;
需要给主页面创建一个组件以便用户可以查看帖子。创建一个名为home.js的文件并将它放到pages目录中。这个组件看起来应该很熟悉——它是使用者将内容分解为若干页面之前拥有的主要组件。代码清单8-2展示了Home组件,其使用了之前已实现的方法逻辑,并且为了简洁起见注释掉了这些方法。记住,和所有章一样,如果想了解应用是如何变化的或者应用在一章结束时的情况,可以从本书GitHub上检出每章的不同分支。
代码清单8-2 重构后的Home组件(src/pages/Home.js)
import React, { Component } from 'react';
import parseLinkHeader from 'parse-link-header';
import orderBy from 'lodash/orderBy';
import * as API from '../shared/http'; ⇽--- 别忘了调整导入路径——组件位于不同的目录中
import Ad from '../components/ad/Ad';
import CreatePost from '../components/post/Create';
import Post from '../components/post/Post';
import Welcome from '../components/welcome/Welcome';
export class Home extends Component {
constructor(props) {
super(props);
this.state = { ⇽--- 这些逻辑是完全相同的——只是移动组件以适应新的层次结构
posts: [],
error: null,
endpoint: `${process.env
.ENDPOINT}/posts?_page=1&_sort=date&_order=DESC&_embed=comments&_expand=
user&_embed=likes`
};
this.getPosts = this.getPosts.bind(this);
this.createNewPost = this.createNewPost.bind(this);
}
componentDidMount() { ⇽--- 这些逻辑是完全相同的——只是移动组件以适应新的层次结构
this.getPosts();
}
getPosts() {
API.fetchPosts(this.state.endpoint)
.then(res => {
return res.json().then(posts => {
const links = parseLinkHeader(res.headers.get('Link'));
this.setState(() => ({
posts: orderBy(this.state.posts.concat(posts),
'date', 'desc'),
endpoint: links.next.url,
}));
});
})
.catch(err => {
this.setState(() => ({ error: err }));
});
}
createNewPost(post) {
post.userId = this.props.user.id;
return API.createPost(post)
.then(res => res.json())
.then(newPost => {
this.setState(prevState => {
return {
posts: orderBy(prevState.posts.concat(newPost),
'date', 'desc')
};
});
})
.catch(err => {
this.setState(() => ({ error: err }));
});
}
render() { ⇽--- 这些逻辑是完全相同的,只是移动组件以适应新的层次结构
return (
<div className="home">
<Welcome />
<div>
<CreatePost onSubmit={this.createNewPost} />
{this.state.posts.length && (
<div className="posts">
{this.state.posts.map(({ id }) => {
return <Post id={id} key={id}
user={this.props.user} />;
})}
</div>
)}
<button className="block" onClick={this.getPosts}>
Load more posts
</button>
</div>
<div>
<Ad url="https://ifelse.io/book"
imageUrl="/static/assets/ads/ria.png" />
<Ad url="https://ifelse.io/book"
imageUrl="/static/assets/ads/orly.jpg" />
</div>
</div>
);
}
}
export default Home;
目前已经移动了Home组件,准备配置路由并勾连 history 工具,以便路由器能响应浏览器的地址变化。将一个模块作为实用方法提供给应用程序的其他部分通常是非常有用的,这样就避免了重复工作。本书后面会做更多这样的事情,而且开发者自己也可能已经这样做了。如代码清单8-3所示,我们将这样处理 history 库,因为最终希望(在其他部分)使用它创建与路由器协同工作的链接而不必使用常规的 <a href=""></a> 标签。
代码清单8-3 部署history库(src/history/history.js)
import createHistory from 'history/createBrowserHistory';
const history = createHistory(); ⇽--- 创建一个用于应用的history 库的单例
const navigate = to => history.push(to); ⇽--- 导出navigate方法和history实例(之后需要直接访问的情况)
export { history, navigate };
目前已经设置好了 history ,可以设置index.js的其余部分并配置Router,代码清单8-4展示了如何做。
代码清单8-4 为路由设置index.js(src/index.js)
import React from 'react';
import { render } from 'react-dom'; ⇽--- 导入React DOM
import { App } from './pages/App'; ⇽--- 导入App、Home、Router及Route组件
import { Home } from './pages/Home';
import Router from './components/router/Router';
import Route from './components/router/Route';
import { history } from './history; ⇽--- 导入刚创建的history实用方法
import './shared/crash';
import './shared/service-worker';
import './shared/vendor';
import './styles/styles.scss';
export const renderApp = (state, callback = () => {}) => { ⇽--- 创建一个用来渲染应用程序的函数,封装React DOM的render方法以便能传入地址数据和回调函数
render(
<Router {...state}> ⇽--- 使用JSX延展操作符“填充”作为Router属性的位置状态
<Route path="" component={App}> ⇽--- 为App和Home组件创建路由
<Route path="/" component={Home} />
</Route>
</Router>,
document.getElementById('app'), ⇽--- 将应用渲染到index.html的目标DOM元素内
callback
);
};
let state = { ⇽--- 创建一个状态对象来跟踪地址和用户
location: window.location.pathname,
};
history.listen(location => { ⇽--- 当地址变化时触发并更新路由器,促使应用使用新数据重新渲染
state = Object.assign({}, state, {
location: location.pathname
});
renderApp(state);
});
renderApp(state); ⇽--- 渲染应用程序