【AICon】探索RAG 技术在实际应用中遇到的挑战及应对策略!AICon精华内容已上线73%>>> 了解详情
写点什么

Ballerina Swan Lake:10 个令人瞩目的云原生编程语言特性

  • 2021-10-08
  • 本文字数:9376 字

    阅读完需:约 31 分钟

Ballerina Swan Lake:10个令人瞩目的云原生编程语言特性

Ballerina 诞生的背景

集成可以看作是一种编程类型,而且为了简化和抽离集成的复杂性,人们借助不同的技术实现了集成的可视化表示。DSL 已经变得非常流行,因为它们提供了恰当的编程抽象,但也有一些局限——很多时候,集成开发人员都不得不使用常规代码来解决一部分问题。而且,集成编程实践已经变成了孤岛,开发人员要选择一种集成工具进行集成编程,还必须使用另外一种工具或编程语言开发应用程序的其他部分。可视化表示还是很重要,我们可以借此观察端点之间的数据流和交互。此外,对于云原生工程,集成系统现在运行在容器中,应用程序使用分布在许多节点上的微服务来实现。


如果有一种语言既能提供代码集成能力,又能提供可视化工具,那岂不是非常有用?那样,我们岂不是既可以克服 DSL 的局限,又能够保留软件工程的最佳实践?Ballerina 就是在这种想法下诞生的。其目标是创建一种现代化的编程语言,一种集编程语言、集成技术和云原生计算优点于一体的、集成优化的、有潜力成为主流的文本和图形语言。


下面让我们逐条看下 Ballerina 语言的关键特性。有些之前已经介绍过,有些在Swan Lake Beta版本中做了增强。从中我们可以理解,为什么 Ballerina 很适合创建网络服务及实现分布式系统集成。

Ballerina 的关键特性

1. Ballerina 以应用程序语言的健壮性和可扩展性解决了脚本语言的大多数用例需求

你可以想象有这样一个语言图谱,从像 Perl、Awk 这样的脚本语言到像 Rust、C 这样的系统语言,中间是像 Go、Java 这样的应用程序语言。程序可以是单行脚本,也可以是数百万行代码。在这个图谱上,Ballerina 介于脚本语言和应用程序语言之间。


Ballerina 独有的特性使其非常适合小型程序。其他大多数为小型程序设计的脚本语言与 Ballerina 有很大的不同,它们是动态类型的,而且无法提供 Ballerina 独有的可扩展性和健壮性。在前云时代,你用其他脚本语言解决的问题仍然是很重要的问题。只是现在会涉及到网络服务;健壮性也比以往任何时候更重要。使用标准的脚本语言,一个 50 行的程序几年后往往就会变成一个上千行的、难以维护的程序,而这还不是终点。Ballerina 可以用于解决脚本程序的问题,而且更具扩展性,更健壮,也更适合云。此外,脚本语言通常也不提供任何可视化组件,但 Ballerina 提供。

2. Ballerina 是面向数据的,而不是面向对象的

在网络交互中,面向对象的方法将数据和代码绑在一起,在分布广泛的微服务和 API 网络中,这并不是最佳的数据发送方式。


在前云时代,API 是对路径中库函数的调用,你可以在调用中传递对象。但当 API 在云上时,就没法这样做了。你会希望通过网络发送的数据独立于代码,因为你不想暴露代码。虽然 Java RMI 和 CORBA 都曾设法支持分布式对象,但前提是紧耦合,而且两端同属一个组织。在松耦合的云上,分布式对象就无法使用了。Ballerina 突出的是纯数据,独立于任何处理数据的代码。虽然 Ballerina 为内部接口提供了对象,但它不是一种面向对象的语言。


随着云上的服务越来越多,开发人员又多了在代码中处理网络资源的职责。编程语言本身必须提供相应的帮助。这就是为什么 Ballerina 带来了一个网络友好的类型系统,提供了强大的在线数据处理功能。


JSON在Ballerina中是一种通用语言。Ballerina 中的数据类型非常接近 JSON,数值、字符串、Map 数组等基础数据类型可以一一映射到 JSON。Ballerina 的普通内存数据值几乎就是内存中的 JSON。这样,通过网络传输过来的 JSON 负载可以立即由 Ballerina 处理,不需要转换或序列化。


