资讯专栏INFORMATION COLUMN

「Vue实践」武装你的前端项目

曹金海 / 3169人阅读

摘要:所有的高阶抽象组件是通过定义选项来声明的。所以一般在生命周期或者中,需要用实例的方法清除可当你有多个时,就需要重复性劳动销毁这件事儿。更多的配置请看双端开启开启压缩的好处是什么可以减小文件体积,传输速度更快。

本文目录

接口模块处理

Vue组件动态注册

页面性能调试:Hiper

Vue高阶组件封装

性能优化:eventBus封装

webpack插件:真香

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

「Vue实践」项目升级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 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请求加上时间戳参数,避免从缓存中拿数据。 除了基础请求类型,还有很多类似下载、上传这种,需要特殊的的请求头,此时可以根据自身需求进行封装。

浏览器缓存是基于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 ");$message.warning(res.msg)
}
2. 基础组件自动化全局注册

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

官方文档:基础组件的自动化全局注册

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



写小项目这么引入还好,但等项目一臃肿起来...啧啧。 这里是借助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"
    ");

最后我们在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");
 #  加载指定页面100次
 hiper -n 100 "baidu.com");
 #  禁用缓存加载指定页面100次
 hiper -n 100 "baidu.com"); --no-cache
 #  禁JavaScript加载指定页面100次
 hiper -n 100 "baidu.com"); --no-javascript
 #  使用GUI形式加载指定页面100次
 hiper -n 100 "baidu.com"); -H false
 #  使用指定useragent加载网页100次
 hiper -n 100 "baidu.com"); -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.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)
使用方式
"app"> "1000" events="click"> "1000" events="click" :isDebounce="true"> "3300" events="mouseleave" :isDebounce="true">
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");

每个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后的大小从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区别及其意义

不同之处在于:

    Webpack压缩会在构建运行期间一次压缩文件,然后将这些压缩版本保存到磁盘。

    nginx在请求时压缩文件时,某些包可能内置了缓存,因此性能损失只发生一次(或不经常),但通常不同之处在于,这将在响应 HTTP请求时发生。

    对于实时压缩,让上游代理(例如 Nginx)处理 gzip和缓存通常更高效,因为它们是专门为此而构建的,并且不会遭受服务端程序运行时的开销(许多都是用C语言编写的) 。

    使用 Webpack的好处是, Nginx每次请求服务端都要压缩很久才回返回信息回来,不仅服务器开销会增大很多,请求方也会等的不耐烦。我们在 Webpack打包时就直接生成高压缩等级的文件,作为静态资源放在服务器上,这时将 Nginx作为二重保障就会高效很多(请求其它目录资源时)。

    注:具体是在请求时实时压缩,或在构建时去生成压缩文件,就要看项目业务情况。

求一份深圳的内推

本来还想谢谢动态配置表单相关,但篇幅太长也太难写了。

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

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

微信:huab119

邮箱:454274033@qq.com

作者掘金文章总集

「Vue实践」5分钟撸一个Vue CLI 插件

「Vue实践」武装你的前端项目

「中高级前端面试」JavaScript手写代码无敌秘籍

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

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

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

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

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

公众号

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/7332.html

相关文章

  • 7月份前端资源分享

    摘要:更多资源请文章转自月份前端资源分享的作用数组元素随机化排序算法实现学习笔记数组随机排序个变态题解析上个变态题解析下中的数字前端开发笔记本过目不忘正则表达式聊一聊前端存储那些事儿一键分享到各种写给刚入门的前端工程师的前后端交互指南物联网世界的 更多资源请Star:https://github.com/maidishike... 文章转自:https://github.com/jsfr...

    pingan8787 评论0 收藏0
  • 基于 Node.js 爬虫的数据 API,搭建一套属于自己的 API 数据

    摘要:没想到老师下课点名了,老师李。一哥们答到老师咦,李,你站起来一下这哥们低着脑袋站起来了,老师表格上面写的李是个女的啊这是不知道从哪个角落传来一声讨厌啊,人家就是个女的了啦。 SpliderApi https://github.com/ecitlm/Spl... 基于nodejs 的爬虫 API接口项目,包括前端开发日报、知乎日报、前端top框架排行、妹纸福利、搞笑视频/ 热点新闻资讯 ...

    chenjiang3 评论0 收藏0
  • 前端开发-从入门到Offer - 收藏集 - 掘金

    摘要:一些知识点有哪些方法方法前端从入门菜鸟到实践老司机所需要的资料与指南合集前端掘金前端从入门菜鸟到实践老司机所需要的资料与指南合集归属于笔者的前端入门与最佳实践。 工欲善其事必先利其器-前端实习简历篇 - 掘金 有幸认识很多在大厂工作的学长,在春招正式开始前为我提供很多内部推荐的机会,非常感谢他们对我的帮助。现在就要去北京了,对第一份正式的实习工作也充满期待,也希望把自己遇到的一些问题和...

    sf_wangchong 评论0 收藏0
  • 将函数式进行到底,用 Hooks 武装组件 - TXD 前端月刊 201905 期

    摘要:一个合格的中级前端工程师必须要掌握的个技巧对在毕业季还在找工作的同学,可以说很实用的,如果能全部掌握这些技巧,拿到几个应该是没有问题的,当然要注意平时的积累,能力要全面。而且该托管服务是完全免费的。 showImg(https://segmentfault.com/img/remote/1460000019376228?w=1790&h=898); 【阿里云 TXD 前端月刊】- 热门...

    xi4oh4o 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<