# 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)