import ballerina/io;import ballerina/lang.value;
json j = { "x": 1, "y": 2 };
// 以JSON格式返回表示j的字符串。string s = j.toJsonString();
// 解析JSON格式的字符串,返回它代表的值。json j2 = check value:fromJsonString(s);
// 为了兼容JSON,允许null值。json j3 = null;
public function main() { io:println(s); io:println(j2);}
复制代码

3. Ballerina 有一个灵活的类型系统

编程语言的类型系统是为了让你可以描述各部分是如何组合在一起的,而不仅仅是捕获一类错误——这只是类型系统为你做的一小部分工作。此外,它还有很大一部分工作是提供良好的 IDE 体验。


脚本语言是动态类型的,而应用程序语言则是传统的静态类型,像 C++或 Java。前文已经介绍过,Ballerina 是一种脚本语言,但它提供了一些应用程序语言的特性,其中就包括静态类型系统。在静态类型语言中,类型兼容性是在编译时检查的。通常,静态类型语言更便于重构、更容易调试,也有助于创建更好的语言工具。


虽然 Ballerina 的类型系统是静态的,但其类型比应用程序语言中的类型要灵活得多。Ballerina 语言的类型系统是结构性的,并且增加了对标明类型(nominal typing)的支持。也就是说,类型兼容性的认定考虑了值的结构,而不仅仅是依赖类型名称。这不同于 Java、C++、C#等拥有标明类型系统的语言,它们的类型兼容性受实际的类型名称约束。这样一来,我们付出的代价是有些东西可能在编译时无法捕获,但收获了简洁性和灵活性。


具体来说,你可以说它类似于 XML 模式中的结构定义方式。如果程序收到的 XML 负载有变化或偏差,它仍然可以处理能够识别的内容。它不是那么严格,负载中有无法识别的变化也不一定会失败。Ballerina 的类型系统既可以作为描述网络数据的模式语言,也可以作为操作内存值的程序的类型系统。


要了解关于 Ballerina 类型系统的更多信息,请移步这里

4. Ballerina 提供了强大的网络数据处理功能

Ballerina 还提供了一套用于数据处理的语言特性,其中最突出的就是集成查询。该特性让你可以像下面这样使用类似 SQL 的语法查询数据。查询表达式通过一组类似 SQL 的子句来处理数据。它们必须以 from 子句开头,可以执行过滤、连接、排序、范围、投影等操作。


