現(xiàn)代Web應(yīng)用的一個普遍需求是多子域名,每個用戶可以訪問到用戶特定的子域名撇寞,比如說Slack就為每個聊天室創(chuàng)建了一個單獨的子域名忌堂。這篇文章講述了如何在Phoenix應(yīng)用中設(shè)置多個子域名。
我們知道椒功,Phoenix可以創(chuàng)建Umbrella應(yīng)用白嘁,其下放置多個App坑鱼,每個App分配不同的端口,如4000,4001,4002等絮缅,前端再用Nginx做反向代理鲁沥,這樣也可以實現(xiàn)子域名的功能呼股,但是無法實現(xiàn)類似Slack的那種功能,用戶無法方便地設(shè)置自己的子域名画恰。
創(chuàng)建項目
新建Phoenix項目卖怜,名為subdomainer
mix phoenix.new subdomainer
啟動應(yīng)用
mix phoenix.server
修改hosts,增加如下這條阐枣,我們可以在本地通過這些域名訪問本機127.0.0.1马靠,部署至服務(wù)器上可以使用泛域名解析。
127.0.0.1 subdomainer.dev foo.subdomainer.dev bar.subdomainer.dev
我們可以通過以下三個地址訪問應(yīng)用:
目前這些地址都指向了同一個頁面蔼两,我們將修改代碼來使不同子域名訪問的頁面各不相同甩鳄。
判斷子域名是否設(shè)置
我們首先需要配置應(yīng)用的根域名,因為你沒法保證子域名的數(shù)量额划,在這個例子中妙啃,根域名是subdomainer.dev
,子域名是foo.subdomainer.dev
俊戳。當(dāng)然揖赴,我們也可以使用app.subdomainer.dev
作為根域名,foo.app.subdomainer.dev
作為子域名抑胎。也就是將我們的多子域名應(yīng)用放在一個二級域名之下燥滑。以區(qū)別我們的主應(yīng)用,如www.subdomainer.dev
阿逃。而www和app兩個應(yīng)用可以放在一個umbrella下铭拧。
修改config/config.exs
中的config :subdomain, Subdomain.Endpoint
代碼塊:
url: [host: "localhost"],
修改為:
url: [host: "subdomainer.dev"],
我們還需要修改endpoint來獲知URL里是否是子域名。
創(chuàng)建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
這里我們實現(xiàn)了plug必須的call/2
函數(shù)恃锉。這里第二個參數(shù)是如果子域名存在搀菩,我們將使用的module。
String.replace(host, ~r/.?#{root_host}/, "")
返回了子域名名稱:
"foo.subdomainer.com" -> "foo"
"foo.app.subdomainer.com" -> "foo.app"
如果subdomain長度大于0破托,即URL里包含subdomain肪跋,那么進(jìn)入router。
在lib/subdomainer/endpoint.ex
的plug :router, Subdomainer.Router
之前增加
plug Subdomainer.Plug.Subdomain, Subdomainer.SubdomainRouter
這里我們指定SubdomainRouter模塊作為子域名的Router土砂。
現(xiàn)在我們運行應(yīng)用會有如下錯誤提示:
undefined function: Subdomainer.SubdomainRouter.init/1 (module Subdomainer.SubdomainRouter is not available)
因為我們還未創(chuàng)建這個router州既。
添加子域名路由
創(chuàng)建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
現(xiàn)在就可以運行了瘟芝。我們還想讓subdomain訪問不同的頁面易桃。
scope "/", Subdomainer.Subdomain do
pipe_through :browser # Use the default browser stack
get "/", PageController, :index
end
這里改變了scope后面的Subdomainer.Subdomain
褥琐,所以PageController就會在subdomain文件夾下尋找對應(yīng)文件锌俱。
創(chuàng)建web/controllers/subdomain/page_controller.ex
,為子域名創(chuàng)建特定的Controller
defmodule Subdomainer.Subdomain.PageController do
use Subdomainer.Web, :controller
def index(conn, _params) do
text(conn, "Subdomain home page")
end
end
現(xiàn)在也可以運行敌呈,訪問子域名贸宏,會看到如下錯誤提示:
(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
自定義子域名響應(yīng)
最后要做的是根據(jù)子域名響應(yīng)對應(yīng)的內(nèi)容吭练。我們可以把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
全部完成诫龙!現(xiàn)在我們再次訪問 http://subdomainer.dev:4000, http://foo.subdomainer.dev:4000 http://bar.subdomainer.dev:4000
來看看效果吧!
最終效果
這僅僅是一個開始鲫咽,你可以根據(jù)此來擴展引用签赃。一個常見需求是從數(shù)據(jù)庫中搜索subdomain是否存在,如果不存在分尸,則返回404锦聊。Subdomainer.SubdomainRouter
在請求中獲取了subdomain,你可以添加一個plug到pipeline中箩绍,來在controller動作之前檢查subdomain是否存在孔庭。
參考
[1] http://blog.gazler.com/blog/2015/07/18/subdomains-with-phoenix/