yikegaya’s blog

yikegayaのブログ

Nuxt.js触ってみた

Nuxt.js(Vue.jsベースのフロントエンドフレームワーク)が気になったので軽く触ってみた。

ググって調べてもいまいち物足りなかったのでUdemyのセールで講座買って受講してみた。セールなら安いし4時間で終わるので気軽にできてよい感じ。ただライブラリのバージョン違いで動かないところがあったりFirebaseのUIが変わってたりはしたけどまあしょうがない。

https://www.udemy.com/course/nuxtjs-the-complete-guide/

感想

VueにはVue CLIっていうコマンド叩くとプロジェクトのディレクトリ自動で作ってくれて、さらによく使われるライブラリや設定最初から揃えてくれる便利ツールがあるけど、Nuxtもそれに似たイメージでcreate-nuxt-app {project name}を実行すると一気にプロジェクトの雛形を作ってくれる。

例えばTypescript、axios、テストフレームワーク、リントツールなどを一発でインストールできる。最近Next.jsのプロジェクトでTypescript、Jest、ESLintと設定していったら結構インストールしなくてはいけないライブラリが多くて結構な時間食ったのでこの仕組みは良さそう。

Vuexも最初から入っていてフォルダもできてるのですぐ使える。

後はNext.jsと同じようにpagesフォルダ以下にファイルつくると自動的にRoutingしてくれたり、Layoutファイルやエラーページの仕組みが最初からできていてVue CLIらしい便利さに加えてフレームワークらしい仕組みが追加されている感じ。

Vue自体はとりあえずCDNで読み込んでプレーンなhtmlファイルにjQuery感覚でさくっと組み込めるライブラリ。であってフレームワークじゃないっていうのは知識として知ってはいたけどこうして触ってみるとよくわかった。

Typescript+Next.js+Goで作ったサービスが大体できてきた

ポートフォリオとしてフロントエンドをTypescript+Next.js、バックエンドをGolangで作っていた映画情報を取得、レビューできるWebサービスの基本機能の実装が大体できてきた。映画情報の取得にはTMDBのAPIを使用している。

基本機能はできたもののやりたいことはまだ諸々あり。

github.com

作った機能

  • ユーザ登録、サインイン、ログイン、ログアウト機能
  • 公開中、人気の映画を取得する機能
  • 映画のレビュー機能(スコアとレビュー文章、レビューしたユーザ情報、レビュー日時を登録)
  • 映画の検索機能
  • スマホ表示対応
  • ESLintでのコードチェック
  • JestとEnzymeでのテストコード実装

今後やりたいこと

  • デプロイ対応(どうやるかは未定。Dockerの勉強がてらECSとかKubenetesとかでやってみようかと)
  • 細かいUI改善
  • CI、CD構築
  • storybook導入
  • Golangのテスト
  • Golint
  • マイページ作成
  • 一部でもいいからGraphQL使ってみる。今は普通にREST

もう少し詰めたいこと

  • Goで作ったAPIサーバの設計(パッケージに分ける必要あるのかとか、なんとなくテストがしづらいとか、clean architectureっぽくしたいとか)
  • Component設計

今後

ぼちぼちメンテしつつNuxt.js+Firebaseで作りたいものがあったりもするので並行して進める予定

Vuexの勉強をしてみる

vue.js入門の7章のタスク管理アプリケーションを写経しながらVuexの勉強をしてみる

  • App.vueのtemplate部分

ここはVuexを使わない場合と何も変わらない。ディレクティブでVueインスタンスに指示を送っているだけ。

<template>
  <div id="app">
    <h2>タスク一覧</h2>
    <ul>
      <li v-for="task in tasks" v-bind:key="task.id">
        <input type="checkbox" v-bind:checked="task.done"
          v-on:change="toggleTaskStatus(task)">
        {{ task.name }}
        -
        <span v-for="id in task.labelIds" v-bind:key="id">
          {{ getLabelText(id) }}
        </span>
      </li>
    </ul>
    <form v-on:submit.prevent="addTask">
      <input type="text" v-model="newTaskName" placeholder="新しいタスク">
    </form>

    <h2>ラベル一覧</h2>
    <ul>
      <li v-for="label in labels" v-bind:key="label.id">
        <input type="checkbox" v-bind:value="label.id" v-model="newTaskLabelIds">
        {{ label.text }}
      </li>
    </ul>
    <form v-on:submit.prevent="addLabel">
      <input type="text" v-model="newLabelText" placeholder="新しいラベル">
    </form>

    <h2>ラベルでフィルタ</h2>
    <ul>
      <li v-for="label in labels" v-bind:key="label.id">
         <input type="radio" v-bind:checked="label.id === filter"
          v-on:change="changeFilter(label.id)">
          {{ label.text }}
      </li>
      <li>
        <input type="radio" v-bind:checked="filter == null" v-on:chenge="changeFilter(null)">
        フィルタしない
      </li>
    </ul>

    <h2>保存と復元</h2>
    <button type="button" v-on:click="save">保存</button>
    <button type="button" v-on:click="restore">復元</button>
  </div>
</template>
  • App.vueのscriptタグ内

script部分のcomputed、method部分でVuexのstoreに対するデータの取得、更新処理を書いている。更新部分にcommitとdispatchの2パターンがあるのがややこしいけど

  • ミューテーション(通常のstate更新)を行う際はcommit
  • アクション(非同期処理や外部APIとのやり取り)を行う際はdispatch

と分けて使う。ゲッター(stateの取得処理)とミューテーションを基本的には使ってカバーできないものをアクションで実装するらしい。

<script>
export default {
  data () {
    return {
      newTaskName: '',
      newTaskLabelIds: [],
      newLabelText: ''
    }
  },
  computed: {
    tasks () {
      return this.$store.getters.filteredTasks
    },
    labels () {
      return this.$store.state.labels
    },
    filter () {
      return this.$store.state.filter
    }
  },
  methods: {
    addTask () {
      this.$store.commit('addTask',  {
        name: this.newTaskName,
        labelIds: this.newTaskLabelIds
      })
      this.newTaskName = ''
      this.newTaskLabelIds = []
    },

    toggleTaskStatus (task) {
      this.$store.commit('toggleTaskStatus', {
        id: task.id
      })
    },

    addLabel () {
      this.$store.commit('addLabel', {
        text: this.newLabelText
      })
      this.newLabelText = ''
    },

    getLabelText(id) {
      const label = this.labels.filter(label => label.id === id)[0]
      return label? label.text : ''
    },

    changeFilter (labelId) {
      this.$store.commit('changeFilter', {
        filter: labelId
      })
    },

    save () {
      this.$store.dispatch('save')
    },

    restore () {
      this.$store.dispatch('restore')
    }
  }
}
</script>
  • Vuexのstore部分

storeはstate(アプリケーションの状態)を管理する部分でこれがVuexの根幹。

Vueインスタンスにdataやmethods、computedなどのoptionを書いていくのと同じような感じでstate(Vueインスタンスのdataのようなもの)と上記のgetters、mutations、actionsを書いていく。

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    tasks: [
      {
        id: 1,
        name: '牛乳を買う',
        labelIds: [1, 2],
        done: false
      },
      {
        id: 2,
        name: 'Vue.jsの本を買う',
        labelIds: [1, 3],
        done: true
      }
    ],
    labels: [
      {
        id: 1,
        text: '買い物'
      },
      {
        id: 2,
        text: '食料'
      },
      {
        id: 3,
        text: '本'
      }
    ],
    nextTaskId: 3,
    nextLabelId: 4,

    filter: null
  },
  getters: {
    filteredTasks (state) {
      if(!state.filter) {
        return state.tasks
      }

      return state.tasks.filter(task => {
        return task.labelIds.indexOf(state.filter) >= 0
      })
    }
  },
  mutations: {
    addTask (state, { name, labelIds }) {
      state.tasks.push({
        id: state.nextTaskId,
        name,
        labelIds,
        done: false
      })
      state.nextTaskId++
    },

    toggleTaskStatus (state, { id }) {
      const filtered = state.tasks.filter(task => {
        return task.id === id
      })

      filtered.forEach(task => {
        task.done = !task.done
      })
    },

    addLabel (state, { text }) {
      state.labels.push({
        id: state.nextLabelId,
        text
      })
      state.nextLabelId++
    },

    changeFilter (state, { filter }) {
      state.filter = filter
    },

    restore(state, { tasks, labels, nextTaskId, nextLabelId })
    {
      state.tasks = tasks
      state.labels = labels
      state.nextTaskId = nextTaskId
      state.nextLabelId = nextLabelId
    }
  },

  actions: {
    save ({ state }) {
      const data = {
        tasks: state.tasks,
        labels: state.labels,
        nextTaskId: state.nextTaskId,
        nextLabelId: state.nextLabelId
      }
      localStorage.setItem('task-app-data', JSON.stringify(data))
    },

    restore ({ commit }) {
      const data = localStorage.getItem('task-app-data')
      if (data) {
        commit('restore', JSON.parse(data))
      }
    }
  }
})

