借助 LocatorJS ,快速定位本地代码

引言

前端coder在刚接触一个新项目时是十分迷茫的,修改一个小 bug 可能要从路由结构入手逐级查找。 LocatorJS 提供了一种更便捷的方式,你只需要按住预设的快捷键选中元素点击,就可以快速打开本地编辑器中的代码,是不是非常神奇?

安装

访问 google 商店进行插件安装 地址

用法

本文以 MacOS 系统为例, Win系统可以用 Control 键替代 options键使用

LocatorJS 是 Chrome 浏览器的一个扩展程序,使用很便捷,只需要进行下面的三个步骤:

  1. 运行一个本地项目(本文以 LocatorJS源码 的 React 项目为例)
  2. 打开项目访问本地链接(例如:http://localhost:3348 )
  3. 按住键盘的 option 键(win系统是 control)后选中某一个元素并点击

这时候,就会跳出一个是否打开的提示,点击 "打开Visual Studio Code" 后 元素所在的本地代码就会通过你的 VsCode (或者其他编辑器) 打开。是不是很神奇,那么它是怎么实现的呢?

原理解读

解读 Chrome 扩展程序,我们先打开 apps/extension/src/pages 路径,可以看到如下几个文件夹:

● Background 是放置后台代码的文件夹,本插件不涉及

● ClientUI 这里只有一行,引入了 @locator/runtime(本插件的核心代码)

● Content 放着插件与浏览器内容页面的代码,与页面代码一起执行

● Popup 文件夹下是点击浏览器插件图标弹出层的代码

4.1 解读 Content/index.ts

Content/index.ts 中最重要的代码是 injectScript 方法,主要做了两件事情,一个是创建了 Script 标签执行了 hook.bundle.js ,另一个是将 client.bundle.js 赋值给了 document.documentElement.dataset.locatorClientUrl(通过 Dom 传值),其余代码是一些监听事件

ts 复制代码
function injectScript() {
  const script = document.createElement('script');
  // script.textContent = code.default;
  script.src = browser.runtime.getURL('/hook.bundle.js');

  document.documentElement.dataset.locatorClientUrl =
    browser.runtime.getURL('/client.bundle.js');

  // This script runs before the <head> element is created,
  // so we add the script to <html> instead.
  if (document.documentElement) {
    document.documentElement.appendChild(script);
    if (script.parentNode) {
      script.parentNode.removeChild(script);
    }
  }
}

4.2 解读 hook.bundle.js

hook.bundle.js 是 hook 文件夹下的 index文件打包后的产物,因此我们去·看 apps/extension/src/pages/hook/index.ts 即可

ts 复制代码
import { installReactDevtoolsHook } from '@locator/react-devtools-hook';
import { insertRuntimeScript } from './insertRuntimeScript';

installReactDevtoolsHook();
insertRuntimeScript();

● installReactDevtoolsHook 会确保你的 react devtools扩展已安装 (没安装就install一个,猜测是仅涉及使用 API 的轻量版(笔者未深究))

● insertRuntimeScript 会对页面生命周期做一个监听,尝试加载 LocatorJS 的 runtime 组件, 在 insertRuntimeScript() 中,看到了这两行:

ts 复制代码
  const locatorClientUrl = document.documentElement.dataset.locatorClientUrl;
  delete document.documentElement.dataset.locatorClientUrl;

这个 locatorClientUrl 就是之前在 Content/index.ts 里传值的那个 client.bundle.js,这里笔者简单说下,在尝试加载插件的方法 tryToInsertScript() 第一行判断如下:

ts 复制代码
   if (!locatorClientUrl) {
      return 'Locator client url not found';
    }

这行判断其实已经可以推测出 client.bundle.js 的重要性了,它加载失败,整个插件直接返回错误信息了。 回过头来看向 ClientUI 文件夹下的 index.tsx 文件:

ts 复制代码
import '@locator/runtime';

至此,我们已经完成了 locatorJs 的加载逻辑推导,下一步我们讲揭开"定位器"的神秘面纱...

4.3 解读核心代码 runtime 模块

打开 packages/runtime/src/index.ts 文件

在这里我们看到不论是本地加载 runtime,还是浏览器加载扩展的方式都会去执行 initRuntime

initRuntime.ts

packages/runtime/src/initRuntime.ts 的initRuntime

这个文件中声明了一些全局样式,并用 shadow dom 的方式进行了全局的样式隔离,我们关注下底部的这几行代码即可:

ts 复制代码
  // This weird import is needed because:
  // SSR React (Next.js) breaks when importing any SolidJS compiled file, so the import has to be conditional
  // Browser Extension breaks when importing with "import()"
  // Vite breaks when importing with "require()"
  if (typeof require !== "undefined") {
    // eslint-disable-next-line @typescript-eslint/no-var-requires
    const { initRender } = require("./components/Runtime");
    initRender(layer, adapter, targets || allTargets);
  } else {
    import("./components/Runtime").then(({ initRender }) => {
      initRender(layer, adapter, targets || allTargets);
    });
  }

兼容了一下服务端渲染和 SolidJs 的引入方式,引入相对路径下的 ./components/Runtime

核心组件 Runtime.tsx

packages/runtime/src/components/Runtime.tsx 抽丝剥茧,我们终于找到了它的核心组件 Runtime,这是一个使用 SolidJs框架编写的组件,包含了我们选中元素时出现的红框样式,以及所有的事件:

我们重点关注点击事件 clickListener ,最后点击跳转的方法是 goToLinkProps

ts 复制代码
export function goToLinkProps(
  linkProps: LinkProps,
  targets: Targets,
  options: OptionsStore
) {
  const link = buildLink(linkProps, targets, options);
  window.open(link, options.getOptions().hrefTarget || HREF_TARGET);
}

采用逆推的方式,看 clickListener 事件里的 LinkProps 是怎样生成的:

ts 复制代码
  function clickListener(e: MouseEvent) {
      ...
      const elInfo = getElementInfo(target, props.adapterId);

      if (elInfo) {
        const linkProps = elInfo.thisElement.link;
        ...
      }
     ...
   }

同样的方式,我们去看看 getElementInfo 怎么返回的(过程略过),我们以 react 的实现为例,打开 packages/runtime/src/adapters/react/reactAdapter.ts, 查看 getElementInfo 方法

ts 复制代码
export function getElementInfo(found: HTMLElement): FullElementInfo | null {
  const labels: LabelData[] = [];

  const fiber = findFiberByHtmlElement(found, false);
  if (fiber) {
    ...
    const thisLabel = getFiberLabel(fiber, findDebugSource(fiber)?.source);
    ...
     return {
         thisElement: {
           box: getFiberOwnBoundingBox(fiber) || found.getBoundingClientRect(),
           ...thisLabel,
         },
         ...
      };
  }
  return null;
}

前面 goToLinkProps 使用的是 thisElement.link 字段, thisLabel 又依赖于 fiber 字段,等等! 这不是我们 react 玩家的老朋友 fiber 吗,我们查看一下生成它的 findFiberByHtmlElement 方法

ts 复制代码
export function findFiberByHtmlElement(
  target: HTMLElement,
  shouldHaveDebugSource: boolean
): Fiber | null {
  const renderers = window.__REACT_DEVTOOLS_GLOBAL_HOOK__?.renderers;
  const renderersValues = renderers?.values();
  if (renderersValues) {
    for (const renderer of Array.from(renderersValues) as Renderer[]) {
      if (renderer.findFiberByHostInstance) {
        const found = renderer.findFiberByHostInstance(target as any);
        console.log('found', found)
        if (found) {
          if (shouldHaveDebugSource) {
            return findDebugSource(found)?.fiber || null;
          } else {
            return found;
          }
        }
      }
    }
  }
  return null;
}

可以看到,这里是直接使用的 window 对象下的 __REACT_DEVTOOLS_GLOBAL_HOOK__ 属性做的处理,我们先打印一下 fiber 查看下生成的结构

惊奇的发现 _debugSource 字段里竟然包含了点击元素所对应本地文件的路径

我们到 goToLinkProps 方法里打印一下跳转的路径发现果然一致,只是实际跳转的路径加上了 vscode:// 开头,进行了协议跳转。

真相解读,_debugOwner 是怎么来的

一路砍瓜切菜终于要接近真相了,回顾代码我们其实只需要搞懂 window.REACT_DEVTOOLS_GLOBAL_HOOK 是怎么来的以及它做了什么,就可以收工了。

  1. _debugOwner 怎么来的?

    _debugOwner 是通过 window.REACT_DEVTOOLS_GLOBAL_HOOK 根据 HtmlElement 生成的 fiber 得来的, 它是 React Devtools 插件的全局变量 HOOK,这就是为什么 hook.bundle.js 要确保安装了 React Devtools

  2. REACT_DEVTOOLS_GLOBAL_HOOK 做了什么

    它是通过 @babel/plugin-transform-react-jsx-source 实现的,这个 plugin 可以在创建 fiber 的时候,将元素本地代码的位置信息保存下来,以 _debugSource 字段进行抛出

总结

LocatorJs 的 React 方案使用 React Devtools 扩展的全局 Hook,由 @babel/plugin-transform-react-jsx-source plugin 将元素所在代码路径写入 fiber 对象当中,通过 HtmlElement 查找到相对应的 fiber,取得本地代码的路径,随即可实现定位代码并跳转的功能。

结语

本文粗略的讲解了 LocatorJs 在 React 框架的原理实现,算是抛砖引玉,供大家参考。

篇幅原因,略过很多细节,感兴趣的朋友建议看看源码,结合调试学习

我是饮东,欢迎点赞关注,江湖再见

相关推荐
范特西是只猫1 小时前
echarts 自定义标注样式&自定义tooltip弹窗样式
前端·javascript·echarts
JohnsonXin1 小时前
【兼容性记录】video标签在 IOS 和 安卓中的问题
android·前端·css·ios·h5·兼容性
建群新人小猿1 小时前
CRMEB Pro版 DIY功能玩法即将升级,先来一睹为快!
前端·javascript·html
api773 小时前
1688商品详情API返回值中的售后保障与服务信息
java·服务器·前端·javascript·python·spring·pygame
赵广陆3 小时前
SprinBoot+Vue门诊管理系统的设计与实现
前端·javascript·vue.js·spring boot·maven
华山令狐虫3 小时前
el-tabs 样式修改
前端
史努比的大头5 小时前
前端开发深入了解webpack
前端
Dovir多多5 小时前
渗透测试入门学习——php与mysql数据库连接、使用session完成简单的用户注册、登录
前端·数据库·后端·mysql·安全·html·php
B.-5 小时前
Remix 学习 - @remix-run/react 中主要的 hooks
前端·javascript·学习·react.js·web
小刘|6 小时前
《实现 HTML 图片轮播效果》
前端·html