Gatsby 和 Netlify 上的表单、身份验证和无服务器函数

Avatar of Maxime Laboissonnière
Maxime Laboissonnière 发布

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 $200 免费积分!

抽象化基础设施是我们的 DNA。 道路、学校、供水网络——您懂的。 Web 开发也不例外: 无服务器架构 是这种现象的完美体现。 **静态网站,特别是正在转变为动态、丰富的体验。**

处理 静态表单身份验证 和静态生成网站上的后端功能现在已经成为可能。 特别是像 JAMstack 这样的先锋平台,比如 Netlify。 最近,他们在 宣布支持面向前端的网站和应用程序上的 AWS Lambda 函数。 我一直想深入研究他们的“后端”功能。

今天,我正在做这件事,使用 **静态 Gatsby 网站、Netlify 的表单、身份验证和函数功能**。 本教程将向您展示如何

  • 将静态表单添加到您的网站
  • 为受密码保护的内容添加用户身份验证
  • 创建 AWS Lambda 函数

准备好使用无服务器功能增强静态网站了吗?

在阅读完本文后,请考虑查看 Netlify 的 React 静态 CMS! 这里还有一个使用 JAMstack 的 完整评论工作流程教程,包含批准系统。

静态网站表单、身份验证和 AWS Lambda 函数

在深入研究代码之前,让我们详细说明我们的用例。 我将使用三种不同的 Netlify 无服务器功能

1. 身份验证

身份验证 将用于在 Gatsby 网站上创建受密码保护的门控内容部分。 没有后端的身份验证一直是静态网站的一个难题。 但是这个巧妙的功能优雅地解决了这个问题,允许开发人员

管理注册、登录、密码恢复等——所有这些都不需要自己滚动身份验证服务。

2. 表单

表单 将用于启用用户在网站上提交的产品评论。 动态表单可以采取多种形式(您看到了吗?),从简单的联系表单到评论、报价和评论系统,这些系统与内部工具相连。

大量解决方案 可用于处理静态网站上的交互式表单。 但是使用 **表单**,您可以在您的构建和托管服务(Netlify 的核心产品)中直接处理它们。 不需要垃圾邮件陷阱 mailto: 链接、配置自己的服务器、设置无服务器函数或集成第三方,如 FormspreeFormKeep

不需要 JavaScript、API 或后端:只需一个用 netlify HTML 属性标记的简单 HTML 表单。

3. 函数

函数 将用于直接在 Slack 中设置评论审核工作流程。 此功能背后的技术是 AWS Lambda 函数——您可以运行的事件触发的可扩展后端代码,而无需您自己的服务器。 使用 Netlify 部署它们与将文件添加到 Git 仓库一样简单。

Lambda 函数确实功能强大,但它们通常需要一个 AWS 帐户和 API 网关配置。 与 **表单** 一样,**函数** 通过将繁重的工作卸载到 Netlify 来简化您的生活

您的函数与您的 Netlify 网站的其余部分一起进行版本控制、构建和部署,而 Netlify API 网关会自动处理服务发现。 此外,您的函数可以从部署预览和回滚的强大功能中受益。

简而言之,函数允许您增强网站交互性并连接前端和后端,以让数据在服务之间流动。

Netlify 上的无服务器 Gatsby:身份验证、静态表单和 Lambda 函数

我将使用 我们之前构建的一个 Gatsby 网站的简化版本 来启动本教程。

要了解 Gatsby 基础知识,请查看 官方教程。 我们还有两个使用 Gatsby 的电子商务教程 这里这里

先决条件

在本教程中,您需要

1. 分叉 Gatsby 项目

首先分叉存储库

在 GitHub 上查看

我建议您尝试一下,熟悉该项目。 产品位于 src/data/products 文件夹中——所有都在 Markdown 文件中。 这些文件在构建时加载,用于将正确的信息注入我们的模板中。 这是在 gatsby-node.js 文件中完成的。

2. 添加身份验证以进行身份验证

如果您打开任何产品文件,您可能会看到我们通常不会在 Snipcart 演示中使用的字段:**一个私有属性**。 目标很简单:仅当用户登录时才显示这些“独家”产品。

