前置き
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 で下記のようなサンプルページにアクセスできると思います。
動かしてみる(ローカル)
ローカルでの動作させる場合は、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
にまとめて記載していますが、本来はコンポーネントに分けて実装されることになります。
<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時のサンプルコードを元にしているので、サンプルコードに追記する形で実装しました。
追記箇所には、//追加
のコメントアウトを入れています。
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)
}
}
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を追加しておきます。
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アプリを起動することができます。
※ソースの全量はこちらに。
まとめ
コマンド一発でDB含めた環境が準備でき、かつサンプルコードがついてくるので、それをもとにしてwebアプリを作っていくことができそうです。
golangをベースに新規Webアプリを作ろうとしている人には、とても便利なツールだと思います。
現在、起動時の引数でAngular/React/Vueを選択することができますが、その他、DBが選べたりできるようになると、もっと汎用的に使えそうですね。