สร้าง Single Page Application ด้วย Vue.js และ Firebase authentication

Wattanachai Prakobdee
Firebase Thailand
Published in
9 min readDec 13, 2017

ห่างหายกันไปนาน เนื่องจากช่วงนี้ผมก็ยุ่งๆอยู่กับหลายๆอย่าง เลยไม่ค่อยมีเวลามาเขียน blog เท่าไร พึ่งจะมีเวลาว่าง เลยถือโอกาสมาเขียน blog เกี่ยวกับ web apps สักหน่อย เนื่องจากช่วง sprint ที่ผ่านมา ผมได้มีโอกาสได้กลับมาเขียน web อีกครั้ง หลังจากที่ห่างหายจากการเขียน web แบบจริงๆจังๆไปนานพอสมควร เนื่องจากตัวงานหลักจริงๆก็จะเป็นงานในส่วนของ backend เป็นส่วนใหญ่

สำหรับบทความนี้เราจะมาสร้าง web apps แบบง่ายๆโดยใช้ Vue.js และใช้ Firebase ในการทำ Authentication และเทคนิคที่เราจะใช้ในบทความนี้จะรวมไปถึง Vue router ใช้ในการทำ routing และใช้ Vuex ในการจัดการกับ Global state และใช้ Vuetify เป็น UI framework

Setting up a Project using Vue CLI

สำหรับการสร้าง project เราจะใช้ Vue.js CLI โดยทำการติดตั้งดังนี้

$ npm install --global vue-cli

จากนั้นเราก็มาทำการสร้าง Vue Vuetify project โดยคำสั่ง

$ vue init vuetifyjs/webpack-advanced vue-fire-demo

หลังจาก run command cli จะทำการ download Vuetify template พร้อมทำการ setup webpack ให้เราด้วย ระหว่าง setup ก็จะมีคำถามเกี่ยวกับการ setting project เล็กน้อย เช่น ชื่อ app ,รายละเอียดของ app, เจ้าของผู้ดูแล หรือจะเป็นคำถามเกี่ยวกับพวกต้องการที่จะ install Vue router หรือไม่ ต้องการ ESLint หรือไม่

จากคำสั่งด้านบน สิ่งที่เราจะได้ก็คือ project folder และข้างในจะประกอบไปด้วย subfolders ดังนี้

  • build จะประกอบไปด้วย webpack และ vue-loader configuration files
  • config ประกอบไปด้วย app config ของเรา เช่น environments และ parameters ต่างๆ
  • src จะประกอบไปด้วย source code ของ application
  • static จะประกอบไปด้วยรูปภาพและอื่นๆ

ในที่นี่ผมไม่ได้เลือก test ก็จะมีไม่ folder test มาให้นะครับ จากนั้นก็ทำการ install node และ start server

$ cd vue-fire-demo
$ npm install
$ npm run dev

หลังจากที่เรา start server เรียบร้อยแล้ว ทำการเรียกไปที่ localhost:8080 จะได้ app หน้าตาแบบด้านล่างนี้

Our future App Architecture

หลังจากที่เราได้ทำการสร้าง project เรียบร้อยแล้ว ทีนี้จะเป็นการเริ่มเขียน code จริงๆสักที โดย web app ของเราจะประกอบไปด้วย 3 หน้า ก็คือ หน้า sign-up และหน้า sign-in ที่สามารถเข้าถึงได้โดยไม่ต้องทำการ authentication และอีกหน้าหนึ่งก็คือ หน้า BNK48 ที่ต้องทำการ authentication ก่อนเข้าถึงได้ โดยหลังจากที่ทำการ sign-up และ sign-in เรียบร้อยแล้ว เราจะทำการ redirect ไปที่หน้า BNK48

Creating SignUp, Login and BNK48 View

- SignUp

ทำการสร้าง SignUp ก่อนเลยครับ โดยเราต้องทำการสร้าง component ที่ชื่อ SignUp ภายใต้ src/components โดย component จะประกอบไปด้วย input fields โดยจะมี email และ password และ button สำหรับ submit

SignUp.vue

