見出し画像

goのウェブ関係を学ぼう(静的ファイルを扱う)

4連休でgoのウェブ関係の基本的なことを学んでいきました。しかし4連休では時間が足りずクライアントに文字列を返す程度でしたが、それだと大したことはできないので、今回からもう少し掘り下げていこうと思います。

前回までと同様参考サイトは以下になります。参考サイトだとフォームの項からになります。

今回は、フォームに入る前に簡単にhtmlやcss, javascript, 画像などの静的ファイルをどのようにホスティングするかをまとめていこうと思います。参考サイトには載っていないと思います。

本来ホスティングとはインターネットサービスプロバイダなどが、お客様のメールサービスやウェブサービスをお預かりし、運用するサービスです。ここでいうホスティングはクライアントのリクエストに応じて必要なファイルを出力出来るようにすることとします。

FileServerを使う

簡単にやる方法としてはFileServer関数を使う方法があります。ディレクトリ構成を以下のようにしてください。

スクリーンショット 2020-09-26 17.44.23

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>テストページ</title>
   <link rel="stylesheet" type="text/css" href="styles/style.css"/> 
</head>
<body>
   <h1>テストページ</h1>
   <div class="contents">
       <p>こんにちは!</p>
       <p>2006/1/2 15:04:05</p>      
   </div>
</body>
</html>

style.css

