Cro 入门

on

1. Cro 开发工具

Cro 包含帮助开发人员更有效地工作的工具。 当前,这些工具可通过命令行界面使用。 将来还将添加 Web 界面。 没有这些工具,完全有可能使用 Cro。 它们旨在提供一些合理的默认值,但并不适合每个项目。

1.1. 存根服务

可以使用 cro stub 命令对新服务进行存根。 一般用法是 :

cro stub <service-type> <service-id> <path> ['links-and-options']

其中

service-type 是要创建的服务的类型 service-id 是服务的 ID(与其他 cro 命令一起使用;也将在.cro.yml 中用作服务的默认描述性名称)

路径是创建服务的位置 links-and-options 指定指向应添加到存根的其他服务的链接,以及特定于服务类型的选项

如果未指定链接和选项,则将以交互方式请求它们。 要提供选项,请使用类似于 Perl 6 冒号对的语法将其放在引号中,其中 :foo 启用选项,:!foo 禁用选项,而 :foo <bar>是带有值 bar 的选项 foo。 例如 :

cro stub http foo services/foo ':!secure :websocket'
cro stub http bar services/bar ':!secure :websocket'

已存根的服务从环境变量获取端口和证书配置,并且当服务之间存在关系时,其地址也将使用环境变量注入。 设置容器部署时,这很方便。

链接导致存根服务包含创建可以与另一个端点通信的“客户端”的代码。 这些与选项一起使用,格式为 :link <service-id:endpoint-id>。 service-id 是目标.cro.yml 的 id 字段,endpoint-id 是该.cro.yml 文件的终结点列表中条目的 id 字段。

cro stub http foo services/foo ':link<flash-storage:http>'

1.2. HTTP 服务

HTTP 服务中的 http 服务类型存根使用 Cro::HTTP::Router 并由 Cro::HTTP::Server 提供服务。 默认情况下,它对将接受 HTTP / 1.0,HTTP / 1.1 和 HTTP / 2.0 请求的 HTTPS 服务存根。

cro stub http flashcard-backend backend/flashcards

可以提供以下选项:

:secure:生成 HTTPS 服务而不是 HTTP 服务(默认为 :!secure); 暗含 :http1:http2,默认情况下,使用 ALPN 协商是否使用 HTTP / 2

1.3. 运行服务

cro run [<service-id> ...]

cro run 命令用于运行服务。 它会自动设置文件监视功能,并在服务发生源更改时重新启动服务(使用反跳功能来处理更改的踩踏事件,例如,由于从版本控制中获取正在运行的服务的最新更改或在编辑器中保存了许多文件)。要运行所有服务(通过在当前工作目录及其子目录中搜索 .cro.yml 文件来标识),请使用 :

cro run

要运行特定服务,请编写其服务 ID(必须在当前工作目录或其子目录之一的.cro.yml 文件中显示为 ID 字段):

cro run flashcard-backend

它也可以列出多个服务

cro run flashbard-backend users frontend

将显示服务的输出,并以服务名称为前缀。 发送 SIGINT(按 Ctrl + C)将终止所有服务。

1.4. 追踪服务

cro trace <service-id-or-filter>

cro trace 命令与 cro run 非常相似,只是它在服务中打开管道调试。 这样就可以查看每个服务正在接收和发送的流量,以及中间件如何解释和影响它。

输出的数量可能会有点不堪重负,因此可以通过消息类型名称对其进行过滤。 这是通过检查是否有任何名称分量(不区分大小写)等于过滤器来完成的。 包含过滤器表示为 :name,而包含过滤器表示为 :!name。 例如,要从跟踪中排除所有 TCP 消息,请执行以下操作 :

cro trace :!tcp

只看到 HTTP 消息,这样做 :

cro trace :http

为了进一步限制,只是要求,做到 :

cro trace :http :request

任何不以 : 开头的内容均视为服务名称。 顺序并不重要,因此它们是等效的:

cro trace :http flashcard-backend
cro trace flashcard-backend :http

1.5. 服务静态内容

cro serve <host-port> [<directory>]

有时将 HTTP 服务器设置为提供一些静态内容很有用。 使用以下命令为 localhost 的端口 8080 提供当前目录 :

cro serve 8080

或指定目录服务 :

cro serve 8080 static_content/

还可以在端口号之前提供要绑定的 IP 地址 :

cro serve 192.168.0.1:8080 static_content/

1.6. 使用服务链接

cro link 子命令用于管理 .cro.yml 文件的链接部分。 这些描述了一种 Cro 服务如何使用另一种 Cro 服务,从而导致注入环境变量,这些变量指定了可以在其中找到该服务的主机和端口。 在生产中,这些将由诸如 Kubernetes 之类的容器引擎,某种配置管理系统来设置,甚至只是硬编码到包装脚本中。

要添加服务链接,请使用添加:

cro link add <from-service-id> <to-service-id> [<to-endpoint-id>]

其中 from-service-id 是应修改其链接的 .cro.yml 的 ID,to-service-id 是将要使用的服务的 .cro.yml 的 ID,以及 to-endpoint-id 是该服务的 .cro.yml 中端点的 ID。 该命令将在存在匹配所链接服务协议的链接模板的情况下生成一些存根代码,你可以将其存入适当位置的服务代码中(Cro 并不疯狂,以为它可以在以下位置编辑你的代码 你!)

如果未指定 to-endpoint-id,并且 to-service-id 服务只有一个端点,那么默认情况下将使用该端点。 否则,含糊不清将被抱怨。

要重新生成现有链接的代码,请执行以下操作 :

cro link code <from-service-id> <to-service-id> [<to-endpoint-id>]

要移除链接,请使用

cro link rm <from-service-id> <to-service-id> [<to-endpoint-id>]

这只是从 from-service-id 标识的 .cro.yml 的链接部分中删除条目。

2. Cro::HTTP::Router

在 Cro 中,HTTP 应用程序是 Cro::Transform,将 Cro::HTTP::Request 对象转换为 Cro::HTTP::Response 对象。Cro::HTTP::Router 模块提供了一种声明式方式,可将 HTTP 请求映射到适当的处理程序,该处理程序进而产生响应。

2.1. URI 段匹配

路由器使用 Perl 6 签名来匹配 URL 并从中提取段。一组路由放置在路由块中。空签名对应于/。

my $app = route {
    # GET /
    get -> {
        ...
    }
}

文字 URL 片段表示为文字:

my $app = route {
    # GET /catalogue/
    get -> 'catalogue' {
        ...
    }

    # GET /catalogue/products
    get -> 'catalogue', 'products' {
        ...
    }
}

位置变量用于捕获 URI 段。默认情况下,它们将作为 Str 提供,因此在参数上放置 Str 类型约束等同于将其保留。

在参数上放置一个 Int 类型约束意味着仅当该段可以解析为整数(/ ^'-'?\ d + $ /)时,路由才会匹配。UInt 的识别和处理方式相同,但只允许使用正整数。也可以使用 int8,uint8,int16,uint16,int32,uint32,int64 和 uint64,它们的工作方式类似于 Int 或 UInt,但是执行范围验证。

my $app = route {
    # GET /catalogue/products/42
    get -> 'catalogue', 'products', uint32 $id {
        ...
    }

    # GET /catalogue/search/saussages
    get -> 'catalogue', 'search', $term {
        ...
    }
}

为了进行更强大的匹配,还可以使用 where 子句或子集类型。

my $app = route {
    my subset UUIDv4 of Str where /^
        <[0..9a..f]> ** 12
        4 <[0..9a..f]> ** 3
        <[89ab]> <[0..9a..f]> ** 15
        $/;

    get -> 'user-log', UUIDv4 $id {
        ...
    }
}

对于子集类型,基础标称类型必须为 Str 或 Int(或 Any 或 Mu,与 Str 等效)。URL 段上的所有其他类型都是不允许的。

可变段可以设为可选,在这种情况下,即使不存在该段,路由也将匹配。

my $app = route {
    # GET /products/by-tag
    # GET /products/by-tag/sparkly
    get -> 'products', 'by-tag', $tag-name? {
        ...
    }
}

可以使用浆糊的位置来捕获所有尾随段。

route {
    get -> 'catalogue', 'tree', *@path {
        ...
    }
}

与某些路由引擎不同,Cro::HTTP::Router 并不纯粹根据路由的声明顺序考虑路由。规则如下:

最长的文字初始段获胜。例如,对于 GET/category/search 的请求,无论以什么顺序声明,路线 get→'category','search'{}都会胜过 get → 'category', $name {}

