likes
comments
collection
share

体验在AI的辅助下完成一个自定义操作历史记录需求的实现

作者站长头像
站长
· 阅读数 38

今天遇到一个比较小众的应用场景,顺便来验证下AI对编程的有多少效率提升? 结论还是尚可,在解决思路上,可能因为 Prompt 提示的不够,导致产出不太满意。但一旦到了明确的实现环节,速度及正确率还是很高的。有效节省了基础编码的时间。

从另一个视角来看,也许持续迭代后,不久便可以接手很多工作,开发人员只需关注设计架构、模型,进行一些 CodeReview 提供改进建议,即可完成大部分编码工作。

直击现场

言归正传,来看这样一个场景:

体验在AI的辅助下完成一个自定义操作历史记录需求的实现

在Web端,用户首先通过进入A 页面,交互前往B >>C >> D >> B >> E 页面,此时在 E 页面,期待返回A 页面,而不是再次经历B 页面等等逐级返回到A 页面

这还不简单,页面E 返回时直接 go(-5) 到页面A,本文结束。

你说的没错,直指问题核心,给你大拇哥👍。

让我们再次观察上面的案例,可以看到有两个路径前往E 页面,分别是AB页面点击进入,但是B 页面的进入路径却存在B >> C >> D >> B 这样一层层套娃的可能。

那我们只需要动脑筋来记录这些跳转的操作,再通过一些计算得到 -5 这个(当前页面与前往页面的差异)具体数字,进行返回即可。

咨询室外

首先,我们需要先构思一下我们需要什么。

  1. 要拦截监听浏览器的 histroy 历史相关方法,来实现历史记录的保存
  2. 要知道当前页面处于历史记录的第几个
  3. 要知道想前往的页面处理历史记录的第几个
  4. 最好有一个简单的方法来调用,获取差异的具体数字

好了,让我们,移步咨询室。

咨询室

温馨提示,如您不感兴趣,可点击目录 SKIP 跳过该段剧情。

Round 1

我希望拦截 history 相关方法,记录历史记录数组

如果你希望拦截浏览器的 history 相关方法,并记录历史记录数组,可以使用 window.history.pushState()window.history.replaceState()window.addEventListener('popstate', handler) 方法。

下面是一个示例代码,演示如何拦截这些方法并记录历史记录数组:

const history = [];

// 拦截 pushState 方法
const originalPushState = window.history.pushState;
window.history.pushState = function(state, title, url) {
  // 在调用原始的 pushState 方法之前执行你的逻辑
  // 记录路由操作,例如将相关信息添加到历史记录数组

  const routeAction = {
    action: 'push',
    state: state,
    title: title,
    url: url
  };

  // 将路由操作添加到历史记录数组或进行其他处理
  // ...

  // 调用原始的 pushState 方法
  originalPushState.call(window.history, state, title, url);
};

// 拦截 replaceState 方法
const originalReplaceState = window.history.replaceState;
window.history.replaceState = function(state, title, url) {
  // 在调用原始的 replaceState 方法之前执行你的逻辑
  // 记录路由操作,例如将相关信息添加到历史记录数组

  const routeAction = {
    action: 'replace',
    state: state,
    title: title,
    url: url
  };

  // 将路由操作添加到历史记录数组或进行其他处理
  // ...

  // 调用原始的 replaceState 方法
  originalReplaceState.call(window.history, state, title, url);
};

// 监听 popstate 事件
window.addEventListener('popstate', event => {
  // 在 popstate 事件处理程序中执行你的逻辑
  // 更新历史记录数组或进行其他处理
  // ...
});

在这个示例中,我们使用 window.history.pushState()window.history.replaceState() 方法分别拦截了浏览器的 pushState()replaceState() 方法。在调用原始方法之前,你可以执行自己的逻辑来记录路由操作并将相关信息添加到历史记录数组。

我们还通过监听 popstate 事件来捕获浏览器返回按钮的点击。在 popstate 事件处理程序中,你可以执行你自己的逻辑来更新历史记录数组或进行其他处理。

请注意,这种方法只能拦截通过 JavaScript 调用的路由操作,而不能拦截用户直接在浏览器地址栏中输入或点击书签等方式进行的页面导航。如果你需要完全记录所有页面导航的历史记录,可能需要使用更复杂的技术,如使用路由库或状态管理库来管理历史记录。

别管我对不对,就说我快不快吧,这里只是定义了对应 routeAction 对象,并没有向 history 进行添加。

Round 2

