[译]将Kobweb部署到云端

Kobweb是一个基于Compose HTML构建的框架,Compose HTML是JetBrains提供的一个响应式Web UI框架。它允许你使用强大的API用Kotlin创建Web应用。

Note

你也可以在这篇早期文章中了解更多关于Kobweb的信息。

Kobweb提供了一个称为API路由的功能。本质上,这些是当你在网站上获取特定URL时会被调用的函数(下面会详细讨论▼)。它们可能非常有用,但要使用它们,你需要在互联网上运行一个Kobweb服务器。

在这篇文章中,我们将讨论如何使用Render将你的Kobweb项目部署到云端,Render是一个流行的托管服务,可以免费托管和管理你的Web服务。

背景

我们在深入创建和部署我们的网站之前,让我们先介绍一些有用的背景信息。不过,如果你已经熟悉这些概念,可以直接跳到实践部分▼

你真的需要运行自己的Web服务器吗?

与开发全栈应用相比,创建由静态托管服务提供服务的纯客户端网站通常开发和部署更快。

你可以在大约1-2分钟内推出网站的新迭代(主要受限于导出网站所需的时间),而在云端部署服务器则需要5-10分钟(或更长!)。

静态网站始终在不间断运行(除了偶尔的服务器中断),而云服务器有时需要实例化或唤醒。在像Render提供的免费托管层级上,这个过程可能需要长达10秒(甚至可能更长)。

此外,静态网站托管通常比通用云托管更具成本效益,因为静态托管服务器可以针对简单的文件传输进行优化。

令人惊讶的是,你可以创建一个具有大量动态行为的网站,而无需编写服务器。例如,像Firebase这样的服务可以为你管理数据库、存储和身份验证功能。在许多情况下,通过编写与其API通信的客户端代码,你可以提供与另一个花费更多时间和金钱实现全栈解决方案的网站相同的体验。

那么什么时候应该编写服务器呢?

尽管有上述警告,但有几个原因你可能想要编写服务器:

  • 你想运行一些只与你的网站相关的自定义代码。例如,你可能想在首次登录时收集用户的一系列答案,然后运行一些自定义算法来生成个性化体验。
  • 你想编写与私有后端服务(如存储私有用户数据的公司服务器)通信的代码,而不需要面向客户端的API。
  • 你想代表用户使用需要私有API密钥进行身份验证的外部服务(例如,ChatGPT API)执行一些工作。公开暴露这些凭据是一个重大的安全问题。
  • 你期望你的后端作为连接多个用户的中心(例如,聊天服务器)。

在这一点上,如果你仍然不确定,纯客户端网站可能是更好的选择。我在这篇文章中更详细地讨论了这种方法。

你总是可以从静态网站开始,如果情况需要,以后再迁移到云端的Web服务器。

如果你仍然在这里且没有被劝退,让我们继续!

服务器API路由

服务器API路由本质上是当用户获取与之关联的URL时触发的函数。下面,我们将演示一些具体的例子,帮助你更深入地理解这个功能。

API路由通常有两种类型 -- 只读查询和修改。

对于查询,GET操作很常见,而对于修改,POST用于添加数据,PUT用于替换数据,DELETE用于删除数据。还有其他几种HTTP方法你可以探索,但在实践中,仅使用GET和POST操作就可以完成很多工作。

GET

首先,让我们从声明一个简单的GET查询开始。这是一个生成唯一ID的API路由,客户端可以请求并根据需要使用:

// src/jvmMain/kotlin/api/id.kt
package api

@Api
fun generateId(ctx: ApiContext) {
  if (ctx.req.method != HttpMethod.GET) return
  ctx.res.setBodyText(UUID.randomUUID().toString())
}

@Api注解告诉Kobweb这个函数是应该被注册的API路由。如果你用这个注解标记一个函数,必须满足以下两个条件:

  1. 函数必须存在于api包下。
  2. 函数必须有一个类型为ApiContext的单一参数。
Tip

API方法可以根据需要标记为suspend

完整讨论ApiContext类超出了本文的范围,但如上所示,它包含两个属性:req表示用户的请求,res表示发送回给他们的响应。

如果你的API路由收到请求但没有显式设置响应的状态码和/或正文文本(例如通过提前返回),那么Kobweb将向客户端发送错误响应。

