axios传数组到后台_我是如何让公司后台管理系统焕然一新的

作者:yeyan1996

https://juejin.im/post/5c76843af265da2ddd4a6dd0

e778e545ae7f8488a74d01b10c777f00.png

写在前面

马上到了金三银四的时间,很多公司开启了今年第一轮招聘的热潮,虽说今年是互联网的寒冬,但是只要对技术始终抱有热情以及有过硬的实力,即使是寒冬也不会阻挠你前进的步伐。在面试的时候,往往在二面,三面的时面试官会结合你的简历问一些关于你简历上项目的问题,而以下这个问题在很多时候都会被问到

在这个项目中你有遇到什么技术难点,你是怎么解决的?

其实这个问题旨在了解你在遇到问题的时候的解决方法,毕竟现在前端技术领域广,各种框架和组件库层出不穷,而业务需求上有时纷繁复杂,观察一个程序员在面对未知问题时是如何处理的,这个过程相对于只出一些面试题来考面试者更能了解面试者实际解决问题的能力

而很多人会说我的项目不大,并没有什么难点,或者说并不算难点,只能说是一些坑,只要google一下就能解决,实在不行请教我同事,这些问题并没有困扰我很久。其实我也遇到过相同的情况,和面试官说如何通过搜索引擎解决这些坑的吧不太好,让面试官认为你只是一个API Caller,但是又没有什么值得一谈的项目难点

我的建议是,如果没有什么可以深聊的技术难点,不妨在日常开发过程中,试着封装几个常用的组件,同时尝试分析项目的性能瓶颈,寻找一些优化的方案,同样也能让面试官对你有一个整体的了解

在这篇文章中,我会分享在我目前公司的项目里,是如何在满足业务需求的基础上,让整个系统焕然一新的过程

技术栈是Vue + Element的单页面应用

起源

在我刚入职的那会,编码能力不怎么好,加上之前离职的前端技术栈是React,接手这个Vue项目的时候,代码高度的耦合,而那个时候因为能力有限,也只是在他的基础上继续开发,好在接手的时候开发进度也只是刚刚开始,因此在几个月后的某一天,我做了一个决定:准备把整个项目重写

得益于整个后台管理系统都是我独立开发的,项目的不足点我都深有体会,并且修改的时候能够更加的自由,恰好在那段时间看了花裤衩的vue-element-admin,我决定新开一个工程把之前的代码全部重写

项目结构

之前我有打算基于Webpack4自己写个脚手架用来打包文件,但是那段时间刚好Vue-cli3刚刚发布正式版并且也是基于Webpack4封装的,于是想了一下还决定使用新的Vue-cli3脚手架搭建,最后我将项目分为以下层级

├─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

5db24f02a6c257b00c8c5c5046a82ff1.png

然而在我优化系统之后,打包后的体积只有2M,缩小了8倍

b6a116c2b1e6d1a5a49e2e2c14024ddd.png

这里我从以下4个方面分享一下我在项目中是如何改善系统的性能,让系统"步履如飞"的

  • 网络请求相关

  • 构建相关

  • 静态资源优化

  • 编码相关

网络请求相关

这部分旨在实现需求的前提下尽量减少http请求的开销,或者减少响应时间

CDN

将第三方的类库放到CDN上,能够大幅度减少生产环境中的项目体积,另外CDN能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上

另外因为CDN和服务器的域名一般不是同一个,可以缓解同一域名并发http请求的数量限制,有效分流以及减少多余的cookie的发送(CDN上面的静态资源请求时不需要携带任何cookie)

通俗的来说就是使用CDN会一定程度上提升项目中的静态文件的传输速度,在vue-cli3中可以通过externals配置项,将第三方的类库的引用地址从本地指向你提供的CDN地址

externals只适用于ES Module的默认导入

ae5aba69ea0b2791ed08fdc1bd11f09e.png

这里通过环境变量来判断生产环境才启用CDN,除了需要开启CDN外,你还需要在index.html注入CDN的域名,所以我这里通过html-webpack-plugin根据cdn域名动态的注入script标签,同时需要在index.html中通过模版的语法声明循环的数组和注入的元素

打包前的index.html:

c23f8ad8d2471571e6a293423eef1de1.png

打包后的index.html:

968eef492199aa375fb74c616419e6e9.png

可以看到通过这个插件可以将cdn域名动态的注入到打包后的index.html中

还有一点要注意的是,externals对象的属性为你引入包的名字,而属性值是对应的全局变量名称(CDN引入的类库文件会自动挂载到window对象下面,而挂载时的属性名需要去对应的CDN在源码中寻找,一般在开头行都会有声明,除此之外导入还有困难的还可以看下这篇博客webpack externals 深入理解)

c7b76492fb624e2493e038f9851fe2d8.png

这里还是建议尽量放到公司专用的CDN上,不推荐使用公共的CDN,因为容易挂,生产环境还是以稳定为主吧

