如何为 SPA 创建一个 vanilla JS 路由?

Lui*_*igi 5 javascript routes single-page-application ecmascript-6

我正在创建一个没有框架/工具/库的 Web 应用程序,所有的都是 Vanilla JS。我正在以更多的“反应”风格来做这件事。

我想调用我的 views/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";
    }
});
Run Code Online (Sandbox Code Playgroud)

ggo*_*len 11

制作香草 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>
Run Code Online (Sandbox Code Playgroud)

历史API

现代方法使用History API,这对用户来说更自然,因为 URL 中不涉及哈希字符。

我使用的策略是为所有同域链接点击添加事件侦听器。window.history.pushState侦听器使用目标 URL进行调用。

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('[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>
Run Code Online (Sandbox Code Playgroud)

上面的例子是尽可能少的。我在Glitch上有一个功能更全面的概念证明,它添加了基于组件的系统和模块。

如果您想处理更复杂的路线,该route-parser软件包可以节省一些轮子改造。

没有JS

顺便说一句,有一个技巧可以在没有 JS 的情况下制作基于哈希的 SPA,使用:targetCSS 伪选择器来切换display: nonedisplay: block重叠的全屏部分,如单个 HTML 文件中的整个网站https://john-doe中所述.neocities.org

<!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>
Run Code Online (Sandbox Code Playgroud)
<!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('[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>
Run Code Online (Sandbox Code Playgroud)


Emi*_*sen 4

正如我在评论中所说,监听popstate并使用 hashtag( #) 方法是在 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>"; } });
Run Code Online (Sandbox Code Playgroud)

现在,在任何真实的上下文中,您都需要可重用的 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);
            }
        })()
    }
]);
Run Code Online (Sandbox Code Playgroud)