news 2026/10/13 6:20:10

Wing 云编程语言实战:深入解析 `cloud.Api` 的 PUT 路由在 tf-aws 目标下生成的 Terraform 配置

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Wing 云编程语言实战:深入解析 `cloud.Api` 的 PUT 路由在 tf-aws 目标下生成的 Terraform 配置
  • 编程语言
  • 编译器
  • 云原生
  • 语言运行时

【免费下载链接】wing

A programming language for the cloud ☁️ A unified programming model, combining infrastructure and runtime code into one language ⚡

项目地址:https://gitcode.com/gh_mirrors/wi/wing
点击查看免费下载

本篇技术指南以 Wing(winglang)官方测试套件中的快照文件 put.test.w_compile_tf-aws.md 为解剖对象,完整解读一个带路径参数的 PUT 路由从 Wing 源码到 AWS 基础设施代码(Terraform JSON)的编译全过程。你将理解cloud.Api的抽象设计、tf-aws平台如何把一次api.put()调用展开为 API Gateway、Lambda、IAM、S3 等十余个底层资源,并能用快照反推 Wing 的部署与权限模型,最终在自己的 Wing 工程里复现并验证同样的编译产物。

快照的来龙去脉:hangar 编译快照与 put.test.w

本次分析的对象是tools/hangar/__snapshots__/test_corpus/sdk_tests/api/put.test.w_compile_tf-aws.md,它由 hangar 测试框架自动生成。从文件名的三段式命名即可读出它的身份:被测源文件为put.test.w,操作阶段为compile(编译),目标平台为tf-aws。

hangar 的生成逻辑位于 generated_test_targets.ts:compileTest函数会对每个.test.w入口文件执行wing compile -t tf-aws,将产物目录target/<name>.tfaws下的main.tf.json与.wing目录中的 preflight/inflight JavaScript 汇总成一个 Markdown 快照,写入__snapshots__目录(快照目录定义见 paths.ts)。test_corpus/sdk_tests/api前缀表明它属于 SDK 测试语料库中的api主题目录(源文件位于 tests/sdk_tests/api)。

值得注意的细节:快照中的<ASSET_KEY>、<ASSET_SOURCE>等占位符并非编译产物本来面目,而是 utils.ts 中sanitize_json_paths的清理结果——它把形如"asset.xxx.zip"的密钥与本地绝对源码路径统一替换为占位符,避免快照因本地路径差异而不稳定;同时该函数会删除terraform字段与 CDKTF 版本号,保证快照跨环境可复现。

被测 Wing 源码:put.test.w 的 PUT 路由定义

快照的主体是编译产物,但理解编译产物必须先回到源头。完整源码见 tests/sdk_tests/api/put.test.w,其核心逻辑如下:

bring cloud; bring http; bring util; let api = new cloud.Api(); let body = Json {"cat": "Tion"}; let user = "guy"; let _id = "12345"; api.put("/path/:id/nn/:user", inflight (req: cloud.ApiRequest): cloud.ApiResponse => { let path = "/path/{_id}/nn/{user}"; assert(req.method == cloud.HttpMethod.PUT); assert(req.vars?.get("id") == _id); assert(req.vars?.get("user") == user); assert(req.path == path); assert(req.body == Json.stringify(body)); assert(req.headers?.get("content-type") == "application/json"); return cloud.ApiResponse { status: 200, headers: {"content-type" => "application/json; charset=utf-8"}, body: req.vars?.get("id") }; }); test "http.put and http.fetch can preform a call to an api" { let url = "{api.url}/path/{_id}/nn/{user}"; let response: http.Response = http.put(url, headers: { "content-type" => "application/json" }, body: Json.stringify(body)); let fetchResponse: http.Response = http.put(url, method: http.HttpMethod.PUT, headers: { "content-type" => "application/json" }, body: Json.stringify(body)); assert(response.headers.get("content-type") == "application/json; charset=utf-8"); assert(response.body == _id); assert(response.status == 200); assert(response.url == url); assert(fetchResponse.headers.get("content-type") == "application/json; charset=utf-8"); assert(fetchResponse.body == _id); assert(fetchResponse.status == 200); assert(fetchResponse.url == url); }

