LoginSignup
5
2

More than 3 years have passed since last update.

goxygenをつかって、Go + Front Framework + MongoDB構成のWebアプリテンプレートをかんたんに作成する

Last updated at Posted at 2020-03-08

前置き

githubのTrending Go repositoriesを見ていたところ、goxygenという面白そうなテンプレート作成ツールがあったので、使ってみました。

リポジトリは下記です。
https://github.com/Shpota/goxygen

リポジトリのトップページに、

Generate a modern Web project with Go, Angular/React/Vue, and MongoDB in seconds🚀

とあるように、コマンド一発叩くだけで、MariaDB + golang + FrontFW(Angular or React or Vue)構成のWebアプリの骨子が出来上がるみたいです。

インストール

READMEに記載の通り、go get -u github.com/shpota/goxygenにて取得します。

goxygen起動

go run github.com/shpota/goxygen init [appname]一発でOKです。
なお、デフォルトはReactなので、ほかのFrontFWを使いたい場合は、--frontendオプションを使用します。
vueの場合は、go run github.com/shpota/goxygen init --frontend vue [appname]です。

実行すると、下記のような構成のディレクトリがgenerateされます。(vueで起動した場合)

.
├── Dockerfile
├── README.md
├── docker-compose-dev.yml
├── docker-compose.yml
├── init-db.js
├── server
│   ├── db
│   │   └── db.go
│   ├── go.mod
│   ├── model
│   │   └── technology.go
│   ├── server.go
│   └── web
│       └── app.go
└── webapp
    ├── babel.config.js
    ├── package.json
    ├── public
    │   ├── favicon.ico
    │   └── index.html
    └── src
        ├── App.vue
        ├── assets
        ├── components
        └── main.js

9 directories, 16 files

webappフォルダがFrontのソース、serverフォルダがserverのソースとなっています。

動かしてみる(Docker)

READMEに記載の通り、docker-compose upコマンドをたたけばOKです。
dockerが正常に立ち上がったら、http://localhost:8080 で下記のようなサンプルページにアクセスできると思います。
image.png

動かしてみる(ローカル)

ローカルでの動作させる場合は、docker-compose-dev.ymlを使えば良さそうです。
DBのみのコンテナが準備されています。
開発で頻繁にコードを触る場合は、こちらのdocker-composeファイルのほうがお世話になりそうですね。

  • docker-compose -f docker-compose-dev.yml upでDB起動
  • serverディレクトリ内でgo run server.goでサーバ起動
  • webappディレクトリ内でnpm install npm run startでクライアント起動

上記コマンド後、http://localhost:3000 でサンプルページにアクセスすることができます。

Todoアプリを作ってみる

generate時にあったサンプルコードをもとに、Todoアプリを作ってみました。

webapp側の実装

App.vueにまとめて記載していますが、本来はコンポーネントに分けて実装されることになります。

src/App.vue
<template>
  <div>
    <h3>My Todo</h3>
    <input v-model="newTodo" placeholder="input here..."> 
    <button @click="addTodo()">ADD</button>

    <h5>Todo List</h5>
    <ul>
      <li v-for="todo in todos" :key="todo.id">
        <div :id="todo.id">
        {{ todo.text }}
        <button @click="deleteTodo(todo.id)">DEL</button>
      </div>
    </li>
    </ul>
  </div>
</template>

<script>
import axios from 'axios'
export default {
  data() {
    return {
      todos: [],
      newTodo: ''
    }
  },
  mounted() {
    this.fetchTodo()
  },
  methods: {
    fetchTodo() {
      axios
      .get(`${process.env.VUE_APP_API_URL}/api/todos`)
      .then(response => (this.todos = response.data))
    },
    addTodo() {
      if (this.newTodo==='') return;
      axios
        .post(`${process.env.VUE_APP_API_URL}/api/todos`, {
          text: this.newTodo
        })
        .then(() => this.fetchTodo())
      this.newTodo = ''
    },
    deleteTodo(i) {
      axios
        .delete(`${process.env.VUE_APP_API_URL}/api/todos?id=${i}`)
        .then(() => this.fetchTodo())
    }
  }
}
</script>

サーバ側とのやり取りは、axiosによるhttp通信で実現しています。

server側の実装

つづいてサーバ側の実装です。
generate時のサンプルコードを元にしているので、サンプルコードに追記する形で実装しました。
追記箇所には、//追加のコメントアウトを入れています。

web/app.go
package web

import (
    "bytes"
    "encoding/json"
    "io"
    "log"
    "my-app2/db"
    "net/http"
)

type App struct {
    d        db.DB
    handlers map[string]http.HandlerFunc
}

// 追加
type TodoForm struct {
    Text string `json:"text"`
}

func NewApp(d db.DB, cors bool) App {
    app := App{
        d:        d,
        handlers: make(map[string]http.HandlerFunc),
    }
    techHandler := app.GetTechnologies
    // 追加
    todoHandler := app.Todos
    if !cors {
        techHandler = disableCors(techHandler)
        // 追加
        todoHandler = disableCors(todoHandler)
    }
    app.handlers["/api/technologies"] = techHandler
    // 追加
    app.handlers["/api/todos"] = todoHandler
    app.handlers["/"] = http.FileServer(http.Dir("/webapp")).ServeHTTP
    return app
}

