实战|使用 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
html
  head
    meta(charset='UTF-8')
    title Contact Manager

    link(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')
  body
    header
      a(href='/contacts')
        h1 Contact Manager

    section#sidebar
      ul.contact-list
        each contact in contacts
          li #{contact.name}
      div.actions
        a(href='/contacts/new') New Contact

    main#content
      p Select a contact

    script(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-list
  each contact in contacts
    li
      a(
        href=`/contacts/${contact.id}`,
        hx-get=`/contacts/${contact.id}`,
        hx-target='#content',
        hx-push-url='true'
      )= contact.name

div.actions
  a(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#sidebar
  include 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#content
  case action
    when 'show'
      h2 #{contact.name}
      p #[strong Name:] #{contact.name}
      p #[strong Email:] #{contact.email}
    when 'new'
      // Coming soon
    when 'edit'
      // Coming soon
    default
      p 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#content
  case action
    when 'show'
      include contact.pug

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

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

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

新的联系表

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

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

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

… 到:

div.actions
  a(
    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 Contact

form(
  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.actions
    button(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.actions
  a(
    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}` : false
  hx-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.actions
    button(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.actions
  form(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 Contact

  a( 
    // 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

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

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

相关文章

Redis中的事件

事件 概述 Redis服务器是一个事件驱动程序:服务器需要处理以下两类事件: 1.文件事件(file event):Redis服务器通过套接字与客户端(或者其他Redis服务器)进行连接&#xff0c;而文件事件就是服务器对套接字操作的抽象。服务器与客户端(或者其他服务器)的通信会产生相应的文件…

Web实现猜数字游戏:JavaScript DOM基础与实例教程

&#x1f31f; 前言 欢迎来到我的技术小宇宙&#xff01;&#x1f30c; 这里不仅是我记录技术点滴的后花园&#xff0c;也是我分享学习心得和项目经验的乐园。&#x1f4da; 无论你是技术小白还是资深大牛&#xff0c;这里总有一些内容能触动你的好奇心。&#x1f50d; &#x…

STM32学习笔记(6_5)- TIM定时器的输出捕获原理

无人问津也好&#xff0c;技不如人也罢&#xff0c;都应静下心来&#xff0c;去做该做的事。 最近在学STM32&#xff0c;所以也开贴记录一下主要内容&#xff0c;省的过目即忘。视频教程为江科大&#xff08;改名江协科技&#xff09;&#xff0c;网站jiangxiekeji.com 现在开…

Jmeter脚本优化——CSV数据驱动文件

使用 CSV 数据文件设置实现参数化注册 1&#xff09; 本地创建 csv 文件&#xff0c;并准备要使用的数据&#xff0c;这里要参数化的是注册的用户名和邮箱。所以在 csv 文件中输入多组用户名和邮箱。 2&#xff09; 通过测试计划或者线程组的右键添加->配置元件->CSV…

【日常记录】【CSS】css实现汉堡菜单

文章目录 1、介绍2、布局3、鼠标移入变成 X 1、介绍 在移动端或者响应式中&#xff0c;可能会遇到 三个横线 鼠标移动到的时候&#xff0c;会变成 一个 X 符号&#xff0c;这个就是汉堡菜单 2、布局 <style>* {margin: 0;padding: 0;box-sizing: border-box;}body {displ…

后端常见面经之JVM

JVM组成 有垃圾回收的是哪些地方&#xff1f; 垃圾回收主要是针对堆内存中的对象进行的&#xff0c;包括以下几个方面&#xff1a; 堆内存&#xff1a;垃圾回收主要针对堆内存中不再被引用的对象进行回收&#xff0c;包括新生代和老年代中的对象。 永久代/元空间&#xff1a…

跑腿小程序|基于微信小程序的跑腿平台小程序设计与实现(源码+数据库+文档)

跑腿平台小程序目录 目录 基于微信小程序的跑腿平台小程序设计与实现 一、前言 二、系统设计 三、系统功能设计 1、用户信息管理 2、跑腿任务管理 3、任务类型管理 4、公告信息管理 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、…

【数据结构】非线性结构——二叉树

文章目录 前言1.树型结构1.1树的概念1.2树的特性1.3树的一些性质1.4树的一些表示形式1.5树的应用2.二叉树 2.1 概念2.2 两种特殊的二叉树2.3 二叉树的性质2.4 二叉树的存储2.5 二叉树的基本操作 前言 前面我们都是学的线性结构的数据结构&#xff0c;接下来我们就需要来学习非…

42 ajax 下载文件未配置 responseType blob 导致的文件异常

前言 这是一个最近的关于文件下载碰到的一个问题 主要的情况是, 基于 xhr 发送请求, 获取下载的文件 然后 之后 xhr 这边拿到 字节序列之后, 封装 blob 来进行下载 然后 最开始我们这边没有配置 responseType 为 blob, arraybuffer, 然后 导致下载出来的 文件大小超过了…

基于前端技术实现的全面预算编制系统

前言 在现代商业环境中&#xff0c;预测销售数据和实际成本是每个公司CEO和领导都极为重视的关键指标。然而&#xff0c;由于市场的不断变化&#xff0c;准确地预测和管理这些数据变得愈发具有挑战性。为了应对这一挑战&#xff0c;建立一个高效的系统来管理和审查销售数据的重…

QT环境搭建

学习QT 一、QT环境搭建二、QT的SDK下载三、认识QT SDK 中自带的一些程序 一、QT环境搭建 QT开发环境&#xff0c;需要安装三个部分。 c编译器&#xff08;gcc、cl.exe……不是visual studio&#xff09;QT SDK&#xff08;QT SDK里面已经内置了C编译器&#xff1b;SDK就是软件…

如何使用Harmony OS控制外设——输入输出?

相关知识点 Hi3861开发板第一个示例程序演示 熟悉使用DevEco Device Tool插件进行程序烧录 熟悉串口调试工具sscom的使用 官方文档中控制核心板上LED的led_example.c讲解及演示 源码路径&#xff1a;applications/sample/wifi-iot/app/iothardware/led_example.cHarmony OS …

docker--Dockerfile (三)

1&#xff0c;Dcockerfile是什么 docker推荐使用dockerfile的定义文件和docker build命令来构建镜像。dockerfile使用基本的基于DSL&#xff08;面向领域语言&#xff09;语法的指令来构建Docker镜像。另一种创建Docker镜像的方式是使用docker commit&#xff0c;不推荐使用。 …

酷开系统让用户和电视双向传递,酷开科技实现商业变现

电视在我们的日常生活中扮演着重要的角色。虽然&#xff0c;作为客厅C位的扛把子——电视的娱乐作用深入人心&#xff0c;但是&#xff0c;它的涵义和影响力却因我们每个人的具体生活环境而存在着种种差异&#xff0c;而我们的生活环境又受到我们所处的社会及文化环境的影响。 …

6.使用个人用户登录域控的成员服务器,如何防止个人用户账号的用户策略生效?

&#xff08;1&#xff09;需求&#xff1a; &#xff08;2&#xff09;实战配置步骤 第一步:创建新的策略-并编辑策略 第二步&#xff1a;将策略应用到服务器处在OU 第三步&#xff1a;测试 &#xff08;1&#xff09;需求&#xff1a; 比如域控&#xff0c;或者加入域的…

以XX大学校园为例的智慧能源管理系统建设方案【能源物联网+智能微电网数字校园、节能校园、低碳校园】

建设背景 贯彻落实《中共中央 国务院关于完整准确全面贯彻新发展理念做好碳达峰碳中和工作的意见》和《国务院关于印发2030年前碳达峰行动方案的通知》要求&#xff0c;把绿色低碳发展纳入国民教育体系。 2021年3月26日为推动信息技术与教育教学深度融合&#xff0c;教育部印…

AI基础知识扫盲

AI基础知识扫盲 AIGCLangchain--LangGraph | 新手入门RAG&#xff08;Retrieval-Augmented Generation&#xff09;检索增强生成fastGPT AIGC AIGC是一种新的人工智能技术&#xff0c;它的全称是Artificial Intelligence Generative Content&#xff0c;即人工智能生成内容。 …

线性代数 - 应该学啥 以及哪些可以交给计算机

AI很热&#xff0c;所以小伙伴们不免要温故知新旧时噩梦 - 线代。 &#xff08;十几年前&#xff0c;还有一个逼着大家梦回课堂的风口&#xff0c;图形学。&#xff09; 这个真的不是什么美好的回忆&#xff0c;且不说老师的口音&#xff0c;也不说教材的云山雾绕&#xff0c;单…

Python程序设计 分支结构

1.判断三角形类型 编写一个能判断三角形类型的小程序。 输入三个数值&#xff0c;判断其是否能构成三角形的三条边。如果能构成&#xff0c;判断其是否等边三角形、直角三角形还是普通三角形。 xeval(input("边长一")) yeval(input("边长二")) zeval(inp…

SQLite使用的临时文件(二)

返回&#xff1a;SQLite—系列文章目录 上一篇&#xff1a;SQLite数据库文件损坏的可能几种情况 下一篇&#xff1a;未发表 ​ 1. 引言 SQLite的显着特点之一它是一个数据库由一个磁盘文件组成。 这简化了 SQLite 的使用&#xff0c;因为移动或备份 数据库就像复制单个文…