1. 思维导图
2. 什么是前端路由?
前端路由是前端页面的状态管理器
前端路由起源于 SPA 单页应用架构(现代前端开发中最流行的页面模型):
浏览器地址变化 => 视觉上的页面切换 => 实际上的组件切换
前端路由就是用来完成这个任务的技术
3. 路由基本原理
前端三杰 Angular、React、Vue 都推荐单页面应用 SPA 开发模式,它们都有自己的前端路由解决方案:
一般来说,这些路由组件会在浏览器环境下,提供两种不同方式的路由:Hash 和 History;也提供非浏览器环境下(例如:Native环境、单元测试环境)的路由能力。
3.1. Hash
Hash —— 即地址栏 URL 中的 # 符号。路由里的 # 我们称之为 hash。
Any URL that contains a # character is a fragment URL. The portion of the URL to the left of the # identifies a resource that can be downloaded by a browser and the portion on the right, known as the fragment identifier, specifies a location within the resource.
示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Hash-Router</title>
<script src="jquery-1.11.3.min.js"></script>
</head>
<body>
<div class="router_box">
<a href="/home" class="router" replace="true">主页</a>
<a href="/news" class="router">新闻</a>
<a href="/team" class="router">团队</a>
<a href="/about" class="router">关于</a>
<a href="/abcd" class="router">随便什么</a>
</div>
<div id="router-view"></div>
<script>
class Router {
constructor(params) {
// 记录routes配置
this.routes = params.routes || [];
const that = this;
// 绑定路由响应事件
$('.router').click(function(e){
// 阻止a标签的默认行为
e.preventDefault();
const $this = $(this);
const aHref = $this.attr('href');
const aReplace = $this.attr('replace');
that.changeHash(aHref, aReplace);
});
// 监听路由改变
window.addEventListener('hashchange', () => {
this.changeRoute();
});
// 初始路由跳转
this.changeRoute();
}
changeHash(href, replace) {
// 判断是replace方法还是push方法
if (replace) {
const i = window.location.href.indexOf('#');
// 通过replace方法直接替换url
window.location.replace(
window.location.href.slice(0, i >= 0 ? i : 0) + '#' + href
);
} else {
// 通过赋值追加
window.location.hash = href;
}
}
// 路由改变监听事件
changeRoute() {
const nowHash = window.location.hash;
const index = this.routes.findIndex((item, index) => {
return nowHash == ('#' + item.path);
});
if (index >= 0) {
document.querySelector('#router-view').innerHTML = this.routes[index].component;
} else {
const defaultIndex = this.routes.findIndex((item, index) => {
return item.path == '*';
});
if (defaultIndex >= 0) {
const i = window.location.href.indexOf('#');
window.location.replace(
window.location.href.slice(0, i >= 0 ? i : 0) + '#' + this.routes[defaultIndex].redirect
);
}
}
};
}
new Router({
routes: [
{ path: '/home', component: '<h1>主页</h1><h4>这是主页</h4>' },
{ path: '/news', component: '<h1>新闻</h1><h4>这是新闻</h4>' },
{ path: '/team', component: '<h1>团队</h1><h4>这是团队</h4>' },
{ path: '/about', component: '<h1>关于</h1><h4>这是关于</h4>' },
{ path: '*', redirect: '/home' }
]
});
</script>
</body>
</html>
3.2. History
HTML5 引入了 history.pushState() 和 history.replaceState() 方法,它们分别可以添加和修改历史记录条目。允许在不刷新页面的前提下,通过脚本语言的方式来进行页面上某块局部内容的更新。这些方法通常与 window.onpopstate 配合使用。
示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>History-Router</title>
<script src="jquery-1.11.3.min.js"></script>
</head>
<body>
<div class="router_box">
<a href="/home" class="router" replace="true">主页</a>
<a href="/news" class="router">新闻</a>
<a href="/team" class="router">团队</a>
<a href="/about" class="router">关于</a>
<a href="/abcd" class="router">随便什么</a>
</div>
<div id="router-view"></div>
<script>
class Router {
constructor(params) {
// 记录routes配置
this.routes = params.routes || [];
const that = this;
// 绑定路由响应事件
$('.router').click(function(e) {
// 阻止a标签的默认行为
e.preventDefault();
const $this = $(this);
const aHref = $this.attr('href');
const aReplace = $this.attr('replace');
that.changeHis(aHref, aReplace);
});
// 监听路由改变
window.addEventListener('popstate', e => {
this.changeRoute();
});
// 初始路由跳转
this.changeRoute();
}
changeHis(href, replace) {
// 判断是replace方法还是push方法
if (replace) {
window.history.replaceState({}, '', window.location.origin + href);
this.changeRoute();
} else {
window.history.pushState({}, '', window.location.origin + href);
this.changeRoute();
}
}
// 路由改变监听事件
changeRoute() {
let path = window.location.href.replace(window.location.origin, '');
let index = this.routes.findIndex((item, index) => {
return path == item.path;
});
if (index >= 0) {
document.querySelector('#router-view').innerHTML = this.routes[index].component;
} else {
let defaultIndex = this.routes.findIndex((item, index) => {
return item.path == '*';
});
if (defaultIndex >= 0) {
window.history.pushState({}, '', window.location.origin + this.routes[defaultIndex].redirect);
this.changeRoute();
}
}
};
}
new Router({
routes: [
{ path: '/home', component: '<h1>主页</h1><h4>这是主页</h4>' },
{ path: '/news', component: '<h1>新闻</h1><h4>这是新闻</h4>' },
{ path: '/team', component: '<h1>团队</h1><h4>这是团队</h4>' },
{ path: '/about', component: '<h1>关于</h1><h4>这是关于</h4>' },
{ path: '*', redirect: '/home' }
]
});
</script>
</body>
</html>
4. 有哪些路由集成方案?
除了 React、Vue 体系下的基础路由库:ReactRouter、VueRouter。通常各技术体系的 UI 开发框架上,还会提供对路由的深度集成功能。例如:
4.1. Umijs
Umi,中文可发音为乌米,是可扩展的企业级前端应用框架。Umi 以路由为基础的,同时支持配置式路由和约定式路由,保证路由的功能完备,并以此进行功能扩展。然后配以生命周期完善的插件体系,覆盖从源码到构建产物的每个生命周期,支持各种功能扩展和业务需求。 https://umijs.org/zh-CN/docs
4.2. iView-admin
iView-admin是iView生态中的成员之一,是一套采用前后端分离开发模式,基于Vue的后台管理系统前端解决方案。iView-admin2.0脱离1.x版本进行重构,换用Webpack4.0 + Vue-cli3.0作为基本开发环境。内置了开发后台管理系统常用的逻辑功能,和开箱即用的业务组件,旨在让开发者能够以最小的成本开发后台管理系统,降低开发量。 https://lison16.github.io/iview-admin-doc/#/
4.3. vue-element-admin
vue-element-admin 是一个后台前端解决方案,它基于 vue 和 element-ui实现。它使用了最新的前端技术栈,内置了 i18 国际化解决方案,动态路由,权限验证,提炼了典型的业务模型,提供了丰富的功能组件,它可以帮助你快速搭建企业级中后台产品原型。 https://panjiachen.github.io/vue-element-admin-site/zh/guide/
5. React Router 案例分析
5.1. 示例:基础
描述:
效果图:
实现策略:
关键代码:
import React from "react";
import {
BrowserRouter as Router,
Switch,
Route,
Link
} from "react-router-dom";
// This site has 3 pages, all of which are rendered
// dynamically in the browser (not server rendered).
//
// Although the page does not ever refresh, notice how
// React Router keeps the URL up to date as you navigate
// through the site. This preserves the browser history,
// making sure things like the back button and bookmarks
// work properly.
export default function BasicExample() {
return (
<Router>
<div>
<ul>
<li>
<Link to="/login">Login</Link>
</li>
<li>
<Link to="/home">Home</Link>
</li>
<li>
<Link to="/path-not-exist">path-not-exist</Link>
</li>
</ul>
<hr />
{/*
A <Switch> looks through all its children <Route>
elements and renders the first one whose path
matches the current URL. Use a <Switch> any time
you have multiple routes, but you want only one
of them to render at a time
*/}
<Switch>
<Route path="/login">
<Login />
</Route>
<Route path="/home">
<Home />
</Route>
<Route path="/">
<Error404 />
</Route>
</Switch>
</div>
</Router>
);
}
// You can think of these components as "pages"
// in your app.
function Home() {
return (
<div>
<h2>Home</h2>
</div>
);
}
function Error404() {
return (
<div>
<h2>404</h2>
</div>
);
}
function Login() {
return (
<div>
<h2>Login</h2>
</div>
);
}
function Dashboard() {
return (
<div>
<h2>Dashboard</h2>
</div>
);
}
5.2. 示例:URL 参数
描述:
效果图:
实现策略:
关键代码:
import React from "react";
import {
BrowserRouter as Router,
Switch,
Route,
Link,
useParams,
} from "react-router-dom";
// Params are placeholders in the URL that begin
// with a colon, like the `:id` param defined in
// the route in this example. A similar convention
// is used for matching dynamic segments in other
// popular web frameworks like Rails and Express.
export default function ParamsExample() {
return (
<Router>
<div>
<h2>Accounts</h2>
<ul>
<li>
<Link to="/group1/netflix">Netflix</Link>
</li>
<li>
<Link to="/group1/zillow-group">Zillow Group</Link>
</li>
<li>
<Link to="/group2/yahoo">Yahoo</Link>
</li>
<li>
<Link to="/group2/modus-create">Modus Create</Link>
</li>
</ul>
<Switch>
<Route path="/group1/:id">
<Child1 />
</Route>
<Route path="/group2/:id">
<Child2 />
</Route>
</Switch>
</div>
</Router>
);
}
function Child1() {
// We can use the `useParams` hook here to access
// the dynamic pieces of the URL.
const { id } = useParams();
return (
<div>
<h2>This is [Child1], ID: {id}</h2>
</div>
);
}
function Child2() {
// We can use the `useParams` hook here to access
// the dynamic pieces of the URL.
const { id } = useParams();
return (
<div>
<h2>This is [Child2], ID: {id}</h2>
</div>
);
}
5.3. 示例:嵌套路由
描述:
效果图:
实现策略:
关键代码:
import React from "react";
import {
BrowserRouter as Router,
Switch,
Route,
Link,
useParams,
useRouteMatch
} from "react-router-dom";
// Since routes are regular React components, they
// may be rendered anywhere in the app, including in
// child elements.
//
// This helps when it's time to code-split your app
// into multiple bundles because code-splitting a
// React Router app is the same as code-splitting
// any other React app.
export default function NestingExample() {
return (
<Router>
<div>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/topics">Topics</Link>
</li>
<li>
<Link to="/resources">Resources</Link>
</li>
</ul>
<hr />
<Switch>
<Route exact path="/">
<Home />
</Route>
<Route path="/topics">
<Topics />
</Route>
<Route path="/resources">
<Topics />
</Route>
</Switch>
</div>
</Router>
);
}
function Home() {
return (
<div>
<h2>Home</h2>
</div>
);
}
function Topics() {
// The `path` lets us build <Route> paths that are
// relative to the parent route, while the `url` lets
// us build relative links.
let { path, url } = useRouteMatch();
return (
<div>
<h2>{url}</h2>
<ul>
<li>
<Link to={`${url}/rendering`}>Rendering with React</Link>
</li>
<li>
<Link to={`${url}/components`}>Components</Link>
</li>
<li>
<Link to={`${url}/props-v-state`}>Props v. State</Link>
</li>
</ul>
<Switch>
<Route exact path={path}>
<h3>Please select a topic.</h3>
</Route>
<Route path={`${path}/:topicId`}>
<Topic />
</Route>
</Switch>
</div>
);
}
function Topic() {
// The <Route> that rendered this component has a
// path of `/topics/:topicId`. The `:topicId` portion
// of the URL indicates a placeholder that we can
// get from `useParams()`.
let { topicId } = useParams();
return (
<div>
<h3>{topicId}</h3>
</div>
);
}
5.4. 示例:路由重定向(鉴权)
效果图:
实现策略:
关键代码:
import React, { useContext, createContext, useState } from "react";
import {
BrowserRouter as Router,
Switch,
Route,
Link,
Redirect,
useHistory,
useLocation,
} from "react-router-dom";
// This example has 3 pages: a public page, a protected
// page, and a login screen. In order to see the protected
// page, you must first login. Pretty standard stuff.
//
// First, visit the public page. Then, visit the protected
// page. You're not yet logged in, so you are redirected
// to the login page. After you login, you are redirected
// back to the protected page.
//
// Notice the URL change each time. If you click the back
// button at this point, would you expect to go back to the
// login page? No! You're already logged in. Try it out,
// and you'll see you go back to the page you visited
// just *before* logging in, the public page.
export default function AuthExample() {
return (
<ProvideAuth>
<Router>
<div>
<AuthButton />
<ul>
<li>
<Link to="/public">Public Page</Link>
</li>
<li>
<Link to="/protected">Protected Page</Link>
</li>
</ul>
<Switch>
<Route exact path="/">
<Redirect to={"/public"} />
</Route>
<Route path="/public">
<PublicPage />
</Route>
<Route path="/login">
<LoginPage />
</Route>
<PrivateRoute path="/protected">
<ProtectedPage />
</PrivateRoute>
</Switch>
</div>
</Router>
</ProvideAuth>
);
}
/**
* 权限管理逻辑
* 1. fakeAuth:登录、登出模拟
* 2. ProvideAuth:以 Provider 模式,向下传递认证信息
* 3. useAuth:Hooks 方式,获取当前认证信息
*/
const fakeAuth = {
isAuthenticated: false,
signin(cb) {
fakeAuth.isAuthenticated = true;
setTimeout(cb, 100); // fake async
},
signout(cb) {
fakeAuth.isAuthenticated = false;
setTimeout(cb, 100);
},
};
const authContext = createContext<{
user: string;
signin: (cb: Function) => void;
signout: (cb: Function) => void;
}>(null);
function ProvideAuth({ children }) {
const [user, setUser] = useState(null);
const signin = (cb) => {
return fakeAuth.signin(() => {
setUser("user");
cb();
});
};
const signout = (cb) => {
return fakeAuth.signout(() => {
setUser(null);
cb();
});
};
const auth = {
user,
signin,
signout,
};
return <authContext.Provider value={auth}>{children}</authContext.Provider>;
}
function useAuth() {
return useContext(authContext);
}
/**
* 权限组件
* 1. 权限按钮(负责显示登陆状态、退出登录状态)
* 2. 私有路由(无权限时,重定向到登陆页)
* 3. 登陆页
*/
function AuthButton() {
let history = useHistory();
let auth = useAuth();
return auth.user ? (
<p>
Welcome!{" "}
<button
onClick={() => {
auth.signout(() => history.push("/"));
}}
>
Sign out
</button>
</p>
) : (
<p>You are not logged in.</p>
);
}
function PrivateRoute({ children, ...rest }) {
let auth = useAuth();
return (
<Route
{...rest}
render={({ location }) =>
auth.user ? (
children
) : (
<Redirect
to={{
pathname: "/login",
state: { from: location },
}}
/>
)
}
/>
);
}
function LoginPage() {
let history = useHistory();
let location = useLocation();
let auth = useAuth();
let { from } = location.state || { from: { pathname: "/" } };
let login = () => {
auth.signin(() => {
history.replace(from);
});
};
return (
<div>
<p>You must log in to view the page at {from.pathname}</p>
<button onClick={login}>Log in</button>
</div>
);
}
/**
* 业务逻辑页面
*/
function PublicPage() {
return <h3>Public</h3>;
}
function ProtectedPage() {
return <h3>Protected</h3>;
}
参考:
Angular、React、Vue 路由解决方案: https://angular.io/guide/router https://reacttraining.com/react-router/ https://github.com/ReactTraining/react-router/ https://github.com/ReactTraining/history https://router.vuejs.org/zh/ 路由原理: https://developer.mozilla.org/en-US/docs/Web/API/Window/hashchange_event http://blog.httpwatch.com/2011/03/01/6-things-you-should-know-about-fragment-urls/ https://developer.mozilla.org/zh-CN/docs/Web/API/History_API 路由路径匹配: https://github.com/pillarjs/path-to-regexp 路由集成方案: https://umijs.org/zh-CN/docs/routing https://pro.ant.design/docs/router-and-nav-cn https://nextjs.org/docs/routing/introduction https://panjiachen.github.io/vue-element-admin-site/zh/ https://lison16.github.io/iview-admin-doc/#/%E8%B7%AF%E7%94%B1%E9%85%8D%E7%BD%AE