声明的段总是比粗俗的段好。例如,给定 GET/tree/describe,无论声明的顺序如何,route get→'tree', $operation {} 都会胜过 get → 'tree',*@path {}

具有某些约束的路段的路线将始终比那些没有约束的路段尝试。例如,get → 'product', ISBN13 $ i {} 会在 get → 'product', Str $query {} 之前尝试,然后选择是否匹配。就 Cro 路由器而言,与 Str 相比,Int 受限制。以任何方式约束的路由均被视为“相等”,并且将按照在源代码中写入的顺序对其进行测试。

如果网段上没有路由匹配,则路由器将生成 HTTP 404 Not Found 响应。

2.2. 请求方法

除了 get 之外,路由器还为 HTTP 方法 post,put,delete 和 patch 导出子项。

my $app = route {
    # POST /catalogue/products
    post -> 'catalogue', 'products' {
        ...
    }

    # PUT /catalogue/products/42
    post -> 'catalogue', 'products', uint32 $id {
        ...
    }

    # DELETE /catalogue/products/42
    delete -> 'catalogue', 'products', uint32 $id {
        ...
    }

    # PATCH /catalogue/products/42
    patch -> 'catalogue', 'products', uint32 $id {
        ...
    }
}

这些都是 http 子代码的缩写:

my $app = route {
    # POST /catalogue/products
    http 'POST', -> 'catalogue', 'products' {
        ...
    }

    # PUT /catalogue/products/42
    http 'PUT', -> 'catalogue', 'products', uint32 $id {
        ...
    }

    ...
}

除了可以与任何 HTTP 请求方法一起使用之外:

my $app = route {
    http 'LINK', -> $id {
        ...
    }
    http 'UNLINK', -> $id {
        ...
    }
}

但是,除非将用于承载应用程序的 Cro::HTTP::Server 配置为使用其 allowed-methods 构造函数参数接受它们,否则它们将无法工作。

如果路由在网段上匹配,但在 HTTP 方法上不匹配(例如,执行了 PUT,但是唯一匹配的路由是 GET 和 POST),则结果将是 HTTP 405 Method Not Allowed 响应。

2.3. 查询字符串,header 和 cookies

路由签名中的命名参数可用于解压缩和约束请求其他方面的路由匹配。默认情况下,值是从查询字符串中获取的,但是可以应用特征使它们使用其他来源的数据。请注意,默认情况下,Perl 6 中的命名参数是可选的。如果需要查询字符串参数,则应将其标记为此类。

my $app = route {
    # Must have a search term in the query string.
    get -> 'search', :$term! {
        ...
    }

    # Mininum and maximum price filters are optional; sourced from the query
    # string.
    get -> 'category', $category-name, :$min-price, :$max-price {
        ...
    }

    # Must have a term in the query string (the `is query` trait here is purely
    # for documentation purposes).
    get -> 'search', :$term! is query {
        ...
    }
}

与 URL 段一样,可以使用 Int,UInt,int64,uint64,int32,uint32,int16,uint16,int8 和 uint8 类型将参数解析为整数并进行范围检查。此外,还允许基于 Int,UInt 和 Str 的子集类型。

有时,可以在查询字符串中为给定名称提供多个值。在这些情况下,如果没有类型约束,则将传递 Cro::HTTP::MultiValue 类型的对象。该对象继承自 List,因此可以进行迭代以获取值。它还扮演 Stringy 角色,并字符串化为逗号分隔的各种值。为了排除这种情况,可以应用 Str 约束,如果存在多个值,该约束将拒绝接受请求。要显式地允许多个值,请使用 @sigil(请注意,这不能与其他类型约束一起使用)。这也将很乐意接受零或一个值,分别给出一个空列表和一个 1 元素列表。使用 where 子句进一步约束它。

my $app = route {
    # Require a single city to search in, and allow multiple selection of
    # interesting numbers of rooms.
    get -> 'apartments', Str :$city!, :@rooms {
        ...
    }
}

要获取所有查询字符串数据,可以使用一个名为 hash 的参数(可选地,带有 is 查询特征):

my $app = route {
    # Get a hash of all query string parameters
    get -> 'search', 'advanced', :%params {
        ...
    }
}

Cookie 和标头也可以使用 is cookie 和 is 标头特征解压缩到命名参数中。标头会考虑到关于多个值的相同规则(cookie 的每个名称可能没有重复的值)。is 标头特征是唯一不区分大小写的功能,因为请求标头被定义为不区分大小写。

my $app = route {
    # Gets the accept header, if there is one.
    get -> 'article', $name, :$accept is header {
        ...
    }

    # Gets the super-sneaky-tracking-id cookie; does not match if there
    # is no such cookie (since it's marked required).
    get -> 'viral', $meme, :$super-sneaky-tracking-id! is cookie {
        ...
    }

    # Gets all the cookies and all the headers in hashes.
    get -> 'dump', :%cookies is cookie, :%headers is header {
        ...
    }
}

命名参数不参与使用路由段完成的请求的初始路由。但是,它们可能会在与相同路线段匹配的多个可能路线之间平局。在这种情况下,将按照在源代码中写入的顺序对其进行尝试,但不带任何命名参数的路由处理程序将在最后尝试。这意味着可以通过所需的命名参数来区分处理程序。

my $app = route {
    # /search?term=mountains&images=true
    get -> search, :$term!, :$images where 'true' {
        ...
    }

    # /search?term=mountains
    get -> search, :$term! {
        ...
    }
}

如果在 URL 段上至少有一个匹配的路由处理程序,但是所有候选路由在使用命名参数表示的条件下均不匹配,则路由器将产生 HTTP 400 错误请求响应。

2.4. 访问 Cro::HTTP::Request 实例

在请求处理程序内部,可以使用请求项来获取与当前请求相对应的 Cro::HTTP::Request 对象。在许多请求处理程序中,这不是必需的,因为签名允许解包最常见形式的请求信息。但是,如果需要完整的请求标头集(按其原始顺序),则可以使用 request 访问它们。

my $app = route {
    get -> {
        say "Request headers:";
        for request.headers {
            say "{.name}: {.value}";
            content 'text/plain', 'Response';
        }
    }
}

2.5. 请求体

标题可用后,Cro::HTTP::Router 将调度请求; 一旦通过网络到达,请求主体(如果有)将变得可用。请求项提供了 Cro::HTTP::Request 对象,该对象具有用于访问请求主体的各种方法(主体,主体文本和主体主体),所有方法均返回 Promise。为方便起见,路由器还导出子请求体,请求体文本和请求体 blob,这些请求占用一个块。这些例程将在请求对象上调用适当的方法,等待它,然后使用它调用该块。例如:

post -> 'product' {
    # Get the body produced by the body parser for a JSON request body (it
    # is deserialized automatically if the content-type of the request
    # indicates JSON).
    request-body -> %json-object {
        # Save it, and then...
        created 'product/42', 'application/json', %json-object;
    }
}

post -> 'photos', 'add' {
    # Given a HTML form using enctype="multipart/form-data", with a text
    # input "title" and an file upload input "photo", get the title and
    # uploaded file's filename and contents.
    request-body -> (:$title, :$photo, *%rest) {
        my $file-name = $photo.filename;
        my $file-content = $photo.body-blob;
        # Process the upload
    }
}

put -> 'product', $id, 'description' {
    # Get the body as text (assumes the client set the body to some text; note
    # this is not something a web browser form would do).
    request-body-text -> $description {
        # Save it; produce no content response
    }
}

put -> 'product', $id, 'image', $filename {
    # Get the body as a binary blob (again, this assumes that a client send a
    # request with a body that's a bunch of bytes that we want; this may happen
    # in a HTTP API, but not from a browser).
    request-body-blob -> $image {
        # Save it; produce no content
    }
}

块签名可以使用 Perl 6 签名来解压缩提交的数据。例如,这对于解压缩 JSON 对象很有用:

post -> 'product' {
    request-body -> (:$name!, :$description!, :$price!) {
        # Do stuff here...
    }
}

如果无法绑定签名,则会引发异常,从而导致 400 Bad Request 响应。这意味着签名也可以用于对请求主体进行基本验证。

如果 request-body,request-body-text 或 request-body-blob 被成对传递,则该键将被视为要与请求的 Content-type 标头匹配的媒体类型。媒体类型上的所有参数都将被忽略(例如,文本/纯文本的 Content-type 标头; charset = UTF-8 将仅考虑文本/纯文本部分)。如果请求与 Content-type 不匹配,则将引发异常,导致 404 错误请求响应。

