Vue 專案裡戳中你痛點的問題及解決辦法(更新)

愣錘發表於2018-07-03

最近要求使用vue進行前後端分離開發微信公眾號,不斷摸索踩坑之後,總結出如下幾點vue專案開發中常見的問題及解決辦法。如果你是vue大佬,請忽略小弟的愚見^V^

  • 列表進入詳情頁的傳參問題。
  • 本地開發環境請求伺服器介面跨域的問題
  • axios封裝和api介面的統一管理
  • UI庫的按需載入
  • 如何優雅的只在當前頁面中覆蓋ui庫中元件的樣式
  • 定時器問題
  • rem檔案的匯入問題
  • Vue-Awesome-Swiper基本能解決你所有的輪播需求
  • 打包後生成很大的.map檔案的問題
  • fastClick的300ms延遲解決方案
  • 元件中寫選項的順序
  • 路由懶載入(也叫延遲載入)
  • 開啟gzip壓縮程式碼
  • 詳情頁返回列表頁快取資料和瀏覽位置、其他頁面進入列表頁刷洗資料的實踐
  • css的scoped私有作用域和深度選擇器
  • hiper開啟速度測試
  • vue資料的兩種獲取方式+骨架屏
  • 自定義元件(父子元件)的雙向資料繫結
  • 路由的拆分管理
  • mixins混入簡化常見操作
  • 打包之後檔案、圖片、背景圖資源不存在或者路徑錯誤的問題
  • vue外掛的開發、釋出到github、設定展示地址、釋出npm包

===========================這是華麗麗的分割線~~=========================

列表進入詳情頁的傳參問題。

例如商品列表頁面前往商品詳情頁面,需要傳一個商品id;

<router-link :to="{path: 'detail', query: {id: 1}}">前往detail頁面</router-link>複製程式碼

c頁面的路徑為http://localhost:8080/#/detail?id=1,可以看到傳了一個引數id=1,並且就算重新整理頁面id也還會存在。此時在c頁面可以通過id來獲取對應的詳情資料,獲取id的方式是this.$route.query.id

vue傳參方式有:query、params+動態路由傳參。

說下兩者的區別:

    1.query通過path切換路由,params通過name切換路由

// query通過path切換路由
<router-link :to="{path: 'Detail', query: { id: 1 }}">前往Detail頁面</router-link>
// params通過name切換路由
<router-link :to="{name: 'Detail', params: { id: 1 }}">前往Detail頁面</router-link>複製程式碼

    2.query通過this.$route.query來接收引數,params通過this.$route.params來接收引數。

// query通過this.$route.query接收引數
created () {
    const id = this.$route.query.id;
}

// params通過this.$route.params來接收引數
created () {
    const id = this.$route.params.id;
}複製程式碼

    3.query傳參的url展現方式:/detail?id=1&user=123&identity=1&更多引數

       params+動態路由的url方式:/detail/123

    4.params動態路由傳參,一定要在路由中定義引數,然後在路由跳轉的時候必須要加上引數,否則就是空白頁面:

{      
    path: '/detail/:id',      
    name: 'Detail',      
    component: Detail    
},複製程式碼

注意,params傳參時,如果沒有在路由中定義引數,也是可以傳過去的,同時也能接收到,但是一旦重新整理頁面,這個引數就不存在了。這對於需要依賴引數進行某些操作的行為是行不通的,因為你總不可能要求使用者不能重新整理頁面吧。 例如:

// 定義的路由中,只定義一個id引數
{
    path: 'detail/:id',
    name: 'Detail',
    components: Detail
}

// template中的路由傳參,
// 傳了一個id引數和一個token引數
// id是在路由中已經定義的引數,而token沒有定義
<router-link :to="{name: 'Detail', params: { id: 1, token: '123456' }}">前往Detail頁面</router-link>

// 在詳情頁接收
created () {
    // 以下都可以正常獲取到
    // 但是頁面重新整理後,id依然可以獲取,而token此時就不存在了
    const id = this.$route.params.id;
    const token = this.$route.params.token;
}複製程式碼


本地開發環境請求伺服器介面跨域的問題

Vue 專案裡戳中你痛點的問題及解決辦法(更新)

上面的這個報錯大家都不會陌生,報錯是說沒有訪問許可權(跨域問題)。本地開發專案請求伺服器介面的時候,因為客戶端的同源策略,導致了跨域的問題。

下面先演示一個沒有配置允許本地跨域的的情況:

Vue 專案裡戳中你痛點的問題及解決辦法(更新)

Vue 專案裡戳中你痛點的問題及解決辦法(更新)


Vue 專案裡戳中你痛點的問題及解決辦法(更新)

可以看到,此時我們點選獲取資料,瀏覽器提示我們跨域了。所以我們訪問不到資料。

那麼接下來我們演示設定允許跨域後的資料獲取情況:

Vue 專案裡戳中你痛點的問題及解決辦法(更新)

注意:配置好後一定要關閉原來的server,重新npm run dev啟動專案。不然無效。

Vue 專案裡戳中你痛點的問題及解決辦法(更新)

Vue 專案裡戳中你痛點的問題及解決辦法(更新)

我們在1出設定了允許本地跨域,在2處,要注意我們訪問介面時,寫的是/api,此處的/api指代的就是我們要請求的介面域名。如果我們不想每次介面都帶上/api,可以更改axios的預設配置axios.defaults.baseURL = '/api';這樣,我們請求介面就可以直接this.$axios.get('app.php?m=App&c=Index&a=index'),很簡單有木有。此時如果你在network中檢視xhr請求,你會發現顯示的是localhost:8080/api的請求地址。這樣沒什麼大驚小怪的,代理而已:

