实战|使用 Node.js 和 htmx 构建全栈应用程序

在本教程中,我将演示如何使用 Node 作为后端和 htmx 作为前端来构建功能齐全的 CRUD 应用程序。这将演示 htmx 如何集成到全栈应用程序中,使您能够评估其有效性并确定它是否是您未来项目的不错选择。

htmx 是一个现代 JavaScript 库,旨在通过实现部分 HTML 更新来增强Web应用,而无需重新加载整个页面。与传统前端框架中的 JSON 有效载荷不同,它通过有线方式发送 HTML 来实现这一功能。

我们将要构建什么

我们将开发一个简单的联系人管理器,能够执行所有 CRUD 操作:创建、读取、更新和删除联系人。通过利用 htmx,该应用程序将提供单页应用程序 (SPA) 的感觉,从而增强交互性和用户体验。

如果用户禁用 JavaScript,应用程序将以整页刷新的方式运行,从而保持可用性和可发现性。这种方法展示了 htmx 创建现代 Web 应用程序的能力,同时保持它们的可访问性和 SEO 友好性。

这就是我们最终得到的结果。

本文的代码可以在随附的 GitHub 存储库中找到。

先决条件

要学习本教程,您需要在 PC 上安装 Node.js。如果您尚未安装 Node,请前往官方 Node 下载页面并获取适合您系统的正确二进制文件。或者,您可能想使用版本管理器安装 Node。这种方法允许您安装多个 Node 版本并在它们之间随意切换。

除此之外,熟悉 Node、Pug(我们将使用它们作为模板引擎)和 htmx 会有所帮助,但不是必需的。如果您想复习以上任何内容,请查看我们的教程:使用 Node 构建简单的初学者应用程序、Pug HTML 模板预处理器指南和 htmx 简介。

在开始之前,请运行以下命令:

node -v
npm -v

您应该看到如下输出:

v20.11.1
10.4.0

这确认了 Node 和 npm 已安装在您的计算机上,并且可以从命令行环境进行访问。

设置项目

让我们从搭建一个新的 Node 项目开始:

mkdir contact-manager
cd contact-manager
npm init -y

这应该在项目根目录中创建一个 package.json 文件。

接下来,让我们安装我们需要的依赖项:

npm i express method-override pug

在这些包中,Express 是我们应用程序的支柱。它是一个快速且简约的 Web 框架,提供了一种简单的方法来处理请求和响应,并将 URL 路由到特定的处理函数。 Pug 将充当我们的模板引擎,而我们将使用方法覆盖在客户端不支持的地方使用 HTTP 动词,例如 PUT 和 DELETE。

接下来,在根目录中创建一个 app.js 文件:

touch app.js

并添加以下内容:

const express = require('express');
const path = require('path');
const routes = require('./routes/index');const app = express();app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');app.use(express.static('public'));
app.use('/', routes);const server = app.listen(3000, () => {console.log(`Express is running on port ${server.address().port}`);
});

在这里,我们正在设置 Express 应用程序的结构。这包括将 Pug 配置为渲染视图的视图引擎、定义静态资产的目录以及连接路由器。

该应用程序侦听端口 3000,并使用控制台日志来确认 Express 正在运行并准备好处理指定端口上的请求。此设置构成了我们应用程序的基础,并准备好通过更多功能和路由进行扩展。

接下来,让我们创建路由文件:

mkdir routes
touch routes/index.js

打开该文件并添加以下内容:

const express = require('express');
const router = express.Router();// GET /contacts
router.get('/contacts', async (req, res) => {res.send('It works!');
});

在这里,我们在新创建的路由目录中设置基本路由。此路由在 /contacts 端点侦听 GET 请求,并使用简单的确认消息进行响应,表明一切正常。

接下来,使用以下内容更新 package.json 文件的“scripts”部分:

"scripts": {"dev": "node --watch app.js"
},

这利用了 Node.js 中的新监视模式,只要检测到任何更改,该模式就会重新启动我们的应用程序。

最后,使用 npm run dev 启动所有内容,然后在浏览器中访问 http://localhost:3000/contacts/。您应该会看到一条消息“It works!”。

激动人心的时刻!

显示所有联系人