post -> 'product' {
    request-body 'application/json' => -> (:$name!, :$description!, :$price!) {
        # Do stuff here...
    }
}

如果给定了多个参数,则将按顺序尝试它们,直到一个匹配为止,并抛出一个异常,如果不匹配,则会导致 400 Bad Request 响应。参数可以是块,对或混合。这允许打开请求正文的 Content-type:

put -> 'product', $id, 'image' {
    request-body-blob
        'image/gif' => -> $gif {
            ...
        },
        'image/jpeg' => -> $jpeg {
            ...
        };
}

或打开 Content-type,但最后提供一个块作为后备:

put -> 'product', $id, 'image' {
    request-body-blob
        'image/gif' => -> $gif {
            ...
        },
        'image/jpeg' => -> $jpeg {
            ...
        },
        {
            bad-request 'text/plain', 'Only gif or jpeg allowed';
        }
}

如果主体块的参数具有类型约束,where 约束或子签名,则将对其进行测试以查看其是否与主体对象匹配。如果没有,那么将尝试下一个替代方法。这允许对某些传入数据的结构和内容进行模式匹配:

post -> 'log' {
    request-body
        -> (:$level where 'error', :$message!) {
            # Process errors specially
        },
        -> (:$level!, :$message!) {
            # Process other levels
        };
}

2.6. 添加自定义请求体解析器

默认情况下,为请求提供了五个正文解析器:

Cro::HTTP::BodyParser::WWWFormUrlEncoded-每当内容类型为 application/x-www-form-urlencoded 时使用;解析表单数据并将其提供为 Cro::HTTP::Body::WWWFormUrlEncoded 的实例

Cro::HTTP::BodyParser::MultiPartFormData-每当内容类型为 multipart/form-data 时使用;解析多部分文档并将其提供为 Cro::HTTP::Body::MultiPartFormData 的实例

Cro::HTTP::BodyParser::JSON-每当内容类型是 application-json 或带有+ json 后缀的任何内容时使用;使用 JSON::Fast 模块解析数据,该模块返回哈希或数组

Cro::HTTP::BodyParser::TextFallback-每当内容类型具有文本类型时使用(例如,text/plain,text/html);使用正文

Cro::HTTP::BodyParser::BlobFallback-不得已而为之,它将匹配任何消息;使用身体斑点

Cro 可以使用其他主体解析器进行扩展,这些主体解析器将实现 Cro::HTTP::BodyParser 角色。可以在设置 Cro::HTTP::Server 时通过传递它们来全局添加它们。它们也可以在路由块的范围内应用:

my $app = route {
    body-parser My::Custom::BodyParser;

    post -> 'product' {
        request-body -> My::Type $body {
        }
    }
}

在这里,使用了正文解析器 My::Custom::BodyParser,它大概会生成 My::Type 类型的对象。从使用 YAML 或 XML 解析器到将请求解析到特定于应用程序的域对象中,都可以使用它。

2.7. 生成响应

在调用请求处理程序之前,路由器会创建一个 Cro::HTTP::Response 对象。默认响应为 204 No Content,如果设置了正文,则为 200 OK。可以使用响应项访问此对象。因此,可以通过在响应项上调用方法来产生响应:

my $app = route {
    get -> 'test' {
        given response {
            .append-header('Content-type', 'text/html');
            .set-body: q:to/HTML/;
                <h1>Did you know...</h1>
                <p>
                  Aside from fingerprints, everyone has a unique tongue print
                  too. Lick to login could really be a thing.
                </p>
                HTML
        }
    }
}

这可能会花费很多时间,因此路由器模块还会导出各种例程,以处理最常见的形成响应的情况。

标头例程调用 response.append-header。它可以接受两个字符串(名称和值),形式为 Name:value 的单个字符串或 Cro::HTTP::Header 的实例。

内容例程采用一种内容类型和应构成响应主体的数据。数据将由主体串行器处理。有效的默认正文序列化器集允许:

将内容类型设置为 application/json 或带有+ json 后缀的任何媒体类型,并提供可以由 JSON::Fast 处理的主体

提供一个 Str 主体,它将根据 content-type 中的任何字符集参数进行编码

提供一个 Blob,它将用作主体

提供补给,这将意味着身体将随着时间的推移而产生;除非明确设置了内容长度标头,否则将使用分块传输编码发送响应

因此,一个简单的 HTML 响应可以写为:

my $app = route {
    get -> 'test' {
        content 'text/html', q:to/HTML/;
            <h1>Did you know...</h1>
            <p>
              Aside from fingerprints, everyone has a unique tongue print
              too. Lick to login could really be a thing.
            </p>
            HTML
    }
}

创建的例程用于响应创建新资源的 POST 请求。结果为 HTTP 201 响应。可能需要:

一个位置参数,用于指定所创建资源的位置,该位置参数将用于填充 Location 标头

三个位置论点。第一个将用于设置 Location 标头;其余两个将传递给内容例程。为方便起见,先保存对创建的调用,然后再保存对内容的调用。

重定向例程用于重定向。它使用一个单独的位置参数来指定要重定向到的 URL,并将其放置在 Location 响应标头中。默认情况下,重定向将导致 HTTP 307 临时重定向。:permanent named 参数可以用来指示应该执行 HTTP 308 永久重定向。出于文档目的,可以传递: temporary。或者,:see-other 可以用来实现 HTTP 303 响应;这是临时的,但表示应该在目标上执行 GET 请求,而不是保留原始请求方法。

my $app = route {
    get -> 'testing' {
        redirect :permanent, '/test';
    }
}

对于包含主体的重定向响应,可以使用重定向例程的三参数形式。后两个参数将用于调用内容(因此,这完全等同于重定向后调用内容)。

对于最常见的 HTTP 错误代码,还存在其他例程。这些全部采用零自变量或两个自变量。设置状态码后,两个参数的形式会将其两个参数传递给内容。他们是:

找不到,用于 HTTP 404 找不到 错误请求,用于 HTTP 400 错误请求 禁止,用于 HTTP 403 禁止 冲突,用于 HTTP 409 冲突 如果请求处理程序的评估结果为…​(即存根代码的 Perl 6 语法),则将生成 HTTP 510 Not Implemented 响应。如果评估路由处理程序产生异常,则该异常将被传递。然后,通常由响应序列化程序处理该响应序列化程序,该序列化程序将生成 HTTP 500 Internal Server Error 响应,但是可以插入其他中间件来更改发生的情况(例如,提供自定义错误页面)。

所有其他响应代码都是通过显式设置 response.status 产生的。

2.8. 服务静态内容

静态例程用于轻松提供静态内容。它将请求主体的内容设置为所服务文件的内容,并根据文件的扩展名设置内容类型。

以其最简单的形式,static 只是为单个文件提供服务:

get -> {
    static 'www/index.html';
}

在其多参数形式中,它将第一个参数视为基本目录,然后对其余参数进行处理并将其视为要附加到基本目录的路径段。这对于从基本目录提供内容很有用:

get -> 'css', *@path {
    static 'css', @path;
}

此表单永远不会在基本目录之外提供内容; 试图做../把戏的路径将无法逃脱。基础也可以指定为 IO::Path 对象。

对于这两种形式中的任何一种,如果都找不到文件,则将提供 HTTP 404 响应。如果路径解析为非常规文件(例如目录)或无法读取的文件,则将提供 HTTP 403 响应。

MIME 类型映射的文件扩展名的默认集合是从 Apache Web 服务器随附的列表中派生的。如果未找到映射,则内容类型将设置为 application/octet-stream。要提供额外功能或覆盖默认值,请将 mappins 的哈希值传递给名为参数的 mime 类型。

get -> 'downloads', *@path {
    static 'files', @path, mime-types => {
        'foo' => 'application/x-foo'
    }
}

要在请求目录时提供索引文件(例如,对 content/foo /的请求将为 downloads/foo/index.html 提供服务),请传递名为参数的索引,并指定应考虑的文件名:

get -> 'content', *@path {
    static 'static-data/content', @path,
        :indexes<index.html index.htm>;
}

2.9. 添加自定义响应体序列化器

自定义主体序列化程序实现了 Cro::BodySerializer 角色。他们可以通过考虑正文和/或响应标头(最通常是内容类型标头)的类型来决定何时适用。

