傳統的多頁模式
後端控制路由
在以前我們採用的都是一個 URL 對應一個 html 頁面的方式,由後端或者服務器去做路由控制。當一個 URL 找不到對應的頁面時就會返回 404.
單頁模式(single page application,簡稱SPA)
單頁應用現在是越來越流行了,單頁應用和傳統的多頁應用相比,前端只有一個頁面。在不刷新頁面的前提下,通過監聽 URL 的變化來渲染對應的 UI ,以此來實現多頁的功能。
前端控制路由
如果使用的是 browserHistory,這裏需要注意一個問題,我們在進入某個路由下的時候,頁面刷新就會 404。這是因爲服務器上只有一個 index.html,而你當前訪問的比如 /home 在服務器上根本就找不到。所以需要讓後端或者服務器把所有匹配不到的路徑都指向這個 index.html,然後讓前端 js 來控制路由。如 nginx 中可以這樣配置
server {
...
location / {
try_files $uri /index.html
}
}
爲了實現 URL 變化而頁面不刷新,可以有兩種方式:
- 通過hash來實現,如 http://www.mysite.com#666。hash 的變化是不會引起瀏覽器刷新的,可以在頁面 onload 的時候獲取到location.hash,找到對應的 UI 模塊渲染到頁面上。另外還要 onhashchange 監聽下 hash 的變化,來更新 UI。
- 通過 h5 提供的 history 來實現, 如 http://www.mysite.com/666。可以在頁面 onload 的時候獲取到 location.pathname,根據自己的規則找到對應的 UI 模塊渲染到頁面上。然後通過 pushState、replaceState 和 popState 實現前進和後退(這兩位仁兄也不會引起瀏覽器刷新),再監聽一下 popState 事件來處理 UI 層的更新
history 介紹
history 是一個 JavaScript 庫,可讓您在 JavaScript 運行的任何地方輕鬆管理會話歷史記錄。history 抽象出各種環境中的差異,並提供最小的 API ,使您可以管理歷史堆棧,導航,確認導航以及在會話之間保持狀態。
history 有三種實現方式:
- BrowserHistory:用於支持 HTML5 歷史記錄 API 的現代 Web 瀏覽器(請參閱跨瀏覽器兼容性)
- HashHistory:用於舊版Web瀏覽器
- MemoryHistory:用作參考實現,也可用於非 DOM 環境,如 React Native 或測試
react-router 的基本原理
這老兄其實就是一些 react 組件的集合,作用就是實現 URL 和 UI 的同步,內部是基於 history.js 來實現的瀏覽器歷史記錄管理。它包括了一下幾個核心部分:
- react-router,路由核心內容,Rrouter、Rroute、Redirect、withRouter都在這裏
- react-router-dom,基於 react-router,針對瀏覽器做的一些封裝,如 Link 組件
- react-router-native,基於 react-router,針對 ReactNative 做的一些封裝
幾個核心的組件的作用
- Router,包裹着 Route 組件,維護這一張路由與組件映射關係的路由表
- Route,描述了每條路由與組件的匹配規則
- Link,最終會被編譯成<a>標籤,它的
to、query、hash
屬性會被組合在一起並做爲 href 屬性
我們通過一個小例子來庖丁解牛一下其中原理
import { browserHistory } from 'react-router'
React.render((
<Router history={ browserHistory }>
<Route path="/" component={App}>
<IndexRoute component={Dashboard} />
<Route path="about" component={About} />
<Route path="inbox" component={Inbox}>
{/* 使用 /messages/:id 替換 messages/:id */}
<Route path="/messages/:id" component={Message} />
</Route>
<Link to="/about">About</Link>
<Link to="/inbox">Inbox</Link>
</Route>
</Router>
), document.body)
下面我們一個一個看
Router
render() {
return (
<RouterContext.Provider
children={this.props.children || null}
value={{
history: this.props.history,
location: this.state.location,
match: Router.computeRootMatch(this.state.location.pathname),
staticContext: this.props.staticContext
}}
/>
);
}
Router 組件是基於 RouterContext 來實現的,也就是 react 中的上下文對象 context,只不過這個 context 是利用 'mini-create-react-context' 這個包去創建的,和原生的 context 用法有些不同,但是目的都是讓數據可以跨組件縱向傳遞。
Provider 是生產數據的地方,這裏會放入 history、location、match 三個對象,以便子孫組件可以方便的使用
Route
render() {
return (
<RouterContext.Consumer>
{context => {
invariant(context, "You should not use <Route> outside a <Router>");
const location = this.props.location || context.location;
const match = this.props.computedMatch
? this.props.computedMatch // <Switch> already computed the match for us
: this.props.path
? matchPath(location.pathname, this.props)
: context.match;
const props = { ...context, location, match };
let { children, component, render } = this.props;
// Preact uses an empty array as children by
// default, so use null if that's the case.
if (Array.isArray(children) && children.length === 0) {
children = null;
}
return (
<RouterContext.Provider value={props}>
{props.match
? children
? typeof children === "function"
? __DEV__
? evalChildrenDev(children, props, this.props.path)
: children(props)
: children
: component
? React.createElement(component, props)
: render
? render(props)
: null
: typeof children === "function"
? __DEV__
? evalChildrenDev(children, props, this.props.path)
: children(props)
: null}
</RouterContext.Provider>
);
}}
</RouterContext.Consumer>
);
}
Link
return (
<RouterContext.Consumer>
{context => {
invariant(context, "You should not use <Link> outside a <Router>");
const { history } = context;
const location = normalizeToLocation(
resolveToLocation(to, context.location),
context.location
);
const href = location ? history.createHref(location) : "";
const props = {
...rest,
href,
navigate() {
const location = resolveToLocation(to, context.location);
const method = replace ? history.replace : history.push;
method(location);
}
};
// React 15 compat
if (forwardRefShim !== forwardRef) {
props.ref = forwardedRef || innerRef;
} else {
props.innerRef = innerRef;
}
return React.createElement(component, props);
}}
</RouterContext.Consumer>
);
<Link> 組件最終會被轉譯成 <a> 標籤,然後給 <a> 標籤加一個 click 事件,並組織 <a> 標籤的默認行爲(跳轉),然後執行 history.push(to) 或 history.replace(to) 來實現跳轉。
withRouter
return (
<RouterContext.Consumer>
{context => {
invariant(
context,
`You should not use <${displayName} /> outside a <Router>`
);
return (
<Component
{...remainingProps}
{...context}
ref={wrappedComponentRef}
/>
);
}}
</RouterContext.Consumer>
);
withRouter 是一個高階組件,它的入參是一個組件假設爲 A 。經過它封裝之後返回一個新的組件,這個新組件會把 history、location、match 三個對象當做屬性傳遞給組件 A。這也是爲什麼我們使用 withRouter 包裝之後的組件,可以在內部使用 props.xxx 調用這三者的原因。
那麼 withRouter 這個高階組件又是從哪裏獲取的這三個對象呢?
還記得之前在 Router 裏,使用 Provider 提供的這三個對象嗎? 這裏使用了 Consumer 來消費數據,其實就是通過 react 的上下文對象 context 來跨組件傳遞數據的。
問題
<Link>標籤和<a>標籤有什麼區別?
react-router:只更新變化的部分從而減少DOM性能消耗
而 <a> 標籤是整個頁面刷新,重新渲染