React Router 4并不需要在一个地方集中声明应用需要的所有 Route, Route实际上也是一个普通的 React 组件,可以在任意地方使用它(前提是,Route必须是 Router 的子节点)。当然,这样的灵活性也一定程度上增加了组织 Route 结构层次的难度。
我们先考虑第一层级的路由。登录页和帖子列表页(首页)应该属于第一层级:
<Router> <Switch> <Route exact path="https://www.jb51.net/" component={Home}></Route> <Route exact path="/login" component={Login}></Route> <Route exact path="/posts" component={Home}></Route> </Switch> </Router>
第一个Route 使用了 exact 属性,保证只有当访问根路径时,第一个 Route 才会匹配成功。Home 是首页对应组件,可以通过 "/posts" 和 “/” 两个路径访问首页。注意,这里并没有直接渲染帖子列表组件,真正渲染帖子列表组件的地方在 Home 组件内,通过第二层级的路由处理帖子列表组件和帖子详情组件渲染,components/Home.js 的主要代码如下:
class Home extends Component { /**省略其余代码 */ render() { const {match, location } = this.props; const { username } = this.state; return( <div> <Header username = {username} onLogout={this.handleLogout} location = {location} > </Header> {/* 帖子列表路由配置 */} <Route path = {match.url} exact render={props => <PostList username={username} {...this.props}></PostList>} ></Route> </div> ) } }
Home的render内定义了两个 Route,分别用于渲染帖子列表和帖子详情。PostList 是帖子列表组件,Post是帖子详情组件,代码使用Router 的render属性渲染这两个组件,因为它们需要接收额外的 username 属性。另外,无论访问是帖子列表页面还是帖子详情页面,都会共用相同 Header 组件。
七、代码分片
默认情况下,当在项目根路径下执行 npm run build 时 ,create-react-app内部使用 webpack将 src路径下的所有代码打包成一个 JS 文件和一个 Css 文件。
当项目代码量不多时,把所有代码打包到一个文件的做法并不会有什么影响。但是,对于一个大型应用,如果还把所有的代码都打包到一个文件中,显然就不合适了。
create-react-app 支持通过动态 import() 的方式实现代码分片。import()接收一个模块的路径作为参数,然后返回一个 Promise 对象, Promise 对象的值就是待导入的模块对象。例如
// moduleA.js const moduleA = 'Hello' export { moduleA }; // App.js import React, { Component } from 'react'; class App extends Component { handleClick = () => { // 使用import 动态导入 moduleA.js import('./moduleA') .then(({moduleA}) => { // 使用moduleA }) .catch(err=> { //处理错误 }) }; render() { return( <div> <button onClick={this.handleClick}>加载 moduleA</button> </div> ) } } export default App;
上面代码会将 moduleA.js 和它所有依赖的其他模块单独打包到一个chunk文件中,只有当用户点击加载按钮,才开始加载这个 chunk 文件。
当项目中使用 React Router 是,一般会根据路由信息将项目代码分片,每个路由依赖的代码单独打包成一个chunk文件。我们创建一个函数统一处理这个逻辑:
import React, { Component } from 'react'; // importComponent 是使用 import()的函数 export default function asyncComponent(importComponent) { class AsyncComponent extends Component { constructor(props) { super(props); this.state = { component: null //动态加载的组件 } } componentDidMount() { importComponent().then((mod) => { this.setState({ // 同时兼容 ES6 和 CommonJS 的模块 component: mod.default ? mod.default : mod; }); }) } render() { // 渲染动态加载组件 const C = this.state.component; return C ? <C {...this.props}></C> : null } } return AsyncComponent; }
asyncComponent接收一个函数参数 importComponent, importComponent 内通过import()语法动态导入模块。在AsyncComponent被挂载后,importComponent就会阴调用,进而触发动态导入模块的动作。
下面利用 asyncComponent 对上面的例子进行改造,代码如下:
import React, { Component } from 'react'; import { ReactDOM, BrowserRouter as Router, Switch, Route } from 'react-dom'; import asyncComponent from './asyncComponent' //通过asyncComponent 导入组件,创建代码分片点 const AsyncHome = asyncComponent(() => import("./components/Home")) const AsyncLogin = asyncComponent(() => import("./components/Login")) class App extends component { render() { return( <Router> <Switch> <Route exact path="https://www.jb51.net/" component={AsyncHome}></Route> <Route exact path="/login" component={AsyncLogin}></Route> <Route exact path="/posts" component={AsyncHome}></Route> </Switch> </Router> ) } } export default App;
这样,只有当路由匹配时,对应的组件才会被导入,实现按需加载的效果。