这段测试代码揭示了cloud.Api路由 API 的几个关键设计:

  • 路径参数语法:Wing 使用:id、:user声明路径参数。在云抽象基类 api.ts 中,renderOpenApiPath会把/:name形式的路径转换为 OpenAPI 规范的/{name}形式(快照中的 OpenAPI body 正是/{id}、/{user}形态)。路径合法性由_validatePath校验(cloud/api.ts),参数名只允许字母、数字、下划线与连字符。
  • inflight 处理器:api.put()接收一个inflight闭包,签名是(req: cloud.ApiRequest): cloud.ApiResponse。ApiRequest包含method、path、query、vars(路径参数)、body、headers等字段,ApiResponse支持status、body、headers(定义见 cloud/api.ts)。
  • 测试即飞行前验证:Wing 的test块在模拟器与真实云上均可执行,通过http.put和http.fetch两种途径发起真实 HTTP 请求,断言状态码、响应体、响应头与 URL,从而验证 PUT 路由的端到端行为。

编译产物全景:main.tf.json 完整快照

对put.test.w执行wing compile -t tf-aws后,hangar 将main.tf.json原样收录进快照。下面完整呈现这份产物(即关联文档的主体内容):

{ "//": { "metadata": { "backend": "local", "stackName": "root" }, "outputs": { "root": { "Default": { "Default": { "Api": { "Endpoint": { "Url": "Api_Endpoint_Url_473FEE9F" } } } } } } }, "data": { "aws_caller_identity": { "account": { "//": { "metadata": { "path": "root/Default/account", "uniqueId": "account" } } } }, "aws_region": { "Region": { "//": { "metadata": { "path": "root/Default/Region", "uniqueId": "Region" } } } } }, "output": { "Api_Endpoint_Url_473FEE9F": { "value": "https://${aws_api_gateway_rest_api.Api_api_91C07D84.id}.execute-api.${data.aws_region.Region.name}.amazonaws.com/${aws_api_gateway_stage.Api_api_stage_E0FA39D6.stage_name}" } }, "provider": { "aws": [ {} ] }, "resource": { "aws_api_gateway_deployment": { "Api_api_deployment_7FB64CC4": { "//": { "metadata": { "path": "root/Default/Default/Api/api/deployment", "uniqueId": "Api_api_deployment_7FB64CC4" } }, "lifecycle": { "create_before_destroy": true }, "rest_api_id": "${aws_api_gateway_rest_api.Api_api_91C07D84.id}", "triggers": { "redeployment": "${sha256(aws_api_gateway_rest_api.Api_api_91C07D84.body)}" } } }, "aws_api_gateway_rest_api": { "Api_api_91C07D84": { "//": { "metadata": { "path": "root/Default/Default/Api/api/api", "uniqueId": "Api_api_91C07D84" } }, "body": "{\"paths\":{\"/path/{id}/nn/{user}\":{\"put\":{\"operationId\":\"put-path/:id/nn/:user\",\"responses\":{\"200\":{\"description\":\"200 response\",\"content\":{}}},\"parameters\":[{\"name\":\"id\",\"in\":\"path\",\"required\":true,\"schema\":{\"type\":\"string\"}},{\"name\":\"user\",\"in\":\"path\",\"required\":true,\"schema\":{\"type\":\"string\"}}],\"x-amazon-apigateway-integration\":{\"uri\":\"arn:aws:apigateway:${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/arn:aws:lambda:${data.aws_region.Region.name}:${data.aws_caller_identity.account.account_id}:function:put_path_-id_nn_-user0-c8982eb3/invocations\",\"type\":\"aws_proxy\",\"httpMethod\":\"POST\",\"responses\":{\"default\":{\"statusCode\":\"200\"}},\"passthroughBehavior\":\"when_no_match\",\"contentHandling\":\"CONVERT_TO_TEXT\"}}},\"/{proxy+}\":{\"x-amazon-apigateway-any-method\":{\"produces\":[\"application/json\"],\"x-amazon-apigateway-integration\":{\"type\":\"mock\",\"requestTemplates\":{\"application/json\":\"\\n {\\\"statusCode\\\": 404}\\n \"},\"passthroughBehavior\":\"never\",\"responses\":{\"404\":{\"statusCode\":\"404\",\"responseParameters\":{\"method.response.header.Content-Type\":\"'application/json'\"},\"responseTemplates\":{\"application/json\":\"{\\\"statusCode\\\": 404, \\\"message\\\": \\\"Error: Resource not found\\\"}\"}},\"default\":{\"statusCode\":\"404\",\"responseParameters\":{\"method.response.header.Content-Type\":\"'application/json'\"},\"responseTemplates\":{\"application/json\":\"{\\\"statusCode\\\": 404, \\\"message\\\": \\\"Error: Resource not found\\\"}\"}}}},\"responses\":{\"404\":{\"description\":\"404 response\",\"headers\":{\"Content-Type\":{\"type\":\"string\"}}}}}}},\"openapi\":\"3.0.3\"}", "lifecycle": { "create_before_destroy": true }, "name": "api-c8f613f0" } }, "aws_api_gateway_stage": { "Api_api_stage_E0FA39D6": { "//": { "metadata": { "path": "root/Default/Default/Api/api/stage", "uniqueId": "Api_api_stage_E0FA39D6" } }, "deployment_id": "${aws_api_gateway_deployment.Api_api_deployment_7FB64CC4.id}", "rest_api_id": "${aws_api_gateway_rest_api.Api_api_91C07D84.id}", "stage_name": "prod" } }, "aws_cloudwatch_log_group": { "Api_put_path_id_nn_user0_CloudwatchLogGroup_6FDE8C46": { "//": { "metadata": { "path": "root/Default/Default/Api/put_path_:id_nn_:user0/CloudwatchLogGroup", "uniqueId": "Api_put_path_id_nn_user0_CloudwatchLogGroup_6FDE8C46" } }, "name": "/aws/lambda/put_path_-id_nn_-user0-c8982eb3", "retention_in_days": 30 } }, "aws_iam_role": { "Api_put_path_id_nn_user0_IamRole_1C9DE02F": { "//": { "metadata": { "path": "root/Default/Default/Api/put_path_:id_nn_:user0/IamRole", "uniqueId": "Api_put_path_id_nn_user0_IamRole_1C9DE02F" } }, "assume_role_policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":\"sts:AssumeRole\",\"Principal\":{\"Service\":\"lambda.amazonaws.com\"},\"Effect\":\"Allow\"}]}" } }, "aws_iam_role_policy": { "Api_put_path_id_nn_user0_IamRolePolicy_0EB967A3": { "//": { "metadata": { "path": "root/Default/Default/Api/put_path_:id_nn_:user0/IamRolePolicy", "uniqueId": "Api_put_path_id_nn_user0_IamRolePolicy_0EB967A3" } }, "policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":\"none:null\",\"Resource\":\"*\"}]}", "role": "${aws_iam_role.Api_put_path_id_nn_user0_IamRole_1C9DE02F.name}" } }, "aws_iam_role_policy_attachment": { "Api_put_path_id_nn_user0_IamRolePolicyAttachment_045FFFB7": { "//": { "metadata": { "path": "root/Default/Default/Api/put_path_:id_nn_:user0/IamRolePolicyAttachment", "uniqueId": "Api_put_path_id_nn_user0_IamRolePolicyAttachment_045FFFB7" } }, "policy_arn": "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", "role": "${aws_iam_role.Api_put_path_id_nn_user0_IamRole_1C9DE02F.name}" } }, "aws_lambda_function": { "Api_put_path_id_nn_user0_A9826CF0": { "//": { "metadata": { "path": "root/Default/Default/Api/put_path_:id_nn_:user0/Default", "uniqueId": "Api_put_path_id_nn_user0_A9826CF0" } }, "architectures": [ "arm64" ], "environment": { "variables": { "NODE_OPTIONS": "--enable-source-maps", "WING_FUNCTION_NAME": "put_path_-id_nn_-user0-c8982eb3", "WING_TARGET": "tf-aws" } }, "function_name": "put_path_-id_nn_-user0-c8982eb3", "handler": "index.handler", "logging_config": { "log_format": "JSON" }, "memory_size": 1024, "publish": true, "role": "${aws_iam_role.Api_put_path_id_nn_user0_IamRole_1C9DE02F.arn}", "runtime": "nodejs20.x", "s3_bucket": "${aws_s3_bucket.Code.bucket}", "s3_key": "${aws_s3_object.Api_put_path_id_nn_user0_S3Object_5E224671.key}", "timeout": 60, "vpc_config": { "security_group_ids": [], "subnet_ids": [] } } }, "aws_lambda_permission": { "Api_api_permission-PUT-526849ff_9087005A": { "//": { "metadata": { "path": "root/Default/Default/Api/api/permission-PUT-526849ff", "uniqueId": "Api_api_permission-PUT-526849ff_9087005A" } }, "action": "lambda:InvokeFunction", "function_name": "${aws_lambda_function.Api_put_path_id_nn_user0_A9826CF0.function_name}", "principal": "apigateway.amazonaws.com", "source_arn": "${aws_api_gateway_rest_api.Api_api_91C07D84.execution_arn}/*/PUT/path/{id}/nn/{user}", "statement_id": "AllowExecutionFromAPIGateway-PUT-526849ff" } }, "aws_s3_bucket": { "Code": { "//": { "metadata": { "path": "root/Default/Code", "uniqueId": "Code" } }, "bucket_prefix": "code-c84a50b1-" } }, "aws_s3_object": { "Api_put_path_id_nn_user0_S3Object_5E224671": { "//": { "metadata": { "path": "root/Default/Default/Api/put_path_:id_nn_:user0/S3Object", "uniqueId": "Api_put_path_id_nn_user0_S3Object_5E224671" } }, "bucket": "${aws_s3_bucket.Code.bucket}", "key": "<ASSET_KEY>", "source": "<ASSET_SOURCE>" } } } }