现在让我们添加一些要显示的联系人。由于我们专注于 htmx,因此为了简单起见,我们将使用硬编码数组。这将使事情变得精简,使我们能够专注于 htmx 的动态功能,而无需复杂的数据库集成。

对于那些有兴趣稍后添加数据库的人来说,SQLite 和 Sequelize 是不错的选择,它们提供了不需要单独数据库服务器的基于文件的系统。

话虽如此,请将以下内容添加到第一个路由之前的 index.js 中:

const contacts = [{ id: 1, name: 'John Doe', email: 'john.doe@example.com' },{ id: 2, name: 'Jane Smith', email: 'jane.smith@example.com' },{ id: 3, name: 'Emily Johnson', email: 'emily.johnson@example.com' },{ id: 4, name: 'Aarav Patel', email: 'aarav.patel@example.com' },{ id: 5, name: 'Liu Wei', email: 'liu.wei@example.com' },{ id: 6, name: 'Fatima Zahra', email: 'fatima.zahra@example.com' },{ id: 7, name: 'Carlos Hernández', email: 'carlos.hernandez@example.com' },{ id: 8, name: 'Olivia Kim', email: 'olivia.kim@example.com' },{ id: 9, name: 'Kwame Nkrumah', email: 'kwame.nkrumah@example.com' },{ id: 10, name: 'Chen Yu', email: 'chen.yu@example.com' },
];

现在,我们需要为路由创建一个显示模板。创建一个包含 index.pug 文件的 views 文件夹:

mkdir views
touch views/index.pug

并添加以下内容:

doctype html
htmlheadmeta(charset='UTF-8')title Contact Managerlink(rel='preconnect', href='https://fonts.googleapis.com')link(rel='preconnect', href='https://fonts.gstatic.com', crossorigin)link(href='https://fonts.googleapis.com/css2?family=Roboto:wght@300;400&display=swap', rel='stylesheet')link(rel='stylesheet', href='/styles.css')bodyheadera(href='/contacts')h1 Contact Managersection#sidebarul.contact-listeach contact in contactsli #{contact.name}div.actionsa(href='/contacts/new') New Contactmain#contentp Select a contactscript(src='https://unpkg.com/htmx.org@1.9.10')

在此模板中,我们为应用程序布置 HTML 结构。在 head 部分,我们包含了来自 Google Fonts 的 Roboto 字体和自定义样式的样式表。

正文分为标题、用于列出联系人的侧边栏以及用于存放所有联系信息的主要内容区域。内容区域当前包含一个占位符。在正文的末尾,我们还包含来自 CDN 的最新版本的 htmx 库。

该模板期望接收一个联系人数组(在 contacts 变量中),我们在侧边栏中对其进行迭代,并使用 Pug 的插值语法在无序列表中输出每个联系人姓名。

接下来,让我们创建自定义样式表:

mkdir public
touch public/styles.css

我不想在这里列出样式。请从随附的 GitHub 存储库中的 CSS 文件中复制它们,或者随意添加一些您自己的 CSS 文件。 🙂

回到 index.js,更新路由以使用模板:

// GET /contacts
router.get('/contacts', (req, res) => {res.render('index', { contacts });
});

现在,当您刷新页面时,您应该会看到类似这样的内容。

显示单个联系人

到目前为止,我们所做的只是建立了一个基本的 Express 应用程序。让我们改变一下,最后添加 htmx。下一步要做的是,当用户点击侧边栏中的联系人时,该联系人的信息就会显示在主内容区域–自然不需要重新载入整个页面。

首先,让我们将侧边栏移至其自己的模板中:

touch views/sidebar.pug

将以下内容添加到这个新文件中:

ul.contact-listeach contact in contactslia(href=`/contacts/${contact.id}`,hx-get=`/contacts/${contact.id}`,hx-target='#content',hx-push-url='true')= contact.namediv.actionsa(href='/contacts/new') New Contact

这里我们为每个联系人创建了一个指向 /contacts/${contact.id} 的链接,并添加了三个 htmx 属性:

  • hx-get:当用户单击链接时,htmx 将拦截单击并通过 Ajax 向 /contacts/${contact.id} 端点发出 GET 请求。
  • hx-target:当请求完成时,响应将被插入到 ID 为 content 的 div 中。我们在这里没有指定任何类型的交换策略,因此 div 的内容将被 Ajax 请求返回的内容替换。这是默认行为。
  • hx-push-url:这将确保 htx-get 中指定的值被推送到浏览器的历史堆栈中,从而更改 URL。

