「Vue實踐」武裝你的前端項目

本文目錄

本文項目基於Vue-Cli3,想知道如何正確搭建請看我以前的文章:javascript

「Vue實踐」項目升級vue-cli3的正確姿式css

1. 接口模塊處理

1.1 axios二次封裝

這裏封裝的依據是後臺傳的JWT,已封裝好的請跳過。html

import axios from 'axios'
import router from '../router'
import {MessageBox, Message} from 'element-ui'

let loginUrl = '/login'
// 根據環境切換接口地址
axios.defaults.baseURL = process.env.VUE_APP_API
axios.defaults.headers = {'X-Requested-With': 'XMLHttpRequest'}
axios.defaults.timeout = 60000

// 請求攔截器
axios.interceptors.request.use(
  config => {
    if (router.history.current.path !== loginUrl) {
      let token = window.sessionStorage.getItem('token')
      if (token == null) {
        router.replace({path: loginUrl, query: {redirect: router.currentRoute.fullPath}})
        return false
      } else {
        config.headers['Authorization'] = 'JWT ' + token
      }
    }
    return config
  }, error => {
    Message.warning(error)
    return Promise.reject(error)
  })
複製代碼

緊接着的是響應攔截器(即異常處理)前端

axios.interceptors.response.use(
  response => {
    return response.data
  }, error => {
    if (error.response !== undefined) {
      switch (error.response.status) {
        case 400:
          MessageBox.alert(error.response.data)
          break
        case 401:
          if (window.sessionStorage.getItem('out') === null) {
            window.sessionStorage.setItem('out', 1)
            MessageBox.confirm('會話已失效! 請從新登陸', '提示', {confirmButtonText: '從新登陸', cancelButtonText: '取消', type: 'warning'}).then(() => {
              router.replace({path: loginUrl, query: {redirect: router.currentRoute.fullPath}})
            }).catch(action => {
              window.sessionStorage.clear()
              window.localStorage.clear()
            })
          }
          break
        case 402:
          MessageBox.confirm('登錄超時 !', '提示', {confirmButtonText: '從新登陸', cancelButtonText: '取消', type: 'warning'}).then(() => {
            router.replace({path: loginUrl, query: {redirect: router.currentRoute.fullPath}})
          })
          break
        case 403:
          MessageBox.alert('沒有權限!')
          break
        // ...忽略
        default:
          MessageBox.alert(`鏈接錯誤${error.response.status}`)
    }
    return Promise.resolve(error.response)
  }
  return Promise.resolve(error)
})
複製代碼

這裏作的處理分別是會話已失效和登錄超時,具體的須要根據業務來做變動。vue

最後是導出基礎請求類型封裝。java

export default {
  get (url, param) {
    if (param !== undefined) {
      Object.assign(param, {_t: (new Date()).getTime()})
    } else {
      param = {_t: (new Date()).getTime()}
    }
    return axios({method: 'get', url, params: param})
  },
  // 不常更新的數據用這個
  getData (url, param) {
    return axios({method: 'get', url, params: param})
  },
  post (url, param, config) {
    return axios.post(url, param, config)
  },
  put: axios.put,
  _delete: axios.delete
}
複製代碼

其中給get請求加上時間戳參數,避免從緩存中拿數據。 除了基礎請求類型,還有不少相似下載、上傳這種,須要特殊的的請求頭,此時能夠根據自身需求進行封裝。node

瀏覽器緩存是基於url進行緩存的,若是頁面容許緩存,則在必定時間內(緩存時效時間前)再次訪問相同的URL,瀏覽器就不會再次發送請求到服務器端,而是直接從緩存中獲取指定資源。webpack

1.2 請求按模塊合併

模塊的請求:

import http from '@/utils/request'
export default {
  A (param) { return http.get('/api/', param) },
  B (param) { return http.post('/api/', param) }
  C (param) { return http.put('/api/', param) },
  D (param) { return http._delete('/api/', {data: param}) },
}
複製代碼

utils/api/index.js:ios

import http from '@/utils/request'
import account from './account'
// 忽略...
const api = Object.assign({}, http, account, \*...其它模塊*\)
export default api
複製代碼

1.3 global.js中的處理

global.js中引入:nginx

import Vue from 'vue'
import api from './api/index'
// 略...

const errorHandler = (error, vm) => {
  console.error(vm)
  console.error(error)
}