合理的缓存策略

将长时间不会改变的第三方类库或者静态资源设置为强缓存,将max-age设置为一个非常长的时间,再将访问路径加上哈希达到哈希值变了以后保证获取到最新资源(vue-cli3会自动构建,自己搭建的webpack脚手架需要自行配置contentHash,chunkHash)

CDN上的缓存策略,可以看到max-age的值非常大,这样下次访问就只会读取本地磁盘/内存中缓存的资源:

6fe4e02405c15386a7b7c4ee5d0efde7.png

对于index.html和一些图片等多媒体资源,可以选择协商缓存(max-age<=0,Last-Modified,ETag),保证返回服务器最新的资源

一个好的缓存策略,有助于减轻服务器的压力,并且显著的提升用户的体验

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后缀的压缩文件

c8656132c24836dbd7a63d99a6cbb069.png
65020b787fc3e7c97a32021703965991.png

这样不需要服务器主动压缩我们就已经可以得到gzip文件,在上面的nginx配置项中可以发现这一行

 #nginx对于静态文件的处理模块,开启后会寻找以.gz结尾的文件,直接返回,不会占用cpu进行压缩,如果找不到则不进行压缩
    gzip_static on

只要把.gz的文件放到服务器上,开始gzip_static就可以让服务器优先返回.gz文件,在面对高流量时,也能一定程度减轻对服务器的压力,属于用空间来换时间(.gz文件会额外占有服务器的空间)

资源嗅探

对于现代浏览器来说,可以给link标签添加preload,prefetch,dns-prefetch属性

preload

对于SPA应用来说,当浏览器解析完script脚本才会生成DOM节点,如果你的项目中没有使用服务端渲染的话且需要加载一个比较耗时的首屏图片时,可以考虑将这个首屏图片放在preload标签中让浏览器预先请求并加载执行,这样当script脚本执行完毕后就会瞬间加载图片(否则需要等脚本执行完毕后再向后台请求图片)

另外使用preload预加载首屏需要的css样式也是一个不错的选择,类似的库有critical

没有使用preload

e4e9c0cb7a3c5b7d31361d6cbfcc87b2.png

使用preload

5321a6b0b06a77ce33303f61f5fd88e1.png

通过Waterfall可以看到这个webp图片需要等到脚本加载完之后才回去请求,如果这个图片比较大就会浪费不必要的时间

在工程中,利用一些preload的webpack插件可以很方便的给打包后的index.html注入预加载的资源标签,有兴趣的朋友可以试着搜索一下相关的插件

prefetch

prefetch可以让浏览器提前加载下个页面可能会需要的资源,vue-cli3默认会给所有懒加载的路由添加prefetch属性,这样可以在你访问使用到懒加载的路由页面时能够获得更快的加载速度

preload和prefetch的区别在于,preload的资源会和页面需要的静态资源并行加载,而prefetch则会等到浏览器加载完必要的资源后,在空闲时间加载被标记为prefetch的资源

dns-prefetch

dns-prefetch可以让浏览器提前对域名进行解析,减少DNS查找的开销,如果你的静态资源和后端接口不是同一个服务器的话,可以将考虑你后端的域名放入link标签加入dns-prefetch属性

京东首页也使用到了dns-prefetch技术

2256561dfea892774717ba3a24397227.png

http2

http2从2015年问世以来已经走过了4个年头,如今在国内也有超过50%的覆盖率,得益于http2的分帧传输,它能够极大的减少http(s)请求开销

http2和http1.1的性能差异对比

如果系统首屏同一时间需要加载的静态资源非常多,但是浏览器对同一域名的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

d5e38b7ac857980dee4e647b7a30998b.png

构建相关

构建方面通过合理的配置构建工具,达到减少生产环境的代码的体积,减少打包时间,缩短页面加载时间

路由懒加载

传统的路由组件是通过import静态的打包到项目中,这样做的缺点是因为所有的页面组件都打包在同一个脚本文件中,导致生产环境下首屏因为加载的代码量太多会有明显的卡顿(白屏)

通过import()使得ES6的模块有了动态加载的能力,让url匹配到相应的路径时,会动态加载页面组件,这样首屏的代码量会大幅减少,webpack会把动态加载的页面组件分离成单独的一个chunk.js文件

f139c403c8c08aac255daf2ba0c37a25.png
aaa7df1c5b4394fcacf6b71ee566209e.png

当然懒加载也有缺点,就是会额外的增加一个http请求,如果项目非常小的话可以考虑不使用路由懒加载

预渲染

由于浏览器在渲染出页面之前,需要先加载和解析相应的html,css和js文件,为此会有一段白屏的时间,如何尽可能的减少白屏对用户的影响,目前我选择的是在html模版中,注入一个loading动画,这里我拿D2-Admin中的loading动画举例

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="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">
          <imgclass="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">
          <ahref="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搭建的

