一、业务描述

由于项目需要融合工蜂,因此需要实现一个用于展示某个工蜂仓库的某次 MR 提交的详情页,包括 MR 基础信息的展示、MR 包含的文件列表的展示以及每一个文件的新旧版本之间的差异信息。

因此,主要的业务流程是:

  • 通过工蜂 API 获取 MR 详情数据;
  • 进行一些数据处理,返回给前端;
  • 前端通过 v-code-diff 组件展示。

二、从后端拿到数据

之前的一篇文章中,封装了 Java 调用 HttpURLConnection 来访问第三方 API 的接口,感兴趣可以移步链接:Java 封装 HTTP 请求对接企业 API

因此,后端的接口具体实现如下:

public MrDetail getMrDetail(int gitProjectId, int mrId) {
	String url = "/api/v3/projects/"+ gitProjectId +"/merge_request/" + mrId + "/changes";
	String params = "";
	MrDetail mrDetail = new MrDetail();
	List<FileDiff> fileDiffList = new ArrayList<>();
	try {
	   JSONObject json = requestService.getGitCodeRequest(url, params);
	   JSONArray jsonFileList = json.getJSONArray("files");
	   for(Object file : jsonFileList) {
	       JSONObject jsonFile = (JSONObject) file;
	       FileDiff fileDiff = new FileDiff();
	       fileDiff
	               .setOldPath(jsonFile.getString("old_path"))
	               .setNewPath(jsonFile.getString("new_path"))
	               .setAMode(jsonFile.getInteger("a_mode"))
	               .setBMode(jsonFile.getInteger("b_mode"))
	               .setDiff(jsonFile.getString("diff"))
	               .setNewFile(jsonFile.getBoolean("new_file"))
	               .setRenameFile(jsonFile.getBoolean("renamed_file"))
	               .setDeleteFile(jsonFile.getBoolean("deleted_file"))
	               .setTooLarge(jsonFile.getBoolean("is_too_large"))
	               .setCollapse(jsonFile.getBoolean("is_collapse"))
	               .setAdditions(jsonFile.getInteger("additions"))
	               .setDeletions(jsonFile.getInteger("deletions"));
	       /* ** 注释掉的这部分是数据清洗的操作,后面会讲到。
	       **
	       splitFileDiff(fileDiff); // 处理file中的Diff信息;
	       if(isVaildFile(fileDiff)) { // 判断file是否合法 ;
	           fileDiff.setForShow(true);
	       } else {
	           fileDiff.setForShow(false);
	       }
	       **
	       ** */
	       
	       fileDiffList.add(fileDiff);
	   }
	   mrDetail.setId(json.getInteger("id"))
	           .setTitle(json.getString("title"))
	           .setSourceBranch(json.getString("source_branch"))
	           .setTargetBranch(json.getString("target_branch"))
	           .setState(json.getString("state"))
	           .setMergeStatus(json.getString("merge_status"))
	           .setDescription(json.getString("description"))
	           .setAuthor(((JSONObject)json.get("author")).getString("username"))
	           .setCreatedTime(json.getString("created_at"))
	           .setGitProjectId(Integer.toString(gitProjectId))
	           .setFileDiffs(fileDiffList);
	} catch (Exception e) {
	   throw new BusinessException(e.toString());
	}
	return mrDetail;
}

通过这种方式,可以获得 MR 详情的 JSON 对象。如下图所示。