这份快照包含 12 类资源,覆盖了一个 HTTP 端点从入口到后端的完整链路。下面逐资源结合 SDK 源码深度解析。

逐资源深度解析

元数据与数据源:确定性命名与区域探测

快照最上层的//.metadata记录了backend: "local"(本地 Terraform 状态后端)与stackName: "root",这是 CDKTF 合成 Terraform 配置时的标准头。outputs块则声明了本 stack 对外暴露的输出——Api/Endpoint/Url,其最终值指向 output 区块中的Api_Endpoint_Url_473FEE9F。

data区块中的aws_caller_identity(路径root/Default/account)与aws_region(路径root/Default/Region)是 AWS provider 的数据源:前者在部署期解析当前账号 ID,后者解析部署区域。它们被大量引用——Lambda 函数 ARN、API Gateway 集成 URI、Endpoint URL 都依赖这两个值。这意味着编译产物对账号/区域完全无感,同一份main.tf.json可在任意 AWS 账号与区域直接 apply。

API Gateway 三件套:RestApi、Deployment 与 Stage

tf-aws平台把cloud.Api实现为 API Gateway REST API(aws_api_gateway_rest_api)、部署(aws_api_gateway_deployment)与阶段(aws_api_gateway_stage)三件套,对应 target-tf-aws/api.ts 中WingRestApi构造器依次调用_initApiGatewayRestApi、_initApiGatewayDeployment、_initApiGatewayStage的逻辑。

