实战|使用 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 简介

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

shell 复制代码
node -v
npm -v

您应该看到如下输出:

v20.11.1
10.4.0

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

设置项目

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

shell 复制代码
mkdir contact-manager
cd contact-manager
npm init -y

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

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

shell 复制代码
npm i express method-override pug

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

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

shell 复制代码
touch app.js

并添加以下内容:

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 正在运行并准备好处理指定端口上的请求。此设置构成了我们应用程序的基础,并准备好通过更多功能和路由进行扩展。

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

shell 复制代码
mkdir routes
touch routes/index.js

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

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"部分:

json 复制代码
"scripts": {
  "dev": "node --watch app.js"
},

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

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

激动人心的时刻!

显示所有联系人

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

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

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

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 文件夹:

shell 复制代码
mkdir views
touch views/index.pug

并添加以下内容:

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 的插值语法在无序列表中输出每个联系人姓名。

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

shell 复制代码
mkdir public
touch public/styles.css

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

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

js 复制代码
// GET /contacts
router.get('/contacts', (req, res) => {
  res.render('index', { contacts });
});

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

显示单个联系人

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

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

shell 复制代码
touch views/sidebar.pug

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

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 以使用我们的模板:

pug 复制代码
section#sidebar
  include sidebar.pug

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

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

js 复制代码
// 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。如果存在,我们就可以发送部分内容。否则,我们需要发送整个页面。

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

js 复制代码
// 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 语句:

pug 复制代码
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

最后更新路由处理程序:

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

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

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

快速重构

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

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

shell 复制代码
touch views/contact.pug

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

pug 复制代码
h2 #{contact.name}

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

在主模板( index.pug )中:

pug 复制代码
main#content
  case action
    when 'show'
      include contact.pug

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

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

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

新的联系表

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

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

pug 复制代码
div.actions
  a(href='/contacts/new') New Contact

... 到:

pug 复制代码
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。

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

shell 复制代码
touch views/form.pug

并添加以下代码:

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 中添加表单的路由:

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 模板以使用以下形式:

pug 复制代码
when 'new'
  include form.pug

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

创建联系人

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

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

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

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"。

更新路由处理程序如下:

js 复制代码
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 模板以添加以下内容:

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 模板以使用以下形式:

pug 复制代码
when 'edit'
  include form.pug

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

js 复制代码
// 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 检索联系人,然后将该联系人传递到表单。

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

js 复制代码
// 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: {} });
  }
});

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

pug 复制代码
- 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 将从标签中省略该属性。

这意味着:

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 时将编译为以下内容:

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

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

html 复制代码
<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

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

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 消息,告知用户联系人已更新。

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

删除联系人

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

pug 复制代码
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 中:

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

相关推荐
m0_748256783 分钟前
SpringBoot 依赖之Spring Web
前端·spring boot·spring
web1350858863531 分钟前
前端node.js
前端·node.js·vim
m0_5127446432 分钟前
极客大挑战2024-web-wp(详细)
android·前端
若川41 分钟前
Taro 源码揭秘:10. Taro 到底是怎样转换成小程序文件的?
前端·javascript·react.js
潜意识起点1 小时前
精通 CSS 阴影效果:从基础到高级应用
前端·css
奋斗吧程序媛1 小时前
删除VSCode上 origin/分支名,但GitLab上实际上不存在的分支
前端·vscode
IT女孩儿1 小时前
JavaScript--WebAPI查缺补漏(二)
开发语言·前端·javascript·html·ecmascript
m0_748256563 小时前
如何解决前端发送数据到后端为空的问题
前端
请叫我飞哥@3 小时前
HTML5适配手机
前端·html·html5
@解忧杂货铺5 小时前
前端vue如何实现数字框中通过鼠标滚轮上下滚动增减数字
前端·javascript·vue.js