net/httpによるHTTPメソッドを含んだルーティングの実装
最近GoによるWebアプリケーション開発を学び始めたので間違っている箇所があればコメントください。
ServeMux型によるルーティング
http.Handle
関数を使うとパスに対するルーティングを登録することができる。http.Handler
型は実際にリクエストを処理するオブジェクトで、下のように実装すると/foods
へのリクエストを*FoodsHandler
型が処理することになる。
http.Handle("/foods", &handlers.FoodsHandler{})
http.Handle
関数によって登録されたルーティングはhttp.DefaultServeMux
という*ServeMux
型の変数が保持することになる。
type ServeMux struct { mu sync.RWMutex m map[string]muxEntry hosts bool } type muxEntry struct { h Handler pattern string }
登録されたルーティングはフィールドm
で保持される。サーバーはm
から一致するパスを探し、対応するHandler
を呼び出す。
見たところ、ServeMux
型ではGET
, POST
等のHTTPメソッドを考慮していない。RESTful APIを実装するにはHTTPメソッドを考慮する必要があるため、ServeMux
型によるルーティングでは不十分だと分かる。そこで、ルーティングを自前で実装する。
Handlerによるルーティング
http.Handle
関数の代わりにhttp.ListenAndServe
関数に渡すhttp.Handler
によってルーティングを実装する。
http.ListenAndServe(":8080", handler)
http.DefaultServeMux
を使う場合はhandler
の代わりにnil
を渡すが、自前のハンドラーを使う場合はここに渡す。
type RoutesHandler struct { routes map[string]map[string]http.Handler } func (h *RoutesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { paths, ok := h.routes[r.Method] if !ok { w.WriteHeader(http.StatusNotFound) return } handler, ok := paths[r.URL.Path] if !ok { w.WriteHeader(http.StatusNotFound) return } handler.ServeHTTP(w, r) }
*ServeMux
型とは違い、map[string]map[string]http.Handler
型のフィールドroutes
でHTTPメソッドを含むルーティングを管理するようにした。ServeHTTP
関数を実装することでhttp.Handler
型のインターフェイスを満たしている。内部でroutes
から一致するハンドラーを呼び出す。
func (h *RoutesHandler) GET(path string, handler http.Handler) { h.register("GET", path, handler) } func (h *RoutesHandler) POST(path string, handler http.Handler) { h.register("POST", path, handler) } func (h *RoutesHandler) register(method, path string, handler http.Handler) { if h.routes == nil { h.routes = make(map[string]map[string]http.Handler) } _, ok := h.routes[method] if !ok { h.routes[method] = make(map[string]http.Handler) } h.routes[method][path] = handler }
こうした関数を定義し、ルーティングを登録できるようにする。
routesHandler := &handlers.RoutesHandler{} routesHandler.GET("/foods", &handlers.FoodsHandler{}) http.ListenAndServe(":8080", routesHandler)
GoのためのDockerfile
base image
library/golang
で公式イメージが用意されている。ユースケースに合わせていくつかの種類が用意されている。
golang:<version>
: 何が必要なのか分かっていない場合はこれを使った方がよさそう。golang:alpine
: Alpine Linuxをベースとしているため非常に軽い。イメージサイズを小さくしたい場合に推奨されている。golang:onbuild
: ネット上ではよく紹介されているが、公式では非推奨とされている。
ディレクトリレイアウト
$ docker run -i -t --rm golang:1.9 /bin/bash root@xxxxxxxxx:/go# pwd /go root@xxxxxxxxx:/go# ls bin src root@xxxxxxxxx:/go# env | grep GO GOLANG_VERSION=1.9 GOPATH=/go
GOPATH
が/go
に設定されているので、/go/src/
以下にWORKDIR
を設定していく。
Dockerfile
FROM golang:1.9 WORKDIR /go/src/github.com/naoty/golang-sample COPY . . RUN go install github.com/naoty/golang-sample ENTRYPOINT ["golang-sample"]
go install ...
で/go/bin/
以下にバイナリがビルドされる。PATH
は/go/bin
も含まれているため、そのままビルドされたバイナリを指定するだけでOK。
参考
Goでちょっとしたツールを作った
Go言語のレベルアップを目的としてちょっとしたツールを2つ作った。
license
MITライセンスファイル(LICENSE
)を作成するとき、いつもMIT License | Choose a Licenseからコピペしていた。さすがに毎回同じことをするのは面倒になってきたのでテンプレートからテキストを生成するだけのコマンドラインツールを書いた。text/template
を使ったことがなかったのでちょうどいい練習になった。
brewery
Goで書いたコマンドラインツールはnaoty/homebrew-miscからHomebrewでインストールできるようにしている。その準備をするためにformulaを作るとき、brew create <url> --tap naoty/misc
を実行していた。しかし、この方法だと/usr/local/Homebrew/Library/Taps/naoty/homebrew-misc/
以下にformulaが作成されてしまい、その後ワークスペースにコピペする作業が発生していた。
そこで、formulaをテンプレートから生成して標準出力に出力するだけのコマンドラインツールを作った。SHA256もちゃんと計算してくれるので便利。今後はformulaを書く作業が捗りそう。
学び
text/template
の使い方。Webアプリケーションを開発するのであれば、同じようなパッケージであるhtml/template
が確実に必要になるので、覚えておきたかった。- https://github.com/jteeuwen/go-bindataによってテンプレートをバイナリに含めること。これもテンプレートを使う以上シングルバイナリにして配布を簡単にするために必要になるだろう。
- golang/depの使い方。おおかたの仕様についてはstableになったとのことなので、今から使い方に慣れておきたい。
dep ensure
のバリエーションとGopkg.toml
の書き方をもう少し把握したい。
近況
5月
- 結婚式を挙げ、新婚旅行にいった。
6月
- 30歳になった。
- 仕事が忙しくなりはじめる。最近はRailsでサーバーサイドを書いている。
- 結婚式の準備などの忙しさから解放されたため、個人開発を少しずつ始める。ElectronでTodoアプリを作り始め、Webpack, React, Redux, TypeScriptなどモダンなWebフロントエンドの技術スタックについて学んだ。
7月
- 仕事の忙しさがピークに至る。
8月
- 仕事がだいたい落ち着く。
- お盆休みは海いったりバーベキューしたり、ここ10年で最も充実した夏だった。
- 個人開発では、RailsでモダンなWebフロントエンド環境を導入するための知見を学んだ。具体的にはwebpackerについて学んだ。http://naoty.hatenablog.com/entry/2017/08/17/201249
- また、GraphQLにも興味をもちRailsで試してみたりした。引き続き学んでいきたい。
- ある日、思い立ってこのブログのスタイルを書いた。なるべく文章が読みやすいデザインを心がけた。 https://github.com/naoty/focus-theme
`bin/webpack`を読んだ
webpackerを理解するため、rails g webpacker:install
で追加されるbin/webpack
や設定の中身を読むことにした。
bin/webpack
newenv = { "NODE_PATH" => NODE_MODULES_PATH.shellescape } cmdline = ["yarn", "run", "webpack", "--", "--config", WEBPACK_CONFIG] + ARGV Dir.chdir(APP_PATH) do exec newenv, *cmdline end
bin/webpack
では実際にはyarn run webpack -- --config WEBPACK_CONFIG
を実行している。WEBPACK_CONFIG
はconfig/webpack/#{NODE_ENV}.js
となっているため、config/webpack/development.js
などとなる。
config/webpack/development.js
const sharedConfig = require('./shared.js') module.exports = merge(sharedConfig, { // ... })
config/webpack/shared.js
というファイルが環境ごとの設定ファイルでmergeされているようだ。
config/webpack/shared.js
const { env, settings, output, loaderDir } = require('./configuration.js')
settings
はconfig/webpacker.yml
をロードしたオブジェクトを参照している。settings.extensions
:[.coffee, .erb, .js, .jsx, .ts, .vue, ...]
settings.source_path
:app/javascript
settings.source_entry_path
:packs
output
はpath
とpublicPath
というプロパティをもったオブジェクトを参照している。path
:public/packs
publicPath
: ‘/packs’ASSET_HOST
という環境変数を指定することでホストを変更できそう。
loadersDir
はconfig/webpack/loaders/
を参照している。
const extensionGlob = `**/*{${settings.extensions.join(',')}}*` const entryPath = join(settings.source_path, settings.source_entry_path) const packPaths = sync(join(entryPath, extensionGlob)) module.exports = { entry: packPaths.reduce( // ... ) }
entry
はwebpackによってbundleされる対象のファイルを設定する。sync
はhttps://github.com/isaacs/node-globからexportされている。同期的にglobサーチをしている。- ここでは、
app/javascript/packs/**/*{.coffee,.erb,.js,.jsx}*
のようなglobでファイルを検索し、マッチしたファイルのリストがpackPaths
に代入されている。 - つまり、
app/javascript/packs/
以下のsettings.extensions
で指定された拡張子をもつファイルがwebpackによってbundleされるということになる。
module.exports = { entry: packPaths.reduce( (map, entry) => { const localMap = map const namespace = relative(join(entryPath), dirname(entry)) localMap[join(namespace, basename(entry, extname(entry)))] = resolve(entry) return localMap }, {} ) }
entry
にオブジェクトが指定された場合、プロパティごとにbundleされるファイルが分割される。output.filename
で[name]
と指定された箇所にプロパティ名が入る。
const { env, settings, output, loaderDir } = require('./configuration.js') module.exports = { output: { filename: '[name].js', path: output.path, publicPath: output.publicPath } }
output
はbundleされたファイルの出力先を設定する。output.filename
でbundleされたファイル名を設定する。entry
がオブジェクトで指定されているため、[name]
にはオブジェクトの各プロパティ名が代入される。output.path
は出力先のパスを設定する。上記の通りpublic/packs
が設定されている。output.publicPath
は本番ビルド時のCSSやHTML内のURLを設定する。これは本番のみCDNを使う場合に便利。上述の通りこれは/packs
が設定されているが、ASSET_HOST
という環境変数でこれを変更することができるようになっている。
module.exports = { module: { rules: sync(join(loadersDir, '*.js')).map(loader => require(loader)) } }
rules
はwebpackのモジュールを設定する。config/webpack/loaders/*.js
にマッチするファイルを検索している。- マッチしたファイルを
require
している。各ファイルは以下のようになっている。これによって、config/webpack/loaders/*.js
内の設定を展開している。
module.exports = { test: /\.(jpg|jpeg|png|gif|svg|eot|ttf|woff|woff2)$/i, use: [{ loader: 'file-loader', options: { publicPath, name: env.NODE_ENV === 'production' ? '[name]-[hash].[ext]' : '[name].[ext]' } }] }
const webpack = require('webpack') const ExtractTextPlugin = require('extract-text-webpack-plugin') const ManifestPlugin = require('webpack-manifest-plugin') module.exports = { plugins: [ new webpack.EnvironmentPlugin(JSON.parse(JSON.stringify(env))), new ExtractTextPlugin(env.NODE_ENV === 'production' ? '[name]-[hash].css' : '[name].css'), new ManifestPlugin({ publicPath: output.publicPath, writeToFileEmit: true }) ] }
plugins
はwebpackのプラグインを設定する。webpack.EnvironmentPlugin
はprocess.env
から環境変数にアクセスできるようにするプラグイン。ExtractTextPlugin
はコンパイルされたテキストを別ファイルに出力するプラグイン。コンパイルしたCSSをJavaScriptでロードする他にLinkタグからロードしたい場合、コンパイルしたCSSをCSSファイルとして出力するためにこのプラグインを使う。[name]-[hash].css
の[hash]
はビルド毎のユニークなハッシュ値を表す。
ManifestPlugin
はマニフェストファイルを生成するプラグイン。マニフェストファイルには、ファイル名と対応するコンパイル後のファイル名が載っている。マニフェストファイルによって、コンパイル前のファイル名からコンパイル後のファイル名に名前解決し、ヘルパーからアクセスできる。
= stylesheet_pack_tag "application" # load /packs/application-xxxxxxxx.css
{ "application.css": "/packs/application-xxxxxxxx.css" }
module.exports = { resolve: { extensions: settings.extensions, modules: [ resolve(settings.source_path), 'node_modules' ] } }
resolve
はモジュール解決方法を設定する。webpackはデフォルトではいい感じに設定されている。resolve.extensions
はファイル名からモジュールを解決する際に自動的に付与する拡張子を設定する。resolve.modules
はモジュールを解決する際に検索されるディレクトリを設定する。
github.com/rails/webpacker/lib/webpacker/helper.rb
#stylesheet_pack_tag
がマニフェストファイルからどのようにアセットを参照するかを確認する。
def stylesheet_pack_tag(*names, **options) unless Webpacker.dev_server.running? && Webpacker.dev_server.hot_module_replacing? stylesheet_link_tag(*sources_from_pack_manifest(names, type: :stylesheet), **options) end end def sources_from_pack_manifest(names, type:) names.map { |name| Webpacker.manifest.lookup(pack_name_with_extension(name, type: type)) } end def pack_name_with_extension(name, type:) "#{name}#{compute_asset_extname(name, type: type)}" end
#sources_from_pack_manifest
でマニフェストからアセットのファイル名を解決しているようだ。ActionView::Helpers::AssetUrlHelper#compute_asset_extname
はファイル名とtype
から適切な拡張子を返す。Webpacker.manifest
はWebpacker::Manifest
インスタンスを返す。
github.com/rails/webpacker/lib/webpacker/manifest.rb
def lookup(name) compile if compiling? find name end def find(name) data[name.to_s] || handle_missing_entry(name) end def data if env.development? refresh else @data ||= load end end def refresh @data = load end def load if config.public_manifest_path.exist? JSON.parse config.public_manifest_path.read else {} end end