本文專案基於Vue-Cli3,想知道如何正確搭建請看我之前的文章:
1. 介面模組處理
1.1 axios
二次封裝
很基礎的部分,已封裝好的請跳過。這裡的封裝是依據JWT
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)
})
複製程式碼
這裡做的處理分別是會話已失效和登陸超時,具體的需要根據業務來作變更。
最後是匯出基礎請求型別封裝。
export default {
get (url, param) {
if (param !== undefined) {
Object.assign(param, {_t: (new Date()).getTime()})
} else {
param = {_t: (new Date()).getTime()}
}
return new Promise((resolve, reject) => {
axios({method: 'get', url, params: param}).then(res => { resolve(res) })
})
},
getData (url, param) {
return new Promise((resolve, reject) => {
axios({method: 'get', url, params: param}).then(res => {
if (res.code === 4000) {
resolve(res.data)
} else {
Message.warning(res.msg)
}
})
})
},
post (url, param, config) {
return new Promise((resolve, reject) => {
axios.post(url, param, config).then(res => { resolve(res) })
})
},
put: axios.put,
_delete: axios.delete
}
複製程式碼
其中給get
請求加上時間戳引數,避免從快取中拿資料。
瀏覽器快取是基於url進行快取的,如果頁面允許快取,則在一定時間內(快取時效時間前)再次訪問相同的URL,瀏覽器就不會再次傳送請求到伺服器端,而是直接從快取中獲取指定資源。
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
:
import http from '@/utils/request'
import account from './account'
// 忽略...
const api = Object.assign({}, http, account, \*...其它模組*\)
export default api
複製程式碼
1.3 global.js中的處理
在global.js
中引入:
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.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
來實現無感除錯)。
我們開發完一個專案或者給一個專案做完效能優化以後,如何來衡量這個專案的效能是否達標?
我們的常見方式是在Dev Tool
中的performance
和network
中看資料,記錄下幾個關鍵的效能指標,然後重新整理幾次再看這些效能指標。
有時候我們發現,由於樣本太少,受當前「網路」、「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.throttleMap[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.init中可以得知:
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid vm例項唯一標識
vm._uid = uid++
// ....
}
複製程式碼
每個Vue例項有自己的_uid
作為唯一標識,因此我們讓EventBus
和_uid`關聯起來,並將其改造:
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訂閱的事件了
}
複製程式碼
求一份深圳的內推
本來還想謝謝動態配置表單或者webapck相關,但篇幅太長也太難寫了。
好了,又水完一篇,入正題:
目前本人在(又)準備跳槽,希望各位大佬和HR小姐姐可以內推一份靠譜的深圳前端崗位!996.ICU
就算了。
- 微信:
huab119
- 郵箱:
454274033@qq.com
作者掘金文章總集
- 「從原始碼中學習」面試官都不知道的Vue題目答案
- 「從原始碼中學習」Vue原始碼中的JS騷操作
- 「從原始碼中學習」徹底理解Vue選項Props
- 「Vue實踐」專案升級vue-cli3的正確姿勢
- 為何你始終理解不了JavaScript作用域鏈?