import ballerina/io; type Employee record {    string firstName;    string lastName;    decimal salary;}; public function main() {    Employee[] employees = [        {firstName: "Rachel", lastName: "Green", salary: 3000.00},        {firstName: "Monica", lastName: "Geller", salary: 4000.00},        {firstName: "Phoebe", lastName: "Buffay", salary: 2000.00},        {firstName: "Ross", lastName: "Geller", salary: 6000.00},        {firstName: "Chandler", lastName: "Bing", salary: 8000.00},        {firstName: "Joey", lastName: "Tribbiani", salary: 10000.00}    ];      // 用于list解析的类SQL查询,以from开始,以select结束。  // order by子句根据姓氏对employees中的成员进行排序。    Employee[] sorted = from var e in employees                         order by e.lastName ascending                          select e;    io:println(sorted);}
复制代码


Ballerina 还提供了一个 Table 数据类型,简化关系型表格数据的处理。下面的代码创建了一个表,其中是 Employee 类型的成员,每位成员使用 name 字段唯一标识。main 函数通过键值 John 检索 Employee,并增加每名员工的工资。


import ballerina/io;
type Employee record { readonly string name; int salary;};
// 创建一个表,其成员类型为Employee,其中每位成员使用name字段唯一标识。table<Employee> key(name) t = table [ { name: "John", salary: 100 }, { name: "Jane", salary: 200 }];
function increaseSalary(int n) { // 按指定的顺序遍历t中的行。 foreach Employee e in t { e.salary += n; }}
public function main() { // 使用键值`John`检索Employee。 Employee? e = t["John"]; io:println(e);
increaseSalary(100); io:println(t);}
复制代码


要想了解更多关于 Ballerina 集成查询的信息,请移步这里


此外,Ballerina 内置了 XML 支持,其功能类似于 XQuery,具有类似 XPath 的 XML 导航机制。这特别适合那些大量使用 XML 但又不想使用 XML 专用语言的人,因为他们现如今要处理各种数据格式。


import ballerina/io;
public function main() returns error? { xml x1 = xml `<name>Sherlock Holmes</name>`; xml:Element x2 = xml `<details> <author>Sir Arthur Conan Doyle</author> <language>English</language> </details>`;
// `+`做串联。 xml x3 = x1 + x2;
io:println(x3);
xml x4 = xml `<name>Sherlock Holmes</name><details> <author>Sir Arthur Conan Doyle</author> <language>English</language> </details>`; // `==`做深层相等比较。 boolean eq = x3 == x4;
io:println(eq);
// `foreach`迭代每个数据项。 foreach var item in x4 { io:println(item); }
// `x[i]` 获取第i项数据(如果没有,则为空序列)。 io:println(x3[0]);
// `x.id`访问名为`id`的属性,如果属性不存在,或者不是一个单元素项,则返回错误。 xml x5 = xml `<para id="greeting">Hello</para>`; string id = check x5.id;
io:println(id);
// `x?.id`访问名为`id`的可选属性,如果属性不存在,则结果为`()`。 string? name = check x5?.name;
io:println(name is ());
// 使用`e.setChildren(x)`转换元素。 x2.setChildren(xml `<language>French</language>`);
io:println(x2); io:println(x3);}
复制代码


在无缝处理不同数据类型的能力中,Ballerina 还提供了一个 decimal 数据类型。这是为满足商业需求而设计的浮点数,如标注价格。由于在一般的语言中,值都是用二进制表示的,所以并不能准确地表示所有实数。当位数超出了格式限制时,剩余部分会被忽略——数值成了近似值,这会导致精度错误。真实世界是运转在十进制数上的,这也是为什么我们会认为这是 Ballerina 的一个强大能力。


import ballerina/io;
// `decimal`类型表示128位IEEE 754R十进制浮点数集合。 decimal nanos = 1d/1000000000d;
function floatSurprise() { float f = 100.10 - 0.01; io:println(f);}
public function main() { floatSurprise(); io:println(nanos);}
复制代码

5. Ballerina 本来就是并发的,而且内置了并发安全性

在云上,并发是一个基本需求,因为网络操作有高延迟。脚本语言对于并发的处理通常也不是很好。典型地,像 JavaScript 这样的脚本语言使用异步函数,这比会回调稍微好点,但也好不了多少。Ballerina 提供了一个更简单的编程模型,它提供的并发方法有异步函数的优点,但比异步函数更简单、直观。


在 Ballerina 中,主要的并发概念是 strand,类似于 Go 语言中的goroutine。Ballerina 程序运行在一个或多个线程上。线程可以与其他线程一起同时运行在一颗内核上,也可以与其他线程一起在一颗内核上执行抢占式多任务。每个线程都可以分成一个或多个 strand,这是由语言托管的逻辑控制线程。从程序员的角度来看,strand 像一个 OS 线程,但它不是——它更轻量级,开销更小。Strand 可以调度到单独的 OS 线程上。Go 采用了类似的方法,但这样做的编程语言并不多。事实上,大部分动态脚本语言都不支持并发。例如,Python 有一个全局锁,所以它并不是真正支持并发执行。


Ballerina 中的函数可以拥有命名“worker”,每个 worker 并发运行在一个新的 strand 上,函数的默认 worker 和其他命名 worker 如下所示:


import ballerina/io;
public function main() { // 任何命名worker之前的代码都会在worker启动之前执行。 io:println("Initializing"); final string greeting = "Hello";
// 命名worker和函数的默认worker以及其他命名worker并行运行。 worker A { // 在所有命名worker和函数参数之前声明的变量都可以在命名worker中访问。 io:println(greeting + " from worker A"); }
worker B { io:println(greeting + " from worker B"); }
io:println(greeting + " from function worker");}
复制代码


Ballerina 还允许 strand 之间共享不可变状态。通常,将并发和不可变状态共享相结合会导致数据竞争,产生错误结果。这是动态语言通常不暴露线程的其中一个原因。不过,对于这个问题,Ballerina 提供了一个很好的解决方案,通过协作式多任务保证并发安全性。在 Ballerina 中,同一线程上的所有 strand 都是以协作式多任务(而非抢占式)方式执行,从而避免了锁问题。这类似于异步函数,所有东西在一个线程上运行,但没有复杂的编程模型。Strand 通过暂停来实现协作式多任务。当一个 strand 在特定的”暂停点“暂停,运行时调度器就会挂起该 strand 的执行,将其线程切换为运行另一个 strand。我们还可以决定什么时候并行运行 strand——可以使用注解让一个 strand 在一个单独的线程上运行。这是因为 Ballerina 独特的类型系统使它能够确定服务何时已经锁定,从而可以安全地使用多线程并行处理传入的请求。虽然这看起来可能无法提供大量的并行执行,但也足以有效利用常见的云实例类型了。


 import ballerina/io;
public function main() {// 每个命名worker都有一个“strand”(逻辑线程控制),strand只在特定的“暂停点”切换执行。 worker A { io:println("In worker A"); }
// 注解可以用于使一个strand在单独的线程上运行。 @strand { thread: "any" }
worker B { io:println("In worker B"); }
io:println("In function worker");}
复制代码

6. Ballerina 内置了图形化视图

处理并发和网络交互是编写云程序固有的一部分工作。在 Ballerina 中,每个程序都可以自动生成一个序列图,通过图形说明分布式并发交互。Ballerina 程序中有等价的文本语法和序列图表示。你可以在这两种视图之间无缝切换。Ballerina 独有的图形化视图不是事后才有的想法。事实上,Ballerina 在设计时就做了深入的考虑,为的是在函数的网络交互和并发使用方面提供真正的洞察力。序列图是最适合这种情形的一种图形。


说明一下,Ballerina 的命名 worker(第 5 点讨论过)以及其他函数级并发特性描述了并发性,而客户端和服务的语言抽象则描述了网络交互。竖线,也称为生命线,表示 worker 和远程端点。一个远程端点是一个客户端对象,它包含远程方法,表示和远程系统的出站交互。大部分语言都不区分远程调用和常规方法调用,但 Ballerina 提供了截然不同的远程方法调用。横线表示从一个函数的 worker 发送给另一个 worker 或是远程端点的消息。这些方面在 Ballerina 中很容易区分,它提供了高级视图,用户什么都不需要做。Ballerina 之所以能做到这种程度,是因为它从设计之初就加入了图形化元素。这是其他任何语言都没有的特性。



Ballerina VSCode 插件可以从源代码动态生成序列图。要生成上述 Ballerina 代码的序列图,请下载VSCode插件并启动图形查看器

7. Ballerina 是云原生的,它提供了一个简单的模型,用于生成和消费服务以及把代码部署到云上

除了具有网络感知特点的类型系统外,Ballerina 还提供了用于处理网络服务的基本语法抽象。该语言还内置支持使用 Docker 和 Kubernetes 在云上部署 Ballerina 应用程序。


生成服务的服务对象


Ballerina 迎合了服务的概念,使用 Ballerina 只需 3、4 行代码就可以写出一个服务。在 Ballerina 中,服务基于 3 个概念:应用程序、监听器和库。应用程序定义服务对象,并将它们连接到监听器。监听器由库提供。举例来说,每种协议(HTTP、GraphQL 等)都有一个监听器,都由一个库提供。监听器接收网络输入,然后调用应用程序找到服务对象。服务对象支持以下两种接口类型:


  • 远程方法——使用动态命名,支持 RPC 风格

  • 资源——使用方法(如 GET)加名词定义,支持 RESTful 风格(用于 HTTP 和 GraphQL)


得益于 Ballerina 提供的服务方法以及独特的面向连接的类型系统,你可以从 Ballerina 代码生成一个接口描述,可以是 OpenAPI 规格的,也可以是 GraphQL 规格的。这样,你就可以实际编写服务对象,并生成客户端代码了。这些特性组合在一起就使得云集成可以顺利进行了。


import ballerina/http; service on new http:Listener(9090) {  resource function get greeting(string name) returns string {     return "Hello, " + name;   } }
复制代码


消费远程服务的客户端对象出站网络交互表示为客户端对象。客户端中有远程方法,表示和远程系统的出站交互。客户端对象是其中一个语法元素,让我们可以绘制序列图。


import ballerina/email;
function main() returns error? { email:SmtpClient sc = check new("smtp.example.com", "user123@example.com", "passwd123"); check sc -> sendMessage({ to: "contact@ballerina.io", subject: "Ballerina" body: "Ballerina is pretty awesome!" });}
复制代码



代码上云


Ballerina 支持从代码生成 Docker 和 Kubernetes 工件,不需要任何额外的配置。这简化了开发以及向云上部署 Ballerina 代码的体验。代码上云要先从代码中获取所需的值然后再构建容器和所需的工件。要了解详细信息,可以看下这个例子。要将代码部署到不同的云平台上,如 AWS 和微软 Azure,可以使用服务对象注解轻松实现云部署,如下所示。Ballerina 编译器可以生成 Dockerfile、Docker 镜像、Kubernetes YAML 文件、无服务器函数等工件。例如,Ballerina 函数可以使用 @azure_functions:Function 注解部署到 Azure 上。


import ballerina/uuid;import ballerinax/azure_functions as af;
// 没有身份验证的HTTP请求/响应。@af:Functionpublic function hello(@af:HTTPTrigger { authLevel: "anonymous" } string payload) returns @af:HTTPOutput string|error {
return "Hello, " + payload + "!";
}
复制代码

8. 显式错误控制流

错误处理方法对于语言设计和使用有着深远的影响。它会影响语言的方方面面。当你和网络打交道时,错误是正常业务处理的一部分,尤其是考虑到分布式计算的8大谬误时。像 Java、JavaScript、TypeScript 等前云时代的语言,使用异常作为其错误处理方式。但并不是每种语言都遵循那种设计。像 Go 和 Rust 这样的语言根本就没有异常。


使用异常,控制流是隐式的,代码理解和维护的难度都比较大。当出现问题时,只是方便地抛出一个异常,就会使什么东西都失控。为了实现恰当的错误处理,你必须得仔细看下程序,弄清楚可能出现错误的地方是否有错,以及控制流如何变化。所以,现在有一个相当强的趋势,就是消除异常,回归一种更简单的方法,错误是显式的,使用正常控制流进行处理。Go、Rust、Swift 都使用了这种方法。Ballerina 也使用了这一方法,让开发人员可以使用 error 数据类型,以及显式的错误控制流。


import ballerina/io;
// 将bytes转换成string,然后再转换成int。function intFromBytes(byte[] bytes) returns int|error {
string|error ret = string:fromBytes(bytes);
// is操作符可以用于区分错误和其他值。 if ret is error {
return ret; } else { return int:fromString(ret); }}
// main会返回一个error。public function main() returns error? {
int|error res = intFromBytes([104, 101, 108, 108, 111]); if res is error { // 可以使用`check`表达式来简化这个判断模式。 return res;
} else { io:println("result: ", res); }}
复制代码

