使用 Cloudflare Workers 构建全栈无服务器应用程序

Avatar of Kristian Freeman
Kristian Freeman

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

我最喜欢的软件开发进展之一是 无服务器 的出现。 作为一个倾向于陷入部署和 DevOps 细节的开发者,能够使用一种构建 Web 应用程序的模式,该模式简单地将扩展和基础设施从我身边抽象出来,这让人耳目一新。 无服务器让我更擅长实际交付项目!

话虽如此,如果您是无服务器的新手,您可能不清楚如何将您已经了解的东西转换为新的范式。 如果您是前端开发者,您可能对无服务器声称要从您身边抽象出来的东西没有经验 - 那么您如何才能开始呢?

今天,我将尝试通过将项目从构思到生产,使用 Cloudflare Workers 来帮助您消除使用无服务器的实际部分的神秘感。 我们的项目将是一个每日排行榜,名为“Repo Hunt”,灵感来自像 Product HuntReddit 这样的网站,用户可以在其中提交和点赞来自 GitHubGitLab 的酷炫开源项目。 您可以查看网站的最终版本,已发布 此处

Workers 是一个无服务器应用程序平台,构建在 Cloudflare 网络 之上。 当您将项目发布到 Cloudflare Workers 时,它会立即分布到全球 180 个(并且还在不断增加)城市,这意味着无论您的用户位于何处,您的 Workers 应用程序都将从附近的 Cloudflare 服务器提供服务,延迟极低。 最重要的是,Workers 团队已经全力以赴地进行开发者体验:我们本月早些时候发布的最新版本引入了名为 Wrangler 的功能齐全的命令行工具,它使用几个易于学习且功能强大的命令来管理构建、上传和发布您的无服务器应用程序。

最终的结果是一个平台,它允许您只需编写 JavaScript 并将其部署到 URL - 不必再担心“Docker”的含义,或者您的应用程序是否会在登上 Hacker News 首页时崩溃!

如果您是那种想先看看项目,然后再开始冗长的教程的人,那么您来对地方了! 此项目的源代码可在 GitHub 上获得。 现在,让我们跳入命令行并构建一些很棒的东西。

安装 Wrangler 并准备我们的工作区

Wrangler 是用于生成、构建和发布 Cloudflare Workers 项目的命令行工具。 我们已使其安装变得超级容易,特别是如果您之前使用过 npm。

npm install -g @cloudflare/wrangler

安装 Wrangler 后,您可以使用 generate 命令创建一个新项目。 Wrangler 项目使用“模板”,这些模板是为使用 Workers 的开发人员重复使用而构建的代码存储库。 我们维护着一个不断增长的模板列表,以帮助您在 Workers 中构建各种项目:查看我们的 模板库 以开始使用!

在本教程中,我们将使用 “路由器” 模板,它允许您在 Workers 之上构建基于 URL 的项目。 generate 命令接受两个参数:第一个是您的项目名称(我将使用 repo-hunt),第二个是 Git URL。 这是我最喜欢的 generate 命令部分:您可以通过将 Wrangler 指向 GitHub URL 来使用各种模板,因此共享、分叉和协作模板非常容易。 现在让我们运行 generate 命令

wrangler generate repo-hunt https://github.com/cloudflare/worker-template-router
cd repo-hunt

路由器模板包括对使用 webpack 构建项目的支持,因此您可以将 npm 模块添加到您的项目,并使用您所熟知的和喜爱的所有 JavaScript 工具。 此外,正如您可能预期的那样,该模板包括一个 Router 类,它允许您在您的 Worker 中处理路由,并将它们绑定到函数。 让我们看一个简单的示例:设置一个 Router 实例,处理对 /GET 请求,并返回对客户端的响应

// index.js
const Router = require('./router')

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
  try {
    const r = new Router()
    r.get('/', () => new Response("Hello, world!"))
    const resp = await r.route(request)
    return resp
  } catch (err) {
    return new Response(err)
  }
}