Vue.config.errorHandler = errorHandler
export default {
  install (Vue) {
    // 添加組件
    // 添加過濾器
    })
    // 全局報錯處理
    Vue.prototype.$throw = (error) => errorHandler(error, this)
    Vue.prototype.$http = api
    // 其它配置
  }
}
複製代碼

寫接口的時候就能夠簡化爲:

async getData () {
    const params = {/*...key : value...*/}
    let res = await this.$http.A(params)
    res.code === 4000 ? (this.aSata = res.data) : this.$message.warning(res.msg)
}
複製代碼

2. 基礎組件自動化全局註冊

來自 @SHERlocked93:Vue 使用中的小技巧

官方文檔:基礎組件的自動化全局註冊

咱們寫組件的時候一般須要引入另外的組件:

<template>
    <BaseInput  v-model="searchText"  @keydown.enter="search"/>
    <BaseButton @click="search">
        <BaseIcon name="search"/>
    </BaseButton>
</template>
<script>
    import BaseButton from './baseButton'
    import BaseIcon from './baseIcon'
    import BaseInput from './baseInput'
    export default {
      components: { BaseButton, BaseIcon, BaseInput }
    }
</script>
複製代碼

寫小項目這麼引入還好,但等項目一臃腫起來...嘖嘖。 這裏是藉助webpack,使用 require.context() 方法來建立本身的模塊上下文,從而實現自動動態require組件。

這個方法須要3個參數:

  • 要搜索的文件夾目錄
  • 是否還應該搜索它的子目錄
  • 一個匹配文件的正則表達式。

在你放基礎組件的文件夾根目錄下新建componentRegister.js:

import Vue from 'vue'
/**
 * 首字母大寫
 * @param str 字符串
 * @example heheHaha
 * @return {string} HeheHaha
 */
function capitalizeFirstLetter (str) {
  return str.charAt(0).toUpperCase() + str.slice(1)
}
/**
 * 對符合'xx/xx.vue'組件格式的組件取組件名
 * @param str fileName
 * @example abc/bcd/def/basicTable.vue
 * @return {string} BasicTable
 */
function validateFileName (str) {
  return /^\S+\.vue$/.test(str) &&
    str.replace(/^\S+\/(\w+)\.vue$/, (rs, $1) => capitalizeFirstLetter($1))
}
const requireComponent = require.context('./', true, /\.vue$/)
// 找到組件文件夾下以.vue命名的文件,若是文件名爲index,那麼取組件中的name做爲註冊的組件名
requireComponent.keys().forEach(filePath => {
  const componentConfig = requireComponent(filePath)
  const fileName = validateFileName(filePath)
  const componentName = fileName.toLowerCase() === 'index'
    ? capitalizeFirstLetter(componentConfig.default.name)
    : fileName
  Vue.component(componentName, componentConfig.default || componentConfig)
})
複製代碼

最後咱們在main.js

import 'components/componentRegister.js'

咱們就能夠隨時隨地使用這些基礎組件,無需手動引入了。

3. 頁面性能調試:Hiper

咱們寫單頁面應用,想看頁面修改後性能變動其實挺繁瑣的。有時想知道是「正優化」仍是「負優化」只能靠手動刷新查看network。而Hiper很好解決了這一痛點(其實Hiper是後臺靜默運行Chromium來實現無感調試)。

Hiper官方文檔

咱們開發完一個項目或者給一個項目作完性能優化之後,如何來衡量這個項目的性能是否達標?

咱們的常見方式是在Dev Tool中的performancenetwork中看數據,記錄下幾個關鍵的性能指標,而後刷新幾回再看這些性能指標。

有時候咱們發現,因爲樣本太少,受當前「網絡」、「CPU」、「內存」的繁忙程度的影響很重,有時優化後的項目反而比優化前更慢。

若是有一個工具,一次性地請求N次網頁,而後把各個性能指標取出來求平均值,咱們就能很是準確地知道這個優化是「正優化」仍是「負優化」。

而且,也能夠作對比,拿到「具體優化了多少」的準確數據。這個工具就是爲了解決這個痛點的。

全局安裝

sudo npm install hiper -g
# 或者使用 yarn:
# sudo yarn global add hiper
複製代碼

性能指標

Key Value
DNS查詢耗時 domainLookupEnd - domainLookupStart
TCP鏈接耗時 connectEnd - connectStart
第一個Byte到達瀏覽器的用時 responseStart - requestStart
頁面下載耗時 responseEnd - responseStart
DOM Ready以後又繼續下載資源的耗時 domComplete - domInteractive
白屏時間 domInteractive - navigationStart
DOM Ready 耗時 domContentLoadedEventEnd - navigationStart
頁面加載總耗時 loadEventEnd - navigationStart

