diff.js使用指南

前言

最近在开发过程中遇到了需要diff文件内容或者大json的业务场景,发现了一个比较好用且经典的js库diff。这个库功能十分强大,不仅能够简洁地输出字符串结果,也能够输出规范化的数据结构方便二次开发。这里笔者针对这个库的文档进行翻译和简单的讲解,同时也会展示自己的测试demo。

库简介

diff是一个基于javascript实现的文本内容diff的库。它基于已发表论文中的算法An O(ND) Difference Algorithm and its Variations" (Myers, 1986).

安装

npm install diff --save

引用

//  不支持import 语法,也就是module引入
const jsDiff = require('diff');

API

  • JsDiff.diffChars(oldStr, newStr[, options]) 这个方法将比较两段文字,比较的维度是基于单个字符
    返回一个由描述改变的对象组成的列表。大致如下:

    image

    added表示是否是添加内容,removed表示是否为删除内容。共有的内容这两个属性都没有,value表示内容,count表示字符的个数(在某些用法中表示内容的行数)
    可选的配置属性ignoreCase: 标记为true时忽略字符的大小写,默认为false,这里给出一个测试例子:
    image

    文中例子的线上演示地址演示地址

  • JsDiff.diffWords(oldStr, newStr[, options]) 该方法比较两段文字,比较的维度是单词,忽略空格,返回一个由描述改变对象组成的列表,可选的配置属性ignoreCase: 同diffChars中一样,这里给出一个使用例子:

    image

  • JsDiff.diffWordsWithSpace(oldStr, newStr[, options]) 该方法比较两段文字,比较的维度是单词,同上一个方法不同的是,它将比较空格的差异,返回一个由描述改变的对象组成的列表。这里给出一个例子:

    image

  • JsDiff.diffLines(oldStr, newStr[, options]) 比较两段文字,比较的维度是行。可选的配置项:
    ignoreWhitespace:设置为true时,将忽略开头和结尾处的空格,在diffTrimmedLines中也有这个配置。
    newlineIsToken: 设置为true时,将换行符看作是分隔符。这样就可以独立于行内容对换行结构进行更改,并将其视为独立的(原文:This allows for changes to the newline structure to occur independently of the line content and to be treated as such, 这一句是机翻的,感觉不大准确)。总得来说,这样使得diffLines的输出对人类阅读(相较于其他对计算机更为友好的输出方式)更为友好,更加方便于比较差异。返回一个由描述改变的对象组成的列表。(这里返回的obj列表中,count表示这段内容的行数,下面的方法类似),接下来展示一个例子:

    image

  • sDiff.diffTrimmedLines(oldStr, newStr[, options]) 比较两段文字,比较的维度是行,忽略开头和结尾处的空格,返回一个由描述改变的对象组成的列表。实例截图:

    image

  • JsDiff.diffSentences(oldStr, newStr[, options]) 比较两段文字,比较的维度是句子。返回一个由描述改变的对象组成的列表。实例截图:

    image

  • JsDiff.diffCss(oldStr, newStr[, options]) 比较两段内容,比较基于css中的相关符号和语法。返回一个由描述改变的对象组成的列表。

  • JsDiff.diffJson(oldObj, newObj[, options]) 比较两个JSON对象,比较基于对象内部的key。这些key在json对象内的顺序,在比较时将不会影响结果。返回一个由描述改变的对象组成的列表。展示一个例子:

    image

  • JsDiff.diffArrays(oldArr, newArr[, options]) 比较两个数组,每一个元素使用严格等于来判定(===)。可选参数:comparator: function(left, right)用来进行相等性的比较,返回一个由描述改变的对象组成的列表。

  • JsDiff.createTwoFilesPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader) -创造一个统一的diff补丁输出。参数:

    • oldFileName: 移除内容在文件名部分输出的字符串
    • newFileName: 增添内容在文件名部分输出的字符串
    • oldStr: 原始的字符串(作为基准)
    • newStr: 比较内容的字符串
    • oldHeader: 在老文件头部新增的信息
    • newHeader: 在新文件头部新增的信息
    • options: 一个描述配置的对象,目前仅支持context,用来描述应该展示context的多少行
      这里展示一个例子:
      image

      这里可以看到,该方法返回的是已经格式化的可直接输出的字符串,方便直接展示。
  • JsDiff.createPatch(fileName, oldStr, newStr, oldHeader, newHeader) -创造一个统一的diff补丁输出,该方法的使用和JsDiff.createTwoFilesPatch几乎一致,唯一的区别是oldFileName等于newFileName

  • JsDiff.structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options) 返回一个由描述具体变化的对象构成的数组。这个方法类似于createTwoFilesPatch,但是返回了一个适合于开发者后续处理的数据结构。其参数跟createTwoFilesPatch保持一致,返回的数据类似于如下:

    image

    与之对应的应用实例如下:
    image

  • JsDiff.applyPatch(source, patch[, options]) 使用一个统一的diff补丁。该方法会返回一个应用了补丁的新版本字符串。这里的补丁(patch)可能是字符串形式的diff或者parsePatchstructuredPatch方法返回的输出。可选的配置项有如下:

    • fuzzFactor: 拒绝应用补丁之前允许比较的内容的行数。默认是0
    • compareLine(lineNumber, line, operation, patchContent) 用来比较给定的行内容在应用补丁时是否应该被认定为相等。默认是使用严格相等来比较的,但是这容易与fuzzier比较相冲突。当内容应该被拒绝时返回false。
  • JsDiff.applyPatches(patch, options) 应用一个或者多个补丁。这个方法将会迭代补丁的内容并且将其应用在回调中传入的内容上。每个补丁被使用的整体工作流程是:

    • options.loadFile(index, callback) 调用者应该加载文件的内容并且将其传递给回调(callback(err, data))。传入一个err将会中断未来补丁的执行
    • options.patched(index, content, callback) 该方法在每个补丁被使用时调用。传入一个err将会中断未来补丁的执行
  • JsDiff.parsePatch(diffStr) 将一个补丁解析为结构化数据。返回一个由补丁解析而来的JSON对象,该方法适合同applyPatch配合使用。该方法返回的内容同JsDiff.structuredPatch返回的内容结构上一致。

  • convertChangesToXML(changes) 转换一个changes的列表到序列化的XML格式

