路由的发展阶段
后端路由阶段
- 早期的网站开发整个 HTML 页面是由服务器来渲染的,服务器直接生产渲染好对应的 HTML 页面,返回给客户端进行展示
- 但是,一个网站,有很多页面,服务器是如何处理的呢?
- 一个页面有自己对应的网址,也就是 URL
- URL 会发送到服务器,服务器会通过正则对该 URL 进行匹配,并且最后交给一个 Controller 进行处理
- Controller 进行各种处理,最终生成 HTML 或者数据,返回给前端
- 这就完成了一个 IO 操作
- 上面的这种操作,就是后端路由
- 当我们页面中需要请求不同的路径内容时,交给服务器来进行处理,服务器渲染好整个页面,并且将页面返回给客户端
- 这种情况下渲染好的页面,不需要单独加载任何的 js 和 css,可以直接交给浏览器展示,这样也有利于 SEO 的优化
- 后端路由的缺点
- 一种情况是整个页面的模块由后端人员来编写和维护的
- 另一种情况是前端开发人员如果要开发页面,需要通过 PHP 和 Java 等语言来编写页面代码
- 而且通常情况下 HTML 代码和数据以及对应的逻辑会混在一起,编写和维护都是非常糟糕的事情
前后端分离阶段
- 前端渲染:
- 每次请求设计到的静态资源都会从静态资源服务器获取
- 这些资源包括 HTML+CSS +JS, 然后在前端对这些请求回来的资源进行渲染
- 需要注意的是,客户端的每一次请求,都会从静态资源服务器请求文件
- 同时可以看到,和之前的后端路由不同,这时后端只是负责提供 API 了
- 前后端分离阶段:
- 随着 Ajax的出现,有了前后端分离的开发模式
- 后端只提供 API 来返回数据,前端通过 Ajax 获取数据,并且可以通过 JavaScript 将数据渲染到页面中
- 这样做最大的优点就是前后端责任的清晰,祸端专注于数据上,前端专注于交互和可视化上
- 并且当移动端出现后,后端不需要进行任何处理,依然使用之前的一套API 即可
- 目前很多网站依然采用这种模式开发(jQuery 开发模式)
单页面富应用(SPA)阶段
- 单页面富应用的理解:
- 单页面富应用的英文是 single-page application, 简称 SPA
- 整个Web应用实际上只有一个页面,当 URL 发生改变时,并不会从服务器请求新的静态资源
- 而是通过 JavaScript 监听 URL 的改变,并且根据 URL 的不同去渲染新的页面
- 如何可以应用 URL 和渲染的页面呢? 使用前端路由
- 前端路由维护着 URL 和渲染页面的映射关系
- 路由可以根据不同的 URL,最终让我们的框架(比如 Vue、React、Angular)去渲染不同的组件
- 最终我们在页面上看到的实际就是渲染的一个个组件页面
前端路由的原理
- 前端路由是如何做到 URL 和内容进行映射的呢? 通过监听 URL的改变
- URL 发生变化,同时不引起页面和的刷新有两个办法:
- 通过 URL 的 hash 改变 URL
- 通过 HTML5 中的 history 模式修改 URL;
- 当监听到 URL 发生变化时,我们可以通过自己判断当前的 URL,决定到底渲染什么样的内容
URL 的 hash
- URL 的 hash 也就是锚点(#),本质是改变 window.location 的 href 属性;
- 我们可以通过直接赋值 location.hash 来改变 href.但是页面不发生刷新;
- hash 的优势是兼容性好,在老版的 IE 中都可以运行
- 缺点是有一个#,显得不像真是的路径
<body>
<div id="app">
<a href="#/home">首页</a>
<a href="#/about">关于</a>
<div class="router-view"> 初始内容 </div>
</div>
<script>
// 获取router-view的DOM
const routerViewEl = document.getElementsByClassName("router-view")[0];
// 监听URL的改变. location 是一个全局变量
window.addEventListener("hashchange", () => {
switch (location.hash) {
case "#/home":
routerViewEl.innerHTML = "改成首页";
break;
case "#/about":
routerViewEl.innerHTML = "改成关于";
break;
default:
routerViewEl.innerHTML = "";
}
})
</script>
</body>
默认我们的 url 是http://127.0.0.1:5500/监听hash的该改变.html
; 当我们点击首页的时候,就会改成http://127.0.0.1:5500/监听hash的该改变.html#/home
, 同时router-view div 中的内容也会改变;
HTML5 的 history
- history 接口是 HTML5 新增的,它有六种模式改变 URL 而不刷新页面
- replaceState:替换原来的路径;
- pushState:使用新的路径;
- popState:路径的回退;
- go:向前或向后改变路径;
- forward:向前改变路径;
- back:向后改变路径;
<body>
<div id="app">
<a href="/home">首页</a>
<a href="/about">关于</a>
<div class="router-view"></div>
</div>
<script>
// 1.获取router-view的DOM
const routerViewEl = document.getElementsByClassName("router-view")[0];
// 获取所有的a元素, 自己来监听a元素的改变
const aEls = document.getElementsByTagName("a");
for (let el of aEls) {
el.addEventListener("click", e => {
e.preventDefault();
const href = el.getAttribute("href");
history.pushState({}, "", href);
urlChange();
})
}
// 执行返回操作时, 依然来到urlChange
window.addEventListener('popstate',urlChange);
// 监听URL的改变
function urlChange() {
switch (location.pathname) { // 这里的 pathname 就是点击的 a 标签的 href 值
case "/home":
routerViewEl.innerHTML = "改成首页";
break;
case "/about":
routerViewEl.innerHTML = "改成关于";
break;
default:
routerViewEl.innerHTML = "";
}
}
</script>
</body>
React-router
- 目前前端流行的三大框架, 都有自己的路由实现:
- Angular的ngRouter
- React的react-router
- Vue的vue-router
- React Router的版本4开始,路由不再集中在一个包中进行管理了:
- react-router是router的核心部分代码
- react-router-dom是用于浏览器的;
- react-router-native是用于原生应用的;
- 目前我们使用最新的React Router版本是v5的版本:
- 实际上v4的版本和v5的版本差异并不大;
- 安装react-router:
- 安装react-router-dom会自动帮助我们安装react-router的依赖;
yarn add react-router-dom
基本使用
BrowserRouter、HashRouter
- Router 中包含了对路径改变的监听,并且会将对应的路径传递给子组件
- BrowserRouter使用 history 模式
- HashRouter使用 hash 模式
Link 和 NavLink
- 通常路径的跳转是使用 Link 组件,最终会被渲染成 a 元素
- to属性: Link 中最重要的属性,用于设置跳转到的路径
- NavLink 是在 Link 基础之上增加了一些样式属性,可以设置我们选中元素的样式
- activeStyle:活跃(匹配)时的样式;
<NavLink exact to="/" activeStyle={{color: "red", fontSize: "30px"}}>首页</NavLink>
- activeClassName: 活跃时添加的 class, 默认情况下NavLink匹配成功时会动态添加一个名为 active 的 class, 所以我们也可以直接编写样式, 如果担心这个 class 在其他地方也使用了,出现样式的层叠,就可以使用 activeClassName 自己添加样式
- exact: 精准匹配
a.active { // 设置活跃时的样式
color: red;
font-size: 30px;
}
a.link-active {
color: yellow;
font-size: 40px;
}
<NavLink exact to="/">首页</NavLink>
<NavLink to="/about" activeClassName="link-active">关于</NavLink>
Route
- Route 用于匹配路径
- path属性: 用于设置匹配到的路径
- component 属性: 设置匹配到路径后,渲染的组件
- exact: 精准匹配,只有精准匹配到完全一致的路径,才会渲染对应的组件, 比如: Home 组件的 path 是'/', About 组件的 path 是'/about', 那么在匹配'/'的时候如果不是精准匹配会将 '/' 和'/about'都匹配出来,然后将两个组件都渲染到界面上
<HashRouter>
<Link to="/">首页</Link>
<Link to="/about">关于</Link>
<Link to="/profile">我的</Link>
<Route exact path="/" component={Home}/>
<Route exact path="/about" component={About}/>
<Route exact path="/profile" component={Profile}/>
</HashRouter>
- 所有的Route 都只是占位,只有在匹配到的时候才在所占的位置显示, 如下,当匹配到'/', '/about','/profile'任一 path 时,会将对应的组件显示在
<div> 哈哈哈 </div>
和<div> 呵呵呵 </div>
之间
<HashRouter>
<Link to="/">首页</Link>
<Link to="/about">关于</Link>
<Link to="/profile">我的</Link>
<div> 哈哈哈 </div>
<Route exact path="/" component={Home}/>
<Route exact path="/about" component={About}/>
<Route exact path="/profile" component={Profile}/>
<div> 呵呵呵 </div>
</HashRouter>
Switch
- 默认情况下路由会将匹配到的所有组件都会被渲染出来,比如下面,我们想匹配/about 路径,但是结果会将/:userid 和 NoMatch组件也匹配到,这是我们不希望的,我们希望再传入 userid 的时候显示 User 组件,在所有路由都没有匹配到的时候,显示 NoMatch 组件;这时候就可以使用 Switch帮我们解决这个问题了
- Switch 包裹的路由,只要匹配到了第一个,那么后面的就不会再继续匹配了
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/:id" component={User} />
<Route component={NoMatch} />
</Switch>
Redirect
- Redirect 用于路由的重定向,当这个组件出现时,就会执行跳转到对应的to 路径中, 比如我们在用户信息页中根据是否登录来判断显示内容,如果登录显示用户信息,如果没登录显示登录, 当用户未登录时,下面就会重定向到路径为/login 匹配的组件上
render() {
return this.state.isLogin ? (
<div>
<h2>User</h2>
<h2>用户名: ****</h2>
</div>
): <Redirect to="/login" />
}
路由的嵌套
- 在开发中,路由之间是存在嵌套关系的,假设 about 页面中有两个子页面,点击 tab 进行页面的切换, 那就需要在 about 中进行子路由的设置,设置子路由的 path 时需要带上父路由的 path,即 path 要写全
class App extends PureComponent {
render() {
return (
<div>
<HashRouter>
<Link to="/">首页</Link>
<Link to="/about">关于</Link>
<Link to="/profile">我的</Link>
<Route exact path="/" component={Home}/>
<Route exact path="/about" component={About}/>
<Route exact path="/profile" component={Profile}/>
</HashRouter>
<div>
)
}
}
export default class About extends PureComponent {
render() {
return (
<div>
{/* 这里默认展示企业历史*/}
<NavLink exact to="/about" >企业历史</NavLink>
{/*子路由的路径需要写全*/}
<NavLink exact to="/about/culture" >企业文化</NavLink>
<NavLink exact to="/about/contact" >联系我们</NavLink>
<Switch>
{/* 如果不加 exact 进行精准匹配,那么所有子路由的都能匹配到/about, 这样的话就只能展示AboutHisotry组件了*/}
<Route exact path="/about" component={AboutHisotry}/>
<Route path="/about/culture" component={AboutCulture}/>
<Route path="/about/contact" component={AboutContact}/>
</Switch>
</div>
)
}
}
手动路由跳转
- 路由中我们不止可以通过Link 或 NavLink 进行跳转,也可以通过 JS 代码进行跳转, 通过 JS 代码跳转的前提是必须拿到 history 对象。
- 获取 history 对象有两种方式
- 如果该组件是通过路由直接跳转过来的,那么可以通过 props 直接获取 history、location、match 对象,这些属性是路由帮我传进来的
class About extends PureComponent {
render() {
return (
<div>
<NavLink exact to="/about" >企业历史</NavLink>
<NavLink exact to="/about/culture" >企业文化</NavLink>
<NavLink exact to="/about/contact" >联系我们</NavLink>
<button onClick={e => this.jumpToJoin()}>加入我们</button>
<Switch>
<Route exact path="/about" component={AboutHisotry}/>
<Route path="/about/culture" component={AboutCulture}/>
<Route path="/about/contact" component={AboutContact}/>
<Route path="/about/join" component={AboutJoin}/>
</Switch>
</div>
)
}
jumpToJoin() {
this.props.history.push("/about/join");
}
}
- 如果该组件不是通过路由直接跳转过来的,就不能通过props 获取 histroy 等对象,这就需要我们使用高阶组件 withRouter 对我们的组件进行包裹,同时还需要将我们的高阶组件包裹在 Router 组件之中
参数传递
- 路由中传递参数有三种方式
- 动态路由的方式
- search 传递参数
- Link 中 to 传入对象
动态路由
- 动态路由的概念指的是路由中的路径并不会固定
- 比如/detail 的 path 对应一个组件 Detail
- 如果我们将 path 在 Route 匹配时写成/detail/:id,那么/detail/abc、/detail/123 都可以匹配到该 Route,并且进行显示
{/* 设置路由匹配路径*/}
<Route exact path="/detail/:id" component={About}/>
{/* 传递参数 id*/}
<NavLink to={`/detail/${id}`} a>详情</NavLink>
{/* 取出传递的 id*/}
const id = this.props.match.params.id;
- 这个匹配规则,我们就称之为动态路由
- 通常情况下,使用动态路由可以为路由传递参数
- 动态路由只能传递简单的字符串
search 传递参数
- 我们在在路径后面跟上一个?,然后在?后面以 key=value 的形式拼接的字符串的传参方式称为 search 方式传递参数
<Route path="/detail" component={Detail} />
<NavLink to={
/detail2?name={age}} </NavLink>
然后我们可以通过this.props.location.search
取出传递的参数
"?name=***&age=***"
, 这个传过来的参数是需要我们自己解析的,因为现在React-router推荐 to 的方式传递,不再推荐这种方式了,所以不再帮我们解析了。
Link 中使用 to 传递对象
- Link 的 to 属性可以接收 string、object、func 三种类型的数据,当我们要通过 Router 传递对象时就可以给to 赋值一个对象, 这个对象主要有四个属性:
- pathname: A string representing the path to link to
- search: A string representation of query parameters.
- hash: A hash to put in the URL, e.g. #a-hash.
- state: State to persist to the location. 传递的对象赋值给 state 就可以了, 取的时候通过
this.props.location.state.属性名就可以
<Route path="/detail" component={Detail} />
<NavLink to={{
pathname: "/detail",
state: {name:"zhangsan", age=18}
}}> 详情3</NavLink>
const name = this.props.location.state.name; //取出传递的 name 值
pathname: A string representing the path to link to.
search: A string representation of query parameters.
hash: A hash to put in the URL, e.g. #a-hash.
state: State to persist to the location.
react-router-config 库
- react-router-config 这个库,可以将所有的路由配置放到一个地方进行集中管理,而不是使用 Route 组件写的到处都是, 集中管理起来就方便修改和维护
安装
- 首先我们要先安装这个库:
yarn add react-router-config
使用
- 创建一个专门管理 router 的文件夹 router, 然后创建
index.js
文件 - 在 index.js 文件中实现如下, 每个 route对象都有以下几个属性:
- path: /路径
- exact: 是否精准匹配
- component: 对应组件
- routes: 子路由列表, 列表中的对象同样也是 route 对象
import Home from '../pages/home';
import About, { AboutHisotry, AboutCulture, AboutContact, AboutJoin } from '../pages/about';
import Profile from '../pages/profile';
const routes = [
{
path: "/",
exact: true,
component: Home
},
{
path: "/about",
component: About,
routes: [
{
path: "/about",
exact: true,
component: AboutHisotry
},
{
path: "/about/culture",
component: AboutCulture
},
{
path: "/about/contact",
component: AboutContact
},
]
},
{
path: "/profile",
component: Profile
},
]
export default routes;
- 在使用的地方导入 route 列表, 导入react-router-config的renderRoutes函数
import React, { PureComponent } from 'react';
import { renderRoutes } from 'react-router-config';
import routes from './router'; // 如果是 index.js 可以省略,默认导入的就是 index.js
class App extends PureComponent {
render() {
return (
<div>
<NavLink exact to="/" >首页</NavLink>
<NavLink to="/about" >关于</NavLink>
<NavLink to="/profile" >我的</NavLink>
{/* 使用renderRoutes 渲染 routes*/}
{renderRoutes(routes)}
</div>
)
}
}
export default withRouter(App);
- 如果有嵌套,我们同样调用renderRoutes函数渲染我们的子路由, 这里子路由的 routes 列表可以从 props.route.routes 中获取,renderRoutes函数会在props 中添加 route 属性,如果没有使用renderRoutes函数,props 中是不会有 route 这个属性的
import React, { PureComponent } from 'react'
import { NavLink } from 'react-router-dom';
import { renderRoutes } from 'react-router-config';
export function AboutHisotry(props) {
return <h2>企业成立于2000年, 拥有悠久的历史文化</h2>
}
export function AboutCulture(props) {
return <h2>创新/发展/共赢</h2>
}
export function AboutContact(props) {
return <h2>联系电话: 020-68888888</h2>
}
export default class About extends PureComponent {
render() {
return (
<div>
<NavLink exact to="/about" >企业历史</NavLink>
<NavLink exact to="/about/culture" >企业文化</NavLink>
<NavLink exact to="/about/contact" >联系我们</NavLink>
{renderRoutes(this.props.route.routes)}
</div>
)
}
}
- react-router-config 还提供了一个matchRoutes函数,来帮我获取匹配到的route
const branch = matchRoutes(this.props.route.routes, "/about");