package.json 封装 vue模块_Vue实战武装你的项目

本文项目基于Vue-Cli3,想知道如何正确搭建请看我之前的文章:

「Vue实践」项目升级vue-cli3的正确姿势

9bf3d0e43d2712bd2659696544c0595f.png

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 请求按模块合并

1f5fe9ee4ded0d709a06453949538975.png模块的请求:

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基础组件全局注册

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

我们写组件的时候通常需要引入另外的组件:

v-model="searchText" @keydown.enter="search"/>

@click="search">

name="search"/>

import BaseButton from './baseButton'

import BaseIcon from './baseIcon'

import BaseInput from './baseInput'

export default {

components: { BaseButton, BaseIcon, BaseInput }

}

写小项目这么引入还好,但等项目一臃肿起来...啧啧。 这里是借助 webpack,使用 require.context()方法来创建自己的模块上下文,从而实现自动动态 require组件。

这个方法需要3个参数:

  • 要搜索的文件夹目录

  • 是否还应该搜索它的子目录

  • 一个匹配文件的正则表达式。

40047320258582e830702b3c320a255c.png

在你放基础组件的文件夹根目录下新建 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来实现无感调试)。

d42a0781c0f624086acc4f56f5f88dad.png

Hiper官方文档

我们开发完一个项目或者给一个项目做完性能优化以后,如何来衡量这个项目的性能是否达标?

我们的常见方式是在 DevTool中的 performance和 network中看数据,记录下几个关键的性能指标,然后刷新几次再看这些性能指标。

有时候我们发现,由于样本太少,受当前「网络」、「CPU」、「内存」的繁忙程度的影响很重,有时优化后的项目反而比优化前更慢。

如果有一个工具,一次性地请求N次网页,然后把各个性能指标取出来求平均值,我们就能非常准确地知道这个优化是「正优化」还是「负优化」。

并且,也可以做对比,拿到「具体优化了多少」的准确数据。这个工具就是为了解决这个痛点的。

安装

sudo npm install hiper -g

# 或者使用 yarn:

# sudo yarn global add hiper

性能指标

KeyValue
DNS查询耗时domainLookupEnd - domainLookupStart
TCP连接耗时connectEnd - connectStart
第一个Byte到达浏览器的用时responseStart - requestStart
页面下载耗时responseEnd - responseStart
DOM Ready之后又继续下载资源的耗时domComplete - domInteractive
白屏时间domInteractive - navigationStart
DOM Ready 耗时domContentLoadedEventEnd - navigationStart
页面加载总耗时loadEventEnd - navigationStart

6a49d55c2667da2655c8dd1f9ef61527.png

用例

# 当我们省略协议头时,默认会在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高阶组件封装

我们常用的 和 就是一个高阶(抽象)组件。

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)

使用:

id="app">

:time="1000" events="click">

@click="onClick($event, 1)">click+1 {{val}}

:time="1000" events="click" :isDebounce="true">

@click="onAdd">click+3 {{val}}

:time="3300" events="mouseleave" :isDebounce="true">

@mouseleave.prevent="onAdd">click+3 {{val}}

使用:

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`关联起来,并将其改造:

实现来自:让在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订阅的事件了

}

求一份深圳的内推

目前本人在(又)准备跳槽,希望各位大佬和HR小姐姐可以内推一份靠谱的深圳前端岗位! 996.ICU 就算了。

11680600f0fc4cacde708bd8cf5df264.png

  • 微信: huab119

  • 邮箱: 454274033@qq.com

作者掘金文章总集

  • 「从源码中学习」面试官都不知道的Vue题目答案

  • 「从源码中学习」Vue源码中的JS骚操作

  • 「从源码中学习」彻底理解Vue选项Props

  • 「Vue实践」项目升级vue-cli3的正确姿势

  • 为何你始终理解不了JavaScript作用域链?

公众号

6231bd30d710a1ce58dd4330839d3f53.png 8c288ec2899946bbfab7e84ca51add84.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值