js專題系列-前端路由
路由是根據不同的 url 地址展示不同的內容或頁面
早期的路由都是後端直接根據 url 來 reload 頁面實現的,即後端控制路由。
後來頁面越來越複雜,伺服器壓力越來越大,隨著 ajax(非同步刷新技術) 的出現,頁面實現非 reload 就能重新整理資料,讓前端也可以控制 url 自行管理,前端路由由此而生。
單頁面應用的實現,就是因為有了前端路由這個概念。
2. 前端路由的兩種實現原理
1 Hash路由
我們經常在 url 中看到 #,這個 # 有兩種情況,一個是我們所謂的錨點,比如典型的回到頂部按鈕原理、Github 上各個標題之間的跳轉等,路由裡的 # 不叫錨點,我們稱之為 hash,大型框架的路由系統大多都是雜湊實現的。
我們需要一個根據監聽雜湊變化觸發的事件 —— hashchange 事件
window物件提供了onhashchange事件來監聽hash值的改變,一旦url中的hash值發生改變,便會觸發該事件。
我們用 window.location 處理雜湊的改變時不會重新渲染頁面,而是當作新頁面加到歷史記錄中,這樣我們跳轉頁面就可以在 hashchange 事件中註冊 ajax 從而改變頁面內容。
window.addEventListener('hashchange', function () { <!--這裡你可以寫你需要的程式碼--> }); 複製程式碼
2 History 路由
HTML5的History API 為瀏覽器的全域性history物件增加的擴充套件方法。
重點說其中的兩個新增的API history.pushState 和 history.replaceState
這兩個 API 都接收三個引數,分別是
狀態物件(state object) — 一個JavaScript物件,與用pushState()方法建立的新歷史記錄條目關聯。無論何時使用者導航到新建立的狀態,popstate事件都會被觸發,並且事件物件的state屬性都包含歷史記錄條目的狀態物件的拷貝。
標題(title) — FireFox瀏覽器目前會忽略該引數,雖然以後可能會用上。考慮到未來可能會對該方法進行修改,傳一個空字串會比較安全。或者,你也可以傳入一個簡短的標題,標明將要進入的狀態。
地址(URL) — 新的歷史記錄條目的地址。瀏覽器不會在呼叫pushState()方法後加載該地址,但之後,可能會試圖載入,例如使用者重啟瀏覽器。新的URL不一定是絕對路徑;如果是相對路徑,它將以當前URL為基準;傳入的URL與當前URL應該是同源的,否則,pushState()會丟擲異常。該引數是可選的;不指定的話則為文件當前URL。
我們在控制檯輸入
window.history.pushState(null, null, "https://www.baidu.com/?name=lvpangpang");
可以看到瀏覽器url的變化

注意:這裡的 url 不支援跨域,比如你在不是百度域名下輸入上面的程式碼。