RestApi 的 OpenAPI body 是最值得研读的部分。快照中body字段是一段完整的 OpenAPI 3.0.3 规范("openapi":"3.0.3"),由两条路径组成:

  1. 业务路径/path/{id}/nn/{user}:由api.put("/path/:id/nn/:user", ...)生成。operationId为put-path/:id/nn/:user,两个路径参数id、user均被声明为required: true的 string 类型——这正是 cloud/api.ts 中_addToSpec解析:param语法并生成parameters数组的结果。每个方法(GET/POST/PUT 等)都会自动获得一个200响应定义。

  2. 集成扩展x-amazon-apigateway-integration:这是 AWS API Gateway 的 OpenAPI 扩展,将请求代理到 Lambda。其uri指向put_path_-id_nn_-user0-c8982eb3这个 Lambda 函数,type: "aws_proxy"表示全代理模式(请求原样转发、响应原样返回),httpMethod: "POST"是 Lambda 集成的内部调用方式,passthroughBehavior: "when_no_match"与contentHandling: "CONVERT_TO_TEXT"定义了映射行为。该扩展的生成逻辑在 target-tf-aws/api.ts 的_createApiSpecExtension中。

  3. 兜底路径/{proxy+}:对未匹配任何已定义路由的请求,Wing 注入了一个 mock 集成,直接返回404与{"statusCode": 404, "message": "Error: Resource not found"}。该逻辑由 shared-aws/api.default.ts 的createApiDefaultResponse生成,defaultKey会根据已有路径的最长参数前缀构造(本快照中无根级参数路径,故直接为/{proxy+})。若开启了 CORS,此兜底路径还会额外处理 OPTIONS 预检请求(返回 204 并注入 CORS 响应头)。

