路由本质上是操作的history的地址的历史记录
SPA应用(single page web application)
- 整个应用只有一个完整的页面
- 点击页面的链接不会刷新页面,只会做页面的局部更新
- 数据都需要通过ajax请求获取,并在前端异步展现
单页面用显示隐藏其实也可以实现,但是没人会用,因为页面初始化时所有的标签都会渲染,会增加巨大的开销。
理解react路由
多页面网站是通过超链接来实现页面的切换的, 所有每次点击超链接时浏览器都需要刷新请求新的页面,而单页面应用程序主要通过路由来实现内容的切换,在react当中也就是切换组件
那如何实现组件的切换呢?
react路由是通过一种特殊的链接, 点击时会修改浏览器地址栏中的地址,但是浏览器是会忽略掉这次变化所以界面不会被跳转,但是在路由会监听到地址(path) 的变化从而根据不同的地址渲染出来不同的组件(component)
相当于给组件打标识(一种特殊的链接),并根据不同的情况进行''条件渲染''。
react route
目前,官方同时维护2.x和4.x两个版本,React 16及以上版本只支持router4 x版本
react Router4.0 (以下简称RR4)它遵循React的设计理念, 即万物皆组件。所以RR4只是一堆提供了导航功能的组件(还有若干对象和方法),具有声明式 (引入即用),可组合性的特点。
安装
cnpm install react-router-dom -D
三大核心内容
- 路由器组件
- 路由匹配组件
- 导航组件
路由器组件
作用:根据地址栏的变化引导显示相应组件,一般会包裹在最外层
最常用的路由器分为两种:<BrowserRouter> <HashRouter>
1、<Router>
如果我们希望页面中某个部分的内容需要根据URL来动态显示,需要用到Router组件,该组件是一个容器组件,只需要用它包裹URL对应的根组件即可
Router是所有路由组件共用的底层接口,一般我们的应用并不会使用这个接口, 而是使用高级的路由(用来是为了监听)
2、<BrowserRouter>
<BrowserRouter>:使用HTML5提供的history API来保持UI和URL的同步;(因此可以使用前进后退)
3、<HashRouter>
使用URL的hash (例如: window.location.hash)来保持UI和URL的同步 )(基本不用)
4、<MemoryRouter>
能在内存保存你“URL"的历史纪录(并没有对地址栏读写);
5.<StaticRouter>
从不会改变地址;
注:这里<Router>组件下只允许存在一个子元素,如存在多个则会报错。
路由匹配组件
作用:注册路由
(1)<Route>
Route组件主要的作用就是当location匹配路由的path时, 渲染某些UI。
(2)<Route>组件有如下属性
- path (sting) :路由匹配路径。(没有path属性的Route 总是会匹配) ;
- component: 设置要显示的组件,该属性以props的方式传入
- exact (bool) : exact 属性表示路由使用精确匹配模式,非exact模式下匹配所有以“开头的路由,一般都会设置,值可以省略
<Route path="/about" component={About}></Route>
<Route path="/home" component={Home}></Route>
导航组件
作用:替换a标签。点击时,实现地址栏地址的改变
点击时,其实react-router-dom拦截了a标签的默认动作,然后根据所有使用的路由模式进行处理,改变了url但不会发送请求,同时根据router中设置的对应组件来进行显示。
导航组件有两类:Link和NavLink
NavLink是对Link的一个特定版本,作用是当匹配到当前的url时会给已渲染的 元素添加样式参数。
使用语法:
<li> <NavLink to="/about" activeClassName="navActive"> About</NavLink> </li>
<li> <NavLink to="/home" activeClassName="navActive"> Home</NavLink> </li>
当点击当前link时,会自动给当前标签加一个“navActive”的类。这时我们可以自定义该类的 css样式。
switch
一个路径匹配多个组件的时候,会用到switch。 (效率的优化)
因为路由匹配到匹配到一个后还会继续向下匹配。(只匹配一个)
使用:把route那switch包一下
<Switch>
<Route path="/about" component={About}></Route>
<Route path="/home" component={Home}></Route>
</Switch>
精准匹配与模糊匹配
<Link to="/home/a/b"></Link>
<Route path="/home" component={Nav}> </Route>
上面可以匹配上。这是模糊匹配
如果加上exact,会开启严格匹配,地址必须精准对应。
注意:严格匹配不能随便开!,有时开启会导致无法匹配二级路由。
redirect 重定向
重定向。当用户访问某界面时,该界面并不存在,此时用Redirect重定向,重新跳到一个我们自定义的组件里。
写在注册路由最后位置,和router写在一起。
<Switch>
<Route path="/about" component={About}></Route>
<Route path="/home" component={Home}></Route>
<Redirect to="/about" />
</Switch>
上面代码,在地址栏输入localhost:3000
时,会重定向至/about
一级路由练习
组件结构:
- about
- home
(1)定义两个组件
(2)在app.js中进行Link的设置和路由的注册
(3)在index.js中使用B
about.jsx
// home组件
import React from "react";
class About extends React.Component{
render(){
return(
<div>
这是About的内容
</div>
)
}
}
export default About
home.jsx
// home组件
import React from "react";
class Home extends React.Component{
render(){
return(
<div>
这是Home的内容
</div>
)
}
}
export default Home
app.jsx
import { Route, Switch,Redirect } from "react-router-dom";
import React from "react";
import MyNavLink from "./component/pages/MyNavLink/MyNavLink";
// 引入组件
import About from "./component/pages/about/about";
import Home from "./component/pages/home/home";
class App extends React.Component {
render() {
return (
<div id="zz">
<li>
<MyNavLink to="/about">About </MyNavLink>
</li>
<li>
<MyNavLink to="/home">Home</MyNavLink>
</li>
<Switch>
<Route path="/about" component={About}></Route>
<Route path="/home" component={Home}></Route>
<Redirect to="/about" />
</Switch>
</div>
);
}
}
export default App;
index.js
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter, Route } from "react-router-dom";
import App from './App'
import "./index.css";
ReactDOM.render(
// BrowserRouter 组件法放在这个位置,是因为全局所有的路由都包起来,因为,不同的路由器之间不能通信
<BrowserRouter>
<App/>
</BrowserRouter>,
document.getElementById("root")
);
嵌套路由(二级路由)
组件结构:在一级路由的基础上书写二级路由
将About下设置两个子组件:aboutSon1、aboutSon2.(除了About,其他东西没动)
然后再About组件中设置二级路由。
路由匹配先从一级路由开始匹配
点二级路由时,先匹配一级的about,然后再在一级的路由向下匹配/about/left
注意此时不能加exact ,开启后,子路由就废了,因为不开始模糊匹配。
模糊匹配:如果书写的路由多,则可以匹配到,如果少了就不行。
例如若给的是/home/a/b,则先匹配到一级的home,这就算匹配上了,这就不会出错。
about.jsx
// about组件
import React from "react";
import AboutSon1 from "./aboutSon1/aboutSon1";
import AboutSon2 from "./aboutSon2/aboutSon2";
import { Route, NavLink ,Redirect} from "react-router-dom";
class About extends React.Component {
render() {
return (
<ul>
<li>
<NavLink to="/about/left" activeClassName="navActive">
左边
</NavLink>
</li>
<li>
<NavLink to="/about/right" activeClassName="navActive">
右边
</NavLink>
</li>
<Route path="/about/left" component={AboutSon1}></Route>
<Route path="/about/right" component={AboutSon2}></Route>
<Redirect to="/about/right" />
</ul>
);
}
}
export default About;
aboutSon1.jsx
import React, { Component } from "react";
export default class AboutSon1 extends Component {
render() {
return <div className="about-left">这时about右边的内容</div>;
}
}
aboutSon2.jsx
import React, { Component } from "react";
export default class AboutSon2 extends Component {
render() {
return <div className="about-right">这时about右边的内容</div>;
}
}
路由传值
param传值
三步走:
(1)路由时,携带参数
(2)在子组件声明接受params参数
(3)子组件取值
传来的值毫无疑问是在props中,我们来看一下,他在什么位置。
如图可知,在this.props.match.params;
中。
(1)传值
<ul>
{this.state.messages.map(message => {
return (
<li key={message.id}>
//传值
<Link to={`/about/right/message/${message.id}/${message.title}`}> {message.title}</Link>
</li>
);
})}
</ul>
(2)声明接收
{/*声明接受 */}
<Route path="/about/right/message/:id/:title" component={ Details}/>
(3)取值
const { id, title } = this.props.match.params;
完整代码:
message:
import React, { Component } from "react";
import { Route, Link } from "react-router-dom";
import Details from "./details/details";
export default class Message extends Component {
state = {
messages: [
{
id: "01",
title: "title1"
},
{
id: "02",
title: "title2"
},
{
id: "03",
title: "title3"
}
]
};
render() {
return (
<div>
<ul>
{this.state.messages.map(message => {
return (
<li key={message.id}>
<Link to={`/about/right/message/${message.id}/${message.title}`}> {message.title}</Link>
</li>
);
})}
</ul>
<hr />
{/*声明接受 */}
<Route path="/about/right/message/:id/:title" component={ Details}/>
</div>
);
}
}
messageDetails:
import React, { Component } from "react";
export default class Details extends Component {
state = {
messageDetail: [
{
id: "01",
content: "你好中国"
},
{
id: "02",
content: "你好南京"
},
{
id: "03",
content: "你好杭州"
}
]
};
render() {
console.log(this.props);
const { id, title } = this.props.match.params;
const content = this.state.messageDetail.find(value => {
return value.id == id;
});
return (
<li>
<ul>
<li>id:{id}</li>
<li>title:{title}</li>
<li> content:{content.content}</li>
</ul>
</li>
);
}
}
效果:
有历史记录,replace没有历史记录。
通过search传值:
这个仅需两步走:
(1)传值
(2)子组件取值
注:无需声明接收
再看一下传过来的参数在什么位置 this.props.location.search
引入一个库:
import qs from "querystring";
这个可转么做search类型的解析转化:
两个方法:
qs.parse() 将
"id=01&title=title1"
解析成{id: "01", title: "title1"}
qs.stringify():将
{id: "01", title: "title1"}
解析成"id=01&title=title1"
"id=01&title=title1"
是encoded格式编码的字符串
(1)传值
<ul>
{this.state.messages.map(message => {
return (
<li key={message.id}>
<Link to={`/about/right/message/details/?id=${message.id}&title=${message.title}`}> {message.title}</Link>
</li>
);
})}
</ul>
(2)读取
const { id, title } = qs.parse(search.slice(1));
完整代码:
message:
import React, { Component } from "react";
import { Route, Link } from "react-router-dom";
import Details from "./details/details";
export default class Message extends Component {
state = {
messages: [
{
id: "01",
title: "title1"
},
{
id: "02",
title: "title2"
},
{
id: "03",
title: "title3"
}
]
};
render() {
return (
<div>
<ul>
{this.state.messages.map(message => {
return (
<li key={message.id}>
<Link to={`/about/right/message/details/?id=${message.id}&title=${message.title}`}> {message.title}</Link>
</li>
);
})}
</ul>
<hr />
{/*声明接受 */}
<Route path="/about/right/message/details" component={ Details}/>
</div>
);
}
}
messageDetails:
import React, { Component } from "react";
import qs from "querystring";
export default class Details extends Component {
state = {
messageDetail: [
{
id: "01",
content: "你好中国"
},
{
id: "02",
content: "你好南京"
},
{
id: "03",
content: "你好杭州"
}
]
};
render() {
console.log(this.props);
// 接收search
const { id, title } = qs.parse(search.slice(1));
const result = this.state.messageDetail.find(value => {
return value.id == id;
});
console.log(result)
return (
<li>
<ul>
<li>id:{id}</li>
<li>title:{title}</li>
<li> content:{result.content}</li>
</ul>
</li>
);
}
}
但只要页面刷新,数据就会丢失。
通过state传值:
这个需要两步走:
(1)传值
(2)读取
也是无需声明接收
老套路,看位置
在this.props.location.state
中
(1)传值
<ul>
{this.state.messages.map(message => {
return (
<li key={message.id}>
<Link to={ { pathname:'/about/right/message/details',state:{id:message.id,title:message.title}}}> {message.title}</Link>
</li>
);
})}
</ul>
(2)读取
const { id, title } = this.props.location.state;
完整代码:
message:
import React, { Component } from "react";
import { Route, Link } from "react-router-dom";
import Details from "./details/details";
export default class Message extends Component {
state = {
messages: [
{
id: "01",
title: "title1"
},
{
id: "02",
title: "title2"
},
{
id: "03",
title: "title3"
}
]
};
render() {
return (
<div>
<ul>
{this.state.messages.map(message => {
return (
<li key={message.id}>
{/* <Link to={`/about/right/message/details/?id=${message.id}&title=${message.title}`}> {message.title}</Link> */}
<Link to={ { pathname:'/about/right/message/details',state:{id:message.id,title:message.title}}}> {message.title}</Link>
</li>
);
})}
</ul>
<hr />
{/*声明接受 */}
<Route path="/about/right/message/details" component={ Details}/>
</div>
);
}
}
messageDetails:
import React, { Component } from "react";
import qs from "querystring";
export default class Details extends Component {
state = {
messageDetail: [
{
id: "01",
content: "你好中国"
},
{
id: "02",
content: "你好南京"
},
{
id: "03",
content: "你好杭州"
}
]
};
render() {
console.log(this.props);
// 接收search
const { id, title } = this.props.location.state;
const result = this.state.messageDetail.find(value => {
return value.id == id;
});
console.log(result);
return (
<li>
<ul>
<li>id:{id}</li>
<li>title:{title}</li>
<li> content:{result.content}</li>
</ul>
</li>
);
}
}
注:
刷新丢失。
路径无提示
优点:优雅,可传对象。
路由组件与一般组件
最大区别就是:路由组件会受到路由器传递过来的props信息,核心的是上面三个。
- 存放位置不同
- 写法不同
replace和push
路由跳转的两种模式
浏览器的存储区是以一个栈,每次push时都会压栈。
replace则会替换每次的历史记录
语法:replace={true}
<Link replace={true} to={ { pathname:'/about/right/message/details',state:{id:message.id,title:message.tit}}}> {message.title}</Link>
默认是push模式
编程式路由导航
之前的路由跳转都是使用路由连接Link组件实现。
但有些需求是路由是动态的,例如等待三秒后跳转路由。
手动前进后退
路由组件的懒加载
路由的懒加载:将路由组件进行分包。可以提升首页的加载速度,提高用户的转化率。
(1)引入
import React, {lazy,Suspense} from 'react';
import {BrowserRouter as Router, NavLink, Route} from "react-router-dom"
(2)
const One = lazy(() => import ("./pages/One"));
const Two = lazy(() => import ("./pages/Two"));
const Three = lazy(() => import ("./pages/Three"));
(3)使用
<Router>
<NavLink to={"/"} exact>one</NavLink>
<NavLink to={"/two"}>two</NavLink>
<NavLink to={"/three"}>three</NavLink>
<Suspense fallback={<p>加载中......</p>}>
<Route path={"/"} component={One} exact></Route>
<Route path={"/two"} component={Two}></Route>
<Route path={"/three"} component={Three}></Route>
</Suspense>
</Router>
深入理解React:懒加载(lazy)实现原理
使用loadable 插件
(1)下载loader
react-loadable : npm i react-loadable
antd : npm i antd
(2)引入使用
import React from 'react'
import {Router, Route, Switch,Redirect} from 'react-router-dom'
import { createBrowserHistory } from 'history'
import {loadable} from '@/utils/loadable'; //懒加载模块
import Login from '@/pages/login';//普通加载模块
const Main = loadable({loader: () => import('@/pages/main')}); //引用
const NotFound404 = loadable({loader: () => import('@/components/NotFound/NotFound')}); //引用
const history = createBrowserHistory();
class RouteMap extends React.Component {
render () {
return (
<Router history={ history }>
<Switch >
<Route path="/" exact component={Login}/>
<Route path="/login" component={Login}/>
<Route path="/main" component={Main}/>
<Route path="/404" component={NotFound404}/>
<Redirect from="/*" to="/404"/>
</Switch>
</Router>
)
}
}
export default RouteMap
//_this.props.history.push('/main')//跳转可后退
//_this.props.history.replace('/main')//跳转不可后退
// <Redirect from="/*" to="/" /> //重定向
// <Route path="*" component={NotFound404}/>//默认
1.代码分割
(1)为什么要进行代码分割?
现在前端项目基本都采用打包技术,比如 Webpack,JS逻辑代码打包后会产生一个 bundle.js 文件,而随着我们引用的第三方库越来越多或业务逻辑代码越来越复杂,相应打包好的 bundle.js 文件体积就会越来越大,因为需要先请求加载资源之后,才会渲染页面,这就会严重影响到页面的首屏加载。
而为了解决这样的问题,避免大体积的代码包,我们则可以通过技术手段对代码包进行分割,能够创建多个包并在运行时动态地加载。现在像 Webpack、 Browserify等打包器都支持代码分割技术。
(2)什么时候应该考虑进行代码分割?
这里举一个平时开发中可能会遇到的场景,比如某个体积相对比较大的第三方库或插件(比如JS版的PDF预览库)只在单页应用(SPA)的某一个不是首页的页面使用了,这种情况就可以考虑代码分割,增加首屏的加载速度。
2.React的懒加载
示例代码:
import React, { Suspense } from 'react'; const OtherComponent = React.lazy(() => import('./OtherComponent')); function MyComponent() { return ( <div> <Suspense fallback={<div>Loading...</div>}> <OtherComponent /> </Suspense> </div> ); }
如上代码中,通过
import()
、React.lazy
和Suspense
共同一起实现了 React 的懒加载,也就是我们常说了运行时动态加载,即 OtherComponent 组件文件被拆分打包为一个新的包(bundle)文件,并且只会在 OtherComponent 组件渲染时,才会被下载到本地。那么上述中的代码拆分以及动态加载究竟是如何实现的呢?让我们来一起探究其原理是怎样的。
import() 原理
import() 函数是由TS39提出的一种动态加载模块的规范实现,其返回是一个 promise。在浏览器宿主环境中一个
import()
的参考实现如下:function import(url) { return new Promise((resolve, reject) => { const script = document.createElement("script"); const tempGlobal = "__tempModuleLoadingVariable" + Math.random().toString(32).substring(2); script.type = "module"; script.textContent = `import * as m from "${url}"; window.${tempGlobal} = m;`; script.onload = () => { resolve(window[tempGlobal]); delete window[tempGlobal]; script.remove(); }; script.onerror = () => { reject(new Error("Failed to load module script with URL " + url)); delete window[tempGlobal]; script.remove(); }; document.documentElement.appendChild(script); }); }
当 Webpack 解析到该
import()
语法时,会自动进行代码分割。React.lazy 原理
以下 React 源码基于 16.8.0 版本
React.lazy 的源码实现如下:
export function lazy<T, R>(ctor: () => Thenable<T, R>): LazyComponent<T> { let lazyType = { $$typeof: REACT_LAZY_TYPE, _ctor: ctor, // React uses these fields to store the result. _status: -1, _result: null, }; return lazyType; }
可以看到其返回了一个 LazyComponent 对象。
而对于 LazyComponent 对象的解析:
... case LazyComponent: { const elementType = workInProgress.elementType; return mountLazyComponent( current, workInProgress, elementType, updateExpirationTime, renderExpirationTime, ); } ... function mountLazyComponent( _current, workInProgress, elementType, updateExpirationTime, renderExpirationTime, ) { ... let Component = readLazyComponentType(elementType); ... } // Pending = 0, Resolved = 1, Rejected = 2 export function readLazyComponentType<T>(lazyComponent: LazyComponent<T>): T { const status = lazyComponent._status; const result = lazyComponent._result; switch (status) { case Resolved: { const Component: T = result; return Component; } case Rejected: { const error: mixed = result; throw error; } case Pending: { const thenable: Thenable<T, mixed> = result; throw thenable; } default: { // lazyComponent 首次被渲染 lazyComponent._status = Pending; const ctor = lazyComponent._ctor; const thenable = ctor(); thenable.then( moduleObject => { if (lazyComponent._status === Pending) { const defaultExport = moduleObject.default; lazyComponent._status = Resolved; lazyComponent._result = defaultExport; } }, error => { if (lazyComponent._status === Pending) { lazyComponent._status = Rejected; lazyComponent._result = error; } }, ); // Handle synchronous thenables. switch (lazyComponent._status) { case Resolved: return lazyComponent._result; case Rejected: throw lazyComponent._result; } lazyComponent._result = thenable; throw thenable; } } }
注:如果 readLazyComponentType 函数多次处理同一个 lazyComponent,则可能进入Pending、Rejected等 case 中。
从上述代码中可以看出,对于最初
React.lazy()
所返回的 LazyComponent 对象,其 _status 默认是 -1,所以首次渲染时,会进入 readLazyComponentType 函数中的 default 的逻辑,这里才会真正异步执行import(url)
操作,由于并未等待,随后会检查模块是否 Resolved,如果已经Resolved了(已经加载完毕)则直接返回moduleObject.default
(动态加载的模块的默认导出),否则将通过 throw 将 thenable 抛出到上层。为什么要 throw 它?这就要涉及到
Suspense
的工作原理,我们接着往下分析。Suspense 原理
由于 React 捕获异常并处理的代码逻辑比较多,这里就不贴源码,感兴趣可以去看 throwException 中的逻辑,其中就包含了如何处理捕获的异常。简单描述一下处理过程,React 捕获到异常之后,会判断异常是不是一个 thenable,如果是则会找到 SuspenseComponent ,如果 thenable 处于 pending 状态,则会将其 children 都渲染成 fallback 的值,一旦 thenable 被 resolve 则 SuspenseComponent 的子组件会重新渲染一次。
为了便于理解,我们也可以用 componentDidCatch 实现一个自己的 Suspense 组件,如下:
class Suspense extends React.Component { state = { promise: null } componentDidCatch(err) { // 判断 err 是否是 thenable if (err !== null && typeof err === 'object' && typeof err.then === 'function') { this.setState({ promise: err }, () => { err.then(() => { this.setState({ promise: null }) }) }) } } render() { const { fallback, children } = this.props const { promise } = this.state return <>{ promise ? fallback : children }</> } }
小结
至此,我们分析完了 React 的懒加载原理。简单来说,React利用
React.lazy
与import()
实现了渲染时的动态加载 ,并利用Suspense
来处理异步加载资源时页面应该如何显示的问题。