spa流行的今天不少同學會把前端路由跟後端路由弄混, 莫名其妙的怎麼頁面404啦之類'奇怪'的問題, 其實這就是沒弄清楚前端路由和後端路由的原因(當然你用hash當我沒說).
本文所有前端路由都是spa的情況下, 不存在後端渲染好變數的情況
原理
首先我們看看前後端路由在瀏覽器中是怎麼工作的, 上圖:
後端控制的路由:
我們可以知道後端其實返回的是html字串, 也就是dom節點不出意外的話是確認的. 不管你請求多少次, 結果都是確定的(get 冪等). 所以也就不存在404的情況
前端控制的路由:
如果是spa的話, 我們可以知道不管你請求那個頁面, 在後端處理好的情況下後端都會返回一個html檔案(所謂單頁的由來), 靜態資源當然也是類似的. 那麼我們可能有點疑問, 比如一個個人主頁, 如果只返回一個html檔案的話, 怎麼得到不同的使用者資料呢, 答案就是前端路由(大部分情況, 不排除本地儲存?), js根據不同的路由再向伺服器請求相關資料, 也就是說其實第一次服務端渲染我們的頁面是空的, 後期ajax請求. 所以我們看到很多單頁頁面開啟了首先要loading一會. 就是在向伺服器請求渲染頁面.
實現
後端路由我們暫且不去管它, 我們看看是怎麼實現的:
在非hash的情況下, 前端路由的實現基礎是window.history, 當然我們不用去管它的相容性了, 反正現在大部分瀏覽器能用就是了:
history
有個重要的方法就是pushState
, 其它的方法暫時用不到不提, 它的作用呢就是改變瀏覽器位址列裡的地址, 以及在歷史紀錄里加上一條, 除此以外沒啥別的副作用了, 比如:
var stateObj = { foo: "bar" };
history.pushState(stateObj, "page 2", "bar.html");
複製程式碼
上面的程式碼只會跳到一個 bar.html
的地址, 但是頁面本身並未跳轉, 我們不是來講history物件本身的, 有興趣可以自行翻看mdn.
其實參考後端對路由的控制, 我們大略可以想像一個前端路由所具有的功能:
- 對路由做出響應
- 渲染
- 一些事件, 比如beforeChange之類的
當然我們現在一切從簡, 上面那些說清楚了起實現無非就是苦力了, 先給大家看看效果吧:
還是有點意思的吧.
下面是html程式碼:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<link href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-default nav-static-top">
<div class="container-fluid">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<a class="navbar-brand" href="#">LOGO</a>
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li><a href="/1" data-role="custom-history">地址1 <span class="sr-only">(current)</span></a></li>
<li><a href="/2" data-role="custom-history">地址2</a></li>
</ul>
</div><!-- /.navbar-collapse -->
</div><!-- /.container-fluid -->
</nav>
<div id="app" class="container">
<div class="panel panel-default">
<div class="panel-heading">Panel heading without title</div>
<div class="panel-body">
Panel content
</div>
</div>
</div>
<script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
<script src="./route.js"></script>
<script src="./index.js"></script>
</body>
</html>
複製程式碼
index.js
const $routeController = $('a[data-role=custom-history]')
const app = new Route()
app.set('/1', function () {
$('#app').html('1')
})
app.set('/2', function () {
$('#app').html(2)
})
複製程式碼
route.js
class Route {
constructor () {
this.urls = []
this.handles = {}
window.addEventListener('popstate', (e) => {
const state = e.state || {}
const url = state.url || null
if (url) {
this.refresh(url)
}
})
const $routeController = $('a[data-role=custom-history]')
$routeController.on('click', e => {
e.preventDefault()
const link = $(e.target).attr('href')
history.pushState({ url: link }, '', link)
this.refresh(link)
})
}
set (route, handle) {
if (this.urls.indexOf(route) === -1) {
this.urls.push(route)
this.handles[route] = handle
}
}
refresh (route) {
if (this.urls.indexOf(route) === -1) throw new Error('請不要這樣呼叫, 路由表中不存在!')
this.handles[route]()
}
}
複製程式碼
按我的本意是不想在一篇文章裡貼這麼多程式碼的, 但是因為也不可以直接嵌入jsbin之類的, 方便大家試試看效果, 就放進來把, 因為程式碼比較簡單, 而且深度繫結到了dom上, 就不要嘲笑啦!