借助 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 小时前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
瑶琴AI前端1 小时前
uniapp组件实现省市区三级联动选择
java·前端·uni-app
会发光的猪。1 小时前
如何在vscode中安装git详细新手教程
前端·ide·git·vscode
我要洋人死2 小时前
导航栏及下拉菜单的实现
前端·css·css3
科技探秘人3 小时前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人3 小时前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR3 小时前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香3 小时前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
q2498596933 小时前
前端预览word、excel、ppt
前端·word·excel
小华同学ai3 小时前
wflow-web:开源啦 ,高仿钉钉、飞书、企业微信的审批流程设计器,轻松打造属于你的工作流设计器
前端·钉钉·飞书