首页 文章 精选 留言 我的

精选列表

搜索[微信云支付],共10000篇文章
优秀的个人博客,低调大师

前端开发之函数式编程实践 | 京东云技术团队

作者:京东科技牛志伟 函数式编程简介 常见应用场景 1、ES6中的map、filter、reduce等函数 [1,2,3,4,5].map(x => x * 2).filter(x => x > 5).reduce((p,n) => p + n); 2、React类组件 -> 函数式组件+hooks、Vue3中的组合式API 3、RxJS、Lodash和Ramda等JS库 4、中间件/插件,如Redux中的applyMiddleware中间件实现 const store = applyMiddleware(...middlewares)(createStore)(reducer, initialState)   什么是函数式编程 函数式编程是一种编程范式,它将程序抽象为函数和数据结构,通过函数调用来实现程序的功能,并且函数可以作为参数传递给其他函数。 在 JavaScript 中,函数式编程可以实现面向对象编程的一些功能,比如抽象、封装、继承和多态等。 它还可以使用高阶函数、柯里化、组合和延迟计算来实现函数式编程的功能。 函数式编程有哪些特性 函数是「一等公民」 • 函数可以当做参数传递给其他函数,也可以作为函数的返回值返回(高阶函数)。 惰性执行 • 惰性执行是指在代码中的某些函数调用,只有在它们的返回值被使用时才会被执行。 • 它利用了延迟计算的技术,使得函数只在被调用时才会执行,而不是在编写代码时就被执行。 • 这样可以提高性能,因为不需要无用的计算。 无副作用(纯函数) • 函数的执行不会改变程序的外部状态,也就是说函数的执行不会影响程序的其他部分。 • 因为它只是单纯的计算结果,而不会产生其他的副作用。 常见函数式概念 柯里化-currying 柯里化函数是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。 函数表达:f(a, b, c) => f(a)(b)(c) 简单实现(有兴趣的同学可以研究下Lodash和Ramda库中的实现) // 函数柯里化 function curry(fn, args){ args = args || [] return function(...params){ let _args = [...args, ...params] if(_args.length < fn.length){ return curry(fn, _args) } return fn.apply(this, _args) } } function sum(a, b, c){ return a+b+c } // 自由组合参数 const currySum = curry(sum) console.log(currySum(1)(2)(3)) //6 console.log(currySum(1)(2,3)) //6 console.log(currySum(1,2)(3)) //6 管道-pipe 管道pipe函数是一个高阶函数,它接受一系列函数作为参数,将函数串联起来,一步步将上一步的输出作为下一步的输入,这样可以更加高效地处理复杂的业务逻辑。 函数表达:pipe(f, g, t) => x => t(g(f(x)),进一步结合curry可以实现pipe(f)(g)(t) => x => t(g(f(x)) 借助reduce简单实现,支持异步和非异步函数 export const pipe: any = (...fns: Promise<Function>[]) => (input: any) => fns.reduce((chain: Promise<Function>, func: Function | Promise<Function> | any) => chain.then(func), Promise.resolve(input)); 组合-compose 组合compose指的是将多个函数组合成一个函数,这样一个函数的输出就可以作为另一个函数的输入,从而实现多个函数的链式调用。 组合compose可以提高代码的可读性和可维护性,减少重复代码的出现,更加便捷的实现函数的复用。 函数表达:compose(f, g, t) => x => f(g(t(x)),进一步结合curry可以实现compose(f)(g)(t) => x => f(g(t(x)) 借助reduceRight简单实现,和pipe的区别只是运算顺序不同 export const compose: any = (...fns: Promise<Function>[]) => (input: any) => fns.reduceRight((chain: Promise<Function>, func: Function | Promise<Function> | any) => chain.then(func), Promise.resolve(input)); 函数式编程实践 需求背景介绍 AgileBI在线报表是一款报表应用工具:易学易用,零代码,通过简单拖拽操作,制作中国式复杂报表,轻松实现报表的多样展示、交互分析、数据导出等需求, 在线体验 已有功能:在线报表中的每个单元格都可以配置相关的属性:比如扩展方向、父格、单元格格式、过滤条件、条件属性等  新增需求:需要支持批量设置单元格,其中【文本类型】单元格支持扩展方向、父格的设置;【字段类型】、【公示类型】单元格支持所有配置;  大致设计思路 1. 获取当前批量设置中,所有的配置项信息 2. 为每个配置项设计一个处理器(高阶函数):主要处理【批量设置的配置信息】和【当前单元格的配置信息】合并或替换逻辑 3. 通过管道的方式,加工每个单元格所有的配置项信息 核心实现 • pipe函数 private pipe = (...args: any) => { return (result: any, config?: any) => { return args.reduce((acc: any, fn: any) => fn(acc, config), result); }; }; • 高阶函数处理每个配置项 // 扩展方向替换 private handleExpand(expandConf: string) { return (conf: any) => { if (expandConf) { conf.expandDirection = expandConf; } return conf; }; } // 父行/父列替换 private handleParentCell(columnParentCell: any, rowParentCell: any) { return (conf: any) => { if (columnParentCell?.parentSelectType) { conf.columnParentCell = columnParentCell; } if (rowParentCell?.parentSelectType) { conf.rowParentCell = rowParentCell; } return conf; }; } // 条件属性追加 private handleCondition(conditionBatchConf: any) { return (conf: any) => { conf.conditionConf = this.mergeCondition(conf?.conditionConf || [], conditionBatchConf); return conf; }; } // 批量修改 private mergeCondition(c1: any, c2: any) { for (let j = 0; j < c1.length; j++) { // 批量删除 if ( c1[j]?.batchFlag && this.batchConf.conditionConf?.find((item: any) => item.uuid === c1[j]?.uuid) && !c2.find((item: any) => item.uuid === c1[j]?.uuid) ) { c1.splice(j, 1); } } for (let j = 0; j < c1.length; j++) { for (let i = 0; i < c2.length; i++) { // 如果字段已存在则替换 if (c2[i]?.uuid === c1[j]?.uuid) { c1.splice(j, 1); } } } return [...c1, ...c2]; } //... • 组合函数,获取每个单元格的最终配置信息 //... let handles: Array<any> = []; if (cell?.dataConf?.cellType === "文本") { handles = [ this.handleExpand(conf.expandDirection), this.handleParentCell(conf.columnParentCell, conf.rowParentCell) ]; } else if (cell?.dataConf?.cellType === "字段" || cell?.dataConf?.cellType === "公式") { handles = [ this.handleExpand(conf.expandDirection), this.handleParentCell(conf.columnParentCell, conf.rowParentCell), this.handleFormat(conf.dataFormatConf), this.handleFilter(conf.cellFilterConf), this.handleCondition(conf.conditionConf) ]; } if (handles.length > 0) { const mergeConf = this.pipe(...handles)(JSON.parse(JSON.stringify(cell.dataConf))); //... } 总结 1. 函数式编程可以可提高代码的可重用性,减少重复代码的开发时间; 2. 函数式编程可以提高代码的可读性,使得代码更容易理解和调试; 3. 函数式编程可以更容易实现函数组合,以帮助提高可维护性; 4. 组合优于继承;

优秀的个人博客,低调大师

京喜APP - 图片库优化 | 京东云技术团队

作者:京东零售 何骁 介绍 京喜APP早期开发主要是快速原生化迭代替代原有H5,提高用户体验,在这期间也积累了不少性能问题。之后我们开始进行一些性能优化相关的工作,本文主要是介绍京喜图片库相关优化策略以及关于图片相关的一些关联知识。 图片性能问题 作为电商APP,图片在各个业务场景被大量使用。我们需要做到尽可能降低网络消耗/内存消耗/硬盘消耗,同时不降低图片质量,提高图片加载速度,给用户带来更好的使用体验。基于这些性能目标,我们也通过初步性能评估梳理出了一些性能问题: 图片加载慢/流量消耗高 图片链接主要由后台接口下发,下发图片格式和尺寸由每个业务后台指定。部分业务没有使用更小的图片格式比如WebP,或图片尺寸过大,都会使图片过大导致网络消耗高。特别是网络状况不佳的场景,图片加载过慢给用户带来不好的体验。同时也会导致更多的I/O读写和解码耗时,造成更多的电量消耗。 图片内存占用高 经过初步的APP内存使用评估,图片内存消耗占APP总内存消耗的比例最高,特别是大尺寸图片会占用很多内存。一方面APP占用太高内存退到后台容易被系统杀死,导致下次打开重新启动影响体验。另一方面APP大量使用内存,容易被系统杀死产生OOM。特别是我们目前有大量的低端设备用户,设备内存相对比较低。 优化方向 基于上面分析出的一些性能问题,我们对图片框架进行了整体重构优化。一方面是降低图片网络传输,提高图片加载速度。另一方面是减少图片内存消耗。 最小化网络传输 京东图片服务器提供了多种处理功能,例如图片格式转换,图片降质,图片缩放,图片圆角等功能。这些功能通过在图片URL中添加特定参数实现,图片服务器会根据参数设置提前将图片处理完成并保存到CDN服务器。我们可以通过添加图片处理参数,减少图片传输大小。 虽然后台可以提前进行URL预处理,下发已添加过图片参数的图片URL。但是由于对接后台业务很多,每个业务图片参数设置差异很大无法统一,而且可能会造成性能影响,例如没有使用webP图片格式,下发太大的图片尺寸。同时考虑到推动各业务后台修改成本也很高,并且前端机型多,不同机型需要使用不同的图片尺寸。另外也不方便灰度降级功能,后续功能修改也不方便。所以在客户端进行图片URL预处理是更好的方式,可以统一控制,也方便之后功能更新。 图片URL预处理 图片库在网络图片加载前,检测是否是京东域名的图片URL。如果域名匹配,图片框架先对图片URL进行预处理,预处理包括域名统一,添加缩放参数,添加webP参数,添加降质参数的方式减少图片网络传输大小。 提示:因为后台返回的图片URL可能会带有一部分图片处理参数,例如https://img11.360buyimg.com/img/pingou-head/25.jpg!webp,直接追加图片参数可能会导致图片处理参数不生效,或格式错误导致加载失败。所以转换时会先将所有图片参数提前计算出来,之后一起处理,避免添加重复参数。 域名统一 目前图片服务器提供了多个图片域名可使用,例如m.360buyimg.com,img10.360buyimg.com等多个域名。m.360buyimg.com主要提供给移动端使用。但是由于对接了各种业务后台,导致接口会下发不同的域名图片。图片使用不同域名可能会导致以下问题: 不利于缓存复用- 图片框架通常默认以URL字符串生成图片缓存key,不同域名导致生成不同的缓存key。硬盘缓存无法复用会导致图片重复下载,内存缓存无法复用导致同样的图片占用多份内存。 不利于HTTP/2连接复用- 大部分界面图片比较多,很多场景都会同时加载多张图片,特别是首屏通常会加载几十张图片。当加载多个图片时,每个域名都需要重新建立HTTPS连接,经历DNS解析/TCP连接/TLS握手过程(目前一次HTTPS请求创建过程大概耗时50-150ms)。如果利用HTTP/2链接复用就只需要创建一次HTTPS请求,之后的图片请求可以减少这部分的耗时。 所以在预处理时,如果是京东域名的图片,将图片URL域名统一替换为m.360buyimg.com。 追加图片参数 图片缩放 很多业务后台返回的原始图片URL的size都比客户端实际显示的size要大。一方面导致使用更多的网络流量造成浪费。另一方面会导致占用更多内存。同时因为图片size和实际显示size不一致导致像素不对齐,GPU需要做额外的插值处理,也会一定的影响渲染性能。所以我们通过添加缩放参数的方式,指定图片服务器下发更小和更匹配实际显示size的图片尺寸。 动态scale计算尺寸 因为iOS设备主要使用2x/3x的分辨率,所以业务方使用API时需要传入对应的ptsize大小,图片库内部根据设备的scale进行动态计算出真实的像素宽高。 提示:android设备因为屏幕差异比较大,更适合使用固定的scale。太多的图片尺寸不利于CDN缓存,无缓存的时候需要对图片进行相关参数处理,图片处理本身是耗时操作。 Scale降级 低端机降级- 对于部分3xscale的低端设备,因为机器本身内存比较低,使用3x分辨率计算出来的图片像素宽高比较大,会造成更多的内存消耗以及解码/渲染更多的性能消耗。所以对于宽高超过一定要求的图片,降级到使用2x分辨率来计算像素宽高,减少设备性能消耗。 iPad降级- 因为目前APP并没有针对iPad做特定优化,所以iPad设备下默认是放大显示。这会导致在iPad下图片尺寸计算出来特别大。所以也是针对iPad图片尺寸做了特定限制,防止下发图片尺寸过大。 大图片降级- 正常情况下图片宽/高不应该超过屏幕宽/高。为了防止部分业务使用过大的图片size,所以添加了一个限制,最终生成的图片像素尺寸不能超过屏幕宽/高。 降质 图片服务器支持0-100的图片质量参数设置,通过降低图片质量可以减少图片大小,但是质量降低太多也会影响图片的观看体验。我们将图片质量参数设置为q70,指定图片服务器下发70%质量的图片。对于大部分业务,一方面可以大幅减少图片下载大小,同时也可以保证观看体验。通过添加图片降质参数至少可以减少30-40%的图片大小。 使用WebP 按照Google官方的数据,与PNG相比,WebP无损图像的字节数要少26%。WebP有损图像比同类JPG图像字节数少25-34%。图片服务器支持转换webP格式,可以减少图片大小。针对png/jpg图片格式,添加webP参数,指定图片服务器下发webp格式。虽然webP相比png/jpg图片解码需要更长时间,但相对网络传输速度提升还是很大。 提示:由于目前图片服务器并不支持GIF转webP,GIF并没有做处理。 URL预处理缓存 添加轻量缓存,提高URL转换性能。因为URL转换本身有一定的耗时,而且单个图片URL可能会多次加载/多次转换。转换后的URL会直接保存到缓存中,下次使用可以直接返回。缓存key由URL+相关图片转换参数拼接组成。 图片API设计 图片处理参数通过options设置,默认使用q70图片质量以及webP格式。业务方在调用加载图片方法时传入,下面是iOS端的API: imageView6.jx.setImage(url: URL(string: "https://img11.360buyimg.com/img/pingou-head/25.jpg"), placeholder: nil, options: [.imageSize(CGSize(width: 40, height: 40))]) 磁盘缓存优化 图片缓存查找优化 设置图片不同的size参数会导致更多的图片下载和磁盘缓存,例如同样一张图片100px、200px、300px尺寸因为URL不同会下载3次,同时缓存也无法不同。由于图片库通常默认使用URL作为图片缓存key,所以我们需要针对图片缓存key查找图片进行优化改造。简单来讲,相同的图片小size的图片可以直接复用更大size的缓存,这样当存在更大尺寸图片时,可以避免图片直接下载并且复用磁盘缓存。 降低图片内存消耗 png/jpg等图片格式在显示之前都需要经过解码生成一张位图,之后根据位图创建纹理传给GPU做渲染。一张位图的内存消耗大概是像素宽x像素高x位深。通常图片使用的是RGBA,位深为32位。一张500px_500px的大概1MB内存。对于GIF图片因为本身有多帧,所以最终的内存消耗为单帧内存x帧数。 我们的优化方向一方面是通过图片缩放的方式,减少图片位图的内存消耗。另一方面限制图片缓存上限避免缓存使用过高。 图片缩放 通过上面URL预处理过程让图片服务器下发更小的图片格式,已经降低了一部分内存。但是URL预处理只处理了jd域名的jpg/png图片,对于GIF或京东域名外的图片没有处理,包括一部分URL转换后加载失败的图片。所以对于这部分图片,我们会在端侧做图片缩放的处理,降低内存消耗。例如一张300px_300px包含100帧的GIF图片,实际显示区域只有50px_50px,优化后总内存消耗可从30MB+内存降低到3MB。 GIF动态帧率播放 之前根据线上监控数据发现,部分页面场景偶尔会配置尺寸大/帧数多的GIF图片,导致内存占用极高。例如一张500x400px播放200帧的GIF图片会占用100MB+内存消耗。所以针对这种场景,我们针对GIF做了减帧播放改造。当GIF图片总内存消耗大于一定量级时(例如图片内存缓存上线的20%),将GIF播放的帧数适当减少,每一帧的播放时间增加,这样可以将内存控制在一定范围之内。 提示:这里也可以通过 GIF 图片缓存 Buffer 控制内存总量,但是会导致更频繁的解码造成更多的 CPU 消耗。 图片内存缓存上限 图片缓存的设计目的是减少图片解码消耗。图片第一次使用的时候,将图片进行解码后的位图保存在内存中,这样可以避免下次使用时避免重复解码。虽然图片内存高可以尽量避免图片重复解码,但是占用太高内存也会导致APP后台被系统杀掉或产生OOM等问题。所以我们应该将内存缓存控制在一定范围内。 例如iOS的第三方图片库SDWebImage/Kingfisher默认都使用系统库NSCache来实现内存缓存。虽然NSCache会在设备内存紧张时回收内存,但是默认并不限制可保存内存最大字节数,所以在设备内存可用的情况下内存可以一直增加。所以通过设置图片缓存上限,防止图片缓存占用太高内存。图片缓存定义了一个默认的初始值上限,之后对于3x大屏幕设备和高端设备(内存比较高),适当增加更多内存上限。 优化成果 其他收益 域名统一- 减少了10%+的重复图片下载和内存消耗。同时减少之前多域名图片加载时重复创建HTTPS请求的过程,减少图片加载时间。 其他策略 加载异常处理 因为少量图片通过URL预处理转换后,可能会存在图片不存在的异常场景导致加载失败。所以当发生图片加载失败时,我们还是需要加载原始图片URL。但是这里需要屏蔽一些特殊的加载错误,避免非必要的加载,例如无网络/网络超时/主动取消加载等错误。之后会将错误图片URL上报到后台,方便之后调整URL转换策略,也可以发现一部分错误的图片URL推动业务修改。同时将这部分连接加入到错误连接缓存中,避免下次重复执行预处理和重复上报。 线上配置 目前存在的一些功能,例如URL预处理/统一域名/WebP使用等功能,都添加了线上配置,方便灰度/降级。一在出现问题时可以降级某些功能,新功能上线时也可以进行灰度测试。 大图检测 需要有一个机制及时发现图片不符合规范的问题。一方面我们通过线上灰度检测的方式,当发现大图片时会进行上报,后续推动业务方进行优化。另一方面我们在日常测试阶段,会开启Debug检测工具,当检测到大图片时,通过图片翻转/高亮背景颜色的方式提醒业务开发同学进行优化。 Flutter图片库优化 目前京喜APP有10+个二级界面是基于Flutter开发,所以我们也针对Flutter图片加载做了一些优化。 对接原生图片库 因为Flutter框架自带图片库只提供内存图片缓存,并不支持硬盘缓存,所以会导致图片重复下载。所以我们通过重写ImageProvider,当加载网络图片时,通过Channel调用原生图片库,原生图片库下载图片到本地磁盘后,返回图片文件目录。之后Flutter通过文件目录加载解码图片显示。这样一方面可以利用原生图片库相关优化能力,同时也可以复用图片硬盘缓存避免重复下载。 减少内存消耗 使用Image组件时,通过设置cacheHeight/cacheWidth,将图片解码为置顶像素宽高的位图尺寸,减少内存消耗。同时因为Flutter内存消耗相对原生更高,所以在Flutter界面关闭时,通过调用imageCache方法清除图片内存消耗降低内存消耗。 GIF优化 动画优化- 因为通常使用Flutter都是混合栈的机制,原生和Flutter界面在页面导航中相互跳转。所以当Flutter界面存在GIF图片时,跳转到原生以后GIF动画还会一直执行。所以我们通过在Image组件内监听Flutter engine发送的生命周期通知,当Flutter界面不在栈顶时,停止GIF动画执行,减少内存和CPU消耗。 减少解码次数- Flutter框架内部对GIF渲染的处理方式,在屏幕每一帧判断当前需要显示的GIF帧,之后对该GIF帧进行解码之后渲染。因为并不会把解码过的帧保存,所以会导致频繁解码导致内存波动大。经过优化,对已经解码过的帧进行保存,避免重复解码的消耗,同时避免内存的波动。 优化前内存波动很明显 优化后内存倾于平稳 提示:保存每一帧也会导致更多的内存消耗。目前APP中通常是小尺寸的GIF所以整体可控。可以考虑设置缓冲区上限来控制缓存的图片帧数避免内存过高。 后续优化方向 更优的缓存算法 优先移除最大内存- iOS系统NSCache实现。通过设置最大内存数,当内存不足时优先移除最大的值。 LRU缓存- 优先淘汰最久未使用的图片内存。对于很多二级界面的场景,用户打开界面后并不会再次打开。但是因为这些图片缓存是最后使用,所以清除内存时也会最后移除,但是在这种场景下就不太合适。 界面栈管理- 当界面关闭时将该界面的所有的图片内存移除,但是对于经常会打开的界面会导致频繁图片编解码也不太合适。 所以针对不同的业务场景使用不同的回收方式可能更加合适: 对于购物车/我的订单这类界面,用户每次加载的图片基本固定,所以更适合在内存中常驻,当内存消耗过高时再回收。 对于商详/搜索商品列表这类界面,通常商品列表展示的图片不一样并且用户也不会频繁进某一个特定的商详,所以更适合优先移除这部分的内存。 对于部分弹窗功能,图片显示后并不会再次使用,可以考虑不添加到内存中。 使用更好的图片格式 使用更好的图片格式通常可以带来更小的图片字节大小。同时因为压缩率的提高,可以在减少大小的同时提高图片质量。 提示:使用系统支持硬解码的图片格式更有优势。硬解码就是使用GPU进行解码,相比使用CPU软解码性能更好更省电。 APNG/动画WebP代替GIF- 按照Google官方的说法,GIF转换为有损WebP的字节数缩小了64%,而无损WebP字节数缩小了19%。所以使用动画WebP可以减少更多的网络流量传输。APNG是Mozilla推出的基于PNG的动图格式并且完全支持RGBA,相比GIF可以减少20%+的图片大小。而且GIF本身只支持256色索引颜色以及1位alpha(加上透明度后,边缘会出现明显的锯齿),使用APNG/WebP也可以带来相比GIF更好的显示效果。 提示:相比GIF,WebP的解码比GIF占用更多的CPU资源。有损WebP的解码时间是GIF的2.2倍,而无损WebP的解码时间是GIF的1.5倍。 HEIC-HEIC是基于H.265视频编码格式推出的图片格式。HEIC相比WebP可以减少20%+的图片大小,并且编解码性能更好。在系统兼容性上,Android 9.0以上的系统支持HEIC。苹果在iOS14以上系统才提供了WebP硬解码,之前的系统只能使用软解码,而HEIC在iOS11之后的机器上都已经支持硬解码,不过并不支持浏览器。 AVIF-AVIF是基于AV1编码格式推出的图片格式。AVIF相比WebP可以减少30%+的图片大小。不过目前只有Android 12以上的版本支持。 提示:这里主要是以VP8编码格式的WebP,VP9编码格式的WebP整体性能和HEIC差异不大。 不过这些图片格式需要图片服务器支持之后才能使用。 Flutter 虽然我们对Flutter图片库做了一些优化,但总体上还有很多优化空间。包括业界有在使用的基于纹理的图片方案。在原生侧将图片解码后,通过Flutter引擎创建纹理。之后讲图片纹理id传递给Flutter进行渲染。这样可以统一在原生侧管理图片内存缓存,优化之前Flutter和原生都分别有一份内存缓存的方式。而且针对于混合栈的导航栈方式,也可以更好的进行图片内存回收。另外针对Flutter,需要提供更灵活的图片内存回收策略,避免内存消耗过高。 提示:纹理可以复用内存中的位图缓存,所以并不会导致更多的内存占用。纹理方式大概能减少30%的内存消耗相比Flutter引擎图片库,主要是一些其他对象使用导致。 优化H5图片加载 我们可以通过拦截WebView图片加载的方式,让原生图片库来下载图片之后传递图片二进制数据给WebView显示。 减少流量消耗 通过这种方式,我们可以将原生图片库URL预处理相关功能支持到H5图片,减少H5加载过程中图片流量消耗,提高图片加载速度。同时因为APP原生和WebView图片缓存机制是相互独立的,所以通过统一在原生侧管理图片缓存,可以减少相同图片的重复下载。 支持更多图片格式 例如在iOS系统上,WKWebView目前只支持PNG/JPG/GIF图片格式。所以我们可以通过在原生端实现下载WebP/HEIC图片,之后对图片进行解码再传给WebView,这样就可以支持其他图片格式的显示。 提示:因为WebView不支持直接传递位图二进制数据显示,所以需要提前转换为PNG/JPG二进制数据传递。所以对于其他图片格式增加一次PNG/JPG编码过程会造成更多的性能消耗。不过对于Android系统应该可以在web内核层优化减少这块消耗。 总结 本文并没有讲底层图片加载库的具体实现,目前图片库不管是直接用第三方库还是自研图片库实现方式通常差异不大。我们更多是关注自身业务以及如何利用图片服务器能力最大化改善网络图片加载性能。所以部分策略可能不一定针对所有APP都合适,应该针对自身业务场景仔细评估优化方案。 扩展链接 WebP 手淘图片库HEIC使用 动画WebP和GIF比较 WebP支持 APNG支持 AVIF

优秀的个人博客,低调大师

前端配置化表单组件设计方法 | 京东云技术团队

一、背景 前端开发中涉及表单的页面非常多,看似功能简单,开发快速,实则占去了很大一部分时间。当某个表单包含元素过多时还会导致html代码过多,vue文件过大。从而不容易查找、修改和维护。为了提高开发效率及降低维护成本,下面介绍表单配置化组件的封装原理与封装方法。 二、技术方案 如上图所示,封装表单配置化组件的关键点有三个一是如何解决表单元素排布的行列问题,二是表单数据的绑定问题,三是表单元素的参数配置校验等问题。下面分别介绍这三个问题的解决方法。 •配置化表单组件的入参及说明 参数 说明 类型 可选值 默认值 labelWidth 表单元素label所占宽度 String —— 150px columnList 表单元素所组成的配置,是一个数组 Array —— [] formData 表单元素值的集合 Object —— {} columnSpan 表单排布分栏 Number —— 24 size 表单元素尺寸 String medium / small / mini medium •计算配置化表单的行数,本表单通过基础的24分栏计算表单最终的行数和列数,通过下面方法最终得到一个关于行列的二维数组 newColumnList() { const newColumnList= [] const row = Math.floor(24 / this.columnSpan) let newColumnItem = [] for(let i=0; i< this.columnList.length; i++) { newColumnItem.push(this.columnList[i]) if(newColumnItem.length === row || i === this.columnList.length-1) { newColumnList.push(newColumnItem) newColumnItem = [] } } return newColumnList } •通过上面得到的二维数组进行循环渲染,首先循环渲染行,其次循环渲染列。本方案采用element中的表单,当然也可以用其他组件库或者原生表单进行渲染,其原理通用。最终将会根据参数column.type决定加载哪一个具体的表单元素。 <el-form ref="form" :model="formData" :label-width="labelWidth" :size="size"> <el-row :gutter="20" v-for="(element,index) in newColumnList" :key="index+'formRow'"> <template v-for="(item, index) in element" > <column :key="index + 'formView'" :columnSpan="columnSpan" :column="item" :formData="formData" /> </template> </el-row> </el-form> •column组件最终根据type加载具体的表单元素。下面展示column组件的入参及其说明,通过component加载不同的表单元素 参数 说明 类型 可选值 默认值 column 表单元素的具体配置 Object —— {} formData 表单元素值的集合 Object —— {} columnSpan 表单排布分栏 Number —— 24 <el-col :span="columnSpan"> <component :is="column.type + 'View'" :column="column" :formData="formData" v-model="formData[column.name]" :columnSpan="columnSpan"/> </el-col> •这里主要以select表单元素为例进行说明,表单元素的双向绑定、校验以及值更新等问题 参数 说明 类型 可选值 默认值 column 表单元素的具体配置 Object —— {} value 表单元素值 Number/String/Array —— —— •column参数 参数 说明 类型 可选值 默认值 placeholder 空值说明 String —— —— required 是否必填 Boolean —— —— rules 校验规则 Array —— —— title 表单元素label String —— —— name 表单元素值名称 String —— —— multiple 是否多选 Boolean —— —— filterable 是否过滤 Boolean —— —— disabled 是否禁用 Boolean —— —— dictionary 下拉选项枚举 Array —— —— changeFunction 值改变时的回调函数 Function —— —— <el-form-item :label="column.title + ':'" :prop="column.name" :rules="rules"> <el-select v-model="val" clearable :multiple="column.multiple" :filterable="column.filterable" :placeholder="'请选择' + column.title" :disabled="column.disabled" style="width: 100%" @change="onChange" @clear="onClear"> <el-option v-for="item in column.dictionary" :key="item.code" :label="item.name" :value="item.code"> </el-option> </el-select> </el-form-item> rules: [ { required: this.column.required, message: this.column.placeholder placeholder ? this.column.placeholder : `请输入${this.column.title}`, trigger: 'change' }, ...this.column.rules ] onChange(){ this.$emit('input',this.val) if(this.column && this.column.changeFunction){ this.column.changeFunction(this.val) } }, onClear(){ this.onChange() } 三、项目实践 •配置化表单为bs-form,在页面中引入bs-form表单组件 <bs-form ref="formDemo" :columnList="columnList" :formData="formData" :columnSpan="columnSpan" labelWidth="120px"> </bs-form> <el-row style="text-align: center;"> <el-button type="primary" @click="onSave">保存</el-button> <el-button @click="onCancel">取消</el-button> </el-row> •formData参数 formData: { name: '', yearIncome: '', // 业务类型 goodsCategoryId: '', // 托寄物品类id projectManagerErp: '', // 项目经理erp projectName: '', // 项目名称 projectStage: '', // 项目阶段编码 projectStandardName: '', // 标准名称 projectYear: 2023, // 年份 startRegionId: '', // 始发区域id startBattleId: '', // 始发战区id address: [], // 省市 category: null, //图文类型 range: [] //发布范围 } •分栏参数 columnSpan: 6 •表单配置参数 columnList(){ const self = this return [ { type: 'text', name: 'name', title: '项目名称', required: true, maxlength: 20, showwordlimit: true, placeholder: '请输入' }, { name: 'category', type: 'radio', dictionary: [ { code: 1, name: '类型一' }, { code: 2, name: '类型二' } ], title: '图文类型', required: true }, { name: 'range', type: 'checkbox', title: '发布范围', dictionary: [ { code: 1, name: '范围一' }, { code: 2, name: '范围二' } ], required: true }, { type: 'text', // 字段类型文本框 name: 'yearIncome', //与后台对接字段 title: '年均收入', // 前端展示字段 required: true, // 必填项设置 maxlength: 50, // 字符串长度限制 showwordlimit: true, // 是否显示字符串长度 placeholder: '请输入', // 占位文本提示 rules: [ { pattern: /(^[1-9]([0-9]+)?(.[0-9]{1,2})?$)|(^(0){1}$)|(^[0-9].[0-9]([0-9])?$)/, message: '请输入数字最多两位小数' } ], }, { type: 'select', name: 'goodsCategoryId', title: '托寄物品类', required: true, filterable: true, placeholder: '请选择', dictionary: [{ name: '苹果', code: '1' },{ name: '手机', code: '2' },{ name: '测试', code: '3' },{ name: '樱桃', code: '7' },{ name: '荸荠', code: '9' }] }, { type: 'select', name: 'startRegionId', title: '区域', required: true, placeholder: '请选择', dictionary: [{ name: '销售-华北区域', code: '1' },{ name: '销售-华东区域', code: '2' },{ name: '销售-华南区域', code: '3' },{ name: '销售-西南区域', code: '4' },{ name: '销售-华中区域', code: '5' },{ name: '销售-东北区域', code: '6' }], // 点击下来触发切换联动的事件,为一个函数 changeFunction: function (val) { } }, { type: 'select', name: 'startBattleId', title: '战区', required: true, placeholder: '请选择', dictionary: this.battleByRegionList }, { type: 'select', name: 'projectStage', title: '项目阶段', required: true, placeholder: '请选择', dictionary: [{ name: '项目发起阶段', code: '10' },{ name: '项目调研阶段', code: '20' },{ name: '可行性分析阶段', code: '30' },{ name: '立项阶段', code: '40' }] }, { type: 'text', name: 'projectStandardName', title: '标准名称', required: true, placeholder: '请输入', append: '.com', // 文本框后置内容 }, { type: 'text', name: 'projectManagerErp', title: '项目经理', required: true, placeholder: '请输入' },{ type: 'cascader', // 字段类型下拉框 name: 'address', //与后台对接字段 title: '省市区', // 前端展示字段 required: true, // 必填项设置 placeholder:'请选择', // 占位文本提示 dictionary: [{ value: 'shanxi', label: '陕西省', children: [{ value: 'xian', label: '西安市', children: [{ value: 'yanta', label: '雁塔区' }, { value: 'beilin', label: '碑林区' }, { value: 'xincheng', label: '新城区' }, { value: 'weiyang', label: '未央区' }] }] }], // 点击下来触发切换联动的事件,为一个函数 changeFunction: function(){} },{ type: 'static', name: 'projectYear', title: '年份' } ] } •表单保存 // 保存 async onSave() { const valid = await this.$refs.formDemo.onValidate() if(valid) { this.$message.success('校验通过') }else { this.$message.error('校验失败') } } 四、成果展示 作者:京东物流 田雷雷

资源下载

更多资源
Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册