{
// 仅截取部分关键信息
	"id": 123456,
	"title": "WIP ajax filter",
    "target_branch": "master",
    "source_branch": "user",
    "state": "opened",
    "created_at": "2022-07-08T08:58:08+0000",
    "files": [
        {
            "old_path": "web/pages/user/regist_success.html",
            "new_path": "web/pages/user/regist_success.html",
            "a_mode": 33188,
            "b_mode": 33188,
            "diff": "@@ -3,7 +3,8 @@\n <head>\n <meta charset=\"UTF-8\">\n <title>尚硅谷会员注册页面</title>\n-<link type=\"text/css\" rel=\"stylesheet\" href=\"../../static/css/style.css\" >\n+\t<base href=\"http://localhost:8080/book/\">\n+<link type=\"text/css\" rel=\"stylesheet\" href=\"static/css/style.css\" >\n <style type=\"text/css\">\n \th1 {\n \t\ttext-align: center;\n@@ -11,13 +12,13 @@\n \t}\n \t\n \th1 a {\n-\t\tcolor:red;\n+\t\tcolor: #ff0000;\n \t}\n </style>\n </head>\n <body>\n \t\t<div id=\"header\">\n-\t\t\t\t<img class=\"logo_img\" alt=\"\" src=\"../../static/img/logo.gif\" >\n+\t\t\t\t<img class=\"logo_img\" alt=\"\" src=\"static/img/logo.gif\" >\n \t\t\t\t<span class=\"wel_word\"></span>\n \t\t\t\t<div>\n \t\t\t\t\t<span>欢迎<span class=\"um_span\">韩总</span>光临尚硅谷书城</span>\n@@ -29,7 +30,7 @@\n \t\t\n \t\t<div id=\"main\">\n \t\t\n-\t\t\t<h1>注册成功! <a href=\"../../index.html\">转到主页</a></h1>\n+\t\t\t<h1>注册成功! <a href=\"index.html\">转到主页</a></h1>\n \t\n \t\t</div>\n \t\t\n",
            "new_file": false,
            "renamed_file": false,
            "deleted_file": false,
            "is_too_large": false,
            "is_collapse": false,
            "additions": 5,
            "deletions": 4
        },
        {
            "old_path": "web/pages/user/login_success.html",
            "new_path": "web/pages/user/login_success.html",
            "a_mode": 33188,
            "b_mode": 33188,
            "diff": "@@ -3,7 +3,8 @@\n <head>\n <meta charset=\"UTF-8\">\n <title>尚硅谷会员注册页面</title>\n-<link type=\"text/css\" rel=\"stylesheet\" href=\"../../static/css/style.css\" >\n+\t<base href=\"http://localhost:8080/book/\">\n+<link type=\"text/css\" rel=\"stylesheet\" href=\"static/css/style.css\" >\n <style type=\"text/css\">\n \th1 {\n \t\ttext-align: center;\n@@ -17,12 +18,12 @@\n </head>\n <body>\n \t\t<div id=\"header\">\n-\t\t\t\t<img class=\"logo_img\" alt=\"\" src=\"../../static/img/logo.gif\" >\n+\t\t\t\t<img class=\"logo_img\" alt=\"\" src=\"static/img/logo.gif\" >\n \t\t\t\t<div>\n \t\t\t\t\t<span>欢迎<span class=\"um_span\">韩总</span>光临尚硅谷书城</span>\n-\t\t\t\t\t<a href=\"../order/order.html\">我的订单</a>\n-\t\t\t\t\t<a href=\"../../index.html\">注销</a>&nbsp;&nbsp;\n-\t\t\t\t\t<a href=\"../../index.html\">返回</a>\n+\t\t\t\t\t<a href=\"pages/order/order.html\">我的订单</a>\n+\t\t\t\t\t<a href=\"index.html\">注销</a>&nbsp;&nbsp;\n+\t\t\t\t\t<a href=\"index.html\">返回</a>\n \t\t\t\t</div>\n \t\t</div>\n \t\t\n",
            "new_file": false,
            "renamed_file": false,
            "deleted_file": false,
            "is_too_large": false,
            "is_collapse": false,
            "additions": 6,
            "deletions": 5
        },
    ]
}

其中,files 是一个对象列表,里面包含了每个文件的新旧版本差异信息。

但是可以看到,diff 中的字符串是将新旧版本的代码行添加进了一个文件中,并没有给我们拆分好两个文件,因此,这也是后续需要数据清洗的原因。

三、前端集成 v-code-diff 显示 code diff 信息

