拋開以前本身捯飭的小項目,工做以後第一次獨自承擔一個完整的 vue 項目。記錄一下所學習到的以及本身沉澱下來的東西。
一般會使用 vue-cli3
腳手架工具進行項目的初始化,完成以後添加自定義的 webpack
配置文件 vue.config.js
css
// vue.config.js module.exports = { //打包以後不出現 404 publicPath: './', devServer: { // 開發端口 port: 3000, // 請求轉發以及重定向路徑 proxy: { '/api': { target: 'http://localhost:3001/', pathRewrite: { "^/api": "/" } } } } };
對於單頁面應用來講,每一個路由對應一個頁面,隨着應用功能的豐富,路由數量也會逐漸增多,所以,一個便於維護的路由配置是相當重要的。html
將整個項目按功能劃分爲多個模塊,每一個模塊內部分別對自身的路由進行管理。例如「用戶」模塊下,可能有 /#/user
,/#/user/setting
、/#/user/list
等這幾個路由,能夠將這個模塊下的路由(包含了相同的路由前綴 /user
)單獨寫入一個文件 user.js
進行管理。前端
// user.js export default [ { // 匹配 '/#/user' path: '', component: () => import('../views/User/Index.vue') }, { // 匹配 '/#/user/setting' path: 'setting', component: () => import('../views/User/Setting.vue') }, { // 匹配 '/#/user/list' path: 'list', component: () => import('../views/User/List.vue') } ]; // index.js import Vue from 'vue'; import VueRouter from 'vue-router'; import user from './user'; // 避免 router.push 相同路由的錯誤 const originalPush = VueRouter.prototype.push; VueRouter.prototype.push = function push(location) { return originalPush.call(this, location).catch(err => err) }; Vue.use(VueRouter); const routes = [ { path: '/user', component: () => import('../views/CommonWrap.vue'), children: user } ]; const router = new VueRouter({ routes }); export default router;
index.js
文件的根路由配置中,利用 children
屬性引入了 user 模塊的路由配置,但與 express
框架的模塊化路由寫法不一樣的是, vue-router
的 children
屬性中的路由只能渲染父組件中 <router-view />
的那部分。因此須要一個臨時工組件來做爲各模塊子路由的出口。vue
// CommonWrap.vue // 臨時工組件,只須要提供子路由的渲染出口 <template> <router-view /> </template>
單頁面應用中如何作到登陸狀態的保持,不一樣用戶權限對頁面的訪問權限(後臺能夠作到數據層面的控制,路由的訪問權限則須要前端去作)。node
單頁應用中登陸狀態保持和頁面訪問權限控制的解決辦法react
一般仍是會進行模塊化狀態管理linux
// index.js import Vue from 'vue'; import Vuex from 'vuex'; import user from './user'; Vue.use(Vuex); export default new Vuex.Store({ modules: { user } }); // user.js export default { namespaced: true, state: {}, getters: {}, // (state) mutations: {}, // (state, payload) actions: {} // ({ commit }) }
我的感受 vuex
比 react-redux
用起來簡單方便,不用在好幾個文件裏找來找去……webpack
添加 scoped
屬性可讓樣式只在組件內生效,可是會發現有些選擇器沒有達到預期的效果。例如在使用 el-menu
組件時,直接設置類名 el-submenu__title
的樣式沒有效果,或者在父組件設置子組件中類名的樣式也無法生效。ios
利用 /deep/
或者 ::v-deep
進行深度選擇(在使用 scss
的狀況下只能用 ::v-deep
),就沒必要採用設置全局樣式的方式致使類名覆蓋了。web
實際使用 display: flex
進行佈局以後發現,會出現嵌套的狀況,而且這種狀況是很常見的。預先將 flex
配置寫成全局類名,是不是一個解決辦法呢?
一般會將目錄配置成數據的形式,數據的結構多是下面這樣
interface menu { name: string; route: string; icon: string; children: menu[]; }
顯然,有 children
屬性的目錄表明有子目錄,它須要做爲 el-submenu
組件,而沒有這個屬性就做爲 el-menu-item
組件。對於不定層數的目錄結構,須要對自身進行遞歸。
// MenuTemplate.vue <template> <div> <template v-for="(menu, index) in menus"> <!-- 沒有子目錄,做爲 el-menu-item 組件 --> <template v-if="!menu.children || menu.children.length == 0"> <el-menu-item :index="menu.name" :key="index"> <span>{{ menu.name }}</span> </el-menu-item> </template> <!-- 有子目錄 --> <template v-else> <el-submenu :index="menu.name" :key="index"> <template slot="title"> <span>{{ menu.name }}</span> </template> <!-- 遞歸 children --> <menu-template :menus="menu.children" /> </el-submenu> </template> </template> </div> </template> <script> export default { name: 'menuTemplate', props: { menus: { type: Array, default() { return []; } } } } </script> // Menu.vue <el-menu> <menu-template :menus="menusData" /> </el-menu>
Element UI 分別提供了表格組件 Table
和 分頁組件 Pagination
,管理系統中,表格是使用最多的組件,也都是須要分頁功能的,而且前端分頁和後端分頁的需求都會有,因而便學習 Easy UI 中的方式封裝了一個符合大部分業務場景的數據表格組件 Datagrid
。
相似 easyui 中 datagrid 使用習慣的 element-ui 數據表格組件(el-datagrid)
但每每理想是豐滿的,而現實是骨感的。隨着各類各樣不一樣的功能須要加在數據表格中,例如須要各類各樣的權限條件來控制顯示與否等等,對於不按套路出牌的狀況,想一勞永逸是沒那麼容易的,仍是須要在這個組件上稍做修改……
目的:
response.data
這部分我寫的這個比較簡單,沒有作超時等錯誤處理……
目的:
// extend.js import axios from 'axios'; export default function(obj) { // 發送 ajax obj._ajax = function(type, url, params={}) { switch (type) { case 'get': case 'delete': return axios[type](url, { params }).then(res => { // 根據後端的不一樣返回值提早處理錯誤,正常狀況則返回數據 return res.data; }).catch(err => err); case 'post': case 'put': return axios[type](url, params).then(res => { // 根據後端的不一樣返回值提早處理錯誤,正常狀況則返回數據 return res.data; }).catch(err => err); } } // 成功消息。info、warning、error 等消息類型寫法相似 obj._success = function(message) { this.$message({ type: 'success', message, duration: 1500 }); }; // confirm 確認消息 obj._confirm = function(message, callback) { this.$confirm(message, '提示', { confirmButtonText: '確認', cancelButtonText: '取消', type: 'warning', dangerouslyUseHTMLString: true }).then( callback ).catch( () => {}); } }
把增長在 obj
中的方法賦給 Vue.prototype,這樣就能在組件中直接以 this[METHOD]
的形式使用 。
// main.js import Vue from 'vue'; import extend from './utils/extend'; extend(Vue.prototype); // Test.vue <script> export default { data() { return { id: 1 } }, methods: { async getDataById() { const res = this._ajax('delete', '/api/delete', { id: this.id }); if (res.status == 200) { this._success('操做成功'); // …… } }, showConfirm() { this._confirm('確認刪除***嗎?', this.getDataById); } } } </script>
對於單頁面應用而言,打包以後就是一些靜態內容,比較常規的作法是將這些靜態文件部署到後端應用的指定位置,但最終目的也只是經過一個指定的請求返回一個靜態的 html
文件。那麼若是有一個 node.js 應用可以達到一樣的目的就能夠實現先後端分開部署了。
// start.js const express = require('express'); const app = express(); const path = require('path'); app.use(express.static(path.join(__dirname, 'dist'))); app.get('/', (req, res) => { res.sendFile( __dirname + '/dist/index.html'); }); app.listen(3000);
這是一段能夠在 windows
和 linux
中運行的 node.js
關閉指定端口的代碼。
// stop.js var port = '3000'; var exec = require('child_process').exec; if (process.platform == 'win32') { exec('netstat -ano | findstr ' + port, (err, stdout, stderr) => { if (err) { return; } const line = stdout.split('\n')[0].trim().split(/\s+/); const pid = line[4]; exec('taskkill /F /pid ' + pid, (err, stdout, stderr) => { if (err) { return; } console.log('佔用指定端口的程序被成功殺掉!'); }); }); } else { exec('netstat -anp | grep ' + port, (err, stdout, stderr) => { if (err) { return; } const line = stdout.split('\n')[0].trim().split(/\s+/); const pid = line[6].split('/')[0]; exec('kill -9 ' + pid, (err, stdout, stderr) => { if (err) { return; } console.log('佔用指定端口的程序被成功殺掉!'); }); }); }
開發環境、測試環境、正式環境最基本的差別就是請求路徑的不一樣。爲了確保各環境對應請求路徑的準確性,必然要經過代碼來控制,vue-cli
恰好也提供了這麼一套機制。
經過配置 package.json
、.env.***
文件,經過環境變量判斷當前應該選擇的請求路徑。
// package.json { "scripts": { "build-test": "vue-cli-service build --mode test", "build-online": "vue-cli-service build --mode online" } } // .env.test NODE_ENV = 'production'; VUE_APP_ENV = 'test'; // .env.online NODE_ENV = 'production'; VUE_APP_ENV = 'online'; // setting.js let BASE_URL = ''; if (process.env.NODE_ENV == 'production') { if (process.env.VUE_APP_ENV == 'online') { BASE_URL = 'https://online.xxx.com'; } else { BASE_URL = 'https://test.xxx.com'; } } else { BASE_URL = 'http://localhost:3000' }
與其餘服務端語音同樣,node.js 應用須要的也是一個打包以後的靜態文件夾。利用 vue-cli
腳手架構建的項目,一般會利用其集成好的命令進行打包輸出。
直接將源代碼發佈到服務器,安裝好 package.json
中的依賴以後運行 npm run build-online
進行打包,這看起來是最簡單直接的方法。我使用的自動化構建平臺須要將構建機器上面的內容傳輸到發佈機器上以後,再執行 npm run start
監聽服務的指定端口。缺點是速度較慢,npm install
的耗時不穩定,node_modules 文件夾的傳輸很慢……
服務器打包的優點是操做簡單,但消耗的時間比較久,但對於 node.js 應用來講,有哪些是最少須要的依賴呢?仔細想一想,node.js 應用只須要監聽一個服務端口以及相應的靜態文件就能夠了,vue-cli
等開發依賴,vue
、element-ui
等運行依賴對於只做爲靜態文件服務的 nodejs 應用來講,其實都不是必須的。
在發佈到測試(正式)環境前,先在本地運行 npm run build-test
(npm run build-online
),由於不須要進行 npm install
和傳輸 node_modules 文件夾,因此這個時間是穩定可控的。最後將包含靜態文件的 dist 文件夾一併推送到遠端,就能夠在服務器上面直接運行 npm run start
開啓服務了。
在這裏總結一下開發 vue 中用到的特性以及對這些特性的理解,因爲涉及到的業務不復雜,有不少特性其實尚未用到……
不少場景下,在列表渲染的組件中須要判斷每一項的某些屬性的值來決定是否顯示或者是否有某個不同效果,官方教程不推薦在同一個組件中同時使用 v-for
和 v-if
,我一般會在外層增長一個 <template>
使用 v-for
,將 v-if
和 key
值寫在實際須要渲染的組件中。
// Test.vue <template> <div> <template v-for="(item, index) in datas"> <span v-if="item.name" :key="index"> {{ item.name }} </span> <p v-else>暫無數據</p> </template> </div> </template>
watch
用來監聽變量的改變,這個變量能夠是 data
中的屬性,也能夠是 computed
中的屬性。除了最直接的將須要監聽的屬性賦值爲一個函數的用法,完整的用法包括如下三個屬性
watch: { someData: { handle(newVal, oldVal) { // 監聽變量變化的處理函數 }, deep: true, // 是否深度監聽,例如對象某些屬性的變化 immediate: true // 是否在第一次賦值時執行 } }
data
中的變量通常是經過從新賦值實現響應式效果,而 computed
中的變量只會在初始化的時候寫好處理邏輯,以後的響應式效果不須要再操做這些變量。例如這個場景,根據路由中的參數的改變獲取不一樣的數據,有如下兩種實現方式
// 變量在 data 中 data() { return { id: this.$route.params.id } }, methods: { getData() { console.log(this.id) } }, watch: { id: { handler() { this.getData(); }, immediate: true }, '$route.params.id': { handler() { this.id = this.$route.params.id; } } } // 變量在 computed 中 computed: { id() { return this.$route.params.id; } }, methods: { getData() { console.log(this.id) } }, watch: { id: { handler() { this.getData(); }, immediate: true } }
將路由的參數存儲在 data
中,就必須額外監聽路由參數的改變,而後再手動的修改 data
中的值;而將路由參數存儲在 conmputed
中,這個變量就會隨着路由的改變而改變,不須要再顯式的從新賦值。
vue 的組件之間消息傳遞方式不少,這裏只列舉在開發過程當中經常使用、能知足大部分場景的幾種方式。
最經常使用的父組件給子組件傳遞方法,子組件不能直接修改這個數據,但能夠在子組件的 data
中深拷貝一份以後再修改。
子組件向父組件傳遞一個包含數據的事件,父組件監聽這個事件名而後作出相應的處理。
多用來父組件通知子組件作出一些操做,經過 $refs
獲取到子組件的實例,調用實例上的方法。
利用一個臨時組件,分別在兩個須要通訊的組件中進行 $on
和 $emit
的操做。
這是終極解決方案。