注册
web

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

引言


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


安装


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


用法


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


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



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

0.jpeg


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


原理解读


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


1.png


● 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 传值),其余代码是一些监听事件


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 即可


import { installReactDevtoolsHook } from '@locator/react-devtools-hook';
import { insertRuntimeScript } from './insertRuntimeScript';

installReactDevtoolsHook();
insertRuntimeScript();

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


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


  const locatorClientUrl = document.documentElement.dataset.locatorClientUrl;
delete document.documentElement.dataset.locatorClientUrl;

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


   if (!locatorClientUrl) {
return 'Locator client url not found';
}

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


import '@locator/runtime';

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


4.3 解读核心代码 runtime 模块


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


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


initRuntime.ts


packages/runtime/src/initRuntime.ts 的initRuntime 


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


  // 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框架编写的组件,包含了我们选中元素时出现的红框样式,以及所有的事件:


5.png


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


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 是怎样生成的:


  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 方法


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 方法


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 查看下生成的结构


image.png


惊奇的发现 _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 框架的原理实现,算是抛砖引玉,供大家参考。



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



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


作者:饮东
来源:juejin.cn/post/7358274599883653120

0 个评论

要回复文章请先登录注册