Phoenix 多子域名应用

现代Web应用的一个普遍需求是多子域名,每个用户可以访问到用户特定的子域名,比如说Slack就为每个聊天室创建了一个单独的子域名。这篇文章讲述了如何在Phoenix应用中设置多个子域名。

我们知道,Phoenix可以创建Umbrella应用,其下放置多个App,每个App分配不同的端口,如4000,4001,4002等,前端再用Nginx做反向代理,这样也可以实现子域名的功能,但是无法实现类似Slack的那种功能,用户无法方便地设置自己的子域名。

创建项目

新建Phoenix项目,名为subdomainer

mix phoenix.new subdomainer

启动应用

mix phoenix.server

修改hosts,增加如下这条,我们可以在本地通过这些域名访问本机127.0.0.1,部署至服务器上可以使用泛域名解析。

127.0.0.1       subdomainer.dev foo.subdomainer.dev bar.subdomainer.dev

我们可以通过以下三个地址访问应用:

  1. http://subdomainer.dev:4000
  2. http://foo.subdomainer.dev:4000
  3. http://bar.subdomainer.dev:4000
Phoenix 多子域名应用_第1张图片
http://subdomainer.dev:4000/

目前这些地址都指向了同一个页面,我们将修改代码来使不同子域名访问的页面各不相同。

判断子域名是否设置

我们首先需要配置应用的根域名,因为你没法保证子域名的数量,在这个例子中,根域名是subdomainer.dev,子域名是foo.subdomainer.dev。当然,我们也可以使用app.subdomainer.dev作为根域名,foo.app.subdomainer.dev作为子域名。也就是将我们的多子域名应用放在一个二级域名之下。以区别我们的主应用,如www.subdomainer.dev。而www和app两个应用可以放在一个umbrella下。

修改config/config.exs中的config :subdomain, Subdomain.Endpoint代码块:

url: [host: "localhost"],

修改为:

url: [host: "subdomainer.dev"],

我们还需要修改endpoint来获知URL里是否是子域名。
创建lib/subdomainer/plugs/subdomain.ex

defmodule Subdomainer.Plug.Subdomain do
  import Plug.Conn

  @doc false
  def init(default), do: default

  @doc false
  def call(conn, router) do
    case get_subdomain(conn.host) do
      subdomain when byte_size(subdomain) > 0 ->
        conn
        |> router.call(router.init({}))
      _ -> conn
    end
  end

  defp get_subdomain(host) do
    root_host = Subdomainer.Endpoint.config(:url)[:host]
    String.replace(host, ~r/.?#{root_host}/, "")
  end
end

这里我们实现了plug必须的call/2函数。这里第二个参数是如果子域名存在,我们将使用的module。

String.replace(host, ~r/.?#{root_host}/, "")返回了子域名名称:

"foo.subdomainer.com" -> "foo"
"foo.app.subdomainer.com" -> "foo.app"

如果subdomain长度大于0,即URL里包含subdomain,那么进入router。

lib/subdomainer/endpoint.explug :router, Subdomainer.Router之前增加

plug Subdomainer.Plug.Subdomain, Subdomainer.SubdomainRouter

这里我们指定SubdomainRouter模块作为子域名的Router。
现在我们运行应用会有如下错误提示:

undefined function: Subdomainer.SubdomainRouter.init/1 (module Subdomainer.SubdomainRouter is not available)

因为我们还未创建这个router。

添加子域名路由

创建web/subdomain_router.ex用于存放子域名的router。

defmodule Subdomainer.SubdomainRouter do
  use Subdomainer.Web, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
  end

  scope "/", Subdomainer do
    pipe_through :browser # Use the default browser stack

    get "/", PageController, :index
  end

  # Other scopes may use custom stacks.
  # scope "/api", Subdomainer do
  #   pipe_through :api
  # end
end

现在就可以运行了。我们还想让subdomain访问不同的页面。

  scope "/", Subdomainer.Subdomain do
    pipe_through :browser # Use the default browser stack

    get "/", PageController, :index
  end

这里改变了scope后面的Subdomainer.Subdomain,所以PageController就会在subdomain文件夹下寻找对应文件。

创建web/controllers/subdomain/page_controller.ex,为子域名创建特定的Controller

defmodule Subdomainer.Subdomain.PageController do
  use Subdomainer.Web, :controller

  def index(conn, _params) do
    text(conn, "Subdomain home page")
  end

end

现在也可以运行,访问子域名,会看到如下错误提示:

(exit) an exception was raised: ** (Plug.Conn.AlreadySentError) the response was already sent

这也容易解决,我们只需要避免找到子域名后的plugs的运行即可。

lib/subdomainer/plugs/subdomain.ex添加Plug.Conn.halt/1

  def call(conn, router) do
    case get_subdomain(conn.host) do
      subdomain when byte_size(subdomain) > 0 ->
        conn
        |> router.call(router.init({}))
        |> halt
      _ -> conn
    end
  end

自定义子域名响应

最后要做的是根据子域名响应对应的内容。我们可以把subdomain信息添加到Plug.Conn的private storage中

  def call(conn, opts) do
    case get_subdomain(conn.host) do
      subdomain when byte_size(subdomain) > 0 ->
        conn
        |> put_private(:subdomain, subdomain)
        |> router.call(router.init({}))
        |> halt
      _ -> conn
    end
  end

然后在Subdomainer.Subdomain.PageController中获取信息:

  def index(conn, _params) do
    text(conn, "Subdomain home page for #{conn.private[:subdomain]}")
  end

全部完成!现在我们再次访问 http://subdomainer.dev:4000, http://foo.subdomainer.dev:4000 http://bar.subdomainer.dev:4000
来看看效果吧!

最终效果

http://foo.subdomainer.dev:4000/
http://bar.subdomainer.dev:4000/

这仅仅是一个开始,你可以根据此来扩展引用。一个常见需求是从数据库中搜索subdomain是否存在,如果不存在,则返回404。Subdomainer.SubdomainRouter在请求中获取了subdomain,你可以添加一个plug到pipeline中,来在controller动作之前检查subdomain是否存在。

参考

[1] http://blog.gazler.com/blog/2015/07/18/subdomains-with-phoenix/

你可能感兴趣的:(Phoenix 多子域名应用)