更新 index.pug 以使用我们的模板:

section#sidebarinclude sidebar.pug

请记住:Pug 对空格敏感,因此请务必使用正确的缩进。

现在让我们在 index.js 中创建一个新端点以返回 htmx 期望的 HTML 响应:

// GET /contacts/1
router.get('/contacts/:id', (req, res) => {const { id } = req.params;const contact = contacts.find((c) => c.id === Number(id));res.send(`<h2>${contact.name}</h2><p><strong>Name:</strong> ${contact.name}</p><p><strong>Email:</strong> ${contact.email}</p>`);
});

如果保存并刷新浏览器,您现在应该能够查看每个联系人的详细信息。

网络上的 HTML

让我们花点时间了解一下这里发生了什么。正如文章开头提到的,htmx 通过网络传输 HTML,而不是传统前端框架的 JSON 有效负载。

如果我们打开浏览器的开发人员工具,切换到“Network”选项卡并单击其中一个联系人,我们就可以看到这一点。收到来自前端的请求后,我们的 Express 应用程序会生成显示该联系人所需的 HTML,并将其发送到浏览器,其中 htmx 将其交换到 UI 中的正确位置。

处理全页刷新

所以事情进展得很顺利,是吧?感谢 htmx,我们通过在锚标记上指定几个属性来使页面动态化。不幸的是,有一个问题……

如果您显示联系人,然后刷新页面,我们可爱的用户界面就会消失,您看到的只是裸露的联系方式详细信息。如果您直接在浏览器中加载 URL,也会发生同样的情况。

如果你仔细想想,其原因是显而易见的。当您访问 http://localhost:3000/contacts/1 之类的 URL 时, '/contacts/:id' 的 Express 路由将启动并返回联系人的 HTML,正如我们告诉它的那样。它对我们用户界面的其余部分一无所知。

为了解决这个问题,我们需要做一些改动。在服务器上,我们需要检查是否存在 HX-Request 标头,它表明请求来自 htmx。如果存在,我们就可以发送部分内容。否则,我们需要发送整个页面。

像这样更改路由处理程序:

// GET /contacts/1
router.get('/contacts/:id', (req, res) => {const { id } = req.params;const contact = contacts.find((c) => c.id === Number(id));if (req.headers['hx-request']) {res.send(`<h2>${contact.name}</h2><p><strong>Name:</strong> ${contact.name}</p><p><strong>Email:</strong> ${contact.email}</p>`);} else {res.render('index', { contacts });}
});

现在,当您重新加载页面时,用户界面不会消失。但是,它确实会从您正在查看的任何联系人恢复为消息“选择联系人”,这并不理想。

为了解决这个问题,我们可以在 index.pug 模板中引入 case 语句:

main#contentcase actionwhen 'show'h2 #{contact.name}p #[strong Name:] #{contact.name}p #[strong Email:] #{contact.email}when 'new'// Coming soonwhen 'edit'// Coming soondefaultp Select a contact

最后更新路由处理程序:

if (req.headers['hx-request']) {// As before
} else {res.render('index', { action: 'show', contacts, contact });
}

请注意,我们现在传入一个 contact 变量,该变量将在整个页面重新加载时使用。

这样,我们的应用程序应该能够承受刷新或直接加载联系人。

快速重构

虽然这样做可行,但您可能会注意到,我们的路由处理程序和主 pug 模板中都有一些重复的内容。这种情况并不理想,只要联系人的属性不超过几个,或者我们需要使用一些逻辑来决定显示哪些属性,事情就会开始变得臃肿。

为了解决这个问题,让我们将联系人移动到自己的模板中:

touch views/contact.pug

在新创建的模板中,添加以下内容:

h2 #{contact.name}p #[strong Name:] #{contact.name}
p #[strong Email:] #{contact.email}

在主模板( index.pug )中:

main#contentcase actionwhen 'show'include contact.pug

还有我们的路由处理程序:

if (req.headers['hx-request']) {res.render('contact', { contact });
} else {res.render('index', { action: 'show', contacts, contact });
}

事情应该仍然像以前一样工作,但现在我们已经删除了重复的代码。

新的联系表

