接續上次講嘅 Vue PWA + Docker,今次係要加多少少功能,加個 Firebase Push Notifications,中文係咪叫「推送短訊」? 雖然今時今日嚟講係小功能,不過由於有 Windows、macOS、Android、同埋 iOS,仲有 Chrome、Edge Chromium 等等不同嘅 browser,都算幾麻煩㗎,我為咗減少工作量,淨係針對 Chrome 做測試,間唔中用埋 Edge Chromium 嚟証實有冇搞錯。
Notifications 有分 foreground 同 background,foreground 即係當個 web app 喺度 run 緊,你睇到個畫面嘅,background 就即係個 web app 喺分頁 run 緊,不過你個 browser 睇緊其他網站,或者完全冇 run 到(關咗)。
開波,先嚟睇個 project 多咗啲咩 filo?
加咗啲咩嘢 plugins?去睇 package.json:
{
"name": "x5-easyrip",
"version": "1.0.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"axios": "^0.19.2",
"core-js": "^3.6.4",
"fingerprintjs2": "^2.1.0",
"firebase": "^7.14.2",
"firebase-admin": "^8.11.0",
"firebase-functions": "^3.6.1",
"register-service-worker": "^1.6.2",
"ua-parser-js": "^0.7.21",
"velocity-animate": "^1.5.2",
"vue": "^2.6.11",
"vue-file-agent": "^1.6.1-beta.2",
"vue-i18n": "^8.15.5",
"vue-js-cookie": "^2.1.0",
"vue-notification": "^1.3.20",
"vue-router": "^3.1.5",
"vuetify": "^2.2.11",
"vuex": "^3.1.2",
"vuex-persist": "^2.2.0"
},
"devDependencies": {
"@intlify/vue-i18n-loader": "^0.6.1",
"@vue/cli-plugin-babel": "^4.2.0",
"@vue/cli-plugin-eslint": "^4.2.0",
"@vue/cli-plugin-pwa": "^4.2.0",
"@vue/cli-service": "^4.2.0",
"babel-eslint": "^10.0.3",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.1.2",
"fibers": "^4.0.2",
"node-sass": "^4.13.1",
"sass": "^1.26.3",
"sass-loader": "^8.0.0",
"vue-cli-plugin-vuetify": "^2.0.5",
"vue-template-compiler": "^2.6.11",
"vuetify-loader": "^1.3.0"
}
}
#14~16:係 Firebase 相關嘅 libraries
#17:用嚟登記個 service-worker JS,service-worker 係用嚟喺 background 接收 notifications
#13+18:先用 #18 嚟分析個 browser 嘅 User Agent 資料,然後根據 User Agent 嘅資料經 #13 gen 一個 unique 嘅 ID 用嚟記住個 user,我個 backend 要知邊個 user 然後會叫 Firebase 獨獨送出 notification 俾佢,咁就唔使送錯 notifications 俾唔相關嘅 users
#24+19:係用嚟顯示 customized 嘅 notifications,個 popup notifications 可以靚仔啲咁啫,#24 係主,#19 係美化
Okay?由上至下咁睇啲 filo 內容,firebase-messaging-sw.js:
// Give the service worker access to Firebase Messaging.
// Note that you can only use Firebase Messaging here, other Firebase libraries
// are not available in the service worker.
importScripts('https://www.gstatic.com/firebasejs/7.14.2/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/7.14.2/firebase-messaging.js');
if (firebase.messaging.isSupported()) {
// Initialize the Firebase app in the service worker by passing in
// your app's Firebase config object.
// https://firebase.google.com/docs/web/setup#config-object
const config = {
apiKey: 'XXXXXXXXXXXXXXXXXXXXXXXX',
authDomain: "xxxxxxxxxxxxxxxxxxxx",
databaseURL: "xxxxxxxxxxxxxxxxxxxxx",
projectId: "xxxxxxxxxxx",
storageBucket: "xxxxxxxxxxxxxxxxxxxx",
messagingSenderId: "9999999999999",
appId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
};
firebase.initializeApp(config);
// Retrieve an instance of Firebase Messaging so that it can handle background
// messages.
const messaging = firebase.messaging();
// Get Instance ID token. Initially this makes a network call, once retrieved
// subsequent calls to getToken will return from cache.
messaging.getToken()
.then(function(currentToken) {
if (currentToken) {
console.log("getToken", currentToken);
} else {
// Show permission request.
console.log('getToken: No Instance ID token available. Request permission to generate one.');
}
})
.catch(function(err) {
console.log('getToken: An error occurred while retrieving token. ', err);
});
// Callback fired if Instance ID token is updated.
// messaging.onTokenRefresh(function() {
// messaging.getToken()
// .then(function(refreshedToken) {
// console.log('onTokenRefresh getToken Token refreshed.');
// console.log('onTokenRefresh getToken', refreshedToken);
// })
// .catch(function(err) {
// console.log('onTokenRefresh getToken Unable to retrieve refreshed token ', err);
// });
// });
// 顯示個收到嘅 message
// refer: https://www.cometchat.com/tutorials/vue-chat-push-notifications
messaging.setBackgroundMessageHandler(function (payload) {
console.log('firebase-messaging-sw.js] Received background message ', payload);
// var sender = JSON.parse(payload.data.message);
var notificationTitle = payload.data.title;
var notificationOptions = {
body: payload.data.body,
icon: '/img/favicon-32x32.png',
};
return self.registration.showNotification(
notificationTitle,
notificationOptions
);
});
self.addEventListener('notificationclick', function (event) {
event.notification.close();
//handle click event onClick on Web Push Notification
});
}
用嚟處理 background 收 notifications,
#12~18 係照抄你喺 Firebase 個 console 設定俾喱個 web app 嘅資料,人人唔同,隻隻 web app 唔同。
#29~40 係問 Firebase 攞個 unique 嘅 token 嚟認住喱個 user,同一部機唔同 browser 都會唔同嘅,有時會叫 FCM Token。
#42~52 係當個 Token 有變嘅時候會 trigger,我冇理佢,留低啲手尾? 
#57~69 就係當有 notifications 送到,而啱啱你又冇 foreground,咁佢就會 fire,啲 codes 會喺入嚟嘅 embedded data 抽出個 Title 同 Body 嚟顯示。
講就好似好易,不過基本上係冇得 debug 嘅,我就唔識點樣去 debug,你識嘅話可以教下我。
service-worker.js:
// Set this to true for production
var doCache = true;
// Name our cache
var CACHE_NAME = 'x5-easyrip';
// Delete old caches that are not our current one!
self.addEventListener("activate", event => {
const cacheWhitelist = [CACHE_NAME];
event.waitUntil(
caches.keys()
.then(keyList =>
Promise.all(keyList.map(key => {
if (!cacheWhitelist.includes(key)) {
console.log('Deleting cache: ' + key);
return caches.delete(key);
}
}))
)
);
});
// The first time the user starts up the PWA, 'install' is triggered.
self.addEventListener('install', function (event) {
if (doCache) {
event.waitUntil(
caches.open(CACHE_NAME)
.then(function (cache) {
// Get the assets manifest so we can see what our js file is named
// This is because webpack hashes it
fetch("asset-manifest.json")
.then(response => {
response.json();
})
.then(assets => {
// Open a cache and cache our files
// We want to cache the page and the main.js generated by webpack
// We could also cache any static assets like CSS or images
const urlsToCache = [
'/'
// '/plate',
// '/film',
// '/login',
// '/reset'
];
cache.addAll(urlsToCache);
console.log('cached');
});
})
);
}
});
// When the webpage goes to fetch files, we intercept that request and serve up the matching files
// if we have them
self.addEventListener('fetch', function (event) {
if (doCache) {
// SOLUTION:
// i) detect your upload endpoint
// ii) return so that you can use XHR
if (event.request.url.indexOf('BaseForm') !== -1) {
return;
}
event.respondWith(
caches.match(event.request).then(function (response) {
return response || fetch(event.request);
})
);
}
});
因為我特登用咗 firebase-messaging-sw.js 嚟分開 Firebase 同 PWA 要用嘅 codes,所以 service-worker.js 應該冇改過,同 PWA 嘅時候相同。
utils/firebase.js:
import firebase from 'firebase/app';
import 'firebase/messaging';
import 'firebase/functions';
/*
* firebase init
*/
const initializeFirebase = () => {
//? 有網友教用 .env 裝啲 data,例如:VUE_APP_FCM_API_KEY...,未決定跟唔跟
const firebaseConfig = {
apiKey: "xxxxxxxxxxxxxxxxxxxxxxxx",
authDomain: "xxxxxxxxxxxxxxxxxxxxxx",
databaseURL: "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
projectId: "xxxxxxxxxx",
storageBucket: "xxxxxxxxxxxxxxxxxxxxxx",
messagingSenderId: "9999999999999",
appId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx6"
}
firebase.initializeApp(firebaseConfig);
navigator.serviceWorker
.register('service-worker.js')
.then(function(registration){
window.console.log('Service worker successfully registered.');
return registration;
})
.then((registration) => {
window.console.log('call firebase.messaging');
firebase.messaging().useServiceWorker(registration);
})
.catch(function(e) {
window.console.log('Unable to register service worker.', e);
});
}
export {
initializeFirebase
}
重複出現 Firebase 嘅設定資料,係俾 foreground 用嘅。
App.vue:
<template>
<div id="app-root" v-cloak>
<keep-alive exclude="">
<router-view :key="$route.fullPath" />
</keep-alive>
<notifications group="custom-template"
:duration="5000"
animation-name="v-fade-left"
width= "100%"
position="top left">
<template slot="body" slot-scope="props">
<div class="custom-template">
<div class="custom-template-icon">
<v-icon large>mdi-cloud-upload</v-icon>
</div>
<div class="custom-template-content">
<div class="custom-template-title">
{{props.item.title}}
</div>
<div class="custom-template-text"
v-html="props.item.text"></div>
</div>
<div class="custom-template-close"
@click="props.close">
<v-icon medium>mdi-close</v-icon>
</div>
</div>
</template>
</notifications>
</div>
</template>
<script>
import { initializeFirebase } from '@/utils/firebase'
import firebase from 'firebase/app';
import 'firebase/messaging';
import 'firebase/functions';
import uaHelper from '@/utils/uaHelper';
export default {
name: 'App',
data: () => ({
darkMode: JSON.parse(localStorage.getItem('darkMode')) === true,
}),
created() {
window.getApp = this;
/**
* native notification 唔 work,改用 vue-notification
*/
window.getApp.$on('showNotification', (payload) => {
window.console.log("showNotification:", payload);
this.$notify({
group: 'custom-template',
title: payload.data.title,
text: payload.data.body,
type: 'success',
duration: -1,
position: 'top left'
});
})
/**
* 轉換 light or dark theme,用 cookie 記住目前嘅選擇
*/
window.getApp.$on('APP_DARK_THEME_TOGGLED', () => {
this.darkMode = !this.darkMode;
localStorage.setItem('darkMode', JSON.stringify(this.darkMode));
window.console.log("darkMode: ", localStorage.getItem('darkMode'));
this.$vuetify.theme.dark = this.darkMode;
})
/**
* check 下有冇 notification 嘅 permission,依家冇咩用,日後睇下點
* firebase 都係叫你用 Notification.permission 去 check
* 參考:https://juejin.im/post/59ed37f5f265da431e15eaac
*/
if (Notification.permission === 'granted') {
console.log('用户允许通知');
} else if (Notification.permission === 'denied') {
console.log('用户拒绝通知');
} else {
console.log('用户还没选择,去向用户申请权限吧');
}
/**
* init Firebase,問 user 准唔准收 push notifications?
*/
initializeFirebase();
const messaging = firebase.messaging();
// Get Instance ID token. Initially this makes a network call, once retrieved
// subsequent calls to getToken will return from cache.
messaging.getToken().then((currentToken) => {
if (currentToken) {
// 收到 FCM token,即係有 push notifications permission
window.console.log('Permisson already granted.');
window.console.log('FCM Token: ', currentToken) // 3. Receiver Token to use in the notification
localStorage.setItem('user-fcm-token', currentToken);
uaHelper.storeUserAgent();
} else {
// 冇 FCM token,問 user 攞 permission,然後再去攞 FCM token
window.console.log('No Instance ID token available. Request permission to generate one.');
this.askUserPermission();
}
}).catch((err) => {
window.console.log('An error occurred while retrieving token. ', err);
});
/**
* 參考:https://www.sentinelstand.com/article/handling-firebase-notification-messages-in-your-web-app
* Messages received. Either because the app is running in the foreground, or
* because the notification was clicked.
* `payload` will contain your data.
*/
messaging.onMessage(function(payload) {
window.console.log('Receiving foreground message', payload);
// Customize notification here
// const firebaseUid = '772018130355';
// if (payload.from !== firebaseUid) {
var notificationTitle = payload.data.title; //'CometChat Pro Notification';
var notificationOptions = {
body: payload.data.body, //.data.alert,
icon: '/img/favicon-32x32.png', // sender.data.entities.sender.entity.avatar,
requireInteraction: true
};
/** native notification localhost 唔 work 嘅 ,要用 showNotification 代替
var notification = new Notification(notificationTitle, notificationOptions);
notification.onclick = function(event) {
notification.close();
window.console.log(event);
};
*/
window.getApp.$emit('showNotification', payload);
navigator.serviceWorker.ready.then(registration => {
registration.showNotification(notificationTitle, notificationOptions);
});
// }
// ...
});
/** End */
},
mounted() {
//vuetify 2.x 冇再用 v-app dark prop,改用 this.$vuetify.theme.dark 嚟 toggle dark
window.console.log("Mounted: ", this.darkMode);
this.$vuetify.theme.dark = this.darkMode;
},
methods: {
askUserPermission() {
const messaging = firebase.messaging();
messaging
.requestPermission() // 1. Ask user to Allow push notifications
.then(() => firebase.messaging().getToken()) // 2. Permission granted. Ask Firebase for FCM Token
.then((token) => {
window.console.log('Permisson granted.');
window.console.log('FCM Token: ', token) // 3. Receiver Token to use in the notification
localStorage.setItem('user-fcm-token', token)
uaHelper.storeUserAgent();
})
.catch(function(err) {
window.console.log("Unable to get permission to notify.", err);
localStorage.removeItem('user-fcm-token');
uaHelper.removeUserAgent();
});
},
showNotification() {
this.$notify('something to tell', 'success');
}
}
};
</script>
<style lang="scss">
.custom-template {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
text-align: left;
font-size: 16px;
font-family: arial;
margin: 5px;
margin-bottom: 0;
align-items: center;
justify-content: center;
&, & > div {
box-sizing: border-box;
}
background: #E8F9F0;
border: 2px solid #D0F2E1;
.custom-template-icon {
flex: 0 1 auto;
color: #15C371;
font-size: 32px;
padding: 0 10px;
}
.custom-template-close {
flex: 0 1 auto;
padding: 0 20px;
font-size: 16px;
opacity: 0.2;
cursor: pointer;
&:hover {
opacity: 0.8;
}
}
.custom-template-content {
padding: 10px;
flex: 1 0 auto;
.custom-template-title {
letter-spacing: 1px;
text-transform: uppercase;
font-size: 18px;
font-family: arial;
font-weight: 400;
}
}
}
.v-fade-left-enter-active,
.v-fade-left-leave-active,
.v-fade-left-move {
transition: all .5s;
}
.v-fade-left-enter,
.v-fade-left-leave-to {
opacity: 0;
transform: translateX(-500px) scale(0.2);
}
</style>
vue 檔案分成 3 個段落,分別係 Template、Script、同 Style,Template 係個畫面見到嘅嘢,Style 係特別為咗今個 Template 而建嘅 Style codes,Script 就係個靈魂。
#6~29 係個 foreground Notificaton,我依照個 plugin vue-notification 做,不過我冇用 basic style,用咗 custom,左邊有個大啲嘅 icon,中間係個 notification,右邊加個 X icon,用嚟 close 個 notification。啲 Style 係為咗配合喱個畫面而加嘅。
#53~64 係顯示個 notification
#96 係喺 foreground 啟動個 Firebase
#102~118 係去 Firebase 攞個 FCM Token 返嚟用
#126~154 當收到 notifications 時會 fire,fire 咗就 call #53 去顯示
#148~150 應該係因為依家個 native notification 唔再用,退役咗 deprecated,要叫個 service-worker 顯示個 notification,不過我唔覺佢會叫到 service-worker 做嘢?
#165~185 係問個 user 攞 permission,如果冇 permission 就收唔到 notifiations,而且 macOS 同 Windows 又唔同處理,好麻煩嘅,多數收唔倒 notifications 都同 permission 有關,更何況我用 Windows 就長期 disabled 😀
main.js:
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import './registerServiceWorker'
import vuetify from './config/vuetify';
import i18n from './locale/i18n'
import VueFileAgent from 'vue-file-agent';
import VueFileAgentStyles from 'vue-file-agent/dist/vue-file-agent.css';
import VueNotification from 'vue-notification'
import velocity from 'velocity-animate'
Vue.config.productionTip = false
Vue.use(VueFileAgent);
Vue.use(VueNotification, { velocity });
new Vue({
VueFileAgent,
VueFileAgentStyles,
i18n,
router,
store,
vuetify,
created() {
// 跳去 landing-page
this.$router.push(localStorage.getItem('landing-page') === undefined ? 'plate' : localStorage.getItem('landing-page'));
},
render: h => h(App)
}).$mount('#app')
#10~11 import 個 vue-notification,喺 #15 加入。我冇特別搞 velocity 啲 animate 效果,目前嘅就已經好滿意。
好似就係咁多,thanks!
短片:


