路由的由来
路由一词其实最早是网络工程中的专业术语,代表通过互联网把信息从源地址传输到目的地址的一个活动,后来这个概念被web开发的人员所借鉴,所以才有了现在的前端开发中的路由。
什么是后端渲染
后端渲染也叫作服务器端渲染(Servier Side Render)。早期的web开发中,整个HTML页面都是由后端开发人员用java语言写的比如JSP的页面,直接在后端将HTML页面渲染完成,然后发送给客户端浏览器,浏览器解析HTML代码即可呈现一个页面,这种将HTML页面直接在后端渲染完成后拿到前端展示的渲染模式叫做后端渲染。
什么是后端路由
每一个页面都有一个对应的url网址,浏览器会将URL发送到服务器,服务器会基于正则表达式对该URL进行匹配,最后交给一个Controll进行处理,Controll处理完成之后返回一个HTML页面,返回给客户端浏览器。针对于每一个前端的URL后端都有一个对应的HTML页面返回,这种由后端来管理和维护的 URL地址-HTML页面 的路由映射关系,叫做后端路由。
模式优点
后端渲染好的html页面浏览器可以直接进行解析渲染呈现出来,不再需要去加载css和js等静态资源,有利于SEO的优化并且没有js和css加载的阻塞,也有利于减少首屏(FP)呈现的时间。
模式缺点
缺点:负责结构的HTML页面和逻辑会混合在一起,不符合W3C提倡的结构行为样式相分离;而且也不利于项目的维护,因为一个前端还得会后端语言才可以进行维护.
什么是前端渲染
客户端浏览器在输入url请求页面的过程中,该页面所对应的HTML+css+js+JQuery等静态资源从静态资源服务器进行获取,浏览器拿到这些资源先进行渲染,此时前端页面的基本框架已经呈现,但是一些动态展示的列表等数据,需要基于前面请求的js代码中的ajax请求,从对应的专门提供API接口(restful规范)的API服务器获取到数据,然后前端开发人员再通过js代码将这部分数据插入到当前的HTML页面中,这种前端负责渲染HTML页面,后端只负责设计API接口和提供API文档的阶段就是前后端分离阶段,并且这种交由js来渲染页面的技术叫做前端渲染。
模式优点
将前后端进行了一个清晰的分离,前端负责页面和交互;后端负责api接口和数据处理
提供API接口的服务器不仅仅可以给web端页面提供数据,给移动端等多端都可以提供数据
相比于后端路由阶段,这个阶段的路由还是后端管理,但是将之前的页面渲染这个活交给了前端,后端只负责数据处理。
单页面富应用中的前端渲染
单页面富应用的意思是single-page-application,简称SPA。意思就是整个web应用只有一个HTML页面,当我们在页面上触发某个操作改变页面地址栏中的URL的时候,通过javascript来监听浏览器地址栏中URL的改变,并根据改变后的URL渲染对应的页面(组件),很明显这种通过js来渲染页面或组件的方式也属于前端渲染。
单页面富应用中的前端路由
这种由前端的javascript代码来管理和维护的URL地址和页面(组件)一一对应的映射关系,就是前端路由。前端路由可以根据url的不同,最终让框架(Vue/React)来渲染不同的组件,最终我们在页面上看到的实际就是渲染的一个个组件页面。
要实现一个前端路由,必须要满足以下两个条件:
第一:当浏览器地址栏中的URL发生变化的时候,不能引起页面的刷新,也就是不会向后端服务器发送请求。目前前端开发来说,实现这一个要求有两种方案,一种是URL的hash模式;另外一种是HTML5新增的history API。
第二:需要在前端监听URL地址栏中的变化,并且前端可以通过判断当前的URL来决定页面要渲染哪些元素(组件)
只要能实现以上两个条件,那么就是一个前端路由的简单实现,其实诸如Vue\React等框架的router的底层原理也是基于以上来实现的。
原理:浏览器的URL的hash其实就是锚点(#),它的改变不会引起浏览器页面的刷新。首先实现前端路由的第一个条件满足;同时可以基于window.onhashchange来监听URL中hash值的变化,并在事件回调处理函数中根据location.hash获取到当前地址栏的hash值,然后判断渲染那个组件或者页面。
注意:改变URL的hash值的方法有两个,一个是直接在a标签的href中进行赋值;一种是通过直接给window.location.hash赋值来修改url中的hash值,不管是那种方法,本质都是改变的window.location的href属性。
优点:兼容性较好,低版本浏览器都可以使用
缺点:地址栏中有一个#号,不美观
简单实现一个基于修改URL的hash来完成前端路由的demo
<div id="app"> <a href="#/home">首页</a> <a href="#/about">关于</a> </div> <div class="router-view"></div> <script type="text/javascript"> let routerView = document.querySelector('.router-view'); let aBtns = document.querySelectorAll('a'); /* 监听地址栏中URL的hash值发生改变 */ window.onhashchange = function(e){ console.log(window.location.hash); switch(window.location.hash){ case '#/home': routerView.innerHTML = "首页页面的内容" break; case '#/about': routerView.innerHTML = "关于页面的内容" break; default: routerView.innerHTML = "" break; } } </script>
pushStata(state,title,newUrl)
比如当前url的基础路径为:http://localhost:3000/alibaba/home
如果这里传递的newUrl为'list',那么是基于当前所在目录的路径进行跳转的,跳转之后的新路径为:
默认浏览器地址栏:"http://localhost:3000/alibaba/home" history.pushState({},"",'list') 新的浏览器地址栏:"http://localhost:3000/alibaba/list" 也就是只传递一个字符串,等于访问的是当前目录alibaba下的list文件
如果这里传递的newUrl为'/list',那么是基于根路径进行跳转的,跳转之后的新路径为:
默认浏览器地址栏:"http://localhost:3000/alibaba/home" history.pushState({},"",'/list') 新的浏览器地址栏:"http://localhost:3000/list" 也就是传递一个前面加/的字符串,等于访问的是根目录下的list文件
注意: pushState() 绝对不会触发 hashchange 事件,即使新的URL与旧的URL仅哈希不同也是如此。
注意:调用history.pushState()或者history.replaceState()不会触发popstate事件. popstate事件只会在浏览器某些行为下触发, 比如点击后退、前进按钮(或者在JavaScript中调用history.back()、history.forward()、history.go()方法),此外,a 标签的锚点也会触发该事件.
6个可以触发该事件执行的操作
条件一:url变化但是不刷新页面
当点击a标签的时候url会发生变化,但是阻止a标签的默认行为,通过监听a标签的点击事件,通过HTML5新增的5history的pushState或者replaceState方法实现URL路径的变化但不刷新页面。除此之外,history的那5个api都具有改变url且不刷新页面的情况。
基于getAttribute方法获取到当前被点击的a标签的href属性,来手动执行一遍自定义的urlChange方法,其实也就是一个路由映射表。
条件二:url变化可以监听并且基于不同的url渲染不同的页面
基于window.onpopstate来进行监听页面的前进以及后退操作,监听到之后基于location.pathname获取到地址栏中路径,然后手动执行一遍自定义的urlChange方法。
优点:没有#号,比较符合常规的URL地址的写法
缺点:
HTML5新特性,兼容性不够好;
并且如果后端没有进行配置的话,点击浏览器的刷新按钮会导致404 not found
<body> <div id="app"> <a href="/home">首页</a> <a href="/about">关于</a> </div> <div class="router-view"></div> <script type="text/javascript"> let routerView = document.querySelector('.router-view'); let aBtns = document.querySelectorAll('a'); /* 监听a标签的点击 阻止默认事件 */ for(let i=0;i<aBtns.length;i++){ aBtns[i].onclick = (e)=>{ console.log('a标签发生点击'); e.preventDefault(); const href = aBtns[i].getAttribute('href'); history.pushState({},"",href) /* 手动调用一次urlChange */ urlChange(); } } /* window.onpopstate监听浏览器的前进后退按钮点击 以及history.go()API的调用情况 */ window.addEventListener('popstate',urlChange); /* 自定义urlChange函数 */ function urlChange(){ console.log('路径发生了改变'); switch(window.location.pathname){ case '/home': routerView.innerHTML = "首页页面的内容" break; case '/about': routerView.innerHTML = "关于页面的内容" break; default: routerView.innerHTML = "" break; } } </script> </body>
从react-router@4.0开始,路由不再集中在一个包里面进行管理了,由以下几个包分包管理:
react-router是router的核心部分代码
react-router-dom是用于浏览器的
react-router-native是用于原生native应用的
我们在使用的时候只需要安装一个react-router-dom包就可以了,因为react-router-dom这个库依赖react-router,所以会自动将react-router等它依赖的库都进行安装。
react-router主要提供了以下组件类型的API,通过这些API可以构建一个前端路由。
路由模式API
react-router中包含了对路径改变的监听,并且会将相应的路径传递给子组件
BrowserRouter:代表底层采用HTML5的history API来实现前端路由
HashRouter:代表底层采用hash模式来实现前端路由
导航元素API
都是基于to属性指向的路径来实现路径的跳转
Link:用来实现路由跳转的点击按钮,最终会被渲染为一个a标签
NavLink:在Link的基础上做了一些属性的增强
展示渲染API
Route组件主要用于路径的匹配,并根据匹配到的结果在该组件内部渲染出组件内容,等于是一个组件渲染的盒子。
path属性:设置匹配到的路径
component属性:用于设置路径匹配到之后用于渲染的组件
exact属性:精确匹配路径,只有路径完全一致才会渲染对应组件,模糊匹配不渲染
import React,{PureComponent} from 'react'; import Home from './react-router/home.js' import About from './react-router/about.js' import Profile from './react-router/profile.js' import { BrowserRouter, HashRouter, Link, NavLink, Route }from "react-router-dom" class App extends PureComponent{ render(){ return( <div> <BrowserRouter> <Link to="/">首页</Link> <Link to="/about">关于</Link> <Link to="/profile">我的</Link> <Route path="/" component={Home} exact></Route> <Route path="/about" component={About}></Route> <Route path="/profile" component={Profile}></Route> </BrowserRouter> </div> ) } } export default App;
NavLink的exact代表浏览器的url path在匹配该组件的to属性值的时候,要采用精确匹配,以便于给这些组件渲染出来的a元素动态添加active样式类名
component属性:当路由匹配成功之后要渲染的组件
activeClassName属性:当前路由处于激活状态下的时候,为了防止类名冲突,自定义添加的class类名
注意:一般情况下,只要当前路径匹配成功之后,就会自动给当前的Link或者NavLink渲染出来的a标签添加上一个名为active的class类名,我们可以在全局公共的App.css样式文件中定义路由激活时的样式,如果担心active类名和其他地方用到的冲突,那么需要使用NavLink组件提供的activeClassName来自定义一个class类名解决。
<NavLink exact to="/" activeClassName="home-active">首页</NavLink> <NavLink to="/about" activeClassName="about-active">关于</NavLink> <NavLink to="/profile" activeClassName="profile-active">我的</NavLink>
<NavLink exact to="/" activeStyle={{color:"pink",fontSize:"18px"}}>首页</NavLink> <NavLink to="/about" activeStyle={{color:"pink",fontSize:"18px"}}>关于</NavLink> <NavLink to="/profile" activeStyle={{color:"pink",fontSize:"18px"}}>我的</NavLink> > 所对应的App.css文件中书写路由激活时按钮的样式 a.active{ font-size: 20px; color: brown; margin: 0 10px; } /* 自定义的类名 */ a.home-active{ font-size: 20px; color: blue; margin: 0 10px; }
除了Route组件定义一个path值固定的路由之外,日常开发中还会有两种路由:
实际开发中,我们希望的是只要有一个路由Route匹配成功,就中止后续的匹配,没必要将后续有可能匹配到的动态路由或者NoMatch组件再渲染出来,类似一种排他的思想。要实现这个效果,就需要用到Switch组件,将所有Route组件包裹在内,只要有一个匹配成功就中止后续匹配。
<BrowserRouter> <NavLink exact to="/">首页</NavLink> <NavLink to="/about" >关于</NavLink> <NavLink to="/profile">我的</NavLink> <NavLink to="/user">用户信息</NavLink> <Switch> <Route exact path="/" component={Home} ></Route> <Route path="/about" component={About}></Route> <Route path="/profile" component={Profile}></Route> <Route path="/:id" component={User}></Route> <Route component={NoMatch}></Route> </Switch> </BrowserRouter>
Redirect组件用于路由的重定向,一旦再某个组件渲染的时候渲染到了Redirect组件那么会直接重定向到该组件的to属性指向的地址,然后基于地址来渲染对应的组件。
利用这一特性可以做一个建议的判断用户进入某个页面是否已经登录:
如果登录,那么展示该组件的页面;
如果没有登录,那么直接重定向到登录页面
link和navlink必须和用户交互才可以,redirect只要有就会直接跳转
一旦出现redirect组件就马上进行路由的重定向,立马去Route中找和自己to属性匹配的path,渲染对应的组件
自动跳转的时候使用
class User extends PureComponent{ constructor(props) { super(props); this.state = { isLogin:false } } render(){ return this.state.isLogin?( <div> <h2>这是User组件的内容,这里展示用户信息</h2> <h3>用户名:直接起飞⑧</h3> <h3>密码:66668888</h3> </div> ):<Redirect to="/login" /> } }
子路由是写在父组件指向的那个组件中的,是在父路由对应的组件的容器中渲染的
子路由的NavLink也可以使用activeClassName来自定义路由激活时按钮的类名
import React,{PureComponent} from 'react'; import { BrowserRouter, HashRouter, Link, NavLink, Route, Switch }from "react-router-dom" function AboutUs(props){ return <div>关于我们那可说的多了</div> } function AboutHistory(props){ return <div>公司的历史就不用我多说了吧</div> } function AboutCompany(props){ return <div>我们公司就是一个字:钱少事多</div> } class About extends PureComponent{ render(){ return( <div> <h3>这是About组件的内容</h3> <h5> <NavLink exact to="/about" activeClassName="about-active">关于我们</NavLink> <NavLink to="/about/history" activeClassName="about-active">公司历史</NavLink> <NavLink to="/about/company" activeClassName="about-active">公司介绍</NavLink> <Route exact path="/about" component={AboutUs}></Route> <Route path="/about/history" component={AboutHistory}></Route> <Route path="/about/company" component={AboutCompany}></Route> </h5> </div> ) } } export default About;
react-router为我们实现路由跳转提供了两种途径:
如果一个组件本身就是基于Route直接跳转过来的,那么react-router会对这个组件做一个属性增强,在这个组件的props属性中可以直接获取history、loctaion以及match对象。
history对象
go goBack goForward push replace五个historyApi的封装
{ action: "PUSH" block: ƒ block(prompt) createHref: ƒ createHref(location) go: ƒ go(n) goBack: ƒ goBack() goForward: ƒ goForward() length: 36 listen: ƒ listen(listener) location: {pathname: "/goods", search: "", hash: "", state: undefined, key: "ewkjlo"} push: ƒ push(path, state) replace: ƒ replace(path, state) }
location对象
{ hash: "" key: "sorfsk" pathname: "/profile" search: "" state: null }
match对象
{ isExact: true params: {} path: "/profile" url: "/profile" }
如果是一个普通的经过JSX语法渲染的组件,那么该组件的props属性是一定没有history这些对象的,这时候我们就需要想办法给该组件进行一个属性增强。
react-router已经为我们提供了进行属性增强的办法,那就是使用withRouter高阶组件。一般需要两步来完成:
第一步:假设Home组件想进行增强,那么需要导出一个withRouter高阶组件增强后的Home组件
import {withRouter}from "react-router-dom" class Home extends PureComponent{ navToIndex(){ console.log(this.props.history) this.props.history.push('/index'); } render(){ return( <div> <P>Home组件</P> <button onClick={e=>this.navToIndex()}>跳转至首页</button> </div> ) } } export default withRouter(Home);
第二步:Home组件在App组件中使用的时候,必须使用BroswerRouter或者HashRouter组件进行包裹才会生效
import {BrowserRouter}from "react-router-dom" class App extends PureComponent{ render(){ return( <div> <Broswer> <Home></Home> </Broswer> </div> ) } } export default App;
一般来说,在React中当路由跳转的时候传递参数有三种方式:
一般动态路由指的是路由中的路径不会固定,而是根据某个参数在一直变化,并且基于该路由渲染出来的组件也是基于这个参数的变化,该组件内部的内容也跟着变化,它指的是路径是变化的。
其次比如/detail的path对应一个Detail组件,该组件专门展示商品的详情。但是如果将匹配组件Route的path写成/detail/id,那么不管这个id传递什么值,加载的都是Detail组件,这种匹配的规则也可以称为动态路由。
在react-router搭建的路由中,通过组件的props的match属性中的params属性获取动态路由跳转时携带的参数.
NavLink组件定义动态路由
const goodsId = 10086; <NavLink to={`/detail/${goodsId}`}></NavLink>
Route组件匹配动态路由规则
<Route path="/detail/:id" component={Detail}></Route>
Detail组件中获取动态路由this.props.match.params
match对象中的path属性和url属性在绝大部分情况下都是相同的,只有在动态路由的情况下才不一样
path: "/detail/:id" 获取的是在配置路由表的时候的动态路由配置
url: "/detail/10086" 获取的是实际跳转之后浏览器地址栏中的path
import React,{PureComponent} from 'react'; class Detail extends PureComponent{ render(){ console.log(this.props.match); let id = this.props.match.params.id; return( <div> 这是ID为{`${id}`}的商品详情 </div> ) } } export default Detail;
缺点是传递的时候参数比较多的对象会在地址栏中需要拼接很长的字符串
然后拿的时候拿到了还需要自己写方法然后解析,所以现在react已经不推荐使用这种方法传递参数了
传递的时候类似query查询参数传递
<NavLink to={`/detail1?name=lilei&age=18`}>商品详情1</NavLink> <Route path="/detail1" component={Detail1}></Route>
跳转过去以后基于location.search拿到参数然后自己写方法解析
import React,{PureComponent} from 'react'; class Detail extends PureComponent{ /* 封装解析查询参数为对象的方法 */ parseInfo(str){ let arr = str.slice(1).split('&'); let obj = {}; arr.forEach(item=>{ let tempArr = item.split('='); obj[tempArr[0]] = tempArr[1]; }) return obj; } render(){ /* 获取路由传递的查询参数 */ let userInfo = this.props.location.search; /* 获取解析后的对象 */ const parseInfoObj = this.parseInfo(userInfo); return( <div> 用户姓名为:{parseInfoObj.name} 用户年龄为:{parseInfoObj.age} </div> ) } } export default Detail;
Link或者NavLink组件都有一个to属性可以用来指向当点击按钮之后要跳转的路由,这个to属性除了可以接受一个字符串类型的路径比如'/detail'之外,还可以接受一个对象作为其属性值,这个对象有如下属性参数:
传递的时候基于to属性的state参数接收复杂对象参数
<NavLink to={{ pathname:'/detail2', search:'?job=coder&car=benz', hash:'', state:{ name:'lilei', age:18, study:'good' }}}> 商品详情2 </NavLink> <Route path="/detail2" component={Detail2}></Route>
接收的时候基于location.state拿到参数
import React,{PureComponent} from 'react'; class Detail extends PureComponent{ render(){ console.log(this.props.location.state); const {name,age,study} = this.props.location.state; return( <div> 这是用户姓名{name}; 这是用户年龄{age}; 这是用户学习{study}; </div> ) } } export default Detail;
之前基于react-route提供的Switch组件和Route组件,我们保证了路由和组件的一一映射关系。但是这种配置路由有一个缺点,那就是配置比较混乱。既不利于维护也不利于二次开发,所以我们需要一个类似vue的那种集中管理项目路由配置表的方式来管理这些路由和组件的映射关系,再react中一般采用第三方库react-router-config来实现。
npm i react-router-config --save
实现原理:将定义再routes数组中的一个个对象,按照Switch的渲染模式,依次渲染为一个个的Route组件。路由对应的组件渲染不再通过Route组件来进行占位,而是直接基于一个函数renderRoutes(routes),传入要渲染的路由表即可实现和原生react渲染路由对应组件一模一样的效果。
本质就是将原来写在Route组件中的属性换个集中的地方写,一般写在router文件夹下的index.js中
二级子路由是通过一级路由的routes属性来进行配置的
然后调用{renderRoutes(routes)}方法实现路由对应组件的渲染
import Home from '../react-router/home.js'; import About,{AboutUs,AboutHistory,AboutCompany}from '../react-router/about.js'; import Profile from '../react-router/profile.js'; import Detail1 from '../react-router/detail1.js'; const routes = [ { path:'/', component:Home, exact:true }, { path:'/home', component:Home, }, { path:'/about', component:About, routes:[ { path:'/about', component:AboutUs, exact:true }, { path:'/about/history', component:AboutHistory, }, { path:'/about/company', component:AboutCompany, } ] }, { path:'/profile', component:Profile, }, { path:'/detail1', component:Detail1, }, ] export default routes;
该方法的参数主要是routes,也就是用于渲染的路由配置数组,里面是一个个的路由配置对象。
该方法是 react-router-config这个库导出的,它的主要作用是渲染路由对应的组件,起到和Route组件一样的占位
在渲染子路由的时候,只有父路由是通过renderRoutes方法渲染出来的,那么这个父路由对应的组件身上才会有route属性,这个props.routes属性的值就是这个父路由配置在表里面的子路由routes数组。
如果父路由是基于Switch组件渲染出来的,那么这个父路由对应的组件是没有这个获取子路由routes数组的属性的。
function renderRoutes(routes, extraProps, switchProps) { if (extraProps === void 0) { extraProps = {}; } if (switchProps === void 0) { switchProps = {}; } /* 如果传递了routes,那么返回一个Switch组件,它的子组件是routes数组中的每一个对象,然后子组件是一个个的Route组件,这也就说明renderRoutes函数的本质还是把路由配置表转化为Switch包裹的Route组件 */ return routes ? React.createElement(reactRouter.Switch, switchProps, routes.map(function (route, i) { return React.createElement(reactRouter.Route, { key: route.key || i, path: route.path, exact: route.exact, strict: route.strict, render: function render(props) { return route.render ? route.render(_extends({}, props, {}, extraProps, { route: route })) : React.createElement(route.component, _extends({}, props, extraProps, { route: route })); } }); })) : null; }
这个方法是一个类工具函数,接收一个需要查询的路由配置数组和一个路由的path,查询当前路由配置数组中的哪些配置是符合这个path的。
function matchRoutes(routes, pathname,branch) { if (branch === void 0) { branch = []; } routes.some(function (route) { var match = route.path ? reactRouter.matchPath(pathname, route) : branch.length ? branch[branch.length - 1].match // use parent match : reactRouter.Router.computeRootMatch(pathname); // use default "root" match if (match) { branch.push({ route: route, match: match }); if (route.routes) { matchRoutes(route.routes, pathname, branch); } } return match; }); return branch; }
可以学习手写Object.assign(target,...sources)
function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
手写的Object.assign(target,...sources)方法
(function(){ function myAssign(target,...sources){ for(let i=0;i<sources.length;i++){ let source = sources[i]; for(let key in source){ if(source.hasOwnProperty(key)){ target[key] = source[key]; } } } return target; } })()