DllPlugin

当没有一个稳定的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后在生产环境也只会引入这一部分的代码

38231fb3b91235ceba09037a31a77915.png

对于UI库(element-ui)打包后的体积也会非常大,尽量使用按需加载,官方文档上也有详细教程

element-ui的压缩后的体积竟然是Vue的十倍

c42fa2acd2edf4a7e01a99e41352d864.png

常用的路径创建文件别名

给常用的模块路径创建一个别名是一个不错的选择,可以减少模块查找时耗费的时间,项目越大收益也就越明显

vue-cli3中的配置和使用方法(webpack链式调用文档)

03cc817bf3545de0c6e93f55d20659b3.png

使用可视化工具分析打包后的模块体积

我通过webpack-bundle-analyzer这个插件在每次打包后能够更加直观的分析打包后模块的体积,再对其中比较大的模块进行优化

ecb205861cffae95b3bffa8110a21e3b.png

这是我在优化前的各模块体积:

49ca52606921e0302ed3c9d47f1acfbd.png

因为业务需求,要求前端导出pdf和excel文件,我这里引入了xlsx和pdf.js这2个包,但是打包后通过可视化工具发现光着2个文件就占了一半的项目体积,另外elementui和moment也非常的大

ab9a07f48e04c098487e7f3eb0457a12.png

这是优化后通过可视化工具观察到的各模块体积,通过将这些类库放到CDN上或者使用dllPlugin将类库和业务文件分离,可以看到没有明显特别大的模块了

静态资源优化

这部分旨在减少请求一些图片资源所造成的影响

图片懒加载

如果你的系统是一个偏展示的项目需要给用户展示大量图片,是否启用图片懒加载可能是你需要考虑的一个点,不在用户视野中的图片是没有必要加载的,图片懒加载通过让图片先加载成一张统一的图片,再给进入用户视野的图片替换真正的图片地址,可以同一时间减少http请求开销,避免显示图片导致的画面抖动,提高用户体验

下面我提供2种图片懒加载的思路,这2个方案最终都是用将占位的图片替换成真正的图片,然后给img标签设置一个自定义属性src存放真正的图片地址,src存放占位图片的地址

getBoundingClientRect

DOM元素包含一个getBoundingClientRect方法,执行该方法返回当前DOM节点相关的CSS边框集合

a65a0006571d7b6c9a91085005c0b012.png

其中有一个top属性代表当前DOM节点距离浏览器窗口顶部的高度,只需判断top值是否小于当前浏览器窗口的高度(window.innerHeight),若小于说明已经进入用户视野,然后替换为真正的图片即可

另外使用getBoundingClientRect作图片懒加载需要注意3点

  1. 因为需要监听scroll事件,不停的判断top的值和浏览器高度的关系,请对监听事件进行函数节流

  2. 当屏幕首次渲染时,不会触发scroll事件,请主动调用一次事件处理程序,否则若用户不滚动则首屏的图片会一直使用懒加载的默认图片

  3. 当所有需要懒加载的图片都被加载完,需要移除事件监听,避免不必要的内存占用

IntersectionObserver

IntersectionObserver作为一个构造函数,传入一个回调函数作为参数,生成一个实例observer,这个实例有一个observe方法用来观察指定元素是否进入了用户的可视范围,随即触发传入构造函数中的回调函数

同时给回调函数传入一个entries的参数,记录着这个实例观察的所有元素的一些阈值信息(对象),其中intersectionRatio属性表示图片进入可视范围的百分比,大于0表示已经有部分进入了用户视野

f7a9b663157d79c41f1046e2254bc91c.png

此时替换为真实的图片,并且调用实例的unobserve将这个img元素从这个实例的观察列表的去除

815a056bb53e83919ac80b0e75ac8d5c.png

实例代码

对懒加载还有迷惑的同学我这里写了一个DEMO可以参考一下实现的方式源代码

结论

这2种的区别在于监听的方式,我个人更推荐使用Intersection Observer,因为通过监听scroll事件开销比较大,而让将这个工作交给另一个线程异步的去监听开销会小很多,但是它的缺点是一些老版本的浏览器可能支持率不高,好在社区有polyfill的方案

或者可以直接使用第三方的组件库vue-lazyload

使用svg图标

相对于用一张图片来表示图标,svg拥有更好的图片质量,体积更小,并且不需要开启额外的http请求,svg是一个未来的趋势,阿里的图标库iconfont支持导出svg格式的图标,但是在项目中需要封装一个支持svg的组件,具体封装的教程可以参考花裤衩的文章这里就不多赘述了手摸手,带你优雅的使用 icon,或者可以参考我的github

使用webp图片

webp图片最初在2010年发布,目标是减少文件大小,但达到和JPEG格式相同的图片质量,希望能够减少图片档在网络上的发送时间。webp图片无损比png图片无损的平均体积要小 20%~40%,并且图片质量用肉眼看几乎没什么差别