帮我补全上面的代码,下面是我的需求:

  1. 使用对应方法,拦截 history 相关操作,记录到 historyRecords 这个数组。
  2. 提供一个 diffHistory(findURL, nowURL) 函数,根据findURL找到 historyRecords 中对应的的位置,与nowURL的位置进行相减,返回差异的数字。
  3. 提供一个 goHistory(findURL) 函数,调用 diffHistory 函数,nowURL 为当前 href, 跳转到对应的 findURL 历史位置。

下面是根据你的需求补全的代码:

const historyRecords = [];

// 拦截 pushState 方法
const originalPushState = window.history.pushState;
window.history.pushState = function(state, title, url) {
  // 在调用原始的 pushState 方法之前执行你的逻辑
  // 记录路由操作,例如将相关信息添加到历史记录数组

  const routeAction = {
    action: 'push',
    state: state,
    title: title,
    url: url
  };

  // 将路由操作添加到历史记录数组
  historyRecords.push(routeAction);

  // 调用原始的 pushState 方法
  originalPushState.call(window.history, state, title, url);
};

// 拦截 replaceState 方法
const originalReplaceState = window.history.replaceState;
window.history.replaceState = function(state, title, url) {
  // 在调用原始的 replaceState 方法之前执行你的逻辑
  // 记录路由操作,例如将相关信息添加到历史记录数组

  const routeAction = {
    action: 'replace',
    state: state,
    title: title,
    url: url
  };

  // 将路由操作添加到历史记录数组
  historyRecords.push(routeAction);

  // 调用原始的 replaceState 方法
  originalReplaceState.call(window.history, state, title, url);
};

// 监听 popstate 事件
window.addEventListener('popstate', event => {
  // 在 popstate 事件处理程序中执行你的逻辑
  // 更新历史记录数组或进行其他处理
  // ...
});

function diffHistory(findURL, nowURL) {
  const findIndex = historyRecords.findIndex(record => record.url === findURL);
  const nowIndex = historyRecords.findIndex(record => record.url === nowURL);
  
  return findIndex - nowIndex;
}

function goHistory(findURL) {
  const nowURL = window.location.href;
  const diff = diffHistory(findURL, nowURL);
  
  window.history.go(diff);
}

在这个补全的代码中,我们添加了对 pushStatereplaceState 方法的拦截,并在拦截函数中将路由操作记录到 historyRecords 数组中。同时,我们提供了 diffHistory 函数,用于计算指定 URL 在历史记录中的位置与当前 URL 位置的差异。最后,我们提供了 goHistory 函数,它调用 diffHistory 函数来计算差异,并使用 window.history.go() 方法进行页面跳转。

请注意,这里假设 findURLnowURL 都是完整的 URL 字符串。如果你只传递了路径部分,而不是完整的 URL,你可能需要根据实际情况进行适当的处理。

在明确并指定需求后,可以看到,🤔代码变多了

Round 3

replaceState 拦截里应该对historyRecords数组进行替换,而不是 push

对不起,我之前的回答有误。在 replaceState 的拦截中,应该替换 historyRecords 数组中对应的记录,而不是添加新的记录。

以下是修正后的代码:

// 已为尊贵的SVIP跳过10S重复内容
// ... 

  // 查找当前历史记录数组中最后一个替换操作的索引
  const lastIndex = historyRecords.findIndex(
    record => record.action === 'replace'
  );

  // 替换历史记录数组中最后一个替换操作的记录
  if (lastIndex !== -1) {
    historyRecords[lastIndex] = routeAction;
  } else {
    // 如果没有找到替换操作的记录,则添加新的记录
    historyRecords.push(routeAction);
  }

  // 调用原始的 replaceState 方法
  originalReplaceState.call(window.history, state, title, url);
};

// ...

function diffHistory(findURL, nowURL) {
  const findIndex = historyRecords.findIndex(record => record.url === findURL);
  const nowIndex = historyRecords.findIndex(record => record.url === nowURL);

  return findIndex - nowIndex;
}

function goHistory(findURL) {
  const nowURL = window.location.href;
  const diff = diffHistory(findURL, nowURL);

  window.history.go(diff);
}

在修正后的代码中,我们在 replaceState 的拦截中首先查找历史记录数组中最后一个替换操作的索引。如果找到了替换操作的记录,我们将其替换为新的记录;如果没有找到替换操作的记录,我们将新的记录添加到历史记录数组中。这样可以确保历史记录数组中只有一个替换操作的记录。

对于其他的 pushState、popstate 等操作,我们仍然将其添加到历史记录数组中。

怎么说呢,去街边吃串串如果拉肚子,我肯定要吐槽卫生堪忧,但是如果是去恰知名品牌还拉肚子,西吧,该不会是我肠胃的问题吧

😄 哈哈,这也许是我不懂提问的艺术,让我们来矫正吧