Vue 專案裡戳中你痛點的問題及解決辦法(更新)


好了,最後附上proxyTable的程式碼:

proxyTable: {
      // 用‘/api’開頭,代理所有請求到目標伺服器
      '/api': {
        target: 'http://jsonplaceholder.typicode.com', // 介面域名
        changeOrigin: true, // 是否啟用跨域
        pathRewrite: { //
          '^/api': ''
        }
      }
}複製程式碼

注意:配置好後一定要關閉原來的server,重新npm run dev啟動專案。不然無效。


axios封裝和api介面的統一管理

axios的封裝,主要是用來幫我們進行請求的攔截和響應的攔截。

在請求的攔截中我們可以攜帶userToken,post請求頭、qs對post提交資料的序列化等。

在響應的攔截中,我們可以進行根據狀態碼來進行錯誤的統一處理等等。

axios介面的統一管理,是做專案時必須的流程。這樣可以方便我們管理我們的介面,在介面更新時我們不必再返回到我們的業務程式碼中去修改介面。

由於這裡內容稍微多一些,放在另一篇文章,這裡送上鍊接


UI庫的按需載入:

為什麼要使用按需載入的方式而不是一次性全部引入,原因就不多說了。這裡以vant的按需載入為例,演示vue中ui庫怎樣進行按需載入:

  • 安裝: cnpm i vant -S
  • 安裝babel-plugin-import外掛使其按需載入:  cnpm i babel-plugin-import -D
  • 在 .babelrc檔案中中新增外掛配置 :

libraryDirectory { 
    
    "plugins": [ 
        // 這裡是原來的程式碼部分
        // …………

        // 這裡是要我們配置的程式碼
        ["import", 
            { 
                "libraryName": "vant", 
                "libraryDirectory": "es", 
                "style": true 
            }
        ] 
    ] 
}複製程式碼
  • 在main.js中按需載入你需要的外掛:

// 按需引入vant元件
import {   
    DatetimePicker,   
    Button,   
    List 
} from 'vant';複製程式碼
  • 使用元件:

// 使用vant元件
Vue.use(DatetimePicker)  
    .use(Button)  
    .use(List);複製程式碼
  • 最後在在頁面中使用:

<van-button type="primary">按鈕</van-button>複製程式碼

ps:出來vant庫外,像antiUi、elementUi等,很多ui庫都支援按需載入,可以去看文件,上面都會有提到。基本都是通過安裝babel-plugin-import外掛來支援按需載入的,使用方式與vant的如出一轍,可以去用一下。


如何優雅的只在當前頁面中覆蓋ui庫中元件的樣式

首先我們vue檔案的樣式都是寫在<style lang="less" scoped></style>標籤中的,加scoped是為了使得樣式只在當前頁面有效。那麼問題來了,看圖:

Vue 專案裡戳中你痛點的問題及解決辦法(更新)

我們正常寫的所有樣式,都會被加上[data-v-23d425f8]這個屬性(如1所示),但是第三方元件內部的標籤並沒有編譯為附帶[data-v-23d425f8]這個屬性。所以,我們想修改元件的樣式,就沒轍了。怎麼辦呢,有些小夥伴給第三方元件寫個class,然後在一個公共的css檔案中或者在當前頁面再寫一個沒有socped屬性的style標籤,然後直接在裡面修改第三方元件的樣式。這樣不失為一個方法,但是存在全域性汙染和命名衝突的問題。約定特定的命名方式,可以避免命名衝突。但是還是不夠優雅。

作為一名優()秀()的()前()端(),怎麼能允許這種情況出現呢?好了,下面說下優雅的解決方式:

通過深度選擇器解決。例如修改上圖中元件裡的van-ellipsis類的樣式,可以這樣做:

.van-tabs /deep/ .van-ellipsis { color: blue};
複製程式碼

編譯後的結果就是:

Vue 專案裡戳中你痛點的問題及解決辦法(更新)

這樣就不會給van-ellipsis也新增[data-v-23d425f8]屬性了。至此你可以愉快的修改第三方元件的樣式了。

當然了這裡的深度選擇器/deep/是因為我用的less語言,如果你沒有使用less/sass等,可以用>>>符號。

更多的關於深度選擇器的內容,在文章後面有介紹。


定時器問題:

我在a頁面寫一個定時,讓他每秒鐘列印一個1,然後跳轉到b頁面,此時可以看到,定時器依然在執行。這樣是非常消耗效能的。如下圖所示:


Vue 專案裡戳中你痛點的問題及解決辦法(更新)

Vue 專案裡戳中你痛點的問題及解決辦法(更新)

解決方法1:

首先我在data函式裡面進行定義定時器名稱:

data() {            
    return {                              
        timer: null  // 定時器名稱          
    }        
},複製程式碼

然後這樣使用定時器:

this.timer = (() => {
    // 某些操作
}, 1000)複製程式碼

最後在beforeDestroy()生命週期內清除定時器:

beforeDestroy() {
    clearInterval(this.timer);        
    this.timer = null;
}複製程式碼
方案1有兩點不好的地方,引用尤大的話來說就是:
  • 它需要在這個元件例項中儲存這個 timer,如果可以的話最好只有生命週期鉤子可以訪問到它。這並不算嚴重的問題,但是它可以被視為雜物。
  • 我們的建立程式碼獨立於我們的清理程式碼,這使得我們比較難於程式化的清理我們建立的所有東西。