你可以使用curl并针对https://(yoursite.com)/api/id触发上述API路由。(注意API端点的名称来自文件名,而不是方法名!)

$ curl https://(yoursite.com)/api/id
# 返回例如 96763f81-7307-4c15-b8ca-2475ac16e5c3

POST

接下来,让我们看一个POST查询的例子:

// src/jvmMain/kotlin/api/user/add.kt
package api

@Api
fun addUser(ctx: ApiContext) {
  if (ctx.req.method != HttpMethod.POST) return

  val userId = ctx.req.params["id"]
  val name = ctx.req.params["name"]
  if (userId == null || name == null) {
    return
  }

  ctx.data.getValue<Database>().addUser(User(userId, name))
  ctx.res.status = 200
}

上述方法相当直观。这里的参数("id""name")来自URL查询参数。

换句话说,你可以使用curl和POST请求触发上述API路由:

$ curl -X POST https://(yoursite.com)/api/user/add?id=123&name=Kodee
Tip

有一个ctx.req.body属性,如果设置了,它将包含请求的主体。这是从客户端向服务器传递值的另一种方法。但为了简单起见,我们在这个例子中没有使用它。

@InitApi

在上面的POST示例中,你可能注意到了ctx.data.getValue<Database>()这一行,并想知道它是什么以及它来自哪里。

答案是Kobweb给你一种方法来用你想要的任何对象填充data属性。框架包含一个@InitApi注解,你可以将其应用于方法,这些方法将在服务器每次启动时被调用。这些启动方法是你可以初始化data的地方。

让我们继续实现我们自己的init方法,连接到一个数据库,使用 ourselves创建的Database类:

// src/jvmMain/kotlin/db/Database.kt
class Database {
    init {
        /* 设置到某个外部数据库服务的连接 */
    }

    fun addUser(user: User) { /* ... */ }
}

@InitApi
fun initDatabase(ctx: InitApiContext) {
    ctx.data.add(Database())
}

ctx.data属性持有一个类实例的集合,你可以向其添加任何对象,并稍后通过其类型检索它。

Note

一些敏锐的读者可能认出这是服务定位器模式

在启动时创建了我们的Database实例后,我们现在可以在任何@Api方法中使用ctx.data.getValue<Database>()访问它。

从客户端获取API路由

一旦你定义了API路由,你可以使用Kobweb提供的扩展window.api属性从客户端触发它们。

例如,对于前面的GET方法,你可以这样从客户端查询它:

// 将获取https://(yoursite.com)/api/id的API端点
window.api.tryGet("id")?.then { idBytes ->
    console.log("Got id: ${String(id)}")
}

API路由还有更多深度的内容没有在上面讨论,但这一瞥应该让你开始理解这个功能所提供的强大功能。

Docker容器的最小讨论

Docker容器是一个过于微妙和复杂的主题,无法在这里深入讨论。相反,我们将介绍你需要了解的最低限度,以便理解本文后面的步骤。

  1. Docker容器是一个轻量级的、可执行的软件包,包含以可移植方式运行应用程序所需的一切。
  2. 在构建新的Docker容器时,你从基础镜像开始,这是一个可以用作起点的预构建Docker容器。这通常非常精简,比如一个基础的Linux发行版。
  3. 你可以分层构建Docker镜像,后面的层可以有选择地从前面的层复制部分内容。通过这种方式,你可以在初始层下载一堆工具,这些工具会生成一堆输出,然后有选择地只将你需要的输出复制到最终镜像中。在那之后,你可以单独共享最终镜像,丢弃任何先前的层,这样可以节省空间。
  4. Dockerfile是一个包含如何构建Docker镜像的指令的文本文件。项目在根目录中包含一个Dockerfile是很常见的,这样某些服务可以在同步你的项目后找到它,然后自动构建镜像。

如果你想更深入地了解这个功能,你可能想阅读官方文档

Render快速介绍

Render是一个云服务,提供各种有用的产品和功能来托管Web应用。它对小项目是免费的,在Heroku开始对其以前的免费层收费后,它获得了显著的人气。我们在这篇文章中使用Render是因为它的免费服务。

Render提供几种不同的服务,包括静态网站托管。但是,在本文的剩余部分,我们将专注于Render的"Web服务"产品。

如果你感兴趣,你可以在Render的文档中了解更多关于Web服务的信息。

CORS

如果你已经熟悉CORS,那么我们理解它的记忆无疑正在引起你的不适。❤️‍🔥

强制执行这个限制的底层安全机制被称为同源策略(SOP)。SOP防止恶意网站请求其他网站的敏感数据。例如,如果你访问了一个恶意网站,它不应该能够向你的银行网站发出请求,然后读取响应来查看你的账户余额。

SOP默认阻止跨域服务器请求。CORS提供了一种通过允许受信任的例外来放宽这个策略的方式。

需要注意的是,并非所有操作都被SOP阻止。因此,你可能创建了一个不需要配置CORS就能运行良好的网站,但当你后来引入需要它的新功能时,才遇到问题。

这个简短的介绍应该能让你对CORS及其重要性有基本的了解。如果想深入了解,可以查看Mozilla关于CORSSOP的文档。

部署Kobweb服务器

现在我们已经介绍了必要的背景信息,是时候将Kobweb服务器部署到云端了!我们将按照以下步骤进行:

  • 创建一个Kobweb项目
  • 为我们的代码设置GitHub仓库
  • 在Render上创建账户
  • 配置项目以在Render上部署
  • 部署服务器

创建Kobweb项目

如果你已经有一个项目,可以跳过这一步。但是,如果你只是在探索并想要一个具体的例子,我们将在本指南中使用Kobweb模板中的演示TODO应用。

在终端中,导航到你存储项目的文件夹,执行以下命令:

# 例如在 ~/projects
$ kobweb create examples/todo
# Kobweb会问几个问题,但默认值就可以了
$ cd todo

这些步骤应该用git初始化你的项目。如果你选择不这样做,或者没有按预期工作,你可以手动初始化它:

$ git init -b main
$ git add . && git commit -m "Initial commit"

下一步是可选的,但为了在部署之前感受一下TODO应用,可以在本地运行它:

# 在todo目录中
$ cd site
$ kobweb run

创建一个新的GitHub仓库

按照官方说明创建一个新的GitHub仓库。 选择一个适合你项目的名称。在本指南中,我使用了kobweb-todo-on-render,但你可以选择一个更简洁和适合你具体项目的名称。

当有机会用README.gitignore填充这个仓库时,不要!Kobweb已经为你创建了这些文件。

完成这个过程后,将你的本地项目与GitHub仓库同步:

# REMOTE_URL看起来像
# https://github.com/<user>/<repo>.git
$ git remote add origin <REMOTE_URL>
$ git push -u origin main

创建Render账户

有几种方法可以创建Render账户,但为了简化和兼容后续步骤,我们将使用他们的GitHub登录流程。

Note

如果你已经有一个连接到GitHub的Render账户,请跳过此部分。如果你有一个未连接到GitHub的账户,请按照这些官方说明进行操作。

首先访问Render的注册页面并点击"GitHub"按钮。

Render Sign Up

你将被重定向到一个GitHub页面,提示你授权Render访问你的GitHub账户。Render是一家值得信赖的公司,所以这是一个安全的操作。点击"Authorize Render"继续!

GitHub Authorize Render

确认你的电子邮件并点击"Complete Sign Up"按钮。

Render Verify Email

检查你的收件箱,查找来自Render的电子邮件,其中包含确认你的电子邮件地址的链接。点击它将被重定向到Render仪表板。

现在就这些。我们将在后续步骤中返回Render。

配置CORS

返回你的Kobweb项目。

我们需要配置我们的Kobweb服务器与它将运行的域。

我们面临一个先有鸡还是先有蛋的问题,因为我们还不知道域名,因为你将在后续步骤中在Render上保留一些东西。如果名称已经被占用,可能会被拒绝。

尽管如此,我们将尽力现在配置它。如果你后来无法保留你想要的域名,只需在那时重新访问此步骤并更新配置。

Render Web服务托管提供的免费域名格式为$(servicename).onrender.com。在本指南中,我计划保留kobweb-todo.onrender.com

打开并编辑.kobweb/conf.yaml,然后添加一个CORS条目:

site:
  title: "Todo"

server:
  files:
     # ...

  # ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
  cors:
    hosts:
      - name: "kobweb-todo.onrender.com"
        schemes:
          - "https"
  # ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑

  # ...
Note

指定方案是可选的。如果你不指定它们,Kobweb默认为"http"和"https"。

添加Dockerfile

在你的项目根目录中创建一个名为Dockerfile的文件,并填充以下内容:

#-----------------------------------------------------------------------------
# Variables are shared across multiple stages (they need to be explicitly
# opted into each stage by being declaring there too, but their values need
# only be specified once).
ARG KOBWEB_APP_ROOT="site"
# ^ NOTE: Kobweb apps generally live in a root "site" folder in your project,
# but you can change this in case your project has a custom layout.

FROM eclipse-temurin:21 as java

#-----------------------------------------------------------------------------
# Create an intermediate stage which builds and exports our site. In the
# final stage, we'll only extract what we need from this stage, saving a lot
# of space.
FROM java as export

ENV KOBWEB_CLI_VERSION=0.9.18
ARG KOBWEB_APP_ROOT

ENV NODE_MAJOR=20

# Copy the project code to an arbitrary subdir so we can install stuff in the
# Docker container root without worrying about clobbering project files.
COPY . /project

# Update and install required OS packages to continue
# Note: Node install instructions from: https://github.com/nodesource/distributions#installation-instructions
# Note: Playwright is a system for running browsers, and here we use it to
# install Chromium.
RUN apt-get update \
    && apt-get install -y ca-certificates curl gnupg unzip wget \
    && mkdir -p /etc/apt/keyrings \
    && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
    && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list \
    && apt-get update \
    && apt-get install -y nodejs \
    && npm init -y \
    && npx playwright install --with-deps chromium

# Fetch the latest version of the Kobweb CLI
RUN wget https://github.com/varabyte/kobweb-cli/releases/download/v${KOBWEB_CLI_VERSION}/kobweb-${KOBWEB_CLI_VERSION}.zip \
    && unzip kobweb-${KOBWEB_CLI_VERSION}.zip \
    && rm kobweb-${KOBWEB_CLI_VERSION}.zip

ENV PATH="/kobweb-${KOBWEB_CLI_VERSION}/bin:${PATH}"

WORKDIR /project/${KOBWEB_APP_ROOT}

# Decrease Gradle memory usage to avoid OOM situations in tight environments
# (many free Cloud tiers only give you 512M of RAM). The following amount
# should be enough to build and export our site.
RUN mkdir ~/.gradle && \
    echo "org.gradle.jvmargs=-Xmx300m" >> ~/.gradle/gradle.properties

RUN kobweb export --notty

#-----------------------------------------------------------------------------
# Create the final stage, which contains just enough bits to run the Kobweb
# server.
FROM java as run

ARG KOBWEB_APP_ROOT

COPY --from=export /project/${KOBWEB_APP_ROOT}/.kobweb .kobweb

# Because many free tiers only give you 512M of RAM, let's limit the server's
# memory usage to that. You can remove this ENV line if your server isn't so
# restricted. That said, 512M should be plenty for most (all?) sites.
ENV JAVA_TOOL_OPTIONS="-Xmx512m"
ENTRYPOINT .kobweb/server/start.sh
Note

在撰写本文时,Kobweb CLI v0.9.18是最新版本,但在你阅读本文时可能有更新的版本(尽管旧版本仍应可用)。请参阅Kobweb README顶部的"kobweb cli"徽章,以了解最新版本。

Tip

Kobweb适用于Java 11,但一般建议使用更新的版本作为运行时,因为它们可能包含安全修复和性能改进。如你所见,我们在这里使用了JDK 21,但如果有可用的更新版本,你也可以使用。

eclipse-temurin镜像根据其文档设计,既可以用于运行应用程序,也可以作为基础镜像,这非常适合我们的需求。还有其他镜像,你可以进一步调查。

这个Dockerfile指示Render:

  1. 创建一个初始镜像,基于一个预装了JDK的精简镜像。
  2. 获取一组最小的应用程序来运行和导出我们的Kobweb项目。
  3. 创建一个最终镜像,基于一个预装了JRE的精简镜像。
  4. 仅将Kobweb导出过程的输出复制到最终镜像中。这允许Render丢弃在第一个镜像中创建的所有中间文件,节省空间并可能提高服务器的启动速度。
  5. 声明一个入口点,允许Render在需要启动新服务器时触发start.sh脚本。