这里逻辑上有错误,应该是将最后一项的数据进行替换,而不是查找

const lastIndex = historyRecords.findIndex(    record => record.action === 'replace'  );

对不起,我之前的回答有误。你是正确的,我们应该替换历史记录数组中最后一项的数据,而不是查找最后一个替换操作的索引。

以下是修正后的代码:

// 已为尊贵的SVIP跳过10S重复内容
// ... 

  // 替换历史记录数组中最后一项的数据
  if (historyRecords.length > 0) {
    historyRecords[historyRecords.length - 1] = routeAction;
  } else {
    // 如果历史记录数组为空,则直接添加新的记录
    historyRecords.push(routeAction);
  }

  // 调用原始的 replaceState 方法
  originalReplaceState.call(window.history, state, title, url);

// ...

在修正后的代码中,我们在 replaceState 的拦截中将新的记录替换为历史记录数组中最后一项的数据。如果历史记录数组为空,则直接添加新的记录。这样可以确保替换操作只影响历史记录数组中的最后一项数据。

对于其他的 pushState、popstate 等操作,我们仍然将其添加到历史记录数组中。

不难看出还是没有判断关键条件 是否 replace 操作,一时语塞,先让我们看看其他问题

逻辑上有问题,假设有相同的历史记录时,nowIndex 的值可能与当前实际位置不一致,存在问题,你能提供修复方案并修复么

非常抱歉,我之前的回答仍然存在问题。你的提醒是正确的,当历史记录数组中存在相同的历史记录时,通过 findIndex 方法可能无法准确地获取到对应的索引。

下面是修复并改进代码的版本:

// 已为尊贵的SVIP跳过15S重复内容
// ... 

function diffHistory(findURL, nowURL) {
  const findIndex = findLastIndex(historyRecords, record => record.url === findURL);
  const nowIndex = findLastIndex(historyRecords, record => record.url === nowURL);

  return findIndex - nowIndex;
}


// 从数组末尾开始查找符合条件的元素索引
function findLastIndex(array, predicate) {
  for (let i = array.length - 1; i >= 0; i--) {
    if (predicate(array[i])) {
      return i;
    }
  }
  return -1;
}

在修复后的代码中,我添加了一个名为 findLastIndex 的辅助函数,用于从数组末尾开始查找符合条件的元素索引。这样可以确保获取到的索引始终是最后一次出现的位置,避免了相同历史记录导致的索引不准确问题。