webp图片的缺点是兼容性并不是那么的好,在can l use 上查到webp图片的支持率并不是那么的理想。但是我们仍可以在支持webp图片的浏览器中使用它,而在不支持的浏览器提供png图片

237f786d0d89b50e723e1aec00fb8c71.png

这里需要使用到响应式图片,HTML提供了picture标签让我们可以在不同设备中使用不同的图片格式

MDN:

HTML元素通过包含零或多个元素和一个  元素来为不同的显示/设备场景提供图像版本。浏览器会选择最匹配的子元素,如果没有匹配的,就选择  元素的 src 属性中的URL。然后,所选图像呈现在元素占据的空间中。

在工程中我们可以这样使用

42aa187ef9cd9e4721d692eb87625902.png

picture标签包裹2个source标签,一个提供webp图片,通过srcset属性让浏览器从上到下选择可以支持的图片格式,如果浏览器不支持webp图片会只使用第二个source,会回退到png图片,如果浏览器不支持picture标签,会使用底部的img标签,同样也会生成一个png图片

picture标签的浏览器支持率,相对于webp要好很多(注意底部的img标签无论如何都要有,否则就算支持webp图片也无法渲染出图片)

a5ae2a6a69965e7ec47103aa3cb8a393.png

压缩图片

对于一些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导入,它可以扫描你指定的文件,然后全部导入到指定文件,可以用在

  • vue-router的路由自动导入

  • vuex的模块自动导入

  • svg图标的自动导入

  • 全局组件的自动导入

vuex:

63818933ca7cafad08dc282936154ab1.png

这样在创建一个新的模块时,不需要在index.js中用import引入,在modules中再声明一遍了

全局组件和svg图标:

e69e222afcd5f531dfaac3f231c09e53.png

避免了每创建一个全局组件都需要引入,在调用一次Vue.component的过程,而当加载到Svg组件会自动扫描icons文件夹,将所有svg图标导入进来

封装组件

有人会说,github上那么多好的开源组件,好的开源库放着不用,为啥自己还要折腾呢?

其实我认为自己动手封装一个组件还是很有意义的,因为如果是从零开始编写的组件,你能够更好的掌握自己组件的所有功能,并且还能根据公司的业务需求定制一些特殊的功能,除此之外,理解一个组件内部的实现机制也有助于提升个人的编码能力,而不是别人问起来你只知道我用过某个组件,很好用,但是不知道是怎么做到的。所以我还是比较推荐去尝试编写几个常用的组件

因为是后台管理系统,核心的组件肯定是表单组件和表格组件,公共组件是基于element组件的二次封装,组件的设计遵循以下的思路

  • 高内聚低耦合,尽可能少的暴露组件的api,将功能尽量封装在组件内部

  • 组件内部根据业务需求设置了一些组件默认的配置项,另外再通过不同页面传入不同配置项提高组件的通用性

设计组件的目的就是让组件进一步解耦,将配置项和模板标签分离,一方面是减少在业务逻辑组件中的代码量,另一方面就是单独抽离的配置项使得能够通过后台动态传递给前端,或者自己建一个配置项的js/ts文件(如果有规范的开发者文档还可以使用nodejs编写一个读取开发者文档一键写入配置项的脚本,进一步提升开发效率)

表格组件

表格组件设计大致分为以下几个部分

  • 尽量贴近element组件库的api

  • 传入一个表头的配置项数组,通过这个配置项动态生成el-table-columns标签

  • 交互复杂的表头列的解决方式

尽量贴近element组件库的api

组件中使用了

不能识别此Latex公式: attrs,
listeners实现属性和监听事件的跨级传递,使得在页面中给自定义组件中的传入的属性能够通过自定义组件内部的转发直接成为el-table标签的属性,达到跨级的属性传递,而
不能识别此Latex公式: listeners和
attrs类似,能够监听el-table组件中触发的事件,将事件转发   到页面中的自定义组件上

自定义组件:

13036f55bba6f738989dbf5ec8a82936.png

这样做的目的就是能让你的自定义组件和el-table组件有相同的用法,传入的属性,监听的事件也是相同的

在页面中使用自定义组件,可以看到z-table和el-table的用法几乎是相同的,只需要额外传入一个columns的属性:

fa8fe299db252dee13006e13577c4c87.png

表头的配置项设计

继续给这个表格组件添加表头标签,这里我把一些不必要的判断都去除了,只留下了核心的逻辑,实际在组件内部只需要循环这个配置项动态生成el-table-column标签就可以了

自定义组件:

6747b2eb9ee8dc2f33990a3e603cf7b8.png

配置项文件:

accdbc1ac6ce8963b3ca6be90bc1d363.png

这里的核心是在于这个v-bind,当v-bind后面等号里放入的是一个对象时,它会遍历这个对象的所有属性,将属性和值一一做绑定