export default store

あとはstoreのモジュール分割やRouterとの連携などもVue.js入門のこの後のページで記載されているが、Vuexの基本的なところは

  • Vueインスタンスのdataで保持していた部分をstoreのstateに切り出してgetter、computed、actionで使う。
  • 使いどころはユーザ情報などのコンポーネントを跨いだグローバルなデータ

ということが分かっていればとりあえず良いかな?

Vue.jsの勉強再開した

しばらく仕事でも個人開発でもReact使い続けてきたけど、Vue.jsも気になってきたので勉強し始めた。

とはいえ全く触ったことがないわけではなく、以前の職場で少しだけ触ったことがあって、その時に一応一通り勉強はしていた。

とりあえず本棚から昔買った本を引っ張り出して写経はせず一通りざっと読んでみる。

www.amazon.co.jp

加えてudemyの講座をこっちは全コード写経しながら受講した。

www.udemy.com

これで何となく感じは掴めた気がする。今個人開発で作ってるポートフォリオをもう少し触ったら今度はVueで何かしらを作ってみようと思う。

Vueは学習コストが低いのが人気の理由みたいだけどそれはとりあえずjQueryの代わりに差し込んだり、とりあえず使ってみるのには手軽ってことであって、VuexとかVue routerも使ってコンポーネント設計とか状態管理とかSPA、SSRやらwebpackerとか勉強してったらそれは当然学習コスト高くなりそう。

ただこの辺りは他のJSフレームワークと共通なので、何かしら経験していたらディレクティブとVueインスタンスのoptionを一通り使えるようになれば大丈夫かな?

reactでレビュー機能を実装する

こういう↓星付きのレビュー機能をReactのポートフォリオに実装した時のメモ。

CSSフレームワークbulmaのモーダルで動かしている。

f:id:ikeyu0806720:20200807105827p:plain

const [score, setScore] = useState<number>(3)
const [isShowModal, setIsShowModal] = useState<boolean>(false)

return (
<div className={isShowModal ? "modal is-active" : "modal"}>
<div className="modal-background"></div>
  <div className="modal-card">
    <header className="modal-card-head">
      <p className="modal-card-title">{movie.title}</p>
      <button className="delete" aria-label="close" onClick={closeModal}></button>
    </header>
    <section className="modal-card-body">
    <div className="field">
      <label className="label">感想</label>
      <div className="control">
        <textarea className="textarea" onChange={(e) => { setComment(e.target.value)}}></textarea>
      </div>
    </div>
    <div className="field rate-field columns star">
      <a className={(score >= 1) ? "yellow-star" : "silver-star"} onClick={() => setScore(1)}>★</a>
      <a className={(score >= 2) ? "yellow-star" : "silver-star"} onClick={() => setScore(2)}>★</a>
      <a className={(score >= 3) ? "yellow-star" : "silver-star"} onClick={() => setScore(3)}>★</a>
      <a className={(score >= 4) ? "yellow-star" : "silver-star"} onClick={() => setScore(4)}>★</a>
      <a className={(score >= 5) ? "yellow-star" : "silver-star"} onClick={() => setScore(5)}>★</a>
    </div>
    </section>
    <footer className="modal-card-foot">
      <button className="button is-success" onClick={submitReview}>投稿する</button>
      <button className="button" onClick={closeModal}>キャンセル</button>
    </footer>
  </div>
</div>
)
      <style jsx>{`
        .star {
          position: relative;
          font-size: 30px;
          letter-spacing : 0px;
        }
        .yellow-star {
          color: yellow;
        }
        .silver-star {
          color: silver;
        }
        .comments {
          margin-top: 20px;
        }
        .reviews {
          margin-top: 20px;
        }
        .review-list {
          margin-bottom: 20px;
        }
        .review-comment {
          margin-bottom: 30px;
          margin-left: 10px;
        }
        .reviewed-stars {
          margin-left: 10px;
        }
        .reviewed-star {
          position: relative;
          font-size: 15px;
          letter-spacing : 0px;
        }
      `}</style>

後はaxiosでstateをバックエンドにpostしてDB登録しておく。

  const params: Review = {
    movie_id: movie.id,
    public_id: Math.floor( Math.random() * (999999)),
    comment: comment,
    score: score,
  }

  const submitReview = () => {
    axios.post('http://localhost:3002/review/create', params)
    .then((response) => {
      console.log(response)
      router.push({
        pathname: '/Movie/' + movie.id,
        query: { review: 'success' }
     })
    })
    .catch((error) => {
      console.log(error)
    })
  }

axiosのpostリクエストをginで受け取る

Reactでaxiosからgolang+ginのAPIサーバにpostリクエストを送る時にこんな感じ↓でaxiosからpostされてきた値を受け取ろうとしてうまくいかなかった。

ginで作ったサーバサイド

func (pc Controller) Login(c *gin.Context) {

  param_name := c.PostForm("name")
  param_password := c.PostForm("password")

}

reactのフロントエンド

  const [name, setName] = useState<string>("")
  const [email, setEmail] = useState<string>("")
  const [password1, setPassword1] = useState<string>("")

  const params: User = {
    name: name,
    email: email,
    password: password1
  }
axios.post('http://localhost:3002/login', params

ginでc.ShouldBindJSON(&u)の形式だと受け取れてるっぽいけど何がダメなのか?

解決策

  const [name, setName] = useState<string>("")
  const [password1, setPassword1] = useState<string>("")

  const params = new URLSearchParams();
  params.append('name', name)
  params.append('password', password1)

axios.post('http://localhost:3002/login', params)

paramをURLSearchParamsから作成しないとPostFormで受け取れない

Goの独自パッケージimportメモ

個人開発でGo+Gin+GormでAPIサーバを作ろうとした際に、コードを独自パッケージに切り出した。

パッケージの扱いが多言語とちょっと違って戸惑ったのでメモ。

やろうとしたこと

最初main.goに書いていたGormでのMySQLへの接続処理をdbパッケージに、Ginで書いたAPIサーバをserverパッケージに切り出した。

それをmain.goにimportしようとしたら思いの外苦戦してしまった。

import先のパス指定方法

独自パッケージのimport方法はやり方がいくつかあり

  • 相対パス(import ../db)でimportするか
  • 独自パッケージのパスにgo.modを置いて「import "local.packages"」と指定してimportする
  • github.com/{アカウント名}/{リポジトリ名}を指定してimportする

jsとかの感覚だと相対パスでやってしまいたくなるけど、github.com/{アカウント名}/{リポジトリ名}を指定したimportの仕方が推奨っぽいのでそのやり方で進めた。

あと独自パッケージのパスにgo.modを置くやり方でもimportできたけどこっちも非推奨らしい

ローカルパッケージとしてのimportはまず以下のようにgo.modを作って

module movie-info/ikeyu0806/db

go 1.14

replace local.packages/db => ./db

で、main.goで独自パッケージをimportする

import "local.packages/db"

これでも動いたけど、最終的にはgithubのパス指定してimportする形にした。

package main

import (
    "github.com/ikeyu0806/movie-info-backend/db"
    "github.com/ikeyu0806/movie-info-backend/server"
)

func main() {
    db.Init()
    server.Init()
}

GOPATHのこと

このやり方(github.com/{アカウント名}/{リポジトリ名}の形式)でimportするには - githubに同じパスでpushする or - GOROOT以下のパスを合わせる

のどっちかが必要で、pushせずに作業するには GOROOT/github.com/{アカウント名} の直下にプロジェクトをおかないといけない。

最初GOROOTに合わせる形で進めたけど作業パスここまで深くするのはなんか違和感があった。 dockerの中で開発全てやってしまうのであれば問題ないんだろうけど。

あとgoをinstallした時にできる環境変数としてGOROOTというものがあってややこしいけど、こっちは変更してはいけない(うっかり変更したらgo buildがエラーになった)

感想

他の言語の感覚だと相対パス指定でimportしたくなるし独自パッケージ作るたびにいちいちgithubにpushしたり作業パスをgithubに合わせたりするの大分違和感あるけどこの辺の感覚は慣れなのかな?

Gemとかnpmみたいにライブラリを配布するサーバがないところもGo特有っぽい。

参考

今回の作業のgithubリポジトリ

https://github.com/ikeyu0806/movie-info-backend