取名字真難!react
首先是入口文件,git
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App.js';
ReactDOM.render(
<App/>
,window.root
);
複製代碼
咱們只讓入口文件幹一件事,即渲染真實DOM到掛載元素上。github
注意即便只幹這麼一件事,react
庫也是必須引入的,不然會報緩存
'React' must be in scope when using JSX react/react-in-jsx-scope
複製代碼
接下來咱們把路由和導航都統一放在App
組件裏,只求一目瞭然bash
import React from 'react';
import {HashRouter as Router,Route} from './react-router-dom'; //引入咱們本身的router庫
export default class App extends React.Component{
render(){
return (
<Router>
<div className='container'>
<ul className='Nav'>
<li><Link to='/home'>首頁</Link></li>
<li><Link to='/user'>用戶</Link></li>
<li><Link to='/profile'>我的設置</Link></li>
</ul>
<div className='View'>
<Route path='/home' component={Home}/>
<Route path='/user'component={User}/>
<Route path='/profile' component={Profile}/>
</div>
</div>
</Router>
)
}
}
複製代碼
import React from 'react';
import PropTypes from 'prop-types';
export default class Router extends React.Component{
static childContextTypes = {
location:PropTypes.object
,history:PropTypes.object
}
constructor(props){
...
}
getChildContext(){
...
}
componentDidMount(){
...
}
render(){
...
}
}
複製代碼
Router
組件只是一個路由容器,它並不會生成一個div什麼的,它會直接返回它包裹住的子元素們children
,react-router
render(){
return this.props.children; //注意children只是props下的一個屬性
}
複製代碼
須要注意的是Router依然遵循JSX tag
嵌套時的單一入口規則,So若是有多個平級的子元素須要用一層div或則其它什麼的給包起來。dom
<Router>
<div>
<Route ... />
<Route ... />
...
</div>
</Router>
複製代碼
爲了效仿原版(沒有hash值時,自動補上/
)函數
pathname
屬性賦一個初始值,這個pathname就是咱們之後的hash值了(
除去#部分)
constructor(props){
super(props);
this.state = {
location:{
pathname:window.location.hash.slice(1)||'/'
}
}
}
複製代碼
接着咱們須要在Router組件掛載完畢時對location.hash
進行賦值post
window.location.hash = this.state.location.pathname;
複製代碼
這樣就完成了/
的自動補全功能。測試
Router
最重要的功能之一就是監聽hash值,一旦hash值發生改變,Router應該讓路由Route
從新渲染。
那麼,怎麼才能讓路由組件從新匹配和渲染呢?
嗯,只須要調用this.setState
便可,React中的setState方法只要被調用,就會從新渲染調用這個方法的組件,理所固然,也包括其子組件。
[danger] 注意: 雖然setState只要調用就會從新渲染,但有一種狀況例外,則是setState()什麼也不傳的時。而只要有傳參,哪怕是一個{},也會重啓渲染。
咱們選在在組件渲染完畢時開啓監聽
componentDidMount(){
...
window.addEventListener('hashchange',()=>{
this.setState({location:{pathname:window.location.hash.slice(1)||'/'}});
});
}
複製代碼
關於setState:
setState雖然有自動合併state的功能,但若這個state裏還嵌套了一層,它是不會自動合併的,好比你有一個location的state,它長這樣{location:{pathname:xxx,other:yyy}}
,而後你像這樣更新了一下state {location:{pathname:xxx}}
,那麼location中的other將再也不保留,由於setState並不支持第二層嵌套的自動合併。
Router在監聽hash的時會實時的把hash值同步緩存在state上,這樣咱們就不用在每一次的路由匹配中都重頭獲取這個hash值而只須要從Router中拿便可。
那麼咱們怎麼在route
中從一個父組件(router)中拿取東東呢?
React中提供了一個叫context的東東,在一個組件類中添加這個東東,就至關於開闢了一塊做用域,讓子孫組件可以輕易的經過這個做用域拿到父組件共享出的屬性和方法,我稱之爲React中的任意門。
這個門有兩側,一側在開通這個context
的根組件(相對於其子孫組件的稱謂)這邊
// 申明父組件要在context做用域裏放哪些東東
...
static childContextTypes = {
location:PropTypes.object
,history:PropTypes.object
};
// 定義要放這些東東的具體細節
getChildContext(){
return {
location:this.state.location
,history:{
push(path){
window.location.hash = path;
}
}
}
}
...
複製代碼
一側在要從根組件拿取東東的子孫組件這邊
[danger] 注意: 這裏的的靜態屬性再也不帶child字樣
...
// 和根組件相比 去除了child字樣
// 要用哪些東東就須要申明哪些東東
static contextTypes = {
location:propTypes.object
,history:propTypes.object
}
// 在聲明完要從根組件中拿取哪些東東後,能夠在任意地方獲取到這些東東
fn(){
...
console.log(this.context.location);
}
...
複製代碼
從上一節中咱們已經知道,Router組件最後返回的實際上是它的children們,So,也就是一條條Route
.
<Router>
<Route path='/a' component={myComponent1} />
<Route path='/b' component={myComponent2} />
<Route path='/c' component={myComponent3} />
</Router>
複製代碼
其中每一條<Route .. />
都表明一次Route類的實例化,而且返回這個類中render
函數所返回的東東。
咱們經過將準備要渲染的組件做爲屬性傳遞給<Route ../>
組件,以求Route組件能幫咱們控制住咱們真正想要渲染的那些組件的渲染。(路由Route
的角色就相似於編輯,須要對要渲染的內容進行審稿)
實際中,咱們只有當url中的pathname和咱們在Route中設置的path相匹配時纔會讓Route組件渲染咱們傳遞給它的那些個真正想要渲染在頁面上的可視組件。
像這樣
...
// 接收根組件(Router)Context做用域中的 location 和 history
static contextTypes = {
location:propTypes.object
,history:propTypes.object
}
...
// class Route の render方法中
...
let {component:Component,path} = this.props;
let {location} = this.context;
let pathname = location.pathname;
if(path==pathname||pathname.startsWith(path)){
return <Component />
}else{
return null;
}
...
複製代碼
當路由真正被匹配上時,會傳遞三個參數給真正要渲染的可視組件
// class Route の render方法中
....
...
static contextTypes = {
location:PropTypes.object
,history:PropTypes.object
}
...
let props = {
location
,history:this.context.history
,match:{}
};
...
if(path==pathname||pathname.startsWith(path)){
return <Component {...props}/>
...
複製代碼
如上所示,這三個參數屬性分別是:
pathname
在hashrotuer中是指#後面那一串,是url的子集。
而path
是咱們給Route組件手動指定的匹配路徑,和pathname進行匹配的,但不必定等於pathname,有startsWith匹配。除此以外path還多是一個/user/:id
這樣的動態路由。
最後url
,在react中它並非咱們的url地址,而是pathname通過path轉換成的正則匹配後的結果,它不必定等於path(由於還有動態路由)。
React中路由的渲染有三種方式
<Route path=.. compoent={Component1}/>
複製代碼
這種就是最多見的,會根據路徑是否匹配決定是否渲染傳遞過來的組件。
<Route path=.. render={(props)=>{...}}>
複製代碼
採用render方式渲染時,組件是否渲染不只要看路徑是否匹配,還要由render屬性所接受的函數來共同決定。
注意,此時render函數會接受一個參數props
,即當前Route
組件的props對象。
<Route path=.. children={(props)=>{...}}>
複製代碼
貌似和render沒區別,實則區別挺大!由於這貨不論路由的路徑是否匹配都會調用children這個回調函數。
So,分清楚了三種渲染方式的區別後,咱們來大概寫下如何實現
// Routeのrender函數中
...
if(result){ //表示路由匹配得上
if(this.props.render){
return this.props.render(this.props);
}else{
return <Component {...this.props}/>
}
}else{
if(this.props.children){ //若是children存在,就算路徑沒有匹配也會調用
return this.props.children(this.props);
}else{
return null;
}
}
...
複製代碼
要實現動態路由,須要咱們將給Route
設置的/xxx/:xxx
們替換成正則用以匹配路徑,爲了代碼的清晰我門使用path-to-regexp
模塊對全部路由(包括非動態路由)都進行正則替換。
path-to-regexp 模塊的實如今個人這篇文章中講過 Express源碼級實現の路由全解析(下闋)
而這一步須要在路由初始化的時候就完成
constructor(props){
super(props);
let {path} = props; //user/detail/:id
this.keys = [];
this.regexp = pathToRegexp(path,this.keys,{end:false}); //false表示只要開頭匹配便可
this.keys = this.keys.map(key=>key.name); //便是傳遞給渲染組件的match對象中的params對象
}
複製代碼
這樣路由規則就不會在每次render重繪時都進行一次計算
接下來咱們須要在每次render中對路徑從新進行匹配
// render()中
...
let result = location.pathname.match(this.regexp);
...
複製代碼
若是匹配上了,有結果,還要準備一個params對象傳放進match對象中傳遞給渲染組件
if(result){
let [url,...values] = result;
props.match = {
url //匹配上的路徑(<=pathname)
,path //route上的path
,params:this.keys.reduce((memo,key,idx)=>{
memo[key] = values[idx];
return memo;
},{})
};
}
複製代碼
最後再判斷是根據三種渲染方式中的哪種來渲染
if (result) {
...
if (render) {
return render(props);
} else if (Component) {
return <Component {...props}/>
} else if (children) {
return children(props);
}
return null;
} else {
if (children) {
return children(props);
} else {
return null;
}
}
複製代碼
Link組件能讓咱們經過點擊鏈接來達到切換顯示路由組件的效果
export default class xxx extends React.Component{
static contextTypes = {
history:PropTypes.object
};
render(){
return (
<a onClick={()=>this.context.history.push(this.props.to)}>
{this.props.children}
</a>
)
}
}
複製代碼
export default ({to,children})=>{
return <Route path={to} children={props=>(
<li className={props.match?"active":""}>
<Link to={to}>{children}</Link>
</li>
)}/>
}
複製代碼
<ul className='Nav'>
<MenuLink to='/home'>首頁</MenuLink>
<MenuLink to='/user'>用戶</MenuLink>
<MenuLink to='/profile'>詳情</MenuLink>
</ul>
複製代碼
這組件的做用便是讓匹配得上當前路由的link高亮
在介紹三個相關組件以前須要對Router中存儲的push方法作出調整,以便保存Redirect跳轉前的路徑
...
push(path){
if(typeof path === 'object'){
let {pathname,state} = path;
that.setState({location:{...that.state.location,state}},()=>{
window.location.hash = pathname;
})
}else{
window.location.hash = path; //會自動添加'#'
}
}
...
複製代碼
export default class xxx extends React.Component {
static contextTypes = {
history:PropTypes.object
}
componentWillMount() {
this.context.history.push(this.props.to);
}
render() {
return null;
}
}
複製代碼
export default function({component:Component,...rest}){
return <Route {...rest} render={props=>(
localStorage.getItem('login')?<Component {...props}/>:<Redirect to={{pathname:'/login',state:{from:props.location.pathname}}}/>
)}/>;
}
複製代碼
import React from 'react';
export default class xxx extends React.Component{
handleClick=()=>{
localStorage.setItem('login',true);
this.props.history.push(this.props.location.state.from);
}
render(){
return (
<div>
<button onClick={this.handleClick} className="btn btn-primary">登陸</button>
</div>
)
}
}
複製代碼
<Router>
<Route path='/a' component={myComponent1} />
<Route path='/b' component={myComponent2} />
<Route path='/c' component={myComponent3} />
</Router>
複製代碼
一般狀況下咱們這樣寫Route有一點很差的是,無論第一個路由匹配沒匹配上,Router都會接着往下匹配,這樣就增長運算量。
So,Switch
組件就是爲了解決這個問題
<Router>
<Switch>
<Route path='/a' component={myComponent1} />
<Route path='/b' component={myComponent2} />
<Route path='/c' component={myComponent3} />
</Switch>
</Router>
複製代碼
export default class xxx extends React.Component{
static contextTypes = {
location:PropTypes.object
}
render(){
console.log('Router render'); //只會打印一次
let {pathname} = this.context.location;
let children = this.props.children;
for(let i=0;i<children.length;++i){
let child = children[i]; //一個route
let {path} = child.props;
if(pathToRegexp(path,[],{end:false}).test(pathname)){
return child;
}
}
return null;
}
}
複製代碼
這樣只有一個Route會被初始化以及渲染。
但,有一個bug,咱們上面寫Route
時,是將path轉正則的部分放在constructor裏的,這意味着只有在這個Route初始化的時候纔會將path轉換爲正則,這樣很好,只用計算一次,但和Switch
搭配使用時就很差了,由於React的複用機制,即便路由路徑已經不同了,它仍然把上次的Route拿過來進行渲染,So此時的正則仍是上一次的,也就不會被匹配上,嗯,bug。
解決方案:
倉庫地址:點我~點我!
推薦:
=== ToBeContinue ===