背景
一个最简单的 Web 应用程序由 3 层构成,即客户端(前端)、服务器端(后端)和持久层(数据库)。全栈开发指的是针对 Web 应用程序的全部 3 层的实践。
热门的栈
在上面提到的每一层使用的所有技术中,有一些技术比其他技术更受欢迎。当这些覆盖所有三层的技术结合在一起时,我们称之为“栈”,近些年来,我们已经看到了几个流行的栈。
现代全栈开发
在全栈开发方面,SPA(单页应用程序)和 PWA(渐进式 Web 应用程序)正在成为规范,并且出现了 SSR(服务器端渲染)等概念来解决它们的局限性。这些 Web 应用程序(前端)应该与后端 API(REST、GraphQL 等)一起工作,以便为终端用户提供最终功能。随之出现了诸如 BFF(服务于前端的后端)之类的概念,以使后端 API 与前端用户体验 (UX) 保持一致。
BFF?—?服务于前端的后端
一个组织可以有多个微服务,这些服务被不同的使用方使用,如移动应用程序、Web 应用程序、其他服务/API 和外部使用方。然而,现代 Web 应用程序需要一个紧密耦合的 API 来与前端 UX 紧密配合, 因此,BFF 充当了前端和微服务之间的接口。
一个 BFF 调用多个下游服务在前端构造一个视图。下游 API 可以是不同的类型(REST、GraphQL gRPC 等)。阅读模式:服务于前端的后端来深入了解 BFF 架构模式。
记住上面的概念,让我们进一步讨论一下现代 Web 开发。
全栈开发背景下的后端开发
开发后端 API 可能意味着两件事:
- 开发 BFF?—?充当前端 UX 和后端微服务之间的适配器。
- 开发微服务?—?开发前端直接或间接(通过 BFF)使用的单个微服务。在全栈开发的背景下,我们只需考虑由前端直接调用的后端 API。这些 API 可以编写为 BFF API 或单独的微服务。
选择栈
如今,开发人员不会因为一个栈很流行就去使用它,他们选择最合适的前端技术来匹配他们希望实现的 UI/UX。然后他们选择后端技术时会考虑几个因素,包括其上市时间、可维护性和开发人员的经验。
在这篇文章中,我将介绍一个新的并且很有前景的后端开发候选技术,Ballerina。在将来,当你在为全栈开发做技术选型时,可以考虑一下它。
什么是 Ballerina?
Ballerina 编程语言徽标
Ballerina 是一种开源的云原生编程语言,旨在简化 络服务的使用、组合和创建。Ballerina Swan Lake 是 Ballerina 语言于今年发布的下一个主要版本,它在所有方面都进行了重大改进,包括改进的类型系统、增强的类 SQL 语言集成查询、增强/直观的服务声明等等。
Ballerina 背后的主要动机是**让开发人员能够专注于业务逻辑,同时减少集成云原生技术所需的时间。**使用 Ballerina 的内置 络原语直接在云上定义和运行服务的方式在这场变革中发挥了关键作用。灵活的类型系统、面向数据的语言集成查询、增强和直观的并发处理以及内置的可观察性和跟踪支持,使 Ballerina 成为云时代的首选语言之一。
在开发前端直接调用的 API 时,我们有几个常用的选择:
Ballerina 与 络交互
Ballerina 编程语言的主要目标之一是简化 络交互代码的编写。考虑到这一点,Ballerina 在语言中内置了 络原语。当其他主流编程语言都将 络视为另一种 I/O 资源时,Ballerina 为 络交互提供了更为优秀的支持。为了实现这一目标,Ballerina 采用了以下优秀的组件设计:
- 在你的一项服务中,你可能想要发送一封电子邮件。为此,你需要一个电子邮件客户端。2. 同一个服务可能需要调用一个或多个内部 gRPC 服务。为此,你需要 gRPC 客户端。同样,编写服务需要调用外部服务。为此,Ballerina 有一个丰富的概念,称为客户端,外部调用由远程方法表示。在 Ballerina 运行时中调用远程方法是异步的(非阻塞,同时不需要显式回调或侦听器)。这些语言内置的 络原语与其他语言特性(如显式错误处理、内置 json/xml 支持和灵活的类型)相结合,可帮助开发人员更快地编写直观且可维护的 络交互,这反过来又使开发人员和组织能够更多地关注创新。
Ballerina 特性一览图
让我们探索一下如何使用 Ballerina 对 REST 和 GraphQL API 的支持来编写直观且有意义的后端 API。请按照入门指南安装和设置 Ballerina。
设置 Ballerina
开发 REST API
让我们看看如何使用 Ballerina 编写 REST API。
说 “Hello World!”
用 Ballerina 编写的 hello world REST API 如下所示:
import ballerina/http; service / on new http:Listener(8080) { resource function get greeting() returns string { return "Hello!"; }}
复制代码
让我们在这里解码语法的每个部分:
要深入了解 Ballerina HTTP 服务语法,尤其是如何使用查询和路径参数、payload 数据绑定等,请参考以下文章:
HTTP Deep-Dive with Ballerina: Services
集成示例?—?货币转换 API
以下是一个稍微复杂一点的 REST API。给定基准货币、目标货币和金额,此 API 将返回转换后的金额。此 API 使用外部服务来获取最新汇率。
import ballerina/log;import ballerina/http; configurable int port = 8080; type ConversionResponse record { boolean success; string base; map<decimal> rates;}; service / on new http:Listener(port) { resource function get convert/[string baseCurrency]/to/[string targetCurrency](decimal amount) returns decimal|error { http:Client exchangeEP = check new ("https://api.exchangerate.host"); ConversionResponse response = check exchangeEP->get(string `/latest?base=${baseCurrency}`); if !response.success { return error("Exchange rates couldn't be obtained"); } decimal? rate = response.rates[targetCurrency]; if rate is () { return error("Couldn't determine exchange rate for target currency", targetCurrency = targetCurrency); } log:printInfo("converting currency", baseCurrency = baseCurrency, targetCurrency = targetCurrency, amount = amount); return rate * amount; }}
复制代码
与 hello world 示例相比,这个示例展示了 Ballerina 一些更有趣的功能。
curl http://localhost:8080/convert/USD/to/GBP?amount=100
复制代码
红利:低代码开发
上述货币转换 API 的低代码视图
尽管我们不会在 Ballerina 的低代码方向做过多探索,但它对于非技术或技术水平较低的人来说,这有助于他们理解和编写代码,所以也试一试吧。
无泄漏?—?任何东西都可以用代码编程,代码中的一切都是可视的。
一个简单的 CRUD 服务
下面是一个用 Ballerina 编写的 CRUD 服务示例,它操作一组保存在内存中的产品。
import ballerina/http;import ballerina/log;import ballerina/uuid; # 表示一种产品public type Product record {| # Product ID string id?; # Name of the product string name; # Product description string description; # Product price Price price;|}; # 表示货币的枚举public enum Currency { USD, LKR, SGD, GBP} # 表示价格public type Price record {| # Currency Currency currency; # Amount decimal amount;|}; # 表示错误public type Error record {| # Error code string code; # Error message string message;|}; # 错误响应public type ErrorResponse record {| # Error Error 'error;|}; # 错误的请求响应public type ValidationError record {| *http:BadRequest; # Error response. ErrorResponse body;|}; # 表示已创建响应的标头public type LocationHeader record {| # Location header. A link to the created product. string location;|}; # 产品创建响应public type ProductCreated record {| *http:Created; # Location header representing a link to the created product. LocationHeader headers;|}; # 产品更新响应public type ProductUpdated record {| *http:Ok;|}; # 产品服务service / on new http:Listener(8080) { private map<Product> products = {}; # 列出所有产品 # + return - List of products resource function get products() returns Product[] { return self.products.toArray(); } # 添加一个新产品 # # + product - Product to be added # + return - product created response or validation error resource function post products(@http:Payload Product product) returns ProductCreated|ValidationError { if product.name.length() == 0 || product.description.length() == 0 { log:printWarn("Product name or description is not present", product = product); return <ValidationError>{ body: { 'error: { code: "INVALID_NAME", message: "Product name and description are required" } } }; } if product.price.amount < 0d { log:printWarn("Product price cannot be negative", product = product); return <ValidationError>{ body: { 'error: { code: "INVALID_PRICE", message: "Product price cannot be negative" } } }; } log:printDebug("Adding new product", product = product); product.id = uuid:createType1AsString(); self.products[<string>product.id] = product; log:printInfo("Added new product", product = product); string productUrl = string `/products/${<string>product.id}`; return <ProductCreated>{ headers: { location: productUrl } }; } # 更新一个产品 # # + product - Updated product # + return - A product updated response or an error if product is invalid resource function put product(@http:Payload Product product) returns ProductUpdated|ValidationError { if product.id is () || !self.products.hasKey(<string>product.id) { log:printWarn("Invalid product provided for update", product = product); return <ValidationError>{ body: { 'error: { code: "INVALID_PRODUCT", message: "Invalid product" } } }; } log:printInfo("Updating product", product = product); self.products[<string>product.id] = product; return <ProductUpdated>{}; } # 删除一个产品 # # + id - Product ID # + return - Deleted product or a validation error resource function delete products/[string id]() returns Product|ValidationError { if !self.products.hasKey(<string>id) { log:printWarn("Invalid product ID to be deleted", id = id); return { body: { 'error: { code: "INVALID_ID", message: "Invalud product id" } } }; } log:printDebug("Deleting product", id = id); Product removed = self.products.remove(id); log:printDebug("Deleted product", product = removed); return removed; }}
复制代码
大部分语法是不言自明的,该服务有 4 种资源方法:
# 错误的请求响应public type ValidationError record {| *http:BadRequest; # Error response. ErrorResponse body;|}; # 产品创建响应public type ProductCreated record {| *http:Created; # Location header representing a link to the created product. LocationHeader headers;|};
复制代码
拥有这样的模式有助于开发人员轻松理解代码。只需查看资源方法定义,开发人员就可以清楚地了解资源方法。什么是资源路径,需要什么查询/路径参数,有效负载是什么,以及可能的返回类型是什么。
resource function post products(@http:Payload Product product) returns ProductCreated|ValidationError { }
复制代码
这是一个 POST 请求,发送到 /products(通过查看资源方法派生),需要 Product 类型的有效负载,并返回验证错误 (400) 或带有位置标头的 HTTP CREATED 响应 (201)。
生成 OpenAPI 规范
一旦我们用 Ballerina 编写了服务,只需指向源文件即可生成 OpenAPI 规范。通过查看源代码,它将输出带有相应状态代码和模式的 OpenAPI 规范。
您可以在 OpenAPI 部分阅读更多内容:
Ballerina OpenAPI 工具
生成完整的 OpenAPI 规范可帮助您生成所需的客户端。在我们的例子中,生成 JavaScript 客户端并将我们的前端与后端轻松集成。
保护服务
您可以通过将 HTTP 侦听器更新为 HTTPS 侦听器来保护您的服务,如下所示。
http:ListenerSecureSocket secureSocket = { key: { certFile: "../resource/path/to/public.crt", keyFile: "../resource/path/to/private.key" }};service /hello on new http:Listener(8080, secureSocket = secureSocket) { resource function get world() returns string { return "Hello World!"; }}
复制代码
您也可以启用双向 SSL 并进行高级配置。更多信息请参阅有关 HTTP 服务安全性的 Ballerina 示例。
验证
Ballerina 内置了对 3 种身份验证机制的支持。
JWT
您可以提供证书文件或授权服务器的 JWK 端点 URL 并启用 JWT 签名验证。例如,如果我们要使用像 Asgardeo 这样的 IDaaS(身份即服务)来保护我们的服务,我们只需在服务中添加以下注解:
@http:ServiceConfig { auth: [ { jwtValidatorConfig: { signatureConfig: { jwksConfig: { url: "https://api.asgardeo.io/t/imeshaorg/oauth2/jwks" } }
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!