我们要关注的下一个任务是创建新联系人。本教程的这一部分将指导您设置表单和后端逻辑,使用 htmx 动态处理提交。

让我们从更新侧边栏模板开始。更改:

div.actionsa(href='/contacts/new') New Contact

… 到:

div.actionsa(href='/contacts/new',hx-get='/contacts/new',hx-target='#content',hx-push-url='true') New Contact

这将使用与链接相同的 htmx 属性来显示联系人:hx-get 将通过 Ajax 向 /contacts/new 端点发出 GET 请求,hx-target 将指定插入响应的位置,hx-push-url 将确保更改 URL。

现在让我们为表单创建一个新模板:

touch views/form.pug

并添加以下代码:

h2 New Contactform(action='/contacts',method='POST',hx-post='/contacts',hx-target='#sidebar',hx-on::after-request='if(event.detail.successful) this.reset()'
)label(for='name') Name:input#name(type='text', name='name', required)label(for='email') Email:input#email(type='email', name='email', required)div.actionsbutton(type='submit') Submit

在这里,我们使用 hx-post 属性告诉 htmx 拦截表单提交,并向 /contacts 端点发出带有表单数据的 POST 请求。结果(更新的联系人列表)将被插入到侧边栏中。在这种情况下,我们不想更改 URL,因为用户可能想要输入多个新联系人。但是,我们确实希望在成功提交后清空表单,这就是 hx-on::after-request 的作用。 hx-on* 属性允许您内联嵌入脚本以直接响应元素上的事件。你可以在这里读更多关于它的内容。

接下来,我们在 index.js 中添加表单的路由:

// GET /contacts
...// GET /contacts/new
router.get('/contacts/new', (req, res) => {if (req.headers['hx-request']) {res.render('form');} else {res.render('index', { action: 'new', contacts, contact: {} });}
});// GET /contacts/1
...

路由顺序在这里很重要。如果您先有 '/contacts/:id' 路由,那么 Express 将尝试查找 ID 为 new 的联系人。

最后,更新我们的 index.pug 模板以使用以下形式:

when 'new'include form.pug

刷新页面,此时您应该能够通过单击侧栏中的“New Contact”链接来呈现新的联系人表单。

创建联系人

现在我们需要创建一个路由来处理表单提交。

首先更新 app.js 以使我们能够访问路由处理程序中的表单数据。

const express = require('express');
const path = require('path');
const routes = require('./routes/index');const app = express();app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');+ app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));
app.use('/', routes);const server = app.listen(3000, () => {console.log(`Express is running on port ${server.address().port}`);
});

以前,我们会使用 body-parser 包,但我最近了解到这不再是必要的。

然后将以下内容添加到 index.js

// POST /contacts
router.post('/contacts', (req, res) => {const newContact = {id: contacts.length + 1,name: req.body.name,email: req.body.email,};contacts.push(newContact);if (req.headers['hx-request']) {res.render('sidebar', { contacts });} else {res.render('index', { action: 'new', contacts, contact: {} });}
});

在这里,我们使用从客户端收到的数据创建一个新联系人,并将其添加到 contacts 数组中。然后,我们重新渲染侧边栏,并向其传递更新的联系人列表。

请注意,如果您正在制作任何类型的有用户的应用程序,则由您负责验证从客户端接收的数据。在我们的示例中,我添加了一些基本的客户端验证,但这很容易被绕过。

我上面链接的 Node 教程中有一个示例,说明如何使用 express-validator 包来验证服务器上的输入。

现在,如果您刷新浏览器并尝试添加联系人,它应该按预期工作:新联系人应添加到侧边栏,并且应重置表单。

添加 toast 消息提示

这很好,但现在我们需要一种方法来通知用户联系人已添加。在典型的应用程序中,我们会使用 toast 消息——一种临时通知,提醒用户操作的结果。

我们使用 htmx 遇到的问题是,我们在成功创建新联系人后更新侧边栏,但这不是我们希望显示 toast 消息的位置。更好的位置将位于新联系表格上方。

为了解决这个问题,我们可以使用 hx-swap-oob 属性。这允许您指定响应中的某些内容应交换到目标以外的 DOM 中,即“Out of Band”。

更新路由处理程序如下:

if (req.headers['hx-request']) {res.render('sidebar', { contacts }, (err, sidebarHtml) => {const html = `<main id="content" hx-swap-oob="afterbegin"><p class="flash">Contact was successfully added!</p></main>${sidebarHtml}`;res.send(html);});
} else {res.render('index', { action: 'new', contacts, contact: {} });
}

在这里,我们像以前一样渲染侧边栏,但向 render 方法传递一个匿名函数作为第三个参数。该函数接收通过调用 res.render('sidebar', { contacts }) 生成的 HTML,然后我们可以使用它来组装最终响应。

通过指定交换策略 "afterbegin" ,将 toast 消息插入到容器的顶部。

现在,当我们添加联系人时,我们应该会收到一条不错的消息,告诉我们发生了什么。

编辑联系人

为了更新联系人,我们将重用上一节中创建的表单。

让我们首先更新 contact.pug 模板以添加以下内容:

div.actionsa(href=`/contacts/${contact.id}/edit`,hx-get=`/contacts/${contact.id}/edit`,hx-target='#content',hx-push-url='true') Edit Contact

这将在联系人详细信息下方添加一个编辑联系人按钮。正如我们之前所见,当单击链接时, hx-get 将通过 Ajax 向 /${contact.id}/edit 端点发出 GET 请求, hx-target 将指定插入位置响应, hx-push-url 将确保 URL 发生更改。

现在让我们更改 index.pug 模板以使用以下形式:

when 'edit'include form.pug

还添加一个路由处理程序来显示表单:

// GET /contacts/1/edit
router.get('/contacts/:id/edit', (req, res) => {const { id } = req.params;const contact = contacts.find((c) => c.id === Number(id));if (req.headers['hx-request']) {res.render('form', { contact });} else {res.render('index', { action: 'edit', contacts, contact });}
});

请注意,我们使用请求中的 ID 检索联系人,然后将该联系人传递到表单。

我们还需要更新新的联系人处理程序以执行相同的操作,但此处传递一个空对象:

// GET /contacts/new
router.get('/contacts/new', (req, res) => {if (req.headers['hx-request']) {
-    res.render('form');
+    res.render('form', { contact: {} });} else {res.render('index', { action: 'new', contacts, contact: {} });}
});

然后我们需要更新表单本身:

- isEditing = () => !(Object.keys(contact).length === 0);h2=isEditing() ? "Edit Contact" : "New Contact"form(action=isEditing() ? `/update/${contact.id}?_method=PUT` : '/contacts',method='POST',hx-post=isEditing() ? false : '/contacts',hx-put=isEditing() ? `/update/${contact.id}` : false,hx-target='#sidebar',hx-push-url=isEditing() ? `/contacts/${contact.id}` : falsehx-on::after-request='if(event.detail.successful) this.reset()',
)label(for='name') Name:input#name(type='text', name='name', required, value=contact.name)label(for='email') Email:input#email(type='email', name='email', required, value=contact.email)div.actionsbutton(type='submit') Submit

当我们向此表单传递联系人或空对象时,我们现在有一种简单的方法来确定我们是否处于“编辑”或“创建”模式。我们可以通过检查 Object.keys(contact).length 来做到这一点。我们还可以使用 Pug 的无缓冲代码语法将此检查提取到文件顶部的一个小辅助函数中。

一旦我们知道自己所处的模式,我们就可以有条件地更改页面标题,然后决定向表单标记添加哪些属性。对于编辑表单,我们需要添加 hx-put 属性并将其设置为 /update/${contact.id} 。保存联系人详细信息后,我们还需要更新 URL。

为了做到这一切,我们可以利用这样一个事实:如果条件返回 false ,Pug 将从标签中省略该属性。

这意味着:

form(action=isEditing() ? `/update/${contact.id}?_method=PUT` : '/contacts',method='POST',hx-post=isEditing() ? false : '/contacts',hx-put=isEditing() ? `/update/${contact.id}` : false,hx-target='#sidebar',hx-on::after-request='if(event.detail.successful) this.reset()',hx-push-url=isEditing() ? `/contacts/${contact.id}` : false
)

isEditing() 返回 false 时将编译为以下内容:

<form action="/contacts" method="POST" hx-post="/contacts" hx-target="#sidebar" hx-on::after-request="if(event.detail.successful) this.reset()"
>...
</form>

但是当 isEditing() 返回 true 时,它将编译为:

<form action="/update/1?_method=PUT" method="POST" hx-put="/update/1" hx-target="#sidebar" hx-on::after-request="if(event.detail.successful) this.reset()" hx-push-url="/contacts/1"
>...
</form>

在其更新状态下,请注意表单操作是 "/update/1?_method=PUT" 。添加此查询字符串参数是因为我们正在使用方法覆盖包,它将使我们的路由器响应 PUT 请求。

开箱即用的 htmx 可以发送 PUT 和 DELETE 请求,但浏览器却不行。这意味着,如果我们要处理 JavaScript 被禁用的情况,就需要复制我们的路由处理程序,让它同时响应 PUT(htmx)和 POST(浏览器)。使用这种中间件将使我们的代码保持 DRY。

让我们继续将其添加到 app.js

const express = require('express');
const path = require('path');
+ const methodOverride = require('method-override');
const routes = require('./routes/index');const app = express();app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');+ app.use(methodOverride('_method'));
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));
app.use('/', routes);const server = app.listen(3000, () => {console.log(`Express is running on port ${server.address().port}`);
});

最后,让我们用新的路由处理程序更新 index.js

// PUT /contacts/1
router.put('/update/:id', (req, res) => {const { id } = req.params;const newContact = {id: Number(id),name: req.body.name,email: req.body.email,};const index = contacts.findIndex((c) => c.id === Number(id));if (index !== -1) contacts[index] = newContact;if (req.headers['hx-request']) {res.render('sidebar', { contacts }, (err, sidebarHtml) => {res.render('contact', { contact: contacts[index] }, (err, contactHTML) => {const html = `${sidebarHtml}<main id="content" hx-swap-oob="true"><p class="flash">Contact was successfully updated!</p>${contactHTML}</main>`;res.send(html);});});} else {res.redirect(`/contacts/${index + 1}`);}
});

希望现在没有什么太神秘的事情了。在处理程序的开头,我们从请求参数中获取联系人 ID。然后,我们找到想要更新的联系人,并将其替换为根据我们收到的表单数据创建的新联系人。

在处理 htmx 请求时,我们首先用更新的联系人列表呈现侧边栏模板。然后,我们用更新的联系人渲染联系人模板,并使用这两次调用的结果来组合我们的响应。与之前一样,我们使用 “Out of Band” 更新创建一条 toast 消息,告知用户联系人已更新。

此时,您应该能够更新联系人。

删除联系人

最后一个难题是删除联系人的能力。让我们在联系人模板中添加一个按钮来执行此操作:

div.actionsform(method='POST', action=`/delete/${contact.id}?_method=DELETE`)button(type='submit',hx-delete=`/delete/${contact.id}`,hx-target='#sidebar',hx-push-url='/contacts'class='link') Delete Contacta( // as before )

请注意,最好使用表单和按钮来发出 DELETE 请求。表单是为导致更改(例如删除)的操作而设计的,这确保了语义的正确性。此外,使用链接进行删除操作可能存在风险,因为搜索引擎可能会无意中跟踪链接,从而可能导致不必要的删除。

话虽这么说,我添加了一些 CSS 来将按钮设置为链接样式,因为按钮很难看。如果您之前从存储库复制了样式,那么您的代码中已经包含了该样式。

最后,我们的路由处理程序在 index.js 中:

// DELETE /contacts/1
router.delete('/delete/:id', (req, res) => {const { id } = req.params;const index = contacts.findIndex((c) => c.id === Number(id));if (index !== -1) contacts.splice(index, 1);if (req.headers['hx-request']) {res.render('sidebar', { contacts }, (err, sidebarHtml) => {const html = `<main id="content" hx-swap-oob="true"><p class="flash">Contact was successfully deleted!</p></main>${sidebarHtml}`;res.send(html);});} else {res.redirect('/contacts');}
});

删除联系人后,我们将更新侧边栏并向用户显示一条提示消息。

更进一步

就这样吧。

在本文中,我们使用 Node 和 Express 作为后端,使用 htmx 作为前端,制作了一个全栈 CRUD 应用程序。在此过程中,我演示了 htmx 如何简化向 Web 应用程序添加动态行为,减少对复杂 JavaScript 和整页重新加载的需求,从而使用户体验更流畅、更具交互性。

作为额外的好处,该应用程序无需 JavaScript 也能正常运行。

然而,虽然我们的应用程序功能齐全,但不可否认它还有些简陋。如果您希望继续探索 htmx,您可能希望考虑在应用程序状态之间实现视图转换,或者向表单添加一些进一步的验证 - 例如,验证电子邮件地址是否来自特定域。

我在 htmx 简介中提供了这两件事(以及更多)的示例。


原文:https://www.sitepoint.com/node-js-htmx-build-full-stack-app

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/289969.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

系统架构图怎么画

画架构图是架构师的一门必修功课。 对于架构图是什么这个问题&#xff0c;我们可以按以下等式进行概括&#xff1a; 架构图 架构的表达 架构在不同抽象角度和不同抽象层次的表达&#xff0c;这是一个自然而然的过程。 不是先有图再有业务流程、系统设计和领域模型等&#…

项目四-图书管理系统

1.创建项目 流程与之前的项目一致&#xff0c;不再进行赘述。 2.需求定义 需求: 1. 登录: ⽤⼾输⼊账号,密码完成登录功能 2. 列表展⽰: 展⽰图书 3.前端界面测试 无法启动&#xff01;&#xff01;&#xff01;--->记得加入mysql相关操作记得在yml进行配置 配置后启动…

基于ZHW3548的红外额温枪解决方案

红外额温枪&#xff0c;非接触式测量最典型的方法是红外测温。自红外辐射原理被发现以来&#xff0c;红外技术被广泛应用在温度测量中。红外测温仪具有测温范围广&#xff0c;响应速度快&#xff0c;灵敏度高等特点。红外耳温枪、红外额温计和红外筛检仪都属于非接触式体温计。…

admin端

一、创建项目 1.1 技术栈 1.2 vite 项目初始化 npm init vitelatest vue3-element-admin --template vue-ts 1.3 src 路径别名配置 Vite 配置 配置 vite.config.ts // https://vitejs.dev/config/import { UserConfig, ConfigEnv, loadEnv, defineConfig } from vite im…

C语言 C6031:返回值被忽略:“scanf“ 问题解决

我们在代码中 直接使用 scanf 就会出现这个错误 在最上面 加上 #define _CRT_SECURE_NO_WARNINGS//禁用安全函数警告 #pragma warning(disable:6031)//禁用 6031 的安全警告即可正常运行

TitanIDE与传统 IDE 比较

与传统IDE的比较 TitanIDE 和传统 IDE 属于不同时代的产物&#xff0c;在手工作坊时代&#xff0c;一切都是那么的自然&#xff0c;开发者习惯 Windows 或 MacOS 原生 IDE。不过&#xff0c;随着时代的变迁&#xff0c;软件行业已经步入云原生时代&#xff0c;TitanIDE 是顺应…

Python 基于 OpenCV 视觉图像处理实战 之 OpenCV 简单实战案例 之九 简单闪烁效果

Python 基于 OpenCV 视觉图像处理实战 之 OpenCV 简单实战案例 之九 简单闪烁效果 目录 Python 基于 OpenCV 视觉图像处理实战 之 OpenCV 简单实战案例 之九 简单闪烁效果 一、简单介绍 二、简单闪烁效果实现原理 三、简单闪烁效果案例实现简单步骤 四、注意事项 一、简单…

JimuReport积木报表 v1.7.4 公测版本发布,免费的JAVA报表工具

项目介绍 一款免费的数据可视化报表&#xff0c;含报表和大屏设计&#xff0c;像搭建积木一样在线设计报表&#xff01;功能涵盖&#xff0c;数据报表、打印设计、图表报表、大屏设计等&#xff01; Web 版报表设计器&#xff0c;类似于excel操作风格&#xff0c;通过拖拽完成报…

多源统一视频融合可视指挥调度平台VMS/smarteye系统概述

系统功能 1. 集成了视频监控典型的常用功能&#xff0c;包括录像&#xff08;本地录像、云端录像&#xff08;录像计划、下载计划-无线导出&#xff09;、远程检索回放&#xff09;、实时预览&#xff08;PTZ云台操控、轮播、多屏操控等&#xff09;、地图-轨迹回放、语音对讲…

iptables添加端口映射,k8s主机查询不到端口但能访问。

研究原因&#xff1a;k8s内一台主机使用命令查询没有80端口。但通过浏览器访问又能访问到服务。 查询了资料是使用了hostport方式暴露pod端口。cni调用iptables增加了DNAT规则。访问时流量先经过iptables直接被NAT到具体服务去了。 链接: K8s罪魁祸首之"HostPort劫持了我…

Docker 夺命连环 15 问

目录 什么是Docker&#xff1f; Docker的应用场景有哪些&#xff1f; Docker的优点有哪些&#xff1f; Docker与虚拟机的区别是什么&#xff1f; Docker的三大核心是什么&#xff1f; 如何快速安装Docker&#xff1f; 如何修改Docker的存储位置&#xff1f; Docker镜像常…

使用certbot为网站启用https

1. 安装certbot客户端 cd /usr/local/bin wget https://dl.eff.org/certbot-auto chmod ax ./certbot-auto 2. 创建目录和配置nginx用于验证域名 mkdir -p /data/www/letsencryptserver {listen 80;server_name ~^(?<subdomain>.).ninvfeng.com;location /.well-known…

C/C++ ③ —— C++11新特性

1. 类型推导 1.1 auto auto可以让编译器在编译期就推导出变量的类型 auto的使⽤必须⻢上初始化&#xff0c;否则⽆法推导出类型auto在⼀⾏定义多个变量时&#xff0c;各个变量的推导不能产⽣⼆义性&#xff0c;否则编译失败auto不能⽤作函数参数在类中auto不能⽤作⾮静态成员…

[数据结构]插入和希尔排序

一、插入排序 插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴&#xff0c;但它的原理应该是最容易理解的了&#xff0c;因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法&#xff0c;它的工作原理是通过构建有序序列&#xff0c;对于未排…

星光/宝骏/缤果/长安 车机CarPlay手机操作破解教程V2.0版本(无需笔记本、无需笔记本、无需笔记本)

之前写了个1.0版本&#xff0c;由于太局限&#xff0c;需要用到笔记本才能操作&#xff0c;很多车友反馈不方便。特此出个手机版教程&#xff0c;简单easy&#xff0c;妈妈再也不用担心我搞不定啦 一、准备工作 先卸载车机上的autokit 或者 智能互联 app&#xff0c;这步很关…

神策数据参与制定首份 SDK 网络安全国家标准

国家市场监督管理总局、国家标准化管理委员会发布中华人民共和国国家标准公告&#xff08;2023 年第 13 号&#xff09;&#xff0c;全国信息安全标准化技术委员会归口的 3 项国家标准正式发布。其中&#xff0c;首份 SDK 国家标准《信息安全技术 移动互联网应用程序&#xff0…

计算机网络——30SDN控制平面

SDN控制平面 SDN架构 数据平面交换机 快速、简单&#xff0c;商业化交换设备采用硬件实现通用转发功能流表被控制器计算和安装基于南向API&#xff0c;SDN控制器访问基于流的交换机 定义了哪些可以被控制哪些不能 也定义了和控制器的协议 SDN控制器&#xff08;网络OS&#…

【探讨】光场相机三维重建研究进展与展望

摘要&#xff1a;光场相机利用二维影像同时记录空间中光线的位置和方向信息&#xff0c;能够恢复相机内部的光场&#xff0c;为三维重建提供了新的解决思路。围绕光场相机的三维重建问题&#xff0c;本文综述了光场数据获取手段&#xff0c;梳理和讨论了针对光场相机的标定算法…

Redis命令-List命令

4.6 Redis命令-List命令 Redis中的List类型与Java中的LinkedList类似&#xff0c;可以看做是一个双向链表结构。既可以支持正向检索和也可以支持反向检索。 特征也与LinkedList类似&#xff1a; 有序元素可以重复插入和删除快查询速度一般 常用来存储一个有序数据&#xff…

如何在Windows 10中打开屏幕键盘?这里有详细步骤

本文解释了在Windows 10中打开或关闭屏幕键盘的不同方法&#xff0c;还解释了如何将屏幕键盘固定到开始菜单。 使用屏幕键盘的快捷键 如果你喜欢快捷方式&#xff0c;你会喜欢这个&#xff1a;按物理键盘上的WinCTRLO。这将立即显示屏幕键盘&#xff0c;而无需通过轻松使用。…