为了处理这个问题,我使用了 Netlify 的身份验证小部件,这是一种向静态网站添加身份验证的简单方法。

您可以通过运行以下命令来安装身份验证包

npm install --save netlify-identity-widget

确保将其包含在您的标题中! 在 src/components/Header/index.js 文件中,我在 h1 结束标签之后添加了这一行

<div data-netlify-identity-menu></div>

然后,您可以在文件顶部使用以下命令导入小部件

const netlifyIdentity = require("netlify-identity-widget");

并声明一个 componentDidMount 函数,如下所示

componentDidMount(){    
  netlifyIdentity.init();
}

身份验证小部件现在将在该 <div> 中注入适当的登录表单。 现在您已经有了静态登录表单,您需要适当的逻辑来验证用户是否已登录。

我使用了这种逻辑来向已登录用户显示适当的受密码保护的产品。 为此,我在 src/pages 文件夹中创建了一个 products.js,并定义了以下组件

import React from 'react'
import Link from 'gatsby-link'
import styles from './products.module.css'
const netlifyIdentity = require("netlify-identity-widget");

export default class Products extends React.Component {
  constructor(data){
    super(data);

    this.state = {
      products: []
    }
  }

getProducts(){
  return netlifyIdentity.currentUser() != null
    ? this.props.data.allMarkdownRemark.edges
    : this.props.data.allMarkdownRemark.edges
      .filter(x => !x.node.frontmatter.private)  
}

updateProducts(){
this.setState({ products: this.getProducts() });
}

componentDidMount(){
netlifyIdentity.on("login", user => this.updateProducts());
netlifyIdentity.on("logout", () => this.updateProducts());
this.updateProducts();
}

render(){
return (
<div>
  <h1>Products</h1>
  <p>To login use the email: [email protected] with password: admin</p>

  <ul className={styles.itemsList}>
    {this.state.products.map((o, index) =>
      <li key={index} className={styles.item}>
        <Link to={o.node.frontmatter.loc}>
          <figure>
            <img className={styles.image} src={o.node.frontmatter.image} alt={o.node.frontmatter.name}></img>
            <figcaption className={styles.figCaption}>Buy the {o.node.frontmatter.name} now</figcaption>
          </figure>
        </Link>
      </li>
    )}
  </ul>
    </div>)
  }
}

export const query = graphql`
query allProducts {
  allMarkdownRemark {
    edges {
      node {
        frontmatter {
          sku,
          loc,
          price,
          desc,
          private,
          name,
          image
        }
      }
    }
  }
`

我在这里不会解释 GraphQL。 如果您有兴趣,请 阅读更多内容

要理解的重点是 componentDidMount 生命周期函数中发生了什么。 我将自己绑定到小部件的“登录”和“注销”事件,以更新可用产品。

最终结果非常棒

3. 处理静态评论表单

为了在 Gatsby 网站上添加产品评论,我使用了 Netlify 的 Forms。 你可以在自己的网站上添加他们的 Forms,方法是在表单声明中添加一个 'data-netlify="true"'(或者只是 netlify)属性。 我将它包含在我的 src/components/product.js 文件中,位于最后一个 section 标签之后。

你还需要在渲染函数的返回之前声明一个 formId 变量,例如

render(){
  if(this.props.data.markdownRemark.frontmatter.private
    && !this.state.loggedIn){
    return fourOfour();
  }

  var formId = `product-${this.props.data.markdownRemark.frontmatter.sku}`

  const button = this.props.data.markdownRemark.frontmatter.private ? (
    <button type="button" className={`${styles.buyButton}`}>
        SOLD OUT
    </button>
  ) : (
  <button type="button" className={`${styles.buyButton} snipcart-add-item`}
      data-item-name={this.props.data.markdownRemark.frontmatter.name}
      data-item-id={this.props.data.markdownRemark.frontmatter.sku}
      data-item-image={this.props.data.markdownRemark.frontmatter.image}
      data-item-url={`${NETLIFY_URL}${this.props.location.pathname}`}
      data-item-price={this.props.data.markdownRemark.frontmatter.price}
      data-item-description={this.props.data.markdownRemark.frontmatter.desc}>
      Buy it now for {this.props.data.markdownRemark.frontmatter.price}$
  </button>
);

return (
  <div>
    <h1>{this.props.data.markdownRemark.frontmatter.name}</h1>
    <div className={styles.breadcrumb}>
      <Link to='/'>Back to the products</Link>
    </div>
    <p>{this.props.data.markdownRemark.frontmatter.desc}</p>

    <section className="section__product">
      <figure className={styles.productFigure}>
        <img src={this.props.data.markdownRemark.frontmatter.image} />
      </figure>

      <article>
        {this.props.data.markdownRemark.frontmatter.description}
      </article>
      <div className={styles.actions}>
        {button}
      </div>
    </section>
    <section>
      <h3 className="reviews">Reviews</h3>
      <div className="reviews__list">
        {this.state.reviews.map((o) =>
          <p key={o.number}>
            <div className="review__name">{o.name}</div>
            <div>{o.data.message}</div>
          </p>
        )}
      </div>

      <form className="review__form" name={formId} method="POST" data-netlify-honeypot="bot-field" data-netlify="true">
        <input type="hidden" name="form-name" value={formId} />    
        <div className="field__form">
          <label>NAME</label>
          <input type="text" name="name"></input>
        </div>
        <div className="field__form">
          <label>EMAIL</label>
          <input type="email" name="email"></input>
        </div>
        <div className="field__form">
          <label>MESSAGE</label>
          <textarea name="message"></textarea>
        </div>

        <button className="button__form" type="submit">SEND</button>
      </form>
    </section>
  </div>)
}

就是这样,静态表单就出现在你的网站上了!

但是,要显示通过这些表单提交的内容,你需要一个 Netlify 函数来获取并返回用户评论。 为此,我创建了一个 netlify.toml 文件,内容如下

[build]
  functions = "functions"

然后,我在根项目中直接放了一个 functions 文件夹。 在里面,我放了一个 fetchreviews.js 文件,内容如下

const https = require('https');

exports.handler = function(event, context, callback) {
  var id = event.queryStringParameters.id;
  var token = process.env.netlify_access_token;

    if(id == undefined){
      callback('A product id must be specified.', {
        statusCode: 500
    })
  }

  var options = {
    hostname: 'api.netlify.com',
    port: 443,
    method: 'GET',
    headers: {        
      'Content-Type': 'application/json'
    }
  };
  
  var queryToken = `access_token=${token}`;
  var opts1 = Object.assign({}, options, { path: `/api/v1/sites/${process.env.site_id}/forms?${queryToken}`});

  var req = https.request(opts1, function(res) {

    res.setEncoding('utf8');
    var body = "";

    res.on('data', data => {
      body += data;
    });

    res.on('end', function () {
      body = JSON.parse(body);

      var form = body.filter(x => x.name == `product-${id}`)[0];
      var opts2 = Object.assign({}, options, { path: `/api/v1/forms/${form.id}/submissions?${queryToken}`});

      var req2 = https.request(opts2, function(res2) {
        res2.setEncoding('utf8');         
        var body2 = "";

        res2.on("data", (data) => {
          body2 += data;
        });

        res2.on('end', function () {
          callback(null, {
            statusCode: 200,
              headers: {
                "Access-Control-Allow-Origin" : "*",
                'Content-Type': 'application/json'
              },
              body: body2
            })
          });
        });

        req2.end();
      });
  });

  req.end();
}

该函数检查是否以查询参数的形式给出了产品 ID。 如果有 ID,它会获取名为 product-{product-id} 的表单,以获取其中的所有评论。 这样,我就可以在前端显示评论。

我在 product.js 中添加了两个函数来实现这一点

constructor(props){
  super(props);

  this.state = {
    reviews: [],
    loggedIn: false
  }
}

componentDidMount(){
  fetch(`https://${NETLIFY_FUNC}/fetchreviews?id=${this.props.data.markdownRemark.frontmatter.sku}`)
    .then(x => x.json())
    .then(x => {
      this.setState({reviews: x})
    })

  if(netlifyIdentity.currentUser() != null){
    this.setState({loggedIn: true});
  }

  netlifyIdentity.on("login", user => this.setState({loggedIn: true}));
  netlifyIdentity.on("logout", () => this.setState({loggedIn: false}));
}