ทีนี้เราก็จะได้หน้า component สำหรับ SignUp แล้ว แต่เราจะเอาหน้า SignUp แสดงยังไง เราต้องใช้ vue-router ครับ ที่เราได้ install ไปตอนที่เราใช้ vue-cli ในการสร้าง project

ทำการเพิ่ม SignUp เข้ากับ app router โดยทำการเปิดไฟล์ src/router/index.js และทำการเพิ่ม SignUp component ดังนี้

สิ่งที่เราได้เพิ่มไปข้างบน คือการทำ routes ที่เรียกว่า lazy loading โดยเราได้สร้าง arrays ของ components และใช้ map function ในการสร้าง routes หลังจากที่เราทำการทำ route เรียบร้อยแล้ว อีกอย่างก็คือ เราต้องทำการแก้ไขไฟล์ App.vue นิดหน่อย โดยข้างใน v-container ทำการลบ code ทิ้งให้หมดและเพิ่ม

<router-view></router-view>

เข้าไปแทน โดยการเพิ่ม router-view จะเป็นการบอกให้ Vue-router ทำการ render routes ข้างใน tag นี้ จากนั้นทำการเรียกไปที่ http://localhost:8080/signup

จะได้หน้า Sign Up ดังรูปข้างล่างครับ

- Login

เหมือนกัน SignUp ครับ ทำการสร้าง Login Component ภายใต้ src/components/

และเพิ่ม code ด้านล่างนี้

Login.vue

จากนั้นก็ทำการเพิ่ม SignIn เข้ากับ app router

ทำการเรียกไปที่ http://localhost:8080/signin จะได้หน้า view ดังรูปด้านล่างครับ

- BNK48

สุดท้าย ทำการสร้าง BNK48 view เหมือนเดิมครับ ทำการสร้าง BNK48 component ภายใต้ src/components/

BNK48.vue

ทำการเพิ่ม BNK48 เข้ากับ app router

ทำการเรียกไปที่ http://localhost:8080/bnk48 จะได้ดังรูปด้านล่างครับ

State management with Vuex

มาถึงส่วนสำคัญอีกส่วนของ app ของเราก็คือเรื่อง state management ซึ่งในที่นี้เราจะใช้ Vuex ซึ่งจะช่วยเราในการทำ centralized store สำหรับ components ทุกๆตัวใน application

อันดับแรกทำการ install Vuex ด้วยคำสั่ง

$ npm install --save vuex

จากนั้นภายใต้ /src ทำการสร้าง folder ชื่อ store และทำการสร้างไฟล์ index.js, state.js, mutations.js, actions.js, getters.js ด้วยคำสั่ง

$ mkdir -p src/store && touch src/store/{index.js,state.js,mutations.js,actions.js,getters.js}

ทำการเปิดไฟล์ index.js แล้วทำการ import Vue , Vuex และส่วนต่างๆของ store และใช้บอก Vue ให้ใช้ Vuex และทำการ export store ดัง code ด้านล่าง

ต่อมาเป็นส่วนของ state ซึ่งเป็น single object ที่ประกอบไปด้วย application data ในส่วนของ mutation เป็นทางเดียวที่เราจะเปลี่ยนแปลง state ใน Vuex store และ action ก็คล้ายๆกับ mutations แต่แทนที่จะทำการเปลี่ยนแปลง state ตัว action จะทำหน้าที่ส่งข้อมูลไปบอกให้ mutations เป็นคนแก้ไขหรือเปลี่ยนแปลงข้อมูล และสุดท้าย getter ทำหน้าที่ในการดึงข้อมูลจาก store

สำหรับการใช้งาน store ของเรา เราต้องทำการ import โดยทำการเปิดไฟล์ main.js และทำการ import store ดังนี้

ทีนี้เราก็พร้อมแล้วที่จะใช้งาน store เริ่มด้วยการเพิ่ม variable ใหม่ใน state.js ดังนี้

state.js

export const state = {
appName: 'BNK48'
}

และใน getters.js สร้าง getter function ดังนี้

getters.js