所有 Workers 应用程序都从监听 fetch 事件开始,该事件是来自客户端到您的应用程序的传入请求。 在该事件监听器内部,常见的做法是调用一个 handleRequest 函数,该函数查看传入的请求并确定如何响应。 在处理传入的 fetch 事件(指示传入请求)时,Workers 脚本应始终返回一个 Response 给用户:它与许多 Web 框架(如 Express)类似的请求/响应模式,因此如果您之前使用过 Web 框架,它应该感觉很熟悉!

在我们的示例中,我们将使用几个路由:一个“根”路由 (/),它将渲染我们网站的主页;一个用于提交新仓库的表单,位于 /post,以及一个用于接受 POST 请求的特殊路由,当用户从表单提交仓库时,位于 /repo

构建路由并渲染模板

我们将设置的第一个路由是位于路径 / 的“根”路由。 这将是社区提交的仓库将被渲染的地方。 现在,让我们先练习定义路由并返回纯 HTML。 这种模式在 Workers 应用程序中很常见,因此在继续进行更有趣的部分之前,先了解它是有意义的!

首先,我们将更新 index.js 以设置 Router 的实例,处理对 / 的任何 GET 请求,并调用来自 handlers/index.js 的函数 index(稍后会详细介绍)。

// index.js
const Router = require('./router')
const index = require('./handlers/index')

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

function handleRequest(request) {
  try {
    const r = new Router()
    r.get('/', index)
    return r.route(request)
  } catch (err) {
    return new Response(err)
  }
}

与上一节中的示例 index.js 一样,我们的代码监听 fetch 事件,并通过调用 handleRequest 函数来响应。 handleRequest 函数设置 Router 的实例,该实例将在对 / 的任何 GET 请求上调用 index 函数。 借助路由器设置,我们使用 r.route 路由传入请求,并将其作为响应返回给客户端。 如果出现任何错误,我们只需将函数的内容包装在一个 try/catch 块中,并将 err 返回给客户端(这里要注意:在生产应用程序中,您可能需要更强大的东西,例如记录到异常监控工具)。

为了继续设置我们的路由处理程序,我们将创建一个新文件 handlers/index.js,该文件将接收传入的请求并返回一个 HTML 响应给客户端

// handlers/index.js
const headers = { 'Content-Type': 'text/html' }
const handler = () => {
  return new Response("Hello, world!", { headers })
}
module.exports = handler

我们的 handler 函数很简单:它返回一个新的 Response 实例,其中包含文本“Hello, world!”以及一个将 Content-Type 标头设置为 text/htmlheaders 对象 - 这告诉浏览器将传入的响应渲染为 HTML 文档。 这意味着当客户端对路由 / 发出 GET 请求时,将使用文本“Hello, world!”构建一个新的 HTML 响应并返回给用户。

Wrangler 有一个 preview 函数,非常适合测试我们新函数的 HTML 输出。 现在让我们运行它以确保我们的应用程序按预期工作

wrangler preview

preview 命令应在构建您的 Workers 应用程序并将其上传到我们的测试游乐场后,在您的浏览器中打开一个新选项卡。 在预览选项卡中,您应该看到渲染的 HTML 响应

我们的 HTML 响应出现在浏览器中后,让我们使 handler 函数更令人兴奋一些,通过返回一些外观不错的 HTML。 为此,我们将为我们的路由处理程序设置一个相应的 index “模板”:当请求进入 index 处理程序时,它将调用模板并返回一个 HTML 字符串,以向客户端提供适当的用户界面作为响应。 首先,让我们更新 handlers/index.js 以使用我们的模板返回响应(此外,还要设置一个 try/catch 块来捕获任何错误,并将其作为响应返回)。

// handlers/index.js
const headers = { 'Content-Type': 'text/html' }
const template = require('../templates/index')

const handler = async () => {
  try {
    return new Response(template(), { headers })
  } catch (err) {
    return new Response(err)
  }
}

module.exports = handler

正如您可能想象的那样,我们需要设置一个相应的模板! 我们将创建一个新文件 templates/index.js,并使用 ES6 模板字符串返回一个 HTML 字符串

