本文项目基于Vue-Cli3,想知道如何正确搭建请看我以前的文章:javascript
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
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 复制代码
在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) } 复制代码
咱们写组件的时候一般须要引入另外的组件:
<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'
咱们就能够随时随地使用这些基础组件,无需手动引入了。
咱们写单页面应用,想看页面修改后性能变动其实挺繁琐的。有时想知道是「正优化」仍是「负优化」只能靠手动刷新查看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 复制代码
咱们经常使用的<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 }, }) 复制代码
关于防抖和节流是啥就不赘述了。这里贴出组件代码:
改编自: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标签等)、能够包裹任意的单一子元素等等
至于用不用抽象组件,就见仁见智了。
中央事件总线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
就是更优的解决方案。
eventBus
咱们从Vue源码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订阅的事件了 } 复制代码
uglifyjs
的Terser 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
开启gzip压缩的好处是什么?
能够减少文件体积,传输速度更快。gzip是节省带宽和加快站点速度的有效方法。
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 }) ) 复制代码
Nginx
的gzip
设置打开/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
?经过使用curl
测试每一个资源的请求响应,并检查Content-Encoding
:
显示 Content-Encoding: gzip
,即为配置成功。
不一样之处在于:
Webpack
压缩会在构建运行期间一次压缩文件,而后将这些压缩版本保存到磁盘。
nginx
在请求时压缩文件时,某些包可能内置了缓存,所以性能损失只发生一次(或不常常),但一般不一样之处在于,这将在响应 HTTP请求时发生。
对于实时压缩,让上游代理(例如 Nginx)处理 gzip和缓存一般更高效,由于它们是专门为此而构建的,而且不会遭受服务端程序运行时的开销(许多都是用C语言编写的) 。
使用 Webpack的
好处是, Nginx
每次请求服务端都要压缩好久才回返回信息回来,不只服务器开销会增大不少,请求方也会等的不耐烦。咱们在 Webpack
打包时就直接生成高压缩等级的文件,做为静态资源放在服务器上,这时将 Nginx
做为二重保障就会高效不少(请求其它目录资源时)。
注:具体是在请求时实时压缩,或在构建时去生成压缩文件,就要看项目业务状况。
原本还想谢谢动态配置表单相关,但篇幅太长也太难写了。
好了,又水完一篇,入正题:
目前本人在(又)准备跳槽,但愿各位大佬和HR小姐姐能够内推一份靠谱的深圳前端岗位!996.ICU
就算了。
huab119
454274033@qq.com