解決方案2:

該方法是通過$once這個事件偵聽器器在定義完定時器之後的位置來清除定時器。以下是完整程式碼:

const timer = setInterval(() =>{                    
    // 某些定時器操作                
}, 500);            
// 通過$once來監聽定時器,在beforeDestroy鉤子可以被清除。
this.$once('hook:beforeDestroy', () => {            
    clearInterval(timer);                                    
})複製程式碼

方案2要感謝@zzx18023在評論區提供出的解決方案。類似於其他需要在當前頁面使用,離開需要銷燬的元件(例如一些第三方庫的picker元件等等),都可以使用此方式來解決離開後以後在背後執行的問題。

綜合來說,我們更推薦使用方案2,使得程式碼可讀性更強,一目瞭然。如果不清楚$once、$on、$off的使用,這裡送上官網的地址教程,在程式化的事件偵聽器那裡


rem檔案的匯入問題:

我們在做手機端時,適配是必須要處理的一個問題。例如,我們處理適配的方案就是通過寫一個rem.js,原理很簡單,就是根據網頁尺寸計算html的font-size大小,基本上小夥伴們都知道,這裡直接附上程式碼,不多做介紹。

;(function(c,d){var e=document.documentElement||document.body,a="orientationchange" in window?"orientationchange":"resize",b=function(){var f=e.clientWidth;e.style.fontSize=(f>=750)?"100px":100*(f/750)+"px"};b();c.addEventListener(a,b,false)})(window);複製程式碼

這裡說下怎麼引入的問題,很簡單。在main.js中,直接import './config/rem'匯入即可。import的路徑根據你的檔案路徑去填寫。


Vue-Awesome-Swiper基本能解決你所有的輪播需求

在我們使用的很多ui庫(vant、antiUi、elementUi等)中,都有輪播元件,對於普通的輪播效果足夠了。但是,某些時候,我們的輪播效果可能比較炫,這時候ui庫中的輪播可能就有些力不從心了。當然,如果技術和時間上都還可以的話,可以自己造個比較炫的輪子。

這裡我說一下vue-awesome-swiper這個輪播元件,真的非常強大,基本可以滿足我們的輪播需求。swiper相信很多人都用過,很好用,也很方便我們二次開發,定製我們需要的輪播效果。vue-awesome-swiper元件實質上基於swiper的,或者說就是能在vue中跑的swiper。下面說下怎麼使用:

  • 安裝 cnpm install vue-awesome-swiper --save
  • 在元件中使用的方法,全域性使用意義不大:
// 引入元件
import 'swiper/dist/css/swiper.css' 
import { swiper, swiperSlide } from 'vue-awesome-swiper'

// 在components中註冊元件
components: {
    swiper,
    swiperSlide
}

// template中使用輪播
// ref是當前輪播
// callback是回撥
// 更多引數用法,請參考文件
<swiper :options="swiperOption" ref="mySwiper" @someSwiperEvent="callback">            
    <!-- slides -->            
    <swiper-slide><div class="item">1</div></swiper-slide>            
    <swiper-slide><div class="item">2</div></swiper-slide>            
    <swiper-slide><div class="item">3</div></swiper-slide>            
          
    <!-- Optional controls -->            
    <div class="swiper-pagination"  slot="pagination"></div>            
    <div class="swiper-button-prev" slot="button-prev"></div>            
    <div class="swiper-button-next" slot="button-next"></div>            
    <div class="swiper-scrollbar"   slot="scrollbar"></div>
</swiper>
複製程式碼

// 引數要寫在data中
data() {            
    return {     
        // swiper輪播的引數           
        swiperOption: { 
            // 滾動條                   
            scrollbar: {                        
                el: '.swiper-scrollbar',                    
            }, 
            // 上一張,下一張                   
            navigation: {                        
                nextEl: '.swiper-button-next',                        
                prevEl: '.swiper-button-prev',                    
            },
            // 其他引數…………   
        }            
    }                    
},複製程式碼

swiper需要配置哪些功能需求,自己根據文件進行增加或者刪減。附上文件:npm文件swiper3.0/4.0文件,更多用法,請參考文件說明。


打包後生成很大的.map檔案的問題

專案打包後,程式碼都是經過壓縮加密的,如果執行時報錯,輸出的錯誤資訊無法準確得知是哪裡的程式碼報錯。 而生成的.map字尾的檔案,就可以像未加密的程式碼一樣,準確的輸出是哪一行哪一列有錯可以通過設定來不生成該類檔案。但是我們在生成環境是不需要.map檔案的,所以可以在打包時不生成這些檔案:

在config/index.js檔案中,設定productionSourceMap: false,就可以不生成.map檔案

Vue 專案裡戳中你痛點的問題及解決辦法(更新)

fastClick的300ms延遲解決方案

開發移動端專案,點選事件會有300ms延遲的問題。至於為什麼會有這個問題,請自行百度即可。這裡只說下常見的解決思路,不管vue專案還是jq專案,都可以使用fastClick解決。

安裝 fastClick:

cnpm install fastclick -S複製程式碼

在main.js中引入fastClick和初始化:

import FastClick from 'fastclick'; // 引入外掛
FastClick.attach(document.body); // 使用 fastclick複製程式碼


元件中寫選項的順序