// templates/index.js
const template = () => {
  return <code><h1>Hello, world!</h1>`
}

module.exports = template

我们的 template 函数返回一个简单的 HTML 字符串,它被设置为 handlers/index.js 中的 Response 的主体。 对于我们第一个路由的模板的最后一段代码,让我们做一些稍微更有趣的事情:创建一个 templates/layout.js 文件,它将成为所有模板将渲染到的基本“布局”。 这将允许我们为所有模板设置一致的样式和格式。 在 templates/layout.js 中

// templates/layout.js
const layout = body => `
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Repo Hunt</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.5/css/bulma.min.css">
  </head>
  <body>
    <div class="container">
      <div class="navbar">
        <div class="navbar-brand">
          Repo Hunt
          Find cool open-source projects daily
        </div>
        <div class="navbar-menu">
          <div class="navbar-end">
            <div class="navbar-item">
              Post a repository
            </div>
          </div>
        </div>
      </div>
      <div class="section">
        ${body}
      </div>
    </div>
  </body>
</html>
`
module.exports = layout

这是一个很大的 HTML 代码块,但分解一下,只有几件重要的事情需要注意:首先,这个 layout 变量是一个函数! 传入了一个 body 变量,旨在包含在 HTML 代码段中间的 div 中。 此外,我们还包含了 Bulmahttps://bulma.org.cn) CSS 框架https://bulma.org.cn),以在我们的项目中提供一些简单的样式,以及一个导航栏,以告诉用户这个网站是 *什么*,并提供一个链接来提交新的仓库。

为了使用我们的 layout 模板,我们将将其导入 templates/index.js 中,并用它包装我们的 HTML 字符串

// templates/index.js
const layout = require('./layout')

const template = () => {
  return layout(`<h1>Hello, world!</h1>`)
}

module.exports = template

这样一来,我们就可以再次运行 wrangler preview,查看我们漂亮渲染的 HTML 页面,并从 Bulma 获得一些样式帮助

使用 Workers KV 存储和检索数据

大多数 Web 应用程序如果没有某种数据持久性,就不会很有用。 Workers KV 是一个为 Workers 使用而构建的键值存储 - 可以将其视为一个超快且全球分布式的 Redis。 在我们的应用程序中,我们将使用 KV 存储我们应用程序的所有数据:每次用户提交新的存储库时,它都会存储在 KV 中,我们还将生成一个每天的存储库数组,以便在主页上呈现。

快速说明:在撰写本文时,使用 Workers KV 需要付费 Workers 计划。 在 Workers 文档的“定价”部分中了解更多信息 此处

在 Workers 应用程序中,您可以引用一个预定义的 KV 命名空间,我们将在 Cloudflare UI 中创建该命名空间,并在应用程序部署到 Workers 应用程序后将其绑定到我们的应用程序。 在本教程中,我们将使用名为 REPO_HUNT 的 KV 命名空间,并且作为部署过程的一部分,我们将确保将其附加到我们的应用程序,以便代码中对 REPO_HUNT 的任何引用都将正确解析为 KV 命名空间。

在我们开始在命名空间中创建数据之前,让我们看一下在 Workers 应用程序中使用 KV 的基础知识。 给定一个命名空间(例如 REPO_HUNT),我们可以使用 put 设置具有给定值的键。

const string = "Hello, world!"
REPO_HUNT.put("myString", string)

我们还可以使用 async/await 检索该键的值,并等待 Promise 解析。

const getString = async () => {
  const string = await REPO_HUNT.get("myString")
  console.log(string) // "Hello, world!"
}

API 非常简单,这对于希望使用 Workers 平台开始构建应用程序而无需深入研究关系数据库或任何外部数据服务的 Web 开发人员来说非常棒。 在我们的例子中,我们将通过保存来存储应用程序的数据

  1. 一个 repo 对象,存储在键 repos:$id 处,其中 $id 是新提交的存储库生成的 UUID
  2. 一个 day 数组,存储在键 $date 处(例如 "6/24/2019"),包含一个存储库 ID 列表,表示当天提交的存储库。

我们将首先实现对提交存储库的支持,并通过将存储库数据保存在我们上面指定的对象中来对我们的 KV 命名空间进行首次写入。 在此过程中,我们将为与我们的存储交互创建一个简单的 JavaScript 类 - 在继续渲染主页时,我们将再次使用该类,在那里我们将检索存储库数据,构建 UI 并完成我们的示例应用程序。

允许用户提交数据

无论应用程序是什么,Web 开发人员似乎总是最终不得不编写表单。 在我们的例子中,我们将为用户构建一个简单的表单来提交存储库。

在本教程的开头,我们将 index.js 设置为处理对根路由(`/`)的传入 GET 请求。 为了支持用户添加新的存储库,我们将添加另一个路由,GET /post,它将向用户呈现一个表单模板。 在 index.js 中

// index.js

// ...

const post = require('./handlers/post')

// ...

function handleRequest(request) {
  try {
    const r = new Router()
    r.get('/', index)
    r.get('/post', post)
    return r.route(request)
  } catch (err) {
    return new Response(err)
  }
}

除了 index.js 中的新路由处理程序外,我们还将添加 handlers/post.js,这是一个新的函数处理程序,它将向用户呈现一个关联的模板作为 HTML 响应

// handlers/post.js
const headers = { 'Content-Type': 'text/html' }
const template = require('../templates/post')

const handler = request => {
  try {
    return new Response(template(), { headers })
  } catch (err) {
    return new Response(err)
  }
}

module.exports = handler

难题的最后一块是 HTML 模板本身 - 就像我们之前的模板示例一样,我们将重新使用我们构建的 layout 模板,并用它包装一个简单的三字段表单,将 HTML 字符串从 templates/post.js 导出

// templates/post.js
const layout = require('./layout')

const template = () =>
  layout(`
  <div>
    <h1>Post a new repo</h1>
    <form action="/repo" method="post">
      <div class="field">
        <label class="label" for="name">Name</label>
        <input class="input" id="name" name="name" type="text" placeholder="Name" required></input>
      </div>
      <div class="field">
        <label class="label" for="description">Description</label>
        <input class="input" id="description" name="description" type="text" placeholder="Description"></input>
      </div>
      <div class="field">
        <label class="label" for="url">URL</label>
        <input class="input" id="url" name="url" type="text" placeholder="URL" required></input>
      </div>
      <div class="field">
        <div class="control">
          <button class="button is-link" type="submit">Submit</button>
        </div>
      </div>
    </form>
  </div>
<code>)