func (a *App) Serve() error {
    for path, handler := range a.handlers {
        http.Handle(path, handler)
    }
    log.Println("Web server is available on port 8080")
    return http.ListenAndServe(":8080", nil)
}

func (a *App) GetTechnologies(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    technologies, err := a.d.GetTechnologies()
    if err != nil {
        sendErr(w, http.StatusInternalServerError, err.Error())
        return
    }
    err = json.NewEncoder(w).Encode(technologies)
    if err != nil {
        sendErr(w, http.StatusInternalServerError, err.Error())
    }
}

// 追加
func (a *App) Todos(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case http.MethodGet:
        w.Header().Set("Content-Type", "application/json")
        todos, err := a.d.GetTodos()
        if err != nil {
            sendErr(w, http.StatusInternalServerError, err.Error())
            return
        }
        err = json.NewEncoder(w).Encode(todos)
        if err != nil {
            sendErr(w, http.StatusInternalServerError, err.Error())
        }
    case http.MethodPost:
        body := r.Body
        defer body.Close()
        buf := new(bytes.Buffer)
        io.Copy(buf, body)

        var todo TodoForm
        json.Unmarshal(buf.Bytes(), &todo)
        err := a.d.PostTodos(todo.Text)
        if err != nil {
            sendErr(w, http.StatusInternalServerError, err.Error())
            return
        }

    case http.MethodDelete:
        err := a.d.DeleteTodos(r.FormValue("id"))
        if err != nil {
            sendErr(w, http.StatusInternalServerError, err.Error())
            return
        }
    }
}

func sendErr(w http.ResponseWriter, code int, message string) {
    resp, _ := json.Marshal(map[string]string{"error": message})
    http.Error(w, string(resp), code)
}

// Needed in order to disable CORS for local developazsment
func disableCors(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Access-Control-Allow-Origin", "*")
        w.Header().Set("Access-Control-Allow-Methods", "*")
        w.Header().Set("Access-Control-Allow-Headers", "*")
        h(w, r)
    }
}

db/db.go
package db

import (
    "context"
    "log"
    "my-app2/model"

    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/bson/primitive"
    "go.mongodb.org/mongo-driver/mongo"
)

type DB interface {
    GetTechnologies() ([]*model.Technology, error)
    // 追加
    GetTodos() ([]*model.Todo, error)
    PostTodos(string) error
    DeleteTodos(string) error
}

type MongoDB struct {
    collection *mongo.Collection
    // 追加
    todoCl *mongo.Collection
}

func NewMongo(client *mongo.Client) DB {
    tech := client.Database("tech").Collection("tech")
    // 追加
    todo := client.Database("todo").Collection("todo")
    return MongoDB{collection: tech, todoCl: todo}
}

func (m MongoDB) GetTechnologies() ([]*model.Technology, error) {
    res, err := m.collection.Find(context.TODO(), bson.M{})
    if err != nil {
        log.Println("Error while fetching technologies:", err.Error())
        return nil, err
    }
    var tech []*model.Technology
    err = res.All(context.TODO(), &tech)
    if err != nil {
        log.Println("Error while decoding technologies:", err.Error())
        return nil, err
    }
    return tech, nil
}

// 追加
func (m MongoDB) GetTodos() ([]*model.Todo, error) {
    res, err := m.todoCl.Find(context.TODO(), bson.M{})
    if err != nil {
        log.Println("Error while fetching todos:", err.Error())
        return nil, err
    }
    var todo []*model.Todo
    err = res.All(context.TODO(), &todo)
    if err != nil {
        log.Println("Error while decoding todos:", err.Error())
        return nil, err
    }
    return todo, nil
}

// 追加
func (m MongoDB) PostTodos(text string) error {
    mdl := model.Todo{ID: primitive.NewObjectID(), Text: text}
    _, err := m.todoCl.InsertOne(context.TODO(), mdl)
    if err != nil {
        log.Println("Error while posting todos:", err.Error())
        return err
    }
    return nil
}

// 追加
func (m MongoDB) DeleteTodos(id string) error {
    objectID, _ := primitive.ObjectIDFromHex(id)
    _, err := m.todoCl.DeleteOne(context.TODO(), bson.M{"_id": objectID})
    if err != nil {
        log.Println("Error while deleting todos:", err.Error())
        return err
    }
    return nil
}

また、modelパッケージにtodo.goを追加しておきます。

model/todo.go
package model

import "go.mongodb.org/mongo-driver/bson/primitive"

// 追加
type Todo struct {
    ID   primitive.ObjectID `json:"id" bson:"_id"`
    Text string             `json:"text" bson:"text"`
}

この状態で、非常にシンプルではありますが、
DBへの参照・登録・削除機能を持ったTODOアプリを起動することができます。

goxygen.gif

※ソースの全量はこちらに。

まとめ

コマンド一発でDB含めた環境が準備でき、かつサンプルコードがついてくるので、それをもとにしてwebアプリを作っていくことができそうです。
golangをベースに新規Webアプリを作ろうとしている人には、とても便利なツールだと思います。
現在、起動時の引数でAngular/React/Vueを選択することができますが、その他、DBが選べたりできるようになると、もっと汎用的に使えそうですね。

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2