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

news/2024/4/28 1:40:30/文章来源:https://blog.csdn.net/duninet/article/details/137027682

在本教程中,我将演示如何使用 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.luyixian.cn/news_show_1027454.aspx

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

相关文章

Haproxy2.8.1+Lua5.1.4部署,haproxy.cfg配置文件详解和演示

目录 一.快速安装lua和haproxy 二.配置haproxy的配置文件 三.配置haproxy的全局日志 四.测试负载均衡、监控和日志效果 五.server常用可选项 1.check 2.weight 3.backup 4.disabled 5.redirect prefix和redir 6.maxconn 六.调度算法 1.静态 2.动态 一.快速安装lu…

【python】网络编程socket TCP UDP

文章目录 socket常用方法TCP客户端服务器UDP客户端服务器网络编程就是实现两台计算机的通信 互联网协议族 即通用标准协议,任何私有网络只要支持这个协议,就可以接入互联网。 socket socke模块的socket()函数 import socketsock = socket.socket(Address Family, type)参…

推动制药行业数字化转型:基于超融合架构的MES一体机解决方案

随着中国对信息化重视程度的不断加深&#xff0c;制药行业作为国民经济的重要支柱之一&#xff0c;也在积极寻求通过数字化手段提升产业效率与产品质量。自党的十六大提出“以信息化带动工业化”的战略以来&#xff0c;制药业的这一转型探索尤为迫切。 在现代制药生产中&#…

scala-idea环境搭建及使用

环境搭建 创建一个新项目&#xff0c;选择maven工程 点击next&#xff0c;写入项目名&#xff0c;然后finish 注意&#xff1a;默认下&#xff0c;maven不支持scala的开发&#xff0c;需要引入scala框架&#xff0c;右键项目点击-》add framework pport....&#xff0c;在下图…

基于java+SpringBoot+Vue的书籍学习平台设计与实现

基于javaSpringBootVue的书籍学习平台设计与实现 开发语言: Java 数据库: MySQL技术: SpringBoot MyBatis工具: IDEA/Eclipse、Navicat、Maven 系统展示 前台展示 后台展示 系统简介 整体功能包含&#xff1a; 书籍学习平台是一个基于Internet的在线学习资源平台&#xf…

Apache Hive的基本使用语法(二)

Hive SQL操作 7、修改表 表重命名 alter table score4 rename to score5;修改表属性值 # 修改内外表属性 ALTER TABLE table_name SET TBLPROPERTIES("EXTERNAL""TRUE"); # 修改表注释 ALTER TABLE table_name SET TBLPROPERTIES (comment new_commen…

音视频开发Day01

备注 ffmpeg 库相关函数记忆 FFmpeg 库简介 FFmpeg一共包含8个库: avcodec:编解码(最重要的库) avformat:封装格式处理。 avfilter:滤镜特效处理 avdevice:各种设备的输入输出。 avutil:工具库(大部分库都需要这个库的支持)。 postproc:后加工。 swresample:音频采样数据格式…

一分钟开服 《幻兽帕鲁》游戏专属服务器by京东云主机

使用京东云服务器搭建幻兽帕鲁Palworld游戏联机服务器教程&#xff0c;非常简单&#xff0c;京东云推出幻兽帕鲁镜像系统&#xff0c;镜像直接选择幻兽帕鲁镜像即可一键自动部署&#xff0c;不需要手动操作&#xff0c;真正的新手0基础部署幻兽帕鲁&#xff0c;阿腾云atengyun.…

OSCP靶场--pyLoader

OSCP靶场–pyLoader 考点(信息收集CVE-2023-0297) 1.nmap扫描 ┌──(root㉿kali)-[~/Desktop] └─# nmap -Pn -sC -sV 192.168.178.26 --min-rate 2500 Starting Nmap 7.92 ( https://nmap.org ) at 2024-03-28 09:14 EDT Nmap scan report for 192.168.178.26 Host is up…

基于Python的电商特产数据可视化分析与推荐系统

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长 QQ 名片 :) 1. 项目简介 利用网络爬虫技术从某东采集某城市的特产价格、销量、评论等数据&#xff0c;经过数据清洗后存入数据库&#xff0c;并实现特产销售、市场占有率、价格区间等多维度的可视化统计分析&#xff0c;并…

