kotlin web
您可能听说过Kotlin 。 它是一种现代编程语言,可编译为JVM字节码和JavaScript。 它专为工业用途而设计,这意味着可维护性:静态类型可实现错误检测和自动重构,类型推断可生成清晰易读的代码,而强大的抽象则可促进库开发。
如果您不能在网络上使用某种语言,它并不是很好,因此在本文中,我们将讨论Kotlin中的网络编程。
当然,您可以编写与Java中相同的servlet,也可以在Kotlin中使用jQuery,但是还有其他一些我们要关注的方面。 但是首先,让我快速绕道,并介绍一些有关当今人们如何制作Web应用程序的信息。
即使是适度的Web应用程序也涉及许多技术,其中很多都与它自己的语言相关联。 通常至少涉及五种语言:
- 用于标记HTML;
- CSS样式
- 用于服务器端业务逻辑的Java(在本文中,我们将限于Java平台);
- JSP或其他模板模板引擎;
- 最后是用于客户端脚本JavaScript。
因此,那里有五种语言。 如果仅凭此事实会让您有些不舒服,请继续阅读,我们将继续讨论,但是首先让我们看一下最核心的内容: HTML和CSS 。
关于HTML和CSS的一件非常特别的事情是,它们不是编程语言,而是更像数据格式的东西。 特别是,这两种语言都没有任何抽象或代码重用的方法。 (公平地说,我们应该注意HTML可以重用CSS定义,但是CSS内不能重用CSS,HTML内也不会重用HTML。)我再说一遍:世界上两种最受欢迎的语言不允许代码重用。
此时,您头脑中的软件工程警报应该已经响起; 这是否意味着我们一直在复制并粘贴该标记? 究竟。
许多人意识到这是一个问题。 解决方案以更好的语言形式出现。 例如,大多数模板引擎提供了一些标记重用的方法(尽管在很多情况下这很痛苦)。 并且有LESS和SASS为CSS带来了诸如命名常量之类的尖端功能。
让我解释。 如果您使用的是纯CSS,则每次描述(例如)一种颜色哈密瓜时,都要输入文字常量#ffcc66(或#fc6)。 也许您CSS上有36次出现此常数。 现在,您想将其中一些更改为其他内容,并且今天下午您不太喜欢您的工作。
显然,处理此问题的正确方法是定义与该颜色的角色相对应的命名常量,以便每个角色仅需更改一次值。 命名常量对值进行抽象,并允许代码重用。 CSS没有命名常量。 我们必须使用诸如LESS之类的预处理程序来正确地重用我们CSS代码。
有整个网站可以为您生成CSS。 您说“我想要一个具有这种形状和颜色的按钮”之类的内容,然后获得一个CSS代码段。 到目前为止,一切都很好。 但是随后,您将该代码段复制并粘贴到样式表中。 没有一个库定义具有良好类型的属性shape和color的名为Button的组件,CSS根本不支持。 取而代之的是,网络上的某处有一些文本字段可以执行库应做的工作:提供更高级别的抽象。 但重要的是,它不能提供代码重用。
总结一下:Web编程缺乏抽象和代码重用的 手段 。
最重要的是,当您的应用程序变大时(这就是开发成本开始变得重要的时候),其他问题也开始起作用:在Web应用程序中捕获错误的唯一方法是测试。 甚至是非常愚蠢的错误,例如拼错名称。
不要误会我的意思:必须测试,没有办法解决。 但是到处都显示出一些琐碎的安全特性,为所有这些特性编写测试实在是一件繁重的工作。 不得不大量测试琐碎的事情太昂贵了。
普通错误应由编译器自动检测。 是的,我说的是静态类型。 动态语言很酷,但我也知道现代静态语言甚至更酷,因为它们为您提供几乎相同的灵活性,但除此之外,它们还为您提供安全性。
您可以使用静态语言进行自动重构,准确的代码完成以及其他IDE功能,从而节省了您的时间和精力。
您可能已经看到了它的到来:Kotlin是一种静态类型的语言,具有很好的抽象和代码重用方法,那么为什么我们不使用Kotlin而不是上面提到的五种语言呢?
本文的其余部分将描述如何做到这一点。 我们将专注于原则,而不是任何特定的实现,但我将为您提供参考,以便您有所收获。 Kara是完全用Kotlin编写的开源Web框架。 它以类似DSL的API的形式统一了Web应用程序的许多方面,因此您的标记或任何其他代码的每一位都将被静态检查并可以重复使用。 同样,本文与该特定框架无关,但是我偶尔会使用Kara中的示例来说明所讨论的原理。
鸟瞰
首先,让我们看看“以API的形式统一Web应用程序的许多方面”是什么意思。 清单1显示了此标记在Kara中的外观。
卡拉标记
html {
head {
title(“Example”)
}
body {
ul {
li { +“Item 1” }
li { +“Item 2” }
}
}
}
完结
这段代码创建了一个标记树,可以根据要求将其序列化为文本形式。 您可以看到它几乎是HTML,只是带有花括号而不是尖括号(并且花括号更少)。 实际上,这只是一堆函数调用,绝对是用Kotlin编写的普通代码。 它被称为类型安全的构建器,非常类似于Groovy著名的构建器,但是经过静态检查。 例如,如果您在ul外面放一个li,则会出现编译错误:
body {
li { ... } // Compilation error here
}
建设者可以处理您模板所做的一切。 首先,他们可以使用循环生成任意复杂HTML。 例如,要生成颜色列表,我们可以编写:
ul {
for (color in listOf(“red”, “green”, “blue”)) {
li { +color }
}
}
您还可以通过简单地将代码提取到函数中来重用部分标记,例如,呈现字符串列表的函数,如下所示:
<pre>fun BodyTag.list(vararg items: String) {
ul {
for (x in items)
li { +x }
}
}
body {
list(“red”, “green”, “blue”)
list(“cyan”, “magenta”, “yellow”)
}
完结
稍后我们将介绍其工作原理,但现在让我们看看样式表是什么样子。
ul {
listStyleType = circle
margin = 10.px
}
这甚至看起来很像CSS,但是再次强调了类型安全,这意味着我们不能使用未定义的属性或添加错误的值。更重要的是它是代码,这意味着我们可以使用任意复杂的算术运算和/或命名常量。
val BASE_MARGIN = 5.px // named constant
ul {
listStyleType = circle
margin = BASE_MARGIN * 2 // arithmetic
}
这也意味着我们可以将此代码的一部分提取到一个函数中,如下所示。
// Define a function
fun StyledElement.doubleMargin() {
margin = BASE_MARGIN * 2
}
ul {
listStyleType = circle
doubleMargin() // use the function
}
完结
因此,我们显然获得了重用和类型安全性。 现在,让我们开始
挖掘以了解所有这些工作原理。
在建造者里面
现在,我们准备看看类型安全的构建器是如何工作的。 让我首先提醒您,它不是内置语言构造,而是普通的Kotlin API。 为了使该API看起来像声明性DSL,我们需要做两件事:lambda和扩展功能。
Lambdas
Lambda有时称为匿名函数。 换句话说,它们是其值为函数的表达式。 要过滤列表,您需要传入一个谓词,即一段代码,告诉filter()函数保留哪些元素以及删除哪些元素:
users.filter({u-> u.age> = 21})
在Kotlin,lambda由花括号分隔; 在lambda中,箭头(“->”)将参数名称列表与主体分开。 在上面的示例中,lambda表达式{u-> u.age> = 21}对应于一个函数,该函数采用一个名为u的参数(其类型是从上下文推断出的),并返回比较的布尔值。 该函数作为参数传递给filter()函数。
filter()函数擅长解释lambda的用途,但由于太复杂而无法解释第一个示例,因此,我们现在将使用一个简单的函数:
让(1 + 2,
{x-> println(x * x)})
let()函数非常简单:它接受第一个参数,并将其传递给第二个参数(即lambda)。 上面的示例打印“ 9”。 有一个很好的语法规则,可让您在括号之外编写最后一个lambda:
let(1 + 2) {
x -> println(x * x)
}
这段代码与前面的示例完全相同,但是lambda现在看起来更像是一些“语言构造”的主体。 我们从Groovy借用了这种语法约定,在这种约定下效果很好。 现在让我们看一下let()的定义。 首先,一个仅适用于数字的简单版本:
fun let(x: Int, f: (Int) -> Unit) {
f(x)
}
let的主体很简单:只需将f()应用于x。 让我们仔细看一下f的声明:
f :(整数)
->单位
由于这是一个参数,其值是一个函数,因此它具有一种特殊的类型,即函数类型。 函数类型的形式为(参数类型)->返回类型,因此f()接受一个Int类型的参数并返回一个Unit类型的值(这意味着“无意义的值”,非常类似于void)。 这就是为什么我们可以在let()中将其称为f(x)的原因,这就是为什么我们可以传递lambda作为其值的原因。
现在,让我们的函数更有用,并允许将任何类型T(不仅限于Int)用作参数:
fun <T> let(x: T, f: (T) -> Unit) {
f(x)
}
我们只是声明了一个通用参数T并使用它代替了Int。 没什么大不了的。 将其他函数作为参数的let()之类的函数称为高阶函数,它们作为抽象手段非常重要。 现在,我们进入下一个主题:扩展功能。
扩展功能
在主流语言中,扩展功能由C#引入。 它们的基本目的是扩展类或接口的API,而不必更改(并因此拥有)其类或接口。 例如,这意味着您可以创建一个扩展函数来返回字符串的最后一个字符:
fun String.last(): Char {
return this.charAt(this.size - 1)
}
此函数在String类之外的某个位置定义,并且不会以任何方式更改该类,但是该语言允许您像调用其成员一样调用它:
println(“ abc” .last())
//打印“ c”
实际上,您正在寻找一种简单的静态实用程序方法,就像您在Java项目中拥有的许多方法一样:
public class StringUtil {
public static char last(String s) {
return s.charAt(s.length() - 1)
}
}
上面定义的last()可以精确地编译为此,但是语法更好,并且可以通过代码完成来发现。 在Kotlin中,当您在点后按Ctrl + Space时会显示扩展功能。
但是,让我们仔细看看:将函数变成扩展的是“字符串”。 在它的名字前面? 这意味着可以在字符串上调用此函数。 要访问被调用的字符串,我们可以使用“ this”关键字,就像我们在类内部一样(但没有访问私有成员的特权)。 当然,“ this”可以省略(这对于构建者很重要):
fun String.last(): Char {
return charAt(size - 1)
}
现在,我们已经准备好两个组件,lambda和扩展。 让我们混合使用它们以获得构建器。
混合
为了轻柔地进行处理,我们将通过调用新版本“ with”从上方稍微更改let()函数开始。 让我们将let()的参数f扩展为:
fun <T> with(x: T, f: T.() -> Unit) {
x.f()
}
变化很小。 我们写“ T.()-> Unit”而不是“(T)-> Unit”,f()变成扩展函数,因此我们可以将其称为“ xf()”。 现在让我们看一下如何使用这个新功能:
with(StringBuilder()) {
this.append(“Hello”)
}
然后,我们需要创建一个新的StringBuilder对象(Kotlin中没有新的运算符)并将其传递给with()。 lambda在名称“ this”下看到此StringBuilder并调用其append()。 当然,“ this”可以省略:
with(StringBuilder()) {
append(“Hello”)
}
现在您可以看到lambda(with()的“主体”)具有不同的上下文:它可以调用StringBuilder()的方法而无需明确提及接收者。 也可以使用任意构造。
with(StringBuilder()) {
append(“List:n”)
for (x in list) {
append(x)
append(“n”)
}
}
因为我们拥有所有组件,所以这几乎已经是一个构建器。 现在,让我们使用它们并为一小部分HTML创建一个真正的生成器。
HTML生成器
建设者的关键是跟踪上下文。 正如我们在with()函数中看到的那样,可以使用扩展lambda跟踪上下文。 因此,with(x){…}使x成为花括号内所有内容的上下文。 让我们使用相同的原理来构建HTML标签树。
为简单起见,我们将仅使用三个标签:body,ul和il,以及text元素(表示HTML页面内的自由文本)。 首先,我们将有一个抽象类Element,它表示HTML文档的任何元素(标签或文本):
abstract class Element {
val children = ArrayList<Element>()
}
它所做的只是保留其子级。 现在,正文和文本是
从中派生,如下所示:
open class Body: Element() {
fun text(s: String) {
children.add(Text(s))
}
...
}
class Text(val text: String): Element()
完结
现在,我们准备为我们的构建器创建一个根函数,即一个创建根标记的函数。 在我们的例子中是body():
fun body(init: Body.() -> Unit): Body {
val body = Body()
body.init()
return body
}
调用此函数类似于调用“ with(Body()){…}”,但它更简短,更简洁:
body {
text(“Hello”) // adds a new Text element to body’s children list
text(“world”) // adds another Text element to the children list
}
现在,让我们添加UL标签:
class UL: Element() {
...
}
UL应该在Body内部可用,因此我们需要在Body类中添加相应的构建器函数:
open class Body: Element() {
...
fun ul(init: UL.() -> Unit) {
val ul = UL()
children.add(ul)
ul.init()
}
}
完结
ul()函数类似于body(),但有一个区别:它不返回新的tag元素,而是将其添加到Body的子级列表中:
body {
text(“List:”)
ul {
// a new instance of class UL is added to body.children
}
}
以相同的方式,我们可以在UL内部包含LI:
class UL: Element() {
fun li(init: LI.() -> Unit) {
val li = LI()
children.add(li)
li.init()
}
}
class LI: Body()
完结
模式是相同的:创建一个新的LI,使用传递给li()函数的lambda对其进行初始化,然后将其添加到UL的子级列表中。 请注意,LI扩展了Body并因此获得了其所有成员:您可以在LI内部创建文本元素和列表(但不能直接在UL内部):
body {
text(“List:”)
ul {
li { text(“First”) }
li { text(“Second”) }
}
}
最后一个问题:Kotlin如何知道li()调用对应于UL,而ul()对应于body()? 这对于我们的构建器至关重要,因为我们希望将元素添加到适当的子级列表中并形成适当的树。 大致的答案是,它寻找适用的最里面的“ this”,因此当您说li()时,编译器将查找语法树并找到最接近的具有li()的“ this”。 如果没有这样的“ this”,即您尝试将LI直接添加到Body,则编译器将报告错误。
添加几百个标签,您将获得用于HTML模板的完整DSL。 实际上,一劳永逸地生成所有这些标记类非常容易,这是Kara的创建者要做的事情之一。 CSS类似地完成。
现在,让我们重新回顾在第一部分中未解释的重用示例:
fun Body.list(vararg items: String) {
ul {
for (x in items)
li { +x }
}
}
body {
list(“red”, “green”, “blue”)
list(“cyan”, “magenta”, “yellow”)
完结
诀窍还是在于,扩展功能会为您跟踪上下文:当您调用list()时,编译器会寻找具有此类成员的最接近的“ this”,或者如果没有成员,则会寻找此类扩展。 在此示例中调用list()时,它将当前的Body实例作为其接收方参数,并将其传递给ul()调用。
摘要
当代的网络编程缺乏重用和抽象以及类型安全的手段。 当代的Web开发人员应该得到更好的回报。 可以通过使用现代编程语言(例如Kotlin)并利用其类型系统和抽象来改善这种情况。 我们通过在Kotlin内创建类型安全的构建器DSL来处理HTML生成来演示了这种方法,并概述了如何以类似方式解决Web应用程序的其他方面。 在Kara项目中正在创建上述概念的全面实现。
翻译自: https://jaxenter.com/type-safe-web-with-kotlin-106187.html
kotlin web