9. 将事务作为语言特性

编写使用事务的 Ballerina 程序非常简单,因为事务是它的一个语言特性。Ballerina 提供的不是事务性内存,而是从根本上支持事务划分。这样就可以保证,事务总是包含 begin、rollback 或 commit 选项。


Ballerina 程序正在运行的实例中包含一个事务管理器。它可能是和 Ballerina 程序在同一个进程中运行,也可能是在一个单独的进程中(连接网络要可靠)。事务管理器维护了从每个 strand 到事务栈(或者是分布式上下文中的事务分支)的映射。当 strand 的事务栈非空时,我们就说它处于事务模式;strand 事务栈最顶端的事务就是该 strand 当前的事务。


import ballerina/io;
public function main() returns error? { // 编译时会保证事务以begin开始,以commit或rollback结束。Transaction 语句 // 会开启一个新事务,并执行一个代码块。 transaction { doStage1(); doStage2();
// 事务要使用commit语句显式提交,这可能会导致一个错误。 check commit;
}}
function doStage1() { io:println("Stage1 completed");}
function doStage2() { io:println("Stage2 completed");}
复制代码


为了支持分布式事务,Ballerina 中的事务还加入了网络交互特性(即客户端和服务)。用户可以将服务的资源/远程方法以及客户端对象的远程方法声明为事务性的,从而创建客户端和服务之间的事务流。