export const getters = {
appTitle (state) {
return state.appName
}
}

และสุดท้ายก็มาเรียกใช้งาน variable ของเราใน App.vue โดยทำการเรียก this.$store ซึ่งเป็น global ใน application

เสร็จแล้วจะเห็นว่า title ของเราเรียกใช้ค่า BNK48 ของ variable จาก state แล้ว

Firebase Integration and implement an authorization functionality

อันดับแรกให้ทำการสร้าง Firebase account ก่อนนะครับ จาก https://firebase.google.com/ จากนั้นทำไปที่ firebase console จาก console.firebase.google.com พร้อมทำการสร้าง project ใหม่ตามภาพด้านล่าง

จากนั้น Firebase จะทำการ redirect ไปหน้า project ที่เราสร้าง ดังภาพด้านล่าง

ก่อนอื่นเลย บทความนี้เราจะทำการ sign-up ด้วย email และ password ดังนั้นเราจึงต้องทำการ enable Email/Password sign-in method ก่อน โดยไปที่ menu Develop/Authentication และคลิกที่ sign-in method จากนั้นให้ทำการ enable Email/Password ดังรูปด้านล่าง

ทีนี้เราก็จะมาทำการ integrate Firebase project เข้ากับ app ของเรา โดยคลิกที่ปุ่ม

Add Firebase to your web app

จะมี popup พร้อม code snippet พร้อมให้เราทำการ copy ไปใช้งานในการ integrate ดังภาพด้านล่าง

แต่ก่อนที่เราจะทำการ integrate Firebase กับ app ของเรา เราต้องทำการ setup Firebase SDK ก่อน โดยทำการ run

$ npm install --save firebase

จากนั้นเราก็พร้อมที่จะ import Firebase module เข้ากับ app ของเรา ทำการสร้างไฟล์ชื่อ config.js ภายใต้ /src

config.js

จากนั้นก็ทำการเปิดไฟล์​ main.js และทำการ import config และบอกให้เราใช้งาน firebaseConfig ตาม code ด้านล่างนี้

Create User with SignUp component

ก่อนที่จะเริ่มด้วยการเตรียม states ใน Vuex store ทำการเปิด state.js และเพิ่ม states ดังนี้

  • user object เอาไว้เก็บ user data
  • error เอาไว้เก็บ error data
  • loading เป็น flag เพื่อบอกว่า app ของเรายังโหลดข้อมูลอยู่

state.js

export const state = {
appName: 'NBK48',
user: null,
error: null,
loading: false
}

จากนั้นทำการเพิ่ม getters โดยแก้ไขไฟล์​ getters.js

getters.js

export const getters = {
appTitle (state) {
return state.appName
},
getUser (state) {
return state.user
},
getError (state) {
return state.error
},
getLoading (state) {
return state.loading
}
}

และสุดท้าย ทำการสร้าง mutations เอาไว้เปลี่ยนแปลงค่าของ stats ทำการแก้ไข mutations.js ดังนี้

mutations.js

export const mutations = {
setUser (state, payload) {
state.user = payload
},
setError (state, payload) {
state.error = payload
},
setLoading (state, payload) {
state.loading = payload
}
}

ถึงตอนนี้ก็เรียบร้อยแล้วสำหรับการเตรียม state สำหรับใช้ในการสร้าง sign up/in ด้วย Firebase

ต่อไปเราจะทำการแก้ไข SignUp.vue โดยทำการเพิ่ม v-model attribute เข้ากับ text fields

<v-flex>
<v-text-field
name="email"
label="Email"
id="email"
type="email"
v-model="email" //เพิ่ม v-model
required></v-text-field>
</v-flex>
<v-flex>
<v-text-field
name="password"
label="Password"
id="password"
type="password"
v-model="password" //เพิ่ม v-model
required></v-text-field>
</v-flex>
<v-flex>
<v-text-field
name="confirmPassword"
label="Confirm Password"
id="confirmPassword"
type="password"
v-model="passwordConfirm" //เพิ่ม v-model
></v-text-field>
</v-flex>

