資料庫應用開發(實作) === <xiaoding.edu@gmail.com> ## 內容綱要 * 介紹Web前端介紹 * 前置知識 * 快速體驗Vue.js開發 * Vue.js的學習路線規劃建議 * 使用vue-cli創建 Vue.js專案 * Vue開發環境搭建 * Vue目錄結構與檔案分析 * Vue 組件化 * 打包&部署應用 * Vue Router (路由) * Vue+ElementUI * Vue實體生命週期 (Instance Lifecycle Hooks) * (20191219)接後端php程式做新增改查(CRUD) ## 介紹Web前端介紹 #### 目前流行前端框架: 1. Angular (Google) 2. React (Facebook) 3. **Vue.js (尤雨溪) - 本課使用** * 實現單頁面框架(SPA) * 組件化開發模式 #### HTML、CSS及JavaScript的框架,提供字體排印、表單、按鈕、導航及其他各種元件及Javascript擴充套件: 1. Bootstrap 2. Material 3. **Element UI - 本課使用** 4. Quasar Framework 5. Vux (針對Mobile) 6. Mint UI ## 前置知識 * HTML語法 (不了解就課後了解) * CSS語法 (同上) * JavaScript語法 (同上) * 需要安裝Node.js環境,並花些時間了解這個生態系 * https://nodejs.org/en/download/ --- #### 為什麼要學習流行框架 * 提高開發效率,開發歷程的演進: 原生JS -> jQuery之類的庫 -> 前端模板引擎 -> Angular.js / React / Vue.js * 業務邏輯往往佔了主要業務邏輯的80%時間 * 人無我有,人有我優 * 一個核心的概念,就是讓用戶不再操作DOM元素,解放了用戶的雙手,讓工程師可以更多的時間去關注業務邏輯 * DOM是什麼?傳送門(https://ithelp.ithome.com.tw/articles/10202689) * 幫助我們減少不必要的DOM操作,提高渲染效率 * 提供雙向數據綁定的概念,通過框架提供的指令,coder只需要關係業務邏輯,而不用關心DOM是如何渲染的 * Vue.js是前端的主流框架之一,和Angular.js React.js一起,為前端三大主流框架 * 沒有良好的基礎學不好Vue,但Vue的優點是,就算你基礎不行,也能勉強用 --- ## 快速體驗Vue.js開發 * 參考網址: https://vuejs.org/v2/guide/ * 創建一個vue.html (練習用) * 體驗v-if, v-else, v-else-if, v-for, data biding ```javascript= <!DOCTYPE html PUBLIC "-//IETF//DTD HTML 2.0//EN"> <HTML> <HEAD> <TITLE> A Small Hello </TITLE> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> </HEAD> <BODY> <div id="app"> {{ message }} <span v-if="obj.str === 'A'">Now you see AA</span> <span v-else-if="str === 'B'">Now you see BB</span> <span v-else>Now you see CC</span> <ol> <li v-for="t in todos"> {{ t }} </li> </ol> </div> <script> var app = new Vue({ el: '#app', data: { todos: ['todo1', 'todo2'], seen: false, num: 3, str: 'A', obj: { str: 'A' }, message: 'Hello Vue!1213131321' + new Date().toLocaleString() } }) </script> </BODY> </HTML> ``` --- ## Vue.js的學習路線規劃建議 * HTML/CSS/JS基礎 (沒有良好的基礎學不好Vue,但Vue的優點是,就算你基礎不行,也能勉強用) * HTML常用語法 (div, span, img..) * CSS基礎語法 * Scoped CSS (CSS作用域: https://vue-loader-v14.vuejs.org/zh-cn/features/scoped-css.html) * JS基礎語法 (https://pjchender.blogspot.com/2016/06/javascriptfunction-constructornew.html) * new * this * ES6語法 * Object.defineProperty * class * 設計模式 * MVC模式 * eventbus發佈訂閱 * mixin 混入 * prototype原型 * extends繼承 * 依賴注入 * Vue API (建議分別寫四篇文章整理和學習,因為太多了) * 組件 * data * props * methods * watch * computed * template * render * 鉤子 (hook) * beforeCreate * created * beforeMount * mounted * beforeUpdate * updated * beforeDestroy * destroyed * 模板語法 * 條件 (v-if/ v-else / v-else-if) * 迴圈 (v-for) * 事件 (v-on/ v-once) * 屬性 (v-bind) * 特殊 (v-model) * 其他 (v-text / v-html / v-show / v-pre / v-cloak) * 過度動畫 * transition * transition-group * Webpack配置 * vue-loader (最重要的一個loader) * @vue-cli (用來創建專案) * sass-loader / less-loader / stylus-loader * babel-loader * ts loader * eslint (非常煩的東西,可以訓練自己變成程式碼潔癖) * Vue全家桶 (如果學完這一步,基本上就是一個Vue的掌握者了) * Vuex * state * getter * mutation * action * module * Vue Router * hash 模式 * history 模式 * 導航守護 (路由跳轉前驗證登入) * 懶加載 (組件拆分) * Axios * 攔截器 * RESTFul * Jest/ Mocha * 單元測試 * mock / stub * PWA * service worker * HTTPS * UI框架 (掌握完這一步,作什麼頁面都會超快,生產力會非常高) * Element * Ant Design Vue * iview * cube-ui * Vant * Vux * Vue 3.0 * TypeScript * React Hooks * Proxy API * Reactive 風格 * 函數式程式 * 高級用法 (這邊才涉及到演算法和資料結構) * 虛擬DOM * Diff演算法 * 模板編譯 --- ## 使用vue-cli創建 Vue.js專案 #### 相依環境: * 安裝node.js環境 * https://nodejs.org/en/ * 安裝yarn指令工具 (比node.js原生的npm指令有效率) * 透過npm來安裝: * npm install -g yarn * 或在官網下載安裝懶人包 * https://yarnpkg.com/en/docs/install#windows-stable * Yarn 是 Facebook 發佈的一款 JavaScript 套件管理工具,其功能與 npm 相同,但 npm 最為人詬病的是安裝速度很慢、安裝相依套件沒有順序性,而 Yarn 不但解決了這些問題,還加入了方便的 Cache 機制使套件均只要下載一次,多個專案若使用相同套件時不必重新下載。官方也表示 Yarn 是快速、安全、可靠的,且能支援多系統。 --- ## vue開發環境搭建 #### 編輯器推薦 * vscode: https://code.visualstudio.com/ * atmo: https://atom.io/ #### 安裝vue-cli指令工具 (第二代) - 本課使用 ``` # 使用 npm 安裝 npm install --global vue-cli ``` #### 使用vue-cli創建專案, cd到對應的目錄名稱, ``` vue init webpack vue-demo #創建專案, 名稱為vue-demo ``` 接下來會看到專案建立引導流程,確認專案的環境,以便產生器製作 ``` ? Project name vue-demo? Y #專案的名稱 ? Project description A Vue.js project? Y #專案的描述 ? Author chunting.d? Y #專案的所有者 (通常是你自己) ? Vue build standalone? Y ? Install vue-router? Yes #vue 的 route 意即專案路由 ? Use ESLint to lint your code? No #規範 coding style 的工具, 神煩 ? Set up unit tests? Yes #是否設定單元測試 ? Pick a test runner jest #使用單元測試的工具,這裡選 jest ? Setup e2e tests with Nightwatch? Yes #e2e 測試的工具 ? Should we run `npm install` for you after the project has been created? (recommended) npm #幾種 install 的方式,這裡是用預設的 npm vue-cli · Generated "vue-demo". ``` * 注意:基本上都是YES (enter), 但對初學者建議Use ESLint to lint your code?這邊選NO * ESLint是程式碼檢查器 (非常嚴格),讓你多一個空白或少一個空白都會報錯,先把語法檢察器關掉,便於學習 (但型專案建議使用) * 看到Project initialization finished!就代表成功結束 接下來繼續操作: ``` cd vue-demo #cd到對應的目錄名稱 npm install #安裝相依套件, 若有安裝yarn, 也可以用yarn install npm run dev #啟動hot reload開發模式 ``` > 注意vue-demo可請自行改成喜歡的專案名稱 --- ## Vue目錄結構與檔案分析 參考下列幾個頁面 * vue專案總結之資料夾結構配置詳解[https://codertw.com/%E5%89%8D%E7%AB%AF%E9%96%8B%E7%99%BC/226385/] * 比較重要的部分 * node_modules資料夾: 專案的依賴庫 * src 資料夾: 我們主要的開發程式都會在這邊,元件的增加修改等都在這個資料夾裡操作 * build && config 資料夾裡面是 webpack 相關的設定檔,這邊就不做太多的介紹 * package.json:使用 npm 管理套件會一併出現的檔案,裡面記錄著在這個專案中所使用的套件,以及 npm 的執行指令 * main.js && App.vue * main.js是整個 Vue 專案的最源頭,裡面引入了 Vue 並創建了 Vue 實例,這個 Vue 實例有一個引入的 components: App。 * App.vue 它是一個 component,可能有人很疑惑怎麼有 .vue 這個副檔名,因為專案中有使用 webpack 裡面有使用一個套件 vue-loader,有了這個套件 webpack 才能夠將 .vue 檔解析成 js。 * components * components 這個資料夾就是規劃放置 Vue component 的地方,像初始專案中已經有一個 HelloWorld.vue component。 * assets * assets是放置靜態檔案的地方,像是圖片之類的,必須放在這個資料夾內才能夠在程式部分被引用。 * router * 因為我們執行安裝的時候有選擇要裝 vue-router 所以才會有這個資料夾,那這邊先不多作介紹,後面會詳細介紹 vue-router。 另外使用`npm run dev`開啟了本地 server 後,vue-cli 有支援 hot reload,所以我們再開發的過程中修改後只要儲存了網頁會自動 reload,不需要再按 f5 或 cmd R 來重整。 --- ## Vue 組件化 * 善用 CRM 學習法 * Copy(抄) Run (運作) Modify (改) * Vue的模板中 (template)只能有一個根組件 * 為什麼要組件化: * 提高開發效率 * 方便重複使用 * 簡化調試步驟 * 提升整個項目的可維護性 * 便於協同開發 ![](https://i.imgur.com/5n1co7P.png) #### 版本一: component 範例: App.vue ```javascript= <template> <div> <div class=row> <Cell/> <Cell/> <Cell/> </div> <div class=row> <Cell/> <Cell/> <Cell/> </div> <div class=row> <Cell/> <Cell/> <Cell/> </div> </div> </template> <script> import Cell from '@/components/Cell' export default { name: 'App', components: { Cell }, data () { return { } } } </script> <style> .row { display: flex; } </style> ``` 範例: Cell.vue ```javascript= <template> <div class="cell" @click="clicked = true"> <div v-if="clicked">O</div> <div v-else-if="!clicked"></div> </div> </template> <script> export default { name: 'Cell', data () { return { clicked: false } } } </script> <style> .cell { border: 1px black solid; width: 100px; height: 100px; display: flex; justify-content: center; align-items: center; font-size: 80px; } </style> ``` #### 版本二: component props (從外部導入符號與增加事件回乎) App.vue ```javascript= <template> <div> <div class="row"> <Cell :symbol="curSymbol" v-on:update="onUpdate"/> <Cell :symbol="curSymbol" v-on:update="onUpdate"/> <Cell :symbol="curSymbol" v-on:update="onUpdate"/> </div> <div class="row"> <Cell :symbol="curSymbol" v-on:update="onUpdate"/> <Cell :symbol="curSymbol" v-on:update="onUpdate"/> <Cell :symbol="curSymbol" v-on:update="onUpdate"/> </div> <div class="row"> <Cell :symbol="curSymbol" v-on:update="onUpdate"/> <Cell :symbol="curSymbol" v-on:update="onUpdate"/> <Cell :symbol="curSymbol" v-on:update="onUpdate"/> </div> </div> </template> <script> import Cell from '@/components/Cell' export default { name: 'App', components: { Cell }, data () { return { curSymbol: 'O', clicked: false } }, methods: { onUpdate () { if (this.curSymbol === 'O') { this.curSymbol = 'X' } else { this.curSymbol = 'O' } console.log('onUpdate') } } } </script> <style> .row { display: flex; } </style> ``` Cell.vue ```javascript= <template> <div class="cell" @click="onClick"> <div v-if="!clicked" class="red"></div> <div v-else-if="clicked" class="red">{{ symbol }}</div> </div> </template> <script> export default { name: 'Cell', props: { symbol: { type: String, required: true } }, data () { return { clicked: false } }, methods: { onClick () { this.clicked = true this.$emit('update') console.log('onClick') } } } </script> <style> .cell { border: 1px black solid; width: 100px; height: 100px; display: flex; justify-content: center; align-items: center; font-size: 80px; } </style> ``` #### 版本三: component 增加步數資訊與事件參數 App.vue ```javascript= <template> <div> <div class="row"> <Cell :step="step" v-on:update="onUpdate(0, $event)"/> <Cell :step="step" v-on:update="onUpdate(1, $event)"/> <Cell :step="step" v-on:update="onUpdate(2, $event)"/> </div> <div class="row"> <Cell :step="step" v-on:update="onUpdate(3, $event)"/> <Cell :step="step" v-on:update="onUpdate(4, $event)"/> <Cell :step="step" v-on:update="onUpdate(5, $event)"/> </div> <div class="row"> <Cell :step="step" v-on:update="onUpdate(6, $event)"/> <Cell :step="step" v-on:update="onUpdate(7, $event)"/> <Cell :step="step" v-on:update="onUpdate(8, $event)"/> </div> </div> </template> <script> import Cell from '@/components/Cell' export default { name: 'App', components: { Cell }, data () { return { step: 0, clicked: false } }, methods: { onUpdate (i, symbol) { console.log(`${i}, 格被點擊,內容是${symbol}`) this.step++ console.log(symbol) } } } </script> <style> .row { display: flex; } </style> ``` Cell.vue ```javascript= <template> <div> <div class="cell" @click="onClick"> <div v-if="!clicked" class="red"></div> <div v-else-if="clicked" class="red">{{ symbol }}</div> </div> {{step}} </div> </template> <script> export default { name: 'Cell', props: { step: { type: Number } }, data () { return { symbol: '', clicked: false } }, methods: { onClick () { if (this.symbol !== '') { //已經被點了 return; } this.clicked = true this.symbol = this.step % 2 === 0 ? 'X' : 'O' this.$emit('update', this.symbol) //console.log('onClick') } } } </script> <style scoped> .cell { border: 1px black solid; width: 100px; height: 100px; display: flex; justify-content: center; align-items: center; font-size: 80px; } </style> ``` #### 版本四: 增加判斷輸贏 App.vue ```javascript= <template> <div> <div class="row"> <Cell :step="step" v-on:update="onUpdate(0, $event)"/> <Cell :step="step" v-on:update="onUpdate(1, $event)"/> <Cell :step="step" v-on:update="onUpdate(2, $event)"/> </div> <div class="row"> <Cell :step="step" v-on:update="onUpdate(3, $event)"/> <Cell :step="step" v-on:update="onUpdate(4, $event)"/> <Cell :step="step" v-on:update="onUpdate(5, $event)"/> </div> <div class="row"> <Cell :step="step" v-on:update="onUpdate(6, $event)"/> <Cell :step="step" v-on:update="onUpdate(7, $event)"/> <Cell :step="step" v-on:update="onUpdate(8, $event)"/> </div> {{map}} {{finished}} </div> </template> <script> import Cell from '@/components/Cell' export default { name: 'App', components: { Cell }, data () { return { step: 0, clicked: false, map: [ [null, null, null], [null, null, null], [null, null, null] ], finished: false } }, methods: { onUpdate (i, symbol) { console.log(`${i}, 格被點擊,內容是${symbol}`) // 0 map[0][0] // 1 map[0][1] // 2 map[0][2] // 3 map[1][0] // 4 map[1][1] // 5 map[1][2] // 6 map[2][0] // 7 map[2][1] // 8 map[2][2] this.map[Math.floor(i/3)][i%3] = symbol this.step++ console.log(symbol) let t = this.isFinish() if (t !== '') { console.log(`${t}獲勝`) } }, isFinish () { const map = this.map // 判斷橫的 for (let i = 0; i < 2; i++) { if (map[i][0] !== null && map[i][0] === map[i][1] && map[i][1] === map[i][2]) { this.finished = true return map[i][0] } } // 判斷直的 for (let i = 0; i < 2; i++) { if (map[0][i] !== null && map[0][i] === map[1][i] && map[1][i] === map[2][i]) { this.finished = true return map[0][i] } } // 左上到右下 if (map[0][0] !== null && map[0][0] === map[1][1] && map[1][1] === map[2][2]){ this.finished = true return map[0][0] } // 右上到左下 if (map[0][2] !== null && map[0][2] === map[1][1] && map[1][1] === map[2][0]){ this.finished = true return map[0][2] } return '' } } } </script> <style> .row { display: flex; } </style> ``` 知識點: * 監聽事件,在Cell裡面透過$emit告訴APP 被點擊了 * 在Cell中要定義Props,並在APP中透過v-bind 告訴Cell目前的n,Cell就得宣告一個 * 指令: 在Vue中提供了一些對於頁面+數據更為方便的輸出(e.g v-xxx) 小結: * 組件化就是可以組裝的東西 * 組件化使得大問題變成小問題,小問題很好解決 * 大事化小,小事化了 訊息傳遞 * 父子組件可單向傳遞訊息 * 非父子組件訊息不共享,除非借用外力 * 外力有很多: window, context, eventbus,vuex 語法 * 不用死記,勤看文件和Google,看了忘,忘了看 * 記得自己寫篇筆記總結 Vue是對前端知識的匯總 * 你在用Vue的時候很容易發現自己的短版 * 發現短版時,你應該去補基礎 --- ## 打包&部署應用 開發完到一個段落後,可以透過 npm run build將整個專案<打包> ```shell npm run build ``` 接著會在專案目錄中產生dist資料夾 ![](https://i.imgur.com/9CQir9r.png) 將資料夾內的index.html和static資料夾直接複製到網頁伺服器的網頁根目錄(部屬) --- ## Vue Router (路由) 參考:https://router.vuejs.org/zh/ * Router 路徑與組件佈局 * router-view使用方式 * 如何使用router-link 組件 * 導航程式化: router.push(), router.replace(), router.go() * HTML5 History 模式 * 參數傳遞 * 父子組件 (嵌套路由) #### 路由分成前端路由跟後端路由 * 後端路由: * 定義:通過用戶請求的url導航到具體的html頁面;每跳轉到不同的URL,都是重新訪問服務端,然後服務端返回頁面,頁面也可以是服務端獲取數據,然後和模板組合,返回HTML,也可以是直接返回模板HTML,然後由前端js再去請求數據,使用前端模板和數據進行組合,生成想要的HTML * 前端路由 * 在單頁面應用,大部分頁面結構不變,只改變部分內容的使用 * 從性能和用戶體驗的層面來比較的話,後端路由每次訪問一個新頁面的時候都要向服務器發送請求,然後服務器再響應請求,這個過程肯定會有延遲。而前端路由在訪問一個新頁面的時候僅僅是變換了一下路徑而已,沒有了網絡延遲,對於用戶體驗來說會有相當大的提升 * Vue Router 是前端路由 首先確認在main.js當中,有將router導入至Vue實例參數中 ```javascript= import router from './router' /* eslint-disable no-new */ new Vue({ el: '#app', router, components: { App }, template: '<App/>' }) ``` 在App.vue中增加router-view和router-link App.vue ```javascript= <template> <div> <div> <router-link to="/">Go to HelloWorld</router-link> <router-link to="/chess">Go to Chess</router-link> <router-link :to="{path: '/'}">Go to HelloWorld</router-link> <router-link :to="{name: 'Chess'}">Go to Chess</router-link> </div> <!-- 轉跳後所載入的 component 最後會顯示在此 --> <router-view></router-view> </div> </template> <script> export default { name: 'App', components: { }, data () { return { } }, methods: { } } </script> <style> .row { display: flex; } </style> ``` router/index.js ```javascript= import Vue from 'vue' import Router from 'vue-router' import HelloWorld from '@/components/HelloWorld' import Chess from '@/components/Chess' Vue.use(Router) /* * router-link 就像<a href="/chess">chess</a> * :to 裡面是物件形式,描述要轉跳的目的與需要帶的參數目的 * 可以用 path 或 name */ export default new Router({ routes: [ { path: '/', name: 'HelloWorld', component: HelloWorld }, { path: '/chess', name: 'Chess', component: Chess } ] }) ``` --> #### 將App.vue原本棋盤內容複製到新的Component Chess.vue Chess.vue ```javascript= <template> <div> <div class="row"> <Cell :step="step" v-on:update="onUpdate(0, $event)"/> <Cell :step="step" v-on:update="onUpdate(1, $event)"/> <Cell :step="step" v-on:update="onUpdate(2, $event)"/> </div> <div class="row"> <Cell :step="step" v-on:update="onUpdate(3, $event)"/> <Cell :step="step" v-on:update="onUpdate(4, $event)"/> <Cell :step="step" v-on:update="onUpdate(5, $event)"/> </div> <div class="row"> <Cell :step="step" v-on:update="onUpdate(6, $event)"/> <Cell :step="step" v-on:update="onUpdate(7, $event)"/> <Cell :step="step" v-on:update="onUpdate(8, $event)"/> </div> {{map}} {{finished}} </div> </template> <script> import Cell from '@/components/Cell' export default { name: 'App', components: { Cell }, data () { return { step: 0, clicked: false, map: [ [null, null, null], [null, null, null], [null, null, null] ], finished: false } }, methods: { onUpdate (i, symbol) { console.log(`${i}, 格被點擊,內容是${symbol}`) this.map[Math.floor(i/3)][i%3] = symbol this.step++ console.log(symbol) let t = this.isFinish() if (t !== '') { console.log(`${t}獲勝`) } }, isFinish () { const map = this.map // 判斷橫的 for (let i = 0; i < 2; i++) { if (map[i][0] !== null && map[i][0] === map[i][1] && map[i][1] === map[i][2]) { this.finished = true return map[i][0] } } // 判斷直的 for (let i = 0; i < 2; i++) { if (map[0][i] !== null && map[0][i] === map[1][i] && map[1][i] === map[2][i]) { this.finished = true return map[0][i] } } // 左上到右下 if (map[0][0] !== null && map[0][0] === map[1][1] && map[1][1] === map[2][2]){ this.finished = true return map[0][0] } // 右上到左下 if (map[0][2] !== null && map[0][2] === map[1][1] && map[1][1] === map[2][0]){ this.finished = true return map[0][2] } return '' } } } </script> <style> .row { display: flex; } </style> ``` ![](https://i.imgur.com/g3UBl2r.png) --- #### 導航程式化: router.push(), router.replace(), router.go() #### 以下程式為範例 ```javascript= // 使用 $router.push this.$router.push({ name: 'Chess'}) // 使用 $router.replace this.$router.replace({ name: 'Chess'}) // 可使用name 或著path的方式去切換 this.$router.replace({ path: '/chess'}) this.$router.replace({ name: 'HelloWorld'}) //各自分別的參數傳遞方法, path搭配query, name搭配params this.$router.replace({ name: 'Chess', params: { userId: 123 }}) this.$router.replace({path: '/chess', query:{userId: '123'}}) // 接收資料的components console.log(this.$route.params.userId) // 使用 name+params console.log(this.$route.query.userId) // 使用 path+query ``` 延伸閱讀:https://www.cnblogs.com/jin-zhe/p/10313012.html --- ## Vue實體生命週期 (Instance Lifecycle Hooks) Vue 除了提供雙向綁定將資料跟樣板綁在一起之外,其中也執行了一連串工作,包含在原始資料加料、建立Vue Instance、編譯樣板、綁定資料等,隨著資料新增修改,週而復始直到刪除,稱為實體生命週期(Lifecycle)。 ![](https://i.imgur.com/bZ3tPF8.png) 其中如圖白底紅字的部分,可以提供客製化的空間。 ```javascript= <script> export default { name: 'TodoUpdate', data () { return { } }, beforeCreate () { // vue instance 被 constructor 建立前 console.log('[TodoUpdate] beforeCreate') }, created () { // vue instance 被 constructor 建立後,在這裡完成 data binding console.log('[TodoUpdate] created') }, beforeMount () { // 綁定 DOM 之前 console.log('[TodoUpdate] beforeMount') }, mounted () { // 綁定 DOM 之後 console.log('[TodoUpdate] mounted') }, beforeUpdate () { // 資料更新,但尚未更新 DOM console.log('[TodoUpdate] beforeUpdate') }, updated () { // 因資料更新,而更新 DOM console.log('[TodoUpdate] updated') }, beforeDestroy () { // 移除 vue instance 之前 console.log('[TodoUpdate] beforeDestroy') }, destroyed () { // 移除 vue instance 之後 console.log('[TodoUpdate] destroyed') } } </script> ``` #### 使用timer的範例,在created時啟動timer, beforeDestory時移除timer ```javascript= data () { return { timerA: null } }, created () { this.timerA = setInterval(( () => console.log("Hello!") ), 1000); }, beforeDestroy () { if (this.timerA !== null) { clearTimeout(this.timerA); } } ``` --- ## Vue+ElementUI ```shell= # 安裝element ui npm install element-ui --save ``` > --save 是 npm 安裝指令中的參數之一,目的是安裝某個套件,並幫我加入到 package.json 檔案中,如果後面又串上 -dev 則會特別加在 devDependencies 區域中,否則會放在 dependencies 區域中。 ```javascript= // 在main.js增加下列幾行,把 element ui 載入進來 import Element from 'element-ui' import 'element-ui/lib/theme-chalk/index.css' import locale from 'element-ui/lib/locale/lang/zh-TW' Vue.use(Element, { locale }) ``` --- 參考資料:https://kuro.tw/posts/2017/06/07/%E5%A6%82%E4%BD%95%E5%9C%A8-Vue-CLI-%E5%BB%BA%E7%AB%8B%E7%9A%84%E9%96%8B%E7%99%BC%E7%92%B0%E5%A2%83%E5%91%BC%E5%8F%AB%E8%B7%A8%E5%9F%9F%E9%81%A0%E7%AB%AF-RESTful-APIs/ https://medium.com/@moojing/vue-js-node-js-openapi-%E5%B8%B6%E4%BD%A0%E4%B8%80%E6%AC%A1%E4%BA%86%E8%A7%A3cors%E8%B7%A8%E5%9F%9F%E8%AB%8B%E6%B1%82-b37cd926551f --- #### Element UI Table ```shell= # 安裝axios npm install axios --save ``` axios 基本使用 & Config https://ithelp.ithome.com.tw/articles/10212120 ```javascript= // main.js增加 import axios from 'axios'; Vue.prototype.$https = axios; ``` 增加 TableView.vue ```javascript= <template> <div> <el-table :data="tableData" style="width: 100%;"> <el-table-column prop="seq" label="序列" width="180"> </el-table-column> <el-table-column prop="行政區" label="行政區" width="180"> </el-table-column> <el-table-column prop="臨時停車處所" label="臨時停車處所" width="180"> </el-table-column> <el-table-column prop="可提供小型車停車位" label="可提供小型車停車位" width="180"> </el-table-column> <el-table-column prop="地址" label="地址"> </el-table-column> </el-table> </div> </template> <script> export default { name: 'TableView', components: { }, data () { return { tableData: [] } }, created () { var url = "https://api.kcg.gov.tw/api/service/get/897e552a-2887-4f6f-a6ee-709f7fbe0ee3"; let self = this this.$https.get(url) .then(function (response) { console.log(response) self.tableData = response.data.data // seq: 33, 行政區: "阿蓮區", 臨時停車處所: "阿蓮國小", 可提供小型車停車位: "30", 地址: "民族路163號"} }) .catch(function (error) { console.log(error) }); }, mounted () { }, beforeDestroy () { }, methods: { } } </script> <style> </style> ``` 增加 TableView router/index.js ```javascript= import Vue from 'vue' import Router from 'vue-router' import HelloWorld from '@/components/HelloWorld' import TableView from '@/components/TableView' import Chess from '@/components/Chess' Vue.use(Router) /* * router-link 就像<a href="/chess">chess</a> * :to 裡面是物件形式,描述要轉跳的目的與需要帶的參數目的 * 可以用 path 或 name * 要注意path和name 不要重複 */ export default new Router({ routes: [ { path: '/chess', name: 'Chess', component: Chess }, { path: '/tableview', name: 'TableView', component: TableView }, { path: '/*', name: 'HelloWorld', component: HelloWorld } ] }) ``` 參考資料: https://data.kcg.gov.tw/dataset/kcgoa-00000036-808/resource/deb6eacc-1d07-46be-a1b1-9c71ac2f5932 --- ## 串接後端php程式做新增改查(CRUD) - 20191219 vue安裝 npm install axios --save npm install element-ui --save ```javascript= // 在main.js增加下列幾行,把 element ui 載入進來 import Element from 'element-ui' import 'element-ui/lib/theme-chalk/index.css' import locale from 'element-ui/lib/locale/lang/zh-TW' Vue.use(Element, { locale }) ``` ```javascript= // main.js增加 import axios from 'axios'; Vue.prototype.$https = axios; ``` 以下範例使用misdb.sql做為練習 api/config/database.php ```php= <?php class Database{ private $db_host = 'localhost'; private $db_name = 'misdb'; private $db_username = 'root'; private $db_password = ''; public function dbConnection() { try { // 連結資料庫 $conn = new PDO('mysql:host='.$this->db_host.';port=3307'.';dbname='.$this->db_name,$this->db_username,$this->db_password); $conn->exec("set names utf8"); $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); return $conn; } catch(PDOException $e){ echo "Connection error ".$e->getMessage(); exit; } } } ?> ``` port=3307是mariaDB的port port=3306是mysql的port 讀取misdb範例資料中的product表格 api/v1/product/read.php ```php= <?php // SET Response header header("Access-Control-Allow-Origin: *"); header("Access-Control-Allow-Headers: access"); header("Access-Control-Allow-Methods: GET"); header("Access-Control-Allow-Credentials: true"); header("Content-Type: application/json; charset=UTF-8"); // INCLUDING DATABASE AND MAKING OBJECT require '../../config/database.php'; $db_connection = new Database(); $conn = $db_connection->dbConnection(); // MAKE SQL QUERY // IF GET PRODUCTS ID, THEN SHOW PRODUCTS BY ID OTHERWISE SHOW ALL PRODUCTS $ProdID = isset($_GET['ProdID']) ? $_GET['ProdID'] : 'all_products'; // 如果沒有ProdID參數, 則將所有產品查找出來 $sql = ($ProdID != 'all_products') ? "SELECT * FROM `product` WHERE ProdID='$ProdID'" : "SELECT * FROM `product`"; $stmt = $conn->prepare($sql); $stmt->execute(); //CHECK WHETHER THERE IS ANY PRODUCT IN OUR DATABASE if($stmt->rowCount() > 0){ // CREATE PRODUCTS ARRAY $products_array = []; while($row = $stmt->fetch(PDO::FETCH_ASSOC)){ $product_data = [ 'ProdName' => $row['ProdName'], 'ProdID' => $row['ProdID'], 'UnitPrice' => $row['UnitPrice'], 'Cost' => $row['Cost'] ]; // PUSH PRODUCT DATA IN OUR $products_array ARRAY array_push($products_array, $product_data); } //SHOW PRODUCT/PRODUCTS IN JSON FORMAT echo json_encode(['message'=>'ok', 'results' => $products_array]); } else{ //IF THER IS NO PRODUCT IN OUR DATABASE echo json_encode(['message'=>'No product found']); } ?> ``` ProductList.vue ```javascript= <template> <div> <el-table :data="tableData" style="width: 100%;"> <el-table-column prop="ProdName" label="產品名稱" width="180"> </el-table-column> <el-table-column prop="ProdID" label="產品編號" width="180"> </el-table-column> <el-table-column prop="UnitPrice" label="單價" width="180"> </el-table-column> <el-table-column prop="Cost" label="成本"> </el-table-column> </el-table> </div> </template> <script> export default { name: 'TableView', components: { }, data () { return { tableData: [] } }, created () { var url = "http://localhost/api/v1/product/read.php"; let self = this this.$https.get(url) .then(function (response) { console.log(response) self.tableData = response.data.results // ProdName: "EnhanceIDE VL BUS" // ProdID: "EIDE2RP" // UnitPrice: "1560" // Cost: "990" // seq: 33, 行政區: "阿蓮區", 臨時停車處所: "阿蓮國小", 可提供小型車停車位: "30", 地址: "民族路163號"} }) .catch(function (error) { console.log(error) }); }, mounted () { }, beforeDestroy () { }, methods: { } } </script> <style> </style> ``` --- ## 串接後端php程式做新增改查(CRUD) - 20191226 接續實作新增產品的功能: * 排版 * 增加 Container容器 * 新增產品 * 增加一個新增按鈕 * 使用Dialog對話框 * 將相關數據的內容可以配置在欄位當中 * 點選按鈕上傳或是取消 * 設計create.php 接收資料 並寫進至資料庫 * 新增完成後,將新的資料放進表格當中 * 問題點 * CORS問題(https://stackoverflow.com/questions/8719276/cross-origin-request-headerscors-with-php-headers) * AXIOS POST 範例(https://segmentfault.com/a/1190000015261229) * PHP insert資料 (https://stackoverflow.com/questions/39756812/pdo-insert-statement-with-variables) #### 試組合並修改下列程式碼 解決CORS跨網域存取並接收來自axios所送出的json訊息 ```php= // create.php if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: POST, GET, DELETE, PUT, PATCH, OPTIONS'); header('Access-Control-Allow-Headers: token, Content-Type'); header('Access-Control-Max-Age: 1728000'); header('Content-Length: 0'); header('Content-Type: text/plain'); die(); } header('Access-Control-Allow-Origin: *'); header('Content-Type: application/json'); // INCLUDING DATABASE AND MAKING OBJECT include_once '../../config/database.php'; $db_connection = new Database(); $conn = $db_connection->dbConnection(); // get posted data json_decode $data = json_decode(file_get_contents("php://input")); ``` 判斷post json資料是否完整 ```php= // create.php // make sure data is not empty if ( !empty($data->column1) && !empty($data->column2) && !empty($data->column3) && !empty($data->column4) ) { $sql = "INSERT INTO `product` (ProdName, ProdID, UnitPrice, Cost) VALUES (?,?,?,?)"; $stmt = $conn->prepare($sql); $stmt->execute([$data->column1, $data->column2, $data->column3, $data->column4]); // set response code - 201 created http_response_code(201); // tell the user echo json_encode(['message'=>'ok', 'results' => "Product was created."]); } // tell the user data is incomplete else { // set response code - 400 bad request http_response_code(400); echo json_encode(['message'=>'error', 'results' => "Unable to create product. Data is incomplete.", 'data' => $data]); } ``` 新增dialogVisible 和 editForm欄位 ```javascript= data () { return { dialogVisible: false, tableData: [], editForm: { ProdName: '', ProdID: '', UnitPrice: 0, Cost: 0 } } } ``` axios POST ```javascript= onCreateProductClick () { let self = this let url = "http://localhost/api/v1/product/????????" console.log(self.editForm) this.$https.post(url, { ProdName: self.editForm.ProdName, ProdID: self.editForm.ProdID, UnitPrice: self.editForm.UnitPrice, Cost: self.editForm.Cost }) .then(function (response) { console.log(response) // self.tableData = response.data.results }) .catch(function (error) { console.log(error) }); } ``` ```htmlmixed= <el-dialog title="提示" width="50%" :visible.sync="dialogVisible"> <span>新增產品資料(以下需修改)</span> <el-form label-position="left" label-width="80px" :model="form"> <el-form-item label="產品名稱"> <el-input v-model="form.ProdName"></el-input> </el-form-item> </el-form> <span slot="footer" class="dialog-footer"> <el-button @click="dialogVisible = false">取 消</el-button> <el-button type="primary" @click="onCreateProductClick">確 定</el-button> </span> </el-dialog> ``` 參考: https://www.codeofaninja.com/2017/02/create-simple-rest-api-in-php.html https://kknews.cc/zh-tw/code/z92a3jg.html --- ## 串接後端php程式做新增改查(CRUD) - 20190102 表格內容中增加 el-table-column ,增加編輯和刪除按鈕,並分別對應handleEdit 和 handleDelete methods ```javascript= <el-table-column label="操作"> <template slot-scope="scope"> <el-button size="mini" @click="handleEdit(scope.$index, scope.row)">編輯</el-button> <el-button size="mini" type="danger" @click="handleDelete(scope.$index, scope.row)">刪除</el-button> </template> </el-table-column> ``` handleEdit 與 handleDelete 方法,透過index參數,搭配tableData取得需要修改的資料欄位,並將該資料放置到editForm參數內 以下內容放置在 methods中 ```javascript= handleEdit(index, row) { let self = this let product = self.tableData[index] self.editForm = { ProdName: product.ProdName, ProdID: product.ProdID, UnitPrice: product.UnitPrice, Cost: product.Cost } self.editAction = 'update' self.dialogVisible = true }, handleDelete(index, row) { let self = this let product = self.tableData[index] self.editForm = { ProdName: product.ProdName, ProdID: product.ProdID, UnitPrice: product.UnitPrice, Cost: product.Cost } self.editAction = 'delete' self.$confirm(`確認要刪除? 產品編號:${product.ProdID}`) .then(_ => { self.deleteProduct() }) .catch(_ => {}) } ``` 以及追加一個變數editAction來控制送出按鈕的行為(區分新增與更新功能) 以下內容放置在 methods中 ```javascript= onConfirm () { let self = this if (self.editAction === 'create') { self.createProduct() } else if (self.editAction === 'update') { self.updateProduct() } } ``` 更新後的dialog 以下內容放置在 <template>標籤中 ```htmlmixed= <el-dialog title="產品資料" width="80%" :visible.sync="dialogVisible"> <el-form label-position="left" label-width="80px" :model="editForm"> <el-form-item label="產品名稱"> <el-input v-model="editForm.ProdName"></el-input> </el-form-item> <el-form-item label="產品編號"> <el-input v-model="editForm.ProdID"></el-input> </el-form-item> <el-form-item label="價格"> <el-input v-model="editForm.UnitPrice"></el-input> </el-form-item> <el-form-item label="成本"> <el-input v-model="editForm.Cost"></el-input> </el-form-item> </el-form> <span slot="footer" class="dialog-footer"> <el-button @click="dialogVisible = false">取 消</el-button> <el-button type="primary" @click="onConfirm">確 定</el-button> </span> </el-dialog> ``` 另外新增update.php 與 delete.php 分別讓vue這邊可以透過axios搭配post的方式進行API呼叫 ```javascript= updateProduct () { let self = this let url = "http://localhost/api/v1/product/update.php" }, deleteProduct () { let self = this let url = "http://localhost/api/v1/product/delete.php" } ``` 更新資料 update.php的參考內容 ```php= <?php // create.php if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: POST, GET, DELETE, PUT, PATCH, OPTIONS'); header('Access-Control-Allow-Headers: token, Content-Type'); header('Access-Control-Max-Age: 1728000'); header('Content-Length: 0'); header('Content-Type: text/plain'); die(); } header('Access-Control-Allow-Origin: *'); header('Content-Type: application/json'); // INCLUDING DATABASE AND MAKING OBJECT include_once '../../config/database.php'; $db_connection = new Database(); $conn = $db_connection->dbConnection(); // get posted data json_decode $data = json_decode(file_get_contents("php://input")); // create.php // make sure data is not empty // TODO: 修改column if ( !empty($data->column1) && !empty($data->column2) && !empty($data->column3) && !empty($data->column4) ) { $sql = "UPDATE `product` SET ProdName=?, UnitPrice=?, Cost=? WHERE ProdID=?"; $stmt = $conn->prepare($sql); // TODO: 注意column的順序 (ProdID在結尾) $stmt->execute([$data->column1, $data->column2, $data->column3, $data->column4]); // set response code - 201 created http_response_code(201); // tell the user echo json_encode(['message'=>'ok', 'results' => "Product was created."]); } // tell the user data is incomplete else { // set response code - 400 bad request http_response_code(400); echo json_encode(['message'=>'error', 'results' => "Unable to create product. Data is incomplete.", 'data' => $data]); } ?> ``` ```php= <?php // create.php if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: POST, GET, DELETE, PUT, PATCH, OPTIONS'); header('Access-Control-Allow-Headers: token, Content-Type'); header('Access-Control-Max-Age: 1728000'); header('Content-Length: 0'); header('Content-Type: text/plain'); die(); } header('Access-Control-Allow-Origin: *'); header('Content-Type: application/json'); // INCLUDING DATABASE AND MAKING OBJECT include_once '../../config/database.php'; $db_connection = new Database(); $conn = $db_connection->dbConnection(); // get posted data json_decode $data = json_decode(file_get_contents("php://input")); // create.php // make sure data is not empty // TODO: 修改column if (!empty($data->column)) { $sql = "DELETE FROM `product` WHERE ProdID=?"; $stmt = $conn->prepare($sql); // TODO: 修改column $stmt->execute([$data->column]); // set response code - 201 created http_response_code(201); // tell the user echo json_encode(['message'=>'ok', 'results' => "Product was created."]); } // tell the user data is incomplete else { // set response code - 400 bad request http_response_code(400); echo json_encode(['message'=>'error', 'results' => "Unable to create product. Data is incomplete.", 'data' => $data]); } ?> ``` --- 部屬到fs.mis.kuas.edu.tw上的方法 例如: http://fs.mis.kuas.edu.tw/~你的學號/檔案資料夾/index.html#/ (記得修改學號跟資料夾 不要用中文,然後要跟網址跟目錄對應) 修改vue 專案下的 config/index.js ```javascript= build: { // Template for index.html index: path.resolve(__dirname, '../dist/index.html'), // Paths assetsRoot: path.resolve(__dirname, '../dist'), assetsSubDirectory: 'static', assetsPublicPath: '/~你的學號/檔案資料夾/', /** * Source Maps */ productionSourceMap: true, // https://webpack.js.org/configuration/devtool/#production devtool: '#source-map', // Gzip off by default as many popular static hosts such as // Surge or Netlify already gzip all static assets for you. // Before setting to `true`, make sure to: // npm install --save-dev compression-webpack-plugin productionGzip: false, productionGzipExtensions: ['js', 'css'], // Run the build command with an extra argument to // View the bundle analyzer report after build finishes: // `npm run build --report` // Set to `true` or `false` to always turn it on or off bundleAnalyzerReport: process.env.npm_config_report } ``` 1. 修改完成後 記得要重新使用 npm run build 並將dist/下的檔案覆蓋 伺服器上的檔案! 2. PHP也要丟上去,並且注意database.php連線的參數要改 ---