前端需要用什么方式展示 code diff 信息呢,这里本项目采用了 GitHub 上的一个开源组件:v-code-diff(官方文档:https://github.com/Shimada666/v-code-diff

该组件展示页面如下图所示:

在这里插入图片描述
想亲自体验一把的,请移步官方项目演示链接(https://shimada666.github.io/v-code-diff/)。

在前端代码中集成:

<!-- File borad -->
 <div class="file_board">
   <div class="file_list">
     <el-card style="height: 100%; overflow: scroll;">
       文件列表:
       <div v-for="(file, index) in fileList" :key="index">
         <el-tooltip v-if="file.forShow" class="item" effect="dark" placement="top">
           <template v-slot:content>
             {{file.newPath}}
           </template>
           <el-button v-if="file.changeLineNo > 50" type="text" @click="showFile = file">
             *
             <i class="el-icon-document"></i>
             {{file.newPath}}
           </el-button>
           <div v-else>
             <el-button v-if="file.newFile" type="text" icon="el-icon-document-add" @click="showFile = file">{{file.newPath}}</el-button>
             <el-button v-else type="text" icon="el-icon-document" @click="showFile = file">{{file.newPath}}</el-button>
           </div>
         </el-tooltip>
       </div>
     </el-card>
   </div>
   <div class="diff_list">
     <code-diff
       :old-string="showFile.oldStr"
       :new-string="showFile.newStr"
       :file-name="showFile.newPath"
       :context="15"
       :drawFileList="true"
       output-format="side-by-side"/>
   </div>
 </div>

<style lang="less" scoped>
.file_board {
	width: 100%;
	height: 500px;
	display: flex;
	margin-top: 10px;
}

.file_list {
	height: 500px; 
	width: 30%;
}

.diff_list {
	height: 500px; 
	width: 70%;
	overflow-y: scroll;
}
</style>

但此时,后端传过来的数据是没有办法直接在前端显示的。因为 code-diff 组件需要传入两个字符串(旧版本 + 新版本),但是目前的数据中,新旧版本是整合到一个字符串中的,因此需要在后端进行数据处理。
在这里插入图片描述

四、回到后端,数据清洗

由于前端对于每一个文件,都需要两个不同的字符串属性,分别存储 oldString 和 newString。因此主要的清洗目的,就是将 files 中每一个 file 的 diff 字符串,拆分成两个新的属性。

因此可以采用 正则表达式 匹配的方式,首先将字符串分割成若干个包含 diff 的代码段,然后对于每一个代码段,再次分割成代码行。此时,粒度划分到最低层次。遍历所有的代码行,根据首字符是 “+” 还是 “-”,来决定将这一行代码加入 oldString 还是 newString。

具体实现代码如下所示。

 private void splitFileDiff(FileDiff fileDiff) {
    String pattern1 = "@{2}\\s+[+-]\\d+,\\d+\\s+[+-]\\d+,\\d+\\s+@{2}\\n";
    String pattern2 = "\\n";
    String oldStr = "";
    String newStr = "";
    int lines = 0;
    // 按照 pattern1 分割成若干个代码段
    for(int i = 0; i < fileDiff.getDiff().split(pattern1).length; i++) {
        // 按照 pattern2 分割成代码行
        for(int j = 0; j < fileDiff.getDiff().split(pattern1)[i].split(pattern2).length; j++) {
            // 去除首尾空字符
            String line = fileDiff.getDiff().split(pattern1)[i].split(pattern2)[j];
            // 如果开头是“-”,放入oldStr;如果开头是“+”,放入newStr
            if(line.startsWith("-")) {
                oldStr = oldStr + line.substring(1) + "\n";
            } else if(line.startsWith("+")) {
                newStr = newStr + line.substring(1) + "\n";
            } else {
                oldStr = oldStr + line + "\n";
                newStr = newStr + line + "\n";
            }
            lines++;
        }
        oldStr = oldStr + "\n" + "/* ** **  已省略部分代码  ** ** */ \n";
        newStr = newStr + "\n" + "/* ** **  已省略部分代码  ** ** */ \n";
    }
    fileDiff.setOldStr(oldStr).setNewStr(newStr).setLines(lines);
}

要注意的是,正则表达式

“@{2}\s+[±]\d+,\d+\s+[±]\d+,\d+\s+@{2}\n”

匹配的是:

“@@ -3,7 +3,8 @@”

迭代中,需要在代码块与代码块之间插入分割行。

并且,前端作为简单功能,不可能将一次 MR 的文件尽数展示,因此,需要对文件列表中的不合法的文件进行过滤。这里过滤的条件是:

  • 后缀为 “.jar” 的文件(包文件,不展示);
  • 后缀为 “/null” 的文件(代表已删除的文件,不必展示);
  • 前缀为 “out/” 的文件(代表编译后的制品文件,不展示);
  • 文件行数过长,不展示。

代码实现如下:

private boolean isVaildFile(FileDiff file) {
    if(file.isRenameFile() || file.isDeleteFile() || file.isTooLarge()) {
        return false;
    } else if(file.getNewPath().endsWith(".jar") || file.getNewPath().endsWith("/dev/null")) {
        return false;
    } else if(file.getNewPath().startsWith("out/")) {
        return false;
    } else if(file.getLines() > 50) {
        return false;
    } else {
        return true;
    }
}

这时,再去前端发送请求,可以看到,现在的字符串已经拆分成功了。

在这里插入图片描述

五、对接前端,处理样式冲突

将前端的 oldString 和 newString 进行绑定到v-code-diff 中后,出现了一个讨厌的问题——样式问题。

组件左右两栏的第一列长度超出了既定的高度,溢出了。如下图所示。

在这里插入图片描述
为了解决这个问题,我们进入浏览器的开发者模式,查看组件的样式表。

在这里插入图片描述

我们发现,v-code-diff 组件的表格是基于 tr/td 的,而表头一栏的数字是由 d2h-code-side-linenumber 样式决定的。仔细查看这个样式表,发现其 position 属性被确定了 “absolute”,原来问题出在这里。

回到前端代码样式表,使用 lang=“less” 更改原生样式。

<style lang="less" scoped>
	/deep/ .d2h-code-side-linenumber {
	  position: relative;
	}
</style>

刷新查看,问题解决!

在这里插入图片描述

Logo

前往低代码交流专区

更多推荐