拋開之前自己捯飭的小項目,工作之後第一次獨自承擔一個完整的 vue 項目。記錄一下所學習到的以及自己沉澱下來的東西。
初始化項目
通常會使用 vue-cli3
腳手架工具進行項目的初始化,完成之後添加自定義的 webpack
配置文件 vue.config.js
// vue.config.js
module.exports = {
//打包之後不出現 404
publicPath: './',
devServer: {
// 開發端口
port: 3000,
// 請求轉發以及重定向路徑
proxy: {
'/api': {
target: 'http://localhost:3001/',
pathRewrite: {
"^/api": "/"
}
}
}
}
};
vue-router
模塊化路由配置
對於單頁面應用來說,每個路由對應一個頁面,隨着應用功能的豐富,路由數量也會逐漸增多,因此,一個便於維護的路由配置是至關重要的。
將整個項目按功能劃分爲多個模塊,每個模塊內部分別對自身的路由進行管理。例如“用戶”模塊下,可能有 /#/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 />
的那部分。所以需要一個臨時工組件來作爲各模塊子路由的出口。
// CommonWrap.vue
// 臨時工組件,只需要提供子路由的渲染出口
<template>
<router-view />
</template>
登錄狀態保持、頁面訪問權限
單頁面應用中如何做到登錄狀態的保持,不同用戶權限對頁面的訪問權限(後臺可以做到數據層面的控制,路由的訪問權限則需要前端去做)。
vuex
通常還是會進行模塊化狀態管理
// 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
用起來簡單方便,不用在好幾個文件裏找來找去……
樣式
scoped
添加 scoped
屬性可以讓樣式只在組件內生效,但是會發現有些選擇器沒有達到預期的效果。例如在使用 el-menu
組件時,直接設置類名 el-submenu__title
的樣式沒有效果,或者在父組件設置子組件中類名的樣式也沒法生效。
deep
利用 /deep/
或者 ::v-deep
進行深度選擇(在使用 scss
的情況下只能用 ::v-deep
),就不必採用設置全局樣式的方式導致類名覆蓋了。
flex
實際使用 display: flex
進行佈局之後發現,會出現嵌套的情況,並且這種情況是很常見的。預先將 flex
配置寫成全局類名,是否是一個解決辦法呢?
Element UI
多級目錄(遞歸)
通常會將目錄配置成數據的形式,數據的結構可能是下面這樣
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>
El-Datagrid
Element UI 分別提供了表格組件 Table
和 分頁組件 Pagination
,管理系統中,表格是使用最多的組件,也都是需要分頁功能的,並且前端分頁和後端分頁的需求都會有,於是便學習 Easy UI 中的方式封裝了一個符合大部分業務場景的數據表格組件 Datagrid
。
類似 easyui 中 datagrid 使用習慣的 element-ui 數據表格組件(el-datagrid)
但往往理想是豐滿的,而現實是骨感的。隨着各種各樣不同的功能需要加在數據表格中,例如需要各種各樣的權限條件來控制顯示與否等等,對於不按套路出牌的情況,想一勞永逸是沒那麼容易的,還是需要在這個組件上稍作修改……
拓展 Vue.prototype
Axios
目的:
- 統一發送 get/post 請求的參數形式
- 在具體業務邏輯之前處理一些特定的響應狀態碼
- 提前取一下
response.data
這部分我寫的這個比較簡單,沒有做超時等錯誤處理……
message 和 confirm
目的:
- 統一某些配置
- 減少、簡化業務邏輯中的重複代碼
// 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>
自動化部署 node.js 應用
對於單頁面應用而言,打包之後就是一些靜態內容,比較常規的做法是將這些靜態文件部署到後端應用的指定位置,但最終目的也只是通過一個指定的請求返回一個靜態的 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'
}
服務器打包 VS 本地打包
與其他服務端語音一樣,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 的使用總結
在這裏總結一下開發 vue 中用到的特性以及對這些特性的理解,由於涉及到的業務不復雜,有很多特性其實還沒有用到……
v-for、v-if
很多場景下,在列表渲染的組件中需要判斷每一項的某些屬性的值來決定是否顯示或者是否有某個不一樣效果,官方教程不推薦在同一個組件中同時使用 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>
data、computed、watch
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 的組件之間消息傳遞方式很多,這裏只列舉在開發過程中常用、能滿足大部分場景的幾種方式。
props
最常用的父組件給子組件傳遞方法,子組件不能直接修改這個數據,但可以在子組件的 data
中深拷貝一份之後再修改。
$emit
子組件向父組件傳遞一個包含數據的事件,父組件監聽這個事件名然後做出相應的處理。
$refs
多用來父組件通知子組件做出一些操作,通過 $refs
獲取到子組件的實例,調用實例上的方法。
event bus
利用一個臨時組件,分別在兩個需要通信的組件中進行 $on
和 $emit
的操作。
vuex
這是終極解決方案。