什么意思呢?这里结合配置项文件来说明,如果传入上述的配置项,组件的内部实际是这样子的

c464eaec5377244f0d82ed39d7bea761.png

抛开key不谈,在配置项的每个元素中暴露一个attrs属性,里面保存了所有el-table-column标签可以接受的属性。例子中label,prop,width这3个属性是在配置项每个元素的attrs属性中的,通过v-bind="column.attrs"让这3个属性和它们的值分别在el-table-column标签中做了绑定,从而达到了模板和配置项解耦的目的

交互复杂的表头列的解决方式

对于一些需要特别处理的表头列的数据,我在组件内部利用插槽和作用域插槽,通过插槽定义表头列的插入位置,再通过作用域插槽将信息返回给父组件,在父组件中定义如何显示,使得表格组件非常的灵活能够应对大部分业务需求

1760a41787cbaf596523402a586036ef.png

可以看到具名插槽的名字也是通过配置项传入的,并且作用域插槽将整个表单内部的数据通过scope传给父组件,在复杂的业务场景,无法通过配置项解决问题的时候,通过插槽和作用域插槽让父组件去决定如何去处理数据

配置项中添加插槽属性:

ec6aa17beceebe3c13ad7aa1d13461d5.png

页面组件:

aa01cf0e0ee075c1392775989dc8617e.png

在页面组件中,可以和element提供的作用域插槽的使用方式相似,通过scope可以访问到组件内部的所有数据并且交给页面组件去做复杂的逻辑处理

其他功能

针对公司的需求,我对组件做了进一步的改造

  • 使用render函数使得表头显示能够更加灵活

  • 配置项暴露一个函数能够让当前列的数据执行这个函数达到预处理的效果

  • 配置项中设置一个二维数组,能够让数据字段组合,达到数据显示在不同的行数的效果

  • 添加了操作图标

  • 添加了数据(code码)转对应中文语义的功能

源代码

表格组件

表单组件

表单组件相对于表格组件在实现方面要困难一点,因为表单的控件非常多,每个配置项又需要非常灵活,这里我借鉴了之前在知乎看到的一篇博客,文章中虽然没有把代码列出来,但是罗列了整体的实现方案,随后我根据文章中的思路设计了这个表单组件

设计大致分为以下几个部分

  • 表单配置项设计

  • 表单验证

  • 表单请求

  • 表单控件之间的联动

  • 调用后端接口生成表单控件的选项

表单配置项设计

根据上面的表格组件的封装思路,还是利用

不能识别此Latex公式:
attrs做根元素属性的传递,用v-bind在配置项中设置组件内部的属性

表单组件:

fcc05083ccbfc487c8a6c2a46387c64b.png

配置项文件:

120a42f639916a347dea565075ef4a04.png

和表格组件不同的是,因为表单组件分为el-form-item标签和表单控件2部分,这2个部分都需要在配置项中对应配置属性,在配置项中使用itemAttrs控制el-form-item标签的属性,使用attrs控制表单控件的属性

这里还用到了component标签,通过配置项的tag标签动态生成el-input的表单控件,但是可以看到这里我并没有直接将tag的值设为el-input,那input是如何变成el-input的呢?

这里我又定义了每个组件通用的配置项,使得不需要每次都在组件的attrs中声明一些重复的属性,比如placeholder,clearable等

通用配置项文件:

dc9abc1d1104dc668a0d794186c31413.png

最重要的是我建立了组件配置项和通用配置项之间的关联,通过组件配置项中的tag属性找到通用配置项对应的对象,结合上面的例子如果tag的值是input,那就会从通用配置项中找到input属性对应的对象,并且将真实的tag值指向通用配置项的component,这里就是el-input

而这种关联又是怎么建立起来的呢,其实还是用了Object.assgin做了对象之间的合并

核心逻辑如下(参数formItem指的是组件配置项formItems中的每个元素):

0dc9dab00d837b95623c8c23bb7bccb8.png

这里定义了一个computeFormItem的函数,通过传入配置项数组的每个元素,根据元素的tag值找到通用配置项(basic对象)中相应的值,随后用了Object.assgin做了合并,关于这个computeFormItem函数我稍后在后面的表单控件之间的联动中会详细去讲

通用和组件配置项都有了,接下来要实现的是表单组件需要上传给后端的数据对象

这里我的思路是通过配置项中声明的字段名(key)动态生成数据对象,这样可以减少传入的配置项的数量,在组件内部声明Model变量保存数据对象

但是这里有2点需要注意

因为组件内部声明的Model是一个空对象,Vue的响应式系统是监听不到对象创建了新的属性,需要使用set来设置,使得能够强制更新视图

这里需要通过formItems对Model数据对象赋值,如果放在mounted钩子中执行的话拿到的是一个空数组,所以我这里使用watch来监听formItems,并且使用immediate立即执行(用computed声明一个新数组理论上也可以)

