立刻到了金三银四的时间,不少公司开启了今年第一轮招聘的热潮,虽然说今年是互联网的寒冬,可是只要对技术始终抱有热情以及有过硬的实力,即便是寒冬也不会阻挠你前进的步伐。在面试的时候,每每在二面,三面的时面试官会结合你的简历问一些关于你简历上项目的问题,而如下这个问题在不少时候都会被问到javascript
在这个项目中你有遇到什么技术难点,你是怎么解决的?
css
其实这个问题旨在了解你在遇到问题的时候的解决方法,毕竟如今前端技术领域广,各类框架和组件库层出不穷,而业务需求上有时纷繁复杂,观察一个程序员在面对未知问题时是如何处理的,这个过程相对于只出一些面试题来考面试者更能了解面试者实际解决问题的能力html
而不少人会说个人项目不大,并无什么难点,或者说并不算难点,只能说是一些坑,只要google一下就能解决,实在不行请教我同事,这些问题并无困扰我好久。其实我也遇到过相同的状况,和面试官说如何经过搜索引擎解决这些坑的吧不太好,让面试官认为你只是一个API Caller,可是又没有什么值得一谈的项目难点前端
个人建议是,若是没有什么能够深聊的技术难点,不妨在平常开发过程当中,试着封装几个经常使用的组件,同时尝试分析项目的性能瓶颈,寻找一些优化的方案,一样也能让面试官对你有一个总体的了解vue
在这篇文章中,我会分享在我目前公司的项目里,是如何在知足业务需求的基础上,让整个系统面目一新的过程java
技术栈是Vue + Element的单页面应用node
在我刚入职的那会,编码能力不怎么好,加上以前离职的前端技术栈是React,接手这个Vue项目的时候,代码高度的耦合,而那个时候由于能力有限,也只是在他的基础上继续开发,好在接手的时候开发进度也只是刚刚开始,所以在几个月后的某一天,我作了一个决定:准备把整个项目重写webpack
得益于整个后台管理系统都是我独立开发的,项目的不足点我都深有体会,而且修改的时候可以更加的自由,刚好在那段时间看了花裤衩的vue-element-admin,我决定新开一个工程把以前的代码所有重写ios
以前我有打算基于Webpack4本身写个脚手架用来打包文件,可是那段时间恰好Vue-cli3刚刚发布正式版而且也是基于Webpack4封装的,因而想了一下还决定使用新的Vue-cli3脚手架搭建,最后我将项目分为如下层级nginx
├─api //api接口
├─assets //项目运行时使用到的图片和静态资源
├─components //组件
│ ├─BaseEllipsis //业务组件 (Base开头都是全局组件)
│ ├─BasePagination //分页器组件
│ ├─BaseIcon //svg图标组件
│ ├─BaseToggle //业务组件
│ ├─BaseTable //表格组件
│ ├─FormPanel //业务组件(Form开头是围绕表单相关的小组件)
│ ├─TableOptions //业务组件(Table开头是围绕表格相关的小组件)
│ ├─TheBreadcrumb //面包屑组件(The开头是每一个页面组件只会引入一次的无状态组件)
| ├─TheSidebar //侧边栏组件
│ ├─TransitionSildeDown //业务组件(Transition开头是动画组件)
│ └─index.js //全局组件自动注册的脚本
│
├─directives //自定义指令
├─element //elementui
├─errorLog //错误捕获
├─filters //全局过滤器
├─icons //svg图标存放文件夹
├─interface //TypeScript接口
├─mixins //局部混入
├─router //vue-router
│ ├─modules
│ └─index.js
├─store //vuex
│ ├─modules
│ └─index.js
├─style //全局样式/局部页面可复用的样式
├─util //公共的模块(axios,cookie封装,工具函数)
├─vendor //类库文件
└─views //页面组件(全部给用户显示的页面)
复制代码
一个良好的项目分层在业务迭代的时候可以快速找到对应的模块进行修改,而不是在茫茫的代码海中找到其中的某一行代码
在我重写整个系统以前,每次打包都会花费好几分钟的时间,而且打包后的项目超过了17M
然而在我优化系统以后,打包后的体积只有2M,缩小了8倍
这里我从如下4个方面分享一下我在项目中是如何改善系统的性能,让系统"步履如飞"的
这部分旨在实现需求的前提下尽可能减小http请求的开销,或者减小响应时间
将第三方的类库放到CDN上,可以大幅度减小生产环境中的项目体积,另外CDN可以实时地根据网络流量和各节点的链接、负载情况以及到用户的距离和响应时间等综合信息将用户的请求从新导向离用户最近的服务节点上
另外由于CDN和服务器的域名通常不是同一个,能够缓解同一域名并发http请求的数量限制,有效分流以及减小多余的cookie的发送(CDN上面的静态资源请求时不须要携带任何cookie)
通俗的来讲就是使用CDN会必定程度上提高项目中的静态文件的传输速度,在vue-cli3中能够经过externals配置项,将第三方的类库的引用地址从本地指向你提供的CDN地址
externals只适用于ES Module的默认导入
这里经过环境变量来判断生产环境才启用CDN,除了须要开启CDN外,你还须要在index.html注入CDN的域名,因此我这里经过html-webpack-plugin根据cdn域名动态的注入script标签,同时须要在index.html中经过模版的语法声明循环的数组和注入的元素
打包前的index.html:
打包后的index.html:
能够看到经过这个插件能够将cdn域名动态的注入到打包后的index.html中
还有一点要注意的是,externals对象的属性为你引入包的名字,而属性值是对应的全局变量名称(CDN引入的类库文件会自动挂载到window对象下面,而挂载时的属性名须要去对应的CDN在源码中寻找,通常在开头行都会有声明,除此以外导入还有困难的还能够看下这篇博客webpack externals 深刻理解)
这里仍是建议尽可能放到公司专用的CDN上,不推荐使用公共的CDN,由于容易挂,生产环境仍是以稳定为主吧
将长时间不会改变的第三方类库或者静态资源设置为强缓存,将max-age设置为一个很是长的时间,再将访问路径加上哈希达到哈希值变了之后保证获取到最新资源(vue-cli3会自动构建,本身搭建的webpack脚手架须要自行配置contentHash,chunkHash)
CDN上的缓存策略,能够看到max-age的值很是大,这样下次访问就只会读取本地磁盘/内存中缓存的资源:
对于index.html和一些图片等多媒体资源,能够选择协商缓存(max-age<=0,Last-Modified,ETag),保证返回服务器最新的资源
一个好的缓存策略,有助于减轻服务器的压力,而且显著的提高用户的体验
为你的文件开启gzip压缩是一个不错的选择,一般开启gzip压缩可以有效的缩小传输资源的大小,若是你的项目是用nginx做为web服务器的话,只需在nginx的配置文件中配置相应的gzip选项就可让你的静态资源服务器开启gzip压缩
#开启和关闭gzip模式
gzip on;
#gizp压缩起点,文件大于1k才进行压缩
gzip_min_length 1k;
# gzip 压缩级别,1-9,数字越大压缩的越好,也越占用CPU时间
gzip_comp_level 6;
# 进行压缩的文件类型。
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript ;
#nginx对于静态文件的处理模块,开启后会寻找以.gz结尾的文件,直接返回,不会占用cpu进行压缩,若是找不到则不进行压缩
gzip_static on
# 是否在http header中添加Vary: Accept-Encoding,建议开启
gzip_vary on;
# 设置gzip压缩针对的HTTP协议版本
gzip_http_version 1.1;
复制代码
可是咱们这里要说的是前端输出gzip文件,利用compression-webpack-plugin让webpack在打包的时候输出.gz后缀的压缩文件
这样不须要服务器主动压缩咱们就已经能够获得gzip文件,在上面的nginx配置项中能够发现这一行
#nginx对于静态文件的处理模块,开启后会寻找以.gz结尾的文件,直接返回,不会占用cpu进行压缩,若是找不到则不进行压缩
gzip_static on
复制代码
只要把.gz的文件放到服务器上,开始gzip_static就可让服务器优先返回.gz文件,在面对高流量时,也能必定程度减轻对服务器的压力,属于用空间来换时间(.gz文件会额外占有服务器的空间)
对于现代浏览器来讲,能够给link标签添加preload,prefetch,dns-prefetch属性
对于SPA应用来讲,当浏览器解析完script脚本才会生成DOM节点,若是你的项目中没有使用服务端渲染的话且须要加载一个比较耗时的首屏图片时,能够考虑将这个首屏图片放在preload标签中让浏览器预先请求并加载执行,这样当script脚本执行完毕后就会瞬间加载图片(不然须要等脚本执行完毕后再向后台请求图片)
另外使用preload预加载首屏须要的css样式也是一个不错的选择,相似的库有critical
没有使用preload
使用preload
经过Waterfall能够看到这个webp图片须要等到脚本加载完以后才回去请求,若是这个图片比较大就会浪费没必要要的时间
在工程中,利用一些preload的webpack插件能够很方便的给打包后的index.html注入预加载的资源标签,有兴趣的朋友能够试着搜索一下相关的插件
prefetch可让浏览器提早加载下个页面可能会须要的资源,vue-cli3默认会给全部懒加载的路由添加prefetch属性,这样能够在你访问使用到懒加载的路由页面时可以得到更快的加载速度
preload和prefetch的区别在于,preload的资源会和页面须要的静态资源并行加载,而prefetch则会等到浏览器加载完必要的资源后,在空闲时间加载被标记为prefetch的资源
dns-prefetch可让浏览器提早对域名进行解析,减小DNS查找的开销,若是你的静态资源和后端接口不是同一个服务器的话,能够将考虑你后端的域名放入link标签加入dns-prefetch属性
京东首页也使用到了dns-prefetch技术
http2从2015年问世以来已经走过了4个年头,现在在国内也有超过50%的覆盖率,得益于http2的分帧传输,它可以极大的减小http(s)请求开销
若是系统首屏同一时间须要加载的静态资源很是多,可是浏览器对同一域名的tcp链接数量是有限制的(chrome为6个)超过规定数量的tcp链接,则必需要等到以前的请求收到响应后才能继续发送,而http2则能够在一个tcp链接中并发多个请求没有限制,在一些网络较差的环境开启http2性能提高尤其明显
这里极力推荐在支持https协议的服务器中使用http2协议,能够经过web服务器Nginx配置,或是直接让服务器支持http2
nginx开启http2很是简单,在nginx.conf中只需在原来监听的端口后面加上http2就能够了,前提是你的nginx版本不能低于1.95,而且已经开启了https
listen 443 ssl http2;
复制代码
在network中经过protocol能够查看到当前的资源是经过哪一个版本的http协议传输的
h2表明http2
构建方面经过合理的配置构建工具,达到减小生产环境的代码的体积,减小打包时间,缩短页面加载时间
传统的路由组件是经过import静态的打包到项目中,这样作的缺点是由于全部的页面组件都打包在同一个脚本文件中,致使生产环境下首屏由于加载的代码量太多会有明显的卡顿(白屏)
经过import()使得ES6的模块有了动态加载的能力,让url匹配到相应的路径时,会动态加载页面组件,这样首屏的代码量会大幅减小,webpack会把动态加载的页面组件分离成单独的一个chunk.js文件
固然懒加载也有缺点,就是会额外的增长一个http请求,若是项目很是小的话能够考虑不使用路由懒加载
因为浏览器在渲染出页面以前,须要先加载和解析相应的html,css和js文件,为此会有一段白屏的时间,如何尽量的减小白屏对用户的影响,目前我选择的是在html模版中,注入一个loading动画,这里我拿D2-Admin中的loading动画举例
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>icon.ico">
<title><%= VUE_APP_TITLE %></title>
<style>
html, body, #app { height: 100%; margin: 0px; padding: 0px; }
.d2-home { background-color: #303133; height: 100%; display: flex; flex-direction: column; }
.d2-home__main { user-select: none; width: 100%; flex-grow: 1; display: flex; justify-content: center; align-items: center; flex-direction: column; }
.d2-home__footer { width: 100%; flex-grow: 0; text-align: center; padding: 1em 0; }
.d2-home__footer > a { font-size: 12px; color: #ABABAB; text-decoration: none; }
.d2-home__loading { height: 32px; width: 32px; margin-bottom: 20px; }
.d2-home__title { color: #FFF; font-size: 14px; margin-bottom: 10px; }
.d2-home__sub-title { color: #ABABAB; font-size: 12px; }
</style>
</head>
<body>
<noscript>
<strong>
很抱歉,若是没有 JavaScript 支持,D2Admin 将不能正常工做。请启用浏览器的 JavaScript 而后继续。
</strong>
</noscript>
<div id="app">
<div class="d2-home">
<div class="d2-home__main">
<img
class="d2-home__loading"
src="./image/loading/loading-spin.svg"
alt="loading">
<div class="d2-home__title">
正在加载资源
</div>
<div class="d2-home__sub-title">
初次加载资源可能须要较多时间 请耐心等待
</div>
</div>
<div class="d2-home__footer">
<a
href="https://github.com/d2-projects/d2-admin"
target="_blank">
https://github.com/d2-projects/d2-admin
</a>
</div>
</div>
</div>
</body>
</html>
复制代码
在打包完成后,在这个index.html下方还会注入页面的脚本,当用户访问你的项目时,脚本尚未执行,可是能够显示loading动画,由于它是直接注入在html中的,等到脚本执行完毕后,Vue会新生成一个app的节点而后将旧的同名节点删除,这样能够有效的过渡白屏的时间
loading动画只是一个让用户感知到你程序正在启动的效果,只是一个静态页面没有任何的功能
另外预渲染还可使用服务端渲染(SSR),经过后端输出一个首页的模版,或者使用骨架屏的方案,这里本人没有深刻的了解过,有兴趣的朋友能够去实践一下
webpack4相对于webpack3来讲在打包优化方面性能提高仍是比较明显的,若是以为本身配置脚手架比较复杂的话,可使用vue-cli3来构建你的项目,一样是基于webpack4搭建的
当没有一个稳定的CDN时,使用DllPlugin这个webpack插件一样能够将类库从业务代码中分离出去,其原理是预先将类库文件打包,随后建立一个关联表,在业务代码依赖第三方类库时,会直接去关联表中获取已经打包好的类库文件。这样作的好处在于由于业务代码会经常须要打包上线,而第三方类库基本不会改变,因此每次打包能够跳过这些类库文件,减小没必要要的重复打包
DllPlugin是一个webpack内置的插件,无需安装,可是要让打包后的index.html注入这些打包后第三方类库,须要额外安装add-asset-html-webpack-plugin
插件
当你须要在index.html中注入多个类库时,须要实例化屡次add-asset-html-webpack-plugin
,这里咱们能够利用nodejs的fs模块,遍历DllPlugin打包后的目录,根据类库的数量决定须要生成多少个实例,很是的灵活,具体的配置项能够查看我底部的连接
若是项目中有一些日期操做的需求,不妨将目光从moment转移到day,相对于笨重的moment,它只有2kb,day和moment的api彻底同样,而且中文文档也比较友好
另外对于lodash这类的库若是只须要部分功能,则只要引入其中一部分,这样webpack在treeshaking后在生产环境也只会引入这一部分的代码
对于UI库(element-ui)打包后的体积也会很是大,尽可能使用按需加载,官方文档上也有详细教程
element-ui的压缩后的体积居然是Vue的十倍
给经常使用的模块路径建立一个别名是一个不错的选择,能够减小模块查找时耗费的时间,项目越大收益也就越明显
vue-cli3中的配置和使用方法(webpack链式调用文档)
我经过webpack-bundle-analyzer这个插件在每次打包后可以更加直观的分析打包后模块的体积,再对其中比较大的模块进行优化
这是我在优化前的各模块体积:
由于业务需求,要求前端导出pdf和excel文件,我这里引入了xlsx和pdf.js这2个包,可是打包后经过可视化工具发现光着2个文件就占了一半的项目体积,另外elementui和moment也很是的大
这是优化后经过可视化工具观察到的各模块体积,经过将这些类库放到CDN上或者使用dllPlugin将类库和业务文件分离,能够看到没有明显特别大的模块了
这部分旨在减小请求一些图片资源所形成的影响
若是你的系统是一个偏展现的项目须要给用户展现大量图片,是否启用图片懒加载多是你须要考虑的一个点,不在用户视野中的图片是没有必要加载的,图片懒加载经过让图片先加载成一张统一的图片,再给进入用户视野的图片替换真正的图片地址,能够同一时间减小http请求开销,避免显示图片致使的画面抖动,提升用户体验
下面我提供2种图片懒加载的思路,这2个方案最终都是用将占位的图片替换成真正的图片,而后给img标签设置一个自定义属性data-src存放真正的图片地址,src存放占位图片的地址
DOM元素包含一个getBoundingClientRect方法,执行该方法返回当前DOM节点相关的CSS边框集合
其中有一个top属性表明当前DOM节点距离浏览器窗口顶部的高度,只需判断top值是否小于当前浏览器窗口的高度(window.innerHeight),若小于说明已经进入用户视野,而后替换为真正的图片便可
另外使用getBoundingClientRect做图片懒加载须要注意3点
IntersectionObserver做为一个构造函数,传入一个回调函数做为参数,生成一个实例observer,这个实例有一个observe方法用来观察指定元素是否进入了用户的可视范围,随即触发传入构造函数中的回调函数
同时给回调函数传入一个entries的参数,记录着这个实例观察的全部元素的一些阈值信息(对象),其中intersectionRatio属性表示图片进入可视范围的百分比,大于0表示已经有部分进入了用户视野
此时替换为真实的图片,而且调用实例的unobserve将这个img元素从这个实例的观察列表的去除
对懒加载还有迷惑的同窗我这里写了一个DEMO能够参考一下实现的方式源代码
这2种的区别在于监听的方式,我我的更推荐使用Intersection Observer,由于经过监听scroll事件开销比较大,而让将这个工做交给另外一个线程异步的去监听开销会小不少,可是它的缺点是一些老版本的浏览器可能支持率不高,好在社区有polyfill的方案
或者能够直接使用第三方的组件库vue-lazyload
相对于用一张图片来表示图标,svg拥有更好的图片质量,体积更小,而且不须要开启额外的http请求,svg是一个将来的趋势,阿里的图标库iconfont支持导出svg格式的图标,可是在项目中须要封装一个支持svg的组件,具体封装的教程能够参考花裤衩的文章这里就很少赘述了手摸手,带你优雅的使用 icon,或者能够参考个人github
webp图片最初在2010年发布,目标是减小文件大小,但达到和JPEG格式相同的图片质量,但愿可以减小图片档在网络上的发送时间。webp图片无损比png图片无损的平均体积要小 20%~40%,而且图片质量用肉眼看几乎没什么差异
webp图片的缺点是兼容性并非那么的好,在can l use 上查到webp图片的支持率并非那么的理想。可是咱们仍能够在支持webp图片的浏览器中使用它,而在不支持的浏览器提供png图片
这里须要使用到响应式图片,HTML提供了picture标签让咱们能够在不一样设备中使用不一样的图片格式
MDN:
HTML 元素经过包含零或多个
元素和一个 元素来为不一样的显示/设备场景提供图像版本。浏览器会选择最匹配的子
元素,若是没有匹配的,就选择 元素的 src 属性中的URL。而后,所选图像呈如今
元素占据的空间中。
在工程中咱们能够这样使用
picture标签包裹2个source标签,一个提供webp图片,经过srcset属性让浏览器从上到下选择能够支持的图片格式,若是浏览器不支持webp图片会只使用第二个source,会回退到png图片,若是浏览器不支持picture标签,会使用底部的img标签,一样也会生成一个png图片
picture标签的浏览器支持率,相对于webp要好不少(注意底部的img标签不管如何都要有,不然就算支持webp图片也没法渲染出图片)
对于一些png图片可能质量会很是的高,可是对于Web平台来讲,用户可能并不care图片的画质问题,可是若是加载图片致使页面出现卡顿那就显得得不偿失了,咱们能够考虑将一些画质较高的图片作压缩处理,我这里使用tinypng帮我压缩图片,一样可以保证在肉眼几乎分辨不出区别的状况下,提供一个体积较小的图片,若是有其余好的压缩软件也能够推荐给我
另外有一个图片压缩的 loader image-webpack-loader,在用户肉眼分辨不清的状况下必定程度上压缩图片
编码这方面主要是减小对DOM的访问,减小浏览器的重排/重绘,访问DOM是很是昂贵的操做,由于会涉及到2个不一样的线程交互(JS线程和UI渲染线程)而且DOM自己又是一个很是笨重的对象,这里给出几个建议
若是有须要动态建立DOM的需求,能够建立一个文档碎片(DocumentFragment),在文档碎片中操做由于不是在当前文档流不会引发重排/重绘,最后再一次性插入DOM节点
避免频繁获取视图信息(getBoundingClientRect,clientWidth,offsetWidth),当发生重排/重绘操做时浏览器会维护一个队列,等到超过了最大值或过了指定时间(1000ms/60 = 16.6ms)才会去清空队列一次性执行操做,这样能够节省性能,而获取视图信息会马上清空队列执行重排/重绘
高频的监听事件使用函数防抖/节流(可使用lodash库的throttle函数,可是推荐先搞懂原理)
特效能够考虑单独触发渲染层(CSS3的transform会触发渲染层),动画可使用绝对定位脱离文档流
使用require.context这个webpack的api能够避免每次引入一个文件都须要显式的用import导入,它能够扫描你指定的文件,而后所有导入到指定文件,能够用在
vuex:
这样在建立一个新的模块时,不须要在index.js中用import引入,在modules中再声明一遍了
全局组件和svg图标:
避免了每建立一个全局组件都须要引入,在调用一次Vue.component的过程,而当加载到Svg组件会自动扫描icons文件夹,将全部svg图标导入进来
有兴趣的朋友能够看看我另外一篇介绍这个api的博客
部分优化的方案放在个人github上,有兴趣能够看看
下篇在这里: