多頁應用中,一個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 中。
願你成爲終身學習者
想了解更多生活鮮爲人知的一面,能夠關注個人大遷世界噢