เพื่อที่จะ bind ตัว text fields กับ variables ทำการ initialize variables ใน data property ดังนี้

export default {
data () {
return {
email: '',
password: '',
passwordConfirm: ''
}
}
}

จะเห็นว่า เรามีการตรวจสอบด้วย ว่า password และ confirm password เหมือนกันหรือเปล่า เราจึงต้องสร้าง computed property ชื่อ comparePasswords โดยทำการตรวจสอบ confirm password โดยถ้า password และ confirm password เหมือนกันให้ทำการ return ค่า True ถ้าไม่เหมือนกัน ให้ return ค่า String ‘Password and confirm password don\’t match’ ดังนี้

computed: {
comparePasswords () {
return this.password === this.passwordConfirm ? true :
'Password and confirm password don\'t match'
}
}

ทำการเพิ่ม rules ในการตรวจสอบ password ใน text field ดังนี้

<v-flex>
<v-text-field
name="confirmPassword"
label="Confirm Password"
id="confirmPassword"
type="password"
v-model="passwordConfirm"
:rules="[comparePasswords]"
></v-text-field>
</v-flex>

และเพื่อที่จะ submit เพื่อที่จะสร้าง account ทำการสร้าง submit listener และ function userSignUp ดังนี้

<form @submit.prevent="userSignUp">

และ userSignUp method โดยทำการตรวจสอบ comparePasswords ก่อนว่าเหมือนกันหรือเปล่า ถ้าไม่เหมือนก็ให้ทำการ return ออกไปเฉยๆ แต่ถ้า comparePasswords เหมือนกันก็ให้ทำการ dispatch action ที่ชื่อ userSignUp พร้อมทำการส่ง object ที่ประกอบไปด้วย email และ password ไปด้วย ดังนี้

methods: {
userSignUp () {
if (this.comparePasswords !== true) {
return
}
this.$store.dispatch('userSignUp', { email: this.email, password: this.password })
}
}

และสุดท้าย ทำการ handle error และ loading state โดยสุดท้ายแล้ว SignUp.vue จะเหมือน code ด้านล่างนี้

SignUp.vue

หลังจากที่เราได้ทำการแก้ไข Signup.vue แล้ว ทีนี้เราก็จะมาทำการเพิ่ม action ในการ sign-up กับ Firebase กัน เริ่มด้วยทำการเปิดไฟล์ src/store/actions.js และเพิ่ม action ที่ชื่อ userSignUp

sign up

อันดับแรกทำการ import Firebase และ router เพราะว่าหลังจากที่เรา sign-up เรียร้อยแล้ว เราจะทำการ redirect ไปที่หน้า home หรือว่า BNK 48 จากนั้นสร้าง action ที่ชื่อ userSignUp โดยรับ arguments 2 ตัว คือ commits ที่เอาไว้ access mutations แบะ payload ซึ่งเป็น object ที่เรารับมาจาก component ในที่นี้คือ email และ password

ใน userSignUp อันดับแรกเราทำการ set ค่า loading state เป็นค่า true ซะก่อน จากนั้นก็เรียก createUserWithEmailAndPassword method จาก firebase SDK ซึ่งจะทำหน้าที่ในการ sign-up ให้กับเรา

firebase.auth().createUserWithEmailAndPassword(email,password)

จากนั้นเราก็ทำการ set ค่า firebaseUser จาก response ให้กับ user state พร้อม

set ค่า loading state เป็นค่า false และตามด้วย set ค่า setError เป็น null ในการณีที่ sign-up สำเร็จ

Sign into your account with SignIn component

ทีนี้ก็ถึงเวลาที่เราจะมาทำการสร้างหน้าสำหรับ sign-in โดยทำการเปิดไฟล์ Signin.vue และเพิ่ม code ดังนี้

เหมือนกัน sign-up นั้นแหละครับ โดยทำการเพิ่ม v-model สำหรับ text fields และเพิ่ม event listener ให้กับ form ที่เรียกใช้ method userSignIn ซึ่งจะทำการ dispatch userSignIn จาก Vuex store พร้อมทำการ handle error alert และ loading state