html body {
   margin: 0;
   padding: 0;
   font-size: 16px;
   font-weight: bold;
   background: #2193b0;  /* fallback for old browsers */
   background: -webkit-linear-gradient(to top, #6dd5ed, #2193b0);  /* Chrome 10-25, Safari 5.1-6 */
   background: linear-gradient(to top, #6dd5ed, #2193b0); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
   color: aliceblue;
   width: 100vw;
   height: 100vh;
   overflow: hidden;
   position: relative;
   text-align: center;
}
#title {
   width: 100%;
   color: aliceblue;
   position: fixed;
}
.titleBefore {
   top: 110%;
}
.titleAfter {
   top: 0;
   transition: top 1s ease-in;
}
.contents {
   position: relative;
   top: 100px;
}
.footer {
   position: fixed;
   width: 100%;
   text-align: center;
   bottom: 0;
}

server.go

package main
import (
	"log"
	"net/http"
)
func main() {
	port := "8080"
	http.Handle("/", http.FileServer(http.Dir("src")))
	log.Printf("Server listening on port %s", port)
	log.Print(http.ListenAndServe(":"+port, nil))
}

以下それぞれの関数の説明を簡単に書いておきます。

Handle関数

func Handle(pattern string, handler Handler)

Handle関数はDefaultServeMuxにHandlerとそのパスを登録する関数です。今回は静的ファイルを扱いたいので、そこにHandlerとしてFileServerを渡してあげます。

FileServer関数

func FileServer(root FileSystem) Handler

FileServerはrootをルートディレクトリとするコンテンツを含むHttpリクエストを処理するハンドラーを返します。rootはFileSystemの型ですが、これは次のように文字列をDir型にキャストしてあげることで使えます。

Dir("styles/")

以上のことを踏まえると以下のコードを書き足せばいいことが解ります。

http.Handle("/", http.FileServer(http.Dir("src")))

実行すると下のような感じです。

スクリーンショット 2020-09-26 17.51.34

HTMLに埋め込みを行う

phpのようにhtmlにサーバサイドで値を埋め込んでクライアントに送ることができます。

HTMLファイルをホスティングする

まずは単純にindex.htmlを返すサーバを組んでみましょう。statichostというディレクトリをgoのワーキングディレクトリ/srcに作成します。statichost内に以下のようにファイルを作成します。

スクリーンショット 2020-09-26 12.55.12

server.goがウェブサーバになり、ルートにアクセスしたらindex.htmlファイルを返すようにします。

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Document</title>
</head>
<body>
   <h1>テストページ</h1>
</body>
</html>

server.go

package main
import (
	"html/template"
	"log"
	"net/http"
)
func main() {
	port := "8080"
	http.HandleFunc("/", handleIndex)
	log.Printf("Server listening on port %s", port)
	log.Print(http.ListenAndServe(":"+port, nil))
}
func handleIndex(w http.ResponseWriter, r *http.Request) {
	t, _ := template.ParseFiles("template/index.html")
	t.Execute(w, nil)
}

埋め込みにはhtml/templateを使います。main関数の記述は前回までのnote記事をみていただくとわかると思います。

handleIndex関数では、template.ParseFiles関数で引数に指定されたファイルを解析し、ファイルに応じたテンプレートを出力します(今回はHTML)。

t.Execute(w, nil)

その後、Excuteメソッドでテンプレートに第2引数で必要な情報を埋め込み第1引数で指定したio.Writerに渡すことで、クライアントがアクセスした時に、HTMLファイルを返すようにしています。今回は、第1引数io.Writerにwつまりhttp.ResponseWriterを指定しています。また第2引数はnilなので埋め込む情報はないということです。

実行すると以下のようにきちんと表示されます。

スクリーンショット 2020-09-26 13.23.30

余談ですが、template.ParseFilesで解析が入るのでtxtファイルで読ませても、中の記述をHTML形式で行っていれば、HTMLファイルとしてクライアントに出力してくれます。

HTMLファイルに情報を埋め込む

template.Excuteメソッドの第2引数で情報を埋め込めると記述しましたが、これを試してみましょう。index.htmlを次のように書き換えます。

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>{{.Title}}</title>
</head>
<body>
   <div class="contents">
       <p>{{.Message}}</p>
       <p>{{.Time.Format "2006/1/2 15:04:05"}}</p>      
   </div>
</body>
</html>

phpやlaravelのbladeファイル、またはreactのJSXを使ったことがある人は、似たことをやっていると思いますが、html内に中かっこを二つで中に引数を取ってあげるとtemplate.Excuteの第2引数で渡した変数の値を埋め込むことができます。

合わせてserver.goも以下のように書き換えましょう(変更部分のみ抜粋)。

server.go

type embed struct {
	Title   string
	Message string
	Time    time.Time
}

func handleIndex(w http.ResponseWriter, r *http.Request) {
	t, _ := template.ParseFiles("template/index.html")
	emb := embed{"テストページ", "こんにちは!", time.Now()}
	t.Execute(w, emb)
}

これでそれぞれの場所にemb変数内の値が埋め込まれて出力されます。実行結果は以下のようになります。

スクリーンショット 2020-09-26 14.36.15

このようにサーバサイドで必要な情報を埋め込んでクライアントに送ることも簡単にできます。

また、ヘッダーやフッターなどをコンポーネント化することも可能です。以下のようにファイルを作成します。

スクリーンショット 2020-09-26 14.47.32

それぞれのファイルの中身を以下のようにします。

header.html

{{ define "header" }}
<h1>{{ .Title }}</h1>
{{ end }}

footer.html

{{ define "footer" }}
<div>Copyright &copy; bkc</div>
{{ end }}

define "name"でコンポーネントの名前を付けます。必ず最後には{{end}}をつけるようにしてください。

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>{{.Title}}</title>
</head>
<body>
   {{ template "header" . }}
   <div class="contents">
       <p>{{.Message}}</p>
       <p>{{.Time.Format "2006/1/2 15:04:05"}}</p>      
   </div>
   {{ template "footer" }}
</body>
</html>

index.htmlでheader, footerを読み込みます。{{ template "name" }}を記述した場所にコンポーネントが挿入されます。{{ template "header" . }}のように変数の埋め込みを行うコンポーネントには最後にドットを入れましょう。

最後にserver.goを書き換えます。

func handleIndex(w http.ResponseWriter, r *http.Request) {
	t, _ := template.ParseFiles(
		"template/index.html",
		"template/components/header.html",
		"template/components/footer.html",
	)
	emb := Embed{"テストページ", "こんにちは!", time.Now()}
	t.Execute(w, emb)
}

実行結果が以下になります。

スクリーンショット 2020-09-26 14.55.20

ここまでは、クライアントがリクエストを投げる度にtemplate.ParseFiles関数で解析しテンプレート化していましたが、サーバ立ち上げ時にテンプレート化した方が無駄が少なく処理が早くなるので、そのように書き換えましょう。書き換えるのはserver.goのみです。

server.go

package main
import (
	"html/template"
	"log"
	"net/http"
	"time"
)
// Embed htmlファイルに埋め込むデータ構造体
type Embed struct {
	Title   string
	Message string
	Time    time.Time
}
var templates = make(map[string]*template.Template)
func main() {
	port := "8080"
	templates["index"] = loadTemplate("index")
	http.HandleFunc("/", handleIndex)
	log.Printf("Server listening on port %s", port)
	log.Print(http.ListenAndServe(":"+port, nil))
}
func handleIndex(w http.ResponseWriter, r *http.Request) {
	emb := Embed{"テストページ", "こんにちは!", time.Now()}
	templates["index"].Execute(w, emb)
}
func loadTemplate(name string) *template.Template {
	t, err := template.ParseFiles(
		"template/"+name+".html",
		"template/components/header.html",
		"template/components/footer.html",
	)
	return t
}

最初にテンプレートをマップで保持する変数templatesを宣言します。また引数で指定されたファイルのテンプレートを作成する関数loadTemplateを実装し、main関数で実行します。これによりサーバ立ち上げ時にテンプレートが作成され、クライアントからリクエストがきたときは、データを埋め込むだけになります。

CSS, JavaScript, 画像をホスティングする

埋め込みのためにhtml/templateを使う場合はFileServerを使う場合少し工夫が必要です。ディレクトリ構成を以下のようにします。

スクリーンショット 2020-09-26 18.08.25

画像等は自分で好みのものを用意してください。

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>{{.Title}}</title>
   <link rel="stylesheet" type="text/css" href="/styles/style.css" />
</head>
<body>
   {{ template "header" . }}
   <div class="contents">
       <p>{{.Message}}</p>
       <p>{{.Time.Format "2006/1/2 15:04:05"}}</p>
       <img src="/assets/biplane.png" width="300px"/>        
   </div>
   {{ template "footer" }}
   <script src="/js/script.js"></script>
</body>
</html>

header.html

{{ define "header" }}
<h1 id="title" class="titleBefore">{{ .Title }}</h1>
{{ end }}

footer.html

{{ define "footer" }}
<div class="footer">Copyright &copy; bkc</div>
{{ end }}

script.js

(function() {
   setTimeout(function() {
       const title = document.getElementById("title");
       title.classList.remove("titleBefore");
       title.classList.add("titleAfter");
   }, 500);
})();

h1タグのタイトルが下から上に上がって来るようにコードを書いてます。

server.go(一部略)

package main
import (
	"html/template"
	"log"
	"net/http"
	"time"
)
func main() {
	port := "8080"
	templates["index"] = loadTemplate("index")
	// ソースディレクトリの読み込み。ここの処理を調べる。
	http.Handle("/styles/", http.StripPrefix("/styles/", http.FileServer(http.Dir("styles/"))))
	http.Handle("/js/", http.StripPrefix("/js/", http.FileServer(http.Dir("js/"))))
	http.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.Dir("assets/"))))
	http.HandleFunc("/", handleIndex)
	log.Printf("Server listening on port %s", port)
	log.Print(http.ListenAndServe(":"+port, nil))
}

server.goは重要な部分、つまりファイルをホスティングする部分のコードを抜粋しています。

ここで重要なのは、http.FileServerをhttp.StripPrefixでラップしているところです。StripPrefixは第1引数の文字列をパスから除外します。StripPrefixを付けないと、htmlファイル内の

<link rel="stylesheet" type="text/css" href="/styles/style.css" />

では、ディレクトリのパスが/styles内の

/styles/style.css

を参照してしまい上手くファイルを取得できません。これを

http.Handle("/styles/", http.StripPrefix("/styles/", http.FileServer(http.Dir("styles/"))))

とすることで、最初の/styles/を文字列から除外することができ、/styles内の

style.css

を参照することができます。同様にjsファイル、assetsに関してもコードを記述し実行すると以下のようになります。

スクリーンショット 2020-09-26 18.29.59

エラーハンドリングなども含めた最終的なソースコードは以下にあります。


この記事が気に入ったらサポートをしてみませんか?