Vue 項目優化問題及方案


總結如下幾點vue項目開發中常見的問題及解決辦法。

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

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

例如商品列表頁面前往商品詳情頁面,需要傳一個商品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;
}

本地開發環境請求服務器接口跨域的問題

 

 

 

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

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

 

 

 

 

 

 

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

那么接下來我們演示設置允許跨域后的數據獲取情況:

 

 

 

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

 

 

 

我們在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的請求地址。這樣沒什么大驚小怪的,代理而已:

 

 

 

好了,最后附上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是為了使得樣式只在當前頁面有效。那么問題來了,看圖:

 

 

 

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

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

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

.van-tabs /deep/ .van-ellipsis { color: blue};

編譯后的結果就是:

 

 

 

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

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

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

 

 

定時器問題:

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

 

 

 

解決方法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的路徑根據你的文件路徑去填寫。

 

在我們使用的很多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文件

 

 

 

 

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`
  1. 渲染 (組件輸出的聲明式描述)
- `template`/`render`
- `renderError`





查看打包后各文件的體積,幫你快速定位大文件

如果你是vue-cli初始化的項目,會默認安裝webpack-bundle-analyzer插件,該插件可以幫助我們查看項目的體積結構對比和項目中用到的所有依賴。也可以直觀看到各個模塊體積在整個項目中的占比。很霸道有木有~~

 

 

 npm run build --report // 直接運行,然后在瀏覽器打開http://127.0.0.1:8888/即可查看

 

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

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

路由文件中,

非懶加載寫法:

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 { /* ... */ }

 

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

 

 

如上圖,是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) {}來設置,當頁面路由變化時,顯示頁面頂部的進度條,進入新路由后隱藏掉進度條。

 

 

 

關於怎么添加進度條,因為在另一篇文章已經寫了,這里直接送上鏈接吧,就不再重復浪費地方了。操作也比較簡單,可自行查閱。

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

 

 

自定義組件(父子組件)的雙向數據綁定

說到父子組件的通信,大家一定都不陌生了:父組件通過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文件中一大長串串串串串串的路由,也是很糟糕的。

 

 

首先我們在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,然后聲明一下混入就好,而后就可以像正常的方式去使用就好了。

 

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

 

 

打包之后文件、圖片、背景圖資源不存在或者路徑錯誤的問題

 

 

先看下項目的config文件夾下的index.js文件,這個配置選項就好使我們打包后的資源公共路徑,默認的值為‘/’,即根路徑,所以打包后的資源路徑為根目錄下的static。由此問題來了,如果你打包后的資源沒有放在服務器的根目錄,而是在根目錄下的mobile等文件夾的話,那么打包后的路徑和你代碼中的路徑就會有沖突了,導致資源找不到。

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

 

 這樣的的話,打包后的圖片啊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的函數,在函數中找到對應下面這些配置:

 

 

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

 

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM