How to be a good copy-paste engineer

The dilemma of component reuse

In the usual process of moving bricks, when we get the design draft for new requirements, in order to speed up the development, we usually check to see if there are any previously written components that can be reused. If you can find similar components, there is a high probability that you can tinker on the original basis and use them directly.

For the projects that I develop most often, when I see the design draft, I can often immediately think of where a reusable component is located in the project. However, with the expansion of the project, from 10+ routing configurations to 100+ or ​​even 200+, it is a bit powerless.

Another problem is that other students in the team may have developed components that are very similar to the design draft, but since you are not familiar with other people's projects, you cannot quickly find which components can be used. Even if you know which component it is, this component may have a bunch of tool code dependencies. If you want to completely move the component to the target project, you need to copy the files that the component depends on. It is possible that colleagues are still busy going online and cannot respond to you in time, which will result in high communication costs.

Zhuanzhuan is a second-hand e-commerce platform. If you carefully observe the page UI of each business line, you can find that most of them follow a unified UI style. In fact, many e-commerce platforms have a similar UI structure. The following picture is the home page of the three business lines of Zhuanzhuan. If you look closely, you can find that they have many similarities: the search box at the top, the diamond position, the carousel map, the operation card, and the product card. Although they are very similar, there are slight differences in some details, and these pages are in charge of different front-end students, resulting in similar functions being repeatedly implemented by different members.

So is it possible to add some automated processes to this problem to improve the code reuse rate and enhance the happiness of moving bricks?

Design

First, clarify the purpose: after the emergence of new requirements, let the front-end students know whether there are existing similar components, and quickly get the corresponding code files.

Then the problem becomes, how to compare the design draft with the existing components to find similar components. To compare, there must be quantitative standards. The designers inside Zhuanzhuan mainly use sketch as a design tool, and sketch uses json format to store data internally. If you change the .sketchfile suffix to .zip, and then use a compression tool to decompress it, you will find that a large amount of json format data is actually stored in it, describing the nested relationship between nodes and the style of nodes. Then a certain algorithm can be adopted to produce a quantitative standard that can be used for comparison according to the data in the sketchfile , thus completing the processing of the design draft.

So how to deal with the components that already exist in each project? In fact, for these components, as long as you get the dom structure they actually render, you can extract a feature data according to the dom structure. With the data of the design draft and existing components, you can quickly find the component code required for front-end development by comparing the two data and calculating the similarity. For the above description, the hash algorithm can be compared to help understanding. Objects are often used in JS to store data in the form of key-value. The hash algorithm generates a unique result based on a key value, and stores the data at the corresponding address based on this result. This process of generating results based on the key is similar to the above based on the node/dom structure of the design draft, producing feature data that can be used for comparison.

Suppose you have found 5 similar components according to a design draft, how to let the front-end students know which of these components is the one that ta needs the most? If you let ta look at the code of each component and then decide, it will be too slow. So you need to take screenshots of each component. When you see the screenshots of 5 components, which one looks most like the design draft is basically the component he needs.

To sum up the plan, it is ** "According to the design draft and the DOM of the existing components, a data for comparison is produced, and several similar components are initially screened, and then the business development is based on the screenshots and codes, in the preliminary screening components. Pick the right components and import them into your project.”**

Architecture design

The whole involves four parts:

  • The browser runtime analyzes the component dom characteristics, screenshots, and passes the results to the Vite plugin.

  • Vite plugin

    • Extract all dependencies of the component and publish it as an npm package.
    • 整合 dom 特征和截图,上传数据到服务端。
  • npm 服务器

  • 服务端(马良后台):将数据存储到 MySQL。 详细的数据流见下图,图中的“组件哈希”和上文的“组件特征数据”是同一含义。 架构设计图

下面解释一下这张图可能产生的几个疑问:

Q: 浏览器中是如何递归 Vue 组件的?

A:Vue 在 mount 过程中,会将组件的实例放在 dom 的 __vue__ 属性上。由于 Vue 应用的惯例是将根组件挂载到 #app 上,所以可以通过 const root = document.getElementById('app').__vue__ 获取 Vue 应用的根组件。然后通过 root.$children 递归访问所有的 Vue 组件。

Q:如何对组件进行截图?

