目标
- 针对 react / vue ,能够根据业务需求⼝喷 router 的关键配置,包括但不限于:路由的匹配规则、
路由守卫、路由分层等。
- 能够描述清楚 history 的主要模式,知道 history 和 router 的边界;
知识要点
什么是 Router,以及 Router 发展的历史
在 SPA(即只有⼀个 html ) 的出现后,前端可以⾃由控制组件的渲染,来模拟⻚⾯的跳转。
⻚⾯是怎么发⽣跳转,向服务端请求的呢?-- 浏览器劫持。
在讲这部分内容前,我们先来说⼀下,hash 路由和 history 路由的区别
SPA的⽅法,需要拦截请求;
- hash 路由,当我的hash
- history 的 go / forward / back 的时候,我的浏览器的地址,是发⽣了改变的,
总结:
后端路由是根据 url 访问相关的 controller 进⾏数据资源和模板引擎的拼接,返回前端;
前端路由是通过 js 根据 url 返回对应的组件加载。
所以,前端的路由包含两个部分:
- url 的处理
- 组件加载
分类
history 路由
- hash 路由
- memory 路由 *
window.location.hash = "xxx"
history./(go|back|repalce|push|forward)/
路由守卫
触发流程
-
【组件】- 前⼀个组件 beforeRouteLeave
-
【全局】- router.beforeEach
-
【组件】-如果是路由的参数变化,触发 beforeRouteUpdate ;
-
【配置⽂件】⾥,下⼀个 beforeEnter
-
【组件】内部声明的 beforeRouteEnter
-
【全局】调⽤ beforeResolve
-
【全局】的 router.afterEach
简单路由的实现
history:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>H5 路由</title>
</head>
<body>
<div id="container">
<a href="./" >首页</a>
<a href="./about">关于我们</a>
<a href="./user">用户列表</a>
</div>
<div id="context"></div>
<script>
class BaseRouter {
constructor() {
this.routes = {};
this._bindPopstate();
this.init();
}
init(path) {
window.history.replaceState({path}, null, path);
const cb = this.routes[path];
if(cb) {
cb();
}
}
route(path, callback) {
this.routes[path] = callback || function() {}
}
go(path) {
window.history.pushState({path}, null, path);
const cb = this.routes[path];
if(cb) {
cb();
}
}
_bindPopstate() {
window.addEventListener('popstate', e => {
const path = e.state && e.state.path;
this.routes[path] && this.routes[path]();
})
}
}
const Route = new BaseRouter();
Route.route('./about', () => changeText("关于我们页面"));
Route.route('./user', () => changeText("用户列表页"));
Route.route('./', () => changeText("首页"));
function changeText(arg) {
document.getElementById('context').innerHTML = arg;
}
container.addEventListener('click' , e => {
if(e.target.tagName === 'A') {
e.preventDefault();
Route.go(e.target.getAttribute('href'))
}
})
</script>
</body>
</html>
hash:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>hash 路由</title>
</head>
<body>
<div id="container" >
<button onclick="window.location.hash = '#'">首页</button>
<button onclick="window.location.hash = '#about'">关于我们</button>
<button onclick="window.location.hash = '#user'">用户列表</button>
</div>
<div id="context"></div>
</body>
<script>
class BaseRouter {
constructor() {
this.routes = {};
this.refresh = this.refresh.bind(this);
window.addEventListener('load', this.refresh);
window.addEventListener('hashchange', this.refresh);
}
route(path, callback) {
this.routes[path] = callback || function() {}
}
refresh() {
const path = `/${window.location.hash.slice(1) || ''}`;
this.routes[path]();
}
}
const Route = new BaseRouter();
Route.route('/about', () => changeText("关于我们页面"));
Route.route('/user', () => changeText("用户列表页"));
Route.route('/', () => changeText("首页"));
function changeText(arg) {
document.getElementById('context').innerHTML = arg;
}
</script>
</html>
实现路由核心思想:
history定义:
- 含当前的路径的状态
- 次路径下的状态
- 要实现路由监听,如果路径变化,需要通知⽤户
- createWebHistory()创建⼀个历史导航,创建⼀个对象,包含路径、状态、以及push/replace 切换的⽅法
- 跳转时,没有状态,所以我们需要传⼀些⾃⼰的状态forward, back, current, scroll.
- 实现⼀个 changeLocation 的⽅法,currentLocation 和 historyState ,相当于是路由中的 location 和 history
- 实现push,replace方法
- 实现历史监听historyListenner, 实际调用popstate, 设置监听callback
const history = createWebHistory()
webHistory.listen((to, from, {isBack}) => {
console.log(to, from, {isBack})
})
//1 创建⼀个历史导航
function createWebHistory() {
// 创建⼀个对象,包含路径、状态、以及push/replace 切换的⽅法
const historyNavigation = useHistoryStateNavigation();
// 构建⼀个监听函数,监听浏览器的前进和后退
const {location, state} = historyNavigation;
const historyListeners = useHistoryListeners(state, location);
const routerHistory = Object.assign(
{},
historyNavigation,
historyListeners
)
Object.defineProperty(routerHistory, 'location', {
get: () => historyNavigation.location.value
})
Object.defineProperty(routerHistory, 'state', {
get: () => historyNavigation.state.value
})
return routerHistory
}
function createCurrentLocation() {
const {pathname, search, hash} = window.location;
return pathname + search + hash;
}
function useHistoryStateNavigation() {
// const currentLocation = '/' // 更改的时候,尽量是⼀个引⽤类型。
const currentLocation = {
// 如何获取路径呢?⽤ window.location 的结果去拼接,我们再封装⼀个⽅法
value: createCurrentLocation();
}
// 除了路径以外,还要拿浏览器的状态
const historyState = {
value: window.history.state
}
//2
if(!historyState.value) {
// 但是你这样改,是不会更新你的 historyState 的,没有同步到路由系统中。3
changeLocation(//3
currentLocation.value,
buildState(null, currentLocation.value, null, true),//2
true
)
// 打印⼀下。
}
//3
function changeLocation(to, state, replace) {
window.history[replace?'replace':'pushState'](state, null, to);
historyState.value = state;
}
//4
function push(to, data) {
// 去哪,带的新的状态是谁?
// 跳转前:从哪⼉,去哪⼉
// why ? 为了做路由守卫。
const currentState = Object.assign(
{},
historyState.value,
// 只需要改去哪⼉,和当前的滚动条的位置。
{forward: to, scroll: {left: window.pageXOffset, top:
window.pageYOffset}}
)
// 本质是没有跳转的,只是,更新了状态,后续在 Vue 中,可以监控到详细的状态变化。
// 所以这⾥是 replace 模式
changeLocation(currentState.current, currentState, true);
// 跳转后:从这⼉到哪⼉
const state = Object.assign(
{},
buildState(currentLocation.value, to, null),
{position: currentState.position+1},
data,
)
// 这⾥要真正的跳转,所以是 push 模式
changeLocation(to, state, false);
currentLocation.value = to;
}
//4
function replace(to, data){
// 创建⼀个状态,并进⾏合并
// 只要替换掉 current 即可。
const state = Object.assign(
{},
buildState(historyState.value.back, to, historyState.value.forward,
true),
data
)
return {
location: currentLocation,
state: historyState,
push,
replace
}
}
//5
function useHistoryListeners(historyState, currentLocation) {
const popStateHandler = ({state}) => {
const to = createCurrentLocation(); // 去哪
const from = currentLocation.value; // 从哪⼉来
const prevState = historyState.value;
// 开始修改啦
currentLocation.value = to;
historyState.value = state;
let isBack = state.position - prevState.position < 0;
// ⽤户扩展的地⽅,就在这⾥,也就是连接 vue 组件和 history 之间的核⼼。
// !!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!
listeners.forEach(listener => {
listener(to, from, {isBack})
})
}
window.addEventListener('popstate', popStateHandler)
function listen(cb) {
listeners.push(cb);
}
return {
listen
}
}