主体序列化器可以在配置 Cro::HTTP::Server 时应用,在这种情况下,它们将适用于所有请求。它们也可以在给定的路由块中应用:

my $app = route {
    body-serializer Custom::Serializer::YAML;

    get -> 'userlevels' {
        content 'application/yaml', <reader moderator producer admin>;
    }
}

2.10. 设置响应体缓存能力

cache-control 子控件提供了一种方便的方式来设置 HTTP 响应中的 Cache-control 标头,该标头控制是否可以将响应缓存到缓存以及缓存多长时间。如果已经有一个缓存控制标头,则将其删除并按指定添加一个新的标头。

可以为高速缓存控制子传递以下命名参数:

:public-可以由任何缓存存储 :private-只能由单用户缓存存储(例如,在浏览器中)

:no-cache-缓存条目可能永远都不会被使用,除非检查它是否仍然有效

:no-store-响应甚至可能永远不会存储在缓存中 :max-age(600)-在缓存将其逐出之前内容可以变为多久

:s-maxage(600)-对于 maxage,但仅适用于共享缓存 :must-revalidate-超过最大使用期限后,不得使用该响应

:proxy-revalidate-与必须重新验证相同,但用于共享代理 :no-transform-不得转换响应(例如重新压缩图像)

它将发出一个带有逗号分隔选项的单个 Cache-Control 标头。

在任何高速缓存中最多将图像资产高速缓存 10 分钟的典型用法是:

cache-control :public, :max-age(600);

这可以与静态文件服务一起使用:

get -> 'css', *@path {
    cache-control :public, :max-age(300);
    static 'css', @path;
}

要声明一个响应永远不应该存储在缓存中,从理论上讲,它足以声明:

cache-control :no-store;

但是,由于不同的用户代理给出了不同的解释,因此明智的做法是使用

cache-control :no-store, :no-cache;

2.11. Push 承诺

即将发布的功能, 本节介绍即将发布的 Cro 版本中将包含的功能。

HTTP/2.0 允许响应包括推送承诺。推送承诺用于将与响应相关联的内容推送给客户端。例如,可以推送页面所需的 CSS,JavaScript 或图像,以免客户端在解析页面时无需请求它们。

路由器提供的推送承诺功能是在当前响应中包括推送承诺的最便捷方法。最简单的方法是使用资源的路由来调用它以响应:

get -> {
    push-promise '/css/main.css';
    content 'text/html', $some-content;
}
get -> 'css', *@path {
    cache-control :public, :max-age(300);
    static 'assets/css', @path;
}

如果通过包含或带有前缀的委托到达路由,则前导/将被解释为相对于封闭的路由块(换句话说,任何前缀都将作为形成所承诺的 URL 的前缀)。

推送承诺的处理类似于请求; 它们由 HTTP/2.0 消息解析器发出,因此将通过正常请求所需要的所有中间件。

默认情况下,推送承诺请求中不包含标题。要包含它们,请传递带有散列或成对列表的标题参数形参(如果标题排序或需要多个相同名称的标题,则存在后一种形式)。要简单地传递出现在当前正在处理的请求中的所有标头,请使用*作为值; 如果当前请求没有这样的标头,则不会将其包含在推送承诺中。

get -> {
    push-promise '/js/strings.js', headers => {
        Accept-language => *
    };
    content 'text/html', $some-content;
}

在当前正在处理的请求不是 HTTP/2.0 请求的情况下,推入承诺功能不执行任何操作。

2.12. 组合路由

对于任何非平凡的服务,在单个文件中定义所有路由及其处理程序将变得难以管理。通过 include,可以将它们移至不同的模块。例如,模块 OurService::Products 可以编写如下:

sub product-routes() is export {
    route {
        get -> 'products' { ... }
        get -> 'products', uint32 $id { ... }
        # ...
    }
}

然后将其路由包含到顶级路由表中:

use OurService::Products;

my $app = route {
    get -> { ... }
    include product-routes;
}

由于模块中的每条路线都必须在其路线的开始处包含产品,因此这仍然有些重复。因此,将模块重构为:

sub product-routes() is export {
    route {
        get -> { ... }
        get -> uint32 $id { ... }
        # ...
    }
}

可以通过在 URL 结构中添加前缀来保留 URL 结构:

use OurService::Products;

my $app = route {
    get -> { ... }
    include products => product-routes;
}

请注意,多个片段基础应该作为列表而不是带有/的字符串传递(而是在片段中寻找 URL 编码的“ /”):

my $app = route {
    get -> { ... }
    include <catalogue products> => product-routes;
}

include 函数可以接受多个路由或对,因此无需重复键入 include:

my $app = route {
    include products  => product-routes,
            forum     => forum-routes,
                         static-content-routes;
}

包含将所包含的路线与本地声明的路线合并,这意味着它们以“统一”方式一起考虑。即使存在前缀,这也适用,这意味着可以放心地将路由处理程序分解为模块。前缀被视为前面的其他文字段。此设计的另一个受欢迎的结果是不会因在多个文件上拆分路由处理程序而导致路由性能损失。

如果包含的路由块具有主体解析器和主体序列化器,则它们对于包含的路由块中的路由也是可见的,从而可以排除使用主体解析器和序列化器。由包含的路由块声明的主体解析器和序列化程序将比包含的路由块提供的主体解析器和序列化程序优先。

包含操作只能用于应用来自使用 Cro::HTTP::Router 构造的另一个 HTTP 路由器的路由。

2.13. 代理路由到另一个 Cro::Transform

使用 Cro::HTTP::Router 并不是编写 HTTP 请求处理程序的唯一方法。路由器可以将特定路径或前缀下面的所有路径委托给使用 Cro::HTTP::Request 并生成 Cro::HTTP::Response 的任何 Cro::Transform。它的用法如下:

my $app = route {
    # Delegate requests to /special to MyTransform
    delegate special => MyTransform;

    # Delegate requests to /multi/part/path to AnotherTransform
    delegate <multi part path> => AnotherTransform;

    # Delegate requests to /proxy *and* everything beneath it to ProxyTransform
    delegate <proxy *> => ProxyTransform.new(%some-config);
}

可以将多个对传递给单个委托调用,因此上述示例可以理解为:

my $app = route {
    delegate special           => MyTransform,
             <multi part path> => AnotherTransform,
             <proxy *>         => ProxyTransform.new(|%some-config);
}

使用委托时,Cro::HTTP::Request 对象将被浅层复制,并且副本的前缀将从其目标中剥离; 这也会影响 path 和 path-segments 的返回值。原始目标,原始路径和原始路径段方法将返回原始路径。

在路由块中声明的主体解析器将在请求的主体解析器选择器之前加上前缀,然后再传递给转换。在路由块中声明的任何主体序列化器都将以转换产生的响应的主体序列化器选择器为前缀。

由于 route {}块使对象执行 Cro::Transform,因此也可以将其与委托一起使用。这与 include 的语义略有不同,并且由于需要执行两次路由调度,所以执行起来会更糟。

2.14. 在路由块中应用中间件

*注意: 此处描述的语义适用于 Cro 0.8.0 及更高版本。早期版本缺少当前的 before 和 after 语义,相反,它们的 before 和 after 具有现在由 before-matched 和 after-matched 提供的语义。

在 Cro 中,中间件是请求处理管道中的组件。它可以安装在服务器级别(有关更多信息,请参见 Cro::HTTP::Server),但也可以使用之前,之前匹配,之后和之后匹配的功能针对每个路由块进行安装。对于 Cro 中的中间件新手来说,HTTP 中间件指南概述了什么是中间件,以及 Cro 中不同的编写和使用 HTTP 中间件的方式之间的权衡。

before 函数用于在处理路由块之前应用中间件。同样,在处理了路由块之后,应用中间件之后。因此:

my $app = route {
    before SomeMiddleware;
    after SomeOtherMiddleware;
    get -> {
        content 'text/plain', 'Hello with middleware';
    }
}

等价于

my $routes = route {
    get -> {
        content 'text/plain', 'Hello with middleware';
    }
}
my $app = Cro.compose(SomeMiddleware, $routes, SomeOtherMiddleware);

当中间件:

可能影响路由匹配的内容(例如,会话和授权中间件)

无论路由块中的任何路由是否匹配,都应执行

之前和之后使用的路由块不能与包含一起使用。中间件应该在匹配之前运行,但是在匹配之前,不可能知道所包含的路由之一是否匹配。改用委托。

相反,匹配前和匹配后指定仅在路由已匹配时才使用的中间件。因此,可以在使用它们的路由块上使用包含。