Deployment 与 Stage:aws_api_gateway_deployment通过triggers.redeployment = sha256(rest_api.body)实现"API 定义变化即自动重新部署",源码对应 target-tf-aws/api.ts;aws_api_gateway_stage的stage_name固定为prod,该常量定义于 shared-aws/api.ts。两个资源均使用lifecycle.create_before_destroy,避免更新时先删后建造成短暂停机。

后端 Lambda 函数:运行时、资源与环境变量

aws_lambda_function是本次编译为 PUT 路由生成的处理函数(Api_put_path_id_nn_user0_A9826CF0),其全部属性都可在 target-tf-aws/function.ts 中找到源码对应:

快照属性值源码说明
runtimenodejs20.xWing inflight 代码编译为 JS 后在 Node.js 20 运行时执行
architectures["arm64"]默认使用 ARM64 架构,成本更低
memory_size1024默认内存取自DEFAULT_MEMORY_SIZE(1024 MB)
timeout60默认超时 1 分钟(Duration.fromMinutes(1).seconds)
handlerindex.handlerSDK 生成的 Lambda 入口
publishtrue发布版本,配合 Lambda 版本管理
logging_config.log_formatJSONCloudWatch 日志采用 JSON 格式

环境变量揭示了三件事:WING_FUNCTION_NAME让运行时知道自己是哪个函数(function.ts 中注入);WING_TARGET: "tf-aws"标记部署目标,便于运行时分发;NODE_OPTIONS: "--enable-source-maps"让堆栈信息可映射回 Wing 源码行号,方便排查(function.ts)。

