- 展示網址
- Project setup
- firebase-tools
- express-server.js
- connect-history-api-fallback
- store-firebase
- my-computed
- my-methods
- my-component
- scss & css
- css animate
- firebase
- firestore 安全規則
- bootstrap
- fort-awesome
- vue2-editor
- vue-select
- vue-tippy
- v-responsive
- axios
- imgur api
- lodash
- dayjs
- 正則表達式
- my-ip.io/api
- 遇到的問題
- https://shareurexp-demo.firebaseapp.com/
- 使用 firebase hosting 的免費空間
npm install
npm run serve
npm run build
npm run lint
- Firebase 命令行界面(CLI)工具,可用於從cmd中測試,管理和部署Firebase項目
- 有Database, Firestore, Functions, Hosting, Storage 可以使用
- 目前只使用Hosting
npm install -g firebase-tools
// 串接Google帳號 (會跳出google視窗)
firebase login
// 在當前目錄中串接並設置一個新的 Firebase 項目(創建 firebase.json)
firebase init
// 串接已存在的 firebase 專案
firebase use --add
// 上傳網站到 firebase
firebase deploy
- 將網站部屬在firebase提供的網址上
- 提供https服務
- 免費限制: 上傳:1GB/月,下載: 10GB/月
- 參考: https://www.minwt.com/website/server/21596.html
firebase init
// 1. 選擇 Hosting: Configure and deploy Firebase Hosting sites
// 2. 選擇一個已有的專案(或創一個新的)
// 3. 輸入public目錄名稱(index.html所在的位置)
// 4. 選擇是否為spa,不是的話就要在firebase.json自己設定route
// 本專案選擇
// 2. Don't set up a default project
// 3. dist
// 4. y
firebase use --add
firebase deploy
- 用於讓express server 實現前端 router 的 history mode
- 不使用的話,非首頁重整就會get不到網頁
const history = require('connect-history-api-fallback');
app.use(history());
- 執行 build 過後的 production 版本的server
const port = process.env.PORT || 80
const express = require('express')
const history = require('connect-history-api-fallback');
const app = express()
app.use(history());
app.use(express.static('dist'))
app.listen(port, () => console.log(`Listening on port ${port}`))
- firebase.js 引入firebase並export給其他檔案用的
- firebase-init.js 自己寫的幾個方法,用於初始化collection
use in other .js, .vue
import { firebase, db, actionCodeSettings } from '../firebase.js'
- 為資料庫設置條件,符合條件才能執行read(get, list), write(create, update, delete)
- 規則特殊定義字request, resource,在設定條件一定會用到
- request可以取得auth的資料,resource可以取得當下路徑的那筆資料
{document=**} 代表該路徑所有資料
// 只允許 user.uid == auth.uid 才能更新
// resource.data 就是取得該路徑的資料
match /users/{userId}/{document=**} {
allow read, create;
allow update: if request.auth.uid == resource.data.uid;
}
// 登入後才能創建新文章
match /articles/{articleId}/{document=**} {
allow write: if request.auth.uid != null
}
- 為firebase寫的 vue store
- 包含獲取、新增、修改、刪除、即時更新(資料更動時)
- 還包含搜尋、排序、分頁
- 要把store中的 state.collection 改成要對應的 firebase collection
- setWatchById 是監控某個特定id用的,目前用在users.currentUser
import { db, firebase } from '../firebase.js'
export default {
namespaced: true,
state: {
collection: 'your-firebase-collection',
data: null,
sort: {
field: 'id',
isAsc: true
},
search: {
text: '',
field: '',
},
pagination: {
currentPage: null,
pagesize: null
}
},
getters: {
getData: function(state) {},
getDataById: (state) => (id) => {},
getSortData: function(state) {},
getSearchData: function(state) {},
getPageData: function(state) {},
getFilterData: function(state, getters) {
// sort => search => page
}
},
mutations: {
setData(state, payload) {},
setSort(state, payload) {},
setSearch(state, payload) {},
setPage(state, payload) {}
},
actions: {
setWatchById({ state, commit }, payload) {},
setWatchDataAction({ state, commit }, payload) {},
getDataAction({ state, commit }, payload) {},
addDataAction({ state, commit, dispatch }, payload) {},
removeDataAction({ state, commit, dispatch }, payload) {},
updateDataAction({ state, commit, dispatch }, payload) {}
},
}
- 列出幾個我覺得需要特別記起來的
- 確認firebase auth是否準備好了
authIsReady: function() {
return this.$store.getters['auth/getIsReady'];
}
- 確認firebase auth是否有用戶登入
authIsSignIn: function() {
return this.$store.getters['auth/getIsSignIn'];
}
- 並非取得firebase auth的使用者,而是自己寫的users的
getCurrentUser: function() {
return this.$store.getters['users/getCurrentUser'];
}
- 確認自創的users的 current user 是否準備好了
- 目前用在顯示nav時的判斷,避免註冊與登入會先顯示出來
getIsCurrentUserReady: function() {
return this.$store.getters['users/getIsCurrentUserReady'];
}
- 因為不確定 firebase auth 的準備時間所以要設置interval去重複確認
- 有使用到的computed: getCurrentUser, authIsReady, authIsSignIn
- 使用 try catch 避免出錯時沒清掉 interval
this.setUserChecker(() => {
// 放確認完 auth 與 currentUser 後想做的事
})
setUserChecker: function(callback, time) {
if(!_.isNumber(time))
time = 500
let userChecker = setInterval(() => {
try {
if(!this.authIsReady)
return
if(this.authIsSignIn == false) {
clearInterval(userChecker)
return
}
// 有登入 一定就會有currentuser 所以要避免因延遲沒執行到
if(this.getCurrentUser) {
callback()
clearInterval(userChecker)
}
} catch {
clearInterval(userChecker)
}
}, time)
}
- 自己寫的component
- data 如果有設置搜尋的話,需要放搜尋後結果
- @change-page: 會回傳currentPage,接回來並更新你的currentPage
<Pagination
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:data="getSearchArticles"
@change-page="changePage">
</Pagination>
- 此modal是用bootstrap寫成的,需引入bootstrap
- id: 打開modal中的 data-target="#id"
- signInHandle: 需要給一個回傳 Promise 的函式
- 回傳Promise是為了在登入時檢查是否登入成功,避免失敗還把modal關掉
<SignInModal
id="SignInModal"
:signInHandle="signIn">
</SignInModal>
<button
data-toggle="modal"
data-target="#SignInModal">
Open Modal
</button>
- 此modal是用bootstrap寫成的,需引入bootstrap
- id: 打開modal中的 data-target="#id"
- signUpHandle: 需要給一個回傳 Promise 的函式
- 回傳Promise是為了在登入時檢查是否登入成功,避免失敗還把modal關掉
- 比登入多寫一個關閉 modal 剩餘秒數的 counter ,為了呈現"幾秒後關閉"
<SignUpModal
id="SignUpModal"
:signUpHandle="signUp">
</SignUpModal>
<button
data-toggle="modal"
data-target="#SignUpModal">
Open Modal
</button>
// conter 顯示幾秒後關閉的
let count = 0;
let countdownTimer = setInterval(() => {
count = count + 1000
this.closeLeftTime = Math.floor((this.closeTimeMS - count) / 1000);
this.message = `${ this.closeLeftTime } 後關閉此視窗`
if(this.closeLeftTime <= 0)
clearInterval(countdownTimer);
}, 1000)
- 這專案有用到的客製 css style
- input 後面的清除按鈕( X圖案 )
- 現在不用了,只要type="search"
// 這樣就好
<input type="search">
.searchclear {
position: absolute;
right: 50px;
top: 0;
bottom: 0;
height: 14px;
margin: auto;
font-size: 14px;
cursor: pointer;
color: #ccc;
z-index: 99;
}
- 用 css 做的簡單文字圖像(要把一個文字放進裡面)
// default
.avatar {
display: inline-block;
box-sizing: content-box;
color: #fff;
text-align: center;
vertical-align: top;
background-color: #e5ecf5;
font-weight: 400;
width: 48px;
height: 48px;
border-radius: 48px;
font-size: 24px;
line-height: 48px;
}
// small (.avatar .small)
.small-avatar {
margin: -2px 5px -2px -6px !important;
width: 24px !important;
height: 24px !important;
border-radius: 24px !important;
font-size: 12px !important;
line-height: 24px !important;
}
- 自己寫的簡單動畫
- 只要用transition直接選 all 或只指定變動的屬性(transition: width 0.3s ease-in-out;)
- 剩下就自己指定事件去做屬性變更即可(ex: focus 就把width變大, blur 就把他小回原大小)
- transition: property || duration || delay || timing-function [, ...];
- transition: all 0.5s ease-in-out; / transition: width 0.5s ease-in-out;
<input type="text"
:style="{ width: inputWidth }"
@focus="widenInputWidth()"
@blur="narrowInputWidth()">
input {
transition: width 0.3s ease-in-out;
}
data: function() {
return {
inputWidth: '100px'
}
},
methods: {
widenInputWidth: function() {
this.inputWidth = '300px';
},
narrowInputWidth: function() {
this.inputWidth = '100px';
}
}
firebase.js
const firebase = require('firebase/app');
require('firebase/firestore');
require('firebase/auth');
const firebaseConfig = {
apiKey: "AIzaSyBu0x2xeq-tZ3kLjtJiRQaY_p_c1Y5bAdo",
authDomain: "shareurexp-demo.firebaseapp.com",
databaseURL: "https://shareurexp-demo.firebaseio.com",
projectId: "shareurexp-demo",
storageBucket: "shareurexp-demo.appspot.com",
messagingSenderId: "36474000873",
appId: "1:36474000873:web:6490cd14126ee22433a778"
};
const actionCodeSettings = {
url: 'http://localhost:8080',
handleCodeInApp: true,
};
firebase.initializeApp(firebaseConfig);
const db = firebase.firestore();
export { firebase, db, actionCodeSettings };
- import css & js in main.js
- 用於置左, 置右 ml-auto, mr-auto(margin-left: auto; , margin-right: auto; )
- .text-center
- { margin: auto }
- .mx-auto (flex)
<div class="d-flex">
<div class="mx-auto">
...
</div>
</div>
- 將這段css加上去 dropdown menu 就可以固定高度且有下拉軸了
.scrollable-menu {
height: auto;
max-height: 200px;
overflow-x: hidden;
}
main.js
import 'bootstrap/dist/css/bootstrap.min.css'
import 'bootstrap'
- import css & js in main.js
npm install --save-dev @fortawesome/fontawesome-free
main.js
import '@fortawesome/fontawesome-free/css/fontawesome.min.css'
import '@fortawesome/fontawesome-free/css/solid.min.css'
in .vue script
import { VueEditor } from 'vue2-editor';
components: { VueEditor }
template
<vue-editor
id="editor"
v-model="content"
useCustomImageHandler
@image-added="handleImageAdded"
></vue-editor>
- v-model 放被選的值(參數)
- label 選擇要顯示的欄位
- :options 放data
- :reduce 把值改成object裡的一個欄位
{ id: 0, name: '' } 要指定值是id,顯示的是name的話 name => name.id
main.js
import 'vue-select/dist/vue-select.css'
.vue
<v-select label="name"
:reduce="name => name.id"
:options="getTags"
v-model="article.tags"
multiple
placeholder="選擇科系(複選)"/>
import vSelect from 'vue-select'
- Promise based HTTP client for the browser and node.js
- 簡易使用Ajax
ex: imgur api post image to https://api.imgur.com/3/image
axios({
url: 'https://api.imgur.com/3/image',
method: 'POST',
'timeout': 0,
'headers': {
'Authorization': 'Client-ID ' + imgurClient.id
},
'processData': false,
'mimeType': 'multipart/form-data',
'content-type': false,
'data': formData
})
ArticleAdd.vue, ArticleEdit.vue (with vue2-editor)
handleImageAdded: function(file, Editor, cursorLocation, resetUploader) {
let formData = new FormData();
formData.append("image", file);
// formData.append("user", this.article.creator);
// imgur api
axios({
url: 'https://api.imgur.com/3/image',
method: 'POST',
'timeout': 0,
'headers': {
'Authorization': 'Client-ID ' + imgurClient.id
},
'processData': false,
'mimeType': 'multipart/form-data',
'content-type': false,
'data': formData
}).then(result => {
console.log(result.data.data.link, result.data);
let url = result.data.data.link;
// 將圖片網址加進使用者資料中
let user = this.$store.getters['users/getDataById'](this.article.creator);
user.images.push(url);
this.$store.dispatch('users/updateDataAction', user);
Editor.insertEmbed(cursorLocation, "image", url);
resetUploader();
})
列出幾個比較常用的
- isEmpty() 判斷是不是空值(null, {}, [], undefined, 數字也會被判斷為空值)
- clonedeep() 深拷貝
import _ from 'lodash'
- 極度輕量的處理時間library
- 目前主要用於處理firebase回傳來的時間格式
- 設置時區:
dayjs.locale('zh-tw')
- 引入的 Plugin: RelativeTime
import dayjs from 'dayjs'
// 設置時區(全域)
import 'dayjs/locale/zh-tw.js'
dayjs.locale('zh-tw')
// 調用時才設(只限這行)
dayjs().locale('zh-tw').format()
// import Plugin
import relativeTime from 'dayjs/plugin/relativeTime'
dayjs.extend(relativeTime)
// 遇到'relativeTime' of undefined 的bug的話 下面是解法(作者: 1.18版修正)
import tw from 'dayjs/locale/zh-tw';
dayjs.locale(tw);
dayjs.locale('zh-tw');
// 放在data 這樣就可以在template直接使用
data: function() {
return {
dayjs: dayjs
}
}
- 你要跨的server 沒有允許你這個網域存取
- 有時候是req header沒設定好 (imgur api ex: {content-type: false, 'mimeType': 'multipart/form-data'})
- 自己的server的話,要設定好跨站存取你那個網域
// set res headers
headers: {
Access-Control-Allow-Origin: '*',
Access-Control-Allow-Headers: 'origin, content-type, accept',
Access-Control-Allow-Methods: 'GET, POST, PUT, DELETE, OPTIONS',
Access-Control-Allow-Credentials: true
}
// or use express extend
const cors = require('cors')
app.use(cors()); // 允許全部跨站
app.use(cors({
origin: 'http://yourapp.com' // 允許指定網域
}))
- 通常是遇到ad block阻擋了檔案、ajax存取(檔案名稱、網域名稱被阻擋,可能包含廣告敏感字)
- 尋找沒被擋掉的web api(ex: my-ip.io/api)
- 找不到就只能自己架server寫api了
export default {
created: function() {
window.addEventListener('scroll', this.handleScroll, , { passive: true });
},
beforeDestroy: function() {
window.removeEventListener('scroll', this.handleScroll, , { passive: true });
},
methods: {
handleScroll: function(event) {}
}
}
- window.scrollY 是取得 scroll 與頂部的距離(最頂部時是 0)
- window.innerHeight 是取得viewport的高度 (outerHeight 是瀏覽器的高度)
- document.body.scrollHeight 是取得body的scroll總長度 (通常跟 document.documentElement.scrollHeight 一樣)
- window.scrollY + window.innerHeight = document.body.scrollHeight
- 取得 window.scrollY 最大值: document.body.scrollHeight - window.innerHeight
- 當到底部時 window.scrollY 自然就會是等於上面那個值
// 避免太舊的瀏覽器沒支援 scrollY (IE之類的)
let scrollY = window.scrollY ||
window.pageYOffset ||
document.documentElement.scrollTop ||
window.scrollTop ||
window.offsetTop
// 到最底部時
if(window.scrollY == (document.body.scrollHeight - window.innerHeight))
// 距離底部 <= 100px
if((document.body.scrollHeight - window.innerHeight) - window.scrollY <= 100)