因此,整个过程可以看作是:

Run any before middleware
If a route matches then
    Run applicable before-matched middleware
    Run the route handler
    Run applicable after-matched middleware
Run any after middleware

匹配前和匹配前均可使用任何使用 Cro::HTTP::Request 并生成 Cro::HTTP::Request 的 Cro::Transform 进行调用。

匹配后和匹配后都可以使用 Cro::Transform 调用,该 Cro::Transform 消耗 Cro::HTTP::Response 并生成 Cro::HTTP::Response。

允许在单个路由块中多次使用所有中间件添加功能,并且中间件将按照其出现的顺序进行添加。

为方便起见,可以将匹配前,匹配前,匹配后和匹配后的功能传递给块。这将使用 Cro::HTTP::Request 或 Cro::HTTP::Response 对象作为参数来调用,并且它可以改变请求或响应(忽略该块的返回值)。也可以使用路由处理程序中可用的各种响应帮助器功能,因此可以通过以下方式为响应添加额外的标头:

after {
    header 'Strict-transport-security', 'max-age=31536000; includeSubDomains'
}

在某些情况下,希望中间件本身能够产生响应。使用块形式时,可以使用响应符号和所有有助于产生响应的功能。如果在运行之前,设置了响应的状态,则将之前的视为已生成响应。例如,可以使用以下方法实现对并非来自环回接口的所有请求产生 403 禁止响应:

before {
    forbidden unless .connection.peer-host eq '127.0.0.1' | '::1';
}

“ before”实际上插入了两个管道组件: 一个运行该块,一个输出由中间件产生的任何“早期”响应。插入后一个组件,就好像它是紧接在之前之后的一个 after 引入的一样。这意味着将跳过在 before 块之前指定的任何 after 中间件,以进行早期响应。

# WRONG - the first `after` will be skipped over and so never see the 403
# Forbidden
after {
    if .status == 403 && !.has-body {
        content 'text/html', '<h1>Forbidden</h1>';
    }
}
before {
    forbidden unless .request.connection.peer-host eq '127.0.0.1' | '::1';
}

# CORRECT - the `after` is placed after any early response from the `before`
# is inserted into the output pipeline, and so will add the content to the
# response, as desired.
before {
    forbidden unless .request.connection.peer-host eq '127.0.0.1' | '::1';
}
after {
    if .status == 403 && !.has-body {
        content 'text/html', '<h1>Forbidden</h1>';
    }
}

当使用 include 时,包含路由块的匹配前中间件将在包含目标中的任何中间件之前应用,而包含路由块的匹配后中间件将在包含目标后应用。有效地,包含路由块的中间件环绕了包含路由块的中间件。

3. Cro::HTTP::Test

原则上,可以通过使用 Cro::HTTP::Server 托管应用程序,使用 Cro::HTTP::Client 对该应用程序发出请求并使用标准的测试库检查结果来编写 Cro HTTP 服务的测试。 该库通过以下方式使编写这样的测试更加容易,并更快地执行它们:

提供更方便的 API 来发出测试请求和检查结果

跳过网络,仅将 Cro::TCP 对象从客户端管道传递到服务器管道,反之亦然

3.1. 基本示例

给定一个 MyService::Routes 模块,如下所示:

sub routes() is export {
    route {
        get -> {
            content 'text/plain', 'Nothing to see here';
        }
        post -> 'add' {
            request-body 'application-json' => -> (:$x!, :$y!) {
                content 'application/json', { :result($x + $y) };
            }
        }
    }
}

我们可以像这样编写测试:

use Cro::HTTP::Test;
use MyService::Routes;

test-service routes(), {
    test get('/'),
        status => 200,
        content-type => 'text/plain',
        body => /nothing/;

    test-given '/add', {
        test post(json => { :x(37), :y(5) }),
            status => 200,
            json => { :result(42) };

        test post(json => { :x(37) }),
            status => 400;

        test get(json => { :x(37) }),
            status => 405;
    }
}

done-testing;

3.2. 为服务设置测试

测试服务功能有两个候选。

test-service(Cro::Transform,&tests,:$ fake-auth,:$ http)候选程序针对提供的 HTTP 应用程序运行测试,该应用程序可以是消耗 Cro::HTTP::Request 的任何 Cro::Transform 并产生 Cro::HTTP::Response。用 Cro::HTTP::Router 编写的应用程序可以做到这一点。也可以使用 Cro.compose 来放置(可能是模拟的)中间件。如果传递了可选的:$ fake-auth 参数,它将在中间件之前,该中间件将请求的 auth 设置为指定的对象。这对于模拟用户或会话并因此测试授权非常有用。 http 参数指定要在其下运行测试的 HTTP 版本。由于我们在测试中同时控制客户端和服务器端,因此不允许设置:http <1.1 2>。默认值为:http <2>。通过将这些命名的参数传递给测试服务,还可以伪造对等主机和对等端口。

测试服务($ uri,&tests)候选者针对指定的基本 URI 运行测试,并通过 Cro::HTTP::Client 连接到它。这样就可以使用 Cro::HTTP::Test 为使用 Cro 以外的东西构建的服务编写测试。

所有其他命名参数作为 Cro::HTTP::Client 构造函数参数传递。

3.3. 编写测试

测试功能在传递给测试服务的模块内部使用。它期望传递一个表示要测试的请求的位置参数,以及一个表示响应的期望属性的命名参数。