以上的所有可以接受一个可选的回调的方法,在该参数(callback)被省略时该方法工作在同步模式,当这个参数被传入时工作在异步模式。这使得能够处理更大的范围diff而不会使得事件流被长期挂起。callback要么作为最后一个参数被直接传入要么作为options中的一个属性被传入。

Change Objects

上面的许多方法都会返回change对象(前文翻译成描述改变的对象),这些对象通常包含以下的属性:

  • value: 文本内容
  • added: 如果是文本被插入新内容的话,该值为true
  • removed: 如果是文本被移除内容的话,该值为true

使用小结

上述的内容主要是基于官方的文档。这里结合笔者的实战经验来说说使用的细节。JsDiff的方法绝大多数的入参都是字符串(除了JsDiff.diffJson,JsDiff.diffArrays等少数几个api)。用于比较字符,单词,句子或者文本文件时,需要将以上内容都转换成字符串,句子或者文本文件默认使用\n作为分隔符。输出通常是描述变化的对象组成的Array,方便二次开发,如果只是想简单输出文件之间的diff,可以直接使用JsDiff.createTwoFilesPatch支持输出格式化的内容,不用额外处理。关于二次开发输出满足需求的样式,这里给一个简单的例子:

import React from 'react';
const jsDiff = require('diff');
import s from './index.css';
import cx from 'classnames';

const str1 = 'guanlanluditie';
const str2 = 'smartguanlanluditie';
const diffArr = jsDiff.diffChars(str1, str2);

const charColorMap = {
    'add': s.charAdd,
    'removed': s.charRemoved,
}

export default class Text extends React.Component {
    render() {
        return <div className={s.result}>
            比较结果: 
            {diffArr.map((item, index) => {
                const { value, added, removed } = item;
                const type = added ? 'add' : (removed ? 'removed' : '')
                return <span key={index} className={cx(charColorMap[type], s.charPreWrap)}>{value}</span>
                })
            }
        </div>
    }
}

关于使用diff库实现类似于github的文件diff效果,可以参考笔者的一个仓库,也就是上文中的演示代码,仓库地址,具体的实现思路后续会出一篇文详述,稍候。

参考资料与相关链接

diff库官方文档
演示站点
演示站点代码仓库

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272