為什麼選項要有統一的書寫順序呢?很簡單,就是要將選擇和認知成本最小化。

  1. 副作用 (觸發元件外的影響)

    • el
  2. 全域性感知 (要求元件以外的知識)

    • name
    • parent
  3. 元件型別 (更改元件的型別)

    • functional
  4. 模板修改器 (改變模板的編譯方式)

    • delimiters
    • comments
  5. 模板依賴 (模板內使用的資源)

    • components
    • directives
    • filters
  6. 組合 (向選項裡合併屬性)

    • extends
    • mixins
  7. 介面 (元件的介面)

    • inheritAttrs
    • model
    • props/propsData
  8. 本地狀態 (本地的響應式屬性)

    • data
    • computed
  9. 事件 (通過響應式事件觸發的回撥)

    • watch
    • 生命週期鉤子 (按照它們被呼叫的順序)
      • beforeCreate
      • created
      • beforeMount
      • mounted
      • beforeUpdate
      • updated
      • activated
      • deactivated
      • beforeDestroy
      • destroyed
  10. 非響應式的屬性 (不依賴響應系統的例項屬性)

    • methods
  11. 渲染 (元件輸出的宣告式描述)

    • template/render
    • renderError


檢視打包後各檔案的體積,幫你快速定位大檔案

如果你是vue-cli初始化的專案,會預設安裝webpack-bundle-analyzer外掛,該外掛可以幫助我們檢視專案的體積結構對比和專案中用到的所有依賴。也可以直觀看到各個模組體積在整個專案中的佔比。很霸道有木有~~

Vue 專案裡戳中你痛點的問題及解決辦法(更新)

npm run build --report // 直接執行,然後在瀏覽器開啟http://127.0.0.1:8888/即可檢視複製程式碼

記得執行的時候先把之前npm run dev開啟的本地關掉


路由懶載入(也叫延遲載入)

路由懶載入可以幫我們在進入首屏時不用載入過度的資源,從而減少首屏載入速度。

路由檔案中,

非懶載入寫法:

import Index from '@/page/index/index';
export default new Router({  
    routes: [    
        { 
            path: '/', 
            name: 'Index',     
            component: Index 
        }
    ]
})複製程式碼
路由懶載入寫法:

export default new Router({
  routes: [    
        { 
            path: '/', 
            name: 'Index', 
            component: resolve => require(['@/view/index/index'], resolve) 
        }
   ]
})複製程式碼


開啟gzip壓縮程式碼

spa這種單頁應用,首屏由於一次性載入所有資源,所有首屏載入速度很慢。解決這個問題非常有效的手段之一就是前後端開啟gizp(其他還有快取、路由懶載入等等)。gizp其實就是幫我們減少檔案體積,能壓縮到30%左右,即100k的檔案gizp後大約只有30k。

vue-cli初始化的專案中,是預設有此配置的,只需要開啟即可。但是需要先安裝外掛:

// 2.0的版本設定不一樣,本文寫作時為v1版本。v2需配合vue-cli3cnpm i compression-webpack-plugin@1.1.11 複製程式碼

然後在config/index.js中開啟即可:

build: {
    // 其他程式碼
    …………
    productionGzip: true, // false不開啟gizp,true開啟
    // 其他程式碼
}複製程式碼

現在打包的時候,除了會生成之前的檔案,還是生成.gz結束的gzip過後的檔案。具體實現就是如果客戶端支援gzip,那麼後臺後返回gzip後的檔案,如果不支援就返回正常沒有gzip的檔案。

**注意:這裡前端進行的打包時的gzip,但是還需要後臺伺服器的配置。配置是比較簡單的,配置幾行程式碼就可以了,一般這個操作可以叫運維小哥哥小姐姐去搞一下,沒有運維的讓後臺去幫忙配置。


詳情頁返回列表頁快取資料和瀏覽位置、其他頁面進入列表頁重新整理資料的實踐

這樣一個場景:有三個頁面,首頁/或者搜尋頁,商品分類頁面,商品詳情頁。我們希望從首頁進入分類頁面時,分類頁面要重新整理資料,從分類進入詳情頁再返回到分類頁面時,我們不希望重新整理,我們希望此時的分類頁面能夠快取已載入的資料和自動儲存使用者上次瀏覽的位置。之前在百度搜尋的基本都是keep-alive處理的,但是總有那麼一些不完善,所以自己在總結了之後進行了如下的實踐。

解決這種場景需求我們可以通過vue提供的keepAlive屬性。這裡直接送上另一篇處理這個問題的傳送門


CSS的coped私有作用域和深度選擇器

大家都知道當 <style> 標籤有 scoped 屬性時,它的 CSS 只作用於當前元件中的元素。那麼他是怎麼實現的呢,大家看一下編譯前後的程式碼就明白了:

編譯前:

<style scoped>
.example {
  color: red;
}
</style>複製程式碼

編譯後:

<style>
.example[data-v-f3f3eg9] {
  color: red;
}複製程式碼

看完你肯定就會明白了,其實是在你寫的元件的樣式,新增了一個屬性而已,這樣就實現了所謂的私有作用域。但是也會有弊端,考慮到瀏覽器渲染各種 CSS 選擇器的方式,當 p { color: red } 設定了作用域時 (即與特性選擇器組合使用時) 會慢很多倍。如果你使用 class 或者 id 取而代之,比如 .example { color: red },效能影響就會消除。所以,在你的樣式裡,進來避免直接使用標籤,取而代之的你可以給標籤起個class名。


如果你希望 scoped 樣式中的一個選擇器能夠作用得“更深”,例如影響子元件,你可以使用 >>> 操作符:

<style scoped>
    .parent >>> .child { /* ... */ }
</style>複製程式碼

上述程式碼將會編譯成:

.parent[data-v-f3f3eg9] .child { 
    /* ... */ 
}複製程式碼

而對於less或者sass等預編譯,是不支援>>>操作符的,可以使用/deep/來替換>>>操作符,例如:.parent /deep/ .child { /* ... */ }


==================================

後面會繼續更新:


  • axios封裝和api介面的統一管理(已更新,在上面的連結)
  • hiper開啟速度測試
  • vue資料的兩種獲取方式+骨架屏
  • 自定義元件(父子元件)的雙向資料繫結
  • 路由的拆分管理
  • mixins混入簡化常見操作
  • 打包之後檔案、圖片、背景圖資源不存在或者路徑錯誤的問題
  • vue外掛的開發、釋出到github、設定展示地址、釋出npm包

------------華麗麗的分割線-------------------------華麗麗的分割線-------------------------華麗麗的分割線-------------------------華麗麗的分割線-------------------------華麗麗的分割線-------------------------華麗麗的分割線-------------------------華麗麗的分割線-------------

Hiper:一款令人愉悅的效能分析工具

Vue 專案裡戳中你痛點的問題及解決辦法(更新)

如上圖,是hiper工具的測試結果,從中我們可以看到DNS查詢耗時、TCP連線耗時、第一個Byte到達瀏覽器的用時、頁面下載耗時、DOM Ready之後又繼續下載資源的耗時、白屏時間、DOM Ready 耗時、頁面載入總耗時。

在我們的編輯器終端中全域性安裝:

cnpm install hiper -g複製程式碼

使用:終端輸入命令:hiper 測試的網址

# 當我們省略協議頭時,預設會在url前新增`https://`

 # 最簡單的用法
 hiper baidu.com

 # 如何url中含有任何引數,請使用雙引號括起來
 hiper "baidu.com?a=1&b=2"

 #  載入指定頁面100次
 hiper -n 100 "baidu.com?a=1&b=2"

 #  禁用快取載入指定頁面100次
 hiper -n 100 "baidu.com?a=1&b=2" --no-cache

 #  禁JavaScript載入指定頁面100次
 hiper -n 100 "baidu.com?a=1&b=2" --no-javascript
 
 #  使用GUI形式載入指定頁面100次
 hiper -n 100 "baidu.com?a=1&b=2" -H false

 #  使用指定useragent載入網頁100次
 hiper -n 100 "baidu.com?a=1&b=2" -u "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36"複製程式碼

這段用法示例,我直接拷貝的文件說明,具體的可以看下文件,這裡送上鍊接。當我們專案開啟速度慢時,這個工具可以幫助我們快速定位出到底在哪一步影響的頁面載入的速度。

平時我們檢視效能的方式,是在performance和network中看資料,記錄下幾個關鍵的效能指標,然後重新整理幾次再看這些效能指標。有時候我們發現,由於樣本太少,受當前「網路」、「CPU」、「記憶體」的繁忙程度的影響很重,有時優化後的專案反而比優化前更慢。

如果有一個工具,一次性地請求N次網頁,然後把各個效能指標取出來求平均值,我們就能非常準確地知道這個優化是「正優化」還是「負優化」。

hiper就是解決這個痛點的。


vue獲取資料的兩種方式的實踐+簡單骨架屏實現

在vue中獲取資料有兩種方式,引入尤大大的話就是:

  • 導航完成之後獲取:先完成導航,然後在接下來的元件生命週期鉤子中獲取資料。在資料獲取期間顯示“載入中”之類的指示。

  • 導航完成之前獲取:導航完成前,在路由進入的守衛中獲取資料,在資料獲取成功後執行導航。

從技術角度講,兩種方式都不錯 —— 就看你想要的使用者體驗是哪種。那麼我們來實踐一下這兩種獲取資料的方式,以及使用者體驗優化的一點思考。

一、首先是第一種:導航完成之後獲取,這種方式是我們大部分都在使用的,(因為可能一開始我們只知道這種方式^V^)。使用這種方式時,我們會馬上導航和渲染元件,然後在元件的 created 鉤子中獲取資料。這讓我們有機會在資料獲取期間展示一個 loading 狀態,還可以在不同檢視間展示不同的 loading 狀態。獲取資料大家都會,這裡說下使用者體驗的一些東西:

  • 在資料獲取到之前,頁面元件已經載入,但是資料沒有拿到並渲染,所以在此過程中,我們不能載入頁面內展示資料的那塊元件,而是要有一個loading的載入中的元件或者骨架屏。
  • 當頁面資料獲取失敗,可以理解為請求超時的時候,我們要展示的是斷網的元件。
  • 如果是列表頁,還要考慮到空資料的情況,即為空提示的元件。

那麼,我們的頁面是要有這基本的三個部分的,放程式碼:

<template>
    <div class="list">
        <!--載入中或者骨架屏-->
        <div v-if="loading">
       
        </div>

        <!--請求失敗,即斷網的提示元件-->
        <div v-if="error">
      
        </div>

        <!--頁面內容-->
        <div v-if="requestFinished" class="content">
            <!--頁面內容-->
            <div v-if="!isEmpty">
                <!--例如有個列表,當然肯定還會有其他內容-->
                <ul></ul>
            </div>

            <!--為空提示元件-->
            <div v-else>空空如也</div>
        </div>
    </div>
</template>複製程式碼