A:截图方案考虑过 puppeteerhtml2canvas,在这个场景中,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 框架作为 http server,该框架的用法与 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 实现了类似于 es6 extends 关键字的功能。在 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?.default
if (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 (design to code) 平台(一个可以将 sketch 文件转化为组织良好的代码的平台)但这并不决定性地影响本文想法的整体实现。

想要知道设计稿中的一个模块和组件库里面的哪些模块是相似的,我们就需要一个对比算法,其实最简单的方案是相似图像对比。

方案一:相似图像对比

使用图像相似对比相关的算法,我们虽然可以比较容易的找出相似组件,但这种方案在实际场景中会有明显的缺陷:我们是在真实页面中提取组件,而这时组件里面的数据已经使用了真实的业务数据,会跟设计稿的内容存在很大差异,这就导致相似图像对比的方案几乎无法发挥作用,所以方案一不可取。

方案二:组件特征对比

我们可以用设计稿生成代码的结构样式特征与组件来对比,这里我们看一个例子。 设计稿例子

上图左侧是设计稿中的模块,右侧是项目中真实的组件,我们人脑会根据自然思维认定这两个模块是相似的模块,而这个思维过程是什么样的呢,我们可以将上图内的信息进行抽象和提取,以骨架屏的形式绘制成下图: 骨架屏 这样是不是就更确信他们是相似了呢?

基于这个简单的抽象过程,我们来实现特征对比算法。

步骤一:特征提取

任何一个模块的实际开发,工程师可能会有多种层级嵌套方式来实现,而不同人可能会有不同的嵌套设计,因此我们需要过滤掉层级这个维度,我们要首先通过遍历到达一个 DOM 结构的所有叶子节点,也就是 DOM 节点的最底层,而我们通常情况下,叶子节点可能是以下几种类型:

  1. 文字
  2. 图片
  3. 背景图
  4. 有视觉占位的样式节点,例如:按钮、图形、表单等

类型 4 稍复杂,我们先以类型 1、2、3 为例,我们需要计算提取以下特征:

  1. 节点类型:可能会有 text、img、bgimg
  2. 节点关键样式:
  • 字体相关样式
  • 图片相关样式
  • 背景图相关样式
  1. 每个叶子节点**「相对于组件中心点」**的坐标 第 3 点,为什么我们要提取节点相对于中心点的坐标呢,这就要涉及到对比算法:

步骤二:对比算法

特征对比算法的整体思路是:

  1. 对比两个组件中相似类型的叶子节点比例
  2. 对比每个叶子节点在另一个组件中有类型相同且位置相同的叶子节点的比例
  3. 对比在类型和位置都相同的情况下,关键样式也相似的叶子节点的比例
  4. 通过一个打分算法计算出两个组件的相似分数
  5. 最后通过一个权衡算法挑选超过一定得分的组件认定为相似组件

其中思路 2 中的关键点就是位置相同,而实际对比中我们会发现,即使是同一个组件在不同页面可能会有不同的尺寸和相对位置,我们先将上面的骨架屏右侧图放大一下:

这样可以很清晰的看到,虽然他们大体相似,但位置几乎不一样,因此我们就不能用绝对位置来作为衡量标准,那么我们可以用相对于中心的的坐标来衡量:

image-20220309155514143

image-20220309155514143

我们计算出每个叶子节点相对于中心的的坐标(offsetX,offsetY),然后把两个组件缩放到宽度一致的尺寸:

image-20220309155945056

image-20220309155945056

这时我们再去比较相对位置是不是就更容易一些?当然实际算法会远比这个复杂。 细心的同学会发现,两个组件其实并不完全一致,右侧组件多了一个 HOT 图标:

这一定程度上会影响相似评分,在上面的算法思路中我们都会提到,我们计算的是各项条件相似的比例,也就是我们可以知道任何一个条件下每个节点和另一个节点相似和不相似的比例分别是多少,那就依赖最终的打分和权衡算法来判定对比结果了,在上面这个 case 中,实际上的分数并不影响我们对相似的判断。

Vite 插件

Vite 插件在获取到浏览器发送的数据后,通过 filePath(文件的相对路径)定位到具体的 .vue 文件,并分析其依赖。

在 Vite 插件中,可以获取到 Vite 内部的模块依赖表,里面是几个 map,可以通过文件路径获取到对应的模块。 以图中的 src/main.ts 模块为例,importedModules 就是 main.ts 文件引入的所有依赖。通过浏览器发送的 filePath 属性,获取对应的 vue 文件模块。vue 文件会引入其他文件,其他文件又会引入另外的文件,所以模块其实是一棵 n 叉树。遍历这棵 n 叉树,就可以 vue 文件的所有依赖文件。拿到所有的文件内容后,通过 npm publish 命令,可以将其作为 npm 包发布。后续想要使用这个组件的话,下载这个 npm 包即可。

这个过程会有很多细节,比如一个页面有很多组件,如果每个组件都遍历一次,就会有很多组件被重复遍历到,存在不必要的性能损耗。采用二叉树的后序遍历可以达成一次遍历,就收集完所有 vue 组件各自的依赖。再比如,每个 vue 组件的依赖文件不同,npm publish 通常被用于某个固定工程的发布,发布文件的范围是不变的。但当前的场景是需要动态地决定需要发布的文h还有许许多多的其他细节问题。

Nodejs 服务端 & MySQL

服务端是用于最终存储数据的地方,包括截图 url、组件特征、npm 包名等全过程中收集的所有数据。因为只是简单的 CRUD,这里不再赘述。

效果展示

在做完上述的一切后,当前端同学拿到一个新的设计稿,上传到马良系统上,就会和已提取的所有组件特征做一个相似度的匹配,推荐给前端同学使用。在马良系统上的最终形式是:

总结

在整个过程中,通过运行时代码提取了组件的特征和截图,通过 Vite 插件获取了 Vue 组件代码以及所有的依赖,并整合数据上传至 Nodejs 服务端,存储到数据库中。最终在马良系统中,用户上传一份 sketch 设计稿,通过对比已有组件与设计稿的相似度,向用户推荐相似的组件。

对用户而言,在一份已经写好模板和 css 的文件上修改,比从零开始的速度要快得多。并且这打破了各种不同的工程之间的代码分享壁垒,让业务页面的开发更加顺畅。

致谢

本文提及的方案由@张所勇 (组件特征提取与比较) @强敏 (浏览器端代码) @陈亦涛 (Vite 插件)共同完成。

Guess you like

Origin juejin.im/post/7080032725477883917