通过调用 get,put,post,delete,head 或 patch 中的一种来指定请求。还有其他 HTTP 方法的 request($ method,…​)(实际上,get 只会调用 request('GET',…​))。这些函数接受提供相对 URI 的可选位置参数,如果提供的话,该参数将附加到当前有效的基本 URI 上。 :$ json 命名参数经过特殊处理,扩展为{content-type ⇒'application/json,body ⇒ $ json)。所有其他命名参数将传递给 Cro::HTTP::Client request`方法,从而使所有 HTTP 客户端功能可用。

测试功能的命名参数构成检查。它们很大程度上遵循 Cro::HTTP::Response 对象上的方法名称。可用的检查如下。

3.4. 状态

将响应的状态属性与支票进行智能匹配。虽然最常见的是整数,例如 status ⇒ 200,但也可以使用 status ⇒ * <400(例如不是错误)之类的东西。

3.5. 内容类型

检查内容类型是否等效。如果传递了字符串,它将解析为一种媒体类型,并检查该类型和子类型是否与响应匹配。如果字符串中有任何其他参数(例如字符集),则还将在接收的媒体类型中检查这些参数。如果接收到的媒体类型具有未提及的其他参数,则这些参数将被忽略。因此,check content-type ⇒'text/plain' 匹配 text/plain;响应中为 charset = utf8。

要进行更细粒度的控制,请传递一个块,该块将传递一个 Cro::MediaType 实例,并期望返回某种真实性以通过测试。

3.6. 标题

采用哈希将标头名称映射到标头值,或采用成对的列表进行相同的操作。如果标头存在并且标头的值与该值智能匹配,则测试通过。仅在关心标头存在但不希望检查其值时使用*。响应中的所有其他标头都将被忽略(也就是说,多余的标头被认为是可以的)。

headers => {
        Strict-Transport-Security => *,
        Cache-Control => /public/
    }

为了进一步控制,传递一个块,该块将接收一个成对列表,每个列表代表一个标题。 它的返回值应符合测试的真实性。

3.7. body-text

获取响应的正文并将其与提供的值进行智能匹配。 字符串,正则表达式或代码对象都可能有用。

body-text => /:i success/

如果存在已测试的内容类型并且该测试失败,则将跳过主体测试。

3.8. body-blob

获取响应的主体斑点并将其与提供的值进行智能匹配。

body-blob => *.bytes > 128

如果存在已测试的内容类型并且该测试失败,则将跳过主体测试。

3.9. body

获取响应的主体并将其与提供的值进行智能匹配。请注意,body 属性根据响应的内容类型决定要生成的内容,从而选择合适的 body 解析器。因此,建议将其与 content-type 一起使用(始终在主体之前进行测试,如果主体测试失败,则跳过主体测试)。

3.10. json

这是 JSON 响应的常见情况的便捷捷径。它实现 content-type ⇒ {.type eq’application'&& .subtype-name eq’json'|| .suffix eq’json'}(也就是说,它接受 application/json 或诸如 application/vnd.foobar + json 之类的东西)。

如果传递了代码值,则将使用反序列化的 JSON 主体调用代码,并且应返回真实值以通过测试。否则,将使用 is-deeply 测试例程来检查接收到的 JSON 的结构是否与预期的匹配。

3.11. 使用一个 URI,一组标头等进行许多测试

重复测试的相同细节可能很麻烦。例如,通常希望针对同一 URI 编写许多测试,每次将其传递给不同的正文,或使用不同的请求方法。提供测试的功能有多种形式。它可以与 URI 和块一起使用:

test-given '/add', {
    test post(json => { :x(37), :y(5) }),
        status => 200,
        json => { :result(42) };
    test post(json => { :x(37) }),
        status => 400;
}

在这种情况下,将针对附加在当前有效 URI 上的此 URI 执行所有测试,在测试服务块中,该有效 URI 是要测试的服务的基本 URI。 如果各个测试用例也具有 URI,则还将附加它。 可以嵌套给定的测试块,并且每个块都附加其 URI 段,从而建立新的当前有效 URI。

也可以将命名参数传递给 test-give,这些参数将用作请求参数,并传递给 Cro::HTTP::Client。 请注意,指定要获取或请求的任何命名参数都将覆盖 test-given 中指定的参数。

test-given '/add', headers => { X-Precision => '15' } {
    ...
}

第二种形式不需要相对的 URI,而只是采用选项:

test-given headers => { X-Precision => '15' } {
    ...
}

4. Cro::MediaType

Cro::MediaType 类提供了与媒体类型有关的一系列功能,包括将它们解析成它们的一部分或从它们的一部分序列化。

4.1. 解析媒体类型

要解析媒体类型,请将其传递给 parse 方法:

my $media-type = Cro::MediaType.parse('content/html; charset=UTF-8');

这将产生一个 Cro::MediaType 实例。

4.2. 提取媒体类型部分

考虑一个示例媒体类型 application/vnd.foobar+json; charset = UTF-8。 以下方法可用于提取部分媒体类型:

  • type-返回 application

  • subtype-返回 vnd.foobar+json

  • tree-返回 vnd

  • subtype-name-返回 foobar

  • suffix-返回 json

  • type-and-subtype-返回 application/vnd.foobar+json

  • parameters-返回一个包含 Pair 的数组; 在给定的示例中,charset ⇒'UTF-8'

4.3. 从零件构造媒体类型

可以调用新方法从其各个部分构造媒体类型。 类型和子类型名称的命名参数是必需的; 可以选择提供树,后缀和参数。

4.4. 序列化媒体类型

字符串化 Cro::MediaType 对象,以将其转换为媒体类型的字符串表示形式。

my $media-type = Cro::MediaType.new:
    type => 'application',
    tree => 'vnd',
    subtype-name => 'foobar',
    suffix => 'json',
    parameters => [charset => 'UTF-8'];

say ~$media-type;   # application/vnd.foobar+json; charset=UTF-8

5. Cro 模块结构

Cro 分为多个模块,可以独立安装。这意味着可以将服务容器保持较小,例如,仅包括它们使用的 Cro 部件即可。它还允许依赖于 Cro 的任何模块仅依赖于它们所需的部件。

5.1. Cro::Core

Cro::Core 软件包包含关键的 Cro 基础结构:

  • Cro 的关键角色(Cro::Source,Cro::Transform 等)

  • Cro 作曲家及其默认的连接管理器

  • Cro::Uri 和 Cro::MediaType 值类型

  • Cro::TCP 模块,提供 TCP 支持

所有其他 Cro 模块最终都依赖于此。

5.2. Cro::TLS

Cro::TLS 软件包包含 Cro::TLS 模块,该模块提供 TLS 支持。

5.3. Cro::HTTP

该模块包括:

  • Cro::HTTP::Client(用于发出 HTTP 请求)

  • Cro::HTTP::Server 和 Cro::HTTP::Router(用于构建 HTTP 服务)

  • 用于 multipart / form-data,application / x-www-form-urlencoded 和 JSON 的 HTTP 消息主体解析器和序列化器

  • HTTP / 1.1 和 HTTP / 2 请求/响应解析器和序列化器

  • HTTP 版本选择和连接管理基础结构

这取决于 Cro::Core 和 Cro::TLS。

5.4. Cro::WebSocket

该模块包括:

  • Cro::WebSocket::Client

  • Cro::HTTP::Router::WebSocket(Cro::HTTP::路由器 Web 套接字插件)

  • Web 套接字协议解析器和序列化器

这取决于 Cro::HTTP。

5.5. Cro::ZeroMQ

该模块为 Cro 中的 ZeroMQ 管道提供支持。

5.5.1. cro

Cro 开发工具。包括:

  • cro 命令行工具

  • 用于 Cro 开发的 cro Web Web 界面

它取决于 Cro::WebSocket,因此取决于 Cro::HTTP,Cro::TLS 和 Cro::Core。因此,它始终提供对存根 HTTP 服务的支持。如果安装了 Cro::ZeroMQ,则它将提供对存根 ZeroMQ 服务的选项。

6. Cro::Uri

Cro::Uri 类支持使用 RFC 3986 中指定的统一资源标识符。Cro::Uri 实例是不可变的。

6.1. 解析方法

有很多方法可以将字符串解析为 Cro::Uri 实例。 它们都可以在 Cro::Uri 类型的对象上调用。

6.1.1. 解析

接受字符串并尝试将其解析为绝对 URI。

Cro::Uri.parse("http://example.com");

默认情况下,此方法使用内部解析器和操作类,该类根据 RFC 3986 进行操作,但是可以将不同的解析器指定为命名参数:

Cro::Uri.parse("http://example.com",
               grammar => $custom-grammar,
               actions => $custom-actions);

如果解析失败,将抛出 X::Cro::Uri::ParseError 异常。此异常的 uri-string 字段包含错误的字符串。

6.1.2. parse-relative

接受字符串,并尝试将其解析为相对 URI。在自定义和错误处理方面,诸如解析之类的功能。

6.1.3. parse-ref

接受字符串并将其解析为 URI 引用(即绝对 URI 或相对 URI)。在自定义和错误处理方面,诸如解析之类的功能。

6.2. 获取 URI 部分

6.2.1. schema

返回 URI 的模式。例如,给定 http://example.com/,它将返回 http。对于相对 URI,这将返回一个类型对象。

6.2.2. authority

获取 URI 的授权部分(如果有的话)。例如,给定 http://foo@bar.com:42/baz,它将返回 foo@bar.com:42。如果不存在,则返回一个类型对象。

6.2.3. userinfo

获取授权的 userinfo 部分(如果有的话)。例如,给定 http://foo@bar.com:42/baz,它将返回 foo。如果不存在,则返回一个类型对象。

6.2.4. user

获取 userinfo 的用户部分(如果存在)。解码任何百分比转义序列。

6.2.5. password

获取 userinfo 的密码部分(如果存在)。 (请注意,URI 规范不赞成使用此方法。此处提供此功能是为了方便那些需要使用此类 URI 的人员,并且在可预见的将来将保留在 Cro 中。)解码任何百分比转义序列。

6.2.6. host

返回授权的主机部分(如果有的话)。例如,给定 http://foo@bar.com:42/baz,它将返回 bar。如果不存在,则返回一个类型对象。主机名中的任何百分比转义序列都将被解码。

6.2.7. host-class

如果有一个主机,则返回其类。作为 Cro::Uri::Host 枚举的成员返回,该枚举定义为

enum Cro::Uri::Host <RegName IPv4 IPv6 IPvFuture>;

如果不存在则返回类型对象。

6.2.8. port

返回授权机构的端口部分(如果存在)。例如,给定 http://foo@bar.com:42/baz,它将返回 42。

6.2.9. path

返回 URI 的路径部分。路径部分为空的 URI 将返回空字符串。此方法不执行任何百分比解码。例如,给定 http://foo@bar.com:42/baz/oh%20wow,它将返回 /baz/oh%20wow

6.2.10. path-segments

返回 URI 解码路径段的列表。 ASCII 范围之外的序列将被解码为 UTF-8。给定 http://foo@bar.com:42/baz/oh%20wow,此方法将返回列表(“b​​az”,“oh wow”)。

6.2.11. query

返回 URI 的查询字符串部分。例如,给定 http://bar.com:42/baz?x=1&y=2,它将返回 x = 1&y = 2。没有执行百分比序列解码。 (要解析在 HTTP 应用程序中使用的查询字符串,请使用 Cro::Uri::HTTP,它添加了此功能)。

6.2.12. fragment

返回 URI 的片段部分。例如,给定 http://bar.com/baz#abc,它将返回 abc。

6.3. URI 字符串化

Str 方法将 URI 转换回 Str。

6.3.1. 解决相对 URI

add 方法以调用它的 Cro::Uri 实例为基础,实现相对 URI 的解析。可以使用将被解析为 URI 引用的字符串或另一个 Cro::Uri 对象来调用它。返回表示解析结果的新 Cro::Uri 实例。任何 。和..序列将作为分辨率的一部分进行处理。

my $base = Cro::Uri.parse('http://foo.com/bar/baz/wat.html');
say ~$base.add('../eek.html');  # http://foo.com/bar/eek.html

6.3.2. 百分号解码

解码百分比子例程接受一个字符串,并返回一个新字符串,其中所有百分比转义序列都将转换为 Unicode 字符(假定为 UTF-8 解码)。

此子例程默认情况下不导出,但可以使用解码百分比标签获得:

use Cro::Uri :decode-percents;

6.3.3. 百分号编码

encode-percents 子例程接受一个字符串,并返回一个新字符串,在该字符串中所有不被视为保留的字符都进行了百分比编码。 非 ASCII 字符将使用 UTF-8 进行编码,并且每个字节进行百分比编码。

此子例程默认情况下不导出,但可以使用 encode-percents 标记获得:

use Cro::Uri :encode-percents;

7. Cro::WebApp::Template

模板通常用于将某些数据呈现为 HTML。Cro 模板引擎在设计时就考虑到了 HTML,并注意应转义数据,因为它应该以 HTML 转义。模板一次编译成 Perl 6 代码,然后可以通过将其传递给不同的输入多次使用。输入数据可以是任何 Perl 6 对象,包括哈希或数组。

7.1. 使用模板

要使用模板,请添加 use Cro::WebApp::Template;。在文件顶部,其中包含要使用它们的路由。

要将模板作为路线的结果进行渲染,请使用模板:

route -> 'product', Int $id {
    my $product = $repository.lookup-product($id);
    template 'templates/product.crotmp', $product;
}

这等价于

route -> 'product', Int $id {
    my $product = $repository.lookup-product($id);
    content 'text/html', render-template 'templates/product.crotmp', $product;
}

其中 render-template 渲染模板并返回这样做的结果,其内容来自 Cro::HTTP::Router 并设置响应的内容类型以及正文。请注意,默认情况下,模板设置的内容类型为 text/html。要使其不这样做,请传递 content-type:

route -> 'product', Int $id {
    my $product = $repository.lookup-product($id);
    template 'templates/product.crotmp', $product,
        content-type => 'text/plain';
}

$ product 将成为要渲染的模板的主题(有关模板语言的更多信息,请参见下文)

7.2. 模板位置和编译

默认情况下,将在当前工作目录中查找模板,并且模板中的 <:use'…​'> 指令也是如此。模板在首次使用时也会被延迟编译。

调用 template-location 函数以指定可以放置模板的目录。这些调用位于搜索路径的前面,因此对模板位置的最新调用优先。正在做:

template-location 'templates/';

意味着可以在无需使用该路径限定的情况下找到 template/ 目录下的模板。(可选)传递:compile-all 将立即编译所有模板,并在出现任何错误时死亡。可以将其放入测试用例中:

use Cro::WebApp::Template;
use Test;

lives-ok { template-location 'templates/', :compile-all },
    'All templates have valid syntax';

done-testing;

7.3. 模板语言

模板以内容模式启动,这意味着模板文件由纯 HTML 组成:

<h1>Oh, hello there</h1>
<p>I've been expecting you...</p>

将导致产生 HTML。

对模板引擎重要的语法由类似 HTML 的标记组成,该标记以非字母字符开头。有些单独使用,例如 <.foo><$foo>,而另一些使用关闭器,例如 <@foo> …​ </@> 关闭器不需要一个就再次写出完整的打开器,只需 匹配"印记"。如果需要,可以在更近的地方重复打开器的打开字母字符(因此 <@foo> 可以用 </@foo> 关闭)。

与 Perl 6 一样,存在当前主题的概念,例如 Perl 6 $_。

7.4. 解包散列和对象属性

<.name> 形式可用于访问当前主题的对象属性。如果当前主题扮演关联角色,则此表单将更喜欢采用名称哈希键下的值,如果没有这样的键,则退回到寻找方法名称。

例如,给定一个模板:

<p>Hello, <.name>. The weather today is <.weather>.</p>

使用散列渲染:

{
    name => 'Dave',
    weather => 'rain'
}

结果会是 :

<p>Hello, Dave. The weather today is rain.</p>

哈希回退是为了减轻从最初使用哈希开始,然后再重构为模型对象的过渡。

还有其他各种形式:

<.elems()> 将始终是方法调用,即使在关联上使用(也可以用来克服键回退)

<.<elems>> 将始终是哈希索引 <.[0]> 索引数组元素 0,假设主题是可索引的 <.{$key}> 可用于进行间接哈希索引 <.[$idx]> 可用于进行间接数组索引

这些都可以链接在一起,从而允许 <.foo.bar.baz> 之类的东西用于挖掘对象/哈希。使用索引器形式时,仅前导。是必需的,因此 <.<foo>.<bar>> 可能只是 <.<foo> <bar>>

索引或方法调用的结果将被分层,然后进行 HTML 编码以插入到文档中。

7.5. 变量

<$…​> 语法可用于引用变量。它将被字符串化,HTML 编码并插入到文档中。引用不存在的变量是模板编译时错误。当前主题可以通过 <$_> 访问。

允许变量使用 <.foo> 标记中允许的任何语法,例如 <$product.name><$product <name>>。例如,假设定义了变量 $person$weather,则:

<p>Hello, <$person.name>. The weather is <$weather.description>, wich a low of
  <$weather.low>C and a high of <$weather.high>C.</p>

会渲染成这样的东西:

<p>Hello, Darya. The weather is sunny, wich a low of
  14C and a high of 25C.</p>

7.6. 迭代

@ 标记 sigil 用于迭代。它可以与任何 Iterable 数据源一起使用,并且必须具有结束标记 </@>。将为迭代中的每个值评估两者之间的区域,默认情况下,当前目标将设置为当前值。

例如,给定模板:

<select name="country">
  <@countries>
    <option value="<.alpha2>"><.name></option>
  </@>
</select>

和数据:

{
    countries => [
        { name => 'Argentina', alpha2 => 'AR' },
        { name => 'Bhutan', alpha2 => 'BT' },
        { name => 'Czech Republic', alpha2 => 'CZ' },
    ]
}

结果会是

<select name="country">
    <option value="AR">Argentina</option>
    <option value="BT">Bhutan</option>
    <option value="CZ">Czech Republic</option>
</select>

<@foo> 形式是 <@.foo> 的缩写,遵循与 <.foo> 相同的规则进行解析。也可以编写 <@ $foo> 来遍历变量。

要指定一个变量来声明并使用当前迭代值填充,请在迭代目标之后放置一个 : 并命名该变量。例如,较早的模板可以写为:

<select name="country">
  <@countries: $c>
    <option value="<$c.alpha2>"><$c.name></option>
  </@>
</select>

保留当前的默认目标。如果当前目标本身是可迭代的,则可以简单地编写 <@> …​ </@>

如果开始和结束迭代标签是该行中唯一的东西,那么将不会为那些行生成任何输出,从而使输出更令人愉快。

7.7. 条件

<?$foo> …​ </?>("if")和 <!$foo> …​ </!>("除非")可用于条件执行。它们对指定的变量执行布尔测试。还可以将它们与主题引用语法一起使用,例如 <?.is-admin> …​ </?>。对于更复杂的条件,使用语法 <?{$a eq $b}> …​ </?> 接受 Perl 6 表达式的子集。与 Perl 6 唯一的不同之处在于 <?{.answer == 42}> …​ </?> 将具有与 <.answer> 中相同的哈希/对象语义,以与其余模板保持一致语言。

允许以下构造:

变量 ($foo

<.foo> 标记语法支持的范围内使用主题,方法调用和索引

分组括号

比较运算 ==,!=,<,⇐,> =,>,eq,ne,lt,gt,=== 和 !===

&&,|| 和或短路逻辑运算符

+,-,*,/ 和 % 数学运算

~ 和 x 字符串操作

数值文字(整数,浮点数和有理数)

字符串文字(单引号,不带插值)

鼓励那些希望更多的人考虑在模板之外编写其逻辑。

如果打开和关闭条件标签是该行上唯一的东西,那么这些行将不会生成任何输出,从而使输出更令人愉悦。

7.8. 子例程和宏

可以声明可以重用的模板子例程,以便排除公共元素。

一个简单的模板子例程声明如下所示:

<:sub header>
  <header>
    <nav>
      blah blabh
    </nav>
  </header>
</:>

然后可以这样调用它:

<&header>

可以声明一个带有参数的模板子:

<:sub select($options, $name)>
  <select name="<$name>">
    <@options>
      <option value="<$value>"><$text></option>
    </@>
  </select>
</:>

然后用参数调用它:

<&select(.countries, 'country')>

参数可以是在一定条件下有效的表达式-即,文字,变量访问,取消引用和允许的一些基本运算符。

模板宏的工作原理与模板子例程类似,不同之处在于它的用法具有主体。该主体作为 thunk 传递,这意味着宏可以选择将其渲染 0 次或多次),可以选择设置新的默认目标。例如,将一些内容包装在 Bootstrap 卡中的宏可能看起来像:

<:macro bs-card($title)>
  <div class="card" style="width: 18rem;">
    <div class="card-body">
      <h5 class="card-title"><$title></h5>
      <:body>
    </div>
  </div>
</:>

其中 <:body> 标记要渲染的实体的点。该宏可以用作:

<|bs-card("My Stuff")>
  It's my stuff, in a BS card!
</|>

要在宏中为主体设置当前目标,请使用 <:body $target>

7.9. 分解出 sub 和宏

模板子和宏可以分解为其他模板文件,然后使用 <:use …​> 导入:

<:use 'common.crotmp'>

7.10. 插入 HTML 和 JavaScript

默认情况下,所有内容都是 HTML 转义的。但是,有时需要将一小段预渲染的 HTML 放入模板输出中。有两种方法可以实现此目的。

HTML 内置函数称为 <&HTML(.stuff)>,首先检查是否没有以 javascript 开头的脚本标签或属性: 如果有,它将视为 XSS 攻击尝试并抛出异常。

HTML-AND-JAVASCRIPT 内置函数不会尝试任何 XSS 保护,而只是插入给出的任何内容而不会进行任何转义。

请注意,HTML 函数不能保证完全安全的 XSS 保护。请非常小心地使用这两个功能。

8. Cro 方法

Cro 的核心是建立 Perl 6 供应链,这些供应链处理从网络到达的消息并产生要通过网络发送的消息。

8.1. 关键角色

消息由 Cro::Message 表示。具体的实现包括:

Cro::TCP::Message Cro::HTTP::Request Cro::HTTP::Response 传入连接由 Cro::Connection 表示;实现包括:

Cro::TCP::ServerConnection Cro::TLS::ServerConnection Cro::Source 是消息或连接的源。例如,Cro::TCP::Listener 生成 Cro::TCP::ServerConnection 对象。

Cro::Transform 将一个连接或消息转换为另一个。例如,Cro::HTTP::RequestParser 会将 Cro::TCP::Messages 转换为 Cro::HTTP::Requests,而 Cro::HTTP::ResponseSerializer 将 Cro::HTTP::Responses 转换为 Cro::TCP::消息。这意味着在 Cro 中,HTTP 应用程序只是从 Cro::HTTP::Request 转换为 Cro::HTTP::Response。

Cro::Sink 消耗消息。它什么也不会产生。接收器位于消息处理管道的末尾。TCP 服务器中的接收器将使用 Cro::TCP::Messages 并通过网络发送它们。

某些消息或连接可以通过一个或多个消息来回复。这些扮演 Cro::Replyable 角色。任何产生可答复的内容还负责提供可以处理答复消息的内容。这种"东西"可以是变换,也可以是宿。可应答对象的示例包括 Cro::TCP::ServerConnection 和 Cro::TLS::ServerConnection,它们提供了 Cro::Sink 应答器,可将 Cro::TCP::Message 对象发送回客户端。

8.2. 组成

Cro 组件(源,转换和接收器)可以放在一起形成管道。此过程称为管道组成。也许最简单的示例是设置回显服务器:

class Echo does Cro::Transform {
    method consumes() { Cro::TCP::Message }
    method produces() { Cro::TCP::Message }

    method reply(Supply $source) {
        # We could actually just `return $source` here, but the identity
        # supply is written out here to illustrate what a transform will
        # often look like.
        supply {
            whenever $source -> $message {
                emit $message;
            }
        }
    }
}

然后可以将其组成服务并按以下方式启动:

my Cro::Service $echo-server = Cro.compose(
    Cro::TCP::Listener.new(port => 8000),
    Echo
);
$echo-server.start();

请注意,如果 Cro.compose(…​) 具有以源开头和接收器结尾的内容,则仅返回 Cro::Service。那么在这种情况下,水槽从哪里来? 由于连接是可答复的,因此它提供了接收器。还值得注意的是,连接流神奇地变成了消息流。如果编写者发现生成连接的对象后面是消耗消息的对象,它将把其余的管道传递给 Cro::ConnectionManager 实例,因此其余管道的处理将针对每个连接。

8.3. HTTP 服务器示例

大多数 Cro HTTP 服务将使用诸如 Cro::HTTP::Router 和 Cro::HTTP::Server 之类的高级模块进行组装。但是,可以在没有这些便利的情况下使用 Cro.compose(…​) 将 HTTP 处理管道组合在一起。首先,需要各种组件和消息类型:

use Cro;
use Cro::HTTP::Request;
use Cro::HTTP::RequestParser;
use Cro::HTTP::Response;
use Cro::HTTP::ResponseSerializer;
use Cro::TCP;

HTTP 应用程序本身-一个简单的 "Hello,world" -是一个 Cro::Transform,可将请求转换为响应:

class HTTPHello does Cro::Transform {
    method consumes() { Cro::HTTP::Request }
    method produces() { Cro::HTTP::Response }

    method transformer($request-stream) {
        supply {
            whenever $request-stream -> $request {
                given Cro::HTTP::Response.new(:200status) {
                    .append-header('Content-type', 'text/html');
                    .set-body("<strong>Hello from Cro!</strong>");
                    .emit;
                }
            }
        }
    }
}

这些组成服务:

my Cro::Service $http-service = Cro.compose(
    Cro::TCP::Listener.new( :host('localhost'), :port(8181) ),
    Cro::HTTP::RequestParser.new,
    HTTPHello,
    Cro::HTTP::ResponseSerializer.new
);

然后可以使用这样的:

$http-service.start;
signal(SIGINT).tap: {
    note "Shutting down...";
    $http-service.stop;
    exit;
}
sleep;

8.4. 客户端管道

客户端(例如 HTTP 客户端)也表示为管道。与服务器管道不同,在服务器管道中,应用程序位于管道的中心,在网络的两端。客户端管道在网络的中心,而应用程序在网络的两端。

客户端管道中心的组件将是 Cro::Connector,它建立连接。Cro::Connector 能够建立连接并产生 Cro::Transform,它将发送使用该连接消耗的消息并发出从网络连接接收到的消息。具有连接器的管道不得具有 Cro::Source 或 Cro::Sink。

连接,发送消息并在收到某种东西后立即断开连接的最小 TCP 客户端可以表示为:

my Cro::Connector $conn = Cro.compose(Cro::TCP::Connector);
my Supply $responses = $conn.establish(
    host => 'localhost',
    port => 4242,
    supply {
        emit Cro::TCP::Message.new( :data('hello'.encode('ascii')) )
    }
);
react {
    whenever $responses {
        say .data;
        done;
    }
}

客户端上的建立方法建立连接。它使用带有 Supply 的单个位置参数,将使用它来接收要发送的消息; 所有命名的参数都将传递给 connect 方法,该方法建立连接并返回转换。建立方法将返回一个耗材,将在其上发出响应消息。

更复杂的管道是可能的。例如,一个(不完全方便,但功能齐全的)HTTP 客户端看起来像:

my Cro::Connector $conn = Cro.compose(
    Cro::HTTP::RequestSerializer,
    Cro::TLS::Connector,
    Cro::HTTP::ResponseParser
);

my $req = supply {
    my Cro::HTTP::Request $req .= new(:method<GET>, :target</>);
    $req.add-header('Host', 'www.perl6.org');
    emit $req;
}
react {
    whenever $conn.establish($req, :host<www.perl6.org>, :port(80)) {
        say ~$response; # Dump headers
    }
}