都内でサーバーサイドエンジニアをやっている山下です。
webサービスを作るのが好きで、個人でもいくつか開発をしています。
先日Nuxtとfirebaseを使った個人サービスをリリースしたので、開発から初期ユーザ獲得までに行ったことをまとめておこうと思います。
作ったサービス
行きたい場所への同行者を募集するwithruitを作りました。
もしよかったら、使ってみてください!!
個人サービスなので、もしかしたらバグがあるかもしれません、、。
その際は、お問い合わせフォームから優しく連絡いただけるとありがたいです。
機能一覧
withruitには様々な機能があるのですが、大体がfirebaseを使って実装されています。
今回は、以下の機能について自分がfirebaseでどのように実装したのかを書いていこうと思います。
- facebookログイン
- 投稿(画像アップロード)
- お問い合わせ
- 通知
それぞれ0から作ると大変ですが、firebaseの機能を作ると比較的簡単に実装することができます。
今回はNuxtで作成したVueアプリケーションとfirebaseにおける連携に関しての記事です。
すでにfirebaseの連携は済んでいる前提で書いて行きます。
また、今回はなるべく分かりやすく伝えるため、アプリケーションコードの該当箇所のみを抜粋で書き出しています。
そのままのコピペ等では動かないかと思いますので、あくまで参考にしていただけると幸いです。
facebookログイン
withruitには、facebookアカウントでログインする機能があります。
通常facebook等のソーシャルログイン機能を実装するには
- facebook側にトークンを発行してもらう
- トークンを暗号化してサーバーサイドに送信
- サーバーサイドで、複合化したトークンが正しいかどうかを検証
- 正しかった場合、facebookからログイン情報を取得する
といった処理が必要になるかと思います。
さらに、上記以外でもトークンの有効期限を設定したり、トークンの発行元を検証したりなど、考慮すべき点は結構あるので面倒です。
そこで、firebaseのAuthenticationという機能を使うと、ログイン管理をfirebaseに任せられるので、比較的容易にログイン機能を実装することができます。
https://firebase.google.com/docs/auth/web/facebook-login?hl=ja
今回は、以下のように実装してみました。
<template>
<div>
<button @click="facebookLogin">
facebookログイン
</button>
</div>
</template>
<script>
import { mapActions } from 'vuex'
export default {
methods: {
facebookLogin() {
this.signIn()
},
...mapActions('user', ['signIn'])
}
}
</script>
export const actions = {
signIn() {
firebase.auth().signInWithRedirect(
new firebase.auth.FacebookAuthProvider()
)
}
}
ログイン処理をvuexのactionに分割することで、コンポーネントからロジックを切り離しています。
vuexの基本的な書き方に関しては、公式のドキュメントが分かりやすいです。
https://ja.nuxtjs.org/guide/vuex-store/
投稿(画像アップロード)
withruitでは、自分が行きたい場所を画像と一緒に投稿することができます。
今回はデータストアとしてfirebaseのrealtime databaseを使用したので、投稿時に画像をfirebaseのStorageに保存し、保存した画像のurlをrealtime databaseに保存するようにしました。
https://firebase.google.com/docs/database/?hl=ja
https://firebase.google.com/docs/storage/?hl=ja
export const actions = {
async postPlan({ commit }, plan) {
const imageUrl = await uploadImage(plan.image)
plan.imageUrl = imageUrl
await firebase.database().ref('/plans/').push(plan)
}
}
async function uploadImage(image) {
const storageRef = firebase.storage().ref().child('uploads/plans/' + 'imageName')
await storageRef.put(image)
const uploadedImageUrl = await storageRef.getDownloadURL()
return uploadedImageUrl
}
<template>
<div>
<button @click="submit">
投稿する
</button>
</div>
</template>
<script>
import { mapActions } from 'vuex'
export default {
methods: {
submit() {
this.postPlan(this.plan)
},
...mapActions('plans', ['postPlan'])
},
data() {
return () {
plan: null
}
}
}
</script>
お問い合わせ
withruitでは、サイト内にお問い合わせフォームがあります。
google formで作ってもうよかったのですが、せっかくなので実装してみました。
お問い合わせ内容は、投稿時と同じくrealtime databaseに保存されるようになっていますが、投稿があると僕にslackが飛ぶようにしています。
slack通知の実装は、firebaseのcloud functionsに切り離して実装しました。
firebaseのcloud functionsでは、関数の実行イベントにrealtime databaseを指定することができます。
今回は、realtime databaseの特定のパスにデータが作成されたタイミングで、functionが実行されるようにしてみました。
https://firebase.google.com/docs/functions/?hl=ja
const functions = require('firebase-functions')
const admin = require('firebase-admin')
const rp = require('request-promise')
admin.initializeApp(functions.config().firebase)
exports.notifyContact = functions.database.ref('/contacts/{contactId}').onCreate((snapshot, context) => {
const contact = snapshot.val()
return rp({
method: 'POST',
uri: 'https://hooks.slack.com/services/{token}',
body: {
text: `${contact.username}さんからお問い合わせが来ました。${contact.description} 返信メールアドレス: ${contact.mail}`,
},
json: true,
});
})
通知
自分の投稿に応募が来た場合、何らかの形で投稿者に通知してあげないと、投稿者は気づけません。
今回は、お問い合わせの時と同様にrealtime databaseとfirebase functionsを用いて、メールによる通知機能を実装しました。
応募データは、realtimedatabaseの'/applications'というパスに以下のような形式で保存しています。
application: {
plan: {
title: String,
user: {
name: String,
email: String,
}
}
}
const functions = require('firebase-functions')
const admin = require('firebase-admin')
admin.initializeApp(functions.config().firebase)
// 以下のコマンドで、firebase functionsに環境変数を設定できる
// firebase functions:config:set gmail.email="送信元のメールアドレス" gmail.password="送信元のgoogle アカウントのpassword"
const nodemailer = require('nodemailer')
const gmailEmail = functions.config().gmail.email
const gmailPassword = functions.config().gmail.password
const mailTransport = nodemailer.createTransport({
service: 'gmail',
auth: {
user: gmailEmail,
pass: gmailPassword
}
})
exports.watchApplications = functions.database.ref('/applications/{applicationId}').onCreate(async (snapshot, context) => {
const application = snapshot.val()
const plan = application.plan
const email = {
from: gmailEmail,
to: plan.user.email,
subject: `【withruit】${plan.title}に、リクエストがきました!`,
text: `${plan.user.name}さんの投稿した${plan.title}に応募が来ました!`
}
mailTransport.sendMail(email, (err, info) => {
if (err) {
return console.log(err)
}
return console.log('success')
})
})
hosting
firebaseにはhosting機能もあります。
https://firebase.google.com/docs/hosting/?hl=ja
Nuxtで作られたアプリケーションであれば、以下のコマンドでfirebaseにhostingすることができます。
$firebase deploy --only hosting
google analitycs
今回はアクセス分析のために、google analiticsを導入しました。
以下の手順通りに進めれば、特に問題なく導入できます。
エラー検知
エラーが発生した際に、どんなエラーが発生したかなどをしるためsentryを導入しました。
導入に関しては、こちらの記事を参考にさせていただきました。
https://qiita.com/tanakaworld/items/910d766361d398f43254
初期ユーザー集め
せっかく作ったサービスなので、たくさんの人に触ってもらいたいです。
また、リリース時点で投稿数がない状態だと、訪れたユーザーにも寂しいイメージを与えてしまいます。
そこで、リリースの前に初期ユーザーになってくれる人を募集しました。
そこで使った方法をまとめておきます。
今回は、リリースから1ヶ月の時点でユーザー数100人を越えることができました!
bosyu
bosyuは、SNSで募集をかけられるサービスです。
https://bosyu.me/
Twitterではそんなに発信もしていなかったので、フォロワーも少ない状態ではあったのですが、bosyuではハッシュタグ検索からもツイートをみてくださる方がいるので結構反響はきました。
こちらで応募してくださる方は、比較的意欲が高い方は多いので非常にありがたいです!
個人サービスのまとめサイトへの掲載依頼
こちらのブログ記事に大変お世話になりました。
http://www.eggineer.info/entry/2018/03/19/053000
この方も、個人でサービスを開発されているようで、初期ユーザー集めにどんなことをしたのかを細かく書いてくださっています。
この記事の中で紹介されている、個人サービスを掲載してくれるサイトには片っ端から掲載依頼を出しました。
依頼を出した結果、ほぼ全てのサイトに掲載していただき、流入も増えてきました。
特にEggineerとServiceSafariは流入が大きかったです。
また、上記記事には含まれていないのですが開発会議も反響が大きかったです。
開発会議さんにはインタビュー記事も書いてくださったので、よかったら読んでみてください!
https://devtalk.jp/interviews/4
まとめ
今までの個人サービスのハードルは低くなっているなと思っていたのですが、firebaseの登場によりさらにハードルは下がったのではないかなと思います。
運営コストもほぼ無料で運営できるので、個人でもサービス開発を始めやすいです。
また、今回サービスの企画からデザイン、実装、集客といった一通りのwebサービス開発フローをやってみたのですが、一人ではなかなか大変でした。
今後ともwebサービスづくりは継続して行いますので、もし一緒にやりたいという方がいらしたら、連絡くださると嬉しいです!!