前陣子逛 github 的時候,看見一篇文章 《原生JS實現hash路由》, 想着照着 vue-router 的 api,參考這篇文章實現一個可直接用於 html, 支持 hash 路由和 history 路由的 js 插件。本文是 hash 路由的具體實現。html
話很少說,先上 demo&& 源碼&& 工程文件(htmlRouter文件夾下)前端
router.back() ; router.front()
控制前進後退router.push(option); router.replace(option)
實現路由跳轉使用自定義屬性 data-component-name
使頁面根據當前路由名稱顯示對應組件名的 dom 元素,默認擁有此屬性的 dom 元素隱藏vue
<main>
<div class="nav">
<ul class="nav-list">
<li class="nav-item"><a href="#/monday">週一</a></li>
<li class="nav-item"><a href="#/tuesday">週二</a></li>
<li class="nav-item"><a href="#/wednesday">週三</a></li>
<li class="nav-item"><a href="#/thursday">週四</a></li>
<li class="nav-item"><a href="#/friday">週五</a></li>
</ul>
</div>
<div class="main-content">
<div class="main-box" data-component-name="monday">monday</div>
<div class="main-box" data-component-name="tuesday">tuesday</div>
<div class="main-box" data-component-name="wednesday">wednesday</div>
<div class="main-box" data-component-name="thursday">thursday</div>
<div class="main-box" data-component-name="friday">friday</div>
</div>
</main>
<div class="nav-area">
<button class="nav-area-back" onclick="router.back();">後退</button>
<button class="nav-area-front" onclick="router.front();">前進</button>
<button class="nav-area-front" onclick="router.go(-1);">go(-1)</button>
<button class="nav-area-front" onclick="router.push({path: '/monday', query: {name: 'suporka', age: '26'}});" >
push path
</button>
<button class="nav-area-front" onclick="router.push({name: 'monday', query: {name: 'suporka', age: '26'}});" >
push name
</button>
<button class="nav-area-front" onclick="router.replace({name: 'monday', query: {name: 'suporka', age: '18'}});" >
replace
</button>
</div>
複製代碼
實現 new Router(option)
建立路由,根據 vue-router 的配置選項,本文實現 mode 以及 routes 屬性node
import HashRouter from './HashRouter'
import HistoryRouter from './HistoryRouter';
class Router {
constructor(routerConfig) {
this._mode = routerConfig.mode || "hash"; // hash 或者 history
this._routes = routerConfig.routes;
// 根據不一樣的模式建立不一樣的路由類,本文是 hash 路由
if (routerConfig.mode === "hash")
this._router = new HashRouter(routerConfig);
else this._router = new HistoryRouter(routerConfig);
this._router.init(); // 路由初始化
}
back() {
this._router.back();
}
front() {
this._router.front();
}
go(n) {
window.history.go(n);
}
push(option) {
this._router.push(option);
}
replace(option) {
this._router.replace(option);
}
}
export default Router
複製代碼
由於目前咱們還沒有實現 history 路由,不知道那些屬性或方法是共同擁有的,因此暫時將 hash 路由的屬性所有寫於父類當中,當 history 路由實現時再將共同擁有的屬性方法進行抽離,單獨擁有的屬性方法單獨歸屬。webpack
export default class RouterParent {
constructor(routerConfig) {
this._routes = routerConfig.routes; // 路由列表
this.routeHistory = []; // 路由歷史
this.currentUrl = ''; // 當前的路由地址
this.currentIndex = -1; // 當前的路由序列號
this.frontOrBack = false; // 是否的點擊前進後退形成的路由變化,此時不須要監聽到路由變化函數
this.replaceRouter = false; // 是不是替換當前路由
}
}
複製代碼
vue-router 默認使用 Hash 模式。git
使用 url 的 hash 來模擬一個完整的 url。此時 url 變化時,瀏覽器是不會從新加載的。github
Hash(即#)是 url 的錨點,表明的是網頁中的一個位置,僅僅改變#後面部分,瀏覽器只會滾動對應的位置,而不會從新加載頁面。web
Hash僅僅只是對瀏覽器進行指導,而對服務端是徹底沒有做用的!它不會被包括在 http 請求中,故也不會從新加載頁面。同時 hash 發生變化時,url 都會被瀏覽器記錄下來,這樣你就能夠使用瀏覽器的後退了。算法
所以,咱們須要監聽頁面hash的變化,經過 window.addEventListener('hashchange', func, false);
實現vue-router
哈希路由繼承父類RouterParent
,咱們在其 init()
方法時監聽 hashchange 事件,初始化
class HashRouter extends RouterParent {
constructor(routerConfig) {
super(routerConfig);
}
init() {
// 監聽hash的變化
// refresh 實現對應組件和當前路由綁定顯示
// bind(this) 傳入此實例對象,不然this指向有問題
window.addEventListener('hashchange', this.refresh.bind(this), false);
}
}
複製代碼
由於在頁面加載時,也須要根據此路由顯示對應組件,所以加入 load 監聽事件
class HashRouter extends RouterParent {
constructor(routerConfig) {
super(routerConfig);
}
init() {
// 監聽hash的變化
// refresh 實現對應組件和當前路由綁定顯示
// bind(this) 傳入此實例對象,不然this指向有問題
window.addEventListener('hashchange', this.refresh.bind(this), false);
window.addEventListener('load', this.refresh.bind(this), false);
}
}
複製代碼
在此實例中,咱們使用 frontOrBack 屬性判斷當前是否處於前進後退,若是是前進後退,則路由歷史列表 routeHistory 不變化
根據當前 hash 路徑,從 routes 列表中找出對應的路由 name, 在操做對應的 dom 元素使其顯示或隱藏
refresh() {
if (this.frontOrBack) {
// 前進後退形成的路由變化,此時不須要改變routeHistory的數據
this.frontOrBack = false;
} else {
this.currentUrl = location.hash.slice(1) || '/';
this.routeHistory = this.routeHistory.slice(0,this.currentIndex + 1); // 捨棄掉當前索引後的路由歷史
this.routeHistory.push(this.currentUrl); // 添加當前路徑
this.currentIndex++; // 當前索引自增
}
let path = getPath(),
currentComponentName = '',
nodeList = document.querySelectorAll('[data-component-name]');
// 找出當前路由的名稱
for (let i = 0; i < this._routes.length; i++) {
if (this._routes[i].path === path) {
currentComponentName = this._routes[i].name;
break;
}
}
// 根據當前路由的名稱顯示對應的組件
nodeList.forEach(item => {
if (item.dataset.componentName === currentComponentName) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
}
// 獲取路徑
function getPath() {
let href = window.location.href;
const index = href.indexOf('#');
// empty path
if (index < 0) return '';
href = href.slice(index + 1);
const searchIndex = href.indexOf('?');
if (searchIndex < 0) return href;
else {
return href.slice(0, searchIndex);
}
}
複製代碼
back() front()
都是經過修改當前路由索引和 hash,從而觸發 hashchange 事件調用 refresh 方法
back() {
if (this.currentIndex > 0) {
this.frontOrBack = true; // 在refresh中會重置爲false
this.currentIndex--; // 修改索引
this.currentUrl = this.routeHistory[this.currentIndex]; // 修改當前url
window.location.hash = this.currentUrl; // 修改實際hash
}
}
front() {
const historyLength = this.routeHistory.length;
if (this.currentIndex < historyLength - 1) {
this.frontOrBack = true;
this.currentIndex++;
this.currentUrl = this.routeHistory[this.currentIndex];
window.location.hash = this.currentUrl;
}
}
複製代碼
在vue-router中,能夠經過 path, name 修改當前路由,而且能夠攜帶 query 參數
所以優先判斷 path, 若是有path,則直接修改 hash; 沒有 path, 則根據 name 從 routes 中找出 path, 再修改 hash
push(option) {
if (option.path) {
changeHash(option.path, option.query);
} else if (option.name) {
let path = '';
// 根據路由名稱找路由path
for (let i = 0; i < this._routes.length; i++) {
if (this._routes[i].name === option.name) {
path = this._routes[i].path;
break;
}
}
if (!path) {
error('組件名稱不存在');
} else {
changeHash(path, option.query);
}
}
}
// 報錯
function error(message) {
typeof console !== 'undefined' && console.error(`[html-router] ${message}`);
}
// 根據path和query修改hash
function changeHash(path, query) {
if (query) {
let str = '';
for (let i in query) {
str += '&' + i + '=' + query[i];
}
(str && (window.location.hash = path + '?' + str.slice(1))) ||
(window.location.hash = path);
} else {
window.location.hash = path;
}
}
複製代碼
其實 replace 和 push 很類似,參數也一致,惟一不一樣的是 replace 是替換當前路徑,並且不會往 routerHistory 添加新的歷史。能夠通用 push 方法,經過 this.replaceRouter
聲明當前爲"替換路徑"
replace(option) {
this.replaceRouter = true;
this.push(option);
}
複製代碼
所以在 refresh 方法中,咱們也須要對 this.replaceRouter = true
這種狀態進行單獨處理
refresh() {
if (this.frontOrBack) {
// 前進後退形成的路由變化,此時不須要改變routeHistory的數據
this.frontOrBack = false;
} else {
this.currentUrl = location.hash.slice(1) || '/';
// 當前爲replace狀態
if (this.replaceRouter) {
this.routeHistory[this.currentIndex] = this.currentUrl;
this.replaceRouter = false; // 重置replaceRouter
} else {
this.routeHistory.push(this.currentUrl);
this.currentIndex++;
}
this.routeHistory = this.routeHistory.slice(
0,
this.currentIndex + 1
);
}
let path = getPath(),
currentComponentName = '',
nodeList = document.querySelectorAll('[data-component-name]');
// 找出當前路由的名稱
for (let i = 0; i < this._routes.length; i++) {
if (this._routes[i].path === path) {
currentComponentName = this._routes[i].name;
break;
}
}
// 根據當前路由的名稱顯示對應的組件
nodeList.forEach(item => {
if (item.dataset.componentName === currentComponentName) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
}
複製代碼
import Router from './htmlRouter-dev'
window.router = new Router({
mode: 'hash',
routes: [
{
path: '/monday',
name: 'monday',
},
{
path: '/tuesday',
name: 'tuesday',
},
{
path: '/wednesday',
name: 'wednesday',
},
{
path: '/thursday',
name: 'thursday',
},
{
path: '/friday',
name: 'friday',
},
],
});
複製代碼
效果以下:
以上即是hash路由的實現,關於history路由的實現,我會在下篇文章中詳細介紹,敬請期待
Canvas 進階(二)寫一個生成帶logo的二維碼npm插件
Canvas 進階(三)ts + canvas 重寫」辨色「小遊戲