#はじめに
はじめまして。Qiitaの公開投稿は初めてになります。satoruと申します。
今回の記事では、Webエンジニアになりたいけれど未経験だからとにかくポートフォリオをつくらないといけないよなということでtodoSNSというWebサービスを作った(作っている)際の実装内容やそれまでに思ったことを個人的なアウトプットとして述べていこうと思います。
(test@test.com pass:testtest or Google認証では入れます)
流れとしては、自己紹介、実装にかかるまでの準備(利用技術や設計)、実装のはじめからおわり、開発中に思ったこと、まとめで書き連ねていきます。
※未熟ながら書いているため、指摘箇所等あればコメントいただけると助かります。
#自己紹介
大学で情報系の学部を卒業し、SIer会社に就職して約1年半で退職し、紹介で建築事務所の社内SEとしてGSuiteの運用・管理を行っている。
約半年ほど前にものづくりしたいと思い始め、プログラミングを再度勉強し始めて今に至る。
#実装にかかるまでの準備(利用技術)
今回、利用したのはNuxt.js、Buefy、ESLint、Docker、CircleCI、GCP(Firebase)、(GitHub)になります。
##Nuxt.js
Nuxt.jsはVue.jsアプリケーションを構築するためのフレームワーク
(理由)プログラミングを再度勉強し始めてProgateで一通り勉強したあとに何かしらフレームワークは学習する必要があるので人気のあるVue.jsを選択、できるだけモダンな環境に触れるため。
##Buefy
BuefyはBulmaとVue.jsを組み合わせたUIコンポーネント
(理由)todoSNSの設計したときになにか参考になるようなUIコンポーネントがないか探していたときに良いと思ったため。
##ESLint
ESLintはJavaScriptのための静的検証ツール
(理由)コードの一貫性を保てるように、バグの発見、またCircleCIに組み込みたかったため。またNuxt.jsに追加が簡単。
##Docker
Dockerはコンテナ型仮想環境
(理由)業務でよく使われている技術で、現場を意識して構築してみようと思ったため。
##CircleCI
CircleCIはSaas型のCI/CDサービス
(理由)業務でよく使われている技術で、現場を意識して構築してみようと思ったため。
##GCP(GoogleCloudPlatform)
GCPはGoogleが提供しているクラウドコンピューティングサービス
(理由)クラウドサービスを利用する経験を積むことと、このアプリ開発の前にFirebaseで実装したアプリがありその復習も兼ねたため。
##やりたかったこと
Dockerで環境構築を行い、GitHubでメンターの方にレビューをお願いし、マージ可能になったらCircleCIで自動デプロイをおこす実際の業務に似たような環境を作りたかった。
#実装にかかるまでの準備(設計)
まず自分の頭の中にあるイメージしてるものを簡単にアウトプットするために以下のER図や画面設計をCacoo作成しました。
Firebaseのデータベースをまず作成し
signin画面の作成
home画面の作成
プロフィール画面の作成を行いました。
#機能要件
設計が終わったあとは機能要件を考えました。先程の画面設計に載せようとした機能が以下になります。
【signin】
・ログイン機能
・ユーザ新規登録機能(Googleも)
・ログイン後はhomeへ遷移
【home】
・ToDo追加機能
・ToDoを追加時に非公開にできる
・自分のToDoの削除と更新機能
・自分のコメントの削除と更新機能(実装中)
・homeでは、自身のToDoリストが表示
・homeでは、自身のおきにいりToDoリストが表示
・homeでは、Followしている人のToDoが表示(半実装中)
・ユーザ検索機能
・ToDoに対してのアクション(いいねやコメント)
・ユーザを選択し、Followを選択できる機能(モーダルで)
・もっとみるでmyprofileに遷移
【myprofile】
・自分のプロフィールの変更(実装中)
・上記のToDo機能
・タグによるソート・フィルタ機能
・homeよりおおくの自分のToDoを確認可能
・タブで自分のToDoとfavのToDoを切り替える機能
【その他】
・ログアウトボタン機能
・ログアウトされるとsigninへ遷移
・ログアウトされた状態でsignin以外へアクセスすると、ログイン画面へリダイレクト
#実装のはじめからおわり
・環境構築(Docker、GCP、CircleCI)
・コーディング(Nuxtのディレクトリ構造ごと)
|- components
|- layouts
|- middleware
|- pages
|- plugins
|- store
の流れでまとめていきます。
また現時点で最新のソースコード以下になります。
(https://github.com/noguchisatoru/todo_sns/tree/feature/search)
##環境構築(Docker)
DockerでNuxt.jsの環境構築を参考にさせていただきました。
FROM node:10.15.3-alpine
WORKDIR /app
RUN apk update && \
npm install -g npm && \
npm install -g @vue/cli && \
npm install -g @vue/cli-init
ENV HOST 0.0.0.0
EXPOSE 3000
CMD ["/bin/ash"]
version: '3'
services:
web:
build: .
ports:
- 3000:3000
volumes:
- .:/app
stdin_open: true
tty: true
ビルド、コンテナの作成をし、コンテナ内に入り、Nuxtのプロジェクトを作成しました。
##環境構築(GCP・CircleCI)
・Nuxt.js アプリケーションを CircleCI 経由で App Engine Standard 環境へと自動デプロイする
・CircleCIを使ってNuxt.jsをGitHub Pagesへデプロイする
基本的な設定は上記の記事を参考にしました。
runtime: nodejs10
instance_class: F2
handlers:
- url: /_nuxt
static_dir: .nuxt/dist/client
secure: always
- url: /(.*\.(gif|png|jpg|ico|txt))$
static_files: static/\1
upload: static/.*\.(gif|png|jpg|ico|txt)$
secure: always
- url: /.*
script: auto
secure: always
env_variables:
HOST: '0.0.0.0'
NODE_ENV: 'production'
###package.jsonの修正
- "start": "nuxt start",
+ "start": "PORT=8080 HOST=0.0.0.0 nuxt start",
###CircleCIの作成
CircleCI の設定の追加とCircleCIの実行ファイルを用意するを参考
version: 2
jobs:
build:
working_directory: ~/app
docker:
- image: circleci/node:10.15.3
steps:
- checkout
- restore_cache:
keys:
- npm-{{ checksum "package.json" }}
- run: npm install
- save_cache:
paths:
- node_modules
key: npm-{{ checksum "package.json" }}
- run: npm run lint
deploy:
working_directory: ~/app
docker:
- image: google/cloud-sdk:217.0.0-alpine
steps:
- checkout
- run: apk add --no-cache nodejs npm yarn
- run: echo $ENV_FILE | base64 -d > .env
- run: yarn
- run: yarn build
- run: echo $GCLOUD_SERVICE_KEY > ${HOME}/gcloud-service-key.json
- run: gcloud auth activate-service-account --key-file=${HOME}/gcloud-service-key.json
- run: gcloud app deploy --quiet --project endless-force-252718
workflows:
version: 2
main:
jobs:
- build
- deploy:
filters:
branches:
only: develop
Firebaseを使いたかったのでdotenvファイルの作成を追加しています。
##コーディング
上記の環境構築後、コーディングをはじめました。
コーディングの流れはログイン画面→home画面→myprofile画面
###components
10月29日時点のコンポーネントファイル数は14つあります。ページ数が現状3ページのため、もし今後ページが増えていくのであるならば繰り返し使っていくであろう要素ごとに分けてみています。pagesの項目に関わりますが、できる限りpagesのファイル一つ一つは簡略化させ、コンポーネントの修正だけですむように考えてみました。
<template>
<section>
<article v-for="mytodo in mytodosfilter(user.uId)" :key="mytodo.uId" class="media">
<div class="media-content">
<div class="content" @click="isImageModalActive = true, setTodo(mytodo)">
<p>
{{ mytodo.text }}
</p>
</div>
<nav class="level is-mobile">
<div class="level-left">
<p>{{ mytodo.tag }}</p>
</div>
<div class="level-right">
<div v-show="mytodo.state === '完了'" class="media-right">
<b-button type="is-primary" size="is-small" @click="statusChange('アーカイブ',mytodo.documentId)">
アーカイブ
</b-button>
</div>
<b-dropdown aria-role="list">
<button class="button is-small" slot="trigger">
<span>{{ mytodo.state }}</span>
<b-icon icon="menu-down" />
</button>
<b-dropdown-item aria-role="listitem" @click="statusChange('作業中',mytodo.documentId)">
作業中
</b-dropdown-item>
<b-dropdown-item aria-role="listitem" @click="statusChange('完了',mytodo.documentId)">
完了
</b-dropdown-item>
<b-dropdown-item aria-role="listitem" @click="statusChange('予定',mytodo.documentId)">
予定
</b-dropdown-item>
</b-dropdown>
</div>
</nav>
</div>
</article>
<b-modal :active.sync="isImageModalActive">
<div class="card box">
<article class="media">
<div class="media-content">
<div class="content">
<p>更新前:{{ todo.text }}</p>
<b-input v-model="text" maxlength="100" type="textarea" />
</div>
<nav class="level">
<div class="level-left" />
<div class="level-right">
<p>
投稿時:{{ todo.createdAt }}
</p>
<b-switch
v-model="release"
true-value="公開"
false-value="非公開"
>
{{ release }}
</b-switch>
<b-button @click="todoUpdate(text, release, todo.documentId)">
更新
</b-button>
</div>
</nav>
</div>
</article>
</div>
</b-modal>
</section>
</template>
<script>
import dayjs from 'dayjs'
import { mapState, mapGetters, mapMutations } from 'vuex'
export default {
data () {
return {
isImageModalActive: false,
release: '公開',
text: ''
}
},
computed: {
...mapState({
user: state => state.user.user,
todo: state => state.todo.todo,
todos: state => state.todo.todos
}),
...mapGetters({ mytodosfilter: 'todo/mytodosfilter',
todotest: 'todo/todos' })
},
mounted () {
this.$store.dispatch('todo/initTodos')
},
methods: {
...mapMutations({
setTodo: 'todo/setTodo'
}),
statusChange (status, id) {
this.$store.dispatch('todo/statusChange', { state: status, documentId: id })
},
async todoUpdate (textUpdated, releaseUpdated, id) {
try {
dayjs.locale('ja')
await this.$store.dispatch('todo/todoUpdate', { text: textUpdated, release: releaseUpdated, createdAt: dayjs().format('YYYY/MM/DD/HH:mm:ss'), documentId: id })
this.isImageModalActive = false
alert('更新しました')
this.text = ''
} catch (e) {
alert(e)
}
}
}
}
</script>
###layouts
Nuxt.jsに組み込んだBulmaでスティッキーフッター (sticky footer) を実現する
を参考にしました。
signin以外のページに適応させるlayoutを作成しました。
BuefyだけだとFooterが浮いてしまうためstyleの設定を行っています。
<template>
<div class="sf-site-all">
<Navbar />
<nuxt class="sf-site-content" />
<Bottom />
</div>
</template>
<script>
import Navbar from '~/components/Navbar.vue'
import Bottom from '~/components/Bottom.vue'
export default {
components: {
Navbar,
Bottom
}
}
</script>
<style>
.sf-site-all {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.sf-site-content {
flex: 1;
}
</style>
###middleware
機能要件[その他]のリダイレクト機能の実装部分です。
ログイン状態はvuexに保存するため、そこを参照しログインか非ログインかを判別しています。
export default function ({ store, redirect }) {
const loginUser = store.getters['user/user']
if (!loginUser) {
return redirect('/')
}
}
###pages
できる限りpagesのファイル一つ一つは簡略化させ、コンポーネントの修正だけですむように考えてみました。
<template>
<div class="container">
<div class="tile is-ancestor">
<div class="tile is-5 is-vertical is-parent">
<div class="tile is-child box">
<Profilecard />
<div class="tile is-child box">
<p class="title">
MyToDo
</p>
<Mytodo />
<div>
<Morebutton />
</div>
</div>
</div>
</div>
<div class="tile is-vertical is-parent">
<div class="tile is-child">
<Todoinput />
<Todoarticle />
</div>
</div>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
import Profilecard from '~/components/Profilecard.vue'
import Mytodo from '~/components/Mytodo.vue'
import Todoinput from '~/components/Todoinput.vue'
import Todoarticle from '~/components/Todoarticle.vue'
import Morebutton from '~/components/Morebutton.vue'
export default {
middleware: 'authenticated',
layout: 'homelayout',
components: {
Profilecard,
Mytodo,
Todoinput,
Todoarticle,
Morebutton
},
data () {
return {
checkboxGroup: []
}
},
computed: {
...mapState({
user: state => state.user.user
})
},
mounted () {
this.$store.dispatch('todo/initTodos')
this.$store.dispatch('favorite/initFavorites', this.user.uId)
}
}
</script>
###plugins
基本はFirebaseのSDKを利用しています。configのデータは不特定多数に見られたくないため.envを利用しています。
import firebase from 'firebase'
const firebaseConfig = {
apiKey: process.env.FB_API_KEY,
authDomain: process.env.FB_AUTH_DOMAIN,
databaseURL: process.env.FB_DATABASE_URL,
projectId: process.env.FB_PROJECTID,
storageBucket: process.env.FB_STORAGE_BUCKET,
messagingSenderId: process.env.FB_MESSAGING_SENDER_ID,
appId: process.env.FB_APP_ID
}
// Initialize Firebase
if (!firebase.apps.length) {
firebase.initializeApp(firebaseConfig)
}
const db = firebase.firestore()
const auth = firebase.auth
export { db, auth }
###store
モジュールモードにすることでstoreの管理をしやすくしています。
データベースの要素ごとに作成しています。
import { firestoreAction } from 'vuexfire'
import { db } from '~/plugins/firebase'
const usersRef = db.collection('Users')
const followerRef = db.collection('Followers')
const favoritesRef = db.collection('Favorites')
export const state = () => ({
user: null,
users: [],
followers: []
})
export const mutations = {
setUser (state, user) {
state.user = user
},
logoutUser (state) {
state.user = null
}
}
export const getters = {
user: state => state.user,
users: state => state.users,
followers: state => state.followers
}
export const actions = {
initUser: firestoreAction((context) => {
return context.bindFirestoreRef('users', usersRef)
}),
addUser: firestoreAction(async (context, userdata) => {
try {
await usersRef.doc(userdata.uId).set({
userName: userdata.userName,
userIntroduction: 'test',
createdAt: userdata.createdAt,
imageColor: 'blue'
})
await followerRef.doc(userdata.uId).set({
followingUserId: []
})
await favoritesRef.doc(userdata.uId).set({
favoriteIds: []
})
} catch (e) {
alert(e)
}
}),
setUserdata: async ({ commit }, uid) => {
try {
const userdata = await usersRef.doc(uid).get()
commit('setUser', {
uId: uid,
userName: userdata.data().userName,
userIntroduction: userdata.data().userIntroduction,
createdAt: userdata.data().createdAt,
imageColor: userdata.data().imageColor
})
} catch (e) {
console.log(e)
}
},
logoutUser: ({ commit }) => {
commit('logoutUser')
}
}
###nuxt.config.js
pluginsのfirebase.jsとESLint、buefy、dotenvを追加しています。
export default {
mode: 'universal',
/*
** Headers of the page
*/
head: {
title: process.env.npm_package_name || '',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: process.env.npm_package_description || '' }
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
{ rel: 'stylesheet', href: 'https://use.fontawesome.com/releases/v5.2.0/css/all.css' }
]
},
/*
** Customize the progress-bar color
*/
loading: { color: '#fff' },
/*
** Global CSS
*/
css: [
],
/*
** Plugins to load before mounting the App
*/
plugins: [
'@/plugins/firebase'
],
/*
** Nuxt.js dev-modules
*/
buildModules: [
// Doc: https://github.com/nuxt-community/eslint-module
'@nuxtjs/eslint-module'
],
/*
** Nuxt.js modules
*/
modules: [
// Doc: https://bootstrap-vue.js.org/docs/
'nuxt-buefy',
// Doc: https://axios.nuxtjs.org/usage
'@nuxtjs/axios',
'@nuxtjs/dotenv'
],
/*
** Axios module configuration
** See https://axios.nuxtjs.org/options
*/
axios: {
},
/*
** Build configuration
*/
build: {
/*
** You can extend webpack config here
*/
extend (config, ctx) {
}
}
}
#開発中に思ったこと
あげようと思ったらきりがないですが、開発中や勉強中におもったことを上げていきます。あたりまえのことも書いていきます。
##問題の細分化
なにかを作ると考えたときに、頭の中だけで考えようとすると完成形までの道のりがかなり遠く感じてしまい途方にくれていましたが、きちんと設計して機能要件を上げていくことだけでも見通しがつくし、先に進んでいることがわかるためモチベーションの維持にもつながると思った。
##コンポーネントの難しさ
最初はNuxtでコンポーネントを作成していくと、コードの管理もしやすいしそうコード量も減って便利だなと思ったが、なんでもかんでもコンポーネントとして作ろうとするとそれはそれで管理が大変だし、似たようなものができるとコンポーネントの意味がないし、逆にコード量が増えていくような気を感じてしまった。
もっと画面設計は考えないといけないこと、デザインについて詳しくないながらも重要性を感じた。
##自走力の大切さ
エンジニアになるためには?とか求めてる人材は?とか何かあれば自走力自走力とみかけることが多いのは当たり前だよなと思った。コードを書いて、わからなくなったら調べて、エラー調べて、解決したらまたコード書いての繰り返しで、自走できなければこの工程はできないし人に聞くことも相手の時間を使ってしまうから基本的にこの工程に飽きずに自分で解決できる力はエンジニアに限らずとも改めて必要と感じた。
ただ自分の場合、相手の時間を使うのが申し訳ないと思ってしまうほうが強くて、逆に聞かないよりも時間がかかってしまうのでいい塩梅を考える。(勉強にはなるんですが)
##新しい情報の収集
ネットというものが広がって、qiitaやteratailといったサイトがあり、本でなくてもネットで調べれば大体のことが簡単に解決するようになったことで便利ではあるんですが、とりあえずqiitaやteratail見ておこうというのは危険だと感じた。一例を上げると、vuexとfirebaseをつなぐvuexfireライブラリを使おうとしてqiitaの記事を参考にしたところ、うまく動かず公式のgithub見てみると書き方が変わっていたことがありました。一次情報を収集することは大事だし、技術も日々変わっていくことを意識するのは大切だと感じた。
#まとめ
少し長くなりましたが、未経験が一つのアプリケーションの開発と感じたことを書き連ねてみました。
こういった形でまとめて見ましたが、まだ完成はしていませんしひとまずの目的がweb系エンジニアになることなのでいろいろと業界の話を聞けるように動いていきます。
ご覧いただき誠にありがとうございました。
##学習書籍
JavaScript本格入門
Vue.js&Nuxt.js超入門
Vue.js入門
Firebase入門
試して学ぶ Dockerコンテナ開発
WEB+DB PRESS Vol.107
##おわりに
ここから先は個人的なことになるので見なくても構いません。この記事を書こうとおもったきっかけや今の感情の整理をしようと思います。
完成してから記事の作成をしようと思ったのですが、10月半ばごろ実装している最中にそろそろ企業勉強しておいたほうがいいよなと思い、説明会やwantedly等で情報収集を始めてみました。もちろん未経験のため、なかなか話を聞くことだったり、書類が通らなかったり、できないことが多々ありました。自分の中でもっと勉強しなきゃと不安や焦りを感じ始めてアプリ開発だけではだめだと感じました。開発以外のアウトプットもできるし、今の自分の現状を再確認できると思いこれを書いている次第です。転職活動に影響があるかはわかりませんが、ひとまず再確認ができてよかったかなとは思っています。
web系エンジニアになろうと思ったとき、最初はフロントエンドやりたいなと思ったのですが、Firebaseを触ったことで最近バックエンドにも興味を持ち始めてしまったのが現状です。(後悔はしていません)
ひとまずがんばります。
もしよろしければいいねやコメントいただけるととても嬉しいです。
追記:自分が思っているよりも閲覧していただいてる方がとても多くて驚いています。ありがとうございます。もしよろしければ現役でエンジニアされてる方で私と同じような状況だったときをコメントがあるとありがたいです。
以上