developer.mozilla.org/zh-CN/docs/…

用例配置

# 當咱們省略協議頭時,默認會在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"
複製代碼

此外,還能夠配置Cookie訪問

module.exports = {
    ....
    cookies:  [{
        name: 'token',
        value: process.env.authtoken,
        domain: 'example.com',
        path: '/',
        httpOnly: true
    }],
    ....
}
複製代碼
# 載入上述配置文件(假設配置文件在/home/下)
hiper -c /home/config.json

# 或者你也可使用js文件做爲配置文件
hiper -c /home/config.js
複製代碼

4. Vue高階組件封裝

咱們經常使用的<transition><keep-alive>就是一個高階(抽象)組件。

export default {
  name: 'keep-alive',
  abstract: true,
  ...
}
複製代碼

全部的高階(抽象)組件是經過定義abstract選項來聲明的。高階(抽象)組件不渲染真實DOM。 一個常規的抽象組件是這麼寫的:

import { xxx } from 'xxx'
const A = () => {
    .....
}

export default {
    name: 'xxx',
    abstract: true,
    props: ['...', '...'],
    // 生命週期鉤子函數
    created () {
      ....
    },
    ....
    destroyed () {
      ....
    },
    render() {
        const vnode = this.$slots.default
        ....
        return vnode
    },
})
複製代碼

4.1 防抖/節流 抽象組件

關於防抖和節流是啥就不贅述了。這裏貼出組件代碼:

改編自:Vue實現函數防抖組件

const throttle = function(fn, wait=50, isDebounce, ctx) {
  let timer
  let lastCall = 0
  return function (...params) {
    if (isDebounce) {
      if (timer) clearTimeout(timer)
      timer = setTimeout(() => {
        fn.apply(ctx, params)
      }, wait)
    } else {
      const now = new Date().getTime()
      if (now - lastCall < wait) return
      lastCall = now
      fn.apply(ctx, params)
    }
  }
}

export default {
    name: 'Throttle',
    abstract: true,
    props: {
      time: Number,
      events: String,
      isDebounce: {
        type: Boolean,
        default: false
      },
    },
    created () {
      this.eventKeys = this.events.split(',')
      this.originMap = {}
      this.throttledMap = {}
    },
    render() {
        const vnode = this.$slots.default[0]
        this.eventKeys.forEach((key) => {
            const target = vnode.data.on[key]
            if (target === this.originMap[key] && this.throttledMap[key]) {
                vnode.data.on[key] = this.throttledMap[key]
            } else if (target) {
                this.originMap[key] = target
                this.throttledMap[key] = throttle(target, this.time, this.isDebounce, vnode)
                vnode.data.on[key] = this.throttledMap[key]
            }
        })
        return vnode
    },
})
複製代碼

經過第三個參數isDebounce來控制切換防抖節流。 最後在main.js裏引用:

import Throttle from '../Throttle'
....
Vue.component('Throttle', Throttle)
複製代碼

使用方式

<div id="app">
    <Throttle :time="1000" events="click">
        <button @click="onClick($event, 1)">click+1 {{val}}</button>
    </Throttle>
    <Throttle :time="1000" events="click" :isDebounce="true">
        <button @click="onAdd">click+3 {{val}}</button>
    </Throttle>
    <Throttle :time="3300" events="mouseleave" :isDebounce="true">
        <button @mouseleave.prevent="onAdd">click+3 {{val}}</button>
    </Throttle>
</div>
複製代碼
const app = new Vue({
    el: '#app',
    data () {
        return {
            val: 0
        }
    },
    methods: {
        onClick ($ev, val) {
            this.val += val
        },
        onAdd () {
            this.val += 3
        }
    }
})
複製代碼

抽象組件是一個接替Mixin實現抽象組件公共功能的好方法,不會由於組件的使用而污染DOM(添加並不想要的div標籤等)、能夠包裹任意的單一子元素等等

至於用不用抽象組件,就見仁見智了。

5. 性能優化:eventBus封裝

中央事件總線eventBus的實質就是建立一個vue實例,經過一個空的vue實例做爲橋樑實現vue組件間的通訊。它是實現非父子組件通訊的一種解決方案。

eventBus實現也很是簡單

