抽象化基础设施是我们的 DNA。 道路、学校、供水网络——您懂的。 Web 开发也不例外: 无服务器架构 是这种现象的完美体现。 **静态网站,特别是正在转变为动态、丰富的体验。**
处理 静态表单、身份验证 和静态生成网站上的后端功能现在已经成为可能。 特别是像 JAMstack 这样的先锋平台,比如 Netlify。 最近,他们在 宣布支持面向前端的网站和应用程序上的 AWS Lambda 函数。 我一直想深入研究他们的“后端”功能。
今天,我正在做这件事,使用 **静态 Gatsby 网站、Netlify 的表单、身份验证和函数功能**。 本教程将向您展示如何
- 将静态表单添加到您的网站
- 为受密码保护的内容添加用户身份验证
- 创建 AWS Lambda 函数
准备好使用无服务器功能增强静态网站了吗?
在阅读完本文后,请考虑查看 Netlify 的 React 静态 CMS! 这里还有一个使用 JAMstack 的 完整评论工作流程教程,包含批准系统。
静态网站表单、身份验证和 AWS Lambda 函数

在深入研究代码之前,让我们详细说明我们的用例。 我将使用三种不同的 Netlify 无服务器功能
1. 身份验证
身份验证 将用于在 Gatsby 网站上创建受密码保护的门控内容部分。 没有后端的身份验证一直是静态网站的一个难题。 但是这个巧妙的功能优雅地解决了这个问题,允许开发人员
管理注册、登录、密码恢复等——所有这些都不需要自己滚动身份验证服务。
2. 表单
表单 将用于启用用户在网站上提交的产品评论。 动态表单可以采取多种形式(您看到了吗?),从简单的联系表单到评论、报价和评论系统,这些系统与内部工具相连。
有 大量解决方案 可用于处理静态网站上的交互式表单。 但是使用 **表单**,您可以在您的构建和托管服务(Netlify 的核心产品)中直接处理它们。 不需要垃圾邮件陷阱 mailto:
链接、配置自己的服务器、设置无服务器函数或集成第三方,如 Formspree 或 FormKeep。
不需要 JavaScript、API 或后端:只需一个用 netlify
HTML 属性标记的简单 HTML 表单。
3. 函数
函数 将用于直接在 Slack 中设置评论审核工作流程。 此功能背后的技术是 AWS Lambda 函数——您可以运行的事件触发的可扩展后端代码,而无需您自己的服务器。 使用 Netlify 部署它们与将文件添加到 Git 仓库一样简单。
Lambda 函数确实功能强大,但它们通常需要一个 AWS 帐户和 API 网关配置。 与 **表单** 一样,**函数** 通过将繁重的工作卸载到 Netlify 来简化您的生活
您的函数与您的 Netlify 网站的其余部分一起进行版本控制、构建和部署,而 Netlify API 网关会自动处理服务发现。 此外,您的函数可以从部署预览和回滚的强大功能中受益。
简而言之,函数允许您增强网站交互性并连接前端和后端,以让数据在服务之间流动。
Netlify 上的无服务器 Gatsby:身份验证、静态表单和 Lambda 函数

我将使用 我们之前构建的一个 Gatsby 网站的简化版本 来启动本教程。
要了解 Gatsby 基础知识,请查看 官方教程。 我们还有两个使用 Gatsby 的电子商务教程 这里 和 这里。
先决条件
在本教程中,您需要
- 一个免费的 Netlify 帐户
- 用于 Slack 集成的 Netlify 表单 Pro(可选——付费功能)
1. 分叉 Gatsby 项目
首先分叉存储库
我建议您尝试一下,熟悉该项目。 产品位于 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.js
和 answerslack.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 仓库

结语
这个演示比我预期的花费了更多时间——我承认,我部分原因是文档解读错误。
挑战主要在于以良好的方式将所有服务捆绑在一起。 最大的障碍是,函数在部署到 Netlify 时无法正常加载; 它们会在每次调用时出错。 出于某种原因,我的项目节点依赖项无法访问。 我决定放弃我用来发出请求的包,而是使用传统的 https
原生包。
此外,不断地推送到 Netlify 来测试我的函数,担心它们的行为与本地环境不同,也让人很头疼。
现在,可以进行一些调整来改进这个演示,但总的来说,我非常满意。 对于静态网站来说,这已经相当动态了,不是吗? :)
我真诚地希望这能帮助开发人员开始使用 Netlify 的后端功能。 如果你确实构建了类似的 JAMstack 项目,请务必与我们分享。
我们很乐意深入研究你的代码!