# 前端導入 Vue 建議事項
###### tags: `Vue` `SPA` `SEO` `RWD` `Vuex`
## 吃完 Vue 全家餐,實現所有願望
Vue.js 在處理前段架構解決方案時,仿間有一套俗稱「全家餐」的套件,套件組成如下:
* vue
* vuex(狀態管理)
* vue-router(路由)
* axios(http請求工具)
* vue-cli(建構工具)
另外建議可以使用的插件
* VeeValidate (表單驗證)
* Yup (檢核庫)
* Nuxt.js (處理SEO的靜態渲染)
* Webpack (打包程式)
* Bootstrap (模板)
* bootstrap-vue (vue專用的bootstrap不須載入jquery)
從瀏覽器發出 Request 處理,到前端處理邏輯驗證,一路到後端 API 存取,一個完整的解決方案,十分推薦!
## 建議導入 SPA
如果網站服務很要求使用者體驗,希望操作起來更順暢,同時能做多工,這時就很推薦導入 SPA,
但導入後,由於 SPA 只有單一頁面,初次載入即下載全部內容,導致首次下載速度變慢,外加 SPA 裡面只有放程式邏輯,會讓 SEO 搜尋不到內容,導致網站排名下滑,故此建議下列解決辦法:
* 加快首次下載速度
透過 lazyLoad 的設計,把程式邏輯跟元件延後下載,減輕首次載入負擔
* 不支援 SEO
建議透過 靜態化 套件,將網頁產生靜態頁面,讓 SEO 可搜尋到內容,詳細做法可參閱[連結](#導入-SPA-架構後,解決-SEO-的建議做法)
如何導入 SPA 實際作法如下:
* 用vue-cli建立專案
vue-cli 為官方提供的開發工具,用於快速開發大型 SPA 網站,省去瑣碎的工作,快速建立起開發環境
* 建立專案時選用vue-router
Vue Router 就是由前端模擬的路由,跟過去不同的是,換頁通常會由後端處理。而 Vue Router 存在就是為了實現 SPA(單頁式應用),當我們切換畫面時候就不需要向後端發出請求。
## 建議導入狀態管理 Vuex
Vuex採用單向數據流的概念,避免系統龐大下數據流混亂的情況,讓資料狀態是清晰可以預測的,便於維護與擴充;當元件遇到多個元件共享狀態,單向數據流的簡潔性很容易被破壞,因此 vuex 將元件的共享狀態抽取出來,以一個**全域單例模式管理**
**Vuex的單向數據流**
通過元件執行 Actions , Actions 執行 Mutations , Mutations 更新 State 的狀態,更新後會觸發頁面資料更新,所有的狀態集中管理,所有的元件都能通過這個入口改變 state 中的資料

在 單向數據流中,Vuex 能為我們帶來幾點好處:
* 直觀的運用狀態
* 結構清晰
* 防止重新載入元件時重複發送http請求
* 不用擔心元件結構出現多層時導致的數據流混亂
**Q:為何透過 Actions 來更新 Mutations 而不是直接更改 Mutations ?**
A:因為 Mutation 只支持同步操作,不支持異步操作,因此所有的異步操作(ex: ajax 請求, setTimeout, Promise 等) 都要放在 Action 中執行。
**Q:為什麼 Mutation 不支持異步操作呢?**
A:因為在 devtools 中會紀錄 Mutations 執行時傳進的 payload 以及改變的 state ,當在 Mutations 進行異步操作時,devtools 不知道 State 什麼時候被改變了,會出現資料不同步並且難以追蹤的狀況。
**Q:如果不按照上面兩點來執行,有什麼防範措施?**
A:添加 strict: true 使 Vuex 存儲進入嚴格模式,在嚴格模式下,任何變異處理函數以外修改 Vuex state 都會拋出錯誤。
```
import txtModules form ./product;
export default new Vuex.Store({
strict: true,
modules: {}
})
```
## 導入 SPA 架構後,解決 SEO 的建議做法
SPA 會由單一頁面入口,透過 router 決定渲染的內容,在未做優化的情況下爬蟲只會爬到入口頁面的資料,這樣搜尋引擎優化的效果會不盡理想,如果要提高搜尋引擎內的能見度,需要另外將網頁的內容做處理,有以下幾種做法
解決做法如下:
* 靜態化 (最推薦)
優點
- 純靜態文件,訪問速度快
- 對比 SSR,不涉及到服務器負載方面問題
- 靜態網頁不易遭到駭客攻擊,安全性更高
缺點:
- 如果動態路由參數多的話不適用
* SSR 服務器渲染
* 開發條件所限,瀏覽器特定的代碼,只能在某些 lifecycle hook 中使用,一些 external library 可能需要特殊處理,才能在 Server 渲染應用程序中運行
* 環境和部屬要求更高,需要 Node.js server 運行環境
* 高流量的環境下,請準備對應的服務器負載,並明智地採用緩存
* 預渲染 prerender-spa-plugin
優點
- 改動小,加個插件就完成
缺點
- 無法使用動態路由
- 只適用少量頁面的項目,頁面多達幾百個的情況下,打包會很慢
* 使用 Phantomjs 對爬蟲做處理
優點
- 完全不用改動項目代碼,按原本的 SPA 開發即可,對比開發 SSR 成本小的多
- 對已用 SPA 開發完成的項目,這是不二之選
缺點
- 部屬需要 node 服務器支持
- 爬蟲訪問比網頁訪問要慢一些,因為定時要定時資源加載完成才返回給爬蟲
在不使用 node.js server 的情況下
可以使用靜態化或預渲染
## UI/UX 套件建議使用 Bootstrap
前端頁面開發時,可以使用 UI/UX 套件減少開發的時間,建議可以使用通用性比較高的bootstrap , bootstrap 有兩大優點:
* Grid System
* bootstrap 有預設不同尺寸的樣板,在開發上可以針對需求直接使用預設的標籤
* 在設計響應式網頁時,可以依照不同螢幕寬度使用不同的標籤,使排版更精簡
* UI
* 避免重工,可以應用一些現成的功能
* 較複雜的響應式設計則很快就可以應用
* 在不同專案中、或是同專案不同開發者
* 可以在開發過程中取得應用及解讀程式碼的一致性
* Bootstrap 也提供了一些速成樣板
* 確保跨瀏覽器的閱讀相容性
Bootstrap 是以 jquery 為基底使用的
如果想要使用 bootstrap 又不想加載 jqeury
可以使用 bootstrap-vue
## 建議導入 Validation schema
前端表單把資料傳到後端前,會檢查資料的正確性,處理這部分的流程建議使用 Validation schema。
傳統的表單驗證,無法處理多欄位連動檢核,外加檢核條件跟表單綁在一起,若在不同表單中有相同驗證邏輯,必須將相同邏輯複製過去,外加日後需要變更時,需要更改不同地方,容易造成遺漏,處理起來很麻煩。
透過 Validation schema + yup + vuex 不僅能做到多欄位連動的檢核,透過 yup 檢核庫自訂的檢核邏輯,同時可檢核多個相同邏輯的資料,減少程式出錯率。
建議做法
* Yup (檢核庫)
yup 是針對數值驗證的檢核庫,針對客戶端的資料做驗證,yup 將驗證功能切分成單獨的部分,每個部份可以單獨使用或者合併使用,透過合併使用的方法去做到多欄位連動。
詳細可參考:[連結](https://github.com/jquense/yup)
* VeeValidate (表單驗證插件)
vee-validate 可以很輕鬆地處理複雜的驗證邏輯,並支持同步、異步驗證,並支援 yup 的 Validation schema 驗證,可輕鬆完成表單驗證。
詳細可參考:[連結](https://vee-validate.logaretm.com/v4/guide/validation)
## 建議導入打包工具 Webpack
現今的很多網頁其實可以看做是功能豐富的應用,它們擁有著複雜的 Javascript 程式碼和一大堆依賴包。為了簡化開發的複雜度,可以使用 webpack 之類的打包工具
建議原因:
* 兼容 CommonJS & AMD & ES6 模組規範
* Bundle 效率高
* 可編譯語法和打包資源 (css, img, font…)入 JS 內
* JS 程式碼封裝
* 網路資源豐富、社群強大,Plugin 多
vue cli 提供六種基本樣板如下:
- webpack
- webpack-simple
- browserify
- browserify-simple
- pwa
- simple
多使用 webpack 於新增專案時選擇 webpack 即可
## 撰寫 Vue 語法上幾個 Tips
俗話說的好,拿別人的錯誤的經驗,就能減少走錯路的機會,程式開發時也是如此,撰寫 Vue 時,有幾條應該要遵守的慣例,描述如下:
注意事項:
1. **在使用 v-for 時,永遠要綁定 key 值**
如果是透過 vue-cli 打造的專案,使用 v-for 卻沒有綁定 key 時,系統會報錯提醒,例如:
```
<div v-for="item in 5"> {{item}}</div>
```
直到把程式碼修改完成,才會停止錯誤。
```
<div v-for="item in 5" :key="item.id"> {{item}}</div>
```
為何會發生這個問題呢?想像一下,當我們不綁定 key 時,如果產生了十個 li 節點,要怎麼只控制其中某個節點,綁定 key 用意,在於 v-for 產生的組件都是**唯一的**,以利日後開發。
1. **維持固定的寫法**
使用 v-on & v-bind 或是 slot 時,常常會搭配對應的縮寫
* '@' -> v-on
* ':' -> v-bind
* '#' -> slot
這是相當方便、且可讀性高的寫法,但有時候在同一個專案同時出現 v-on:click&@click 的狀況,這就相當惱人,跟 Code Style 依樣,要馬就全部用縮寫,不然就不要使用。
1. **避免 v-for & v-if 使用在同一個組件上**
使用 v-for 時,你想加入一些條件判斷讓他只顯示部份內容,可能程式會這樣撰寫。
```
<h1>Admin Users</h1>
<div v-for='user in users' v-if='uesr.isAdmin'>
```
但實際運行時,會發覺有些問題,主因是 v-for 優先度高於 v-if,也就是會先迭代所有元素後,最終才檢查 v-if 的部分,如果有類似需求,建議使用 computed 屬性來達到效果。
```
<div v-for="user in adminUsers" :key="user.key"> {{user.name}} </div>
computed: {
adminUsers() {
return this.users.filter(user=> user.isAdmin)
}
}
```
1. **永遠檢查傳入的 props**
父子層溝通時很時常透過 props 屬性,為求方便承接參數時,並不會額外做檢查,就可能寫出這段程式。
```
props: ['currentUser', 'products']
```
但實際上不應該完全相信傳進來的資料,就跟後端會再重新驗證前端傳來的資料是一樣概念,適當的檢查可避免很多錯誤,上述範例建議可改寫為以下
```
props: {
currentUser: {
type: Object,
default: null,
required: true,
},
products: {
type: Object,
default: [],
required: true,
},
}
```
1. **不要在 template 中塞入太複雜的邏輯**
很多時候我們使用 view 操作時,在元素上常常會加入一些基本 js 邏輯,例如:
```
<h1> {{user.name}} </h1>
```
但請避免在 template 中塞入太多複雜得 js 邏輯,像是:
```
<h1> {{user.name.split(' ').reverse().join('')}} </h1>
```
如同上面 v-for & v-if 情況依樣,最好使用 computed 來處理
```
<h1> {{reversedName}} </h1>
computed: {
reversedName() {
return this.user.name.split(' ').reverse().join('')
}
}
```
1. **避免直接去操作DOM元素**
vue 會透過 virtual DOM 的概念去處理資料變動時的畫面刷新,不僅可更有效率的處理刷新,也可節省許多資源,因此我們應該避免直接操作 DOM,如過真的要選取,建議使用 ref 屬性:
```
<h1 ref="title"> Hello World </h1>
this.$refs.title // 利用這樣去選取該元素並作需要的操作
```
## 後端 API 串接時,建議幾種套件
vue 取得後端資料有以下方法
* vue-resource(原官方函式庫)
* Axios
* jQuery ($.ajax, $.get, etc.)
* Fetch API
Vue 原作者推薦使用 axios 替代原本官方的 vue-resource,
現在主要都使用 axios , axios 對 nodejs 的支援度較高
有以下特點
1. 在瀏覽器中發送 XMLHttpRequests 請求
1. 在 node.js 中發送 http請求
1. 支持 Promise API
1. 攔截請求和響應
1. 轉換請求和響應數據
1. 取消請求
1. 自動轉換 JSON 數據
1. 客户端支持保護安全免受 CSRF/XSRF 攻擊
## 使用 Router 的注意事項
* 如何防止使用者未登入就拜訪頁面:
在 routers 內需要驗證的項目上加上屬性
meta: { requiresAuth: true}
* 頁面只異動部分區塊的話可以用嵌套路由,加上 children 屬性
* Component 建議使用 Lazy Loading 的方式加載
例:component: ()=>import("@/view/pageB")