ในส่วนของ userSignIn actions ให้เราทำการเปิดไฟล์ src/store/actions.js

และเพิ่ม userSignIn action ต่อจาก userSignUp โดยทำการเรียก signInWithEmailAndPassword method จาก firebase SDK ซึ่งจะทำหน้าที่ในการ sign-up ให้กับเรา ดังนี้

actions.js

Authorization check and log out from the app

หลังที่เราทำการ sign-in เราได้ทำการ reload หน้า ซึ่งผลจากการ reload คือทำให้เกิดการ reset Vuex state เป็นค่า initial value ซึ่งเราต้องทำการจัดการปัญหานี้ ด้วยการใช้งาน Firebase ในการแก้ปัญหา โดยที่ทาง Firebase ได้มี method onAuthStateChanged ซึ่งจะช่วยให้เราตรวจสอบว่า user คนนี้ได้ทำการ sign-out ไปแล้วหรือยัง

ทำการเปิด src/main.js และเพิ่ม created() lifecycle hook ของ Vue instance โดยทำการเรียก dispatch autoSignIn โดยส่ง firebaseUser เป็น argument ดังนี้

main.js

/* eslint-disable no-new */
new Vue({
el: '#app',
router,
store,
render: h => h(App),
created () {
firebase.auth().onAuthStateChanged((firebaseUser) => {
if (firebaseUser) {
store.dispatch('autoSignIn', firebaseUser)
}
})
}
})

จากนั้นทำการเพิ่ม autoSignIn action โดยทำการ set ค่า firebaseUser ดังนี้

action.js

autoSignIn ({commit}, payload) {
commit('setUser', payload)
}

ถึงตอนนี้เราก็มี function สำหรับ sign-up และ sign-in และหลังจาก sign-up และ sign-in เรียบร้อยแล้ว เราได้ทำการ redirect user ไปที่หน้า BNK48 ทีนี้เราจะทำ function สำหรับ sign-out และหลังจาก user กด sign-out เราก็จะทำการ redirect user ไปที่หน้า sign-in

อ๋อ ลืมไปเลยครับ เรายังไม่ได้สร้างปุ่มสำหรับ sign-up และ sign-in เลย งั้นมาสร้างพร้อมปุ่ม sign-out เลยละกัน ทำการเปิด src/App.vue

App.vue

ทำการเพิ่ม isAuthenticated property และ toolbarItems property ใน computed โดย isAuthenticated property จะทำการตรวจสอบว่า user ได้ทำการ sign-in แล้วหรือยัง และ toolbarItems property จะเป็น list ของปุ่ม sign-up และ sign-in

isAuthenticated() {
return (
this.$store.getters.getUser !== null &&
this.$store.getters.getUser !== undefined
);
},
toolbarItems() {
return this.isAuthenticated ? [] : [
{
icon: "face",
title: "Sign Up",
link: "/singup"
}, {
icon: "lock_open",
title: "Sign In",
link: "/singin"
}
];
}

และทำการเพิ่ม userSignOut method

methods: {
userSignOut() {
this.$store.dispatch("userSignOut");
}
}

พร้อมทำการเพิ่ม userSignOutใน action โดยทำการ set user state เป็นค่า null พร้อมทำการ redirect ไปที่หน้า login ทำการเปิด src/store/actions.js และเพิ่ม

userSignOut ({commit}) {
firebase.auth().signOut()
commit('setUser', null)
router.push('/signin')
}

จากนั้นทำการเพิ่มปุ่ม sign out โดยเปิด App.vue ทำการแทนที่

...
<v-btn icon @click.stop="rightDrawer = !rightDrawer">
<v-icon>menu</v-icon>
</v-btn>
<v-navigation-drawer
temporary
:right="right"
v-model="rightDrawer"
fixed
>
<v-list>
<v-list-tile @click.native="right = !right">
<v-list-tile-action>
<v-icon>compare_arrows</v-icon>
</v-list-tile-action>
<v-list-tile-title>Switch drawer (click me)</v-list-tile-title>
</v-list-tile>
</v-list>
</v-navigation-drawer>
...