module.exports = template

使用 wrangler preview,我们可以导航到路径 /post 并查看我们呈现的表单

如果您查看模板中实际 form 标记的定义,您会注意到我们正在对路径 /repo 发出 POST 请求。 为了接收表单数据并将它持久化到我们的 KV 存储中,我们将添加另一个处理程序。 在 index.js 中

// index.js

// ...

const create = require('./handlers/create')

// ...

function handleRequest(request) {
  try {
    const r = new Router()
    r.get('/', index)
    r.get('/post', post)
    r.post('/repo', create)
    return r.route(request)
  } catch (err) {
    return new Response(err)
  }
}

当表单被发送到一个端点时,它会被发送为一个 查询字符串。 为了使我们的生活更轻松,我们将在项目中包含 qs 库,它将允许我们简单地将传入的查询字符串解析为一个 JS 对象。 在命令行中,我们将简单地使用 npm 添加 qs。 在这里,我们还将安装 node-uuid 包,我们将在以后使用它来为新的传入数据生成 ID。 为了安装它们,使用 npm 的 install --save 子命令

npm install --save qs uuid

有了它,我们可以为 POST /repo 实现相应的处理程序函数。 在 handlers/create.js

// handlers/create.js
const qs = require('qs')

const handler = async request => {
  try {
    const body = await request.text()

    if (!body) {
      throw new Error('Incorrect data')
    }

    const data = qs.parse(body)
    
    // TODOs:
    // - Create repo
    // - Save repo
    // - Add to today's repos on the homepage

    return new Response('ok', { headers: { Location: '/' }, status: 301 })
  } catch (err) {
    return new Response(err, { status: 400 })
  }
}

module.exports = handler