10. Ballerina 提供了许多开发人员熟悉的特性,可谓 Batteries Included

新语言大量出现表明人们还是很愿意学习新语言。但是,企业似乎不太愿意采用新语言,因为他们会担心招聘不到熟悉那门语言的人。因此,要重点说明一下,Ballerina 不仅提供了更好的做事方式,而且还提供了一组 C 语言家族程序员熟悉的特性,他们只需几个小时甚至更少的时间就足以上手开发。


广为流行的 C 语言家族(C、C++、Java、JavaScript、C#、TypeScript)有许多共同点。Ballerina 利用了这一点,许多事情都采用了同样的方式。如果对于 C 语言家族中的任何一门语言,你有一定量的编程经验,那么使用 Ballerina 编码都会相当简单。除了功能强大的语言特性外,Ballerina 还是“batteries included”的,即提供了丰富的标准库(包括用于网络数据、消息和通信协议的库)、包管理系统、结构化文档、测试框架,它还为流行的 IDE(特别是 Visual Studio Code)提供了扩展/插件,以便使该工具支持这门语言。

小结

虽然 Ballerina 具备现代编程语言的所有通用功能,但它的优势在于其提供了一些独特的语言特性,让开发人员可以更容易使用、组合和创建云端网络服务。开发人员现在可以构建富有弹性的、安全的、高性能的服务,消除分布式计算的谬误,并使用一种专门的编程语言将它们整合在一起创建云原生应用程序。


要想快速了解在 Ballerina 中如何创建以及消费 HTTP 服务,可以观看这个录屏视频。如果你更喜欢通过示例进行学习,那么这里提供了许多代码示例,从中你可以了解到 Ballerina 的重要特性和概念。


要想深入了解 Ballerina Swan Lake 的语言特性,可以观看 Ballerina 首席语言设计师 James Clark 提供的系列视频。你还可以阅读这篇博文,了解 Ballerina 的设计原则。

作者简介


Dakshitha Ratnayake 目前在 WSO2 担任 Ballerina 的项目经理。她拥有软件工程的背景,在 WSO2 担任软件工程师、解决方案架构师和技术布道师等职务,拥有超过 10 年的经验。在此期间,她一直是 WSO2 API 管理、企业应用集成、身份和访问管理、微服务架构、事件驱动架构和云原生编程等领域的技术倡导者。与此同时,她还与不同的组织保持着技术关系,以理解业务需求,沟通技术战略。


查看原文:


Ballerina Swan Lake: 10 Compelling Language Characteristics for Cloud Native Programming

2021-10-08 16:101787

评论

发布
暂无评论
发现更多内容

Flutter 中动画的使用

android 程序员 移动开发

Flutter_bloc框架使用笔记,后续估计都不太会用了(1)

android 程序员 移动开发

Flutter_bloc框架使用笔记,后续估计都不太会用了

android 程序员 移动开发

Dart _ 浅析dart中库的导入与拆分

android 程序员 移动开发

esp32~MP3音频文件学习

android 程序员 移动开发

EventBus 发送的消息,如何做到线程切换?

android 程序员 移动开发

Dalvik 和 ART 有什么区别?深扒 Android 虚拟机发展史,真相却出乎意料!

android 程序员 移动开发

Flutter与Dart-入门

android 程序员 移动开发

DateUtils(一个日期工具类)

android 程序员 移动开发

Flutter 制作一个抽屉菜单

android 程序员 移动开发

Flutter 底部浮动按钮(模仿咸鱼APP底部)

android 程序员 移动开发

Flutter 核心原理与混合开发模式

android 程序员 移动开发

Flutter-vs-React-Native,谁才是跨平台应用开发的最佳利器?(1)

android 程序员 移动开发

Flutter40

android 程序员 移动开发

Flutter如何实现下拉刷新和上拉加载更多(1)

android 程序员 移动开发

Flutter实战1 --- 写一个天气查询的APP

android 程序员 移动开发

FFmpeg 之I、B、P帧的基本编码原理(三

android 程序员 移动开发

FFmpeg-之音视频解码与音视频同步(二)

android 程序员 移动开发

Flutter 扩展NestedScrollView (二)列表滚动同步解决

android 程序员 移动开发

Flutter动画:用Flutter来实现一个拍手动画

android 程序员 移动开发

Flutter如何实现下拉刷新和上拉加载更多

android 程序员 移动开发

Dalvik 和 ART 有什么区别?深扒 Android 虚拟机发展史,真相却出乎意料!(1)

android 程序员 移动开发

Flutter-VS-React-Native-VS-Native,谁才是性能之王

android 程序员 移动开发

Flutter-vs-React-Native,谁才是跨平台应用开发的最佳利器?

android 程序员 移动开发

Flutter33

android 程序员 移动开发

Flutter40(1)

android 程序员 移动开发

Flutter - 路由管理 - 02 - Fluro

android 程序员 移动开发

Flutter32

android 程序员 移动开发

Flutter填坑全面总结(包括Flutter1

android 程序员 移动开发

Flutter Interact 的 Flutter 1

android 程序员 移动开发

DatePickerDialog时间选择器+MVPPlugin开发插件的使用

android 程序员 移动开发

Ballerina Swan Lake:10个令人瞩目的云原生编程语言特性_大数据_Dakshitha Ratnayake_InfoQ精选文章