253ab09c25e53bbec96b9006594e8ad9.png

表单验证

表单验证方面尽量贴合element组件的传入方式,保持所有在el-form-item标签中写的属性都写在itemAttrs中,所有在表单控件中写的属性都写在attrs中,所以可以在itemAttrs中编写表单验证方面的逻辑

38a1341913cd8d56f7034023125f20ff.png

表单请求

表单请求方面,因为在重构时新建了api文件夹,存放的是一个个后端接口的api函数,做到一个页面对应一个api文件夹中的一个接口文件

575cc02bf8dbd7906f290135e8c9c2bb.png

每个接口文件中可以导出多个接口的函数

c45367776ba8e0a6258a1e46cce64bfa.png

在表单组件中只需要声明一个api的props让页面组件传入就可以了

0ca91800eae3ef698c5284d9744b0ed5.png

随后给提交按钮绑定click事件,进行表单验证最后执行接口函数,传入Model这个数据对象即可

9db5c67564ae21613a47cadb3559eef8.png

在接口函数调用成功返回响应数据后,我这里通过触发after-submit的事件让页面组件监听这个事件,并且把响应数据传给页面组件,这样页面组件就能拿到响应的数据并且做一些处理了

页面组件监听after-submit事件:

cbf32e61a56cdd354e1c6df43a2a0d92.png

表单控件之间的联动

这一部分我认为也是最难实现的,在日常的业务需求中可能需要某个控件控制另外一个控件显示与否

核心的思路就是在配置项中定义一个getAttrs的函数,这个函数根据当前Model,也就是数据对象中的某个值动态的生成一个attrs对象,最后将这个attrs对象通过合并到当前配置项的attrs中,另外还定义了一个ifRender函数,可以控制表单控件是否被渲染,最后我们的配置项可能长这样

a2471073bc0b249854ac5a64ee3e67ac.png

接下来表单组件内部要实现如何执行这2个函数,依旧是之前的computeFormItem这个函数,它用来计算出当前表单组件的配置项

eed78fff9ccead103fd70be7ad442f5f.png

和上面的computeFormItem函数不同的是,我这里传入了第二个参数Model,使得当前的表单组件配置项能够根据Model动态的变化,在内部执行getAttrs函数传入这个Model,返回的对象通过Object,assgin合并到当前的配置项中,而对于另一个ifRender函数,也传入Model,返回一个Boolean值,最后用这个Boolean值在模版中通过v-if控制是否渲染表单控件

32bd7f93c4c4f1cc38c06eeedc82d752.png

这里要分析一下整个表单最核心的部分:computeFormItem函数,它的作用是根据当前Model中的数据变化,动态的生成一个新的配置项,因为我们的表单控件是根据配置项映射而成的,需要改变表单控件只能去修改配置项

根据上面那个图可以发现,我们并没有直接使用页面组件传来的formItems配置项,而是根据_formItems循环渲染的标签,而_formItems是基于formItems并且经过computeFormItem生成的配置项,只要Model中的数据改变,这个配置项就需要重新计算生成新的值,所以我选择把_formItems放在计算属性中

26189b94537bb09c20df7c5ed40e387c.png

这样,只要依赖项(这里是Model和formItems)变了,就会触发函数重新计算出新的_formItems

下拉框/单选框/复选框

在表单组件中,我使用component标签动态生成表单控件,但是对于一些有子节点的表单控件通过component实现就有些困难,这里我将含有子节点的组件(下拉框/单选框/复选框)又进行了一层封装,消除了子节点,让所有属性都在component这一层配置

自定义select组件

b8c9fa91efe3d9e6a388308ec26d8d69.png

这样以来只要在配置项中声明一个options属性,通过component标签将组件转为自定义的select组件,让options属性就会变为select组件的属性,这样在自定义的select组件内部可以通过$attrs.options获取到它(这里注意value,label必须都要显式的声明否则会报错,因为element组件内部会对传入的属性验证)

组件配置项文件:

f4d63e04b86e482145f6ca5c7c1da588.png

这里再次利用通用配置项文件,将组件配置项中声明的select组件的配置项映射到自定义的select组件中

2cd79ec8bb9fc2959085ea7bfd0c109f.png

调用后端接口生成表单控件的选项

在真实的业务需求中,部分下拉框,单选框的选项是通过拉取后端的接口生成的。放在表单组件中的话还是需要修改配置项,在页面组件中修改formItem。找到下拉框/单选框的key,将接口的数据赋值给options属性

ac570959477fb6e620f5a92af8553c55.png

总结

可以看到表单组件还是比较复杂的,其实这个表单组件相对于表格组件来说还是有一定的局限性,后续可能会给它设计插槽的功能。另外真实的业务需求肯定是更加复杂多变的,不管怎么说,一些交互逻辑不是特别复杂的表单这个组件还是能hold住的,本人能力有限,这里也只是给一个思路,希望后续能够愈发完善

推荐阅读

三年 Git 使用心得 & 常见问题整理「1.3万字」「 墙裂推荐」互联网人必备GIF制作的14种选择「 Map最佳实践」什么时候适合使用 Map 而不是 Object

459e7920af76d7645d0ec85f2b5177c5.png

参考资料

vue-element-adminD2 Admin嗨,送你一张Web性能优化地图vue-cli3 项目从搭建优化到docker部署前端性能优化不完全指北加快Vue项目的开发速度再也不想写表单了

ff842ae3ae096e3dbc92a2cecad37a4f.gif

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/526379.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

学计算机买电脑显卡1605ti够吗,铭瑄GTX1660Ti显卡值得买吗 铭瑄GTX1660Ti终结者显卡评测...

铭瑄GTX1660Ti显卡怎么样&#xff1f;值得买吗&#xff1f;下面小编带来铭瑄GTX1660Ti终结者显卡评测&#xff0c;希望对大家有所帮助。铭瑄GTX1660Ti终结者显卡评测&#xff1a;2019年1月7日&#xff0c;英伟达发布了价格更加实惠的甜品级RTX显卡RTX2060&#xff0c;玩家们对于…

windows无法新建计算机对象,教您activex部件不能创建对象怎么解决

我们有时候在运行ASP程序的时候&#xff0c;会出现提示ActiveX部件不能创建对象&#xff0c;出现这个问题的原因&#xff0c;有可能是服务器系统重装了。那么当我们遇到电脑出现ActiveX部件不能创建对象怎么办&#xff1f;下面&#xff0c;小编给大家准备了activex部件不能创建…

全国计算机三级数据库考试题型,2015年全国计算机三级考试《数据库》测试题及答案...

