这是点击一个按钮随后显示一个Modal弹框,分为上中下三部分。
首先我们需要下载一个依赖包diff
npm install diff --save
这个引入的方式只能使用commonJS的形式引入,而不能使用ES6的形式引入
const Diff = require('diff');
里面涉及到的方法很多:
参考链接 :https://maokun.blog.csdn.net/article/details/104092182
我这里只用到了其中一个对比方法:
Diff.diffChars(oldStr, newStr[, options])
options:ignoreCase:true忽略大小写差异。默认为false。
返回值 :
分析:
返回数组对象,每个对象都有count和value属性
count:字符个数;
value:具体字符;
新增或者删除的对象属性:
added:新增标识,新增为true,否为undefined;
renoved:删除标识,删除为true,否为undefined;
基本使用版本:
拿来的例子
就是将读出来的数组进行循环,随后创建标签,最后整体追加到页面中进行展示。
在html中:
<pre id="display"></pre>
<script src="diff.js"></script>
<script>
var one = 'beep boop',
other = 'beep boob blah',
color = '',
span = null;
var diff = Diff.diffChars(one, other),
display = document.getElementById('display'),
fragment = document.createDocumentFragment();
diff.forEach(function(part){
// green for additions, red for deletions
// grey for common parts
color = part.added ? 'green' :
part.removed ? 'red' : 'grey';
span = document.createElement('span');
span.style.color = color;
span.appendChild(document
.createTextNode(part.value));
fragment.appendChild(span);
});
display.appendChild(fragment);
</script>
react第一版
此外我们还可以看到读文件后读出的换行符,所有我们还能根据这个换行符对整理好的数据进行展示:
import React, { Fragment } from 'react'
import { Modal, Divider, Spin } from 'antd';
import Button from '@com/Button'
const Diff = require('diff');
export default class DiffString extends React.Component {
static defaultProps = {
isModalVisible: false,
title: "预览文件差异",
changeIsModalVisibleState: () => { },
oldStr: "",
newStr: "",
contextBoxLoading: false,
}
/**
* oldStr 为上次文件
*
* newStr 为最新文件
*
* contextBoxLoading 加载文件的loading
*
* isModalVisible modal显示与否
*
*/
handleCancel() {
this.props.changeIsModalVisibleState()
}
render() {
let { title, width, isModalVisible, contextBoxLoading, oldStr ,newStr, ...rest } = this.props
console.log(Diff.diffChars(oldStr, newStr))
return (
<Modal
title={<div>{title}<h4 style={{ float: "right" }}>灰色:<span style={{ color: "grey" }}>无变化</span>;绿色:<span style={{ color: "green" }}>新增</span>;红色:<span style={{ color: "red" }}>删除</span></h4></div>}
visible={isModalVisible}
maskClosable={true}
style={{ top: "20%", minWidth: "1000px" }}
closable={false}
width={width}
{...rest}
footer={[
<Button rsType="noIcon" key={1} title="关闭" onClick={() => this.handleCancel()}></Button>
]}
>
<Spin tip="配置文件获取中..." spinning={contextBoxLoading} size="large" style={{ width: "100%", height: '100%' }}>
<div style={{ height: "600px", overflowY: "scroll" }}>
<div style={{ wordBreak: "break-all", display: "flex" }}>
<div style={{ width: "50%", paddingLeft: "30px" }}>
<h2 style={{ fontWeight: "700" }}>已生效配置</h2>
{Diff.diffWordsWithSpace(newStr, oldStr).map((item, index) => {
return (
<Fragment key={index} >
<span style={{
fontSize: "0",
marginBottom: "0",
padding: "0",
lineHeight: "40px",
color: item.added ? 'red' :
item.removed ? 'green' : 'grey',
fontWeight: item.added ? '600' :
item.removed ? '600' : 'normal',
}}>{!item ? null : item.removed ? null : item.value.split('\n').map((i, indexx) => {
return <Fragment key={indexx} >
{i && <span style={{ marginBottom: "0", lineHeight: "22px", fontSize: "16px", whiteSpace: "pre" }} key={indexx}>{i}</span>}
{indexx !== item.value.split('\n').length - 1 ? <br /> : null}
</Fragment>
})} </span>
</Fragment>
)
})}
</div>
<Divider type="vertical" style={{ width: "10px", height: "auto" }} />
<div style={{ flex: "1", paddingLeft: "20px" }}>
<h2 style={{ fontWeight: "700" }}>待下发</h2>
{Diff.diffWordsWithSpace(oldStr, newStr).map((item, index) => {
return (
<Fragment key={index} >
<span style={{
fontSize: "0",
marginBottom: "0",
padding: "0",
lineHeight: "40px",
color: item.added ? 'green' :
item.removed ? 'red' : 'grey',
fontWeight: item.added ? '600' :
item.removed ? '600' : 'normal',
}}>{!item ? null : item.removed ? null : item.value.split('\n').map((i, indexx) => {
return <Fragment key={indexx} >
{console.log(i, item)}
{i && <span style={{ marginBottom: "0", lineHeight: "22px", fontSize: "16px", whiteSpace: "pre" }} key={indexx}>{i}</span>}
{indexx !== item.value.split('\n').length - 1 ? <br /> : null}
</Fragment>
})} </span>
</Fragment>
)
})}
</div>
</div>
</div>
</Spin>
</Modal>
)
}
}
这里其实已经封装成了一个组件,使用了antd的Modal组件。
里面也是对数据进行循环,对样式进行控制,对内容进行切割,换行展示。
react第二版
最终我选择了使用行比较,最终代码如下,本人最终目的是为了留笔记,所以直接粘贴代码了:
/*
* @Descripttion:
* @version:
* @Author: ZhangJunQing
* @Date: 2021-11-12 17:35:44
* @LastEditors: ZhangJunQing
* @LastEditTime: 2022-01-12 15:08:08
*/
import React, { Fragment } from 'react'
import { Modal, Divider, Spin } from 'antd';
import Button from '@com/Button'
const Diff = require('diff');
export default class DiffString extends React.Component {
static defaultProps = {
isModalVisible: false,
title: "预览文件差异",
changeIsModalVisibleState: () => { },
oldStr: "",
newStr: "",
contextBoxLoading: false,
}
/**
* oldStr 为上次文件 已生效配置
*
* newStr 为最新文件 待下发
*
* contextBoxLoading 加载文件的loading
*
* isModalVisible modal显示与否
*
*/
state = {
isSplitBRFlag: true,//是否为 对齐查看 这个变量目前不做动态改变 和changeisSplitBRFlagFun方法配合
isOriginFlag: false
}
// 源文件切换变量
changeOriginFileFun = () => {
this.setState({
isOriginFlag: !this.state.isOriginFlag
})
}
// 是否为 对齐查看
changeisSplitBRFlagFun = () => {
this.setState({
isSplitBRFlag: !this.state.isSplitBRFlag
})
}
handleCancel() {
this.props.changeIsModalVisibleState()
}
render() {
let { isSplitBRFlag, isOriginFlag } = this.state
let { title, width, isModalVisible, contextBoxLoading, oldStr, newStr, ...rest } = this.props
// console.log(Diff.diffLines(oldStr.replace(/\r/ig, ''), newStr.replace(/\r/ig, '')), "zjqqq")
return (
<Modal
title={<div>{title}<h4 style={{ float: "right" }}>灰色:<span style={{ color: "grey" }}>无变化</span>;绿色:<span style={{ color: "green" }}>新增</span>;红色:<span style={{ color: "red" }}>删除</span></h4></div>}
visible={isModalVisible}
maskClosable={true}
style={{ top: "20%", minWidth: "1000px" }}
closable={false}
width={width}
{...rest}
footer={[
<Button rsType="noIcon" key={1} title={!isOriginFlag ? "切换源文件" : "切换对比文件"} onClick={() => this.changeOriginFileFun()}></Button>,
<Button rsType="noIcon" key={2} title="关闭" onClick={() => this.handleCancel()}></Button>
]}
>
<Spin tip="配置文件获取中..." spinning={contextBoxLoading} size="large" style={{ width: "100%", height: '100%' }}>
<div style={{ height: "600px", overflowY: "scroll" }}>
<div style={{ wordBreak: "break-all", display: "flex" }}>
<div style={{ width: "50%", paddingLeft: "30px" }}>
<h2 style={{ fontWeight: "700" }}>已生效配置</h2>
{Diff.diffLines(oldStr.replace(/\r/ig, ''), isOriginFlag ? oldStr.replace(/\r/ig, '') : newStr.replace(/\r/ig, '')).map((item, index) => {
return (
<Fragment key={index} >
<span style={{
fontSize: "0",
marginBottom: "0",
padding: "0",
lineHeight: "40px",
color: item.added ? 'green' :
item.removed ? 'red' : 'grey',
fontWeight: item.added ? '600' :
item.removed ? '600' : '600',
}}>{!item ? null : item.added ? (isSplitBRFlag ?
item.value.split('\n').map((child, idx) => {
return <Fragment key={idx} >
{child && <span style={{ marginBottom: "0", lineHeight: "22px", fontSize: "16px", whiteSpace: "pre", color: "transparent", userSelect: "none" }} key={idx}>{child}</span>}
{idx !== item.value.split('\n').length - 1 ? <br /> : null}
</Fragment>
}) : null)
: item.value.split('\n').map((i, indexx) => {
return <Fragment key={indexx} >
{i && <span style={{ marginBottom: "0", lineHeight: "22px", fontSize: "16px", whiteSpace: "pre" }} key={indexx}>{i}</span>}
{indexx !== item.value.split('\n').length - 1 ? <br /> : null}
</Fragment>
})} </span>
</Fragment>
)
})}
</div>
<Divider type="vertical" style={{ width: "10px", height: "auto", minHeight: '600px' }} />
<div style={{ flex: "1", paddingLeft: "20px" }}>
<h2 style={{ fontWeight: "700" }}>待下发</h2>
{Diff.diffLines(isOriginFlag ? newStr.replace(/\r/ig, '') : oldStr.replace(/\r/ig, ''), newStr.replace(/\r/ig, '')).map((item, index) => {
return (
<Fragment key={index} >
<span style={{
fontSize: "0",
marginBottom: "0",
padding: "0",
lineHeight: "40px",
color: item.added ? 'green' :
item.removed ? 'red' : 'grey',
fontWeight: item.added ? '600' :
item.removed ? '600' : '600',
}}>{!item ? null : item.removed ? (isSplitBRFlag ?
item.value.split('\n').map((child, idx) => {
return <Fragment key={idx} >
{child && <span style={{ marginBottom: "0", lineHeight: "22px", fontSize: "16px", whiteSpace: "pre", color: "transparent", userSelect: "none" }} key={idx}>{child}</span>}
{idx !== item.value.split('\n').length - 1 ? <br /> : null}
</Fragment>
}) : null)
: item.value.split('\n').map((i, indexx) => {
return <Fragment key={indexx} >
{i && <span style={{ marginBottom: "0", lineHeight: "22px", fontSize: "16px", whiteSpace: "pre" }} key={indexx}>{i}</span>}
{indexx !== item.value.split('\n').length - 1 ? <br /> : null}
</Fragment>
})} </span>
</Fragment>
)
})}
</div>
</div>
</div>
</Spin>
</Modal>
)
}
}
上面做了两个操作 一个是切换源文件,就是只看两个文件,这个也进行了对比,目的是为了要整体格式,还有一种就是隐藏掉的那个按钮,出来的效果没有多余空行,上面多余的空行实现用了透明色和css不可选中的样式。
代码:
/*
/*
* @Descripttion:
* @version:
* @Author: ZhangJunQing
* @Date: 2021-11-12 17:35:44
* @LastEditors: ZhangJunQing
* @LastEditTime: 2022-01-18 14:31:20
*/
import React, { Fragment } from 'react'
import { Modal, Divider, Spin } from 'antd';
import Button from '@com/Button'
import { string } from 'prop-types';
const Diff = require('diff');
export default class DiffString extends React.Component {
static defaultProps = {
isModalVisible: false,
title: "预览文件差异",
changeIsModalVisibleState: () => { },
oldStr: "",
newStr: "",
contextBoxLoading: false,
}
/**
* oldStr 为上次文件 已生效配置
*
* newStr 为最新文件 待下发
*
* contextBoxLoading 加载文件的loading
*
* isModalVisible modal显示与否
*
*/
state = {
isSplitBRFlag: true,//是否为 对齐查看 这个变量目前不做动态改变 和changeisSplitBRFlagFun方法配合
isOriginFlag: false,
defaultAddColor: "green",
defaultDelColor: "red",
defaultColor: "grey"
}
// 源文件切换变量
changeOriginFileFun = () => {
this.setState({
isOriginFlag: !this.state.isOriginFlag,
})
}
// 是否为 对齐查看
changeisSplitBRFlagFun = () => {
this.setState({
isSplitBRFlag: !this.state.isSplitBRFlag
})
}
/**
* 每次进来都显示 对比文件页面
*/
handleCancel() {
this.setState({
isOriginFlag: false,//默认还是显示 对比文件页面
})
this.props.changeIsModalVisibleState()
}
/**
* 这个方法 事件委托
*
* 点击p标签 改变当前p标签的样式
* 以及更改对面等行的p标签的样式
*
* 点击左边 有背景的为defaultDelColor
* 点击右边 有背景的为defaultAddColor
* 不是增加不是删除的的为defaultColor
*
* 2px solid #fff 默认边框
* 如果只给当前点击的加边框 对引起整个页面抖动
*
* 所有每一个p标签都添加一个边框 颜色为底色
*
* 这样每次点击的时候把所有的颜色都重置
*
* 然后再将点击的p换颜色 即可
*/
changeRightLineFun = (flag, ev) => {
let { defaultAddColor, defaultDelColor } = this.state
let RightFileId = flag === 'right' ? document.getElementById('RightFileId') : document.getElementById('LeftFileId')
let LeftFileId = flag === 'right' ? document.getElementById('LeftFileId') : document.getElementById('RightFileId')
let LeftFileIdP = LeftFileId.getElementsByTagName('p')
let RightFileIdP = RightFileId.getElementsByTagName('p')
ev = ev || window.event;
let target = ev.target || ev.srcElement;
let index;
// 点击空P标签
if (target.style.userSelect === 'none') {
return false;
}
if (target.nodeName.toLowerCase() == "p") {
for (let i = 0; i < RightFileIdP.length; i++) {
if (RightFileIdP[i] === target)
index = i;
RightFileIdP[i].style.border = "2px solid #fff"
}
for (let i = 0; i < LeftFileIdP.length; i++) {
if (LeftFileIdP[i] === target)
index = i;
LeftFileIdP[i].style.border = "2px solid #fff"
}
if (flag === 'right') {
if (target.parentNode.style.color === defaultAddColor) {
target.style.border = `2px solid ${defaultAddColor}`;
} else {
target.style.border = "2px solid #000";
}
} else {
if (target.parentNode.style.color === defaultDelColor) {
target.style.border = `2px solid ${defaultDelColor}`;
} else {
target.style.border = "2px solid #000";
}
}
LeftFileIdP[index].style.border = "2px solid #000";
}
}
render() {
let { isSplitBRFlag, isOriginFlag, defaultAddColor, defaultDelColor, defaultColor } = this.state
let { title, width, isModalVisible, contextBoxLoading, oldStr, newStr, fileName, ...rest } = this.props
// console.log(Diff.diffLines(oldStr.replace(/\r/ig, ''), newStr.replace(/\r/ig, '')), "zjqqq")
const styleObj = {
lineHeight: "22px",
border: "2px solid #fff",
paddingTop: "2px",
paddingBottom: "2px",
borderRadius: "4px",
fontSize: "16px",
paddingLeft: "10px",
marginBottom: "0",
}
return (
<Modal
title={<div style={{ display: "flex", justifyContent: "space-between" }}><span style={{ fontWeight: "500" }}>{title}</span><span style={{ paddingLeft: "50px" }}>{fileName ? `文件名称:${fileName}` : ""}</span><h4 style={{ float: "right" }}>灰色:<span style={{ color: defaultColor }}>无变化</span>;绿色:<span style={{ color: defaultAddColor }}>新增</span>;红色:<span style={{ color: defaultDelColor }}>删除</span></h4></div>}
visible={isModalVisible}
maskClosable={true}
style={{ top: "20%", minWidth: "1000px" }}
closable={false}
width={width}
{...rest}
footer={[
<Button rsType="noIcon" key="1" title={!isOriginFlag ? "查看源文件" : "查看对比文件"} onClick={() => this.changeOriginFileFun()}></Button>,
// ,
<Button rsType="noIcon" key="2" title="关闭" onClick={() => this.handleCancel()}></Button>
]}
>
<Spin tip="配置文件获取中..." spinning={contextBoxLoading} size="large" style={{ width: "100%", height: '100%' }}>
{/* 绑定 key 目前这个key有两种形式 false true 字符的形式 正好对应 查看源文件和对比文件两个key */}
<div style={{ height: "600px", overflowY: "scroll" }} key={String(isOriginFlag)}>
{/* // 为了每次都处于文件顶端 */}
<div style={{ wordBreak: "break-all", display: "flex" }} >
<div style={{ width: "50%", paddingLeft: "20px" }} id="LeftFileId">
<h2 style={{ fontWeight: "700" }}>已生效配置</h2>
{Diff.diffLines(oldStr.replace(/\r/ig, ''), isOriginFlag ? oldStr.replace(/\r/ig, '') : newStr.replace(/\r/ig, '')).map((item, index) => {
return (
<Fragment key={index} >
<div
onClick={(e) => { this.changeRightLineFun("left", e) }}
style={{
fontSize: "0",
marginBottom: "0",
padding: "0",
lineHeight: "10px",
color: item.added ? defaultAddColor :
item.removed ? defaultDelColor : defaultColor,
fontWeight: item.added ? '600' :
item.removed ? '600' : '600',
}}>{!item ? null : item.added ? (isSplitBRFlag ?
item.value.split('\n').map((child, idx) => {
return <Fragment key={idx} >
{child && <p style={{ whiteSpace: "pre", color: "transparent", userSelect: "none", ...styleObj }} >{child}</p>}
{idx !== item.value.split('\n').length - 1 ? <br /> : null}
</Fragment>
}) : null)
: item.value.split('\n').map((i, idx) => {
return <Fragment key={idx} >
{i && <p style={{ backgroundColor: `${item.removed ? "rgb(255,227,227)" : "#fff"}`, whiteSpace: "pre", cursor: 'pointer', ...styleObj }} >{i}</p>}
{idx !== item.value.split('\n').length - 1 ? <br /> : null}
</Fragment>
})} </div>
</Fragment>
)
})}
</div>
<Divider type="vertical" style={{ width: "10px", height: "auto", minHeight: '600px' }} />
<div style={{ flex: "1", paddingLeft: "10px", paddingRight: "10px" }} id="RightFileId" >
<h2 style={{ fontWeight: "700" }}>待下发</h2>
{Diff.diffLines(isOriginFlag ? newStr.replace(/\r/ig, '') : oldStr.replace(/\r/ig, ''), newStr.replace(/\r/ig, '')).map((item, index) => {
return (
<Fragment key={index} >
<div
onClick={(e) => { this.changeRightLineFun("right", e) }}
style={{
fontSize: "0",
marginBottom: "0",
padding: "0",
lineHeight: "10px",
color: item.added ? defaultAddColor :
item.removed ? defaultDelColor : defaultColor,
fontWeight: item.added ? '600' :
item.removed ? '600' : '600',
}}>{!item ? null : item.removed ? (isSplitBRFlag ?
item.value.split('\n').map((child, idx) => {
return <Fragment key={idx} >
{child && <p style={{ whiteSpace: "pre", color: "transparent", userSelect: "none", ...styleObj }} >{child}</p>}
{idx !== item.value.split('\n').length - 1 ? <br /> : null}
</Fragment>
}) : null)
: item.value.split('\n').map((i, idx) => {
return <Fragment key={idx} >
{i && <p style={{ backgroundColor: `${item.added ? "rgb(221,255,221)" : "#fff"}`, whiteSpace: "pre", cursor: 'pointer', ...styleObj }} key={idx}>{i}</p>}
{idx !== item.value.split('\n').length - 1 ? <br /> : null}
</Fragment>
})} </div>
</Fragment>
)
})}
</div>
</div>
</div>
</Spin>
</Modal>
)
}
}
加了一个点击事件,利用了事件委托,当然这里没有考虑效率问题,直接使用了事件委托写了。
总结来说diff这个npm还是很好用的,里面有很多的方法,可以按照字符比较、按照单词比较、按照行比较,但是传入的两个字符串,自己首先要先看看,不然由于后端传来的字符串的\r\n问题让我苦恼了很久,所以上面我统一过滤了\r,如果具体指导怎么用的,还是找官方文档吧。
在线测试链接:
http://tangshisanbaishou.xyz/diff/index.html