Hash 模式
Hash 模式路由本质是利用了 HTML 的锚点不跳转的特性,使用 hashchange 事件监听锚点的变换实现路由的切换行为。
公共的部分:
先来写一个 HTML 模板:
1 2 3 4 5 6 7 8 9 10
| <nav> <ul id="nav"> <li onclick="route.push('/')">index</li> <li onclick="route.push('/page1')">page1</li> <li onclick="route.push('/page2')">page2</li> </ul> </nav> <article> <div id="view"></div> </article>
|
然后写一下 config 的配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| const view = document.querySelector("#view"); const config = { mode: "history", routes: [ { path: "/", template: "<div>index<div>", }, { path: "/page1", template: "<div>page1<div>", }, { path: "/page2", template: "<div>page2<div>", }, ], view, };
|
写一下构造函数:
1 2 3 4 5 6 7 8 9 10 11
| function Router(config) { if (!(this instanceof Router)) { return new Router(config); } this.mode = config.mode; this.routes = {}; this.current = ""; this.view = config.view; this.initRoutes(); this.listenAddressBar(); }
|
对于 initRoutes 我们需要绑定路由地址和相应的模板处理的映射,这里只是把简单的指定的模板挂载到进指定的挂载点:
1 2 3 4 5 6 7
| Router.prototype.initRoutes = function () { for (const _i of config.routes) { this.routes[_i.path] = () => { this.view.innerHTML = _i.template; }; } };
|
listenAddressBar 需要监听 load 事件和 hashchange 事件,用来触发 hash 路由的处理:
1 2 3 4
| Router.prototype.listenAddressBar = function () { window.addEventListener("load", this.handleRoute.bind(this)); window.addEventListener("hashchange", this.handleRoute.bind(this)); };
|
定义 handleRoute 来处理事件的回调:
1 2 3 4 5 6 7 8
| Router.prototype.handleRoute = function () { this.current = window.location.hash.slice(1) || "/"; try { this.routes[this.current](); } catch (err) { throw TypeError(`unknown mode: ${this.mode}`); } };
|
提供一个 push 方法用来做跳转:
1 2 3
| Router.prototype.push = function (to) { location.href = "#" + to; };
|
hash 模式完。
History 模式
history 模式有一个问题需要解决一下:
正常使用 Restful 路由做请求会发生跳转,也就是我们需要阻止请求到服务端。vue-router 关于 history 模式的文档中提到了 history.pushState,MDN 一搜,这个函数可以改变地址栏的信息同时,不发生跳转,可以完美解决这个问题。
可是这里又引入一个新问题,如果当前路由使用 F5 刷新一定会向后端发送一次请求,而后端一定不存在这个 Controller,这种情况该怎么办?
其实作为 SPA 应用,只需要要求每次服务端请求,不理会路由信息,返回同样的 HTML 页面就可以了,vue-router 也提示了,这种方式需要自己实现 404 页面,因为页面如果不存在,服务端依然会返回同一单页 HTML。
我们用 node 模拟一下 Web 服务器处理:
1 2 3 4 5 6 7 8 9 10 11 12
| const { readFileSync } = require("fs"); var http = require("http");
http .createServer(async (request, response) => { const page = readFileSync("./index.html"); response.writeHead(200, { "Content-Type": "text/html" }); response.end(page); }) .listen(8081);
console.log("Server running at http://127.0.0.1:8081/");
|
每次请求都返回 index.html,防止出现后端的 404 错误。
然后我们实现一下 Restful 请求。
对于 history 模式,只需要对 handleRoute,listenAddressBar 与 push 方法做一下扩展即可:
push:
1 2 3 4 5 6 7 8 9 10 11 12 13
| Router.prototype.push = function (to) { switch (this.mode) { case "hash": location.href = "#" + to; break; case "history": history.pushState(null, "", this.current); this.handleRoute(); break; default: throw TypeError(`unknown mode: ${this.mode}`); } };
|
handleRoute:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| Router.prototype.handleRoute = function () { switch (this.mode) { case "hash": this.current = window.location.hash.slice(1) || "/"; break; case "history": this.current = window.location.pathname; history.pushState(null, "", this.current); break; default: return; } try { this.routes[this.current](); } catch (err) { throw TypeError(`unknown mode: ${this.mode}`); } };
|
listenAddressBar:
1 2 3 4 5 6 7 8 9 10 11
| Router.prototype.listenAddressBar = function () { window.addEventListener("load", this.handleRoute.bind(this)); switch (this.mode) { case "hash": window.addEventListener("hashchange", this.handleRoute.bind(this)); case "history": window.addEventListener("popstate", this.handleRoute.bind(this)); default: throw new Error(`unknown mode: ${this.mode}`); } };
|
这么写有大量重复代码,且耦合性过高,难扩展。可以优化一下,做个继承然后用工厂模式按需创建对象即可,本文重点在于解释前端路由原理,这么写可以直观的对比两种路由的区别,此处也就不做优化了。
下面贴出完整 JS 代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
| const view = document.querySelector("#view"); const config = { mode: "hash", routes: [ { path: "/", template: "<div>index<div>", }, { path: "/page1", template: "<div>page1<div>", }, { path: "/page2", template: "<div>page2<div>", }, ], view, };
function Router(config) { if (!(this instanceof Router)) { return new Router(config); } this.mode = config.mode; this.routes = {}; this.current = ""; this.view = config.view; this.initRoutes(); this.listenAddressBar(); }
Router.prototype.initRoutes = function () { for (const _i of config.routes) { this.routes[_i.path] = () => { this.view.innerHTML = _i.template; }; } };
Router.prototype.listenAddressBar = function () { window.addEventListener("load", this.handleRoute.bind(this)); switch (this.mode) { case "hash": window.addEventListener("hashchange", this.handleRoute.bind(this)); case "history": window.addEventListener("popstate", this.handleRoute.bind(this)); default: throw new Error(`unknown mode: ${this.mode}`); } };
Router.prototype.handleRoute = function () { switch (this.mode) { case "hash": this.current = window.location.hash.slice(1) || "/"; break; case "history": this.current = window.location.pathname; history.pushState(null, "", this.current); break; default: return; } try { this.routes[this.current](); } catch (err) { throw new Error(`unknown mode: ${this.mode}`); } };
Router.prototype.push = function (to) { switch (this.mode) { case "hash": location.href = "#" + to; break; case "history": history.pushState(null, "", this.current); this.handleRoute(); break; default: throw Error(`unknown mode: ${this.mode}`); } };
const route = new Router(config);
|
https://gitee.com/kilicmu/analog_front_end_route可以获取文章代码
以上~