通过使用 findLastIndex 函数,修复了 `diff

这显然不符合需求,打断施法,让我们给它施加压力

还是存在问题,假设历史记录是 A -> B -> C -> B ,然后通过 go 回到 A,再前进到B,这时获取的索引依旧错误,也许我们可以在更新数组是附加操作的时间戳,存在重复时,根据时间戳找到当前最近的一次

非常抱歉,我之前的回答仍然存在问题。你的提醒是正确的,当历史记录数组中存在相同的历史记录时,通过 findIndexfindLastIndex 方法无法准确地获取到对应的索引。

下面是修复并改进代码的版本,通过添加时间戳来解决重复记录的问

const historyRecords = [];

// 拦截 pushState 方法
const originalPushState = window.history.pushState;
window.history.pushState = function(state, title, url) {
  // 在调用原始的 pushState 方法之前执行你的逻辑
  // 记录路由操作,例如将相关信息添加到历史记录数组

  const timestamp = Date.now(); // 获取当前时间戳

  const routeAction = {
    // ...
    timestamp: timestamp // 添加时间戳
  };

  // 将路由操作添加到历史记录数组
  historyRecords.push(routeAction);

  // 调用原始的 pushState 方法
  originalPushState.call(window.history, state, title, url);
};

// ...
  
function diffHistory(findURL, nowURL) {
  const findTimestamp = findLastTimestamp(historyRecords, record => record.url === findURL);
  const nowTimestamp = findLastTimestamp(historyRecords, record => record.url === nowURL);

  const findIndex = historyRecords.findIndex(record => record.timestamp === findTimestamp);
  const nowIndex = historyRecords.findIndex(record => record.timestamp === nowTimestamp);

  return findIndex - nowIndex;
}

function goHistory(findURL) {
  const nowURL = window.location.href;
  const diff = diffHistory(findURL, nowURL);

  window.history.go(diff);
}

// 从数组末尾开始查找符合条件的元素的时间戳
function findLastTimestamp(array, predicate) {
  for (let i = array.length - 1; i >= 0; i--) {
    if (predicate(array[i])) {
      return array[i].timestamp;
    }
  }
  return -1;
}

修正后的代码在历史记录数组中添加了 timestamp 字段,记录每个历史记录的时间戳。在 diffHistory 函数中,通过首先找到对应历史记录的时间戳,然后再找到该时间戳在数组中的索引,来进行差异计算。

通过添加时间戳,确保了在存在重复历史记录时,能够正确地找到最近的一次记录。

还是压力不到位啊,让我们继续加点重量,连环追击

  • 有些麻烦,我们可以先根据时间戳排序,再进行查找即可

    已为尊贵的SVIP跳过花絮

  • 请优化一下代码,存在重复的代码逻辑

    已为尊贵的SVIP跳过花絮

  • handleRouteAction 函数有逻辑错误,应该只在 action === replace 时才进行数组最后一项替换操作,push 时应直接 push 数组
  • 好的,让我们再优化一下 pushState,replaceState 的拦截代码,这里有很多的重复,我们也许可以优化它
  • 我没有看到定义 originalMethods 变量

到这里,已经没有明显的错误代码了,接下来,也是持续的矫正优化

Round 4

我希望在每次操作加入 log 来方便 debug,并通过一个变量来控制它是否开启,另外优化为 class 语法

好的,根据你的要求,我对代码进行了相应的修改:

class RouterHistory {
  constructor() {
    this.historyRecords = [];
    this.logEnabled = false;
    this.originalMethods = {
      push: window.history.pushState,
      replace: window.history.replaceState
    };

    // 拦截 pushState 方法
    window.history.pushState = this.interceptRouteAction('push');

    // 拦截 replaceState 方法
    window.history.replaceState = this.interceptRouteAction('replace');

    // 监听 popstate 事件
    window.addEventListener('popstate', this.handlePopState.bind(this));
  }

  setLogEnabled(enabled) {
    this.logEnabled = enabled;
  }

  interceptRouteAction(action) {
    const self = this;
    return function(state, title, url) {
      const timestamp = Date.now();

      const routeAction = {
        action: action,
        state: state,
        title: title,
        url: url,
        timestamp: timestamp
      };

      if (action === 'replace' && self.historyRecords.length > 0) {
        self.historyRecords[self.historyRecords.length - 1] = routeAction;
      } else {
        self.historyRecords.push(routeAction);
      }

      self.log(`Intercepted ${action} - URL: ${url}`);

      self.originalMethods[action].call(window.history, state, title, url);
    };
  }

  handlePopState(event) {
    // 在 popstate 事件处理程序中执行你的逻辑
    // 更新历史记录数组或进行其他处理
    // ...
    this.log('popstate event triggered');
  }

  diffHistory(findURL, nowURL) {
    const sortedRecords = this.historyRecords.sort(
      (a, b) => a.timestamp - b.timestamp
    );

    const findIndex = sortedRecords.findIndex(record => record.url === findURL);
    const nowIndex = sortedRecords.findIndex(record => record.url === nowURL);

    return findIndex - nowIndex;
  }

  goHistory(findURL) {
    const nowURL = window.location.href;
    const diff = this.diffHistory(findURL, nowURL);

    window.history.go(diff);
    this.log(`Navigating to ${findURL}`);
  }

  log(message) {
    if (this.logEnabled) {
      console.log('[RouterHistory]', message);
    }
  }
}

// 创建 RouterHistory 实例
const routerHistory = new RouterHistory();

在这个优化后的代码中,我创建了一个名为 RouterHistory 的类,并在构造函数中初始化相关变量和拦截方法。我还添加了 setLogEnabled 方法,用于控制日志记录的开启与关闭。

interceptRouteAction 方法中,使用箭头函数来避免 this 上下文问题,并通过 self 变量引用当前实例。同时,我在适当的地方添加了日志记录,以便调试。

handlePopState 方法用于处理 popstate 事件,你可以在该方法中执行你的逻辑和更新历史记录数组等操作。

最后,我创建了一个 RouterHistory 的实例 routerHistory,你可以通过调用它的方法来操作历史记录和控制日志记录。

希望这次的优化满足了你的需求。如果还有其他问题,请随时提问。

后面当然又追加了完善许多需求,但要贴的字更多,这很不友善,因为流量很贵,心疼 Geigei 呀,所以就到这里吧

体验报告

最终的产出了210行代码,用时约1h

代码贡献比例,因为后面没什么怎么继续去补全,手动修修改改 30 行左右,占比 14%,这个数据是可以继续降低的,只需要投入时间成本和提升提问的艺术。

总体来说体验比之前尝试的CursorCodeGeeXCodeWhisperer 等编辑器AI补全要麻烦一些,不同的切入点,但是语义理解能力是领先这一众工具。

题外话:$20 的 Plus有多少人开了,点个赞统计一下可以不 😏

转载自:https://juejin.cn/post/7239268417059504189
评论
请登录