多页应用中,一个URL对应一个HTML页面,一个Web应用包含不少HTML页面,在多页应用中,页面路由控制由服务器端负责,这种路由方式称为后端路由。css
多页应用中,每次页面切换都须要向服务器发送一次请求,页面使用到的静态资源也须要从新加载,存在必定的浪费。并且,页面的总体刷新对用户体验也有影响,由于不一样页面间每每存在共同的部分,例如导航栏、侧边栏等,页面总体刷新也会致使共用部分的刷新。前端
在单面应用中,URL发生并不会向服务器发送新的请求,因此“逻辑页面”的路由只能由前端负责,这种路由方式称为前端路由。react
目前,国内的搜索引擎大多对单页应用的SEO支持的很差,所以,对于 SEO 很是看重的 Web
应用(例如,企业官方网站,电商网站等),通常仍是会选择采用多页面应用。React 也并不是只能用于开发单页面应用。
这里使用的 React Router 的大版本号是 v4, 这也是目前最新版本。webpack
React Router 包含3个库, react-router、react-router-dom、和 react-router-native。react-router 提供最基本的路由功能,实际使用,咱们不会直接安装 react-router,而是根据应用运行的环境选择安装 react-router-dom(在浏览器中使用)或 react-router-native(在 react-native中使用)。react-router-dom 和 react-router-native 都依赖 react-router,因此在安装时, react-router 也会自动安装。
建立 Web应用,使用web
npm install react-router-dom
建立 navtive 应用,使用npm
npm install react-router-native
React Router 经过 Router 和 Route 两个组件完成路由功能。Router 能够理解成路由器,一个应用中须要一个 Router 实例,全部跌幅配置组件 Route 都定义为 Router 的子组件。在 Web应用中,咱们通常会使用对 Router 进行包装的 BrowserRouter 或 HashRouter 两个组件 BrowserRouter使用 HTML5 的 history API(pushState、replaceState等)实现应用的 UI 和 URL 的同步。HashRouter 使用 URL 的 hash 实现应用的 UI 和 URL 同步。后端
http://example.com/some/path
http://example.com/#/some/path
使用 BrowserRouter 时,通常还须要对服务器进行配置,让服务器能正确地处理全部可能的URL。例如,当浏览器发生 http://example.com/some/path 和 http://example.com/some/path2 两个请求时,服务器须要能返回正确的 HTML 页面(也就是单页面应用中惟一的 HTML 页面)react-native
HashRouter 则不存在这个问题,由于 hash 部分的内容会被服务器自动忽略,真正有效的信息是 hash 前端的部分,而对于单页应用来讲,这部分是固定的。浏览器
Router 会建立一个 history 对象,history 用来跟踪 URL, 当URL 发生变化时, Router,的后代组件会从新渲染。React Router 中提供的其余组件能够经过 context 获取 history 对象,这也隐含说明了 React Router 中其余组件必须做为 Router 组件后代使用。但 Router 中只能惟一的一个子元素,例如:服务器
// 正确 ReactDOM.render( ( <BrowserRouter> <App /> </BrowserRouter>), document.getElementById('root') ) //错误,Router 中包含两个子元素 ReactDOM.render( ( <BrowserRouter> <App1 /> <App2 /> </BrowserRouter>), document.getElementById('root') )
Route 是 React Router中用于配置路由信息的组件,也是 React Router 中使用频率最高的组件。每当有一个组件须要根据 URL 决定是否渲染时,就须要建立一个 Route。
每一个 Route 都须要定义一个 path 属性,当使用 BrowserRouter 时,path 用来描述这个Router匹配的 URL 的pathname;当使用 HashRouter时,path 用来描述这个 Route 匹配的 URL 的 hash。例如,使用 BrowserRouter 时,<Route path=''foo' /> 会匹配一个 pathname 以 foo 开始的 URL (如: http://example.com/foo)。当 URL 匹配一个 Route 时,这个 Route 中定义的组件就会被渲染出来。
当 URL 和 Route匹配时,Route 会建立一个 match 对象做为 props 中的一个 属性传递给被渲染的组件。这个对象包含如下4个属性。
(1)params: Route的 path 能够包含参数,例如 <Route path="/foo/:id" 包含一个参数 id。params就是用于从匹配的 URL 中解析出 path 中的参数,例如,当 URL = 'http://example.ocm/foo/1' 时,params= {id: 1}。
(2)isExact: 是一个布尔值,当 URL 彻底匹时,值为 true; 当 URL 部分匹配时,值为 false.例如,当 path='/foo'、URL="http://example.com/foo" 时,是彻底匹配; 当 URL="http://example.com/foo/1" 时,是部分匹配。
(3)path: Route 的 path 属性,构建嵌套路由时会使用到。
(4)url: URL 的匹配的方式
(1)component
component 的值是一个组件,当 URL 和 Route 匹配时,Component属性定义的组件就会被渲染。例如:
<Route path='/foo' component={Foo} >
当 URL = "http://example.com/foo" 时,Foo组件会被渲染。
(2) render
render 的值是一个函数,这个函数返回一个 React 元素。这种方式方便地为待渲染的组件传递额外的属性。例如:
<Route path='/foo' render={(props) => { <Foo {...props} data={extraProps} /> }}> </Route>
Foo 组件接收了一个额外的 data 属性。
(3)children
children 的值也是一个函数,函数返回要渲染的 React 元素。 与前两种方式不一样之处是,不管是否匹配成功, children 返回的组件都会被渲染。可是,当匹配不成功时,match 属性为 null。例如:
<Route path='/foo' render={(props) => { <div className={props.match ? 'active': ''}> <Foo {...props} data={extraProps} /> </div> }}> </Route>
若是 Route 匹配当前 URL,待渲染元素的根节点 div 的 class 将设置成 active.
当URL 和多个 Route 匹配时,这些 Route 都会执行渲染操做。若是只想让第一个匹配的 Route 沉浸,那么能够把这些 Route 包到一个 Switch 组件中。若是想让 URL 和 Route 彻底匹配时,Route才渲染,那么可使用 Route 的 exact 属性。Switch 和 exact 经常联合使用,用于应用首页的导航。例如:
<Router> <Switch> <Route exact path='/' component={Home}/> <Route exact path='/posts' component={Posts} /> <Route exact path='/:user' component={User} /> </Switch> </Router>
若是不使用 Switch,当 URL 的 pathname 为 "/posts" 时,<Route path='/posts' /> 和 <Route path=':user' /> 都会被匹配,但显然咱们并不但愿 <Route path=':user' /> 被匹配,实际上也没有用户名为 posts 的用户。若是不使用 exact, "/" "/posts" "/user1"等几乎全部 URL 都会匹配第一个 Route,又由于Switch 的存在,后面的两个 Route永远不会被匹配。使用 exact,保证 只有当 URL 的 pathname 为 '/'时,第一个Route才会匹配。
嵌套路由是指在Route 渲染的组件内部定义新的 Route。例如,在上一个例子中,在 Posts 组件内再定义两个 Route:
const Posts = ({match}) => { return ( <div> {/* 这里 match.url 等于 /posts */} <Route path={`${match.url}/:id`} component={PostDetail} /> <Route exact path={match.url} component={PostList} /> </div> ) }
Link 是 React Router提供的连接组件,一个 Link 组件定义了当点击该 Link 时,页面应该如何路由。例如:
const Navigation = () => { <header> <nav> <ul> <li><Link to='/'>Home</Link></li> <li><Link to='/posts'>Posts</Link></li> </ul> </nav> </header> }
Link 使用 to 属性声明要导航到的URL地址。to 能够是 string 或 object 类型,当 to 为 object 类型时,能够包含 pathname、search、hash、state 四个属性,例如:
<Link to={{ pathname: '/posts', search: '?sort=name', hash:'#the-hash', state: { fromHome: true} }}> </Link>
除了使用Link外,咱们还可使用 history 对象手动实现导航。history 中最经常使用的两个方法是 push(path,[state]) 和 replace(path,[state]),push会向浏览器记录中新增一条记录,replace 会用新记录替换记录。例如:
history.push('/posts'); history.replace('/posts');
路由设计的过程能够分为两步:
咱们有三个页面,按照页面功能不难定义出以下的路由名称:
可是这些还不够,还须要考虑打开应用时的默认页面,也就是根路径"/"对应的页面。结合业务场景,帖子列表做为应用的默认页面为合适,所以,帖子列表对应两个路由名称: '/posts'和 '/'
React Router 4并不须要在一个地方集中声明应用须要的全部 Route, Route实际上也是一个普通的 React 组件,能够在任意地方使用它(前提是,Route必须是 Router 的子节点)。固然,这样的灵活性也必定程度上增长了组织 Route 结构层次的难度。
咱们先考虑第一层级的路由。登陆页和帖子列表页(首页)应该属于第一层级:
<Router> <Switch> <Route exact path="/" 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="/" component={AsyncHome}></Route> <Route exact path="/login" component={AsyncLogin}></Route> <Route exact path="/posts" component={AsyncHome}></Route> </Switch> </Router> ) } } export default App;
这样,只有当路由匹配时,对应的组件才会被导入,实现按需加载的效果。
这里还有一个须要注意的地方,打包后没有单独的CSS文件了。这是由于 CSS样子被打包到各个 chunk 文件中,当 chunk文件被加载执行时,会有动态把 CSS 样式插入页面中。若是但愿把 chunk 中的 css打包到一个单独的文件中,就须要修改 webpack 使用的 ExtractTextPlugin 插件的配置,但 create-react-app 并无直接把 webpack 的配置文件暴露给用户,为了修改相应配置
,须要将 create-react-app 管理的配置文件“弹射”出来,在项目根路径下执行:
npm run eject
项目中会多出两个文件夹:config和 scripts,scrips中包含项目启动、编译和测试的脚本,config 中包含项目使用的配置文件,
webpack配置文件 就在这个路径下,打包 webpack.config.prod.js 找到配置 ExtractTextPlugin 的地方,添加 allChunks:true 这项配置:
new ExtractTextPlugin({ filename: cssFilename, allChunks: true })
而后从新编译项目,各个chunk 文件 使用的 CSS 样式 又会统一打包到 main.css 中。
愿你成为终身学习者
想了解更多生活鲜为人知的一面,能够关注个人大迁世界噢