React-Router V4 簡單實現
聽說V4終於釋出了,本文是在閱讀RRV4時做的一點小總結
在此對React-Router4.0版本中重要的幾個元件做簡單的實現。
Match元件
Match
元件的作用很簡單,就是基於url
地址來判斷是否渲染對應元件。
使用方式:
<Match pattern=`/test`component={Component} />複製程式碼
###基本原理實現:
import React from `react`;
const Match = ({ pattern, component: Component }) => {
const pathname = window.location.pathname;
if (pathname.match(pattern)) {
// 此處使用match方法對url地址進行解析是遠遠不夠的!
return (
<Component />
);
} else {
return null;
}
}複製程式碼
Match
可以提取出URL中所有動態引數,然後將他們以傳入到對應的元件。
Link元件
Link
元件其實就是對<a>
標籤的封裝。它會禁止掉瀏覽器的預設操作,通過history
的API更新瀏覽器地址。
###使用方法
<Link to="/test" > To Test</Link>複製程式碼
基本實現原理
import createHistory from `history/createBroserHistory`;
const history = createHistory();
// Link元件因為不需要維持自身資料,因此我們在此可以使用無狀態(stateless)函式
const Link = ({ to, children }) => {
<a
onClick={(e) =>
e.preventDefault();
history.push(to)
}
href={to}
>
{children}
</a>
}複製程式碼
如上面程式碼所示,當我們點選a
標籤的時候。首先會禁止瀏覽器的預設行為,然後會呼叫history.push
的API介面更新URL的地址。
你可能會有疑問,既然通過點選就能更新URL地址了,那我們還設定
a
標籤的href
屬性有何用呢?事實上的確沒什麼用處,但本著體驗最優這一原則我們仍需要設定該屬性,當使用者將滑鼠懸停在標籤上時能夠預覽連結地址和右擊複製連結。
React-Router的作用是在URL更改的時候,顯示對應的URL的SPA元件。因此當我們點選Link
元件更新URL時,需要同時更新到對應的元件。history
為我們提供了相應的監聽方法來監聽URL變化。
class App extends React.Component {
componentDidMount() {
history.listen(() => this.forceUPdate())
}
render(){
return (App);
}
}複製程式碼
Link
晉級版本
我們希望能夠根據給該元件傳入更多引數。例如:
- 當前URL為啟用狀態時,希望能夠顯示特別的樣式
- 如同普通
a
標籤一樣,居有target
屬效能夠在另一個新頁面來顯示. - 傳入特別的引數,給對應URL下元件使用,如
query
。
function resolveToLocation(to, router) {
return
typeof to === `function` ? to(router.location) : to
}
function isEmptyObject(object) {
for (const p in object)
if (Object.prototype.hasOwnProperty.call(object, p))
return false
return true
}
class Link extends React.Component {
//這次要根據React-Router寫個相對簡單,但功能更全的版本
handleClick(e) {
// 如果有自定義的點選處理,就用該方法
if (this.props.onClick) {
this.props.onClick();
}
// 如果設定了target屬性,就使用瀏覽器預設方式來處理點選
if (this.props.target) {
return;
}
e.preventDefault();
history.push(resolveToLocation(to));
}
render() {
const { to, activeClassName, activeStyle, onlyActiveOnIndex, ...props } = this.props;
const toLocation = resolveToLocation(to);
props.href = toLocation;
// 以下為React-router的程式碼,根據當前URL選擇樣式
if (activeClassName || (activeStyle != null && !isEmptyObject(activeStyle))) {
if (router.isActive(toLocation, onlyActiveOnIndex)) {
if (activeClassName) {
if (props.className) {
props.className += ` ${activeClassName}`
} else {
props.className = activeClassName
}
}
if (activeStyle)
props.style = { ...props.style, ...activeStyle }
}
}
return <a {...props} href={href} onClick={this.handleClick} />
}
}
Link.propTypes = {
to: oneOfType([string, object, func]),
query: object,
state: object,
activeStyle: object,
activeClassName: string,
onlyActiveOnIndex: bool.isRequired,
onClick: func,
target: string
}
Link.defaultProps = {
onlyActiveOnIndex: false,
style: {}
}複製程式碼
Redirect
Redirect
元件和Link
元件類似。和它不同之處在於Redirect
不需要點選操作,直接就會更新URL地址。
使用方式
<Redirect to="/test" />複製程式碼
基本實現原理
class Redirect extends React.Component {
static contextTypes = {
history: React.PropTypes.object,
};
componentDidMount() {
// 根本不需要DOM,只要你載入該元件,就立即進行跳轉
const history = this.context.history;
const to = this.props.to;
history.push(to);
}
render() {
return null;
}
}複製程式碼
以上只是簡單的實現,V4版本的Redirect
元件會判斷程式碼是不是在服務端渲染,並根據結果不同來選擇在元件生命週期的哪個階段進行URL更新操作(isServerRender ? componentWillMount : componentDidMount)。
晉級功能
class Redirect extends React.Component {
...
// 服務端渲染的話,連Redirect元件都不需要渲染出來就可以跳轉。
componentWillMount() {
this.isServerRender = typeof window !== `object`;
if (this.isServerRender) {
this.perform();
}
}
// 瀏覽器端渲染則需要Redirect元件渲染之後才能進行跳轉。
componentDidMount() {
if (!this.isServerRender) {
this.perform();
}
}
perform() {
const { history } = this.context;
const { push, to } = this.props;
// push是個bool值,用於選擇使用history的哪個方法來跳轉連結
if (push) {
history.push(to);
} else {
history.replace(to);
}
}
}
Redirect.defaultProps = {
push: false,
}複製程式碼
Router
react-router
中的Router
元件本質就是個高階元件,用於包裹所有的頁面元件(其實在react-router v4中,BrowserRouter
才是最外層元件),併為所有react-router
提供全域性引數history
。因此Router
很簡單,也很容易實現。
class Router extends React.Component {
static propTypes = {
history: ProTypes.object.isRequired,
children: PropTypes.node
};
static childrenContextTypes = {
history: PropTypes.object.isRequired
};
// 通過context給元件內所有元素提供共享資料: history
getChildrenContext(){
// 傳入給所有react-router元件使用的history引數
return { history: this.props.history };
}
render() {
const { children } = this.props;
return children ? React.Children.only(children) : null;
};
}複製程式碼
React-Router V4
SeverRouter (服務端渲染時使用)
createLocation
方法很簡單,就是拆解URL地址,並將其分解為pathname
,search
,hash
三個部分。並以陣列的形式返回。
// 例子url : www.webei.com/#/admin/dashboard/setting?username=liuxin&token=noface;
const createLocation = (url) => {
let pathname = url || `/`
let search = ``
let hash = ``
const hashIndex = pathname.indexOf(`#`)
if (hashIndex !== -1) {
hash = pathname.substr(hashIndex)
pathname = pathname.substr(0, hashIndex)
}
const searchIndex = pathname.indexOf(`?`)
if (searchIndex !== -1) {
search = pathname.substr(searchIndex)
pathname = pathname.substr(0, searchIndex)
}
return {
pathname,
search: search === `?` ? `` : search,
hash: hash === `#` ? `` : hash
}
/* 最後返回 {
pathname: `www.webei.com/`,
search: ``,
hash: `#/admin/dashboard/setting?username=liuxin&token=noface`,
}
*/
}複製程式碼
NavLink
NavLink
元件是對Route
元件的封裝。目的是根據根據URL來Active對應的NavLink
元件,為其新增activeClassName和activeStyle。
Route
在React-router V4版本中,Route元件存在於./Core.js檔案中。它是對createRouteElement
方法的封裝。
const createRouteElement = ({ component, render, children, ...props} => (
// createRouteElement 的執行優先順序是
component ?
(props.match ? React.createElement(component, props) : null)
: render ? (
props.match ?
render(props) : null
) : children ? (
typeof children === `function` ?
children(props) : React.Children.only(children)
) : ( null )
));
// 上面的版本是不是很眼暈?下面這個版本的你大概能理解的更清楚。
const createRouteElement = ({ component, render, children, ...props} => {
/*
引數優先順序 component > render > children
要是同時有多個引數,則優先執行權重更高的方法。
*/
// component 要求是方法
if (component && props.match) {
return React.createElement(component, props);
} else {
return null;
}
// render 要求是方法
if (render && props.match) {
return render(props);
} else {
return null;
}
// children 可以使方法也可以是DOM元素
if (children && typeof children === `function`) {
return children(props);
} else {
return React.Children.only(children)
}
return null;
}
const Route = ({ match, history, path, exact, ...props}) => (
createRouteElement({
...props,
match: match || matchPath(history.location.pathname, path, exact),
history
})
);
Route.propTypes = {
match: PropTypes.object, // private, from <Switch>
history: PropTypes.object.isRequired, // 必備屬性
path: PropTypes.string,
exact: PropTypes.bool,
component: PropTypes.func,
// TODO: Warn when used with other render props
render: PropTypes.func,
// TODO: Warn when used with other render props
children: PropTypes.oneOfType([
// TODO: Warn when used with other render props
PropTypes.func,
PropTypes.node
])
}複製程式碼
Router
根據history引數的設定選擇使用不同封裝的Router
元件
MemoryRouter
原始碼註釋
The public API for athat stores location in memory.
###BrowserRouter
原始碼註釋
The public API for athat uses HTML5 history.
###HashRouter
原始碼註釋
The public API for athat uses window.location.hash.
withHistory
相當重要的元件,沒有它就沒有根據URL變化而產生的重新渲染了。
/**
* A higher-order component that starts listening for location
* changes (calls `history.listen`) and re-renders the component
* each time it does. Also, passes `context.history` as a prop.
*/
const withHistory = (component) => {
return class extends React.Component {
static displayName = `withHistory(${component.displayName || component.name})`
static contextTypes = {
history: PropTypes.shape({
listen: PropTypes.func.isRequired
}).isRequired
}
componentWillMount() {
// Do this here so we can catch actions in componentDidMount (e.g. <Redirect>).
this.unlisten = this.context.history.listen(() => this.forceUpdate())
}
componentWillUnmount() {
this.unlisten()
}
render() {
return React.createElement(component, {
...this.props,
history: this.context.history
})
}
}
}複製程式碼