import Vue from 'Vue'
export default new Vue
複製代碼

咱們在使用中常常最容易忽視,又必然不能忘記的東西,那就是:清除事件總線eventBus

不手動清除,它是一直會存在,這樣當前執行時,會反覆進入到接受數據的組件內操做獲取數據,本來只執行一次的獲取的操做將會有屢次操做。原本只會觸發並只執行一次,變成了屢次,這個問題就很是嚴重。

當不斷進行操做幾分鐘後,頁面就會卡頓,並佔用大量內存。

因此通常在vue生命週期beforeDestroy或者destroyed中,須要用vue實例的$off方法清除eventBus

beforeDestroy(){
    bus.$off('click')
 }
複製代碼

可當你有多個eventBus時,就須要重複性勞動$off銷燬這件事兒。 這時候封裝一個 eventBus就是更優的解決方案。

5.1 擁有生命週期的 eventBus

咱們從Vue源碼Vue.init中能夠得知:

Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid vm實例惟一標識
    vm._uid = uid++
    // ....
    }
複製代碼

每一個Vue實例有本身的_uid做爲惟一標識,所以咱們讓EventBus_uid關聯起來,並將其改造:

實現來自:讓在Vue中使用的EventBus也有生命週期

class EventBus {
  constructor (vue) {
    if (!this.handles) {
      Object.defineProperty(this, 'handles', {
        value: {},
        enumerable: false
      })
    }
    this.Vue = vue
    // _uid和EventName的映射
    this.eventMapUid = {}
  }
  setEventMapUid (uid, eventName) {
    if (!this.eventMapUid[uid]) this.eventMapUid[uid] = []
    this.eventMapUid[uid].push(eventName) // 把每一個_uid訂閱的事件名字push到各自uid所屬的數組裏
  }
  $on (eventName, callback, vm) {
    // vm是在組件內部使用時組件當前的this用於取_uid
    if (!this.handles[eventName]) this.handles[eventName] = []
    this.handles[eventName].push(callback)
    if (vm instanceof this.Vue) this.setEventMapUid(vm._uid, eventName)
  }
  $emit () {
    let args = [...arguments]
    let eventName = args[0]
    let params = args.slice(1)
    if (this.handles[eventName]) {
      let len = this.handles[eventName].length
      for (let i = 0; i < len; i++) {
        this.handles[eventName][i](...params)
      }
    }
  }
  $offVmEvent (uid) {
    let currentEvents = this.eventMapUid[uid] || []
    currentEvents.forEach(event => {
      this.$off(event)
    })
  }
  $off (eventName) {
    delete this.handles[eventName]
  }
}
// 寫成Vue插件形式,直接引入而後Vue.use($EventBus)進行使用
let $EventBus = {}

$EventBus.install = (Vue, option) => {
  Vue.prototype.$eventBus = new EventBus(Vue)
  Vue.mixin({
    beforeDestroy () {
      // 攔截beforeDestroy鉤子自動銷燬自身全部訂閱的事件
      this.$eventBus.$offVmEvent(this._uid) 
    }
  })
}

export default $EventBus
複製代碼

使用:

// main.js中
...
import EventBus from './eventBus.js'
Vue.use(EnemtBus)
...
複製代碼

組件中使用:

created () {
    let text = Array(1000000).fill('xxx').join(',')
    this.$eventBus.$on('home-on', (...args) => {
      console.log('home $on====>>>', ...args)
      this.text = text
    }, this) // 注意第三個參數須要傳當前組件的this,若是不傳則須要手動銷燬
  },
  mounted () {
    setTimeout(() => {
      this.$eventBus.$emit('home-on', '這是home $emit參數', 'ee')
    }, 1000)
  },
  beforeDestroy () {
    // 這裏就不須要手動的off銷燬eventBus訂閱的事件了
  }
複製代碼

6. webpack插件:真香

6.1 取代uglifyjsTerser Plugin

在二月初項目升級Vue-cli3時遇到了一個問題:uglifyjs再也不支持webpack4.0。找了一圈,在Google搜索裏查到Terser Plugin這個插件。

我主要用到了其中這幾個功能:

  • cache,啓用文件緩存。
  • parallel,使用多進程並行來提升構建速度。
  • sourceMap,將錯誤消息位置映射到模塊(儲存着位置信息)。
  • drop_console,打包時剔除全部的console語句
  • drop_debugger,打包時剔除全部的debugger語句

