LocatorJS 源碼分析

LocatorJS 源碼分析

http://zoo.zhengcaiyun.cn/blog/article/locatorjs

前言

前段時間發現了一個好玩的東西 -- LocatorJS。它可以在瀏覽器界面快速定位到 IDE 裏的具體代碼,並且支持 React、Preact、Solid、Vue 和 Svelte 這些主流的框架。頓時覺得好神奇,簡直就是開發神器,YYDS!於是,馬上按照官方文檔,一步步在項目中進行操作一番。對於 react 項目,它提供了 2 種接入方式。第一種是基於 data-id,第二種是基於 react devtools 和 Chrome 插件。對於它的實現特別好奇,所以拉取它的源碼,進行探索一番。

準備工作

1、拉取源碼

把源碼拉到本地,可以看到如下圖目錄

該項目把 Chrome 插件和示例都放在 apps 文件夾裏面,packages 裏面放着 babel-jsx、locatorjs、runtime 等公共代碼。可以看到這個項目是使用 lerna 管理的多包項目,那需要用 lerna bootstrap 進行依賴安裝。

2、啓動項目

依賴安裝完畢,然後使用 yarn dev 啓動項目。可以看到 turbo 編譯速度飛快,端口信息很快就被日誌刷沒了。於是只能通過代碼裏查看,具體的示例在哪個端口。

我們以 vite-react-clean-project 項目爲例,找到對應的 package.json,發現它啓動的是 3348 端口,於是用瀏覽器打開 localhost:3348 頁面。

3、加載 Chrome 插件

因爲我們是採用的 Chrome 插件和 react devtools 進行代碼分析。所以必須安裝這 2 個插件。react devtools 如果沒有安裝,可以去 Chrome 插件市場裏搜索安裝。locatorjs Chrome 插件我們加載開發版本進行代碼調試。(進去 Chrome 擴展管理 -> 加載我們項目 apps/extension/build/development_chrome 這個文件夾)

源碼分析

以上準備工作完畢之後,你可能已經發現 locatorjs 可以用了。如果說不能正常使用,可以參考官網,安裝以下的 babel plugins。

那麼 Chrome 插件到底做了什麼?我們帶着問題去看源碼吧。

1、Chrome 插件

打開 apps/extension/src/pages 這個文件,發現裏面就是常規的插件代碼。

這個插件的執行入口就在 content.ts。如果對插件不熟悉的同學,可以參考 Extensions - Chrome Developers(https://developer.chrome.com/docs/extensions/) 插件開發網站。

1.1 apps/extension/src/pages/Content/index.ts

從上面代碼可以看出,在 content.ts 中把 hook.bundle.js 注入到當前頁面中,把 client.bundle.js 賦值給 document.documentElement.dataset.locatorClientUrl,其他代碼都是一些監聽事件,可以先不管。

1.2 apps/extension/src/pages/Hook/index.ts

hook.bundle.js 就是 hook.ts 打包之後的文件名,它主要做了 2 件事情,第一,確保 react devtools 已經被安裝了;第二,注入 runtime script。

1.3 apps/extension/src/pages/Hook/insertRuntimeScript.ts

該文件主要是監聽 DOMContentLoaded,然後注入我們上面 document.documentElement.dataset.locatorClientUrl 這個文件,也就是 client.bundle.js。

在 insertScript 方法中,作者還考慮到了 iframe 的情況

1.4 apps/extension/src/pages/ClientUI/index.ts

接着我們來看 client.js 到底做了什麼。興致勃勃地打開 ClientUI 文件夾,結果只有一句代碼。直接引入了 @locator/runtime,也就是 runtime 代碼。

這樣看來 Chrome 插件很簡單,就檢查了 react devtools 的安裝情況,然後動態注入 locator 的 runtime 代碼。接下來,我們就定位到 runtime 代碼。

2、locator/runtime

2.1 packages/runtime/src/index.ts

從代碼中可以看到,它就是我們在官網看到的 2 種不同的方式。不管我們是 Chrome 插件引入還是通過 @locator/runtime 引入,最終都會執行 initRuntime 函數,無非就是參數不一樣。

2.2 packages/runtime/src/initRuntime.ts

該文件中,它通過 shadow Dom 的方案去添加了一個 locatorjs 自己的全局樣式和容器,來隔離 CSS,以免對頁面進行影響。

接着,它又引入了 components/Runtime 組件,並且考慮到了 SSR 的情況。我們是客戶端渲染,所以走的是第二種情況。

2.3 packages/runtime/src/components/Runtime.tsx

在這個文件中終於看到了真面目,最終渲染的就是 Runtime 這個函數組件。該組件使用了  solidjs。函數最上面是一些變量的聲明。

接着是在 document 上綁定了 mouseover、keydown、keyup、click、 scroll 事件

當我們把鼠標浮到元素上,並且按住 option 按鍵的時候,就顯示出當前元素的表框。這時候觸發的就是 mouseover 和 keydown 事件。

mouseover 處理函數,就把當前的 element 選中;keydown 處理函數使 holdingModKey 爲 true。在這種情況下,頁面渲染的就是 MaybeOutline 組件

2.4 packages/runtime/src/components/MaybeOutline.tsx

在 MaybeOutline 組件裏面,主要的就是獲取當前 element 的信息。然後根據 elementInfo 去渲染紅色的外邊框。

2.5 packages/runtime/src/adapters/getElementInfo.tsx

接着我們來看 getElementInfo 這個方法到底做了什麼。可以看到,它用來適配器設計模式。根據適配 id 的不同,走不同的邏輯。現在我們來看 react 項目是怎麼獲取元素信息的。

2.6 packages/runtime/src/adapters/react/reactAdapter.ts

在這個方法裏,我們看到了熟悉的影子 fiber。這裏最重要的函數就是 findFiberByHtmlElement,通過命名也能知道。就是通過 html 元素去查詢 fiber 節點。

2.7 packages/runtime/src/adapters/react/findFiberByHtmlElement.ts

通過 findFiberByHostInstance 就可以找到當前元素的 fiber 節點。那 fiber 節點裏有哪些信息呢?我添加了打印信息。

通過下圖可以看到,_debugSource 裏面居然自帶了當前元素位置信息,還有 lineNumber、columnNumber,難怪可以具體定位到代碼中。有了這個信息,跳轉到 vscode 還不是輕而易舉。

跳轉的事件發生在 click 處理函數中,最終調用了 window.open 方法。vscode:// 這個是協議跳轉,electron 本身就支持。

那 _debugSource 的信息是怎麼來的呢?那是 @babel/plugin-transform-react-jsx-source 的功勞。

2.8 @babel/plugin-transform-react-jsx-source

從 babel 官網的示例可以看出,這個 plugin 可以把當前 tag 的位置信息添加到 __source 上。然後通過 React.createElement 把 __source 屬性掛到 _source 下面。

然後在創建 fiber 的時候,把元素的 _source 添加到 _debugSource。

2.9 packages/runtime/src/components/ComponentOutline.tsx

那元素外面的紅框是如何實現的呢?於是我們定位到 ComponentOutline.tsx

可以看到,它是通過 bbox 的屬性計算出了整個元素的外邊框。bbox 又是通過 fiber 元素的 getBoundingClientRect 計算出來的。到這裏就全部解開了 locatorjs 的神祕面紗了。

總結

通過查看這個源碼還是有很多收穫。第一,你能學到 Chrome 插件的開發;第二,solidjs 的使用,雖然本文沒有展開講;第三,shadow Dom 樣式隔離方案;第四,適配器模式的應用;第五,熟悉 react fiber。本文是基於 react devtools 方式的來解析 locatorjs 的原理。官方還有一種 data-id 來實現的方案,它會把位置信息綁定在 data-id 上,然後點擊的時候去處理信息,具體的源碼實現還是留給各位讀者自己去探索。

參考文獻:

Chrome Extensions content scripts - Chrome Developers(https://developer.chrome.com/docs/extensions/mv3/content_scripts/)

LocatorJS(https://www.locatorjs.com/)

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/_H4pcxlOjoQArtcZa30y7Q