如何用標準化的,通用的純函數編程語言Haskell來構建Web?Haskell構建的Web擁有著什么樣的優點呢?讓我們來看看構建的過程吧。
作者%20|%20William%20Yao
譯者%20|彎月,責編%20|%20maozz
出品%20|%20CSDN(ID:CSDNnews)
以下為譯文:
Haskell有大量的庫可用于滿足基本的后臺需求,從日志輸出到數據庫訪問,再到Web服務器的定義和路由,應有盡有。
擁有選擇的自由固然很好,但如果你剛剛接觸這個領域,那么大量的選擇可能會讓你目不暇接。也許你沒法自信地判斷出這些選擇之間的區別。例如,你需要查詢數據庫。
那么,你需要Squeal提供的列名強保證,以及深度SQL嵌入功能嗎?還是青睞Opaleye在保證類型安全的前提下的簡單性?或者只是使用簡單的postgresql-simple就好?還是使用Selda?或者……
我用最簡單的庫寫了一個Web應用程序,一方面是想告訴你,你不需要擔心技術棧是否足夠先進,另一方面也是我自己的學習過程。
如果你不知道怎樣用Haskell構建真正的應用程序,為什么不去學習呢?我會盡可能保持代碼的簡單性。
點擊此處可查看源代碼(https://gitlab.com/williamyaoh/haskell-web-stack)。
下面我們來看一看我選擇的這些庫,以及它們的功能,還有這個應用程序的功能。
1.這個Web應用程序究竟是什么呢?
它是一個網站,用戶可以在這個網站上創建定時器和做筆記。
我舉個例子說明該網站的用途:某人想設置多個定時器來跟蹤多個反應物的過程,同時用筆記來記錄他們需要關心的事情、下次制作配方時需要改進的事情,等等。
再舉一個例子,某個人在玩MOBA游戲,比如英雄聯盟或者刀塔2,他們可以在另一臺顯示器上打開一個頁面來跟蹤關鍵技能的冷卻時間,同時用筆記記錄宏操作、對手的組合,以及團戰時需要關注技的冷卻等。
在這個應用程序中我將演示:
-
會話,用戶可以刷新頁面,或者關閉頁面后再打開,但在打開之后應該看到相同的內容。
-
持久化和數據庫訪問,我們需要將每個用戶的定時器和筆記保存下來。另一個需求是定時器需要維持剩余的時間(用戶設置了一個30分鐘的定時器,然后不小心關閉了頁面,應該如何?)
-
運行時的配置,因為我們不能把數據庫連接的信息硬編碼到代碼里。
-
日志。日志對于Web應用的作用不言而喻。
2.我用到了哪些庫?
路由和Web服務器:Spock
最終我選擇了Spock,因為它易于使用。如果你用過Ruby的Sinatra,那么Spock應該不會陌生。Spock也自帶了會話管理,這一點非常好。
例如,定義一個擁有幾條路由的服務器,返回html和JSON,代碼大致如下:
{-# LANGUAGE OverloadedStrings #-}
import Web.Spock as Spock
import Web.Spock.Config as Spock
import Data.Aeson as A
main :: IO
main = do
spockCfg <- defaultSpockCfg PCNoDatabase
runSpock 3000 $ spock spockCfg $ do
get root $ do
Spock.html "<div>Hello world!</div>"
get "users" $ do
Spock.json (A.object [ "users" .= users ])
get ("users" <//> var <//> "friends") $ \userID -> do
Spock.json (A.object [ "userID" .= (userID :: Int), "friends" .= A. ])
where users :: [String]
users = ["bob", "alice"]
數據庫訪問:postgresql-simple
postgresql-simple基本上只允許您對數據庫運行原始SQL查詢,而沒有多余的裝飾,例如防止注入攻擊。它可以實現您的期望,僅此而已。
{-# LANGUAGE OverloadedStrings #-}
import Database.PostgreSQL.Simple
userLoginsQuery :: Query
userLoginsQuery =
"SELECT l.user_id, COUNT(1) FROM logins l GROUP BY l.user_id;"
getUserLogins :: Connection -> IO [(Int, Int)]
getUserLogins conn = query_ conn userLoginsQuery
數據庫訪問:postgresql-simple
postgresql-simple可以讓你在數據庫上運行原始的SQL查詢,它只提供最基本的額外處理,比如防止注入攻擊等。它僅完成你需要的東西,沒有任何額外的功能。
{-# LANGUAGE OverloadedStrings #-}
import Database.PostgreSQL.Simple
userLoginsQuery :: Query
userLoginsQuery =
"SELECT l.user_id, COUNT(1) FROM logins l GROUP BY l.user_id;"
getUserLogins :: Connection -> IO [(Int, Int)]
getUserLogins conn = query_ conn userLoginsQuery
配置:configurator
configurator能夠從文件中讀取配置,并解析成Haskell數據類型。與普通的配置文件讀取器相比它的功能要多一些。
如果你習慣了直接讀取配置文件,那么configurator還有一些額外功能。例如,配置項可以嵌套分組,configurator還提供了熱重載來監視配置文件變化。
# An example config file.
App_name = "The Whispering Fog"
db {
pool {
stripes = 4
resource_ttl = 300
}
username = "pallas"
password = "thefalloflatinium"
dbname = "italy"
}
{-# LANGUAGE OverloadedStrings #-}
import Data.Configurator as Cfg
import Database.PostgreSQL.Simple
data MyAppConfig = MyAppConfig
{ appName :: String
, appDBConnection :: Connection
}
getAppConfig :: IO MyAppConfig
getAppConfig = do
cfgFile <- Cfg.load ["app-configuration.cfg"]
name <- Cfg.require cfgFile "app_name"
conn <- do
username <- Cfg.require cfgFile "db.username"
password <- Cfg.require cfgFile "db.password"
dbname <- Cfg.require cfgFile "db.dbname"
connect $ defaultConnectInfo
{ connectUser = username
, connectPassword = password
, connectDatabase = dbname
}
pure $ MyAppConfig
{ appName = name
, appDBConnection = conn
}
日志:fast-logger
fast-logger提供了一個相對簡單易用的日志解決方案。在Web應用程序的示例中,我只輸出到了stderr,但它還可以支持輸出日志到文件。雖然它支持許多類型,但絕大多數情況下,你需要定義一個輔助函數,接收一個LoggerSet,以及需要記錄的信息。
import System.Log.FastLogger as Log
logMsg :: Log.LoggerSet -> String -> IO
logMsg logSet msg =
Log.pushLogStrLn logSet (Log.toLogStr msg)
doSomething :: IO
doSomething = do
logSet <- Log.newStderrLoggerSet Log.defaultBufSize
logMsg logSet "message 1"
logMsg logSet "message 2"
HTML生成:blaze-html
盡管本項目的后臺并不需要生成太多HTML,但值得一提的是,blaze-html正是我需要的。
基本上它就是將HTML淺層嵌入到了Haskell DSL中。如果你會編寫HTML,那你就會使用這個庫。
{-# LANGUAGE OverloadedStrings #-}
import Data.ByteString.Lazy
import Text.Blaze.Html5 as HTML
import Text.Blaze.Html5.Attributes as HTML hiding ( title )
import Text.Blaze.Html.Renderer.Utf8 as HTML
dashboardHTML :: HTML.Html
dashboardHTML = HTML.html $
HTML.docTypeHtml $ do
HTML.head $ do
HTML.title "Timers and Notes"
HTML.meta ! HTML.charset "utf-8"
HTML.script ! HTML.src "/js/bundle.js" $ ""
HTML.body $ do
HTML.div ! HTML.id "content" $ ""
dashboardBytes :: ByteString
dashboardBytes = HTML.renderHtml dashboardHTML
構建前端:make + npm
沒錯,它們并不是庫。但我們依然需要某種JAVAScript前端,因為定時器需要實時更新。Webpack能夠生成JS包,而Make能夠組裝最終的應用程序。
這些東西無需我多說,網上有很多相關的資源。
3.我必須要用這些庫嗎?
當然不是。如果你第一次接觸Haskell,那么有這些疑問是很自然的。不要讓這篇文章限制了你的思路。盡管這個應用程序可以運行,但許多部分用于生產環境下的Haskell時并不理想。
例如,許多Haskell程序員很可能會使用Servant而不是Spock來定義API端點。如果你想了解其他庫,那當然應該跟隨你的直覺。
你可以把這些庫和這個應用程序作為起點。我建議你用這些代碼作為學習的機會,理解它的原理,然后自己試著修改。Haskell很好的一點就是它非常易于重構或者升級,而不會破壞已有的功能。
一旦掌握了這個應用程序,就可以用更高級的庫來替換它們,來獲得更多的保證。同時,這也是一個增量學習的過程。
將數據庫訪問的庫從postgresql-simple升級到支持類型安全的庫。我推薦Opaleye!
將API定義的庫從Spock升級到Servant
利用QuickCheck或hedgehog增加自動測試。例如,你可以測試服務器的每個錯誤響應都返回了JSON格式的錯誤信息。
你還可以嘗試替換前端和構建系統。
升級前端,使用PureScript或Elm來替換原始的JavaScript
升級構建系統,利用Shake替換make構建更健壯的系統
原文:https://williamyaoh.com/posts/2019-11-16-a-dead-simple-web-stack.html
本文為 CSDN 翻譯,轉載請注明來源出處。
【End】