# Vue-cli
###### tags: `Vue`
>1. 基於 Webpack 所建置的開發工具
>2. 便於使用各種第三方套件 (BS4、Vue Router)
>3. 可運行 Sass、Babel 等編譯工具
>4. 便於開發 SPA
>5. 簡單設定就能搭建開發時常用環境
## 安裝
:::info
需先安裝 Node.js
:::
### 基礎指令
```cmd
<!-- 安裝 Vue Cli -->
npm install -g vue-cli
<!-- 版本 -->
vue --version
<!-- 查看可使用指令 -->
vue
<!-- 樣板,主要使用 Webpack -->
vue list
<!-- 建立專案樣板 -->
vue init <template-name> <project-name>
<!-- 安裝套件 -->
npm install
<!-- 運行 Vue 開發環境 -->
npm run dev
<!-- 運行正式環境 -->
<!-- 此時會出現 dist 資料夾,所有東西打包壓縮 -->
<!-- build 的檔案只能運行在 HTTP 伺服器下,無法直接打開 -->
npm run build
```
## Vue Cli 所產生的資料夾結構
![file](https://i.imgur.com/7ZaLXdh.png)
* build: Webpack 設定檔
* config: Vue 應用程式設定檔
* dist: npm run build 產生,只能在 HTTP Server 下運行
* src: 最重要資料夾,檔案將會被編譯
* main.js: Webpack 設定 main.js 作為進入點
* assets 會針對特定大小的圖片編譯成 base 64
* static: 放入不會被編譯的檔案
* .vue: 元件,包含 x/template、js、style
* .babelrc: 替 ES6 編譯的設定檔
* .postcssrc.js: 替 CSS 編譯加入前啜詞的設定
> 詳細資料設定看 [Vue 課程 69](https://www.udemy.com/course/vue-hexschool/learn/lecture/10415930#questions)
## 運行解說
![Webpack](https://i.imgur.com/yJsBAXg.png)
Webpack 會使用 loader 工具,將 .sass、.vue、.jpg 檔呈現在 main.js。
Webpack 會一直監控 main.js 並且輸出。
## 安裝 Bootstrap
因為 Vue Cli 沒有完整的 sass loader,所以需要加上後面這段
```cmd
npm install bootstrap node-sass sass-loader --save
```
sass-loader 建議安裝舊版本,才能順利運行
```cmd
npm install --save-d sass-loader@7.1.0
```
### 載入 BS4
```htmlmixed
<style lang="scss">
@import "~bootstrap/scss/bootstrap";
</style>
<!-- scoped 只在當下元件有作用,不會影響其他元件 -->
<style scoped>
</style>
```
## Vue-axios
[https://www.npmjs.com/package/vue-axios](https://www.npmjs.com/package/vue-axios)
```cmd
npm install --save axios vue-axios
```
### 將下列加入到 main.js
```javascript
import Vue from 'vue'
import axios from 'axios' // 主要 AJAX 套件
import VueAxios from 'vue-axios' // 將它轉為 Vue 套件
Vue.use(VueAxios, axios)
```
### 取得遠端資料
> [Random User API](https://randomuser.me/)
>
```javascript
created() {
this.$http.get("https://randomuser.me/api/").then(response => {
console.log(response.data);
});
}
```
## Vue Router
[https://router.vuejs.org/zh/installation.html](https://router.vuejs.org/zh/installation.html)
在建立專案時,應該就可以直接裝 Vue router 了
```cmd
npm install vue-router --save
```
```
<!-- 進入點 -->
main.js
<!-- Router 配置檔案 (前端路由) -->
router/index.js
<!-- 分頁內容 -->
Vue.components (**.vue)
```
src/router/index.js
```javascript=
// 官方的元件
import Vue from 'vue';
import VueRouter from 'vue-router';
// 自訂的元件
import Home from '@/components/HelloWorld';
import Page from '@/components/pages/page';
import child from '@/components/pages/child';
import child2 from '@/components/pages/child2';
import child3 from '@/components/pages/child3';
import Menu from '@/components/pages/menu'
Vue.use(VueRouter); // 啟用
export default new VueRouter({
// routes 的 name 可加可不加
routes: [{
name: '首頁', // 元件呈現名稱
path: '/index', // 對應的虛擬路徑
component: Home, // 對應的元件
},
{
name: '分頁', // 元件呈現名稱
path: '/page', // 對應的虛擬路徑
// component: Page,
components: {
default: Page,
menu: Menu
}, // 對應的元件
children: [{
name: '卡片1',
path: '',
component: child,
},
{
name: '卡片2',
path: 'child2',
component: child2,
},
{
name: '卡片3',
path: 'child3',
component: child3,
}
]
}
]
})
```
main.js
```javascript=
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue';
import App from './App';
// 將 router 的 index.js 掛進來
import router from './router';
import axios from 'axios'; // 主要 AJAX 套件
import VueAxios from 'vue-axios'; // 將它轉為 Vue 套件
Vue.use(VueAxios, axios);
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
components: { App },
template: '<App/>',
router
})
```
### router-view、router-link
router-view: 元件切換的地方
router-link:
:::success
- 默認渲染成 a 標籤
- tag: 可指定渲染成別的標籤,例如 tag="li"
- replace: 改用 history 的 replaceState,切換路由時,則無法使用上下頁。
- router-link-active: 當前路由是 router-link 路由時,會對 router-link 加入 router-link-active 的 class。可以在 router-link 標籤內加入 active-calss = "active",這樣就能替換掉預設的 router-link-active 改為自定義的。也能在 router 資料裡的 index.js 設置 linkActiveClass = 'active',這樣全局都會更改
:::
```htmlmixed
<!-- 兩種切換方式 -->
<router-link :to="{name: '首頁'}" >Home</router-link>
<router-link to="/Page">Page</router-link>
```
### 巢狀路由頁面
```javascript
// 在 index.js
{
name: '分頁2',
path: '/page2',
component: Page2,
// 巢狀
children:[
{
name: '卡片1',
path: '', // 沒有寫就是預設
component: child
},
{
name: '卡片2',
path: 'child2', // 這裡 path 不能加斜線
component: child2
},
{
name: '卡片3',
path: 'child3',
component: child3
}
]
}
```
### 動態路由
index.js
```javascript
{
name: '卡片3',
// 補上:id 成為動態路由
path: 'child/:id',
component: child3
}
```
child3.vue
```javascript
export default {
data() {
return {};
},
created() {
const id = this.$route.params.id;
// ?seed= 是 random user 提供的方法
// $route 是路由對象,params 是取得參數,id 則對應到路由配置頁面
this.$http.get(`https://randomuser.me/api/?seed=${id}`).then(response => {
console.log(response);
});
}
};
```
### 命名路由、載入兩個元件
[Vue 課程 76](https://www.udemy.com/course/vue-hexschool/learn/lecture/10416064#questions/8140341)
```htmlmixed
<router-view name="menu"></router-view>
```
index.js
```javascript
import Menu from '@/components/pages/menu'
;
{
// name: '分頁', 如果 child 已有預設,則不能設置 name
path: 'page',
// components 載入多個元件
components: {
// default 會對應到沒有使用 name 的 router-view
default: Page,
// 會對應到有使用 name 的 router-view
menu: Menu
},
children:
[
{
name: '卡片 1',
path: '',
component: child
}
]
}
```
### Q&A
>[https://www.udemy.com/course/vue-hexschool/learn/lecture/10416064#questions/8204684](https://www.udemy.com/course/vue-hexschool/learn/lecture/10416064#questions/8204684)
>
>[https://www.udemy.com/course/vue-hexschool/learn/lecture/10415924#questions/6833718](https://www.udemy.com/course/vue-hexschool/learn/lecture/10415924#questions/6833718)
>
### 切換路由的方法
[Vue 課程 78](https://www.udemy.com/course/vue-hexschool/learn/lecture/10416084#questions/8140341)
# API
>[六角學院 Vue 課程練習 API 申請](https://vue-course-api.hexschool.io/)
>
>[六角學院 Vue 一個電商網頁 課程的 API 說明文檔
](https://github.com/hexschool/vue-course-api-wiki/wiki)
## 環境設定
dev.env.js 會跟 prod.env.js 有不一樣的 API 路徑,一個是測試一個是正式環境
/config/dev.env.js
```javascript
'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"',
// 自己設定的 API 路徑
APIPATH:'"https://vue-course-api.hexschool.io"',
CUSTOMPATH:'"kenapi"'
})
```
## 取得 API 資料
使用 Vue 的 [axios](https://www.npmjs.com/package/vue-axios) 方法
:sunny: 注意取得 環境變數的寫法 process.env.{名稱}
```javascript=
created() {
const api = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/products`;
console.log(process.env.APIPATH, process.env.CUSTOMPATH);
this.$http.get(api).then(response => {
console.log(response.data);
});
}
```
## 載入 BS4 並客製化
在 /assetes 下新增 `all.scss`
在 /src 下,新增 helpers/_variables.scss
:sunny: 使用 helpers 資料夾主要是避免原始路徑衝突
all.scss 需要 import
```sass
@import "~bootstrap/scss/functions";
@import "./helpers/_variables";
@import "~bootstrap/scss/bootstrap";
```
## 儲存 cookies
需在 main.js 加入這段,才能正確儲存 cookies
```javascript
axios.defaults.withCredentials = true;
```
```javascript
// 需加上 admin
methods: {
signin() {
const api = `${process.env.APIPATH}/admin/signin`;
const vm = this;
this.$http.post(api, vm.user).then(response => {
console.log(response);
console.log(response.data);
if(response.data.success){
vm.$router.push('/index');
}
});
}
}
```
## 導航守衛
>[導航守衛](https://router.vuejs.org/zh/guide/advanced/navigation-guards.html#%E5%85%A8%E5%B1%80%E5%89%8D%E7%BD%AE%E5%AE%88%E5%8D%AB)
>[路由元信息](https://router.vuejs.org/zh/guide/advanced/meta.html)
main.js
```javascript
// to: 即將要進入的頁面
// from: 從哪個頁面過來
// next: 到達下一個頁面
router.beforeEach((to, from, next) => {
// ...
})
```
## 避免使用者未驗證就跳頁
```javascript
// 放在需驗證的元件 routes 內
// meta: 路由訊息
meta: { requiresAuth: true }
// 例如
{
name: '登入',
path: '/login',
component: Login,
meta: { requiresAuth: true }
}
```
實作
```javascript
// main.js
router.beforeEach((to, from, next) => {
// ...
console.log('to:', to, 'from:', from, 'next', next);
// 如果進入的畫面需要驗證,則執行 check 這個 API 程式
if (to.meta.requiresAuth) {
console.log('需要驗證');
const api = `${process.env.APIPATH}/api/user/check`;
// 需改成 axios。this.$http 是元件才有的方法
axios.post(api).then(response => {
console.log(response.data);
if (response.data.success) {
// 如果成功則進入下一頁
next();
} else {
// 否則轉回 login
next({
path: '/login'
})
}
});
} else {
next()
}
})
```
## 避免使用者亂輸入進入不存在的頁面
```javascript
// 在 index.js 的 routes 下加入
routes: [
{
path: '*',
redirect: 'login'
},
...
]
```
## 引入套件注意
```javascript
// 引入 BS4 可以直接在 index.js 裡 import
// 但可能會要額外安裝 BS4 的 JS 檔案
import 'bootstrap';
```
```javascript
// 引入 jQuery 時,可以直接在 index.js 裡
// 直接將 jQuery 掛載到全域
// 如果全域沒有掛載,則要在每個元件上都要 import
import $ from 'jquery'
window.$ = $;
// 如果有啟用 ESLint 則要加入下面這段
/* global $ */
```
## 串接上傳 API
使用 change
```htmlmixed
<input type="file" id="customFile" @change="uploadFile" class="form-control" ref="files" / >
```
>參考 [MDN new FormData()](https://developer.mozilla.org/zh-CN/docs/Web/API/FormData/FormData)
```javascript
uploadFile() {
const vm = this;
// 取得檔案位置
const uploadedFile = this.$refs.files.files[0];
// 使用 Web API,模擬表單送出
const formData = new FormData();
// 將欄位新增進去,會對應到你要 post 的 六角API 所設計的欄位
formData.append('file-to-upload', uploadedFile);
// 對應路徑
const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/admin/upload`;
// 傳送,但要設定格式
this.$http.post(url, formData, {
headers: {
// 取自 六角API
'Content-Type': 'multipart/form-data'
}
}).then((response)=>{
console.log(response.data);
if(response.data.success){
// 這樣子會沒有 getter、setter,所以不會雙向綁定
// vm.tempProduct.imageUrl = response.data.imageUrl;
// 使用 $set() 強制寫進去才能雙向綁定
// 寫入位置、屬性、值
vm.$set(vm.tempProduct, 'imageUrl', response.data.imageUrl);
}
})
}
```
## 讀取效果
>[vue-loading-overlay](https://github.com/ankurk91/vue-loading-overlay)
>
### 安裝
```cmd
npm install vue-loading-overlay --save
```
### 使用
main.js 加入
```javascript
import Loading from 'vue-loading-overlay';
// Import stylesheet
import 'vue-loading-overlay/dist/vue-loading.css';
// 在全域啟用
Vue.component('Loading', Loading);
```
在元件中
```htmlmixed
<!-- 雖然有很多設定,但留這個就能用了,詳細看文件 -->
<!-- 把這段放在上層即可 -->
<!-- 主要是要使用 isLoading 來決定是否開啟關閉 -->
<loading :active.sync="isLoading"></loading>
```
### Fontawesome
>[動態 icon](https://fontawesome.com/how-to-use/on-the-web/styling/animating-icons)
直接以 CDN 較為方便,npm 需要安裝字體等等較為複雜
使用方式可用 v-if 來決定是否開啟
```htmlmixed
<link rel="stylesheet"
href="https://use.fontawesome.com/releases/v5.1.0/css/all.css"
integrity="sha384-lKuwvrZot6UHsBSfcMvOkWwlCMgc0TaWr+30HWe3a4ltaBwTZhyTEggF5tJv8tbt"
crossorigin="anonymous">
```
## 回饋錯誤訊息
>設定複雜,參考[課程 96](https://www.udemy.com/course/vue-hexschool/learn/lecture/10896750#overview)
## VeeValidate
表單驗證套件
>參考:
>[VeeValidate](http://vee-validate.logaretm.com/v2/)
>[驗證事件](http://vee-validate.logaretm.com/v2/guide/events.html#changing-default-events)
### 安裝
```cmd
<!-- 官方最新,但可能出錯 -->
npm install vee-validate --save
<!-- 先用這個 -->
npm install vee-validate@2.2.15 --save
```
main.js
```javascript
import VeeValidate from 'vee-validate';
// 啟用
Vue.use(VeeValidate);
```
### 使用
驗證姓名
```htmlmixed=
<label for="username">收件人姓名</label>
<!-- 使用 required ,所以是不得為空-->
<!-- 可以給予 :class 當 errors.has('name') 為 true 被觸發後
讓 input 框框也變為紅色'-->
<input v-validate="'required'" :class="{'is-invalid':errors.has('name')}" name="name" type="text">
<!-- span 是底下紅字 -->
<!-- errors 是套件提供的變數 -->
<!-- has('') 裡的名稱會對應到 input 的 name 裡的名稱 -->
<!-- 觸發是當 input 被觸發後,如果 name 的名稱不存在,
則會出現 errors 錯誤 (此時 errors.has('name') 會是 true)-->
<span class="text-danger" v-if="errors.has('name')">姓名必須輸入</span>
```
### 驗證 Email (特殊)
```htmlmixed=+
<label for="useremail">Email</label>
<input v-validate="'required|email'" name="email" type="text">
<!-- errors.first('email') 會告知錯誤在哪裡(英文) -->
<span class="text-danger" v-if="errors.has('email')">{{errors.first('email')}}</span>
```
#### 將 Eail 錯誤訊息中文化
1. 安裝 vue-i18n
```cmd
npm install vue-i18n --save
```
2. 在 node_mudules/vee-validate/dist/locale 是語系檔案
將它 import 進來
```javascript
import zhTW from 'vee-validate/dist/locale/zh_TW';
```
3. 在 main.js 中將 vue-i18n import 進來
```javascript
import VueI18n from 'vue-i18n'
Vue.use(VueI18n);
```
4. 將 VeeValidate.Validator.localize('zh_TW', zhTWValidate) 及 Vue.use(VeeValidate) 刪除,並加入下列程式碼
```javascript
const i18n = new VueI18n({
locale: 'zhTW'
});
Vue.use(VeeValidate, {
i18n,
dictionary: {
zhTW
}
});
```
5. 在 Vue 物件中新增 i18n
```cmd
new Vue({
i18n,
el: '#app',
components: { App },
template: '<App/>',
router,
})
```
如果不想使用 chrome 的 required,可以使用套件的 API
在送出表單的 API 之前加入下段
```javascript
this.$validator.validate().then(valid => {
if (!valid) {
// do stuff if not valid.
}
})
```
所以可以寫成下面這樣
```javascript
createOrder() {
const vm = this;
const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/order`;
const order = vm.form;
this.$validator.validate().then(valid => {
// 如果欄位正確,則送出表單執行送出訂單 API
if (valid) {
this.$http.post(url, { data: order }).then(response => {
console.log("訂單已建立", response);
vm.isLoading = false;
});
} else {
// 否則跳出錯誤訊息
console.log("欄位輸入不正確");
}
});
}
```
# Vue cli 3.0
>參考 [Vue cli 3.0](https://cli.vuejs.org/zh/)
## 安裝
如果已安裝 2.0,必須先移除舊版再安裝
```cmd
npm uninstall vue-cli -g
npm install -g @vue/cli
<!-- 查看版本 -->
vue --version
```
## 建立專案
建立方式有 command line 與 GUI 建立
```cmd
vue create hello-world
```
## 運行專案
```cmd
npm run serve
```
## 資料夾結構
* 在 2.0 版本中會出現很多 Webpack 的設定檔,但在 3.0 全部放在 node_mudules/@vue 中,而且基本上不太會動到
* 在 3.0 中 .vue 預設不省略,例如: `import App from './App.vue'`,即使省略還是可以編譯,但 ESlint 會跳提示。
* public: 這裡檔案不會被編譯,但其實 index.html 還是會被壓縮並且插入 src 內容。
* src: 這裡所有檔案都會被編譯。
* views: 分頁的元件。
* components: 內部的元件。
## 打包
此時會出現 dist 資料夾,交檔案主要是要交這個
```cmd
npm run build
```
打開 dist 的 index.html 會看到預設注入的 `<link>` 標籤
最主要是下面的 `<script>`:
```htmlmixed
<!-- vendors 就是外部載入的資源,如 node_mudules 去 import -->
<script src=/js/chunk-vendors.a411fc52.js> </script>
<!-- 自己撰寫的部分 -->
<script src=/js/app.ed52e9ed.js> </script>
```
dist 的 index.html 無法直接開啟,但可以使用 vscode 的 live server 開啟。
## 環境變數
開發環境與正式環境不同
### 新增 .env 檔
[官方文件](https://cli.vuejs.org/zh/guide/mode-and-env.html#%E6%A8%A1%E5%BC%8F)
新增一個 .env 檔(不需要名稱)
### 設定變數
```.env
<!-- 前面的 VUE_APP 是固定的,為了讓 src 讀到這變數 -->
<!-- API 這部分可以自定義名稱 -->
<!-- 注意這裡等號右邊不需要引號,跟 2.0 config 不同 -->
VUE_APP_API=http://localhost:8080/
```
可以在 App.vue 中加入下段查看一下變數
```javascript
export default {
name: "app",
components: {
HelloWorld
},
created() {
console.log(process.env.VUE_APP_API);
}
};
```
### 自訂 .env 的 mode
新增一個 .env.Ken 檔
可以在裡面加入這段
```.env
VUE_APP_APIPATH=https://vue-course-api.hexschool.io
VUE_APP_CUSTOMPATH=kenapi
```
在 package.json 中,修改下列這段
並執行 npm run serve
```json=
scripts": {
"serve": "vue-cli-service serve --mode Ken",
"build": "vue-cli-service build"
}
```
### 預設的 .env
基本上用這兩個預設的就好了
新增檔案:
* .env.development
* .env.production
各放入開發與正式的環境變數即可,注意權重會大於沒有名稱的 .env
:sunny: 為什麼要分環境變數
![](https://i.imgur.com/e7Jo7Ym.png)
## 圖形化介面
>參考[課程 113](https://www.udemy.com/course/vue-hexschool/learn/lecture/11627486)
設定介紹較多,可以直接複習課程即可。
啟用
```cmd
vue ui
```
## 快速原型開發
適合用在小型專案
>參考 [快速原型開發](https://cli.vuejs.org/zh/guide/prototyping.html)
>
>設定複雜,參考[課程 117](https://www.udemy.com/course/vue-hexschool/learn/lecture/11627524)
## Vuex
適合大型專案用的工具並
僅適合用於簡單、資料量小的情境。
>[Vuex](https://vuex.vuejs.org/zh/guide/)
![](https://i.imgur.com/QKhJ7BV.png)
![](https://i.imgur.com/i0j98LY.png)
![](https://i.imgur.com/2SQ6YKt.png)
![](https://i.imgur.com/V8MtQ9F.png)
### 安裝
```cmd
npm install vuex --save
```
### 啟用
```javascript
import Vuex from 'vuex'
Vue.use(Vuex)
```
### 新增 store 資料夾
在 src 下新增 store 資料夾,並在裡面新增 index.js
index.js
```javascript
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
});
```
main.js 就必須 import
```javascript
import store from './store';
// 然後在 new Vue 下就必須掛載 store
new Vue({
el: '#app',
router,
store,
components: { App },
template: '<App/>',
});
```
設定超級複雜,但創建專案時會自動建立。
### state
放資料的地方
跟元件不同的地方在於元件中是用 `data() {}`
state 則是 `state: {}`
:sunny: 可以在 state 上面加入 strict: true 進入嚴謹模式
### actions
actions 只處理 AJAX 行為,==不可以去改變資料==
例如:
```javascript
actions: {
// context 是固定的參數,參考官網,還有許多其他的屬性
// payload 是外部傳來的參數,是自定義的,名稱為'載荷'
updateLoading(context, payload) {
// context.commit() 去呼叫 mutations 並帶入參數
context.commit('LOADING',payload)
}
}
```
### mutations
只改變資料,==不能處理 AJAX 或其他非同步行為==,如 setTimeout
```javascript
mutations: {
// 建議在 mutations 以常數(大寫)撰寫
// 第一個參數是固定的,代表上面資料狀態的 state
LOADING(state, payload){
state.isLoading = payload
}
}
```
### dispatch()
在元件中使用 dispatch 呼叫 actions 的方法
:sunny: 參數只能傳 1 個,若要傳多個參數,可以使用物件方式傳遞
接收參數的 actions 那邊可以使用物件形式來接收,就可以直接解構
```javascript
this.$store.dispatch('actions裡函式', 參數)
```
### getters
>寫法複雜,複習[課程 125](https://www.udemy.com/course/vue-hexschool/learn/lecture/11240780)
==類似 computed==
需先在 /store/index.js 中加入下段
使用解構,只取出 Vuex 的 mapGetters、mapActions 方法
```javascript
import { mapGetters } from "vuex";
```
接著在下面新增
```javascript
// state 為固定
getters:{
categories(state){
return state.categories;
},
products(state){
return state.products;
}
}
```
然後在元件中的 computed 改為
```javascript
...mapGetters(['categories','products'])
```
也能使用 mapActions,在元件中的 methods 改為
```javascript
...mapActions(['getProducts']),
```
### 模組化
大型專案中,index.js 裡也會非常多行程式碼,所以會繼續拆分進行模組化
新增一個檔案 /store/product.js
```javascript
```
超級複雜,直接複習[課程126](https://www.udemy.com/course/vue-hexschool/learn/lecture/11240786)