用vue全家桶開發一年多了,踩過很多坑,也解決了不少的問題,把其中的一些點記錄下來,但願能幫到你們。如下內容基於最新版的vue + vuex + vue-router + axios + less + elementUI,vue腳手架是vue-cli3。javascript
vue 爲了防止 css 污染,當組件的 <style>
標籤有 scoped
屬性時,它的 css 只做用於當前組件中的元素。實現原理很簡單,給當前組件中的每一個標籤都加上惟一的自定義屬性:data-v-惟一的屬性
,而後 css 選擇器都加上屬性選擇器.article-title[data-v-惟一的屬性]
,這樣這個 css 只會匹配到當前頁面的這個元素。php
注意:每一個組件的最外層的標籤會帶上父組件的data-v-
屬性,也就是這個標籤會被父組件的樣式匹配到,因此父組件儘可能不要使用標籤選擇器,這個標籤不要使用父組件中的 id 或者 class。css
在父組件想修改子組件的css(修改elementUI組件的樣式),咱們能夠藉助深度做用選擇器 >>>
html
div >>> .el-input{
width: 100px;
}
/* sass/less的話可能沒法識別,這時候須要使用 /deep/ 選擇器。 */
div /deep/ .el-input{
width: 100px;
}
複製代碼
深度做用選擇器會去掉後面元素的屬性選擇器[data-v-]
,即上面代碼會編譯成:div[data-v-12345667] .el-input{}
。就能夠匹配到子組件的元素,從而覆蓋樣式。vue
組件的生命週期鉤子函數是到了某個生命週期點就會觸發,而不是在這個鉤子函數中進行生命週期,好比說DOM加載好了,就會觸發mounted
鉤子函數,因此在created
裏面寫一個延遲定時器,mounted
鉤子不會等定時器執行。java
各個週期鉤子函數觸發的時間點參考(圖來源於網絡)ios
關於父子組件的生命週期:不一樣的鉤子函數有不一樣的表現。父組件的虛擬 DOM 先初始化好了(beforeMount
),纔會去初始化子組件的虛擬 DOM (beforeMount
),而 mounted
事件,等價於 window.onload
,子組件 DOM 沒加載好,父組件 DOM 永遠不可能加載好。因此基本生命週期鉤子函數執行順序是:父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mountedvue-router
父子組件的update和beforeUpdate執行前後順序:數據修改+虛擬DOM準備好會觸發beforeUpdate,換句話說beforeUpdate等價於beforeMount,而update等價於mounted。因此前後順序是: 父beforeUpdate -> 子beforeUpdate -> 子update -> 父update。vuex
同理beforeDestory
和destoryed
的前後順序是:父beforeDestory -> 子beforeDestory -> 子destoryed -> 父destoryed。vue-cli
生命週期鉤子函數其實也能夠寫成數組的形式:mounted: [mounted1, mounted2]
,同一個生命週期能夠觸發多個函數,這也是mixin
(混入)的原理,mixin
裏面也能夠寫生命週期鉤子,最終會和組件裏面的生命週期鉤子函數一塊兒變成數組形式,mixin
裏面的鉤子函數會先執行。
不少人以爲在 created
事件裏面把數據請求到,而後一塊兒生成虛擬 DOM,再渲染會更好。實際上呢,請求是須要時間的,並且這個時間具備不穩定性,極可能 vue
的虛擬 DOM 準備好了,你的數據才請求到,而後又得更新一遍虛擬 DOM,再渲染,極大地延長了白屏時間,用戶體驗很很差。而在 mounted
事件請求數據呢,靜態頁面會先渲染好,等數據好了,再更新部分 DOM 便可。
補充:經掘友指出,這裏理解有誤,抱歉。
生命週期鉤子函數的中異步會放入事件隊列,而不會在這個鉤子函數中執行。也就是說你在 created
和 mounted
中請求數據是同樣的,都不會當即更新數據,因此不會致使虛擬DOM從新加載,也不影響頁面中靜態的部分加載。生命週期鉤子函數中的異步賦值,vue會在一遍流程走完以後執行update
。另外,給數據賦值而後更新 DOM 也是異步的,偵聽到數據變化,Vue 將開啓一個隊列,並緩衝在同一事件循環中發生的全部數據變動,去掉重複賦值而後更新。
生命週期鉤子函數中的異步行爲測試:
export default {
data(){
return {
list:[],
}
},
methods:{
getData(){
//生成指定範圍的隨機整數
const randomNum = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
//生成固定長度的非空數組
const randomArr = length => Array.from({ length }, (item, index) => index * 2);
const time = randomNum(100,3000);//模擬請求時間
console.log('getData start');
return new Promise(resolve => {
setTimeout(() => {
const arr = randomArr(10);
resolve(arr);
},time)
})
}
},
async created(){
console.log('created');
this.list = await this.getData();
console.log('getData end');
},
beforeMount() {
console.log('beforeMount');
},
mounted(){
console.log('mounted');
},
updated(){
console.log('updated');
}
}
複製代碼
結果以下圖,因此在 created
中和 mounted
中請求數據,數據的更新時間是同樣的,在 created
中發起請求,能夠更早的請求到數據。而且使用服務端渲染SSR的時候, mounted
鉤子不會加載。
能夠寫自定義事件,而後在子組件的生命週期函數中觸發這個自定義事件,可是不優雅,咱們可使用 hook:
<child @hook:created="childCreated"></child>
複製代碼
從 A 頁面切換到 B 頁面,A 頁面中有一個定時器,到了 B 頁面用不上,須要在離開 A 頁面的時候清除掉,辦法很簡單,在 A 頁面的生命週期鉤子函數beforeDestory
或者路由鉤子函數beforeRouteLeave
裏面清除掉就行,可是問題來了,怎麼拿到定時器呢?把定時器寫到 data
裏面,可行可是不優雅,咱們有以下寫法:
//在初始化定時器以後
this.$once('hook:beforeDestory',()=>{
clearInterval(timer);
})
複製代碼
因爲 HTML 標籤的限制,tr 標籤裏面只能有 th, td 標籤,而寫自定義標籤則會被解析到 tr 標籤外層,因此這時候咱們能夠用 is 屬性
<tr>
<td is="child">
</tr>
複製代碼
最近有個頁面有大量的 SVG 圖標,我將每個 SVG 都寫成了一個組件。因爲 SVG 組件名稱又各不相同,因此須要動態標籤來表示:
<!-- 假設咱們的數據以下 -->
arr: [ { id: 1, name: 'first' }, { id: 2, name: 'second' }, { id: 3, name: 'third' }, ]
<!-- 原本須要這樣寫 -->
<div v-for="item in arr" :key="item.id">
<p>item.name</p>
<svg-first v-if="item.id===1"></svg-first>
<svg-second v-if="item.id===2"></svg-second>
<svg-third v-if="item.id===3"></svg-third>
</div>
<!-- 其實這樣寫更優雅 -->
<div v-for="item in arr" :key="item.id">
<p>item.name</p>
<component :is="'svg'+item.name"></component>
</div>
複製代碼
原生 DOM 事件綁定的函數的第一個參數都會是事件對象event
,可是有時候咱們想給這個函數傳其餘的參數,直接傳會覆蓋掉event
,咱們能夠這麼寫<div @click="clickDiv(params,$event)"></div>
,變量$event
就表明事件對象。
若是要傳的變量不是事件對象呢?在使用 elementUI
的時候碰到這麼一個狀況,在表格中使用了下拉菜單組件,代碼以下:
<el-table-column label="日期" width="180">
<template v-slot="{row}">
<el-dropdown @command="handleCommand">
<span>
下拉菜單<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="a">黃金糕</el-dropdown-item>
<el-dropdown-item command="b">獅子頭</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</el-table-column>
複製代碼
下拉菜單事件 command
函數自帶一個參數,爲下拉選中的值,這個時候咱們想把表格數據傳過去,若是@command="handleCommand(row)"
這樣寫,就會覆蓋掉自帶的參數,該怎麼辦呢?這時候咱們能夠藉助箭頭函數:@command="command => handleCommand(row,command)"
,完美解決傳參問題。
順便說一下,elementUI
的表格能夠用變量$index
表明當前的列數,和$event
同樣的使用:
<el-table-column label="操做">
<template v-slot="{ row, $index }">
<el-button @click="handleEdit($index, row)">編輯</el-button>
</template>
</el-table-column>
複製代碼
經掘友指點,默認參數有多個(或者未知個數)的時候,能夠這樣寫:@current-change="(...defaultArgs) => treeclick(ortherArgs, ...defaultArgs)"
v-slot
的用法(slot 語法已經廢棄):至關於在組件中留一個空位,使用該組件的時候能夠傳一些標籤過去,插入到對應的空位。能夠有多個空位,取不一樣的名字便可,默認是 default
。同時還能夠將一些數據傳過去,簡寫是#
。
<!-- 子組件 -->
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
<!-- 父組件 -->
<base-layout>
<!-- 插槽能夠簡寫爲# -->
<template #header="data">
<h1>Here might be a page title</h1>
</template>
<!-- v-slot:default可省略 -->
<div v-slot:default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</div>
<!-- 可使用解構 -->
<template #footer="{ user }">
<p>Here's some contact info</p>
</template>
</base-layout>
複製代碼
總結:
template
標籤。<child :data="data">
<div>在這裏訪問不到data數據</div>
</child>
複製代碼
v-slot="{ user }"
。v-model
在使用的時候很像雙向綁定的,可是 Vue 是單項數據流,v-model
只是語法糖而已:父組件用v-bind
將值傳給子組件,子組件經過 change/input 事件觸發修改父組件的值。
<input v-model="inputValue" />
<!-- 等價於 -->
<input :value="inputValue" @change="inputValue = $event.target.value" />
複製代碼
v-model
不只僅能在 input
上用,在組件上也能使用。
vue 組件間傳遞數據是單向的,即數據老是由父組件傳遞到子組件,子組件在其內部能夠有本身維護的數據,但它無權修改父組件傳遞給它的數據,咱們也能夠參照v-model
語法糖進行修改父組件的值,可是每次都這樣寫太麻煩了,vue 提供了一個修飾符.sync
,用法以下:
<child :value.sync="inputValue"></child>
<!-- 子組件 -->
<script> export default { props: { //props能夠設置值得類型,默認值,是否必傳以及校驗函數 value: { type: [String, Number], required: true, }, }, //用一個變量中轉,子組件中就用_value就不會直接修改父組件的值 computed: { _value: { get() { return this.value; }, set(val) { this.$emit('update:value', val); }, }, }, }; </script>
複製代碼
雖然 vue
提供 $parent
和 $children
來訪問父/子組件,可是組件的父組件/子組件存在不少不肯定性,例如組件被複用,他的父組件有多種狀況。咱們能夠經過 ref 訪問到子組件的數據和方法。
<child ref="myChild"></child>
<script> export default { async mounted() { await this.$nextTick(); console.dir(this.$refs.myChild); }, }; </script>
複製代碼
注意:
ref
必須等 DOM 加載好了才能夠訪問mounted
生命週期 DOM 已經加載好了,可是爲了以防萬一,咱們可使用 $nextTick
函數在用 Webpack
處理打包時,可將某一目錄配置一個別名,代碼中就能使用與別名的相對路徑引用資源
import tool from '@/utils/test'; // Webpack 能正確識別並打包。
複製代碼
可是在 css
文件,如 less, sass, stylus 中,使用 @import "@/style/theme"
的語法引用相對 @
的目錄確會報錯。 解決辦法是是在引用路徑的字符串最前面添加上 ~ 符號。
@import "~@/style/theme.less"
background: url("~@/assets/xxx.jpg")
<img src="~@/assets/xxx.jpg">
咱們先來看一個完整的 URL:https://www.baidu.com/blog/guide/vuePlugin.html#vue-router
。其中 https://www.baidu.com
是網站根目錄,/blog/guide/
是子目錄,vuePlugin.html
是子目錄下的文件(若是隻有目錄,沒有指定文件,會默認請求index.html
文件),而#vue-router
就是哈希值。
vue 是單頁應用,打包以後只有一個 index.html
,將他部署到服務器上以後,訪問對應文件的目錄就是訪問這個文件。
hash 模式:網址後面跟着 hash 值,hash 值對應每個 router
的名稱,hash
值改變意味着router
改變,監聽 onhashchange
事件,來替換頁面內容。
history 模式:網址後面跟着‘假的目錄名’,其值就是 router
的名稱,而瀏覽器會去請求這個目錄的文件(並不存在,會 404),因此 history
模式須要服務器配合,配置 404 頁面重定向到到咱們的 index.html
,而後 vue-router
會根據目錄的名稱來替換頁面內容。
優缺點:
onhashchange
事件切換路由,兼容性會好一點,不須要服務器配合HTML5
的 history API
,兼容性差一點。二者的配置區別在於:
const router = new VueRouter({
mode: 'history', //"hash"模式是默認的,無需配置
base: '/',//默認配置
routes: [...]
})
複製代碼
vue-cli3 的 vue.config.js 配置:
module.exports = {
publicPath: "./", // hash模式打包用
// publicPath: "/", // history模式打包用
devServer: {
open: true,
port: 88,
// historyApiFallback: true, //history模式本地開發用
}
}
複製代碼
若是是網站部署在根目錄,router
的 base
就不用填。若是整個單頁應用服務在 /app/
下,而後 base
就應該設爲 "/app/"
,同時打包配置(vue.config.js
)的 publicPath
也應該設置成/app/
。
vue-cli3
生成新項目的時候會有選擇路由的模式,選擇history
模式就會幫你都配置好。
鉤子函數分三種:組件內鉤子,全局鉤子,路由獨享鉤子。
APP.vue
沒有組件內鉤子函數,由於APP.vue
是頁面的入口,這個組件是一定會加載的,而使用組件內鉤子函數能夠阻止組件加載。
全局鉤子主要用於路由鑑權,可是消耗很大。組件內的鉤子beforeRouteLeave
主要用於用戶離開前的提示(好比說有未保存的文章),這個鉤子有一些坑:hash模式下,瀏覽器的後退按鈕沒法觸發這個鉤子函數。同時咱們還能夠監聽用戶的關閉當前窗口/瀏覽器事件:
window.onbeforeunload = e => "肯定離開當前頁面,你的修改將不會被保存!";
複製代碼
爲了防止惡意網站,用戶關閉窗口/瀏覽器事件是不可阻止的,只能提示,並且不一樣的瀏覽器兼容性也不一樣。
Vuex 中的數據,刷新頁面以後就會丟失。要實現持久化存儲須要藉助本地存儲(cookie 和 storage 等),通常是登陸以後返回的數據(角色,權限,token 等)須要存儲到 Vuex,因此咱們能夠在登陸頁將數據存儲到本地,而在主頁面(除了登陸頁,其餘全部頁面的入口)進入以前(beforeCreate
或者路由鉤子 beforeRouteEnter
)讀取出來,並提交到 Vuex
就行了。這樣即便刷新,也會觸發主頁面的進入鉤子函數,會被提交到 Vuex
。
beforeRouteEnter (to, from, next) {
const token = localStorage.getItem('token');
let right = localStorage.getItem('right');
try{
right = JSON.parse(right);
}catch{
next(vm => {
//彈窗採用elementUI
vm.$alert('獲取權限失敗').finally(() => {
vm.$router.repalce({name:'login'})
})
})
}
if(!right || !token){
next({name:'login',replace:true})
}else{
next(vm => {
//這裏面的事件會在mounted以後觸發
vm.$store.commit('setToken',token);
vm.$store.commit('setRight',right);
})
}
}
複製代碼
beforeRouteEnter
的回調會在mounted
鉤子以後觸發,這就比較蛋疼了。而主頁面的mounted
會在全部子組件的mounted
以後觸發,因此咱們能夠這樣寫。
import store from '^/store';//將實例化的store引入進來
beforeRouteEnter (to, from, next) {
const token = localStorage.getItem('token');
if(!token){
next({name:'login',replace:true})
}else{
store.commit('setToken',token);
next();
}
}
複製代碼
要想實現數據修改以後仍能持久化存儲,咱們能夠先把數據存到localstorage
,而後監聽window.onstorage
事件,數據有修改提交到Vuex
。
mutations
是同步修改 state
的值,假如另外一個值是異步獲取(action
)的,依賴於這個同步的值的修改,須要在 mutations
裏面賦值以前觸發 action
裏面的事件,咱們能夠給實例化的 Vuex
命名,在 mutations
裏面拿到 store
對象。
const store = new Vuex.Store({
state: {
age: 18,
name: 'zhangsan',
},
mutations: {
setAge(state, val) {
// 假如age變化了以後,name也要跟着變化
// 須要在每次給age賦值的時候,同步觸發action裏面的getName
state.age = val;
store.dispatch('getName');
},
setName(state, val) {
state.name = val;
},
},
actions: {
getName({ commit }) {
const name = fetch('name'); //從接口異步獲取
commit('setName', name);
},
},
});
複製代碼
若是項目很小,不須要用到 vuex
,能夠用Vue.observable
來模擬一個:
//store.js
import Vue from 'vue';
const store = Vue.observable({ name: '張三', age: 20 });
const mutations = {
setAge(age) {
store.age = age;
},
setName(name) {
store.name = name;
},
};
export { store, mutations };
複製代碼
get 請求的數據放在 url 裏面,相似於http://www.baidu.com?a=1&b=2
,其中a=1&b=2
就是 get 的參數,而對於 post 請求,參數放到 body 裏面,經常使用的數據格式有表單數據和 json 數據,二者的差別就是數據格式不一樣,表單數據編碼格式和 get 同樣,只不過是放在 body 裏面,而 json 數據則是 json 字符串
qs 基本使用:
import qs from 'qs'; //qs是axios裏面自帶的,因此直接引入就能夠了
const data = qs.stringify({
username: this.formData.username,
oldPassword: this.formData.oldPassword,
newPassword: this.formData.newPassword1,
});
this.$http.post('/changePassword.php', data);
複製代碼
qs.parse()
是將 URL 解析成對象的形式,qs.stringify()
是將對象 序列化成 URL 的形式,以&進行拼接。而對於不一樣的數據格式,axios 會自動設置對應的content-type
,不須要手動設置。
application/x-www-form-urlencoded
multipart/form-data
application/json
碰到過一次接口須要我用表單傳一個數組。假設數據是arr = [1,2,3]
若是直接使用 qs.stringify(),則數據會變成arr[]=1&arr[]=2&arr[]=3
,很容易看出來,多了一個[]
,讓接口把參數名改爲arr[]
就能用,可是這樣很差。不過能夠發現,表單傳數組的本質就是同名參數傳屢次,這時候咱們也能夠這樣:
const data = new FormData();
arr.forEach(item => {
data.append('arr', item);
});
複製代碼
測試一下,完美解決,可是事情到這裏還沒完,翻一下qs 官方文檔,qs 轉換支持第二個參數,完美解決咱們的問題。
const data = qs.stringify(arr, { arrayFormat: 'repeat' }); // arr=1&arr=2&arr=3
複製代碼
const valid = await new Promise(resolve => this.$refs.form.validate(resolve));
if (!valid) return
複製代碼
.el-cascader-menu > .el-scrollbar__wrap{
height: 250px;
}
複製代碼
v-if="true"
。height="100%"
<div class="table-wrap">
<el-table :height="100%"></el-table>
</div>
複製代碼
/* less寫法 */
.table-wrap{
height: calc(~"100vh - 200px");
/* 部分版本這樣寫會失效,須要加上下面一句 */
/deep/ .el-table{
height: 100% !important;
}
}
複製代碼
upload
組件,須要將這些文件一塊兒上傳到服務器。能夠經過this.$refs.poster.uploadFiles
拿到文件對象。而後本身手動組裝成表單數據。<el-form-item label="模板文件:" required>
<el-upload ref="template" action="" :auto-upload="false" accept="application/zip" :limit="1">
<span v-if="temForm.id">
<el-button slot="trigger" type="text"><i class="el-icon-refresh"></i>更新文件</el-button>
</span>
<el-button slot="trigger" size="mini" type="success" v-else>上傳文件</el-button>
</el-upload>
</el-form-item>
<el-form-item label="模板海報:" required>
<el-upload action="" :auto-upload="false" ref="poster" accept="image/gif,image/jpeg,image/png,image/jpg" :show-file-list="false" :on-change="changePhoto">
<img :src="previewUrl" @load="revokeUrl" title="點擊上傳海報" alt="資源海報" width="250" height="140">
<template #tip>
<div>tips: 建議上傳尺寸250*140</div>
</template>
</el-upload>
</el-form-item>
複製代碼
methods:{
//選擇圖片以後替換舊圖片和顯示略縮圖
changePhoto(file, fileList) {
//建立的Blob URL可直接預覽圖片
this.previewUrl = window.URL.createObjectURL(file.raw);
if (fileList.length > 1) {
fileList.shift();
}
},
revokeUrl(e) {
//圖片加載完成以後銷燬Blob URL
if (e.target.src.startsWith("blob:")) window.URL.revokeObjectURL(e.target.src);
},
//提交表單數據
async submitData() {
const template = this.$refs.template.uploadFiles[0], //模板文件
poster = this.$refs.poster.uploadFiles[0], //海報文件
formData = new FormData();
if (!template) return this.$message.warning("必須選擇模板文件");
if (!poster) return this.$message.warning("必須選擇海報文件");
formData.append("zip", template.raw);
formData.append("poster", poster.raw);
const res = await this.$http.post('url', formData);
},
}
複製代碼
VueI18n
國際化,須要將elementUI
的語言包和項目中的語言包合併成一個。import VueI18n from "vue-i18n";
import zhLocale from './locales/zh.js';/* 引入本地簡體中文語言包 */
import zhTWLocale from './locales/zh-TW.js';/* 引入本地繁體中文語言包 */
import enLocale from './locales/en.js';/* 引入本地英語語言包 */
import zhElemment from 'element-ui/lib/locale/lang/zh-CN'//引入elementUI簡體中文語言包
import zhTWElemment from 'element-ui/lib/locale/lang/zh-TW'//引入elementUI繁體中文語言包
import enElemment from 'element-ui/lib/locale/lang/en'//引入elementUI英語語言包
Vue.use(VueI18n);
const messages = {//語言包
zh: Object.assign(zhLocale, zhElemment),//本地語言包加入elementUI的語言包
'zh-TW': Object.assign(zhTWLocale, zhTWElemment),//本地語言包加入elementUI的語言包
en: Object.assign(enLocale, enElemment)//本地語言包加入elementUI的語言包
};
const i18n = new VueI18n({
locale: "zh", //zh默認是簡體中文
messages
});
Vue.use(ElementUI, {
i18n: (key, value) => i18n.t(key, value)
})
複製代碼
有寫錯的,或者有什麼問題,歡迎你們評論