然后,就在评论表单之前

{this.state.reviews.map((o) =>
  <p key={o.number}>{o.name}: {o.data.message}</p>
)}

上面,已挂载的组件会获取新的函数来获取特定产品的评论。 它也会更新状态,并在页面上显示它们。 你还可以看到,我们决定为私有产品添加一个“已售罄”按钮,这是因为这些产品是私有的,如果我们只是简单地使用当前 URL,它们就不会通过我们的验证,我们仍然可以这样做,但这需要更多工作,超出了本演示的范围。

如果你想在不部署到 Netlify 的情况下测试你的函数,可以使用 netlify-lambda 节点包在本地执行。 安装好它(npm install netlify-lambda)后,在你的项目文件夹中运行 netlify-lambda serve ./。 该函数将在 http://localhost:9000/fetchreviews 处运行。

你可以更新上面的获取路由,获得与托管函数相同的行为。

4. 配置带有 Slack 的 AWS Lambda 函数

你需要 Netlify Forms Pro 才能在表单提交时触发函数。

最后但同样重要的是:直接在 Slack 中进行评论审核工作流程。 目标很简单:将评论详情和通知推送到 Slack,并允许从 Slack 中*保留*或*拒绝*评论。

为此,我在 functions 文件夹中创建了两个新函数:notifyslack.jsanswerslack.js。 第一个由 Netlify 的 webhook 通知每个表单提交,并负责将此信息传达给 Slack,并提供相应的操作项。 我为此创建了一个小型 Slack 应用 (参考)。

以下是该应用所需的权限

**交互式组件**配置

**请求 URL** 字段是你的 Netlify 函数可以被调用的地方。

设置完这些后,我安装了我的应用并打开了 **传入 Webhook** 选项卡。 我复制了 Webhook URL,并返回到我的项目。

functions 中,我创建了带有以下内容的 notifyslack.js 文件

var https = require("https");

exports.handler = function(event, context, callback) {
  var body = JSON.parse(event.body);

  if(body != null && body.data != null){
    var data = body.data;

    var message = `New review from ${data.email} \n ${data.name}: ${data.message}`;
    var attach = [
      {
        "title": "Review ID",
        "text": body.id
      },
      {
        "title": "Do you want to keep the review?",
        "text": message,
        "fallback": "You can't take actions for this review.",
        "callback_id": "answer_netlify",
        "color": "#3AA3E3",
        "attachment_type": "default",
        "actions": [
        {
          "name": "response",
          "text": "Keep",
          "type": "button",
          "value": "keep"
        },
        {
          "name": "response",
          "text": "Reject",
          "type": "button",
          "style": "danger",
          "value": "reject",
          "confirm": {
            "title": "Are you sure?",
            "text": "Once it's done the review will be deleted",
            "ok_text": "Yes",
            "dismiss_text": "No"
          }
        }
        ]
      }
    ]

    var postData = JSON.stringify({
      attachments: attach
    });
  
    var options = {
      hostname: 'hooks.slack.com',
      port: 443,
      path: process.env.slack_webhook_url,
      method: 'POST',
      headers: {        
        'Content-Type': 'application/json'
      }
    };
      
    var req = https.request(options, function(res) {
  
      res.setEncoding('utf8');
        
      res.on('end', function () {
        callback(null, {
          statusCode: 200
        })
      });
    });
          
    req.on('error', function (e) {
      console.log('Problem with request:', e.message);
    });
  
    req.write(postData);
    req.end();
  
    callback(null, {
      statusCode: 200
    })
  }
}

在这里,你需要使用你的相应 Slack 应用 Webhook URL 更新选项对象的路径值。

目前,这只会通知 Slack——无论你选择什么操作,都不会触发其他任何操作。

为了让 Slack 通知具有交互性,我在一个名为 answerslack.js 的文件中创建了第三个函数。 这个函数可能最复杂,但它主要涉及请求开销,所以请耐心等待

var https = require("https");
var qs = require('querystring')

