我正在创建一个没有框架/工具/库的 Web 应用程序,全部是 Vanilla JS。我更多的是采用“React”风格。
我想调用视图/pages/dashboard.js 中的视图,显示该视图并在用户单击仪表板导航链接时更改 URL。这是导航栏:https://codepen.io/Aurelian/pen/EGJvZW。
也许将子导航项集成到路线中会很好。如果用户位于个人资料上的 GitHub 文件夹中,我将如何在 URL 中显示它?
如何为此创建路由?
GitHub 存储库是 https://github.com/AurelianSpodarec/JS_GitHub_Replica/tree/master/src/js
这是我尝试过的:
document.addEventListener("DOMContentLoaded", function() {
var Router = function (name, routes) {
return {
name: name,
routes: routes
}
};
var view = document.getElementsByClassName('main-container');
var myRouter = new Router('myRouter', [
{
path: '/',
name: "Dahsboard"
},
{
path: '/todo',
name: "To-Do"
},
{
path: '/calendar',
name: "Calendar"
}
]);
var currentPath = window.location.pathname;
if (currentPath === '/') {
view.innerHTML = "You are on the Dashboard";
console.log(view);
} else {
view.innerHTML = "you are not";
}
});
制作香草 SPA 至少有两种基本方法。
window.onhashchange
添加一个侦听器(或侦听 hashchange 事件),每当 URL 中的哈希值从 https://www.example.com/#/foo
更改为 https://www.example.com/#/bar
时,该侦听器就会触发。您可以解析 window.location.hash
字符串来确定路由并注入相关内容。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
</head>
<body>
<div id="app"></div>
<script>
const nav = `<a href="/#/">Home</a> |
<a href="/#/about">About</a> |
<a href="/#/contact">Contact</a>`;
const routes = {
"": `<h1>Home</h1>${nav}<p>Welcome home!</p>`,
about: `<h1>About</h1>${nav}<p>This is a tiny SPA</p>`,
};
const render = (path) => {
document.querySelector("#app").innerHTML =
routes[path.replace(/^#\//, "")] ?? `<h1>404</h1>${nav}`;
};
window.onhashchange = (evt) => render(window.location.hash);
render(window.location.hash);
</script>
</body>
</html>
现代方法使用 History API,这对用户来说更自然,因为 URL 中不涉及哈希字符。
我使用的策略是为所有同域链接点击添加事件监听器。侦听器使用目标 URL 调用
window.history.pushState
。
popstate
事件捕获,该事件解析 window.location.href
以调用正确的路线。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
</head>
<body>
<div id="app"></div>
<script>
const nav = `<a href="/">Home</a> |
<a href="/about">About</a> |
<a href="/contact">Contact</a>`;
const routes = {
"/": `<h1>Home</h1>${nav}<p>Welcome home!</p>`,
"/about": `<h1>About</h1>${nav}<p>This is a tiny SPA</p>`,
};
const render = path => {
document.querySelector("#app")
.innerHTML = routes[path] ?? `<h1>404</h1>${nav}`;
document.querySelectorAll('#app [href^="/"]').forEach(el =>
el.addEventListener("click", evt => {
evt.preventDefault();
const {pathname: path} = new URL(evt.target.href);
window.history.pushState({path}, path, path);
render(path);
})
);
};
window.addEventListener("popstate", e =>
render(new URL(window.location.href).pathname)
);
render("/");
</script>
</body>
</html>
以上示例已尽可能少。我在 Glitch 上有一个功能更全面的概念证明,它添加了基于组件的系统和模块。
route-parser
包可以节省一些轮子的改造。
顺便说一句,有一个技巧可以在不使用 JS 的情况下制作基于哈希的 SPA,即使用
:target
CSS 伪选择器在重叠的全屏部分上切换 display: none
和 display: block
,如 A Whole Website in a 中所述。单个 HTML 文件 和 https://john-doe.neocities.org.
html {
height: 100%;
}
body {
margin: 0;
height: 100%;
}
section {
padding: 1em;
padding-top: 2em;
display: none;
position: absolute;
width: 100%;
height: 100%;
background: #fff;
}
nav {
padding: 1em;
position: absolute;
z-index: 99;
}
section:target {
display: block;
}
#home {
display: block;
}
<nav>
<a href="#">Home</a> |
<a href="#about">About</a> |
<a href="#contact">Contact</a>
</nav>
<section id="home">
<h1>Home</h1>
<p>Welcome home!</p>
</section>
<section id="about">
<h1>About</h1>
<p>This is a tiny SPA</p>
</section>
<section id="contact">
<h1>Contact</h1>
<p>Contact page</p>
</section>
正如我在评论中所说,监听
popstate
并使用主题标签 (#
) 方法是在 JS 中进行路由的最简单方法。
这是路由器最简单的骨架:
//App area
var appArea = document.body.appendChild(document.createElement("div"));
//Registered routes
var routes = [
{
url: '', callback: function () {
appArea.innerHTML = "<h1>Home</h1><a href=\"#todo\">To-Do</a><br/><a href=\"#calendar\">Calendar</a>";
}
}
];
//Routing function
function Routing() {
var hash = window.location.hash.substr(1).replace(/\//ig, '/');
//Default route is first registered route
var route = routes[0];
//Find matching route
for (var index = 0; index < routes.length; index++) {
var testRoute = routes[index];
if (hash == testRoute.url) {
route = testRoute;
}
}
//Fire route
route.callback();
}
//Listener
window.addEventListener('popstate', Routing);
//Initial call
setTimeout(Routing, 0);
//Add other routes
routes.push({ url: "todo", callback: function () { appArea.innerHTML = "<h1>To-Do</h1><a href=\"#\">Home</a><br/><a href=\"#calendar\">Calendar</a>"; } });
routes.push({ url: "calendar", callback: function () { appArea.innerHTML = "<h1>Calendar</h1><a href=\"#\">Home</a></br><a href=\"#todo\">To-Do</a>"; } });
现在在任何真实的上下文中,您都需要可重用的 DOM 元素和作用域卸载函数,因此上面的内容可能应该是这样的:
// ## Class ## //
var Router = /** @class */ (function () {
function Router() {
}
//Initializer function. Call this to change listening for window changes.
Router.init = function () {
//Remove previous event listener if set
if (this.listener !== null) {
window.removeEventListener('popstate', this.listener);
this.listener = null;
}
//Set new listener for "popstate"
this.listener = window.addEventListener('popstate', function () {
//Callback to Route checker on window state change
this.checkRoute.call(this);
}.bind(this));
//Call initial routing as soon as thread is available
setTimeout(function () {
this.checkRoute.call(this);
}.bind(this), 0);
return this;
};
//Adding a route to the list
Router.addRoute = function (name, url, cb) {
var route = this.routes.find(function (r) { return r.name === name; });
url = url.replace(/\//ig, '/');
if (route === void 0) {
this.routes.push({
callback: cb,
name: name.toString().toLowerCase(),
url: url
});
}
else {
route.callback = cb;
route.url = url;
}
return this;
};
//Adding multiple routes to list
Router.addRoutes = function (routes) {
var _this = this;
if (routes === void 0) { routes = []; }
routes
.forEach(function (route) {
_this.addRoute(route.name, route.url, route.callback);
});
return this;
};
//Removing a route from the list by route name
Router.removeRoute = function (name) {
name = name.toString().toLowerCase();
this.routes = this.routes
.filter(function (route) {
return route.name != name;
});
return this;
};
//Check which route to activate
Router.checkRoute = function () {
//Get hash
var hash = window.location.hash.substr(1).replace(/\//ig, '/');
//Default to first registered route. This should probably be your 404 page.
var route = this.routes[0];
//Check each route
for (var routeIndex = 0; routeIndex < this.routes.length; routeIndex++) {
var routeToTest = this.routes[routeIndex];
if (routeToTest.url == hash) {
route = routeToTest;
break;
}
}
//Run all destroy tasks
this.scopeDestroyTasks
.forEach(function (task) {
task();
});
//Reset destroy task list
this.scopeDestroyTasks = [];
//Fire route callback
route.callback.call(window);
};
//Register scope destroy tasks
Router.onScopeDestroy = function (cb) {
this.scopeDestroyTasks.push(cb);
return this;
};
//Tasks to perform when view changes
Router.scopeDestroyTasks = [];
//Registered Routes
Router.routes = [];
//Listener handle for window events
Router.listener = null;
Router.scopeDestroyTaskID = 0;
return Router;
}());
// ## Implementation ## //
//Router area
var appArea = document.body.appendChild(document.createElement("div"));
//Start router when content is loaded
document.addEventListener("DOMContentLoaded", function () {
Router.init();
});
//Add dashboard route
Router.addRoute("dashboard", "", (function dashboardController() {
//Scope specific elements
var header = document.createElement("h1");
header.textContent = "Dashboard";
//Return initializer function
return function initialize() {
//Apply route
appArea.appendChild(header);
//Destroy elements on exit
Router.onScopeDestroy(dashboardExitController);
};
//Unloading function
function dashboardExitController() {
appArea.removeChild(header);
}
})());
//Add dashboard route
Router.addRoute("dashboard", "", (function dashboardController() {
//Scope specific elements
var header = document.createElement("h1");
header.textContent = "Dashboard";
var links = document.createElement("ol");
links.innerHTML = "<li><a href=\"#todo\">To-Do</a></li><li><a href=\"#calendar\">Calendar</a></li>";
//Return initializer function
return function initialize() {
//Apply route
appArea.appendChild(header);
appArea.appendChild(links);
//Destroy elements on exit
Router.onScopeDestroy(dashboardExitController);
};
//Unloading function
function dashboardExitController() {
appArea.removeChild(header);
appArea.removeChild(links);
}
})());
//Add other routes
Router.addRoutes([
{
name: "todo",
url: "todo",
callback: (function todoController() {
//Scope specific elements
var header = document.createElement("h1");
header.textContent = "To-do";
var links = document.createElement("ol");
links.innerHTML = "<li><a href=\"#\">Dashboard</a></li><li><a href=\"#calendar\">Calendar</a></li>";
//Return initializer function
return function initialize() {
//Apply route
appArea.appendChild(header);
appArea.appendChild(links);
//Destroy elements on exit
Router.onScopeDestroy(todoExitController);
};
//Unloading function
function todoExitController() {
appArea.removeChild(header);
appArea.removeChild(links);
}
})()
},
{
name: "calendar",
url: "calendar",
callback: (function calendarController() {
//Scope specific elements
var header = document.createElement("h1");
header.textContent = "Calendar";
var links = document.createElement("ol");
links.innerHTML = "<li><a href=\"#\">Dashboard</a></li><li><a href=\"#todo\">To-Do</a></li>";
//Return initializer function
return function initialize() {
//Apply route
appArea.appendChild(header);
appArea.appendChild(links);
//Destroy elements on exit
Router.onScopeDestroy(calendarExitController);
};
//Unloading function
function calendarExitController() {
appArea.removeChild(header);
appArea.removeChild(links);
}
})()
}
]);
您可以使用navigo或通过看看其他人在做什么来进行头脑风暴。
远离 React/Angular 的另一个选择是使用 sapper,您可以从 there 进行真正具有启发性的框架比较。
在我看来,路由器应该是通用的,不仅可以显示/隐藏应用程序的现有部分,还可以向服务器发送请求并接收用于页面包含的ajax响应;这样,对
/eshop/phones/samsung
的请求应该发出 ajax 请求,并在像 <div id="eshop">
这样的节点处包含 html 代码。这样我们就需要:
1) 一个 url 处理程序,可阻止所有
clicks
并重新格式化浏览器路径和
2)回调,如何处理
仅此而已!
SEO是通过将完全相同的url与实际缓存页面映射来实现的;此类 url 是路由器处理的子集,因为某些(如上面的)会生成动态构建的页面。
从网络机器人动态构建页面的角度来看,需要js代码才能运行,路由器+支持代码(机器人可以运行js,但即使如此,路径也只是从参数转换为路径构建的数千个路径之一,因此无法用于索引,但应该可用于书签!)。
现在你在路由器之上有了一个 SEO + 书签功能,这很难从 Angular 中获得(这太复杂了,当一个应用程序完成后,你根本不知道如何将它重用到另一个项目中!)。
最终,这样的路由器会镜像一个服务器,用于缓存页面的 url + 动态构建页面部分的 url,它是最小的,并且结合了两个世界:SPA 和服务器呈现的页面。