函数名put_path_-id_nn_-user0-c8982eb3也大有来头:前缀put_path_-id_nn_-user0来自 target-tf-aws/api.ts 中prefix = method.toLowerCase() + path.replace(/\//g, "_")的命名规则,尾缀是资源名哈希;长度上限 64 字符、只允许字母数字与-/_的约束定义在 function.ts。

IAM 三角:角色、内联策略与托管策略

Lambda 需要身份才能运行,快照中对应三个 IAM 资源:

  • aws_iam_role:信任策略只允许lambda.amazonaws.com服务代入(sts:AssumeRole),这是 Lambda 执行角色的标准形态(function.ts)。
  • aws_iam_role_policy:内联策略中有一条Action: "none:null"的占位语句。这是 Wing 的刻意设计——当函数尚未绑定任何资源权限时,Terraform 不允许空策略,SDK 便注入这条无操作语句占位(function.ts)。一旦通过addPolicyStatements绑定了真实权限(例如访问 Bucket、Queue),这些语句会被合并进同一策略,none:null占位即被替换。
  • aws_iam_role_policy_attachment:附加 AWS 托管策略AWSLambdaBasicExecutionRole,授予向 CloudWatch Logs 写日志的基础权限(function.ts)。

CloudWatch 日志组:默认 30 天保留

aws_cloudwatch_log_group的名称与 Lambda 函数名一致(/aws/lambda/put_path_-id_nn_-user0-c8982eb3),retention_in_days: 30是默认保留期。源码位于 function.ts:默认 30 天,可传logRetentionDays覆盖,传负值则改为无限期保留。

路由授权:aws_lambda_permission 的精确来源限制

aws_lambda_permission是 API Gateway 调用 Lambda 的"通行证",其四个关键字段展示了 Wing 的最小权限原则:

  • principal: "apigateway.amazonaws.com":只允许 API Gateway 服务调用。
  • source_arn: "…execution_arn/*/PUT/path/{id}/nn/{user}":来源被精确限定到方法级路径——只有对/path/{id}/nn/{user}的 PUT 请求才能触发该函数,同一 API 下的其他路由无法调用。
  • statement_id: "AllowExecutionFromAPIGateway-PUT-526849ff":由PUT方法与路径的 SHA-1 哈希后 8 位拼接而成(target-tf-aws/api.ts)。

这份资源与快照中//.metadata的路径root/Default/Default/Api/api/permission-PUT-526849ff相互印证:同一个哈希526849ff同时出现在资源名与 statement_id 中。

代码分发:S3 桶与对象

aws_s3_bucket(root/Default/Code)是承载所有 Lambda 代码包的共享桶(bucket_prefix: "code-c84a50b1-"),由 App 级别的codeBucket创建;aws_s3_object则把本次函数的 zip 包上传到该桶(function.ts)。对象key形如asset.<addr>.<bundleHash>.zip——只要代码变化,bundle 哈希即变化,对象键随之变化,从而驱动 Lambda 更新。

快照中的<ASSET_KEY>、<ASSET_SOURCE>是前文提到的sanitize_json_paths替换产物:真实产物里key是一个asset.xxx.zip字符串,source是本机打包目录的绝对路径。aws_lambda_function通过s3_bucket/s3_key引用该对象,形成了「S3 代码包 → Lambda」的部署链路。

Endpoint URL 输出

最后看output.Api_Endpoint_Url_473FEE9F,它拼接为:

https://<rest_api_id>.execute-api.<region>.amazonaws.com/prod

注意该 URL 是手工构造的,而非直接引用invokeUrl属性。源码注释解释了原因(target-tf-aws/api.ts):invokeUrl与 deployment 共享依赖,若在栈中其他位置引用会引发 Terraform 循环依赖,故 SDK 自行拼装 URL 来打破依赖环。

从快照反推的一次 PUT 请求调用链

综合上述资源,可以完整还原一次 HTTP PUT 请求的生命周期:

  1. 客户端请求https://<api-id>.execute-api.<region>.amazonaws.com/prod/path/12345/nn/guy(对应test块中http.put发起的请求);
  2. API Gateway 依据 OpenAPI body 匹配/path/{id}/nn/{user}的 PUT 方法,将12345与guy解析为路径参数id、user;
  3. x-amazon-apigateway-integration通过aws_proxy把请求连同参数转发给 Lambdaput_path_-id_nn_-user0-c8982eb3(授权由aws_lambda_permission保障);
  4. Lambda 中的 inflight 处理器执行断言并返回ApiResponse(status 200、content-type: application/json; charset=utf-8、body 为id参数值);
  5. API Gateway 将响应原样回传,http.put的调用方收到 200 与响应体12345,最终通过快照同源的测试断言。

若请求打到未定义路径,则落入/{proxy+}兜底 mock,直接返回 404{"statusCode": 404, "message": "Error: Resource not found"}。

在本地复现这份快照

你可以不依赖 hangar,直接在仓库中手动复现编译产物:

cd tests/sdk_tests/api wing compile -t tf-aws put.test.w

执行后会在put.test.w同目录下生成target/put.test.tfaws/main.tf.json,其内容与快照一致(除被sanitize_json_paths替换的占位符外)。也可以运行 hangar 的测试套件来校验快照是否仍然匹配:

cd tools/hangar pnpm test -- sdk_tests/api

如果 SDK 行为发生变化导致编译产物偏离快照,测试即失败,这正是快照测试的价值所在——它把「不可见的编译行为」固化为「可 diff 的文本资产」。相关实现参考 generated_test_targets.ts、utils.ts 与 generate_tests.ts。

小结

通过put.test.w这份 tf-aws 编译快照,我们完整看到了 Wing「一份语言、多种云」抽象的具体落地:一行api.put("/path/:id/nn/:user", inflight ...)在编译期被系统性地展开为 OpenAPI 规范、API Gateway 三件套、Lambda 函数、IAM 权限三角、CloudWatch 日志、S3 代码分发与最小权限授权共十余个 Terraform 资源。快照既是 SDK 行为的回归测试锚点,也是理解 Wing 资源命名规则(put_path_-id_nn_-user0)、默认值(1024 MB 内存、60 秒超时、30 天日志保留、prod阶段)与依赖编排思路(手工构造 Endpoint URL 规避循环依赖)的最佳教材。若想继续深入,可对照阅读 tests/sdk_tests/api 下其他路由方法(get/post/delete 等)的快照,以及 packages/@winglang/sdk/src/target-tf-aws/api.ts 中私有 API Gateway 的 VPC 配置分支,观察同一抽象在不同平台与配置下的形态差异。

  • 编程语言
  • 编译器
  • 云原生
  • 语言运行时

【免费下载链接】wing

A programming language for the cloud ☁️ A unified programming model, combining infrastructure and runtime code into one language ⚡

项目地址:https://gitcode.com/gh_mirrors/wi/wing
点击查看免费下载

相关推荐

上一篇:群晖硬盘兼容性警告清不掉?3步跑通Synology_HDD_db,让任意硬盘"上户口"
下一篇:Cyclone与数据库集成:高效处理异步数据操作的完整指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 6:17:52

Vue3路由动画实践:从transition到动态方向,解决页面瞬切体验

做移动端H5商城的时候&#xff0c;用户反馈最多的一句是&#xff1a;"切换页面感觉像闪过去的&#xff0c;没什么过渡&#xff0c;体验太硬了。"那时候我路由动画的代码还是直接在<transition>外面包一层<router-view>&#xff0c;结果动画完全不生效&am…

作者头像 李华
网站建设 2026/10/13 6:17:42

PyQt5图片怎么显示?选QGraphicsView还是QLabel?一文讲透

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 6:15:08

AnyPS5外设适配实战:从USB HID协议到PS5原生控制器转换

先说结论&#xff1a;AnyPS5 并不是某个官方型号&#xff0c;而是我最近折腾的一个外设适配项目&#xff0c;目标很简单——让几乎任何输入设备都能在 PS5 上正常用。说白了&#xff0c;就是把各种手柄、摇杆、方向盘甚至键盘鼠标&#xff0c;统一转换成 PS5 能认的原生协议&am…

作者头像 李华