SPA
Single Page Application : 单页面应用,整个应用只加载一个页面(入口页面),后续在与用户的交互过程中,通过 DOM 操作在这个单页上动态生成结构和内容
优点:
- 有更好的用户体验(减少请求和渲染和页面跳转产生的等待与空白),页面切换快
- 重前端,数据和页面内容由异步请求(AJAX)+ DOM 操作来完成,前端处理更多的业务逻辑
缺点:
- 首次进入处理慢
- 不利于 SEO
SPA 的页面切换机制
虽然 SPA 的内容都是在一个页面通过 JavaScript 动态处理的,但是还是需要根据需求在不同的情况下分内容展示,如果仅仅只是依靠 JavaScript 内部机制去判断,逻辑会变得过于复杂
通过把 JavaScript 与 URL 进行结合的方式:JavaScript 根据 URL 的变化,来处理不同的逻辑,交互过程中只需要改变 URL 即可。
这样把不同 URL 与 JavaScript 对应的逻辑进行关联的方式就是路由,其本质上与后端路由的思想是一样的。
前端路由
前端路由只是改变了 URL 或 URL 中的某一部分,但一定不会直接发送请求,可以认为仅仅只是改变了浏览器地址栏上的 URL 而已,JavaScript 通过各种手段处理这种 URL 的变化,然后通过 DOM 操作动态的改变当前页面的结构
- URL 的变化不会直接发送 HTTP 请求
- 业务逻辑由前端 JavaScript 来完成
目前前端路由主要的模式:
- 基于 URL Hash 的路由
- 基于 HTML5 History API 的路由
https://developer.mozilla.org/zh-CN/docs/Web/API/History_API
React Router
理解了路由基本机制以后,也不需要重复造轮子,我们可以直接使用 React Router 库
React Router 提供了多种不同环境下的路由库
- web
- native
基于 Web 的 React Router
基于 web 的 React Router 为:react-router-dom
安装
npm i -S react-router-dom
路由组件
BrowserRouter 组件 – history
基于 HTML5 History API 的路由组件
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, HashRouter } from "react-router-dom";
import App from './App';
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
);
HashRouter 组件 – hash
基于 URL Hash 的路由组件
Route 组件
通过该组件来设置应用单个路由信息,
Route 组件所在的区域就是就是当 URL 与当前 Route 设置的 path 属性匹配的时候,后面 component 将要显示的区域
属性
-
path
- path 匹配规则: 默认情况下,path是一种模糊匹配,以path为开始时,则认定匹配
- 想要精确匹配时,要设置
exact
为 true - 严格匹配:非严格匹配下,当url为
/about/
也能匹配成功
如果要严格匹配需要设置strict
属性
必须与精确匹配同时存在 - 多路径匹配时:
[路径1, 路径2]
- 当path为空或不写path时 Router 代表所有组件都匹配
-
component
<Fragment>
<Route path="/" exact component={IndexView} />
<Route path={["/", "/home", "/h"]} exact component={IndexView} />
<Route path="/about" exact strict component={AboutView} />
<Route path="/join" component={JoinView} />
</Fragment>
传递 props
<Route exact path='/' component={Home}>
如果 Route 使用的是 component 来指定组件,那么不能使用 props
Route的 render 属性
接收函数,函数的返回值必须是组件
<Route path="/message" render={() => {
return <MessageView userName={userName} />;
}} />
子组件打印props
import React from 'react';
const MessageView = (props) => {
console.log(props);
return (
<h3>留言板</h3>
);
};
export default MessageView;
通过 render 属性来指定渲染函数,render 属性值是一个函数,当路由匹配的时候指定该函数进行渲染
Switch 组件
功能类似于switch语句
一项匹配成功不再匹配后续路径
例如显示404页面
<Switch>
<Route path="/" exact component={IndexView} />
<Route path={["/", "/home", "/h"]} exact component={IndexView} />
<Route path="/about" exact strict component={AboutView} />
<Route path="/join" component={JoinView} />
<Route component={NotFound} />
</Switch>
Link 组件
Link 组件用来处理 a 链接 类似的功能(它会在页面中生成一个 a 标签),但设置这里需要注意的,react-router-dom 拦截了实际 a 标签的默认动作,然后根据所有使用的路由模式(Hash 或者 HTML5)来进行处理,改变了 URL,但不会发生请求,同时根据 Route 中的设置把对应的组件显示在指定的位置
to 属性
to 属性类似 a 标签中的 href
import React from 'react';
import { Link } from "react-router-dom";
const NavView = () => {
return (
<nav>
<Link to="/">首页</Link>
<span> | </span>
<Link to="/about">关于</Link>
<span> | </span>
<Link to="/join">加入</Link>
</nav>
);
};
export default NavView;
Redirect 组件
设置跳转的 URL
<Route path="/login" component={LoginView} />
<Route path="/message" render={() => {
// return <MessageView userName={userName} />;
return <Redirect to="/login" />;
}} />
Route组件 的路由信息
被Router组件的component 直接调用的组件称之为路由组件
Route在调用路由组件时会传递一些相关的路由信息
history: 当前URL信息,及操作URL的相关方法
- go: ƒ go(n) 历史记录跳转几项,正值向前跳转,负值向后跳转
- goBack: ƒ goBack() 返回上一步
- goForward: ƒ goForward() 前进到下一步
- length: 40 历史记录中共记录几项
- location: {pathname: “/”, search: “”, hash: “”, state: undefined}
url 信息 pathname- 当前URL,
- search 当前 search 值,
- hash当前 hash 值,
- 历史记录传递的信息 state
- push: ƒ push(path, state) 跳转
- replace: ƒ replace(path, state) 跳转
location: URL信息
{pathname: "/", search: "", hash: "", state: undefined}
match:
{path: "/", url: "/", isExact: true, params: {…}} 匹配信息,path 当前 route 的 path
params path 传递的参数
render 属性中的路由信息
<Route path="/login" render={(routeProps) => {
return <LoginView setUserName={setUserName} {...routeProps} />;
}} />
其中的history.push()并不会触发浏览器的刷新,知识同步路由更新视图
import React, { Fragment, useEffect } from 'react';
const LoginView = (props) => {
console.log(props);
let {history} = props;
useEffect(() => {
new Promise((resolve) => {
setTimeout(() => {
props.setUserName("myName");
resolve();
}, 1000);
}).then(() => {
console.log("登陆成功,回到首页/留言页");
history.push("/")
});
}, []);
return (
<Fragment>
<h2>登陆</h2>
<button >login</button>
</Fragment>
);
};
export default LoginView;
高阶路由 withRouter函数
如何在非路由组件中使用路由信息?
- 一步一步传递
- 可以使用高阶路由withRouter
如果一个组件不是路由绑定组件,那么该组件的 props 中是没有路由相关对象的,虽然我们可以通过传参的方式传入,
但是如果结构复杂,这样做会特别的繁琐。幸好,我们可以通过 withRouter 方法来注入路由对象
类组件使用
- 调用withRouter时 必须传入一个组件,withRouter会返回一个新的组件
- 其他地方调用新组建时,新组件内部会调用传入的组件,并将路由信息传递给该组件
非路由类组件,使用withRouter函数
import React, { Component } from 'react';
import { withRouter } from 'react-router-dom';
class AboutDetails1 extends Component {
render() {
return (
<h2>关于详情1</h2>
);
}
}
AboutDetails1 = withRouter(AboutDetails1)
export default AboutDetails1;
// export default withRouter(AboutDetails1);
父组件调用子组件时,接收到路由
import React, { Fragment } from 'react';
import AboutDetails1 from './aboutDetails1';
export default function AboutView(props) {
console.log(props);
return (
<Fragment>
<h1>
about
</h1>
<AboutDetails1 />
</Fragment>
);
}
函数式组件使用 withRouter
import React from 'react';
import { withRouter } from 'react-router-dom';
const AboutDetails2 = (props) => {
console.log(props);
return (
<h2>关于详情2</h2>
);
};
export default withRouter(AboutDetails2);
函数式组件通过 hooks 获取路由信息
- useHistory()
- useParams()
- useRouteMatch()
- useLocation()
import React from 'react';
import { useHistory, useRouteMatch, useLocation } from 'react-router-dom';
const AboutDetails2 = (props) => {
let history = useHistory();
console.log(history);
console.log(useRouteMatch());
console.log(useParams());
console.log(useLocation());
return (
<h2>关于详情2</h2>
);
};
export default AboutDetails2;
URL参数匹配的情况
- 在 path 中 通过
:paramsName
- 在URL中截取对应值
paramsValue
<Route path="/about/:type/:page" exact strict component={AboutView} />
NavLink 组件
NavLink 与 Link 类似,但是它提供了两个特殊属性用来处理页面导航
activeClass/activeClassName
activeClass
:当前项被选中时的class,默认为active- 可通过
activeClassName
属性手动设置class
import React from 'react';
import { NavLink } from "react-router-dom";
const NavView = () => {
return (
<nav>
<NavLink to="/" strict exact activeClassName="selected">首页</NavLink>
<span> | </span>
<NavLink to="/about" activeClassName="selected">关于</NavLink>
<span> | </span>
<NavLink to="/join" activeClassName="selected">加入</NavLink>
<span> | </span>
<NavLink to="/message" activeClassName="selected">留言</NavLink>
<span> | </span>
<NavLink to="/login" activeClassName="selected">登陆</NavLink>
</nav>
);
};
export default NavView;
activeStyle
定义当前项被选中时的style
当 URL 与 NavLink 中的 to 匹配的时候,激活 activeStyle 中的样式
<nav>
<NavLink to="/" strict exact activeStyle={{
color: "orange"
}}>首页</NavLink>
<span> | </span>
<NavLink to="/about" activeStyle={{
color: "orange"
}}>关于</NavLink>
<span> | </span>
<NavLink to="/join" activeStyle={{
color: "orange"
}}>加入</NavLink>
<span> | </span>
<NavLink to="/message" activeStyle={{
color: "orange"
}}>留言</NavLink>
<span> | </span>
<NavLink to="/login" activeStyle={{
color: "orange"
}}>登陆</NavLink>
</nav>
isActive
- isActive 是一个函数,返回布尔值
- 默认情况下,匹配的是 URL 与 to 的设置,
- 通过 isActive 可以自定义激活逻辑,
永远持有activeStyle
<NavLink
to="/join"
activeStyle={{
color: "orange"
}}
isActive={() => {
return true;
}}
>加入</NavLink>
返回false 就是永远不持有activeStyle
路由的开发技巧
自定义动态的路由数据
路由数据
import React from "react";
import IndexView from "../view/index/index";
import ListView from "../view/list/index";
import AboutView from "../view/about/index";
import View404 from "../view/404/index";
/*
主导航:
首页
列表页
全部
新闻
问题
作品
关于我们
*/
const route_list = [
{
path: ["/", "/index"],
exact: true,
render(props) {
return <IndexView {...props} />;
}
}, {
path: ["/list/:type/:page", "/list/:type", "/list"],
exact: true,
render(props) {
return <ListView {...props} />;
}
}, {
path: "/about",
exact: true,
render(props) {
return <AboutView {...props} />;
}
}, {
path: "",
exact: true,
render(props) {
return <View404 {...props} />;
}
}
];
const menu = [
{
title: "首页",
to: "/",
exact: true,
activeClass: "active",
active(url) {
// 接收当前 url ,自己定义规则是否需要选中当前项
return url === "/index" || url === "/";
}
}, {
title: "列表页",
to: "/list",
exact: true,
activeClass: "active",
active(url) {
return url.split("/")[1] === "list";
}
}, {
title: "关于我们",
to: "/about",
exact: true,
activeClass: "active"
}
];
/*
全部
/list ""/list/all /list/all/pageNub
新闻
/list/news /list/news/pageNub
问题
/list/ask /list/ask/pageNub
作品
/list/example /list/example/pageNub
*/
const listMenu = [
{
title: "全部",
to: "/list",
exact: true,
activeClass: "listActive",
active(url) {
if (url === "/list") {
return true;
}
let urls = url.split("/");
return urls[2] === "all";
}
}, {
title: "新闻",
to: "/list/news",
exact: true,
activeClass: "listActive",
active(url) {
let urls = url.split("/");
return urls[2] === "news";
}
}, {
title: "问题",
to: "/list/ask",
exact: true,
activeClass: "listActive",
active(url) {
let urls = url.split("/");
return urls[2] === "ask";
}
}, {
title: "作品",
to: "/list/example",
exact: true,
activeClass: "listActive",
active(url) {
let urls = url.split("/");
return urls[2] === "example";
}
}
];
export { route_list, menu, listMenu };
利用路由数据和es6自动生成路由逻辑
路由主视图
import React from "react";
import { Switch, Route } from "react-router-dom";
import { route_list } from "./route_list";
function IndexRoute(props) {
return <Switch>
{
route_list.map((item, index) => {
return <Route
key={index}
path={item.path}
exact={item.exact}
render={(routerProps) => {
return item.render({ ...routerProps, ...props });
}}
/>;
})
}
</Switch>;
}
export default IndexRoute;
nav按钮视图
import React, { Fragment } from "react";
import { menu } from "../router/route_list";
import { NavLink, useLocation } from "react-router-dom";
function Nav() {
let { pathname } = useLocation();
return <Fragment>
<nav>
{menu.map((item, index) => {
return <NavLink
to={item.to}
activeClassName={item.activeClass}
key={index}
exact={item.exact}
isActive={item.active ? () => {
return item.active(pathname);
} : null}
>{item.title}</NavLink>;
})}
</nav>
<hr />
</Fragment>;
}
export default Nav;