1.SPA理解
- 单页Web应用(single page web application,SPA)。即一个项目仅一个html页面,html上的内容可以数据发生变化。
- 整个应用只有一个完整的html页面,但是多组件。简记:单页面、多组件
- 点击页面中的链接不会刷新页面,只会做页面的局部更新。
- 数据都需要通过ajax请求获取, 并在前端异步展现。
2.路由理解
什么是路由?
(1)一个路由就是一个映射关系(key:value).。比如 浏览器地址栏 http://ip:port/home 那么 /home为key 对应的value为 Home组件或者后端服务器的某个方法;
(2)key为路径, value可能是function或component
路由分类
后端路由:
- 理解: value是function, 用来处理客户端提交的请求。
- 注册路由: router.get(path, function(req, res))
- 工作过程:当node接收到一个请求时, 根据请求路径找到匹配的路由, 调用路由中的函数来处理请求, 返回响应数据
前端路由:
- 浏览器端路由,value是component,用于展示页面内容。
- 注册路由: <Route path="/test" component={Test}>
- 工作过程:当浏览器的path变为/test时, 当前路由组件就会变为Test组件
3.前端路由的基石 history
javascript 有三部分构成,ECMAScript,DOM和BOM。ECMAScript描述了JS的语法和基本对象。DOM(文档对象模型)处理网页内容的方法和接口。BOM(浏览器对象模型)与浏览器交互的方法和接口。
在DOM中,HTML文档的层次结构被表示为一个树形结构。并用document对象表示该文档,树的每个子节点表示HTML文档中的不同内容。
BOM的核心是Window,而Window对象又具有双重角色,它既是通过js访问浏览器窗口的一个接口,又是一个Global(全局)对象。这意味着在网页中定义的任何对象,变量和函数,都以window作为其global对象。Window 对象代表一个浏览器窗口或一个框架。
History对象
window.history.length //浏览过的页面数
history.back() //在浏览历史里后退一步
history.forward() //在浏览历史里前进一步
(1) 每一个页面,history都会将其对应的浏览器地址存入栈数据结构。
(2)地址栏显示的始终是history栈顶的地址。可以通过push方法入栈地址,那么地址栏地址也会发生改变,并且入栈会被history的listen方法监听到
(3)a标签的 onclick返回false,那么就不会跳转到href的地址。
<a href="http://www.atguigu.com" onclick="return push('/test1') ">push test1</a><br><br> function push (path) { history.push(path) return false } function back() { history.goBack() } function forword() { history.goForward() } history.listen((location) => { console.log('请求路由路径变化了', location) })
4.react-router-dom
react-router-dom的理解
- react的一个插件库。
- 专门用来实现一个SPA应用。
- 基于react的项目基本都会用到此库。
- react-route分为三个 ,用来开发web的、native应用的和开发两者通用的。react-route-dom为用来开发web应用的库。
使用步骤
(1)安装web路由库 yarn add react-router-dom
(2)根据路由库组件 进行开发
(1)基本路由的使用案例
GitHub项目地址:https://github.com/MeBetterMan/react-study
路由使用
1)页面导航区编写路由链接
导航区使用<Link>标签 to点击以后,浏览地址栏会改变
import { Link, Route } from 'react-router-dom' {/* 原生html 靠a标签跳转不同页面 */} <a className="list-group-item" href="./about.html">About</a> <a className="list-group-item active" href="./home.html">Home</a> {/* 在react中通过路有链接实现切换组件--编写路由链接 */} <Link className="list-group-item" to="/about">About</Link> <Link className="list-group-item" to="/home">Home</Link>
2)页面展示区编写 注册路由
展示区使用<Route>标签注册路由;
path不区分大小写,大写也是小写来看待,因此注册路由时小写字母。
route路由,router路由器。
注册路由必须在render方法的return虚拟DOM,点击路由链接后会在注册路由的位置显示路由链接对应的组件。
import { Link, Route } from 'react-router-dom' {/* 注册路由 */} <Route path='/about' component={About} /> <Route path='/home' component={Home} />
Switch组件
上面的代码会遍历整个路由,寻找匹配的组件。
下面的代码,如果不使用Switch组件包裹,/home会匹配两个组件,两个组件都会显示。
遍历一遍路由,效率太低,而且没有必要,完全可以Home、Test组件组成一个组件。所以使用Switch组件包裹,Switch包裹的路由只会匹配第一个路由,找到一个以后不再继续遍历。
Switch可以提高路由匹配效率(单一匹配)
import { Link, Route,Switch } from 'react-router-dom' {/* 注册路由 */} <Switch> <Route path='/about' component={About} /> <Route path='/home' component={Home} /> <Route path='/home' component={Test} /> </Switch>
3)注册路由器
整个项目使用<BrowserRouter>标签
import { BrowserRouter } from 'react-router-dom' ReactDOM.render( <BrowserRouter><App /></BrowserRouter>, document.getElementById('root') );
注:
- 一个项目只能有BrowserRouter标签。所以放在index.js包裹整个项目。
- 如果一个项目多个BrowserRouter,那么相当于注册了多个路由器,路由器之间注册的路由不互通,会导致点击路由链接无法转移到路由的情况发生。
一般组件和路由组件
- 写法不同。
一般组件:<Home/> ,相当于自己写组件进行渲染;路由组件:<Route path="/demo" component={Demo}/>,由路由来选择渲染哪个组件,只要是组件被写到了路由注册中,那么路由便成了路由组件。
- 存放位置不同
为了更好区分两类组件,习惯上一般组件放src/components文件夹下,路由组件放 src/pages文件夹下。
- 接收到props不同。
最大区别:一般组件写组件标签时传递了什么,就能收到什么;路由组件接收到history、location、match三个固定的属性
- 一般组件通过withRouter变成路由组件
默认情况下必须经过路由匹配渲染的组件才存在this.props,才拥有路由参数,然而不是所有组件都直接与路由相连(通过路由跳转到此组件)的,当这些组件需要路由参数时,使用withRouter就可以给此组件传入路由参数,将react-router的history、location、match三个对象传入props对象上,此时就可以使用this.props。
history: go: ƒ go(n) goBack: ƒ goBack() goForward: ƒ goForward() push: ƒ push(path, state) replace: ƒ replace(path, state) location: pathname: "/about" search: "" state: undefined match: params: {} path: "/about" url: "/about"。
NavLink标签
Navlink标签可以在选中路由标签时,react自动追加activeClassName指定的类,从而让选中的标签显示高亮的效果。
import { NavLink, Route } from 'react-router-dom' <NavLink activeClassName='demo' className="list-group-item" to="/about">About</NavLink> <NavLink activeClassName='demo' className="list-group-item" to="/home">Home</NavLink> <style> .demo { background-color: blue !important; color: cornflowerblue !important; }
总结
- NavLink可以实现路由链接的高亮,通过activeClassName指定样式名
- 标签体内容是一个特殊的标签属性
- 可以通过this.props.children获取标签体内容
封装NavLink
上面使用NavLink,activeClassName、className都是一样的,冗余代码多。所以可以进行一次封装
1)封装NavLink
import React, { Component } from 'react' import { NavLink } from 'react-router-dom' export default class MyNavLink extends Component { render() { return ( <NavLink activeClassName='demo' className="list-group-item" {...this.props}/> ) } }
2)使用自定义NavLink。标签体内容会自动传给 MyNavLink
扩展下去就是:可以对任何组件进行包装,去除冗余重复代码。
<MyNavLink to='/about'>About</MyNavLink> <MyNavLink to='/home'>Home</MyNavLink>
注
- <MyNavLink to='/about'>About</MyNavLink> 标签体内容会被react自动封装为 props.children 属性传给MyNavLink组件.
<NavLink children='About'/> 等同于 <NavLink >About</NavLink >
- 如上便实现了冗余代码都写到自己封装的组件里面。不同的属性直接在MyNavLink传递过去即可。
解决多级路由路径刷新页面样式丢失的问题
问题
(1)多级路由地址 <MyNavLink to='/fri/about'>About</MyNavLink> <MyNavLink to='/fri/home'>Home</MyNavLink> (2)index.html 引入bootstrap.css样式 <link rel="stylesheet" type="text/css" src="./css/bootstrap.css" /> (3)点击路由后,刷新页面,此时样式bootstrap.css无法正常请求回来
解决办法
- public/index.html 中 引入样式时不写 ./ 写 / (常用)。./相当于相对路径,路由多级列表时,相对路径会相对最后一级路由前面的地址; /相当于相对于根目录
- public/index.html 中 引入样式时不写 ./ 写 %PUBLIC_URL% (常用)。仅react脚手架可以这么写
- 使用HashRouter。不常用
npm start启动后,http://ip:port/about http://ip:port/相当于访问服务器
- 服务器根目录为react的public文件夹。
- 如果访问一个不存在的资源,会默认返回index.html
使用单个包管理工具
npm yarn包管理器不要混着用 容易造成包丢失
路由的模糊、精确匹配
路由的模糊匹配
{/* 注册路由 */} <Route path='/about' component={About} /> <Route path='/home' component={Home} /> <Route path='/home/d/e' component={Home} /> {/* 在react中通过路有链接实现切换组件--编写路由链接 */} <NavLink to='/about'>About</NavLink> <NavLink to='/home/a/b'>Home</NavLink> //可以匹配到/home path的路由 <NavLink to='/home/'>Home</NavLink> //不可以匹配到/home/d/e path的路由
规则
to中的路由链接从头开始完全包括 path,且长度大于等于path,就算做匹配,这就是模糊匹配 。
比如to 为 /home/d/e 可以匹配到的path为 /home 、/home/d、 /home/d/e 。不可以匹配 /d/e 因为为从头开始匹配
路由的精确匹配
上面是模糊匹配,如果要开启精确匹配,需要注册路由时指定 exact={true},或者简写为exact。那么必须to path完全相同,才会匹配
{/* 注册路由 */} <Route exact path='/about' component={About} /> <Route exact path='/home' component={Home} />
注:除非使用模糊匹配页面显示出现问题,否则不用精确匹配。开启精确路由会导致无法继续匹配二级路由。
Redirect组件
.一般写在所有路由注册的最下方,当所有路由都无法匹配时,跳转到Redirect指定的路由。
注意:
- 如果父子路由都有Redirect默认路由,那么必须使用Switch,否则进入子路由后,刷新浏览器,会匹配到父路由的redicrect,导致路由地址出现没有匹配到的路由错误。
- 如果需要传递参数,记得Redirect也需要传递。尤其是编程式路由导航时不要忘记。否则,子组件获取参数时会报错。
<Switch> <Route path="/about" component={About}/> <Route path="/home" component={Home}/> <Redirect to="/about"/> </Switch>
(2)嵌套路由的使用
Home组件return返回的虚拟DOM中包括News、Message组件,那么News、Message组件就是Home组件的子组件。
那么子组件可以放在src/pages/Home文件夹下,也可以src/pages/Home_News文件夹下,体现层级即可。两种方式都可以。
嵌套路由基本写法
1.注册子路由时要写上父路由的path值。如果不写的话,点击就会进入一个新的页面,而不是在父路由页面的某部分显示。
2.路由的匹配是按照注册路由的顺序进行的。 根据代码执行顺序,先执行的先注册。
3.Home组件的虚拟DOM中使用Message组件,那么Message就是Home的子组件。
Home 父路由
<Route path='/home' component={Home} />
Home/News 子路由 News为Home组件的组成路由
<Route path='/home/message' component={Message}></Route> <Route path='/home/news' component={News}></Route> <Redirect to='/home/news'></Redirect>
(3)路由传递参数
params参数
1)路由链接时,携带参数。
语法: /变量值/变量值
路由链接使用字符串模板传递参数数据,可以${}接收变量。
模板字符串使用反引号。
地址栏的地址链接中显示 变量值
<Link to='/demo/test/tom/18'}>详情</Link> <Link to={`/home/message/${messageObj.id}/${messageObj.news}`}> {messageObj.news} </Link>
2)注册路由时,声明接收。
注册路由时 使用变量接收路由链接发过来的数据 数据和变量名挂在了this.props.match.params
语法: :变量名
{/* 声明变量接收params参数 :变量名。 此时就默认传给了组件的props属性 */} <Route path='/home/message/:id/:news' component={Detail}></Route>
3)子组件接收参数。
const { id, news } = this.props.match.params
search参数
1)路由链接(携带参数)
语法: ?属性名=值&属性名=值.
地址栏的地址链接中显示变量名=变量值
<Link to='/demo/test?name=tom&age=18'}>详情</Link> <Link to='/demo/test?name=${person.name}&age=${person.age}'}>详情</Link>
2)注册路由(无需声明,正常注册即可):<Route path="/demo/test" component={Test}/>
3)接收参数:this.props.location.search
备注:获取到的search是urlencoded编码字符串,需要借助安装react时自带的querystring库解析。即无需单独安装querystring库
import qs from 'query-string' const { id, news } = qs.parse(this.props.location.search.slice(1))
state参数
1)路由链接(携带参数)
{{}} to需要传递一个对象,第一层{}表示是一个js表达式 第二个{}表示是一个对象
地址栏中不显示变量、变量值,隐性传递。
react中return的虚拟dom中的对象,是通过render方法return之前定义的。比如下面的person对象。
<Link to={{pathname:'/demo/test',state:{name:'tom',age:18}}}>详情</Link> <Link to={{pathname:'/demo/test',state:{name:{person.name},age:{person.age}}}}>详情</Link>
2) 注册路由(无需声明,正常注册即可):<Route path="/demo/test" component={Test}/>
3)接收参数:this.props.location.state
备注:三种传参形式刷新浏览器也可以保留住参数
(4)路由的push和replace模式
1)默认push 如果replace需要编写路由链接时自动replace属性
<Link replace={true} to={'/home/message/detail'}>Home</Link> 可简写 <Link replace to={'/home/message/detail'}>Home</Link>
2)push会将每次路由地址压入history的栈中,然后通过浏览器的回退功能一个个显示出来;replace直接将新的路由覆盖栈顶元素,那么回退时就没有这个地址了。
(5)编程式路由导航
不通过点击实现路由跳转,直接通过编码实现路由跳转。
通过操作路由组件的history对象来实现
借助this.prosp.history对象上的API对操作路由跳转、前进、后退
history:
go: ƒ go(n) n表示前进后退几步
goBack: ƒ goBack()
goForward: ƒ goForward()
push: ƒ push(path, state) 新路由地址不断想栈中存入路由地址,回退时后进先出
replace: ƒ replace(path, state) 新路由地址直接覆盖栈顶路由地址 回退时会发现前面的路由地址不存在了push路由地址到栈顶 此时页面就会跳转到push进去的路由地址 页面显示的始终是history栈顶的页面 //params参数 this.props.history.push(`/home/message/detail/${id}/${news}`) //search参数 this.props.history.push(`/home/message/detail?id=${id}&news=${news}`) //state参数 this.props.history.push(`/home/message/detail`,{id,news})
(6)withRouter
默认情况下必须经过路由匹配渲染的组件才存在this.props,才拥有路由参数,执行this.props.history.push(’/detail’)跳转到对应路由的页面,然而不是所有组件都直接与路由相连(通过路由跳转到此组件)的,当这些组件需要路由参数时,使用withRouter就可以给此组件传入路由参数,将react-router的history、location、match三个对象传入props对象上,此时就可以使用this.props。
Header组件为一般组件, 通过withRouter 方法就可以成为路由组件,就具备了路由组件的history match location 属性。
react-router-dom 版本6.x已经没有这个函数,5.0.3版本有。
在render方法的return返回的虚拟DOM里面注册路由,然后push方法实现路由跳转。
import React, { Component } from 'react' import { withRouter } from 'react-router-dom' class Header extends Component { back = () => { this.props.history.goBack() } render() { return ( <div className="row"> <div className="col-xs-offset-2 col-xs-8"> <div className="page-header"><h2>React Router Demo</h2></div> </div> <button onClick={this.back}>后退</button> </div> ) } } export default withRouter(Header)
(7)HashRouter BrowserRouter区别
1.底层原理不一样:
BrowserRouter使用的是H5的history API,不兼容IE9及以下版本。常用这个。现在的react项目没必要支持旧版浏览器,因为react本身一些库也不支持旧版本。
HashRouter使用的是URL的哈希值。
2.path表现形式不一样
BrowserRouter的路径中没有#,例如:localhost:3000/demo/test
HashRouter的路径包含#,例如:localhost:3000/#/demo/test 类似于锚点,不会根据路由地址重新给服务器发送请求,直接本地前端资源查找。
3.刷新后对路由state参数的影响
BrowserRouter没有任何影响,因为state保存在history对象中。
HashRouter刷新后会导致路由state参数的丢失!!!
4.HashRouter可以用于解决一些路径错误相关的问题。 比如多级路由样式表丢失。
5.lazyLoad 懒加载
如果一个页面导航区有很多个导航项,打开此页面时会默认全部导航项组件都加载,但是此时有些导航项可能并不会点击查看,造成网络浪费。
使用步骤
1.引入lazy、Suspense
import React, { Component,lazy,Suspense} from 'react'
2.懒加载组件
路由组件直接懒加载 不能够再直接import;但是Loading组件只能直接引入,用来当懒加载的组件响应慢时显示的组件
import Loading from './Loading' const Home = lazy(()=> import('./Home') ) const About = lazy(()=> import('./About'))
3.注册路由时 使用Suspense包括
fallback指定当懒加载的组件因响应慢还未返回时,显示的组件。
<Suspense fallback={<Loading/>}> {/* 注册路由 */} <Route path="/about" component={About}/> <Route path="/home" component={Home}/> </Suspense>