现代的前端应用大多都是 SPA(单页应用程序),也就是只有一个 HTML 页面的应用程序。因为它的用户体
验更好、对服务器的压力更小,所以更受欢迎。为了有效的使用单个页面来管理原来多页面的功能,前端路由
应运而生。
React 路由使用
npm i react-router-dom
## 或者
yarn add react-router-dom
// 使用 H5 的 history API 实现(localhost:3000/first)
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
// 使用 URL 的哈希值实现(localhost:3000/#/first)
import { HashRouter as Router, Route, Link } from "react-router-dom";
整个应用使用一次即可!也就是用 Router 包裹整个应用
.....
这个组件最终会被渲染成一个 a 标签
to 属性表示:浏览器地址栏中的 pathname(location.pathname)
主页
Route 组件放在页面中哪个位置,那么,组件的内容就会展示在哪个地方
// 定义一个Home组件
const Home = () => 这是主页
// 匹配到的路由显示对应的组件内容
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
// 定义两个组件
const Home = () => <h1>这是主页</h1>;
const Login = () => <h1>这是登录页</h1>;
class Hello extends React.PureComponent {
render() {
return (
<Router>
<div>
<Link to="/home">主页</Link>
<br />
<Link to="/login">登录页</Link>
<Route path="/home" component={Home} />
<Route path="/login" component={Login} />
</div>
</Router>
);
}
}
路由执行过程
通过 JS 代码来实现页面跳转
history 是 React 路由提供的,用于获取浏览器历史记录的相关信息
push(path):跳转到某个页面,参数 path 表示要跳转的路径
go(n): 前进或后退到某个页面,参数 n 表示前进或后退页面数量(比如:-1 表示后退到上一页)
组件中通过 props
来获取到路由信息
// 改造上面的Home组件
const Home = props => {
console.log(props);
return (
<div>
<h1>这是主页</h1>
<button
onClick={() => {
props.history.go(-1);
}}
>
返回上一页
</button>
<button
onClick={() => {
props.history.push("/login");
}}
>
去登录页
</button>
</div>
);
};
props
中的路由信息主要有三部分
history
用于获取浏览器历史记录的相关信息(有三种模式)
参数信息
length
- (number) 历史堆栈中的条目数,即执行一次路由跳转,就会数量加1action
- (string) 当前操作(PUSH,REPLACE或POP)location
- (object) 这个就是下面的那个location,两者是一样的push(path, [state])
- (function) 将新条目推送到历史堆栈,可以用于实现路由跳转,可以实现返回replace(path, [state])
- (function) 替换历史堆栈上的当前条目,也可以实现路由跳转,但是无法再返回go(n)
- (function) 通过指定参数 n 来实现,跳转到那个堆栈信息,常用 this.props.history.go(-1)
来实现返回goBack()
- (function) 这个就相当于 go(-1)
goForward()
- (function) 这个就相当于 `go(1)block(prompt)
- (function) 常用于提示用户是否要离开当前页面location
url 地址信息
hash
url 中的 hash 值pathname
路径名称search
查询参数 就是 ?
后面的参数部分match
路由的path 和 url 相匹配的信息
params
- (object) 里面包含的就是解析url 得到的 动态路由参数,以键值对的形式存储isExact
- (boolean) 是否精确匹配path
- (string) 就是我们路由组件中 path
属性的值,用于构建嵌套路由url
- (string) 与url 匹配的部分默认路由地址为:
/
, 默认路由,在进入页面时,就会被匹配
<Route path="/" component={Home} />
只要 pathname 以 path 开头就会匹配成功
比如:当我们跳转到 “/login” 路由下时,会发现 默认路由 也匹配到了,这就是 模糊匹配 ,所以会导致这样的结果
“/” : 可以匹配到 所有 pathname
“/home” : 可以匹配到所有以 “/home” 开头的路由 ,比如 “/home” “/home/abc”,但是无法匹配 “/home1” ,需要把 “/home” 当做一个整体
只有当 path 和 pathname 完全匹配时才会展示该路由
exact
属性,就会让当前路由规则变为精确匹配// 精确匹配:
// pathname 是 '/first',path 是 '/',此时,就不会匹配了
// 只有当 pathname 是 '/' 并且 path 也是 '/' ,此时才会匹配
<Route exact path="/" component={Home} />
推荐:给默认路由添加 exact 属性。
在 React 中,我们是用的 Route 就是一个组件,通过这个组件来控制匹配的路由展示哪一个组件的内容,既然是展示的内容是一个组件,那么就可以在那个组件中再来一个路由,这样就实现了嵌套路由
const List = () => <p>这是主页中的一个子路由</p>;
const Home = props => {
console.log(props);
return (
<div>
<h1>这是主页</h1>
<Route path="/home/list" component={List} />
</div>
);
};
注意:子路由的 path 必须以父路由的 path 开头,我们既要展示 home 页的内容,又要展示 list 页的内容,那么就要这样使用 /home/list
,如果你只想展示 list 的内容就没有必要用什么嵌套路由了
react-router-dom
提供了一个组件 Redirect
用于实现重定向
一般我们都是把 ‘/’ 默认路由重定向到首页,这样可以使得在地址栏中的输入更简洁
// 通过 render 属性来返回一个 组件,这个组件可以实现重定向 to 表示定向到那个路由下面
<Route exact path='/' render={() => <Redirect to='/index'/>}
注意:只有通过路由 Route 组件直接渲染的内容,才能够通过 props 获取到路由信息。也就是说:间接渲染的组件(子组件),它不是直接通过 Route 组件渲染出来的,因此,这个组件中,无法直接获取到路由相关信息。
解决方法:
withRouter
来包装 NavHeader 组件,这样,在该组件中就可以获取到路由信息了。import React from "react";
import { NavBar } from "antd-mobile";
function NavHeader({ history, children }) {
return (
<NavBar
className={styles.navBar}
mode="light"
icon={<i className="iconfont icon-back" />}
onLeftClick={() => history.go(-1)}
>
{children}
</NavBar>
);
}
// 通过高阶组件,让我们的 NavHeader 组件有了相应的路由信息
export default withRouter(NavHeader);
让一个路由规则,可以同时匹配多个符合该规则(格式)的 URL 路径。
示例:
/detail/:id
<Route path="/detail/:id" component={xxx} />
/detail/:id/:name
<Route path="/detail/:id/:name" component={xxx} />
第一个情况可以匹配一个参数 id ,可匹配 /detail/1
或者 /detail/2
第二个情况可以匹配两个参数 id 和 name ,匹配 /detail/1/zs
或者 /detail/2/ls
通过路由渲染的组件,可以直接通过 props
来获取相应的路由信息 >>>>> proprs.match.params
对应上面的示例(使用 class 组件就加 this,用函数组件就不加 this )
// /detail/:id ----> /detail/1
const { id } = this.props.match.params
// /detail/:id/:name ----> /detail/1/zs
const { id , name} = this.props.match.params
props.match.params
中对应的参数属性名就是 路由参数中的 名字 例如 id
或者 name
所谓的鉴权路由就是指需要有某些权限才能访问的页面,比如需要登录之后才可以访问,或者有某些角色权限才可以访问,与Vue中的导航守卫功能类似
在react
中,都是基于组件的,并没有想Vue中那样帮我们直接在路由中集成了,因此我们需要自己封装一个组件来实现鉴权路由。react-router-dom
尽管在文档里没有说怎么做,但是给了我们一个完整的例子 Redirect(Auth)
分析例子发现,其实所谓的鉴权路由,其实实际渲染的就是 Route
只是在外面进行了一层包裹处理
function PrivateRoute({ component: Component, ...rest }) {
return (
<Route
{...rest}
render={props =>
fakeAuth.isAuthenticated ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/login",
state: { from: props.location }
}}
/>
)
}
/>
);
}
上面这段代码中的render
,具体成文字描述就是 : 访问 Component
页面组件的时候,如果已经登录了,则渲染 Component
这个页面组件(传递过来的参数是小写字母开头,但是react中的组件是以大写字母开头因此进行重命名),否在就通过重定向跳转到 /login
页面,进行登录,并且传递一个 state
过去,用于当登录成功后,跳转到对应的页面,而不是简单的go(-1)
。
我们封装组件的时候 剩余参数(…rest)这个是干嘛用的呢 ? 其实 Route
组件还有一些别的参数,比如 exact
这些其他参数我们如果不传递过来使用,那么我们封装的组件就会不起作用,无法实现例如精确比配这样的功能,封装路由组件的原则就是不改变原来Route的使用方式,与之前的使用保持一致性
这样封装后,我们就可以用这个组件来替换之前的 Route
路由了
// 原先
<Route path="/protected" component={Protected} />
// 现在
<PrivateRoute path="/protected" component={Protected} />
具体效果可以通过上面的连接进行访问查看
<NavLink activeClassName="todos-active" to="/all">
ALL
</NavLink>
<Switch>
<Route exact path="/"/>
<Route exact path="/:filter?"/>
</Switch>
// 注意:如果两个路由规则中有重叠的部分,那么,应该使用 Switch 组件来包裹。
// 并且 匹配范围小的在前,匹配范围大的在后
<Switch>
<Route path="/detail/:id"/>
<Route path="/:type/:page"/>
</Switch>
CSS IN JS
:是 React 中用来解决组件之间样式相互影响、覆盖问题的一类解决方案的统称
常用的两个方案
CSS Modules 方案解决样式冲突的原理:将我们写的类名,替换为全局唯一的样式名称
BEM
的命名规范[filename]_[classname]__[hash]
[name].module.css
的样式文件import styles from './index.module.css'
// navBar 就是我们自己写的 CSS 类名
// 'NavHeader_navBar__Sby3N' 这是 脚手架 自动生成的全局唯一的类名
// styles 是一个对象
styles = {
navBar: "NavHeader_navBar__Sby3N";
};
使用原则
:global()
包裹(比如:字体图标的样式、组件库的样式等)/* 通过 :global() 来告诉 react 脚手架这是一个全局类名,不要对它进行重命名 */
.navBar :global(.icon-back) {
color: #333;
}
.navBar :global(.am-navbar-title) {
color: #333;
}
/* SASS 配合 CSSModules 使用 */
.navBar {
margin-top: -45px;
background-color: #f6f5f6;
/* 全局样式: */
:global {
.icon-back,
.am-navbar-title {
color: #333;
}
}
}
举个开发中遇到的实际例子
场景:使用了 antd-mobile 组件库中的一个 PickView 组件,并且有三个按钮共用这一个组件,只是渲染的数据是不同的,点击按钮 A,渲染省市数据,点击按钮 B,渲染季节,点击按钮 C,渲染工资范围
问题:当我们在三个中都选择了某些数据条件的情况下,点击任意一个,然后随意切换,这时候会发现我们选择的数据并没有默认的显示(前提是已经设置了 PickView 的 value 属性,并且值是我们选择的值)
分析:这里我们在这个 PickView 组件的外面包裹了一个组件,因为有三类的数据,所以就进行了状态提升。当我们点击按钮进行切换的时,组件是处于更行阶段的,而我们的默认数据是通过父子组件传值从父组件传递过来的,在 state 中进行了初始化赋值,但是这初始化的操作只在挂载阶段执行一次不会再次执行。所以就不会多次出发赋值
// 个别组件不用在意,这是我自己项目用使用到的
export default class FilterPicker extends Component {
// 把父组件传递过来的值赋值给value,这样当我们选择数据后,再次点开就可看到上次的数据
state = {
value: this.props.defaultValue
};
onChange = val => {
this.setState({
value: val
});
};
render() {
const { onCancle, onSave, data, cols, type } = this.props;
return (
<>
{/* 选择器组件: */}
<PickerView
data={data}
value={this.state.value}
onChange={this.onChange}
cols={cols}
/>
{/* 底部按钮 */}
<FilterFooter
onCancle={onCancle}
onSave={() => onSave(type, this.state.value)}
/>
</>
);
}
}
解决办法:
<FilterPicker
key={openType}
defaultValue={defaultValue}
type={openType}
cols={cols}
data={data}
onCancle={this.onCancle}
onSave={this.onSave}
/>
create-react-app 中隐藏了 webpack 的配置,隐藏在 react-scripts 包中。
修改脚手架的 webpack 配置有两种方式:
组件库的按需加载(如antd-mobile)
这部分在官网有详细操作,可以按照官网的步骤来执行
基于路由的代码分割
目的:将代码按照路由进行分割,只在访问该路由时才加载该组件内容,提高首屏加载速度。
React.lazy() 方法 + import() 方法 、Suspense 组件 代码分割
React.lazy() 作用:处理动态导入的组件,让其像普通组件一样使用。
import(‘组件路径’) 作用: 告诉 webpack ,这是一个代码分割点,进行代码分割。
Suspense 组件:用来在动态组件加载完成之前,显示一些 loading 内容,需要包裹动态组件内容。
这是一个比较常见的效果,当我们滚动页面的时候,当滚动一定距离,某个组件就会停留在顶部,比如搜索框、导航栏等
常见的实现方式有两种:
div
来标志是否到达定不了(top=0 ? ),当到达顶部后即(top=0)让目标组件增加样式后脱标了,占位容器高度变为目标容器的高度,这个值可以通过使用组件的时候传递过来import React, { Component, createRef } from "react";
import styles from "./index.module.scss";
import PropTypes from "prop-types";
class Sticky extends Component {
placeholderRef = createRef();
contentRef = createRef();
handleScroll = () => {
const height = this.props.height;
// console.log(this.contentRef.current.getBoundingClientRect().height);
const placeholder = this.placeholderRef.current;
const content = this.contentRef.current;
const { top } = placeholder.getBoundingClientRect();
if (top <= 0) {
// 需要让筛选框吸顶,并且让占位内容有高度(高度为筛选框的高度)
placeholder.style.height = height;
content.classList.add(styles.fixed);
} else {
// 占位内容高度为0,筛选框取消吸顶
placeholder.style.height = 0;
content.classList.remove(styles.fixed);
}
};
// 添加窗口滚动事件
componentDidMount() {
window.addEventListener("scroll", this.handleScroll);
}
// 组件销毁的时候注销事件
componentWillUnmount() {
window.removeEventListener("scroll", this.handleScroll);
}
render() {
const { children } = this.props;
return (
<div>
{/* 占位符,用于当筛选框吸顶脱标后撑高度 */}
<div ref={this.placeholderRef} />
{/* 筛选框的内容 */}
<div ref={this.contentRef}>{children}</div>
</div>
);
}
}
Sticky.propTypes = {
height: PropTypes.number.isRequired
};
export default Sticky;
吸顶的时候添加的样式
.fixed {
position: fixed;
top: 0;
z-index: 1;
width: 100%;
}
使用吸顶组件