這種獲取資料的情況下,我們進來預設的是展示loading或者骨架屏的內容,然後如果獲取資料失敗(即請求超時或者斷網),則載入error的那個元件,隱藏其他元件。如果資料請求成功,則載入內容的元件,隱藏其他元件。如果是列表頁,可能在內容元件中還會有列表和為空提示兩塊內容,所以這時候也還要根據獲取的資料來判斷是載入內容還是載入為空提示。

二、第二種方式:導航完成之前獲取

這種方式是在頁面的beforeRouteEnter鉤子中請求資料,只有在資料獲取成功之後才會跳轉導航頁面。

beforeRouteEnter (to, from, next) {        
    api.article.articleDetail(to.query.id).then(res=> {            
        next(vm => {                
            vm.info = res.data;                
            vm.loadFinish = true            
        })        
    })    
},複製程式碼

1. 大家都知道鉤子中beforeRouteEnter鉤子中this還不能使用,所以要想進行賦值操作或者呼叫方法,我們只能通過在next()方法的回撥函式中處理,這個回撥函式的第一個引數就代表了this,他會在元件初始化成功後進行操作。

2. 我想,很多時候我們的api或者axios方法都是掛載到vue的原型上的,由於這裡使用不了this,所以只能在頁面元件內引入api或者我們的axios。

3. 賦值操作也可以寫在method方法中,但是呼叫這個賦值方法還是vm.yourFunction()的方式。

4. 為空提示、斷網處理等都和第一種方式一樣,但是,由於是先獲取到資料之後再跳轉載入元件的,所以我們不需要在預期的頁面內展示骨架屏或者loading元件。可以,我們需要在當前頁面進入之前,即在上一個頁面的時候有一個載入的提示,比如頁面頂部的進度條。這樣使用者體驗就比較友好了,而不至於因為請求的s速度慢一些導致半天沒反應而使用者又不知道的結果。全域性的頁面頂部進度條,可以在main.js中通過router.beforeEach(to, from, next) {}來設定,當頁面路由變化時,顯示頁面頂部的進度條,進入新路由後隱藏掉進度條。

Vue 專案裡戳中你痛點的問題及解決辦法(更新)

關於怎麼新增進度條,因為在另一篇文章已經寫了,這裡直接送上鍊接吧,就不再重複浪費地方了。操作也比較簡單,可自行查閱。

其實說到了這裡,那麼骨架屏的事情也就順帶已經解決了,一般頁面骨架屏也就是一張頁面骨架的圖片,但是要注意這張圖片要儘可能的小。


自定義元件(父子元件)的雙向資料繫結

說到父子元件的通訊,大家一定都不陌生了:父元件通過props向子元件傳值,子元件通過emit觸發父元件自定義事件。但是這裡要說的是父子元件使用v-model實現的通訊。相信大家在使用別人的元件庫的時候,經常是通過v-model來控制一個元件顯示隱藏的效果等,例如彈窗。下面就一步一步解開v-model的神祕面紗。抓~~穩~~嘍~~,老司機彎道要踩油門了~~~

提到v-model首先想到的就是我們對於表單使用者資料的雙向資料繫結,操作起來很簡潔很粗暴,例如:

<input type="text" v-model="msg">

data () {            
    return {                
        msg: ''            
    }        
}複製程式碼

其實v-model是個語法糖,上面這一段程式碼和下面這一段程式碼是一樣的效果:

<input type="text" :value="msg" @input="msg = $event.target.value">
data () {
    return {
        msg: '' 
    }        
},複製程式碼

由此可以看出,v-model="msg"實則是 :value="msg" @input="msg = $event.target.value"的語法糖。這裡其實就是監聽了表單的input事件,然後修改:value對應的值。除了在輸入表單上面可以使用v-model外,在元件上也是可以使用的,這點官網有提到,但是介紹的不是很詳細,導致剛接觸的小夥伴會有一種雲裡霧裡不知所云的感覺。既然瞭解了v-model語法糖本質的用法,那麼我們就可以這樣實現父子元件的雙向資料繫結:

以上原理實現方法,寫法1:

父元件用法:

<empty v-model="msg"></empty>複製程式碼

子元件寫法:

// 點選該按鈕觸發父子元件的資料同步
<div class="share-btn" @click="confirm">確定</div>

// 接收父元件傳遞的value值
// 注意,這種實現方法,這裡只能使用value屬性名
props: {            
    value: {                
        type: Boolean,                
        default: false            
    }        
},
methods: {            
    confirm () {                
        // 雙向資料繫結父元件:value對應的值 
        // 通過$emit觸發父元件input事件,第二個引數為傳遞給父元件的值,這裡傳遞了一個false值 
        // 可以理解為最上面展示的@input="msg = $event.target.value"這個事件
        // 即觸發父元件的input事件,並將傳遞的值‘false’賦值給msg             
        this.$emit('input', false)            
    }        
}複製程式碼

這種方式實現了父子元件見v-model雙向資料繫結的操作,例如你可以試一下實現一個全域性彈窗元件的操作,通過v-model控制彈窗的顯示隱藏,因為你要在頁面內進行某些操作將他顯示出來,控制其隱藏的程式碼是寫在元件裡面的,當元件隱藏了對應的也要父元件對應的值改變。

以上這種方式實現的父子元件的v-model通訊,雖可行,但限制了我們必須popos接收的屬性名為value和emit觸發的必須為input,這樣就容易有衝突,特別是在表單裡面。所以,為了更優雅的使用v-model通訊而解決衝突的問題,我們可以通過在子元件中使用model選項,下面演示寫法2:

父元件寫法:

<empty v-model="msg"></empty>複製程式碼

子元件寫法:

<div class="share-btn" @click="confirm">確定</div>

// model選項用來避免衝突
// prop屬性用來指定props屬性中的哪個值用來接收父元件v-model傳遞的值
// 例如這裡用props中的show來接收父元件傳遞的v-model值
// event:為了方便理解,可以簡單理解為父元件@input的別名,從而避免衝突
// event的值對應了你emit時要提交的事件名,你可以叫aa,也可以叫bb,但是要命名要有意義哦!!!
model: {            
    prop: 'show',            
    event: 'changed'        
},
props: {
    // 由於model選項中的prop屬性指定了,所以show接收的是父元件v-model傳遞的值            
    show: {                
        type: Boolean,                
        default: false            
    }        
},        
methods: {            
    confirm () {                
        // 雙向資料繫結父元件傳遞的值
        // 第一個引數,對應model選項的event的值,你可以叫aa,bbb,ccc,起名隨你 
        this.$emit('changed', false)            
    }        
}複製程式碼

這種實現父子元件見v-model繫結值的方法,在我們開發中其實是很常用的,特別是你要封裝公共元件的時候。

最後,實現雙向資料繫結的方式其實還有.sync,這個屬性一開始是有的,後來由於被認為或破壞單向資料流被刪除了,但最後證明他還是有存在意義的,所以在2.3版本又加回來了。

例如:父元件:

<empty :oneprop.sync="msg"></empty>

data () {
    return {
        msg: ''
    }
}複製程式碼

子元件:

<div class="share-btn" @click="changeMsg">改變msg值</div>

props: {            
    oneprop: {                
        type: String,                
        default: 'hello world'
    }        
},        
methods: {            
    changeMsg () {                
        // 雙向資料流
        this.$emit('update:msg', 'helow world')           
    }        
}        複製程式碼

這樣,便可以在子元件更新父元件的資料。由於v-model只使用一次,所以當需要雙向繫結的值有多個的時候,.sync還是有一定的使用場景的。.sync是下面這種寫法的語法糖,旨在簡化我們的操作:

<empty
    :msg="message"
    @update:msg="message = $event"
></empty>複製程式碼

掌握了元件的v-model寫法,在封裝一些公共元件的時候就又輕鬆一些了吧。

這裡再提一下:

  • vm.$emit(event ,[...args])這個api,其主要作用就是用來觸發當前例項上的事件。附加引數都會傳給監聽器回撥。子元件也屬於當前例項。第一個引數:要觸發的事件名稱。後續的引數可選:即作為引數傳遞給要觸發的事件。文件
  • 監聽當前例項上的自定義事件,事件可以有$emit觸發,也能通過hook監聽到鉤子函式,

vm.$on( event, callback ):一直監聽;文件

vm.$once( event, callback ):監聽一次;文件

vm.$off( [event, callback] ):移除監聽;文件

監聽$emit觸發的自定義事件,上面已經有過用法了,監聽鉤子函式,在上面的定時器那塊也有演示到。監聽鉤子函式的場景使用的不多,但是還是要知道的。

  • vm.$attrs:可以獲取到父元件傳遞的除class和style外的所有自定義屬性。
  • vm.$listeners:可以獲取到父元件傳遞的所有自定義事件

例如:父元件:

<empty
    :msg="message"
    :title="articleTitle"
    @confirm="func1"
    @cancel="func2"
></empty>複製程式碼

就可以在子元件中獲取父元件傳遞的屬性和事件,而不用在props中定義。子元件簡單演示如下:

created() {            
    const msg = this.$attrs.msg; // 獲取父元件傳遞的msg
    this.$listeners.confirm && this.$listeners.confirm(); //若元件傳遞事件confirm則執行
},複製程式碼

這在我們寫一些高階元件時候,會有用到的。


路由拆分管理

這裡說的路由拆分指的是將路由的檔案,按照模組拆分,這樣方便路由的管理,更主要的是方便多人開發。具體要不要拆分,那就要視你的專案情況來定了,如果專案較小的話,也就一二十個路由,那麼是拆分是非常沒必要的。但倘若你開發一些功能點較多的商城專案,路由可以會有一百甚至幾百個,那麼此時將路由檔案進行拆分是很有必要的。不然,你看著index.js檔案中一大長串串串串串串的路由,也是很糟糕的。

Vue 專案裡戳中你痛點的問題及解決辦法(更新)

首先我們在router資料夾中建立一個index.js作為路由的入口檔案,然後新建一個modules資料夾,裡面存放各個模組的路由檔案。例如這裡儲存了一個vote.js投票模組的路由檔案和一個公共模組的路由檔案。下面直接上index.js吧,而後在簡單介紹:

import Vue from 'vue'
import Router from 'vue-router'

// 公共頁面的路由檔案
import PUBLIC from './modules/public' 
// 投票模組的路由檔案
import VOTE from './modules/vote' 

Vue.use(Router)

// 定義路由
const router = new Router({  
    mode: 'history',  
    routes: [    
        ...PUBLIC,    
        ...VOTE,  
    ]
})

// 路由變化時
router.beforeEach((to, from, next) => {    
    if (document.title !== to.meta.title) {        
        document.title = to.meta.title;    
    }    
    next()
})

// 匯出
export default router複製程式碼

首先引入vue和router最後匯出,這就不多說了,基本的操作。

