无需框架即可构建单页应用程序🔥single-page-app-vanilla-js 反应式 CSS 属性

2025-05-24

无需框架即可构建单页应用程序🔥

单页应用程序-vanilla-js

响应式 CSS 属性

在今天的帖子中,我们将使用原始 JavaScript 构建单页应用程序 - 这意味着我们不需要使用任何框架!

框架很棒,在很多情况下你可能想要使用一个框架,但对于那些较小的项目,或者如果你只是想要更多的控制权,从头开始自己做可能是一个可行的选择👀

我们要创造什么?

这将是一个相当简单的单页应用程序,其特点是右侧有一个导航栏和“视图”部分,当单击导航项时,它们会发生变化。

单页应用程序的 GIF

视频教程

首先,像往常一样,如果您希望以视频形式观看本教程,请随意在下面查看。

跟随源代码

我建议在学习本教程时克隆存储库或简单地查看源代码。

单页应用程序-vanilla-js

摘自我的 YouTube 教程:https://www.youtube.com/watch?v= 6BozpmSjk-Y

快速试驾

对完成的代码进行快速测试:

  1. 安装 Node.js
  2. 导航到项目文件夹并从终端运行以下命令:
    • npm init -y(创建 Node.js 项目)
    • npm i express(安装 Express)
    • node server.js(运行服务器)
  3. 在 Web 浏览器中打开,使用例如http://localhost:3000/localhost中指定的端口server.js

请参阅YouTube 教程,了解创建代码的分步说明。尽情享受吧!:)




创建Web服务器

我们将使用 Express 作为我们的 Web 服务器,因此让我们首先安装依赖项并创建目录结构。



npm init -y
npm i express
mkdir -p frontend/static


接下来,我们可以创建一个server.js文件并包含以下内容。



const express = require("express");
const path = require("path");

const app = express();

/* Ensure any requests prefixed with /static will serve our "frontend/static" directory */
app.use("/static", express.static(path.resolve(__dirname, "frontend", "static")));

/* Redirect all routes to our (soon to exist) "index.html" file */
app.get("/*", (req, res) => {
    res.sendFile(path.resolve("frontend", "index.html"));
});

app.listen(process.env.PORT || 3000, () => console.log("Server running..."));


此后,在目录index.html中创建一个文件frontend并启动服务器:



node server.js


导航到http://localhost:3000现在应该显示您的 HTML 文件。

编写 HTML

对于内的标记index.html,我们可以包括:

  • 我们即将推出的 CSS 样式表
  • 我们即将推出的 JavaScript 模块
  • 导航菜单
  • 应用程序容器“html”

<!DOCTYPE html>




单页应用程序(Vanilla JS)




仪表板
帖子
设置




