以前色々調べながら試していたんですが、久しぶりにまたやってみようと思ったら何も覚えていなかったので、以前作ったソースコードを見ながら、同じような環境を作ってみます。バックエンドはHaskell servantで、フロントエンドはElmで、という構成です。
以下のソースコードは、githubに置いてあります。とりあえず動かしたい場合はここからクローンして動かしてください。
私の環境
- Haskell stack v1.9.3
- node v11.15.0
- npm v6.7.0
- yarn 1.16.0
※ 最初に、node v12.3.1とnpm v6.9.0でやろうとしたら、yarn addでreference error: primordialみたいなエラーが出て、調べるたところこのnodeバージョンだとうまくいかないようだったのでv11.15.0に下げました。
プロジェクト作成 - バックエンド
Haskell stackを使います。テンプレートにservantがあるので、これを使います。そのうちservant dockerも使ってみたいですね。実際にデプロイするときにはこっちの方が運用しやすそうな気がします。ちなみに、作るのはノートアプリとしておきます。resolverは別に指定しなくても良いのですが、以前作った環境に合わせて置くために、ここではlts-13.1にします。
stack new note servant --resolver=lts-13.1
フロントエンド
今作ったプロジェクト下にフロントエンド用のディレクトリを作ります。そして、elm v0.19をインストールします。私は全てローカルにインストールします。
cd note
mkdir frontend
yarn init # とりあえず全部デフォルトのままエンター
yarn add elm@0.19.0-no-deps
# 確認
yarn elm --version
それから、webpackと、webpackでelmを使うためのパッケージなど諸々をインストールします。
yarn add webpack webpack-dev-server elm-webpack-loader file-loader style-loader css-loader url-loader sass-loader
yarn add ace-css@1.1 font-awesome@4
yarn add -D webpack-cli
yarn add -D @webpack-cli/init
yarn add -D html-webpack-plugin uglifyjs-webpack-plugin
Elmのパッケージインストール
cd frontend/
yarn elm init # y
yarn elm install elm/url # y
yarn elm install Fresheyeball/elm-return # y
webpack.config.jsの作成
note/frontend/webpack.config.js
です。Elmのロード方法なども書きます。面倒なのでコピペで。
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
{
loader: 'style-loader',
options: {
sourceMap: true
}
},
{
loader: 'css-loader'
}
]
},
{
test: /\.scss/,
use: [
{
// output to link tag
loader: 'style-loader'
},
// bundle css
{
loader: 'css-loader',
options: {
// prohibit `url()` in css`
url: false,
// use source map
sourceMap: true,
// 0 => no loaders
// 1 => postcss-loader
// 2 => postcss-loader, sass-loader
importLoaders: 2
}
},
{
loader: 'sass-loader',
options: {
sourceMap: true
}
}
]
},
{
test: /\.elm$/,
exclude: [
/elm_stuff/,
/node_modules/
],
use: [
{
loader: 'elm-webpack-loader?verbose=true'
}
]
},
{
test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
loader: 'url-loader?limit=10000&mimetype=application/font-woff',
},
{
test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
loader: 'file-loader',
}
],
noParse: /\.elm$/
},
entry: {
app: './src/index.js'
},
output: {
filename: '[name].[chunkHash]js',
path: path.resolve(__dirname, '../www/dist')
},
mode: 'development',
plugins: [
new UglifyJSPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html',
inject: 'body',
filename: 'index.html'
})
],
optimization: {
splitChunks: {
cacheGroups: {
vendors: {
priority: -10,
test: /[\\/]node_modules[\\/]/
}
},
chunks: 'async',
minChunks: 1,
minSize: 30000,
name: true
}
},
devServer: {
inline: true,
stats: { colors: true },
}
};
最小限のアプリを書いて実行してみる
note/frontend/src/index.html
です。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Note app by Servant and Elm</title>
</head>
<body>
<div id="main"></div>
</body>
</html>
note/frontend/src/index.js
です。Elmはjsにコンパイルされるので、そのメイン関数をインポートしてid="main"の要素に埋め込みます。
'use strict';
require('ace-css/css/ace.css');
require('font-awesome/css/font-awesome.css');
import './style.scss'
var {Elm} = require('./Main.elm');
var mountNode = document.getElementById('main');
var app = Elm.Main.init({node: mountNode});
note/frontend/src/main.Elm
です。ようやくelmです。あとでモジュールを分けていきますが、まずは全部mainに書いてしまいます。
module Main exposing (..)
import Browser exposing (application, Document)
import Browser.Navigation as Nav
import Html exposing (..)
import Return exposing (Return)
import Url
type alias Model =
String
type Msg =
NoOp
init : () -> Url.Url -> Nav.Key -> (Model, Cmd Msg)
init flags url key =
("Hello from Elm.", Cmd.none)
view : Model -> Document Msg
view model =
{ title = "Note"
, body = viewMain model
}
viewMain : Model -> List (Html Msg)
viewMain model =
[ div []
[ text model ]
]
update : Msg -> Model -> Return Msg Model
update msg model =
case msg of
NoOp -> (model, Cmd.none)
main : Program () Model Msg
main =
application
{ init = init
, view = view
, update = update
, subscriptions = \_ -> Sub.none
, onUrlChange = \_ -> NoOp
, onUrlRequest = \_ -> NoOp
}
あと、note/frontend/src/style.scss
を作っておきます。
touch src/style.scss
これで動かせるはずです。frontendディレクトリ直下で以下を実行します。
# note/frontend直下で
yarn webpack
# うまく行ったら以下でdevサーバー立ち上げ
yarn webpack-dev-server --port 3000
http:/localhost:3000にアクセスすると、Hello from Elmと表示されるはずです。これでもうなんでもできます。
おまけ
以下のようにすると、簡単に立ち上げられるようになります。
frontend/package.json
に以下を追記します。
"scripts": {
"build": "webpack",
"client": "webpack-dev-server --port 3000",
"start": "nf start"
}
それから、frontend/Procfile
を作成して、中に以下の1行を書きます。
client: yarn client
これで、yarn start
でビルドとdevサーバー立ち上げを実行できるようになりました。Haskellの方はプロジェクトを作っただけでまだ何も書いてないのにだいぶ長くなってしまいました。どうしましょう。ただのElm+webpack環境構築の記事になってしまいました。
バックエンド
ようやくHaskellに戻ってきました。今Elmで作った静的ページをServantでたてたWebサーバーでホストします。Servantを使う本当の目的はこのように静的ページを扱うことではなく、HaskellバックエンドでWebAPIを提供することだと思いますが、この記事はHaskell+ElmのWeb開発環境を作るところまでなので、とりあえず何か表示できたらOKとします。
以下のようにnote/src/App.hs
を作成します。
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeOperators #-}
module App where
import qualified Data.ByteString.Lazy as B
import Network.HTTP.Media ((//), (/:))
import Servant
import Servant.Server (serve)
import System.FilePath ((</>))
import Config.Config (_distDir_, _entry_)
data HTML
instance Accept HTML where
contentType _ = "text" // "html" /: ("charset", "utf-8")
instance MimeRender HTML B.ByteString where
mimeRender _ bs = bs
type API =
Get '[HTML] B.ByteString
:<|> Raw
api :: Proxy API
api = Proxy
root :: String
root = _distDir_
server :: IO (Server API)
server = do
indexHtml <- B.readFile $ root </> _entry_
let server' =
pure indexHtml
:<|> serveDirectoryWebApp root
return server'
app :: IO Application
app = serve api <$> server
ソース内で、Config.Config
をインポートしていますが、単純にディレクトリやファイルパスを定義しているだけです。note/src/Config/Config.hs
です。
{-# LANGUAGE OverloadedStrings #-}
module Config.Config where
{-
- frontend
-}
_distDir_ = "www/dist"
_entry_ = "index.html"
メインは以下のようになります。note/app/Main.hs
です。
module Main where
import Network.Wai.Handler.Warp
import System.IO (hPutStrLn, stderr)
import App
main :: IO ()
main = do
let
port = 3000
settings =
setPort port $
setBeforeMainLoop (hPutStrLn stderr
("Listening on port " ++ show port ++ "..."))
defaultSettings
runSettings settings =<< app
note.cabal
を以下のように変更する必要があります。モジュールファイル名の追加と、依存パッケージ名の追加です。以下は変更部分のlibrary
のところだけ記載しています。
library
hs-source-dirs: src
exposed-modules: App
, Config.Config
build-depends: base >= 4.7 && < 5
, aeson
, bytestring
, filepath
, http-media
, servant
, servant-server
, wai
, warp
default-language: Haskell2010
これでビルドすれば動くはずです。フロントエンドとバックエンドをそれぞれビルドし、最後にHaskellのプログラム(Servantのwebサーバー)を起動します。
cd frontend
yarn webpack
cd ../
stack build
stack exec note-exe
http://localhost:3000/にアクセスすれば、webpack-dev-serverでやった時と同じ画面が出てくるはずです。