不過這種模式之前在vue或者react裡面選擇了這種模式,發現一重新整理頁面就會到月球。
原因是因為history模式的url是真實的url,伺服器會對url的檔案路徑進行資源查詢,找不到資源就會返回404。說的通俗一點就是這種模式會被伺服器識別,會做出相應的處理。
對於這種404的問題,我們有很多解決方式。
A 配置webpack(開發環境)
historyApiFallback:{ index:'/index.html'//index.html為當前目錄建立的template.html } 複製程式碼
B 配置ngnix(生產環境)
location /{ root/data/nginx/html; indexindex.html index.htm; error_page 404 /index.html; } 複製程式碼
3. 路由demo
接下來會一步一步來講解怎麼樣寫一個前端路由。
也就是把我們的知識轉為技能的過程。
上面我們也看到了路由是根據不同的 url 地址展示不同的內容或頁面。對於前端路由來說就是根據不同的url地址展示不同的內容。
於是有了下面這版程式碼。
<!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> </head> <body> <div id="root"> <a href="#/index">首頁</a> <a href="#/list">列表</a> </div> <script> const root = document.querySelector('#root'); window.onhashchange = function (e) { var hash = window.location.hash.substr(1); if(hash === '/index') { root.innerHTML = '這是index元件'; } if (hash === '/list') { root.innerHTML = '這是list元件'; } } </script> </body> </html> 複製程式碼
上面只能說是一個小demo,為了讓我們能最直觀地感受到前端路由。這次為了能有更好的效果,特意引入了gif。

4. 路由js版
看好了demo,是不是迫不及待想實現一個路由了,那就讓我們一起來一步一步實現它吧。這裡給他取個名-煉獄,主要是方便下文的指代。
4.1 煉獄的引數配置
這裡我是仿造vue,react裡面的路由配置的,預設是一個路由物件陣列。
//路由配置 const routes = [{ path: '/index', url: 'js/index.js' }, { path: '/list', url: 'js/list.js' }, { path: '/detail', url: 'js/detail.js' }]; var router = new Router(routes); 複製程式碼
可以看到上面的路由配置是不是和vue以及react很像呢。只不過這裡的url指向的是js檔案而不是元件(其實元件也是js檔案,一個元件包含html, css, js ,最終都會被編譯到一個js檔案)
4.1 煉獄的整體框架
function Router(opts = []) { } Router.prototype = { init: function () { }, // 路由註冊 initRouter: function () { }, // 解析url獲取路徑以及對應引數陣列化 getParamsUrl: function () { }, // 路由處理 urlChange: function () { }, // 渲染檢視(執行匹配到的js程式碼) render: function (currentHash) { }, // 單個路由註冊 map: function (item) { }, // 切換前 beforeEach: function (callback) { }, // 切換後 afterEach: function (callback) { }, // 路由非同步懶載入js檔案 asyncFun: function (file, transition) { } } 複製程式碼
4.1 煉獄的內部解刨
上面已經列出來煉獄的整體程式碼框架,下面我們就來對每一個函式進行編寫。
A init函式
這是煉獄外掛在被呼叫的時候就會執行的方式,當然是用來註冊路由以及繫結對應的路由切換事件的。
init() { var oThis = this; // 註冊路由 this.initRouter(); // 頁面載入匹配路由 window.addEventListener('load', function () { oThis.urlChange(); }); // 路由切換 window.addEventListener('hashchange', function () { oThis.urlChange(); }); } } 複製程式碼
B initRouter函式+map函式
註冊路由,作用就是將路由物件陣列引數在初始化的時候就做好路由匹配,比如/index路由對應/js/index.js。
// 路由註冊 initRouter: function() { var opts = this.opts; opts.forEach((item, index) => { this.map(item); }); } // 單個路由註冊 map: function (item) { path = item.path.replace(/\s*/g, '');// 過濾空格 this.routers[path] = { callback: (transition) => { return this.asyncFun(item.url, transition); }, // 回撥 fn: null // 快取對應的js檔案 } } 複製程式碼
this.routers用來儲存路由物件,執行每一個路由的callback函式就是載入對應的js檔案。
每一個router物件裡面的fn函式的作用是已經載入過的js檔案,可以做到載入一次多次使用,在路由切換的時候。
C asyncFun函式
這個函式的作用是非同步載入目標js檔案。原理就是利用手動生成javascript標籤動態插入頁面。當然在載入真實js檔案前需要做一個判斷,目標js是否已經載入過。
// 路由非同步懶載入js檔案 asyncFun: function (file, transition) { // console.log(transition); var oThis = this, routers = this.routers; // 判斷是否走快取 if (routers[transition.path].fn) { oThis.afterFun && oThis.afterFun(transition) routers[transition.path].fn(transition) } else { var _body = document.getElementsByTagName('body')[0]; var scriptEle = document.createElement('script'); scriptEle.type = 'text/javascript'; scriptEle.src = file; scriptEle.async = true; SPA_RESOLVE_INIT = null; scriptEle.onload = function () { oThis.afterFun && oThis.afterFun(transition) routers[transition.path].fn = SPA_RESOLVE_INIT; routers[transition.path].fn(transition) } _body.appendChild(scriptEle); } } 複製程式碼
D render函式
看名字都知道這個函式的主要作用就是渲染頁面,在這裡也就是執行載入路由對應的js檔案。這裡做了一個判斷,如果存在路由守護的話則走路由守護。
// 渲染檢視(執行匹配到的js程式碼) render: function (currentHash) { var oThis = this; // 全域性路由守護 if (oThis.beforeFun) { oThis.beforeFun({ to: { path: currentHash.path, query: currentHash.query }, next: function () { // 執行目標路由對應的js程式碼(相當於是元件渲染) oThis.routers[currentHash.path].callback.call(oThis, currentHash) } }); } else { oThis.routers[currentHash.path].callback.call(oThis, currentHash); } } 複製程式碼
E beforeEach函式
路由守護函式,在這裡可以做一些比如登入許可權判斷的事情,這一點是不是和vue-router的全域性路由守護很像呢。
// 切換前 beforeEach: function (callback) { if (Object.prototype.toString.call(callback) === '[object Function]') { this.beforeFun = callback; } else { console.trace('請傳入函式型別的引數'); } }, 複製程式碼
好了,上面寫好了煉獄的主要程式碼,下面我們就可以看到對應的效果了。