我们的处理程序函数非常简单 - 它在 request 上调用 text,等待 Promise 解析以获取我们的查询字符串 body。 如果请求中没有提供 body 元素,则处理程序将抛出错误(这将返回状态代码 400,这得益于我们的 try/catch 块)。 给定一个有效的 body,我们将在导入的 qs 包上调用 parse,并获得一些数据。 现在,我们已经模拟了我们对剩余代码的意图:首先,我们将根据数据创建一个存储库。 我们将保存该存储库,然后将其添加到当天的存储库数组中,以便在主页上呈现。

为了将我们的存储库数据写入 KV,我们将构建两个简单的 ES6 类,以进行一些轻量级的验证并为我们的数据类型定义一些持久化方法。 虽然您可以直接调用 REPO_HUNT.put,但如果您正在处理大量类似的数据,那么执行类似 new Repo(data).save() 的操作可能很不错 - 事实上,我们将实现几乎完全类似于此的操作,这样使用 Repo 就会变得非常容易和一致。

让我们定义 store/repo.js,它将包含一个 Repo 类。 使用此类,我们可以实例化新的 Repo 对象,并使用 constructor 方法,我们可以传入数据并进行验证,然后继续在我们的代码中使用它。

// store/repo.js
const uuid = require('uuid/v4')

class Repo {
  constructor({ id, description, name, submitted_at, url }) {
    this.id = id || uuid()
    this.description = description
    
    if (!name) {
      throw new Error(`Missing name in data`)
    } else {
      this.name = name 
    }
    
    this.submitted_at = submitted_at || Number(new Date())
    
    try {
      const urlObj = new URL(url)
      const whitelist = ['github.com', 'gitlab.com']

      if (!whitelist.some(valid => valid === urlObj.host)) {
        throw new Error('The URL provided is not a repository')
      }
    } catch (err) {
      throw new Error('The URL provided is not valid')
    }

    this.url = url
  }

  save() {
    return REPO_HUNT.put(`repos:${this.id}`, JSON.stringify(this))
  }
}

module.exports = Repo

即使您不熟悉 ES6 类中的 constructor 函数,本例也应该很容易理解。 当我们想要创建一个新的 Repo 实例时,我们将相关数据作为对象传递给 constructor,使用 ES6解构赋值 将每个值提取到它自己的键中。 使用这些变量,我们将遍历它们中的每一个,将 this.$key(例如 this.namethis.description 等)分配给传入的值。

这些值中的许多都有一个“默认”值:例如,如果没有将 ID 传递给构造函数,我们将生成一个新的 ID,使用我们之前保存的 uuid 包的 v4 变体来使用 uuid() 生成一个新的 UUID。 对于 submitted_at,我们将生成一个新的 Date 实例并将其转换为 Unix 时间戳,对于 url,我们将确保 URL 有效 *并且* 来自 github.comgitlab.com,以确保用户提交的是真正的存储库。

有了它,save 函数(可以在 Repo 的实例上调用)将 Repo 实例的 JSON 字符串化版本插入 KV,并将键设置为 repos:$id。 回到 handlers/create.js 中,我们将导入 Repo 类,并使用我们之前解析的 data 保存一个新的 Repo

// handlers/create.js

// ...

const Repo = require('../store/repo')

const handler = async request => {
  try {
    // ...
    
    const data = qs.parse(body)
    const repo = new Repo(data)
    await repo.save()

    // ...
  } catch (err) {
    return new Response(err, { status: 400 })
  }
}

// ...

有了它,一个基于传入表单数据的新的 Repo 实际上应该被持久化到 Workers KV 中! 在保存存储库的同时,我们还想设置另一个数据模型 Day,它包含一个简单的用户在特定日期提交的存储库列表。 让我们创建一个新的文件 store/day.js,并将其填充

// store/day.js
const today = () => new Date().toLocaleDateString()
const todayData = async () => {
  const date = today()
  const persisted = await REPO_HUNT.get(date)
  return persisted ? JSON.parse(persisted) : []
}

module.exports = {
  add: async function(id) {
    const date = today()
    let ids = await todayData()
    ids = ids.concat(id)
    return REPO_HUNT.put(date, JSON.stringify(ids))
  }
}

