插头是:
换句话说,Plug 允许您从小块构建 Web 应用程序并在不同的 Web 服务器上运行它们。 Phoenix 等 Web 框架使用 Plug 来管理请求、响应和 Websocket。本文档将展示一些高级示例并介绍该插件的主要构建块。
为了使用 Plug,您需要一个 Web 服务器及其与 Plug 的绑定。目前有两种选择:
通过将plug_cowboy
包添加到mix.exs
来使用 Cowboy Web 服务器(基于 Erlang):
def deps do
[
{ :plug_cowboy , "~> 2.0" }
]
end
通过将bandit
包添加到mix.exs
来使用 Bandit 网络服务器(基于 Elixir):
def deps do
[
{ :bandit , "~> 1.0" }
]
end
这是一个最小的 hello world 示例,使用 Cowboy Web 服务器:
Mix . install ( [ :plug , :plug_cowboy ] )
defmodule MyPlug do
import Plug.Conn
def init ( options ) do
# initialize options
options
end
def call ( conn , _opts ) do
conn
|> put_resp_content_type ( "text/plain" )
|> send_resp ( 200 , "Hello world" )
end
end
require Logger
webserver = { Plug.Cowboy , plug: MyPlug , scheme: :http , options: [ port: 4000 ] }
{ :ok , _ } = Supervisor . start_link ( [ webserver ] , strategy: :one_for_one )
Logger . info ( "Plug now running on localhost:4000" )
Process . sleep ( :infinity )
将该片段保存到文件中并将其作为elixir hello_world.exs
执行。访问 http://localhost:4000/ 你应该会受到欢迎!
在上面的示例中,我们编写了第一个模块plug ,称为MyPlug
。模块插件必须定义init/1
函数和call/2
函数。 call/2
通过连接和init/1
返回的选项来调用。
Plug v1.14 包含连接upgrade
API,这意味着它提供开箱即用的 WebSocket 支持。让我们看一个示例,这次使用 Bandit Web 服务器和 WebSocket 位的websocket_adapter
项目。由于我们需要不同的路由,因此我们将使用内置的Plug.Router
:
Mix . install ( [ :bandit , :websock_adapter ] )
defmodule EchoServer do
def init ( options ) do
{ :ok , options }
end
def handle_in ( { "ping" , [ opcode: :text ] } , state ) do
{ :reply , :ok , { :text , "pong" } , state }
end
def terminate ( :timeout , state ) do
{ :ok , state }
end
end
defmodule Router do
use Plug.Router
plug Plug.Logger
plug :match
plug :dispatch
get "/" do
send_resp ( conn , 200 , """
Use the JavaScript console to interact using websockets
sock = new WebSocket("ws://localhost:4000/websocket")
sock.addEventListener("message", console.log)
sock.addEventListener("open", () => sock.send("ping"))
""" )
end
get "/websocket" do
conn
|> WebSockAdapter . upgrade ( EchoServer , [ ] , timeout: 60_000 )
|> halt ( )
end
match _ do
send_resp ( conn , 404 , "not found" )
end
end
require Logger
webserver = { Bandit , plug: Router , scheme: :http , port: 4000 }
{ :ok , _ } = Supervisor . start_link ( [ webserver ] , strategy: :one_for_one )
Logger . info ( "Plug now running on localhost:4000" )
Process . sleep ( :infinity )
将该代码片段保存到文件中并将其作为elixir websockets.exs
执行。访问 http://localhost:4000/,您应该会在浏览器控制台中看到消息。
这次,我们使用了Plug.Router
,它允许我们定义 Web 应用程序使用的路由以及要在每个请求上执行的一系列步骤/插件,例如plug Plug.Logger
。
此外,正如您所看到的,Plug 抽象了不同的网络服务器。启动应用程序时,区别在于选择Plug.Cowboy
或Bandit
。
目前,我们直接在一次性管理程序中启动服务器,但对于生产部署,您希望在应用程序管理树中启动它们。请参阅接下来的受监督处理程序部分。
在生产系统上,您可能希望在应用程序的监督树下启动 Plug 管道。使用--sup
标志启动一个新的 Elixir 项目:
$ mix new my_app --sup
添加:plug_cowboy
(或:bandit
)作为mix.exs
的依赖项:
def deps do
[
{ :plug_cowboy , "~> 2.0" }
]
end
现在更新lib/my_app/application.ex
如下:
defmodule MyApp.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@ moduledoc false
use Application
def start ( _type , _args ) do
# List all child processes to be supervised
children = [
{ Plug.Cowboy , scheme: :http , plug: MyPlug , options: [ port: 4001 ] }
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [ strategy: :one_for_one , name: MyApp.Supervisor ]
Supervisor . start_link ( children , opts )
end
end
最后使用MyPlug
模块创建lib/my_app/my_plug.ex
。
现在运行mix run --no-halt
,它将通过在 http://localhost:4001 上运行的 Web 服务器启动您的应用程序。
Plug.Conn
结构在 hello world 示例中,我们定义了第一个名为MyPlug
的插件。插头有两种类型,模块插头和功能插头。
模块插件实现init/1
函数来初始化选项和call/2
函数,该函数接收连接和初始化选项并返回连接:
defmodule MyPlug do
def init ( [ ] ) , do: false
def call ( conn , _opts ) , do: conn
end
函数插头接受连接、一组选项作为参数,并返回连接:
def hello_world_plug ( conn , _opts ) do
conn
|> put_resp_content_type ( "text/plain" )
|> send_resp ( 200 , "Hello world" )
end
连接由%Plug.Conn{}
结构表示:
% Plug.Conn {
host: "www.example.com" ,
path_info: [ "bar" , "baz" ] ,
...
}
数据可以直接从连接读取,也可以进行模式匹配。操作连接通常是通过使用Plug.Conn
模块中定义的函数来进行的。在我们的示例中, put_resp_content_type/2
和send_resp/3
均在Plug.Conn
中定义。
请记住,与 Elixir 中的其他所有内容一样,连接是不可变的,因此每次操作都会返回连接的新副本:
conn = put_resp_content_type ( conn , "text/plain" )
conn = send_resp ( conn , 200 , "ok" )
conn
最后,请记住,连接是到底层 Web 服务器的直接接口。当你调用上面的send_resp/3
时,它会立即将给定的状态和正文发送回客户端。这使得流媒体等功能的使用变得轻而易举。
Plug.Router
为了编写一个根据传入请求的路径和方法进行调度的“路由器”插件,Plug 提供了Plug.Router
:
defmodule MyRouter do
use Plug.Router
plug :match
plug :dispatch
get "/hello" do
send_resp ( conn , 200 , "world" )
end
forward "/users" , to: UsersRouter
match _ do
send_resp ( conn , 404 , "oops" )
end
end
路由器是一个插头。不仅如此:它还包含自己的插头管道。上面的例子说,当调用路由器时,它将调用:match
插件,由本地(导入的) match/2
函数表示,然后调用:dispatch
插件来执行匹配的代码。
Plug 附带了许多插件,您可以将它们添加到路由器插件管道中,从而允许您在路由匹配之前或将路由分派到之前插入某些内容。例如,如果您想向路由器添加日志记录,只需执行以下操作:
plug Plug.Logger
plug :match
plug :dispatch
注意Plug.Router
将所有路由编译为单个函数,并依赖 Erlang VM 将底层路由优化为树查找,而不是匹配每条路由的线性查找。这意味着 Plug! 中的路由查找速度非常快!
这也意味着建议像上面的示例一样定义一个 catch all match
块,否则路由会失败并出现函数子句错误(就像在任何常规 Elixir 函数中一样)。
每个路由都需要根据插头规范返回连接。有关更多信息,请参阅Plug.Router
文档。
Plug 附带一个Plug.Test
模块,可以轻松测试您的插头。以下是我们如何从上面测试路由器(或任何其他插头):
defmodule MyPlugTest do
use ExUnit.Case , async: true
use Plug.Test
@ opts MyRouter . init ( [ ] )
test "returns hello world" do
# Create a test connection
conn = conn ( :get , "/hello" )
# Invoke the plug
conn = MyRouter . call ( conn , @ opts )
# Assert the response and status
assert conn . state == :sent
assert conn . status == 200
assert conn . resp_body == "world"
end
end
该项目旨在提供可以跨应用程序重复使用的不同插头:
Plug.BasicAuth
- 提供基本 HTTP 身份验证;Plug.CSRFProtection
- 为您的应用程序添加跨站点请求伪造保护。如果您使用Plug.Session
通常需要;Plug.Head
- 将 HEAD 请求转换为 GET 请求;Plug.Logger
- 记录请求;Plug.MethodOverride
- 使用请求参数中指定的方法覆盖请求方法;Plug.Parsers
- 负责解析给定内容类型的请求主体;Plug.RequestId
- 设置要在日志中使用的请求 ID;Plug.RewriteOn
- 从x-forwarded-*
标头重写请求的主机/端口/协议;Plug.Session
- 处理会话管理和存储;Plug.SSL
- 通过 SSL 强制执行请求;Plug.Static
- 提供静态文件;Plug.Telemetry
- 使用:telemetry
事件来检测插头管道;您可以在我们的文档中详细了解它们。
使用Plug.Router
或Plug.Builder
帮助开发后可以使用的模块:
Plug.Debugger
- 每次请求失败时都会显示一个有用的调试页面;Plug.ErrorHandler
- 允许开发人员在崩溃时自定义错误页面,而不是发送空白页面; 我们欢迎大家为 Plug 做出贡献并帮助我们解决现有问题!
使用问题跟踪器来报告错误或功能请求。当您准备好做出贡献时,请打开拉取请求。提交拉取请求时,您不应更新CHANGELOG.md
。
如果您打算贡献文档,请查看我们编写文档的最佳实践。
最后,请记住我们官方空间中的所有互动均遵循我们的行为准则。
分支 | 支持 |
---|---|
v1.15 | 错误修复 |
v1.14 | 仅安全补丁 |
v1.13 | 仅安全补丁 |
v1.12 | 仅安全补丁 |
v1.11 | 仅安全补丁 |
v1.10 | 仅安全补丁 |
v1.9 | 从 10/2023 起不再支持 |
v1.8 | 从 01/2023 起不再支持 |
v1.7 | 从 01/2022 起不再支持 |
v1.6 | 从 01/2022 起不再支持 |
v1.5 | 从 03/2021 起不再支持 |
v1.4 | 从 12/2018 起不再支持 |
v1.3 | 从 12/2018 起不再支持 |
v1.2 | 从 06/2018 起不再支持 |
v1.1 | 从 01/2018 起不再支持 |
v1.0 | 从 05/2017 起不再支持 |
插件源代码在 Apache License 2.0 下发布。检查许可证文件以获取更多信息。