ด้วย

...
<v-btn flat @click="userSignOut" v-if="isAuthenticated">
<v-icon left>exit_to_app</v-icon>
Sign Out
</v-btn>
<v-btn
flat
v-for="(item, i) in toolbarItems"
:key="item.i"
:to="item.link">
<v-icon left>{{ item.icon }}</v-icon>
{{ item.title }}
</v-btn>
...

Protected our BNK48

มาถึงขั้นตอนสุดท้ายแล้วนะครับ ก็การปกป้องน้องๆ BNK48 ที่น่ารักของเรา โดยเราจะ allow ให้ user ที่ทำการ sign-up หรือ sign-in เท่านั้น ที่สามารถ access หน้า BNK48 เพื่อที่ปกป้องน้องๆของเรา ทำการเปิด src/router/index.js และทำการเพิ่ม requiresAuth ใน routerOptions ดังนี้

...
{
path: '/',
component: 'BNK48',
meta: { requiresAuth: true }
}
...

จากนั้นทำการเพิ่ม meta ให้กับ routes

const routes = routerOptions.map(route => {
return {
path: route.path,
component: () => import (`@/components/${route.component}.vue`),
meta: route.meta
}
})

และสุดท้ายทำการใช้ global guard ที่ชื่อ beforeEach ซึ่งจะ execute ทุกๆครั้งที่เราเรียก rount ทำการแก้ไข index.js โดยทำการตรวจสอบถ้า requiresAuth และ user state เป็น null ให้ทำการ redirect ไปที่หน้า sign-in ดังนี้

index.js

ถึงตอนนี้ถ้าไม่มีอะไรผิดพลาด ถ้าเราเรียกไปที่ localhost:8080 ควรจะมีการ redirect ไปที่หน้า sign-in นะครับ ทีนี้ลองทำการ sign-up

เราก็จะโดน redirect ไปที่หน้า BNK48

จากนั้นก็ทำการ refresh page จะเห็นว่าเราโดน redirect ไปที่หน้า sign-in

อ้าว เฮ้ย! ไม่เหมือนที่คุยกันไว้นี้นา ปัญหานี้เกิดขึ้นเนื่องจาก beforeEach ทำการตรวจสอบ firebase.auth().currentUser และได้ค่าเป็น null เนื่องด้วยระหว่างที่ excute beforeEach ตัว method onAuthStateChanged ของ Firebase ยังไม่ได้รับ user status เลย การแก้ปัญหาคือให้เราเรียก beforeEach หลังจากเรียก onAuthStateChanged ได้รับ user status เรียบร้อยแล้ว ทำการเปิด src/main.js และแก้ไขตาม code ด้านล่างนี้

main.js

ทีนี้เราก็ทำการ sign-in อีกครั้ง พร้อมทำการ refresh จะเห็นว่า เราไม่โดน redirect ไปหน้า sign-in อีกแล้ว

Note: สำหรับการดู list ของ user ที่ทำการ sign-up สามารถตรวจสอบได้จาก Firebase console โดยไปที่หน้า Authentication/users

The End

ถึงตอนนี้เราก็สามารถสร้าง Vue app พร้อม function authentication ด้วยการใช้ Firebase authentication สำหรับผู้ที่สนใจศึกษาเพิ่มเกี่ยวกับ Firebase authentication เพิ่มเติม จาก app ของเรา สามารถต่อยอดได้อีก อาจจะเป็นเพิ่มขั้นตอน verify email หรือทำการ sign-up ด้วย facebook ซึ่ง Firebase authentication ก็มี function พวกนี้รองรับให้เราได้ใช้งานอย่างง่ายดายแบบไม่เสียตังค์ สำหรับบทความอันยาวเหยียดนี้ ผมก็ขอจบไว้เพียงเท่านี้ ผิดพลาดประการใดขออภัยไว้ณที่นี้ สำหรับ sources code สามารถหาได้จาก link ด้านล่างนี้

--

--

Wattanachai Prakobdee
Firebase Thailand

Software Engineer at LINE Thailand | Learning is a journey, Let's learn together.