请注意,此代码甚至不是一个类 - 它是一个键值对对象,其中值是函数! 我们很快将向其中添加更多内容,但我们定义的单个函数 add 将从今天的日期加载任何现有的存储库(使用 today 函数来生成一个日期字符串,用作 KV 中的键),并添加一个新的 Repo,基于传递给该函数的 id 参数。 回到 handlers/create.js 中,我们将确保导入并调用这个新函数,这样任何新的存储库都会立即添加到当天的存储库列表中

// handlers/create.js

// ...

const Day = require('../store/day')

// ...

const handler = async request => {
  try {

    // ...
    
    await repo.save()
    await Day.add(repo.id)

    return new Response('ok', { headers: { Location: '/' }, status: 301 })
  } catch (err) {
    return new Response(err, { status: 400 })
  }
}

// ...

现在,我们的存储库数据持久化到 KV 中 *并且* 已添加到用户当天提交的存储库列表中。 让我们继续学习教程的最后一部分,获取这些数据并在主页上渲染它。

渲染数据

在这一点上,我们已经实现了在 Workers 应用程序中渲染 HTML 页面,以及获取传入数据并将它持久化到 Workers KV 中。 了解从 KV 中获取这些数据并用它来渲染一个 HTML 页面,即我们的主页,这一点并不奇怪,这与我们迄今为止所做的一切非常相似。 回想一下,路径 / 与我们的 index 处理程序相关联:在该文件中,我们将要加载今天的存储库,并将它们传递给模板,以便进行渲染。 我们需要实现一些部分才能使它正常工作 - 首先,让我们看一下 handlers/index.js

// handlers/index.js

// ...
const Day = require('../store/day')

const handler = async () => {
  try {
    let repos = await Day.getRepos()
    return new Response(template(repos), { headers })
  } catch (err) {
    return new Response(`Error! ${err} for ${JSON.stringify(repos)}`)
  }
}

// ...

虽然函数处理程序的一般结构应该保持不变,但现在我们准备将一些真实数据放入我们的应用程序中。 我们应该导入 Day 模块,并在处理程序内部,调用 await Day.getRepos 以获取 repos 列表(别担心,我们将很快实现相应的函数)。 使用该组 repos,我们将它们传递给我们的 template 函数,这意味着我们实际上可以在 HTML 中渲染它们。

Day.getRepos 内部,我们需要从 KV 中加载存储库 ID 列表,并为它们中的每一个从 KV 中加载相应的存储库数据。 在 store/day.js 中

// store/day.js

const Repo = require('./repo')

// ...

module.exports = {
  getRepos: async function() {
    const ids = await todayData()
    return ids.length ? Repo.findMany(ids) : []
  },
  
  // ...
}

getRepos 函数复用了我们之前定义的 todayData 函数,该函数返回一个 ids 列表。如果该列表包含 *任何* ID,我们希望实际检索这些仓库。同样,我们将调用一个尚未定义的函数,导入 Repo 类并调用 Repo.findMany,传入我们的 ID 列表。正如你可能想象的那样,我们应该跳到 store/repo.js,并实现相应的函数

// store/repo.js

class Repo {
  static findMany(ids) {
    return Promise.all(ids.map(Repo.find))
  }

  static async find(id) {
    const persisted = await REPO_HUNT.get(`repos:${id}`)
    const repo = JSON.parse(persisted)
    return persisted ? new Repo({ ...repo }) : null
  }

  // ...
}

为了支持查找一组 ID 的所有仓库,我们定义了两个类级或 static 函数,findfindMany,它们使用 Promise.all 为集合中的每个 ID 调用 find,并在它们全部完成之前解析 promise。find 中的大部分逻辑通过其 ID(使用之前定义的键 repos:$id)查找仓库,解析 JSON 字符串,并返回一个新实例化的 Repo 实例。

现在我们可以从 KV 中查找仓库了,我们应该获取这些数据并在我们的模板中实际呈现它们。在 handlers/index.js 中,我们将 repos 数组传递给 templates/index.js 中定义的 template 函数。在该文件中,我们将获取 repos 数组,并为其中的每个 repo 渲染 HTML 片段

// templates/index.js

const layout = require('./layout')
const dateFormat = submitted_at =>
  new Date(submitted_at).toLocaleDateString('en-us')

const repoTemplate = ({ description, name, submitted_at, url }) =>
  `<div class="media">
      <div class="media-content">
        <p>
          ${name}
        </p>
        <p>
          ${description}
        </p>
        <p>
          
            Submitted ${dateFormat(submitted_at)}
        </p>
      </div>
    </div>
  `

const template = repos => {
  const renderedRepos = repos.map(repoTemplate)

  return layout(`
  <div>
    ${
      repos.length
        ? renderedRepos.join('')
        : `<p>No repos have been submitted yet!</p>`
    }
  </div>
`)
}

module.exports = template

分解这个文件,我们有两个主要函数:template(我们最初导出函数的更新版本),它接受一个 repos 数组,遍历它们,调用 repoTemplate,以生成一个 HTML 字符串数组。如果 repos 是一个空数组,该函数 simply 返回一个带有空状态的 p 标签。repoTemplate 函数使用解构赋值来设置从传递给函数的 repo 对象内部设置 descriptionnamesubmitted_aturl 变量,并将它们分别渲染成相当简单的 HTML,依靠 Bulma 的 CSS 类来快速定义一个 媒体对象 布局。

就这样,我们完成了项目的代码编写!在 Workers 上构建了一个非常全面的全栈应用程序之后,我们来到了最后一步:将应用程序部署到 Workers 平台。

将你的网站部署到 workers.dev

每个 Workers 用户都可以申请一个免费的 Workers.dev 子域名,在 注册 Cloudflare 账户 后。在 Wrangler 中,我们使用 subdomain 子命令,使申请和配置你的子域名变得超级容易。每个账户只能获得一个 Workers.dev 子域名,所以请明智选择!

wrangler subdomain my-cool-subdomain

配置好子域名后,我们现在可以部署我们的代码了!wrangler.toml 中的 name 属性将指示我们的应用程序将部署到的最终 URL:在我的代码库中,name 设置为 repo-hunt,我的子域名是 signalnerve.workers.dev,因此我的项目的最终 URL 将为 repo-hunt.signalnerve.workers.dev。让我们使用 publish 命令部署项目

wrangler publish

在我们可以在浏览器中查看项目之前,我们还需要完成一个步骤:进入 Cloudflare UI,创建一个 KV 命名空间,并将其绑定到我们的项目。要启动此过程,请登录你的 Cloudflare 仪表板,然后选择页面右侧的“Workers”选项卡。

在你的仪表板的 Workers 部分,找到“KV”菜单项,并创建一个新的命名空间,与你在代码库中使用的命名空间匹配(如果你遵循代码示例,这将是 REPO_HUNT)。

在 KV 命名空间列表中,复制你的命名空间 ID。回到我们的项目中,我们将向我们的 wrangler.toml 添加一个 kv-namespaces 键,以便在代码库中使用我们的新命名空间

# wrangler.toml
[[kv-namespaces]]
binding = "REPO_HUNT"
id = "$yourNamespaceId"

为了确保你的项目使用新的 KV 命名空间,最后再发布一次你的项目

wrangler publish

这样,你的应用程序应该能够成功地从你的 KV 命名空间中读取和写入数据。打开我的项目的 URL 应该会显示我们项目的最终版本——一个完整的、数据驱动的应用程序,无需管理任何服务器,完全构建在 Workers 平台上!

接下来是什么?

在本教程中,我们使用 Wrangler(Cloudflare 的用于构建和部署 Workers 应用程序的命令行工具)在 Workers 平台之上构建了一个全栈无服务器应用程序。你可以做很多事情来继续添加这个应用程序:例如,能够为提交投票,甚至允许评论和其他类型的数据。如果你想查看此项目的完成代码库,请查看 GitHub 仓库

Workers 团队不断维护着越来越多的新模板,你可以使用它们开始构建项目——如果你想看看你能构建什么,请务必查看我们的 模板库。此外,请务必查看 Workers 文档 中的一些教程,例如 构建 Slack 机器人二维码生成器

如果你完成了整个教程(或者如果你正在构建你想分享的酷炫的东西),我很想听听你的感受 在 Twitter 上。如果你对无服务器感兴趣,并且想关注我发布的任何新教程,请务必 加入我的时事通讯订阅我的 YouTube 频道