function getURL(href) {
  var match = href.match(/^(https?\:)\/\/(([^:\/?#]*)(?:\:([0-9]+))?)([\/]{0,1}[^?#]*)(\?[^#]*|)(#.*|)$/);
  return match && {
    href: href,
    protocol: match[1],
    host: match[2],
    hostname: match[3],
    port: match[4],
    pathname: match[5],
    search: match[6],
    hash: match[7]
  }
}

exports.handler = function(event, context, callback) {
  var json = JSON.parse(qs.parse(event.body).payload);

  var answer = json.actions[0].value;
  var access_token = process.env.netlify_access_token;
  var id = json.original_message.attachments[0].text;
  
  if(answer == 'reject'){
    var options = {
      hostname: 'api.netlify.com',
      port: 443,
      path: `/api/v1/submissions/${id}?access_token=${access_token}`,
      method: 'DELETE',
      headers: {        
        'Content-Type': 'application/json'
      }
    };
    
    var req1 = https.request(options, function(res) {

      res.setEncoding('utf8');
            
      res.on('end', function () {
        console.log(`Review with id: ${id} was deleted successfully.`)
      });
    });
        
    req1.on('error', function (e) {
      console.log('Problem with request:', e.message);
    });

    req1.end(); 
  }

  var postData  = JSON.stringify({
    replace_original: true,
    attachments: [{
      text: answer == 'keep'
        ? `The review (${id}) was approved!`
        : `The review (${id}) was rejected.`
    }]
  });

  var url = getURL(json.response_url);

  var options = {
    hostname: url.hostname,
    path: url.pathname,
    method: 'POST',
    headers: {        
      'Content-Type': 'application/json'
    }
  };

  var req = https.request(options, function(res) {

    res.setEncoding('utf8');
        
    res.on('end', function () {
      callback(null, {
        statusCode: 200
      })
    });
  });
    
  req.on('error', function (e) {
    console.log('Problem with request:', e.message);
  });

  req.write(postData);
  req.end();

  callback(null, {
    statusCode: 200
  })    
}

我解析了事件负载,并检查了 action 值是否为 reject。 如果不是,它必然是 keep——没什么可做的。 但如果是,我需要调用 Netlify 的 API 来删除被拒绝的评论。 我使用以下代码行检索之前放在第一个附件文本中的评论 ID

json.original_message.attachments[0].text;

完成之后,我可以使用一个 API 调用将其删除。 然后,我通过调用响应 URL 为我们的 Slack 用户提供反馈。

我对这里最终的工作流程感到有些自豪 TBH

5. 在 Netlify 上部署 Gatsby 网站

在这里,我将所有内容推送到 GitHub,并使用以下设置将其连接到 Netlify

你可以看到我们在演示中使用了一些环境变量,并使用了 process.env.{variable} 符号。

这些是私有设置,我们不想公开。 要直接定义你自己的设置,请在 Netlify 的仪表板中转到 **设置/部署**,点击 **编辑变量**,然后输入以下内容

  • netlify_access_token:之前在 Netlify 中创建的令牌
  • site_id:你的网站 URL,不包括协议
  • slack_webhook_url:你的 Slack 应用 Webhook URL

网站已部署; 现在可以尽情玩耍了!

阅读这篇文章,了解如何在 Netlify 上部署网站。

实时演示和 GitHub 仓库

GitHub 仓库

结语

这个演示比我预期的花费了更多时间——我承认,我部分原因是文档解读错误。

挑战主要在于以良好的方式将所有服务捆绑在一起。 最大的障碍是,函数在部署到 Netlify 时无法正常加载; 它们会在每次调用时出错。 出于某种原因,我的项目节点依赖项无法访问。 我决定放弃我用来发出请求的包,而是使用传统的 https 原生包。

此外,不断地推送到 Netlify 来测试我的函数,担心它们的行为与本地环境不同,也让人很头疼。

现在,可以进行一些调整来改进这个演示,但总的来说,我非常满意。 对于静态网站来说,这已经相当动态了,不是吗? :)

我真诚地希望这能帮助开发人员开始使用 Netlify 的后端功能。 如果你确实构建了类似的 JAMstack 项目,请务必与我们分享。

我们很乐意深入研究你的代码!