😢

MarkdownとGo言語だけでWebアプリ作ってみた

2021/06/30に公開
1

goだけでアプリつくりたいなーと思い、HTMLやCSSを使わずにWebアプリをつくれないか試してみました!
自由度やデザインなどある程度制限がありますが、仮アプリとしてはほどほどに使える状態にはなったと思います。

ソースコードはこちら (HikaruEgashira/simple-server - GitHub)

  • http://localhost:8080/user?name=engineerのページ
    Image from Gyazo

  • 上のページは下のマークダウンで表示しています

<!-- pages/user.md -->
# Welcome

{{ text }}

[home](/)

今回やる内容

  • HTML部分をMarkdownに置き換えたWebアプリの作成
  • フォームに名前を入力したら、userページでユーザー名を表示するシンプルなアプリ
  • サーバーサイドフレームワークとしてginを用います。多分一番有名なので

gin-gonic/gin - GitHub

方針

  1. MarkdownをHTML(正式にはmustache HTML)に変換
  2. mustache HTMLから表示するHTMLを生成
  3. 描画

普通のアプローチと比較すると
手順1が増えただけなので、やり方さえつかめばいろいろな応用ができます。

初期構築

Go moduleを利用します。

go mod init github.com/xxxxx/yyyyy
go get -u github.com/gin-gonic/gin # Goフレームワーク
go get -u github.com/russross/blackfriday # MarkdownをHTMLに変換するライブラリ
go get -u github.com/cbroglie/mustache # Markdownで変数を入れるのに使う

階層構造はこんな感じ。

  • controller ... ページごとの処理を行う
  • lib ... MarkdownからHTMLを生成し描画するライブラリを作成
  • pages ... 描画するMarkdown
  • template ... htmlが書いてあるけど、コピペで大丈夫
  • usecase ... ロジック置き場
simple-server ❯❯❯ tree .
.
├── go.mod
├── go.sum
├── main.go
├── controller
│   ├── index.go
│   └── user.go
├── lib
│   └── lib.go
├── pages
│   ├── index.md
│   └── user.md
├── template
│   └── md.html
└── usecase
    └── hello.go

MarkdownをGitHubのデザインにするためのレイアウトの作成

コピペで動きます。
github-markdown-cssというライブラリを用いてCSSを触らずに見た目を整えています。

{{define "md"}}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Markdown Server</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/4.0.0/github-markdown.min.css" integrity="sha512-Oy18vBnbSJkXTndr2n6lDMO5NN31UljR8e/ICzVPrGpSud4Gkckb8yUpqhKuUNoE+o9gAb4O/rAxxw1ojyUVzg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
    <style>
        .markdown-body {
            box-sizing: border-box;
            min-width: 200px;
            max-width: 980px;
            margin: 0 auto;
            padding: 45px;
        }
    
        @media (max-width: 767px) {
            .markdown-body {
                padding: 15px;
            }
        }
    </style>
</head>
<body>
    <div class="markdown-body">        
        {{.}}
    </div>
</body>
</html>
{{end}}

Markdownのページを作成

フォームにだけHTMLを使いますのでここだけ調べる必要がありそうです。

<!-- pages/index.md -->
# simple-server

### 名前を入力

<form
  action="user"
  method="GET"
>
  <input type="text" name="name" placeholder="type username">
  <input type="submit" value="go">
</form>

値を埋め込むときはmustache形式で書くようにしました。
{{ }}の中に変数名を書くだけです。

<!-- pages/user.md -->
# Welcome

{{ text }}

[home](/)

MarkdownをHTMLに変換するRender関数の作成

第二引数にパス。それ以降の引数でMarkdown上に埋め込みたいデータを入力するライブラリを作成します。

// lib/lib.go
package lib

import (
	"html/template"
	"io"
	"io/ioutil"

	"github.com/cbroglie/mustache"
	"github.com/russross/blackfriday"
)

func Render(w io.Writer, path string, context ...interface{}) {
    md, _ := ioutil.ReadFile(path + ".md")

    // 1. MarkdownをHTML(正式にはmustache HTML)に変換
	mustacheHtml := string(blackfriday.Run(md))
    // 2. mustache HTMLから表示するHTMLを生成
	html, _ := mustache.Render(mustacheHtml, context...)

    // 3. 描画
	tmpl := template.Must(template.ParseFiles("template/md.html"))
	tmpl.ExecuteTemplate(w, "md", template.HTML(html))
}
// 使い方
// pages/userにtextが埋め込まれたHTMLを返す
lib.Render(c.Writer, "pages/user", map[string]string{"text": text})

ginでルーティング

main関数ではginを利用してルーティングを行います。
シンプルですね。

// main.go
package main

import (
	"github.com/xxxxx/yyyyy/controller"
	"github.com/gin-gonic/gin"
)

func main() {
	router := gin.Default()
	router.GET("/", controller.IndexHandler)
	router.GET("/user", controller.UserHandler)
	router.Run(":8080")
}

Controllerの作成

静的なページを返すだけなら一行でおっけー

// controller/index.go
package controller

import (
	"github.com/xxxxx/yyyyy/lib"
	"github.com/gin-gonic/gin"
)

func IndexHandler(c *gin.Context) {
	lib.Render(c.Writer, "pages/index")
}

値を埋め込むときは、引数に渡してあげます。

// controller/user.go
package controller

import (
	"github.com/xxxxx/yyyyy/lib"
	"github.com/xxxxx/yyyyy/usecase"
	"github.com/gin-gonic/gin"
)

func UserHandler(c *gin.Context) {
	name := c.Query("name")

	text := usecase.Hello(name)

	lib.Render(c.Writer, "pages/user", map[string]string{"text": text})
}

こんな感じにHTMLを使わずに画面部分を作成することができました!

Image from Gyazo

まとめ

本格的なアプリになるとこれだけだと難しいのですが、まずはデモアプリをつくるところから始めてみましょう!
実際にサーバーサイドだけのデモアプリからアプリ化までもっていった実例(自分)があるので紹介します。

Twin:teの歴史 - hatenablog

Discussion