这两个组件,实际上是用于路由跳转,也就是更改浏览器的URL。在URL发生变化之后,Router组件和Route组件才会发挥作用。
Link
在第一篇文章中就使用了Link组件,看一下当时的代码:
export const Nav = () => (
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/about">About</Link></li>
<li><Link to="/contact">Contact</Link></li>
</ul>
);
Link组件正常状态下渲染出来的是一个HTML A标签。其中有一个重要的属性是to,Link的常用属性如下:
- to,用于指示这个Link组件对应的最终渲染的href属性,即超链接指向哪里。
- replace,一个布尔值,默认是false,如果设置为true,则点击链接会在history栈覆盖当前的地址,而不是向栈中增加一条路径。
- component,需要传入一个组件或者函数,用于替换Link实际渲染的东西
- 其他HTML的a标签允许附加的属性,会渲染到最后的a标签上,比如id,className,title等属性。
NavLink相比Link,是一个特殊版本的Link,加上了一些关于样式的控制。
to属性
to属性可以接受如下三个参数:
- 可以是一个字符串,包含#和查询串,比如
/contract?supplier=3
- 是一个对象,例如:
<Link to={{pathname: "/courses", search: "?sort=name", hash: "#the-hash", state: { fromDashboard: true }}}/>
,前三个属性很好理解,最后一个state表示存入location的state,即历史帧。 - 可以传入一个函数,这个函数会被传入location作为参数,经过处理之后返回一个字符串或者符合第二条要求的对象,例如:
<Link to={location => ({ ...location, pathname: "/courses" })} />
使用to属性,就可以灵活的根据逻辑来动态生成链接。
NavLink
这个东西加上了一些对于样式的控制和路由的匹配,看起来像是Route和Link的结合,Route来匹配to属性,Link用来渲染链接。来简单看一下属性:
- to属性,这个属性接受的值和Link一样,但是特别的是还会作为匹配的路径。
- className,这个就是React的标准类名,会被原样渲染到最终的元素上,接受一个字符串或者返回字符串的函数,这都是老套路了。
- style,这也是符合React标准的属性,接受一个对象,其中的CSS属性名属性使用驼峰样式。
- activeStyle,这个是当组件处于active状态的时候,使用的样式,什么叫这个组件处于active状态呢,看下一个属性
- isActive,接受布尔值或者函数返回布尔值,函数的两个参数为match和location,如果匹配不成功,match为null,一般用来判断链接的状态。
- exact,这个属性为true的时候,activeStyle只有在精确match的时候才被应用
其实这个用起来很简单,比如:
<NavLink style={{textDecoration:"none"}} activeStyle={{"color":"red"}} to="/contact">Contact</NavLink>
这个NavLink既有链接的功能,又根据/contact来进行匹配,如果当前不是/contact地址,就不会显示红色,是contact地址则会显示红色。
如果加上isActive={()=>false}
,则这个链接就不会应用activeStyle
。
一般如果采取前端样式库比如MUI的时候,不会单独使用NavLink。
Redirect
这个组件很有意思,一旦成功渲染,就会将history当前栈改成to属性,模仿301重定向的行为。
to属性就是目标属性,即要跳转的地方。push属性如果设置为true,则不覆盖,而是向history栈中加入新的地址栈。from属性要搭配Switch组件来使用,下边会详述。
先来写一个简单的组件试验一下,比如在访问/home路径后,三秒中之后跳转到首页
// Switch中加一条路由
<Route path="/home" component={RedirectToHome}/>
// RedirectToHome组件:
export class RedirectToHome extends React.Component {
constructor(props) {
super(props);
this.state = {
time: 0,
timeId: 0,
}
}
componentDidMount() {
this.setState({timeId: setInterval(() => this.setState({time: this.state.time + 1}), 1000)});
}
render() {
if (this.state.time === 3) {
clearInterval(this.state.timeId);
return <Redirect to="/"/>;
} else {
return <p>{3 - this.state.time} 秒后跳转</p>;
}
}
}
这样写完之后,只要访问/home
路径,倒数三秒后就会被重定向到首页。
来稍微看一下from属性。from属性需要搭配Switch来使用,相当于先匹配from属性,匹配成功的话,跳转到to属性中的地址,这中间还能够传递参数,来继续改造一下:
<Switch>
<Route path="/about" render={(props) => (<About {...props} myattr={"saner"}/>)}/>
<Route path="/contact" children={(props) => <div>{props.match ? "是contact路径" : "不是contact路径"}</div>}/>
<Redirect from="/auser/:username" to="/user/:username"/>
<Route path="/user/:user" component={User}/>
<Route path="/" component={Home}/>
</Switch>
这个时候,如果路径是/auser/saner,会跳转到/user/saner路径,此时就会再匹配User组件。
可以发现,此时的Redirect其实也是Route和Redirect的综合体。
动态路由离散式声明
这里来看一个高阶的技巧,目前我们匹配user是直接在Route中声明了变量,比如/user/:user
,虽然变量部分是动态,但这依然整体上是一种静态的声明,即把所有/user/之后的内容都当做了user变量的内容。
如果是纯粹的动态解析,那么在匹配了/user之后,应该将后边的内容再去进行动态解析和操作,此时就可以用一个Route组件返回一个Route组件来执行操作。
步骤如下:
先把Route修改为如下:
<BrowserRouter>
<Nav/>
<Route path="/about" render={(props) => (<About {...props} myattr={"saner"}/>)}/>
<Route path="/contact" children={(props) => <div>{props.match ? "是contact路径" : "不是contact路径"}</div>}/>
<Route path="/user" component={User}/>
<Route path="/" exact component={Home}/>
</BrowserRouter>
注意,动态路由不能和Switch搭配使用,因为等于精确匹配,没有意义了。
这是让Route只匹配/user开始的路径,至于匹配的路径,在User中可以继续定义,就像接力一样:
export class User extends React.Component {
constructor(props, context) {
super(props, context);
}
render() {
const match = this.props.match;
return (
<div>
<Route path={`${match.url}/:user`} component={UserDetail} />
</div>
);
}
}
这里可以看到,User返回了一个Route,其中的path带上了从上一级传下来的match.url,然后拼接上了:user变量,如果此时URL符合/user/***
的内容的话,***
的内容就会以user变量的形式交给UserDetail去渲染,在其中还可以接力继续判断内容。
例子
这里我们使用这个方式来完成一个比较复杂的逻辑,这个逻辑如下:
- 用户访问
/user
,显示无用户名 - 用户访问
/user/***
,这个***
如果是saner以外的字样,显示“用户不是三鹅”,不管之后还没有其他URL。 - 用户是三鹅的时候,路径为
/user/saner
,显示“三鹅你忘记输入内容了” - 用户是三鹅的时候,路径为
/user/saner/***
,把***
的部分打印出来。
第一步
我们一层一层的来,首先需要匹配/user路径,那么先写一个Route如下:
<Route path="/user" component={User}/>
第二步
这个匹配成功之后,匹配的match对象会发送到User组件中,我们在User组件中再做判断:
export class User extends React.Component {
constructor(props, context) {
super(props, context);
}
render() {
const match = this.props.match;
return (
<div>
<Route path={`${match.url}/:user`} component={UserDetail}/>
<Route path={`${match.url}`} exact render={() => <p>无用户名</p>}/>
</div>
);
}
}
这一步就有点搞了,首先我们要清楚,此时的match.url是/user
,在拼接上:user之后,返回的Route组件中的path属性是/user/:user
。此时如果实际的路径中没有user变量,那么会落到下边的带有exact属性的Route组件中,就会显示无用户名。
如果实际路径中有user变量的部分,我们把这个路径又交给了UserDetail组件。
第三步
UserDetail组件的逻辑比较类似:
class UserDetail extends React.Component {
constructor(props, context) {
super(props, context);
}
render() {
const {match} = this.props;
if (match.params.user === "saner") {
return (<>
<Route path={`${match.url}/:gugugu`} render={(props) => {
console.log(props.match.params.gugugu);
return props.match.params.gugugu ? <p>最后的是{props.match.params.gugugu}</p> :
<p>没有内容</p>;
}}/>
<Route path={`${match.url}`} exact render={()=><p>三鹅你忘记输入内容了。</p>} />
</>)
} else {
return (
<p>用户不是三鹅</p>
)
}
}
}
这里要清醒的知道,此时的match.url一定是一个/user/***
的形式,否则进不来UserDetail组件。我们要做的是判断一下这个***
是不是saner,如果不是,就显示“用户不是三鹅”,不用再去匹配之后的路径。
当用户是saner的时候,逻辑和User组件是一样的,继续匹配其后的路径,如果其后没有路径,就匹配到带有exact属性的Route组件中。
这种逻辑,如果试图用单独的Route来定义,是不太好写的,使用此种方式,一点一点解析动态的URL,能够实现很复杂的逻辑。当然一般情况下不会这么动态,肯定还是要对URL有所规划才行。