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
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/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容