> **Note:** the `data-link` attributes on our `<a>` tags - any links marked with this attribute will use the History API to enable changes to the view (#app) without a page refresh. We'll learn more about this shortly.

> **Also note:** the #app div is used as the container for each view (Dashboard, Posts etc.) which we'll be learning more about a bit later on.

## Adding the CSS
We may as well get the CSS over and done with so we have something pretty to look at - let's make a new file within `frontend/static` named `main.css`.
```css


body {
    --nav-width: 200px;
    margin: 0 0 0 var(--nav-width);
    font-family: 'Quicksand', sans-serif;
    font-size: 18px;
}

/* Creates a full-height, left-mounted navigation menu */
.nav {
    position: fixed;
    top: 0;
    left: 0;
    width: var(--nav-width);
    height: 100vh;
    background: #222222;
}

/* Making these look fantastic */
.nav__link {
    display: block;
    padding: 12px 18px;
    text-decoration: none;
    color: #eeeeee;
    font-weight: 500;
}

.nav__link:hover {
    background: rgba(255, 255, 255, 0.05);
}

#app {
    margin: 2em;
    line-height: 1.5;
    font-weight: 500;
}

/* The 'dcode' green always needs to make an appearance */
a {
    color: #009579;
}


由于 CSS 不是本教程的重点,因此我不会详细介绍这些样式的作用 - 而且大多数样式都是不言自明的😁

转向 JavaScript

让我们在static/jsnamed中创建一个新文件index.js。这将是客户端 JavaScript 的主入口点,并将包含路由器的代码。

支持客户端 URL 参数

首先,我们需要编写一个函数来处理客户端 URL 参数。例如,如果我想为 定义一个路由/posts/:id,我希望能够在代码中访问帖子 ID。

由于我们将使用正则表达式进行匹配,因此让我们编写一个函数将我们的/posts/:id路由转换为正则表达式模式:



const pathToRegex = path => new RegExp("^" + path.replace(/\//g, "\\/").replace(/:\w+/g, "(.+)") + "$");


现在,调用pathToRegex("/posts/:id")将返回/^\/posts\/(.+)$/。我们现在可以使用捕获组来获取路由器中的 Post ID 值。

编写路由器

让我们创建另一个函数router- 该函数将在页面加载时、点击链接时以及导航改变时调用。



const router = async () => {
    const routes = [
        { path: "/" },
        { path: "/posts" },
        { path: "/posts/:id" },
        { path: "/settings" }
    ];
}


很快,我们将以 JavaScript 类的形式在每条路线中添加对“视图”的引用。

不过现在,让我们编写一些代码来将路由与当前的 URL 路径匹配。



const potentialMatches = routes.map(route => {
    return {
        route,
        result: location.pathname.match(pathToRegex(route.path))
    };
});


如您所见,我们只是map为每条路线提供一个函数,并返回一个额外的字段result- 这将包含与我们的路线匹配时的正则表达式结果location.pathname

接下来,让我们找出哪条路线匹配,如果没有匹配,则提供默认(未找到)路线。



let match = potentialMatches.find(potentialMatch => potentialMatch.result !== null);

/* Route not found - return first route OR a specific "not-found" route */
if (!match) {
    match = {
        route: routes[0],
        result: [location.pathname]
    };
}


如您所见,我们只是找到了第一个具有正则表达式结果的路线。

如果未找到,我们只是“模拟”了第一条路由。您可以在此处添加您自己的“未找到”路由。

最后,我们可以注销匹配的路由。很快,我们将根据匹配的路由在 #app 中添加一些内容。



console.log(match);


把所有东西联系在一起

在我们继续创建视图并完成之前router,我们应该编写一些代码将所有这些联系在一起。

让我们首先定义一个使用 History API 导航到给定路径的函数。



const navigateTo = url => {
    history.pushState(null, null, url);
    router();
};


接下来,我们可以启用所有带有该data-link属性的链接来使用此功能。此外,我们还可以在文档加载时运行路由器。



document.addEventListener("DOMContentLoaded", () => {
    document.body.addEventListener("click", e => {
        if (e.target.matches("[data-link]")) {
            e.preventDefault();
            navigateTo(e.target.href);
        }
    });

    /* Document has loaded -  run the router! */
    router();
});


当用户使用后退和前进按钮导航时,我们还希望运行路由器。



window.addEventListener("popstate", router);


完成所有这些后,您现在应该能够跳入浏览器并尝试单击其中一个导航链接。

点击链接后,注意 URL 是如何根据每个链接变化的,且无需刷新页面。另外,检查一下控制台,看看你的匹配结果应该都在那里 😁

与日志匹配的预览

解析客户端 URL 参数

在开始编写每个视图的代码之前,我们需要一种方法来解析客户端 URL 参数。让我们定义一个函数来实现这一点。



const getParams = match => {
    const values = match.result.slice(1);
    const keys = Array.from(match.route.path.matchAll(/:(\w+)/g)).map(result => result[1]);

    return Object.fromEntries(keys.map((key, i) => {
        return [key, values[i]];
    }));
};


potentialMatches此函数将接受一个“匹配” -与我们通过上述方法找到的相同find

一旦匹配成功,它将获取所有匹配的捕获组,从索引 1 到末尾。如果/posts/:id/:anotherParam/posts/2/dcode, 的值values将为["2", "dcode"]

就 而言keys,这将使用正则表达式来抓取:路径中每个以 为前缀的标识符。因此,它将获取/posts/:id/:anotherParam并返回["id", "anotherParam"]

values最后,我们取和的结果keys,并将它们粘在一起,Object.entries这将给我们一个返回值,例如



{
    "id": "2",
    "anotherParam": "dcode"
}


我们现在可以开始为每个视图编写代码 - 不过之后,我们可以利用getParams路由器内部的代码。

编写视图

每个“视图”都将由 中的 JavaScript 类表示frontend/static/js/views。我们可以首先定义一个抽象类,每个视图都将继承该类。



// frontend/static/js/views/AbstractView.js
export default class {
    constructor(params) {
        this.params = params;
    }

    setTitle(title) {
        document.title = title;
    }

    async getHtml() {
        return "";
    }
}


这非常简单 - 我们将把每个视图的参数存储为实例属性,并提供一种设置页面标题的便捷方法。

但最值得注意的是,我们有这个async getHtml方法——这个方法将由每个视图实现,并将为它们返回 HTML。

让我们编写仪表板视图的代码。



// frontend/static/js/views/Dashboard.js
import AbstractView from "./AbstractView.js";

export default class extends AbstractView {
    constructor(params) {
        super(params);
        this.setTitle("Dashboard");
    }

    async getHtml() {
        return `
            <h1>Welcome back, Dom</h1>
            <p>Hi there, this is your Dashboard.</p>
            <p>
                <a href="/posts" data-link>View recent posts</a>.
            </p>
        `;
    }
}


如您所见,我们只是扩展了AbstractView并调用了一个方法来设置页面标题。您还可以通过 找到返回的仪表板的 HTML getHtml

您可以根据需要随意创建任意数量的视图。

注意:如果您的路线有参数,您可以在视图中使用引用它们this.params.your-param-here,例如,如果您有一个ViewPost视图,则可以通过执行获取帖子 ID this.params.id

回到路由器

现在我们已经有了观点,让我们对index.js文件做一些细微的调整。

让我们导入我们的观点。



import Dashboard from "./views/Dashboard.js";
import Posts from "./views/Posts.js";
import PostView from "./views/PostView.js";
import Settings from "./views/Settings.js";


现在,我们可以在函数内的路由中引用它们router



const routes = [
    { path: "/", view: Dashboard },
    { path: "/posts", view: Posts },
    { path: "/posts/:id", view: PostView },
    { path: "/settings", view: Settings }
];


最后,我们可以创建匹配视图的新实例,并将 #app 容器的 HTML 设置为该视图提供的 HTML。



const view = new match.route.view(getParams(match));
document.querySelector("#app").innerHTML = await view.getHtml();


注意:我们在这里使用await,因为该getHtml函数可能需要从服务器端执行数据或 HTML 请求。

就这样!你应该有一个功能齐全的单页应用程序了。欢迎在下方提出任何建议😁

文章来源:https://dev.to/dcodeyt/building-a-single-page-app-without-frameworks-hl9
PREV
表格 CSS 使用 CSS 创建漂亮的 HTML 表格
NEXT
使用 CSS 为你的网站添加暗黑模式