写在前面
马上到了金三银四的时间, 很多公司开启了今年第一轮招聘的热潮, 虽说今年是互联网的寒冬, 但是只要对技术始终抱有热情以及有过硬的实力, 即使是寒冬也不会阻挠你前进的步伐. 在面试的时候, 往往在二面, 三面的时面试官会结合你的简历问一些关于你简历上项目的问题, 而以下这个问题在很多时候都会被问到
在这个项目中你有遇到什么技术难点, 你是怎么解决的?
其实这个问题旨在了解你在遇到问题的时候的解决方法, 毕竟现在前端技术领域广, 各种框架和组件库层出不穷, 而业务需求上有时纷繁复杂, 观察一个程序员在面对未知问题时是如何处理的, 这个过程相对于只出一些面试题来考面试者更能了解面试者实际解决问题的能力
而很多人会说我的项目不大, 并没有什么难点, 或者说并不算难点, 只能说是一些坑, 只要 google 一下就能解决, 实在不行请教我同事, 这些问题并没有困扰我很久. 其实我也遇到过相同的情况, 和面试官说如何通过搜索引擎解决这些坑的吧不太好, 让面试官认为你只是一个 API Caller, 但是又没有什么值得一谈的项目难点
我的建议是, 如果没有什么可以深聊的技术难点, 不妨在日常开发过程中, 试着封装几个常用的组件, 同时尝试分析项目的性能瓶颈, 寻找一些优化的方案, 同样也能让面试官对你有一个整体的了解
在这篇文章中, 我会分享在我目前公司的项目里, 是如何在满足业务需求的基础上, 让整个系统焕然一新的过程
技术栈是 vue + Element 的单页面应用
起源
在我刚入职的那会, 编码能力不怎么好, 加上之前离职的前端技术栈是 React, 接手这个 Vue 项目的时候, 代码高度的耦合, 而那个时候因为能力有限, 也只是在他的基础上继续开发, 好在接手的时候开发进度也只是刚刚开始, 因此在几个月后的某一天, 我做了一个决定: 准备把整个项目重写
得益于整个后台管理系统都是我独立开发的, 项目的不足点我都深有体会, 并且修改的时候能够更加的自由, 恰好在那段时间看了花裤衩的, 我决定新开一个工程把之前的代码全部重写
项目结构
之前我有打算基于 webpack4 自己写个脚手架用来打包文件, 但是那段时间刚好 Vue-cli3 刚刚发布正式版并且也是基于 Webpack4 封装的, 于是想了一下还决定使用新的 Vue-cli3 脚手架搭建, 最后我将项目分为以下层级
├─API //API 接口
├─assets // 项目运行时使用到的图片和静态资源
├─components // 组件
│ ├─Breadcrumb // 面包屑组件
│ ├─Ellipsis // 业务组件
│ ├─FormPanel // 业务组件
│ ├─Sidebar // 侧边栏组件
│ └─globalComponents // 全局组件
│ ├─Pagination // 分页器组件
│ ├─SildeDown // 业务组件
│ ├─SvgIcon //svg 图标组件
│ ├─TableOptions // 业务组件
│ ├─Toggle // 业务组件
│ ├─ZTable // 表格组件
│ └─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 个方面分享一下我在项目中是如何改善系统的性能, 让系统 "步履如飞" 的
构建相关
网络请求相关
静态资源优化
编码相关
构建相关
构建方面通过合理的配置构建工具, 达到减少生产环境的代码的体积, 减少打包时间, 缩短页面加载时间
路由懒加载
传统的路由组件是通过 import 静态的打包到项目中, 这样做的缺点是因为所有的页面组件都打包在同一个脚本文件中, 导致生产环境下首屏因为加载的代码量太多会有明显的卡顿(白屏)
通过 import()使得 ES6 的模块有了动态加载的能力, 让 url 匹配到相应的路径时, 会动态加载页面组件, 这样首屏的代码量会大幅减少, webpack 会把动态加载的页面组件分离成单独的一个 chunk.JS 文件
当然懒加载也有缺点, 就是会额外的增加一个 http 请求, 如果项目非常小的话可以考虑不使用路由懒加载
预渲染
由于浏览器在渲染出页面之前, 需要先加载和解析相应的 html,CSS 和 JS 文件, 为此会有一段白屏的时间, 如何尽可能的减少白屏对用户的影响, 目前我选择的是在 HTML 模版中, 注入一个 loading 动画, 这里我拿 D2-Admin https://doc.d2admin.fairyever.com/zh/ 中的 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), 通过后端输出一个首页的模版, 或者使用骨架屏的方案, 这里本人没有深入的了解过, 有兴趣的朋友可以去实践一下
升级到最新的 webpack 版本
webpack4 相对于 webpack3 来说在打包优化方面性能提升还是比较明显的, 如果觉得自己配置脚手架比较复杂的话, 可以使用 vue-cli3 来构建你的项目, 同样是基于 webpack4 搭建的
合理使用第三方库
如果项目中有一些日期操作的需求, 不妨将目光从 moment 转移到 https://github.com/iamkun/dayjs , 相对于笨重的 moment, 它只有 2kb,day 和 moment 的 API 完全一样, 并且中文文档也比较友好
另外对于 lodash 这类的库如果只需要部分功能, 则只要引入其中一部分, 这样 webpack 在 treeshaking 后在生产环境也只会引入这一部分的代码
常用的路径创建文件别名
给常用的模块路径创建一个别名是一个不错的选择, 可以减少模块查找时耗费的时间, 项目越大收益也就越明显
vue-cli3 中的配置和使用方法(webpack 链式调用文档)
使用可视化工具分析打包后的模块体积
我通过 webpack-bundle-analyzer 这个插件在每次打包后能够更加直观的分析打包后模块的体积, 再对其中比较大的模块进行优化
这是我在优化前的各模块体积:
因为业务需求, 要求前端导出 PDF 和 Excel 文件, 我这里引入了 xlsx 和 PDF.JS 这 2 个包, 但是打包后通过可视化工具发现光着 2 个文件就占了一半的项目体积, 另外 elementui 和 moment 也非常的大
这是我优化后通过可视化工具观察到的各模块体积, 我将这些类库放到 CDN 上从生产环境中抽离出去, 可以看到没有明显特别大的模块了
网络请求相关
这部分旨在实现需求的前提下尽量减少 http 请求的开销, 或者减少响应时间
CDN
将第三方的类库放到 CDN 上, 能够大幅度减少生产环境中的项目体积, 另外 CDN 能够实时地根据网络流量和各节点的连接, 负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上
通俗的来说就是提升项目中的静态文件的传输速度, 在 vue-cli3 中可以通过 externals 配置项, 将第三方的类库的引用地址从本地指向你提供的 CDN 地址
这里通过环境变量来判断生产环境才启用 CDN, 除了需要开启 CDN 外, 你还需要在 index.HTML 注入 CDN 的域名, 所以我这里通过 HTML-webpack-plugin 根据 cdn 域名动态的注入 script 标签, 同时需要在 index.HTML 中通过模版的语法声明循环的数组和注入的元素
打包前的 index.HTML:
打包后的 index.HTML:
可以看到通过这个插件可以将 cdn 域名动态的注入到打包后的 index.HTML 中
还有一点要注意的是, externals 对象的属性为你引入包的名字, 而属性值是对应的 AMD 模块名字(这个名字比较特殊, 一般常用的我已经列出来了, 其余的第三方类库名字可以到访问对应的 CDN 在源码中寻找, 一般在开头行都会有声明, 导入有困难的还可以看下这篇博客 webpack externals 深入理解 https://segmentfault.com/a/1190000012113011 )
gzip
为你的文件开启 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 属性
preload 可以让浏览器尽早发现提前加载资源, 而不是等到解析到当前标签才发 http 请求
prefetch 可以让浏览器提前加载下个页面可能会需要的资源
dns-prefetch 可以让浏览器提前对域名进行解析, 减少 DNS 查找的开销
vue-cli3 默认会给所有懒加载的路由添加 prefetch 属性, 如果你的静态资源和后端接口不是同一个服务器的话, 可以将你后端的域名放入 link 标签加入 dns-prefetch 属性
京东首页也使用到了 dns-prefetch 技术
http2
http2 从 2015 年问世以来已经走过了 4 个年头, 如今在国内也有超过 50% 的覆盖率, 得益于 http2 的分帧传输, 它能够极大的减少 http(s)请求开销
http2 和 http1.1 的性能差异对比 https://http2.akamai.com/demo
如果系统首屏同一时间需要加载的静态资源非常多, 但是浏览器对同一域名的 tcp 连接数量是有限制的 (Chrome 为 6 个) 超过规定数量的 tcp 连接, 则必须要等到之前的请求收到响应后才能继续发送, 而 http2 则可以在一个 tcp 连接中并发多个请求没有限制, 在一些网络较差的环境开启 http2 性能提升尤为明显
这里极力推荐在支持 https 协议的服务器中使用 http2 协议, 可以通过 Web 服务器 Nginx 配置, 或是直接让服务器支持 http2
静态资源优化
这部分旨在减少请求一些图片资源所造成的影响
图片懒加载
如果你的系统是一个偏展示的项目需要给用户展示大量图片, 是否启用图片懒加载可能是你需要考虑的一个点, 不在用户视野中的图片是没有必要加载的, 图片懒加载通过让图片先加载成一张统一的图片, 再给进入用户视野的图片替换真正的图片地址, 可以同一时间减少 http 请求开销, 避免显示图片导致的画面抖动, 提高用户体验
下面我提供 2 种图片懒加载的思路, 这 2 个方案最终都是用将占位的图片替换成真正的图片, 然后给 img 标签设置一个自定义属性 data-src 存放真正的图片地址, src 存放占位图片的地址
使用该 DOM 节点相关的 CSS 边框集合, 它返回一个对象, 其中有一个 top 属性代表当前 DOM 节点距离浏览器窗口顶部的高度, 判断是否小于当前浏览器窗口的高度(Windows.innerHeight), 若小于说明已经进入用户视野, 然后替换为真正的图片即可, 同时需要监听 scroll 事件不停的执行上述操作(需要进行节流)
使用构造器传入一个回调函数, 生成一个实例 observer, 这个实例有一个 observe 方法能够监听指定元素是否进入视图, 进入则会触发之前的回调函数. 同时给回调函数传入一个 entries 的参数, 记录着这个实例观察的所有元素的一些阈值信息, 其中 intersectionRatio 大于 0 表示进入了用户视野
此时替换为真实的图片, 并且调用实例的 unobserve 将这个 img 元素从这个实例的观察列表的去除
这 2 种的区别在于监听的方式, 我个人更推荐使用 Intersection Observer, 因为监听 scroll 事件开销比较大, 而让将这个工作交给另一个线程异步的去监听开销会小很多, 但是它的缺点是一些老版本的浏览器可能支持率不高, 好在社区有 polyfill 的方案
或者可以直接使用第三方的组件库 https://www.npmjs.com/package/vue-lazyload
使用 svg 图标
相对于用一张图片来表示图标, svg 拥有更好的图片质量, 体积更小, 并且不需要开启额外的 http 请求, svg 是一个未来的趋势, 阿里的图标库 iconfont 支持导出 svg 格式的图标, 但是在项目中需要封装一个支持 svg 的组件, 具体封装的教程可以参考花裤衩的文章这里就不多赘述了手摸手, 带你优雅的使用 icon, 或者可以参考我的
使用 webp 图片
webp 图片最初在 2010 年发布, 目标是减少文件大小, 但达到和 JPEG 格式相同的图片质量, 希望能够减少图片档在网络上的发送时间. webp 图片无损比 PNG 图片无损的平均体积要小 20%~40%, 并且图片质量用肉眼看几乎没什么差别
webp 图片的缺点是兼容性并不是那么的好, 在 can l use 上查到 webp 图片的支持率并不是那么的理想. 但是我们仍可以在支持 webp 图片的浏览器中使用它, 而在不支持的浏览器提供 PNG 图片
编码相关
编码这方面主要是减少对 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 导入, 它可以扫描你指定的文件, 然后全部导入到指定文件, 可以用在
vue-router 的路由自动导入
vuex 的模块自动导入
svg 图标的自动导入
全局组件的自动导入
vuex:
全局组件和 svg 图标:
有兴趣的朋友可以看看我另一篇介绍这个 API 的博客
写在后面
本人为 18 年毕业本科生, 坐标上海, 1 年的前端开发经验, 如果有比较好的互联网企业内推机会的话, 希望能在评论区能留下您的联系方式或者联系我的邮箱 mailto:1996yeyan@gmail.com , 非常感谢~
下篇在这里:
我是如何让公司后台管理系统焕然一新的(下)- 封装组件
参考资料
D2 Admin https://doc.d2admin.fairyever.com/zh/
嗨, 送你一张 Web 性能优化地图 https://github.com/berwin/Blog/issues/23
vue-cli3 项目从搭建优化到 docker 部署
前端性能优化不完全指北 https://github.com/Weiyu-Chen/blog/issues/9
来源: https://juejin.im/post/5c76843af265da2ddd4a6dd0