找回密码
 会员注册
查看: 24|回复: 0

如何成为一个优秀的复制粘贴工程师

[复制链接]

4

主题

0

回帖

13

积分

新手上路

积分
13
发表于 2024-9-19 23:05:48 | 显示全部楼层 |阅读模式
组件复用的困境在平时的搬砖过程中,当我们拿到新需求的设计稿时,为了加快开发的速度,通常会看看是否有曾经写过的组件可以复用。如果能找到类似的组件,有很大概率可以在原有的基础上修修补补就能直接用了。对于自己最常开发的工程,当看到设计稿往往可以立马想起来,某个可以复用的组件位于工程的哪个位置。但是随着工程的壮大,从10+个路由配置,增长到100+甚至200+的时候,就有些力不从心了。另外一个问题是,团队中其他同学很可能已经开发过和设计稿的非常相像的组件,但由于你不熟悉别人的工程,无法快速地找到哪个组件是可以拿来使用的。即使知道是哪个组件,这个组件可能会有一堆工具代码的依赖,想要完整地将组件搬到目标工程中,需要将组件依赖的文件一并复制。有可能同事还在忙着上线,没法及时给你回应,这样一来沟通成本就高了。转转是一个二手电商平台,如果仔细观察每条业务线的页面UI,可以发现它们大部分遵循统一的UI风格。实际上很多电商平台都有类似的UI结构。下图是转转三条业务线的首页,仔细观察就可以发现,它们有非常多相似的地方:顶部的搜索框、金刚位、轮播图、运营卡片、商品卡片。虽然它们非常相似,却在一些细节上有微小的差异,并且这些页面由不同的前端同学负责,就导致了相似的功能被不同的成员多次重复实现。那么是否可以就这个问题添加一些自动化的流程,来提升代码的复用率,增强搬砖的幸福感呢?方案设计首先明确目的:在新需求出现后,让前端同学知道是否存在已有的相似组件,并迅速得到相应的代码文件。那么问题就转化为,如何将设计稿与已有组件进行对比,找出相似的组件。想要比较,就必须有量化的标准。转转内部的设计师主要使用sketch作为设计工具,而sketch内部使用json格式存储数据。如果将.sketch文件后缀改为.zip,然后使用压缩工具解压,就会发现里面实际上存储了大量的json格式数据,描述了节点之间的嵌套关系以及节点的样式。那么就可以采取某种算法,根据sketch文件中的数据,产出一份可以用于比较的量化标准,这样就完成了设计稿的处理。那么该如何处理各个工程中已存在的组件呢?事实上对于这些组件,只要拿到它们实际渲染出来的dom结构,就可以根据dom结构来提取出一份特征数据。有了设计稿和已有组件的数据,后续只要比较这两份数据,计算出相似度,就可以快速找到前端开发需要的组件代码了。对于上面的说明,可以类比哈希算法来帮助理解。JS中常使用对象,以key-value的形式来存储数据。哈希算法会根据一个key值,产生一个唯一的结果,并根据这个结果将数据存储对应的地址上。这个根据key产生结果的过程,就类似于上文根据设计稿的节点/dom结构,产出可用于比较的特征数据。假设已经根据一份设计稿,找到了5个相似组件后,怎么让前端同学知道这些组件中,哪一个组件是ta最需要的那个呢?如果让ta看一遍每个组件的代码再决定,那效率就太慢了。所以需要对每个组件进行截图,当看到5个组件的截图,哪个与设计稿长得最像,基本上就是ta需要的组件了。总结一下方案,就是「根据设计稿和已有组件的dom,产出一份用于比较的数据,初步筛选出几个相似组件,然后让业务开发根据截图与代码,在初筛组件中挑选合适的组件,导入到自己的工程。」架构设计整体涉及到四个部分:浏览器运行时 分析组件dom特征、截图,将结果传递给Vite插件。Vite插件提取组件的所有依赖,发布为npm包。整合dom特征和截图,上传数据到服务端。npm服务器服务端(马良后台):将数据存储到MySQL。详细的数据流见下图,图中的“组件哈希”和上文的“组件特征数据”是同一含义。下面解释一下这张图可能产生的几个疑问:Q:浏览器中是如何递归Vue组件的?A:Vue在mount过程中,会将组件的实例放在dom的__vue__属性上。由于Vue应用的惯例是将根组件挂载到#app上,所以可以通过constroot=document.getElementById('app').__vue__获取Vue应用的根组件。然后通过root.$children递归访问所有的Vue组件。Q:如何对组件进行截图?A:截图方案考虑过puppeteer和html2canvas,在这个场景中,html2canvas更符合需求。因为html2canvas虽然在性能可能比不上puppeteer,但它的优点是可以生成指定dom的截图。假设页面中有一个全屏弹窗,将所有的dom元素覆盖了,html2canvas依然可以排除弹窗的影响,生成任意出现在html中的dom元素的截图。Q:为什么会有Vite插件这个环节?A:一个组件有很多的依赖,在收集组件代码时,必须连同其依赖一起收集,所以需要构建一个依赖树。构建依赖树有两种办法,一个是通过ast分析所有的导入语句,另一个是通过构建工具已有的依赖树。由于通过ast分析的实现有较多的困难,比如导入语句中存在别名,文件类型众多(less、scss、vue、js)。如果希望有较高的准确率,需要解析构建工具的别名配置,并使用每种文件类型对应的ast转换库。其实现难度远大于借助构建工具能力的实现方案。那么剩下的就是Vite插件和Webpack插件的抉择。由于之前给团队主体项目接入过Vite,熟悉程度大于Webpack,故选择Vite插件作为依赖收集的手段。Q:浏览器运行时代码如何与Vite插件进行通信?A:Vite2.0使用了connect框架作为httpserver,该框架的用法与express非常类似,支持相同的中间件语法。在为connect引入body-parser等必要的中间件后,就可以像写服务端代码一样,为其添加接口。遍历组件这一步的目的是得到三个数据:组件的截图组件的特征组件在工程中的路径截图和特征好理解,但是为什么需要组件在工程中的路径呢?这就需要注意此时代码运行的环境。由于需要访问dom数据,这段代码必须要在浏览器中运行。但是在浏览器中获取的组件实例,如何与工程中的.vue文件对应起来呢?答案就是在开发环境中,Vue实例上vm.$options.__file属性就是vue文件在工程中的相对路径(如下图)。但是这个属性是哪来的呢?通过阅读相关的插件和Vue的源码可以知道为什么(后两段包含Vue源码分析,不熟悉的同学可以跳过)。在Vite中对Vue2的支持是由vite-plugin-vue2插件提供的,在它的源码中有这么一段:// Expose filename. This is used by the devtools and Vue runtime warnings.if (!options.isProduction) {  // Expose the file's full path in development, so that it can be opened  // from the devtools.  result += `\n__component__.options.__file = ${JSON.stringify(  path.relative(options.root, filePath).replace(/\\/g, '/')  )}`}可以看出,该插件在非生产模式下,会将组件相对于根目录的路径,暴露在__component__.options.__file中。这里的__component__就是Vue组件的构造函数。熟悉Vue2源码的同学应该知道,Vue2中通过Vue.extend实现了类似于es6extends关键字的功能。在Vue子组件构造函数中有这么一段逻辑:function initInternalComponent (vm: Component, options: InternalComponentOptions) {  //vm.construct就是Vue.extend的返回值。  //vm.$options的原型就是构造函数的options对象。  const opts = vm.$options = Object.create(vm.constructor.options)  // 省略后面的代码}梳理一下流程,就是:vite-plugin-vue2向构造函数的options属性注入文件在工程中的相对路径。vm.$options对象的原型是构造函数的options对象。vm.$options本身没有__file属性,但是通过原型访问到了构造函数的options对象中的__file属性。所以在浏览器环境中,可以通过vm.$options.__file,来访问到vite-plugin-vue2注入的文件相对路径。下面展示遍历Vue组件实例的伪代码:// Vue 根组件实例const appInstance = document.querySelector('#app').__vue__/** @type {import('vue-router').default} */// 获取 Vue router 实例const router = appInstance.$router// 当前路由匹配的Vue组件实例const pageVM = router.currentRoute.matched[0]?.instances?.defaultif (pageVM) {  traverseAppInstance(pageVM)}function traverseAppInstance(vm) {  // 截图伪代码  const image = html2canvas(vm.$el)  // 组件特征分析伪代码,具体算法见后文。  const characteristic = analyzeComponent(vm.$el)  const filePath = vm.$otpions.__file  // 将数据传递给 Vite 插件  sendToVitePlugin({    image,    characteristic,    filePath  })}特征提取与比较算法❝在实际的实现过程中,为了快速实现效果,借助了转转内部名为马良的d2c(designtocode)平台(一个可以将sketch文件转化为组织良好的代码的平台)但这并不决定性地影响本文想法的整体实现。❞想要知道设计稿中的一个模块和组件库里面的哪些模块是相似的,我们就需要一个对比算法,其实最简单的方案是相似图像对比。方案一:相似图像对比使用图像相似对比相关的算法,我们虽然可以比较容易的找出相似组件,但这种方案在实际场景中会有明显的缺陷:我们是在真实页面中提取组件,而这时组件里面的数据已经使用了真实的业务数据,会跟设计稿的内容存在很大差异,这就导致相似图像对比的方案几乎无法发挥作用,所以方案一不可取。方案二:组件特征对比我们可以用设计稿生成代码的结构样式特征与组件来对比,这里我们看一个例子。上图左侧是设计稿中的模块,右侧是项目中真实的组件,我们人脑会根据自然思维认定这两个模块是相似的模块,而这个思维过程是什么样的呢,我们可以将上图内的信息进行抽象和提取,以骨架屏的形式绘制成下图:这样是不是就更确信他们是相似了呢?基于这个简单的抽象过程,我们来实现特征对比算法。步骤一:特征提取任何一个模块的实际开发,工程师可能会有多种层级嵌套方式来实现,而不同人可能会有不同的嵌套设计,因此我们需要过滤掉层级这个维度,我们要首先通过遍历到达一个DOM结构的所有叶子节点,也就是DOM节点的最底层,而我们通常情况下,叶子节点可能是以下几种类型:文字图片背景图有视觉占位的样式节点,例如:按钮、图形、表单等类型4稍复杂,我们先以类型1、2、3为例,我们需要计算提取以下特征:节点类型:可能会有text、img、bgimg节点关键样式:字体相关样式图片相关样式背景图相关样式每个叶子节点「相对于组件中心点」的坐标第3点,为什么我们要提取节点相对于中心点的坐标呢,这就要涉及到对比算法:步骤二:对比算法特征对比算法的整体思路是:对比两个组件中相似类型的叶子节点比例对比每个叶子节点在另一个组件中有类型相同且位置相同的叶子节点的比例对比在类型和位置都相同的情况下,关键样式也相似的叶子节点的比例通过一个打分算法计算出两个组件的相似分数最后通过一个权衡算法挑选超过一定得分的组件认定为相似组件其中思路2中的关键点就是位置相同,而实际对比中我们会发现,即使是同一个组件在不同页面可能会有不同的尺寸和相对位置,我们先将上面的骨架屏右侧图放大一下:这样可以很清晰的看到,虽然他们大体相似,但位置几乎不一样,因此我们就不能用绝对位置来作为衡量标准,那么我们可以用相对于中心的的坐标来衡量:image-20220309155514143我们计算出每个叶子节点相对于中心的的坐标(offsetX,offsetY),然后把两个组件缩放到宽度一致的尺寸:image-20220309155945056这时我们再去比较相对位置是不是就更容易一些?当然实际算法会远比这个复杂。细心的同学会发现,两个组件其实并不完全一致,右侧组件多了一个HOT图标:这一定程度上会影响相似评分,在上面的算法思路中我们都会提到,我们计算的是各项条件相似的比例,也就是我们可以知道任何一个条件下每个节点和另一个节点相似和不相似的比例分别是多少,那就依赖最终的打分和权衡算法来判定对比结果了,在上面这个case中,实际上的分数并不影响我们对相似的判断。Vite插件Vite插件在获取到浏览器发送的数据后,通过filePath(文件的相对路径)定位到具体的.vue文件,并分析其依赖。在Vite插件中,可以获取到Vite内部的模块依赖表,里面是几个map,可以通过文件路径获取到对应的模块。以图中的src/main.ts模块为例,importedModules就是main.ts文件引入的所有依赖。通过浏览器发送的filePath属性,获取对应的vue文件模块。vue文件会引入其他文件,其他文件又会引入另外的文件,所以模块其实是一棵n叉树。遍历这棵n叉树,就可以vue文件的所有依赖文件。拿到所有的文件内容后,通过npmpublish命令,可以将其作为npm包发布。后续想要使用这个组件的话,下载这个npm包即可。这个过程会有很多细节,比如一个页面有很多组件,如果每个组件都遍历一次,就会有很多组件被重复遍历到,存在不必要的性能损耗。采用二叉树的后序遍历可以达成一次遍历,就收集完所有vue组件各自的依赖。再比如,每个vue组件的依赖文件不同,npmpublish通常被用于某个固定工程的发布,发布文件的范围是不变的。但当前的场景是需要动态地决定需要发布的文h还有许许多多的其他细节问题。Nodejs服务端&MySQL服务端是用于最终存储数据的地方,包括截图url、组件特征、npm包名等全过程中收集的所有数据。因为只是简单的CRUD,这里不再赘述。效果展示在做完上述的一切后,当前端同学拿到一个新的设计稿,上传到马良系统上,就会和已提取的所有组件特征做一个相似度的匹配,推荐给前端同学使用。在马良系统上的最终形式是:总结在整个过程中,通过运行时代码提取了组件的特征和截图,通过Vite插件获取了Vue组件代码以及所有的依赖,并整合数据上传至Nodejs服务端,存储到数据库中。最终在马良系统中,用户上传一份sketch设计稿,通过对比已有组件与设计稿的相似度,向用户推荐相似的组件。对用户而言,在一份已经写好模板和css的文件上修改,比从零开始的速度要快得多。并且这打破了各种不同的工程之间的代码分享壁垒,让业务页面的开发更加顺畅。致谢本文提及的方案由@张所勇(组件特征提取与比较)@强敏(浏览器端代码)@陈亦涛(Vite插件)共同完成。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2024-12-25 13:47 , Processed in 0.354237 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表