部署你的网站

我们已经到了最后一步。

将CORS和Dockerfile更改添加并推送到你的仓库:

$ git add . && git commit -m "Configuration for deploying to a web service on Render"
$ git push

返回Render并打开你的仪表板。

从可用选项中,创建一个新的Web Service。这将提示你找到相关的GitHub仓库并"Connect"它。

Render Connect GitHub

之后,你将被引导到一个Web服务配置页面。你只需指定服务名称,其他默认设置应该可以正常工作。在我的例子中,我使用了"kobweb-todo",但你可能需要指定一个尚未被占用的名称。

准备好后,按"Create Web Service"。

Render Config Web Service

现在,等待Render按照你的Dockerfile中的指令操作。这个过程可能需要10分钟或更长时间,所以请耐心等待。

Render Deploy Screen

完成后,你应该会看到状态从灰色的"In progress"消息切换到绿色的"Live"指示器:

Render Live Indicator

点击你的Web服务的链接,查看你的网站!

Kobweb Site Deployed

Note

你的网站可能会感觉很慢,特别是在启动期间。这是免费服务的权衡!

此时,每当你向你的仓库推送新的提交时,Render将自动重新构建并重新部署你的网站。

Warning

TODO演示并未准备好用于生产环境!

请记住,TODO示例设计为演示用途,并不适用于生产环境。在其当前设计中:

  • 没有身份验证。 应用程序为你生成一个唯一ID,由你的浏览器本地保存。然而,这个ID不会在其他浏览器上或在其他机器上的同一浏览器上保留。
  • 没有滥用保护。 没有检查限制用户可以创建的TODO项或用户帐户的数量。
  • 没有错误处理。 如果在添加或删除TODO项时出现问题,应用程序将无限期旋转。
  • 没有数据库。 应用程序将所有TODO项存储在内存中,这意味着如果服务器崩溃,或被关闭,或启动了一个新实例并且你连接到该实例,所有先前的TODO项将丢失或无法访问。
  • 没有分页。 当用户访问网站时,所有他们的TODO项都会被获取。API应该更新为一次只获取一部分项。

你应该只将TODO演示视为你项目的起点。创建一个生产就绪的全栈应用程序需要相当大的努力,上述问题是你可能更愿意创建一个仅客户端的静态网站而不是全栈产品的额外原因。

结论

恭喜!你的Kobweb服务器现在应该在线了!

如果你遇到问题,可以将你自己的项目与我的项目进行比较。

这篇文章涵盖了在云端运行Kobweb服务器的基本要点。

对于完整的生产服务器体验,还有更多需要考虑的内容,包括:

  • 选择数据中心进行部署(以最小化用户的延迟)
  • 实现用户身份验证、登录和注销流程
  • 设计可扩展的后端以应对网站流量的增加
  • 使用去中心化数据库进行数据存储(以在服务器实例和崩溃之间保持状态)
  • 安全地存储秘密(如API密钥)

一般来说,随着你的后端需求的增长,你的Kobweb服务器将受益于被设计为与其他更长寿的去中心化服务通信的中间人。通过这种方式,你的服务器可以被关闭、重新启动,甚至可以复制以应对不同的负载。

Web服务主机(如Render、AWS、GCP、Azure等)旨在为你处理扩展!但你需要查阅他们的文档以获取设置指南。

然而,使用Kobweb和本文介绍的一般Dockerfile,你可以探索不同的选项,并在必要时将你的代码迁移到另一个服务,几乎不需要任何努力。

没有什么比看到你的网站在网上上线更令人兴奋。感谢像Render这样的公司为爱好者提供免费层,使得开发丰富、强大的Web应用程序变得比以往任何时候都更容易。

编码愉快!

致谢!

非常感谢Stevdza-San(主页YouTube频道)在实验这篇文章的工作时的合作。他向我介绍了Render,并且在我们测试多次尝试将Kobweb运行在Render上的过程中,他的耐心和反馈是无价的。