2015年全国计算机三级考试《数据库》测试题及答案1.假定学生关系是S(S#&#xff0c;SNAME&#xff0c;SEX&#xff0c;AGE)&#xff0c;课程关系是C(C#&#xff0c;CNAME&#xff0c;TEACHER)&#xff0c;学生选课关系是SC(S#&#xff0c;C#&#xff0c;GRADE)。要查找选修 &qu…

七七计算机论文网,qepipnu

标签&#xff1a;长句英语基本单位毕业设计毕业论文杂谈英语毕业论文On the Techniques of Translating English LongSentences如有需要请联系 bstractTranslation is the most important means of communication betweendifferent countries and cultures&#xff0c;in which…

新系统如何测试软件,怎样检测电脑能否升级到最新的Windows11系统?官方检测工具帮你一招搞定!...

日前&#xff0c;微软正式发布了Windows11系统&#xff0c;新的Windows11操作系统不仅采用了全新的居中设计的开始键以及任务栏&#xff0c;同时还取消了Windows8引入的实时磁贴设计&#xff0c;取而代之的是最近文档、应用程序以及单独的搜索界面。再加上Windows11带来的全新的…

服务器个别目录下不能新建文件夹,域服务器不能创建sysvol和netlogon共享文件夹...

请教个问题。我这里有两台win2003sp2的DC&#xff0c;ip&#xff1a;172.16.1.2(PDC)和172.16.1.3(BDCexchange2003sp2)&#xff1b;我发现我们的域服务器有问题&#xff0c;之前目录复制一直不成功&#xff0c;172.16.1.2(主域)的目录数据不能复制到172.16.1.3(bdc)&#xff0…

长安渝北工厂机器人_长安UNI-T智能工厂:机器人、激光焊, 一分钟一台车

生产线上没有人山人海、焊花四溅&#xff0c;误差被控制在0.2毫米、机器人边作业边实时检查……生产UNI-T的长安汽车重庆两江工厂今天(4月21日)下午在线展示其制造智能化、工厂数字化、工人年轻化。冲压车间工作人员介绍&#xff0c;UNI-T车身材质与宝马、奔驰一样&#xff0c;…

matlab 计算汉明距_matlab实现滑动平均滤波

什么是滑动均值滤波滑动平均滤波就是把连续取得的N个采样值看成一个队列&#xff0c;队列的长度固定为N&#xff0c;每次采样得到一个新数据放到队尾&#xff0c;并丢掉原来队首的一次数据&#xff0c;把队列中的N个数据进行平均运算&#xff0c;就可以获得新的滤波结果。具体的…

个人家用nas_NAS不会用?NAS真的很难操作吗?可能是你没选对!

在这个大数据时代&#xff0c;越来越多的小伙伴们发现&#xff0c;无论手机容量如何扩充升级&#xff0c;还是无法满足越来越多资料的存储和备份。于是不少人有了想搭建NAS私有云的想法&#xff0c;配备了一台超大容量的NAS私有云&#xff0c;不仅可以即时备份电脑以及手机等设…

我的世界服务器怎么修改合成表,《我的世界》1.8原版自定义合成表教程 怎么自定义合成表...

《我的世界》1.8原版自定义合成方法&#xff0c;很多玩家还不了解&#xff0c;今天给大家带来玩家“真名”分享的《我的世界》1.8原版自定义合成表教程&#xff0c;一起来看看吧。版本要求1.8优点&#xff1a;自定义合成表数量可以很大合成表可以很复杂没有名字、NBT不会合成自…

postgresql定义访问ip与用户_Postgresql-12.1最新版本在线安装以及配置使用全流程

Postgresql-12.1最新版本在线安装以及配置使用全流程 Postgresql-12.1简单介绍 PostgreSQL 12.1 已经发布&#xff0c;该版本在各方面都得到了加强&#xff0c;包括显著地提升查询性能&#xff0c;特别是对大数据集&#xff0c;总的空间利用率方面。 这个版本主要几大特点如下 …

construct2 ajax,Construct2/3

我们常在游戏中见到各种各样的剧情对话&#xff0c;电子游戏从最早的RPG类对话框演变至今&#xff0c;已经产生了无数种解决方案。但总的来说&#xff0c;常见的对话分为两类&#xff1a;最常见的galgame对话&#xff0c;可以看到较大的立绘图表现人物的表情动态(甚至动画)&…

tcp中的crc检验算法原理_在数据传输过程中的CRC 算法的简单说明

CRC校验(循环冗余校验)是数据通讯中最常采用的校验方式。在嵌入式软件开发中&#xff0c;经常要用到CRC 算法对各种数据进行校验。因此&#xff0c;掌握基本的CRC算法应是嵌入式程序员的基本技能。可是&#xff0c;我认识的嵌入式程序员中能真正掌握CRC算法的人却很少&#xff…

和氟西汀类似的备注_撒狗粮:可爱又霸气的给男朋友的微信备注

在微信里&#xff0c;你的男朋友是怎么被你备注的&#xff1f; 是“老公”&#xff0c;“儿子”&#xff0c;还是“死鬼”&#xff1f; 看看各地网友们的精彩备注吧&#xff01;——————————————————————小垃圾大宝贝小闹闹周少爷chou狗软蛋蛋小可爱免费鸭…

ogg 查看某条更新_明道云Web 6.1更新:日历视图上线

更新时间&#xff1a;2020年12月22日&#xff08;周二&#xff09;19点预计时长&#xff1a;2小时是否停服&#xff1a;否版本代号&#xff1a;Web 6.1&#xff08;移动端需同步强制更新&#xff09;主要更新功能日历视图日历视图让用户能从时间维度排列并管理业务数据&#xf…

服务器的可维护性,可靠性和可维护性

可靠性和可维护性可靠性一直是戴尔服务器产品线的一大亮点&#xff0c;R515也不例外。如内部结构所示&#xff0c;当你打开R515机箱的时候&#xff0c;你可以很明显地看到风扇的数量、分布的各个组件和双电源机箱。你也可以感觉出从中取出各个组件和拆装机箱都十分简便。配合低…

服务器缺少storportSYS文件,Windows操作系统蓝屏日志分析方法

或许你可以先检测下机器硬件健康状况&#xff0c;详情点击查看检测教程工具&#xff1a;X64 Debuggers And Tools-x64_en-us 下载地址&#xff1a;链接&#xff1a;http://pan.baidu.com/s/1cAO2ey 密码&#xff1a;9oms源文件&#xff1a;DMP蓝屏日志 &#xff0c;文件目录查看…

合并工具_你值得拥有这个PDF合并工具 免费获取转换方法

合并PDF文件&#xff1f;很多人在日常生活中经常会需要使用到合并文档的要求&#xff0c;但是不是所有的人都能够很熟练的去合并我们生活中常用的文件&#xff0c;例如PDF&#xff0c;例如Word和Excel等等&#xff0c;其实只要我们学会了PDF合并的方法&#xff0c;以上文件的合…

事物与持久化_跟面试官侃半小时MySQL事务,说完原子性、一致性、持久性的实现...

提到MySQL的事物&#xff0c;我相信对MySQL有了解的同学都能聊上几句&#xff0c;无论是面试求职&#xff0c;还是日常开发&#xff0c;MySQL的事务都跟我们息息相关。而事务的ACID(即原子性Atomicity、一致性Consistency、隔离性Isolation、持久性Durability)可以说涵盖了事务…

cookie里面用到的关键字_晓龙吊打面试官系列:synchronized关键字入门(同步方法与同步代码块)...

文章目录一、 线程安全问题二、synchronized简介1) 原子性2) 可见性3) 有序性4)可重入1. 什么是synchronized2.什么是同步3.synchronized的特性4.synchronized的实现原理(了解即可)三、synchronized的用法1. 同步方法2. 同步代码块四、对象锁和类锁1)对象锁2)类锁1.对象锁的探索…