安裝 Vue Cli:npm install -g vue-cli
(注意這是 2.x 版本)
啟用:vue init webpack my-project
安裝 vue-axios:npm install --save axios vue-axios
注意若你是使用 Vue.js 2.x ,vue-axios 必須使用 2.x 舊版,請在 package.json
中 dependencies
移除 "vue-axios",並安裝 2.x 版本:npm install --save vue-axios@2
如有需要加入 StandardJS…
config/dev.env.js
配置 :
注意!加入環境變數須重啟 npm run dev
,正式上線時需把環境變數也加進 config/prod.env.js
中
App.vue
中測試看看將 API 存入環境變數中能不能使用 vue-axios 發送 Ajax:
安裝 bootstrap:npm install bootstrap --save
回 App.vue
在 <style>
標籤新增 lang
屬性,值為 "scss":
若在此步驟發生錯誤,應該是未安裝 node-sass 及 sass-loader。
安裝:npm install node-sass sass-loader
若還是報錯,應該是版本不符的關係,請把 node-sass 降為 4.x 版,sass-loader 降為 7.x 版。
修正:
package.json
的 "dependencies" 記得移除該兩項目並再安裝:npm install sass-loader@7 node-sass@4
在 src/assets
新增一個 all.scss
配置:
記得 App.vue
中 import 也要改路徑:
在 node_modules/bootstrap
中找到 _variables.scss
複製一份到 scs/assets/helpers
,helpers 資料夾為自行創建,新增此路徑是為了避免與原始套件路徑衝突。
修改 src/assets/helpers/_variables.scss
即可。
注意!若在 <style>
標籤上加入 scoped
代表此段 CSS 會被封裝在該元件內,只會在該元件上有作用!
src/components
新增 pages
資料夾,src/components/pages
新增 Login.vue
,內容自訂
回 router/index.js
,import Login.vue:
直接拷貝一個 bootstrap 的 登入畫面,右鍵檢視原始碼複製貼上到 Login.vue
的 <template>
裡面(記得最外層要用一個 div 包起來),可一併複製原始碼中的 CSS: <link href="signin.css" rel="stylesheet">
在 Login.vue
定義資料結構:
Login.vue
)在輸入帳號及密碼的 input 標籤綁上 v-model
,分別為:v-model="user.username"
及 v-model="user.password"
直接在 <form>
標籤上綁定 @submit.prevent="signin"
事件
因為瀏覽器的同源政策,後端無法透過 set-cookie 將 Cookie 寫入,所以這邊調整為後端將 token 送來並由前端寫入 Cookie,再根據 Axios 文件,把 Cookie 加入 this.$http.defaults.headers.common.Authorization = myCookie;
,這一段會套用至所有 API 所以只需要寫一遍。
讀取 Cookie 寫法:
寫入 Cookie 寫法:
在 Dashboard.vue
(後台)要將 Cookie 取出並將 Cookie 帶入預設傳送的 header 中:
Q:為什麼 sign out 是用 post 發送 Ajax?
A:
定義路由的時候可以配置 meta
字段:
若一個路由有 meta
屬性且配置 { requiresAuth: true }
,代表從其他頁面切換到此路由需要經過認證。驗證手續可透過 導航守衛,說明如下:
導航守衛會在切換頁面時觸發,以下為全局前置守衛語法:
to: Route
:即將要進入的目標路由對象from: Route
:當前導航正要離開的路由next: Function
:一定要調用該方法來 resolve 這個鉤子。執行效果依賴 next
方法的調用參數。
next()
:進行管道中的下一個鉤子。如果全部鉤子執行完了,則導航的狀態就是 confirmed (確認的)。next(false)
:中斷當前的導航。如果瀏覽器的URL改變了(可能是用戶手動或者瀏覽器後退按鈕),那麼 URL 地址會重置到from
路由對應的地址。next('/')
或者 next({ path: '/' })
:跳轉到一個不同的地址。當前的導航被中斷,然後進行一個新的導航。你可以向next
傳遞任意位置對象,且允許設置諸如 replace: true
、name: 'home'
之類的選項以及任何用在 router-link
的to
prop或 router.push
中的選項。next(error)
:(2.4.0+) 如果傳入 next
的參數是一個 Error
實例,則導航會被終止且該錯誤會被傳遞給 router.onError()
註冊過的回調。回 router/index.js
,替路由 path: '/'
配置 meta: { requiresAuth: true }
,代表從其他頁面切換到此路由是需要經過驗證的:
此段程式碼是當使用者自己輸入其他不存在路徑會強制轉回登入頁面。
回 main.js
配置導航守衛:
這邊發送 Ajax 是使用 axios.post(api)
而不是 this.$http.post(api)
是因為該段程式碼在 router.beforeEach()
的執行環境內,而 this.$http.post(api)
只能在元件內使用,所以改用 axios 套件用法。
Q:為什麼導航守衛 router.beforeEach()
是寫在 main.js
而不是 index.js
,官方的教學影片也是設定在 index.js
中,兩者有甚麼差別嗎?
A:導航守衛寫在 router/index.js
和 main.js
都是可行的。
在 src/components
新增 Dashboard.vue
,直接拷貝 bootsrap Dashboard 版型,右鍵檢視原始碼複製貼上到 <template>
複製該版型之 CSS <link href="dashboard.css" rel="stylesheet">
的內容,在 src/assets
新增 dashboard.scss
並貼上,在 src/assets/all.scss
匯入 dashboard.scss
:
回 router/index.js
匯入 Dashboard.vue
:
將 Dashboard.vue
拆解成多個小元件,分別有 Navbar.vue
和 Sidebar.vue
:
Dashboard.vue
內容:
Q:為什麼要把 Dashboard.vue
的網頁內容再拆分成 navbar
和 sidebar
兩元件呢?
A:拆分 sidebar、navbar 可以有較多的彈性,若其他地方有需要 sidebar、navbar 可直接引入,就不用重複寫該區域內容。
Q:為什麼 Navbar.vue
、Sidebar.vue
不用 export 出去?
A:這段是由 Vue Cli 封裝 *.vue
的檔案,而 export 是針對 JS 語法的導出,如果沒有 JS ( Navbar.vue
和 Sidebar.vue
中只有 <template>
標籤而已)就當作為 template 使用。
Vue Cli 是基於 Webpack 開發的,而 .vue 檔案是透過 vue-loader 的方式載入,它會解析檔案中的內容並匯出,在下方的文件中可以看到 “使用 webpack loader 将 <style>
和 <template>
中引用的资源当作模块依赖来处理”, 它會自動將 <template>
和 <style>
轉成 JS,因此 <style>
與 <template>
就不需要另外的 export 語法囉。
Q:為什麼 Dashboard.vue
import 了 Navbar.vue
和 Sidebar.vue
卻又要 export 他們?
A:因為 webpack 所以內容最後都會回到 main.js
,因此在 Dashboard.vue
import 元件使用,最終都還是需要用 export 來給 main.js
編譯。
在 src/components/pages
下新增 Products.vue
,這邊以巢狀路由的方式呈現 Products.vue
回 router/index.js
配置:
回 Dashboard.vue
補上 <routerView>
:
調整一下路徑,HelloWorld.vue
沒有用到,所以在 router/index.js
刪除路由及 import
還記得製作登入頁面 (Login.vue
) 時,後端會傳來一組 token 嘛,我們將這組 token 寫入 Cookie。現在我們在 Dashboard.vue
中的巢狀路由 Products.vue
裡面要取得 品列表,而管理者取得商品的這個 API /api/:api_path/admin/products/all
,是需要透過 token 來驗證的,所以要把這個 token 傳到後端才能取得商品列表:
Products.vue
取得商品列表:在 main.js
匯入 bootstrap 套件:
這時如果有通報以下錯誤:
這時候請先關閉 npm run dev
的狀態並安裝:npm install --save jquery popper.js
Q:為什麼還要再 main.js
匯入 bootstrap 套件,在 App.vue
不是已經匯入 all.scss
(包含 bootstrap.scss
)了?
A:關於 Bootstrap 有分為 JS 和 CSS 部分,JS 部分就是在 main.js
使用。而 Bootstrap 團隊有針對 Webpack 去優化,因此只要附上 import 'bootstrap' 就可以抓到下載的 BS 資源。
複製 bootsrap 的 modal 模板到 Products.vue
裡面,然後在新增產品的按鈕上加上點擊事件,點擊就讓 modal 出現:
記得使用 jQuery 之前要 import $ from 'jquery'
,不然元件會無法認得 $ 符號,至於 bootstrap 的 modal 的相關用法 可參考官方 methods。
Modal 模板可參考 課程部分模板
在 Product.vue
新增 tempProduct
資料,這筆資料是用來存放即將出送給後端的商品資訊,有可能是建立新產品的資料,也有可能是修改舊產品的資料:
觀察新增產品的 API 文件:
依據 API 文件在 modal 中的 input
標籤掛上對應欄位的 v-model
:
特別注意 "is_enabled"
是否啟用欄位的值是 0 和 1,並不是原本 v-model
設置的 true 和 false,所以要額外設置 :true-value="1"
和 :false-value="0"
。
在 modal 中的確認按鈕加上 updateProduct 事件:
新增 updateProduct 事件之後,思考一下,其實點擊新增產品按鈕跟點擊編輯產品的按鈕後所彈出的 modal 需要的 input
欄位都是一樣的,所以新增產品跟編輯產品可以共用這個 modal,都在 updateProduct 事件判斷完後處理
觸發 openModal()
時判斷是要新增產品還是修改舊產品:
注意! openModal()
傳入的 isNew 參數跟 data 中的 this.isNew 是不同的東西,只是名稱相同:
openModal()
傳入的 isNew 是自主傳入 true 或 false,如果是在新增產品的按鈕上就是傳 true,如果是在編輯產品的按鈕上就是傳 false,這樣就能區分到底是新產品還是舊產品:
當觸發 openModal()
的時候,如果是舊產品就把當前 item 寫入 this.tempProduct,但是為什麼要不能寫成 this.tempProduct = item
?
因為物件傳參考的特性, this.tempProduct = item
這行程式碼實際是會讓 this.tempProduct
和 item
指向同一個記憶體位,又因為 v-model
是掛在 this.tempProduct
上面,所以當你更改 modal 裡面 input
標籤的值, v-model
開始作用, this.tempProduct
中的值被更改了,**但是!**連動著產品列表的該 item 中的值也會被改到。
這種寫法的缺點在於當你打開 modal 之後修改裡面的 input
值,修改完後按下 取消 按鈕,畫面中的該 item 的內容是會呈現被修改完的版本,但明明是按下取消的狀態。
解決方法為使用 ES6 的 Object.assign()
,簡言之就是它可以複製一個物件的值(或者合併物件),但可以讓這個被複製出來的新物件指向新的記憶體位址。(此段落為個人見解,若理解錯誤請不吝指正!謝謝)
Object.assign()
語法:
範例:
送出產品資料給後端,製作 updateProduct 事件:
新增產品跟修改產品是用兩個不同的 API 所以要另增判斷式
在 <template>
中加入刪除按鈕和 打開刪除 modal 事件:
openDeleteModal()
事件:
在 #delProductModal
中的確認刪除按鈕加上 deleteProduct()
事件:
deleteProduct()
事件:
在 modal 中找到上傳圖片的 input
標籤加上 @change
事件,當此 input
欄位有改變時就觸發 uploadFile()
方法:
uploadFile()
製作:
若我們上傳了一個檔案,我們可以在 this.$refs
裡面找到一些線索,而該檔案的完整位址是 this.$refs.files.files[0]
。
根據 API 文件,上傳圖片的 API 傳送的檔案格式必須是 form-data,所以這邊透過 Web APIs 的 new FormData
建構子來創造一個空的 FormData 物件,再用 formData.append('欄位名稱', '值')
的語法將值寫入。
當發送上傳檔案的 Ajax 請求時,除了要帶上已經填入值的 FormData 物件,還需要帶上第三個參數:
藉由這個 header 來告訴後端此次傳送的檔案格式是 form-data,後端才看得懂。若沒有設定這個 header 'Content-Type'
的預設值為 application/json
JSON 格式。
完整的 uploadFile()
:
小叮嚀,在將回傳的圖片網址寫入 vm.tempProduct.imageUrl
這個部分,會發生明明有寫入資料,但是在畫面上沒有渲染出來,此時使用 vm.$set(target, key, value)
語法將資料寫入可以改善這個問題。
記得 modal 的 <img>
標籤動態綁定 :src="tempProduct.imageUrl"
圖片才能正確顯示。
Q:我們點擊 <input type="file">
選擇圖片以後,其實就已經把圖片傳給後端了,此時不論有沒有按下表單的確認鍵,圖片都已經在後端的資料庫裡面,因為只要觸發 @change
事件就會觸發 uploadFile()
方法,這樣後端是不是會存入很多用不到的圖片檔案?
A:是的,後端確實會存入很多沒用到的圖檔。
使用 Vue Loading Overlay 套件
安裝:npm install vue-loading-overlay
在 main.js
import 此套件:
vue-loading.css
要 import 在 main.js
或 src/assets/all.scss
裡面都可以
啟用,在 Products.vue
的 <template>
裡面的的最上方加上 <loading :active.sync="isLoading"></loading>
:
在 data 新增 isLoading
變數,當 isLoading
為 true 時就會有讀取中的效果:
在有 Ajax 行為的時候調整 isLoading
的真假值:
在 index.html
的 <head>
標籤中引入 CDN
data 資料新增 status 變數:
從 Font Awesome 自選一個歡的 Animating Icon,這裡選的是 <i class="fas fa-spinner fa-spin"></i>
在 <template>
中綁定 v-if
:
在自己想要放的地方替換 fileUploading
真假值,這邊是用在上傳圖片的方法中:
看註解!
目的:Event bus 直接掛載在 Vue 的原型下來直接操作這個 AlertMessage.vue
。
此小節節錄並改寫自 itsems。
在 Vue 的組件之間,常見的父子組件溝通方式為 props、emit,但如果是兄弟姊妹組件、跨組件之間的溝通,為了避免繁複的 props emit,就可以使用 Event Bus
來做。Event Bus
像是一個全域的事件,可以給各組件共用,適用沒有太複雜的專案和事件,如警示視窗。
要在專案中使用 eventBus 其實就是在 Vue 裡面再註冊一個 Vue 實例,eventBus 主要會使用到 Vue 實例中的四種方法:
$on
:註冊監聽
$once
:只監聽一次
$off
:取消監聽
$emit
:送出事件
建議在頁面 created 的時候就註冊監聽,並在組件銷毀前取消監聽。
範例中我們嘗試製作一個 alert.vue 組件,作為一個全域的警示通知,不管在哪個組件中都可以把這個 alert 事件呼叫出來。
在 src/components
下新增 AlertMessage.vue
,內容可直接複製貼上課程模板,created()
部分的註解記得打開。
回 Dashboard.vue
import AlertMessage.vue
:
將 <AlertMessage>
放入 <template>
:
到這個步驟,畫面不會有任何變化,預設是不顯示的
/src
下新增 bus.js
並配置:
回 main.js
import bus.js
:
在想要的地方提供錯誤提示,這邊是在上傳圖片失敗時(錯誤格式或者圖檔太大),加入 this.$bus.$emit('message:push', '自定義提示文字', 'bootstrap主題樣式')
觀察 取得商品 的 API 會發現發送 Ajax 成功後會回傳 pagination 資訊:
此案例是後端直接傳來的分頁資訊設計良好,前端不用再做很多判斷。
回 Products.vue
在 data 新增 pagination 變數:
在取得商品資訊 getProducts()
後,把回傳的 pagination 存入 this.pagination
:
複製 bootstrap 的 paginaion 模板,並貼在 <talbe>
表格標籤後面
邏輯:
頁碼:
後端傳來的 pagination.total_pages
數值得知總共幾頁,用 v-for
印出,:key="page"
用頁碼綁定。
後端傳來的 pagination.current_page
數值得知目前所在頁碼,用 :class
加上 'active' 的 class。
點選時切換頁碼:將當前頁碼當作參數傳入 getProducts()
上下頁按鈕:
pagination.has_next
和 pagination.has_pre
得知上下頁按鈕存不存在,再用 :class
加上 'disabled' 的 class。pagination.current_page
加減數值 1 的結果傳入 getProducts()
作為參數。getProducts()
中使用的 API 需要輸入頁碼來請求該頁的產品資料,而這個頁碼是自己傳入,這邊利用 ES6 的預設參數:
此小節內容可以做成一個 pagination 元件,可參考此範例。
目的:在顯示價格時,增加千分號及 $ 符號。
src/filters
下新增 currency.js
配置:
回 main.js
import currency.js
並啟用:
在 Products.vue
中套用:
客戶端行為與管理者端行為大同小異,故前臺畫面製作直接提供模板參考。
src\components\CustomerOrders.vue
<loading :active.sync="isLoading"></loading>
等待中動畫標籤記得要加。
動態綁定 style,利用字串模板和 background-image
CSS 屬性替產品加上圖片。
取得單筆資料 API:/api/:api_path/product/:id
當點擊 查看更多
按鈕會再另行彈出一個 modal 顯示產品詳細內容
data 新增 product 變數存取當前點擊的產品資訊:
在 查看更多
按鈕上新增事件,點擊時就將當前 item.id 傳入方法:
串接 API,事件處發時讓 modal 出現:
增加使用者體驗,點擊 查看更多
按鈕時按鈕旁邊出現轉圈圈動畫:
data 新增 status 變數:
vm.status.loadingItem
的值寫入傳入的 id
FontAwesome 的 <i>
標籤上新增 v-if
條件:
加入購物車 API:/api/:api_path/cart
參數:{ "data": { "product_id":"-L9tH8jxVb2Ka_DYPwng","qty":1 } }
addtoCart()
加入購物車方法:
在 HTML 按鈕上加入事件:
modal 裡面的加入購物車按鈕,此選單可選擇商品數量:
使用 v-model
去判定使用者選擇的數量
<option>
標籤 value 記得用動態綁定方式
@click="addtoCart(product.id, product.num)"
參數來自於打開 modal 就將該產品資訊存入的 this.product
取得購物車 API:/api/:api_path/cart
getCart()
:
記得加入完購物車須重新取得購物車資料,在 created()
的時候也要將購物車資料取回。
### 刪除某一筆購物車資料
當最終價格不等於沒有套用優惠券價格時顯示:
目前 commit 進度 CustomerOrders.vue
:
API:
新增 createOrder()
方法並添加 submit 事件在表單上
安裝:npm install vee-validate --save
回 main.js
import 套件
<input>
驗證元件<form>
驗證元件<input>
使用 <validation-provider>
元件標籤包覆 <input>
段落:
v-slot
傳入的 classes 用來在 <input>
上用 :class
綁定 bootstrap 樣式。
classes 由 main.js
引入時寫入:
<form>
使用 <validation-observer>
元件標籤包覆 <form>
段落:
如果驗證未通過則 disabled 該按鈕。
資料結構:
Pagination.vue
頁碼元件製作先將外部資料傳給 Pagination 元件:
Pagination 元件接收資料
製作 Pagination 元件 <template>
利用 props 接收的資料對頁碼 HTML 設計一下:
:class
加上 disabled class。v-for
製作,且被選定時要加上 active class。由於渲染於畫面的資料是經由 computed 運算(this.pagination.currentPage
有寫在裡面),當發生 updatePage(emittedPage)
資料跟畫面會同步更新。
decodeURIComponent()
小叮嚀這邊從首頁點選熱門產品分類連結進入產品列表頁面是透過 query string 方式再用 this.$route.query.category
去取出 query string 的值再將 this.filter.category
替換掉方能正確顯示分類,而若傳入 query string 中的值帶有空白鍵或其他符號的話,經過編譯可能會與原本不相同,這是利用 decodeURIComponent()
可讓經過編譯的 query string 還原成原字串。
我是選擇 Standard,可參考此篇詳細解說。