做爲一個管小組前端的懶B,不少時候寫頁面會遺留console.log,影響性能。設置個drop_console就很是香。如下配置親測有效。

const TerserPlugin = require('terser-webpack-plugin')
....
new TerserPlugin({
cache: true,
parallel: true,
sourceMap: true, // Must be set to true if using source-maps in production
terserOptions: {
  compress: {
    drop_console: true,
    drop_debugger: true
  }
}
})
複製代碼

更多的配置請看Terser Plugin

6.2 雙端開啓 gzip

開啓gzip壓縮的好處是什麼?

能夠減少文件體積,傳輸速度更快。gzip是節省帶寬和加快站點速度的有效方法。

  • 服務端發送數據時能夠配置 Content-Encoding:gzip,用戶說明數據的壓縮方式
  • 客戶端接受到數據後去檢查對應字段的信息,就能夠根據相應的格式去解碼。
  • 客戶端請求時,能夠用 Accept-Encoding:gzip,用戶說明接受哪些壓縮方法。

6.2.1 Webpack開啓gzip

這裏使用的插件爲:CompressionWebpackPlugin

const CompressionWebpackPlugin = require('compression-webpack-plugin')
module.exports = { 
    「plugins」:[new CompressionWebpackPlugin] 
}
複製代碼

具體配置:

const CompressionWebpackPlugin = require('compression-webpack-plugin');

webpackConfig.plugins.push(
    new CompressionWebpackPlugin({
      asset: '[path].gz[query]',
      algorithm: 'gzip',
      test: new RegExp('\\.(js|css)$'),
      // 只處理大於xx字節 的文件,默認:0
      threshold: 10240,
      // 示例:一個1024b大小的文件,壓縮後大小爲768b,minRatio : 0.75
      minRatio: 0.8 // 默認: 0.8
      // 是否刪除源文件,默認: false
      deleteOriginalAssets: false
    })
)
複製代碼

開啓gzip前:

開啓gzip前

開啓gzip後
gzip後的大小從277KB到只有~91.2KB!

6.2.2 擴展知識:Nginxgzip設置

打開/etc/nginx/conf.d編寫如下配置。

server {
    gzip on;
    gzip_static on;    
    gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
    gzip_proxied  any;
    gzip_vary on;
    gzip_comp_level 6;
    gzip_buffers 16 8k;
    gzip_http_version 1.1;    
    ...
}
複製代碼

Nginx嘗試查找併發送文件/path/to/bundle.js.gz。若是該文件不存在,或者客戶端不支持 gzip,Nginx則會發送該文件的未壓縮版本。

保存配置後,從新啓動Nginx:

$ sudo service nginx restart
複製代碼

開啓gzip前

開啓gzip後

6.2.3 如何驗證gzip

經過使用curl測試每一個資源的請求響應,並檢查Content-Encoding

顯示 Content-Encoding: gzip,即爲配置成功

6.2.4 雙端Gzip區別及其意義

不一樣之處在於:

  1. Webpack壓縮會在構建運行期間一次壓縮文件,而後將這些壓縮版本保存到磁盤。

  2. nginx在請求時壓縮文件時,某些包可能內置了緩存,所以性能損失只發生一次(或不常常),但一般不一樣之處在於,這將在響應 HTTP請求時發生。

  3. 對於實時壓縮,讓上游代理(例如 Nginx)處理 gzip和緩存一般更高效,由於它們是專門爲此而構建的,而且不會遭受服務端程序運行時的開銷(許多都是用C語言編寫的) 。

  4. 使用 Webpack的好處是, Nginx每次請求服務端都要壓縮好久纔回返回信息回來,不只服務器開銷會增大不少,請求方也會等的不耐煩。咱們在 Webpack打包時就直接生成高壓縮等級的文件,做爲靜態資源放在服務器上,這時將 Nginx做爲二重保障就會高效不少(請求其它目錄資源時)。

  5. 注:具體是在請求時實時壓縮,或在構建時去生成壓縮文件,就要看項目業務狀況。

求一份深圳的內推

原本還想謝謝動態配置表單相關,但篇幅太長也太難寫了。

好了,又水完一篇,入正題:

目前本人在(又)準備跳槽,但願各位大佬和HR小姐姐能夠內推一份靠譜的深圳前端崗位!996.ICU 就算了。

  • 微信:huab119
  • 郵箱:454274033@qq.com

做者掘金文章總集

公衆號

相關文章
相關標籤/搜索