這裡把router.beforeEach的操作寫了router的index.js檔案中,有些人可能會寫在main.js中,這也沒有錯,只不過,個人而言,既然是路由的操作,還是放在路由檔案中管理更好些。這裡就順便演示了,如何在頁面切換時,自動修改頁面標題的操作。

而後引入你根據路由模組劃分的各個js檔案,然後在例項化路由的時候,在routes陣列中,將匯入的各個檔案通過結構賦值的方法取出來。最終的結果和正常的寫法是一樣的。

然後看下我們匯入的vote.js吧:

/** 
 * 投票模組的router列表  
 */

export default [    
    // 投票模組首頁    
    {        
        path: '/vote/index',        
        name: 'VoteIndex',        
        component: resolve => require(['@/view/vote/index'], resolve),        
        meta: {            
            title: '投票'        
        }    
    },    
    // 詳情頁    {        
    path: '/vote/detail',        
    name: 'VoteDetail',        
    component: resolve => require(['@/view/vote/detail'], resolve),
    meta: {            
        title: '投票詳情'        
    }    
}] 複製程式碼

這裡就是將投票模組的路由放在一個陣列中匯出去。整個路由拆分的操作,不是vue的知識,就是一個es6匯入匯出和結構的語法。具體要不要拆分,還是因專案和環境而異吧。

這裡的路由用到了懶載入路由的方式,如果不清楚,文字上面有介紹到。

還有這裡的meta元欄位中,定義了一個title資訊,用來儲存當前頁面的頁面標題,即document.title。

mixins混入簡化常見操作

我們在開發中經常會遇到金錢保留兩位小數,時間戳轉換等操作。每次我們會寫成一個公共函式,然後在頁面裡面的filters進行過濾。這種方法每次,但是感覺每次需要用到,都要寫一遍在filters,也是比較煩呢!!!但是,我們猿類的極致追究就是懶呀,那這怎麼能行~~~

兄弟們,抄傢伙!上mixins!!!

import { u_fixed } from './tool'

const mixins = {    
    filters: {        
        // 保留兩位小數        
        mixin_fixed2 (val) {            
            return u_fixed(val)        
        },
        // 數字轉漢字,16000 => 1.60萬        
        mixin_num2chinese (val) {            
            return val > 9999 ? u_fixed(val/10000) + '萬' : val;        
    }    
}}
export default mixins複製程式碼

新建一個mixins.js,把我們需要混入的內容都寫在裡面,例如這裡混入了filters,把常用的幾個操作寫在了裡面,大家可以自行擴充套件。

這樣的話,在我們需要的頁面import這個js,然後宣告一下混入就好,而後就可以像正常的方式去使用就好了。

Vue 專案裡戳中你痛點的問題及解決辦法(更新)

例如,我現在可以直接在頁面內使用我們的過濾操作{{1000 | mixin_fixed2}}


打包之後檔案、圖片、背景圖資源不存在或者路徑錯誤的問題

Vue 專案裡戳中你痛點的問題及解決辦法(更新)

先看下專案的config資料夾下的index.js檔案,這個配置選項就好使我們打包後的資源公共路徑,預設的值為‘/’,即根路徑,所以打包後的資源路徑為根目錄下的static。由此問題來了,如果你打包後的資源沒有放在伺服器的根目錄,而是在根目錄下的mobile等資料夾的話,那麼打包後的路徑和你程式碼中的路徑就會有衝突了,導致資源找不到。

所以,為了解決這個問題,你可以在打包的時候把上面這個路徑由‘/’的根目錄,改為‘./’的相對路徑。

Vue 專案裡戳中你痛點的問題及解決辦法(更新)

這樣的的話,打包後的圖片啊js等路徑就是‘./static/img/asc.jpg’這樣的相對路徑,這就不管你放在哪裡,都不會有錯了。但是,凡是都有但是~~~~~這裡一切正常,但是背景圖的路徑還是不對。因為此時的相對就變成了static/css/資料夾下的static/img/xx.jpg,但是實際上static/css/資料夾下沒有static/img/xx.jpg,即static/css/static/img/xx.jpg是不存在的。此時相對於的當前的css檔案的路徑。所以為了解決這個問題,要把我們css中的背景圖的加個公共路徑‘../../’,即讓他往上返回兩級到和index.html檔案同級的位置,那麼此時的相對路徑static/img/xx.jpg就能找到對應的資源了。那麼怎麼修改背景圖的這個公共路徑呢,因為背景圖是通過loader解析的,所以自然在loader的配置中修改,開啟build資料夾下的utils檔案,找到exports.cssLoaders的函式,在函式中找到對應下面這些配置:Vue 專案裡戳中你痛點的問題及解決辦法(更新)

找到這個位置,新增一上配置,就是上圖紅框內的程式碼,就可以把它的公共路徑修改為往上返回兩級。這樣再打包看下,就ok了!

最後再鄭重說一點,如果你的路由模式是history的,那麼打包放在伺服器,必須要後臺伺服器的配合,具體的可以看官方文件,這點很重要。不然你會發現白屏啊等各種莫名其妙的問題。牢記!!!


vue外掛的開發、釋出到github、設定展示地址、釋出npm包

對於平時我們常用的一些元件,我們可以把它封裝成外掛,然後釋出到github上,最後再發布成npm包,這樣以後便可以直接從npm安裝外掛到我們的專案中,省去了我們拷貝的過程了,還能給別人分享呢!

由於外掛的這一塊內容比較多,我暫且放在另外一篇文章吧,這裡呢就附上鍊接吧


相關文章