关于使用vscode搭建c/c++编程环境

目录 关于使用vscode搭建c/c编程环境一、前言二、安装 IDE 二、安装TDM-GCC安装三、安装C/C环境四、编写代码并进行编译 关于使用vscode搭建c/c编程环境 一、前言 一直觉得vscode是生产强有力的生产工具&#xff0c;基于此&#xff0c;做一篇学习笔记进行记录。 二、安装 ID…

Prometheus +Grafana +node_exporter可视化监控Linux虚机

1、介绍 待补充 2、架构图 待补充 Prometheus &#xff1a;主要是负责存储、抓取、聚合、查询方面。 node_exporter &#xff1a;主要是负责采集物理机、中间件的信息。 3、搭建过程 配置要求&#xff1a;1台主服务器 n台从服务器 &#xff08;被监控的linux虚机&am…

WPF自定义Panel:让拖拽变得更简单

在 WPF 应用程序中&#xff0c;拖放操作是实现用户交互的重要组成部分。通过拖放操作&#xff0c;用户可以轻松地将数据从一个位置移动到另一个位置&#xff0c;或者将控件从一个容器移动到另一个容器。然而&#xff0c;WPF 中默认的拖放操作可能并不是那么好用。为了解决这个问…

uniApp使用XR-Frame创建3D场景(7)加入点击交互

上篇文章讲述了如何将XR-Frame作为子组件集成到uniApp中使用 这篇我们讲解如何与场景中的模型交互&#xff08;点击识别&#xff09; 先看源码 <xr-scene render-system"alpha:true" bind:ready"handleReady"><xr-node><xr-mesh id"…

【已修复】iPhone13 Pro 长焦相机水印(黑斑)修复 洗水印

iPhone13 Pro 长焦相机水印&#xff08;黑斑&#xff09;修复 洗水印 问题描述 iPhone13 Pro 后摄3倍相机有黑色斑点&#xff08;水印&#xff09;&#xff0c;如图所示&#xff0c; 后摄相机布局如图所示&#xff0c; 修复过程 拆机过程有风险&#xff0c;没有把握最好不要…

MySQL学习笔记------函数

目录 函数 字符串函数 数值函数 函数 指一段可以直接被另一段程序调用的程序或代码 字符串函数 MySQL中内置了很多字符串函数&#xff0c;常用如下&#xff1a;select 函数 concat(s1,s2,s3,...,sn) 字符串拼接&#xff0c;将s1,s2,...,sn拼接成一个字符串 #字符串拼…

C/C++语言学习路线: 嵌入式开发、底层软件、操作系统方向(持续更新)

初级&#xff1a;用好手上的锤子 1 【感性】认识 C 系编程语言开发调试过程 1.1 视频教程点到为止 1.2 炫技视频看看就行 1.3 编程游戏不玩也罢 有些游戏的主题任务就是编程&#xff0c;游戏和实际应用环境有一定差异&#xff08;工具、操作流程&#xff09;&#xff0c;在…

火车头通过关键词采集文章的原理

随着互联网信息的爆炸式增长&#xff0c;网站管理员和内容创作者需要不断更新和发布新的文章&#xff0c;以吸引更多的用户和提升网站的排名。而火车头作为一款智能文章采集工具&#xff0c;在这一过程中发挥着重要作用。本文将探讨火车头如何通过关键词采集文章&#xff0c;以…

康耐视visionpro-CogCaliperTool工具详细说明

CogCaliperTool功能说明: 卡尺工具,用于测量距离 CogCaliperTool操作说明: ①.打开工具栏,双击或点击鼠标拖拽添加CogCaliperTool ②.添加输入图像,右键“链接到”或以连线拖拽的方式选择相应输入源 ③.拖动屏幕上的矩形框到需要测量的位置。卡尺的搜索框角度与边缘不平…

React系列之合成事件与事件处理机制

文章目录 React事件处理机制原生事件的事件机制事件代理&#xff08;事件委托&#xff09; 合成事件使用合成事件目的合成事件原生事件区别事件池 原生事件和React事件的执行顺序e.stopPropagation() React17事件机制的修改 React事件处理机制 react 事件机制基本理解&#xf…