<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          我點(diǎn)擊頁面元素,VSCode 乖乖打開了組件?原理揭秘。

          共 8483字,需瀏覽 17分鐘

           ·

          2020-12-07 21:24

          前言

          在大型項(xiàng)目開發(fā)中,經(jīng)常會遇到這樣一個(gè)場景,QA 丟給你一個(gè)出問題的鏈接,但是你完全不知道這個(gè)頁面 & 組件對應(yīng)的文件位置。

          這時(shí)候如果可以點(diǎn)擊頁面上的組件,在 VSCode 中自動跳轉(zhuǎn)到對應(yīng)文件,并定位到對應(yīng)行號豈不美哉?

          react-dev-inspector[1] 就是應(yīng)此需求而生。

          使用非常簡單方便,看完這張動圖你就秒懂:

          可以在 預(yù)覽網(wǎng)站[2] 體驗(yàn)一下。

          使用方式

          這個(gè)插件功能很強(qiáng)大,代碼也寫得很漂亮,唯一的缺點(diǎn)就是文檔不是很完善,我閱讀了源碼總結(jié)了成功接入這個(gè)插件需要的幾個(gè)步驟,缺一不可。

          簡單來說就是三步:

          1. 構(gòu)建時(shí)
            • 需要加一個(gè) webpack loader 去遍歷編譯前的的 AST 節(jié)點(diǎn),在 DOM 節(jié)點(diǎn)上加上文件路徑、名稱等相關(guān)的信息 。
            • 需要用 DefinePlugin 注入一下項(xiàng)目運(yùn)行時(shí)的根路徑,后續(xù)要用來拼接文件路徑,打開 VSCode 相應(yīng)的文件。
          2. 運(yùn)行時(shí):需要在 React 組件的最外層包裹 Inspector 組件,用于在瀏覽器端監(jiān)聽快捷鍵,彈出 debug 的遮罩層,在點(diǎn)擊遮罩層的時(shí)候,利用 fetch 向本機(jī)服務(wù)發(fā)送一個(gè)打開 VSCode 的請求。
          3. 本地服務(wù):需要啟動 react-dev-utils 里的一個(gè)中間件,監(jiān)聽一個(gè)特定的路徑,在本機(jī)服務(wù)端執(zhí)行打開 VSCode 的指令。

          下面簡單分析一下這幾步到底做了什么。

          原理簡化

          構(gòu)建時(shí)

          首先如果在瀏覽器端想知道這個(gè)組件屬于哪個(gè)文件,那么不可避免的要在構(gòu)建時(shí)就去遍歷代碼文件,根據(jù)代碼的結(jié)構(gòu)解析生成 AST,然后在每個(gè)組件的 DOM 元素上掛上當(dāng)前組件的對應(yīng)文件位置和行號,所以在開發(fā)環(huán)境最終生成的 DOM 元素是這樣的:

          <div
          ??data-inspector-line="11"
          ??data-inspector-column="4"
          ??data-inspector-relative-path="src/components/Slogan/Slogan.tsx"
          ??class="css-1f15bld-Description?e1vquvfb0"
          >

          ??<p
          ????data-inspector-line="44"
          ????data-inspector-column="10"
          ????data-inspector-relative-path="src/layouts/index.tsx"
          ??>

          ????Inspect?react?components?and?click?will?jump?to?local?IDE?to?view?component
          ????code.
          ??p>
          div>

          這樣就可以在輸入快捷鍵的時(shí)候,開啟 debug 模式,讓 DOM 在 hover 的時(shí)候增加一個(gè)遮罩層并展示組件對應(yīng)的信息:

          這一步通過 webpack loader 拿到未編譯JSX 源碼,再配合 AST 的處理就可以完成。

          運(yùn)行時(shí)

          既然需要在瀏覽器端增加 hover 事件,添加遮罩框元素,那么肯定不可避免的要侵入運(yùn)行時(shí)的代碼,這里通過在整個(gè)應(yīng)用的最外層包裹一個(gè) Inspector 來盡可能的減少入侵。

          import?React?from?'react'
          import?{?Inspector?}?from?'react-dev-inspector'

          const?InspectorWrapper?=?process.env.NODE_ENV?===?'development'
          ????Inspector
          ??:?React.Fragment

          export?const?Layout?=?()?=>?{
          ??//?...

          ??return?(
          ????<InspectorWrapper
          ??????keys={['control',?'shift',?'command',?'c']}?//?default?keys
          ??????...??//?Props?see?below
          ????>

          ?????<Page?/>
          ????InspectorWrapper>

          ??)
          }

          這里也可以自定義你喜歡的快捷鍵,用來開啟 debug 模式。

          開啟了 debug 模式之后,鼠標(biāo) hover 到你想要調(diào)試的組件,就會展現(xiàn)出遮罩框,再點(diǎn)擊一下,就會自動在 VSCode 中打開對應(yīng)的組件文件,并且跳轉(zhuǎn)到對應(yīng)的行和列。

          那么關(guān)鍵在于,這個(gè)跳轉(zhuǎn)其實(shí)是借助 fetch 發(fā)送了一個(gè)請求到本機(jī)的服務(wù)端,利用服務(wù)端執(zhí)行腳本命令code src/Inspector/index.ts 這樣的命令來打開 VSCode,這就要借助我說的第三步,啟動本地服務(wù)并引入中間件了。

          本地服務(wù)

          還記得 create-react-app 或者 vue-cli 啟動的前端項(xiàng)目,在錯(cuò)誤時(shí)會彈出一個(gè)全局的遮罩和對應(yīng)的堆棧信息,點(diǎn)擊以后就會跳轉(zhuǎn)到 VSCode 對應(yīng)的文件么?沒錯(cuò),react-dev-inspector 也正是直接借助了 create-react-app 底層的工具包 react-dev-utils 去實(shí)現(xiàn)。(沒錯(cuò) create-react-app 創(chuàng)建的項(xiàng)目自帶這個(gè)服務(wù),不需要手動加載這一步了)

          react-dev-utils 為這個(gè)功能封裝了一個(gè)中間件:errorOverlayMiddleware[3]

          其實(shí)代碼也很簡單,就是監(jiān)聽了一個(gè)特殊的 URL:

          //?launchEditorEndpoint.js
          module.exports?=?"/__open-stack-frame-in-editor";
          //?errorOverlayMiddleware.js
          const?launchEditor?=?require("./launchEditor");
          const?launchEditorEndpoint?=?require("./launchEditorEndpoint");

          module.exports?=?function?createLaunchEditorMiddleware()?{
          ??return?function?launchEditorMiddleware(req,?res,?next)?{
          ????if?(req.url.startsWith(launchEditorEndpoint))?{
          ??????const?lineNumber?=?parseInt(req.query.lineNumber,?10)?||?1;
          ??????const?colNumber?=?parseInt(req.query.colNumber,?10)?||?1;
          ??????launchEditor(req.query.fileName,?lineNumber,?colNumber);
          ??????res.end();
          ????}?else?{
          ??????next();
          ????}
          ??};
          };

          launchEditor 這個(gè)核心的打開編輯器的方法我們一會再詳細(xì)分析,現(xiàn)在可以先略過,只要知道我們需要開啟這個(gè)服務(wù)即可。

          這是一個(gè)為 express 設(shè)計(jì)的中間件,webpack 的 devServer 選項(xiàng)中提供的 before 也可以輕松接入這個(gè)中間件,如果你的項(xiàng)目不用 express,那么你只要參考這個(gè)中間件去重寫一個(gè)即可,只需要監(jiān)聽接口拿到文件相關(guān)的信息,調(diào)用核心方法 launchEditor 即可。

          只要保證這幾個(gè)步驟的完成,那么這個(gè)插件就接入成功了,可以通過在瀏覽器的控制臺執(zhí)行 fetch('/__open-stack-frame-in-editor?fileName=/Users/admin/app/src/Title.tsx') 來測試 react-dev-utils的服務(wù)是否開啟成功。

          注入絕對路徑

          注意上一步的請求中 fileName= 后面的前綴是絕對路徑,而 DOM 節(jié)點(diǎn)上只會保存形如 src/Title.tsx 這樣的相對路徑,源碼中會在點(diǎn)擊遮罩層的時(shí)候去取 process.env.PWD 這個(gè)變量,和組件上的相對路徑拼接后得到完整路徑,這樣 VSCode 才能順利打開。

          這需要借助 DefinePlugin 把啟動所在路徑寫入到瀏覽器環(huán)境中:

          new?DefinePlugin({
          ??"process.env.PWD":?JSON.stringfy(process.env.PWD),
          });

          至此,整套插件集成完畢,簡化版的原理解析就結(jié)束了。

          源碼重點(diǎn)

          看完上面的簡化原理解析后,其實(shí)大家也差不多能寫出一個(gè)類似的插件了,只是實(shí)現(xiàn)的細(xì)節(jié)可能不太相同。這里就不一一解析完整的源碼了,來看一下源碼中比較值得關(guān)注的一些細(xì)節(jié)。

          如何在元素上埋點(diǎn)

          在瀏覽器端能找到節(jié)點(diǎn)在 VSCode 里的對應(yīng)的路徑,關(guān)鍵就在于編譯時(shí)的埋點(diǎn),webpack loader 接受代碼字符串,返回你處理過后的字符串,用作在元素上增加新屬性再合適不過,我們只需要利用 babel 中的整套 AST 能力即可做到:

          export?default?function?inspectorLoader(
          ??this:?webpack.loader.LoaderContext,
          ??source:?string
          )?
          {
          ??const?{?rootContext:?rootPath,?resourcePath:?filePath?}?=?this;

          ??const?ast:?Node?=?parse(source);

          ??traverse(ast,?{
          ????enter(path:?NodePath)?{
          ??????if?(path.type?===?"JSXOpeningElement")?{
          ????????doJSXOpeningElement(path.node?as?JSXOpeningElement,?{?relativePath?});
          ??????}
          ????},
          ??});

          ??const?{?code?}?=?generate(ast);

          ??return?code
          }

          這是簡化后的代碼,標(biāo)準(zhǔn)的 parse -> traverse -> generate 流程,在遍歷的過程中對 JSXOpeningElement這種節(jié)點(diǎn)類型做處理,把文件相關(guān)的信息放到節(jié)點(diǎn)上即可:

          const?doJSXOpeningElement:?NodeHandler<
          ??JSXOpeningElement,
          ??{?relativePath:?string?}
          >?=?(node,?option)?=>?{
          ??const?{?stop?}?=?doJSXPathName(node.name)
          ??if?(stop)?return?{?stop?}

          ??const?{?relativePath?}?=?option

          ??//?寫入行號
          ??const?lineAttr?=?jsxAttribute(
          ????jsxIdentifier('data-inspector-line'),
          ????stringLiteral(node.loc.start.line.toString()),
          ??)

          ??//?寫入列號
          ??const?columnAttr?=?jsxAttribute(
          ????jsxIdentifier('data-inspector-column'),
          ????stringLiteral(node.loc.start.column.toString()),
          ??)

          ??//?寫入組件所在的相對路徑
          ??const?relativePathAttr?=?jsxAttribute(
          ????jsxIdentifier('data-inspector-relative-path'),
          ????stringLiteral(relativePath),
          ??)

          ??//?在元素上增加這幾個(gè)屬性
          ??node.attributes.push(lineAttr,?columnAttr,?relativePathAttr)

          ??return?{?result:?node?}
          }

          獲取組件名稱

          在運(yùn)行時(shí)鼠標(biāo) hover 在 DOM 節(jié)點(diǎn)上,這個(gè)時(shí)候拿到的只是 DOM 元素,如何獲取組件的名稱?其實(shí) React 內(nèi)部會在 DOM 上反向的掛上它所對應(yīng)的 fiber node 的引用,這個(gè)引用在 DOM 元素上以 __reactInternalInstance 開頭命名,可以這樣拿到:

          /**
          ?*?https://stackoverflow.com/questions/29321742/react-getting-a-component-from-a-dom-element-for-debugging
          ?*/

          export?const?getElementFiber?=?(element:?HTMLElement):?Fiber?|?null?=>?{
          ??const?fiberKey?=?Object.keys(element).find(
          ????key?=>?key.startsWith('__reactInternalInstance$'),
          ??)

          ??if?(fiberKey)?{
          ????return?element[fiberKey]?as?Fiber
          ??}

          ??return?null
          }

          由于拿到的 fiber可能對應(yīng)一個(gè)普通的 DOM 元素比如 div ,而不是對應(yīng)一個(gè)組件 fiber,我們肯定期望的是向上查找最近的組件節(jié)點(diǎn)后展示它的名字(這里使用的是 displayName 屬性),由于 fiber 是鏈表結(jié)構(gòu),可以通過向上遞歸查找 return 這個(gè)屬性,直到找到第一個(gè)符合期望的節(jié)點(diǎn)。

          這里遞歸查找 fiberreturn,就類似于在 DOM 節(jié)點(diǎn)中遞歸向上查找 parentNode 屬性,不停的向父節(jié)點(diǎn)遞歸查找。

          //?這里用正則屏蔽了一些組件名?這些正則匹配到的組價(jià)名不會被檢測到
          export?const?debugToolNameRegex?=?/^(.*?\.Provider|.*?\.Consumer|Anonymous|Trigger|Tooltip|_.*|[a-z].*)$/;

          export?const?getSuitableFiber?=?(baseFiber?:?Fiber):?Fiber?|?null?=>?{
          ??let?fiber?=?baseFiber;

          ??while?(fiber)?{
          ????//?while?循環(huán)向上遞歸查找?displayName?符合的組件
          ????const?name?=?fiber.type?.displayName;
          ????if?(name?&&?!debugToolNameRegex.test(name))?{
          ??????return?fiber;
          ????}
          ????//?找不到的話?就繼續(xù)找?return?節(jié)點(diǎn)
          ????fiber?=?fiber.return;
          ??}

          ??return?null;
          };

          fiber 上的屬性 type 在函數(shù)式組件的情況下對應(yīng)你書寫的函數(shù),在 class 組件的情況下就對應(yīng)那個(gè)類,取上面的的 displayName 屬性即可:

          export?const?getFiberName?=?(fiber?:?Fiber):?string?|?null?=>?{
          ??return?getSuitableFiber(fiber)?.type?.displayName;
          };

          這里有些美中不足的是,大部分我們手寫的函數(shù)組件都不會人為的加上 displayName,這是我認(rèn)為源碼可以優(yōu)化的點(diǎn)。


          服務(wù)端跳轉(zhuǎn) VSCode 原理

          雖然簡單來說,react-dev-utils 其實(shí)就是開了個(gè)接口,當(dāng)你 fetch 的時(shí)候幫你執(zhí)行 code filepath 指令,但是它底層其實(shí)是很巧妙的實(shí)現(xiàn)了多種編輯器的兼容的。

          如何“猜”出用戶在用哪個(gè)編輯器?它其實(shí)實(shí)現(xiàn)定義好了一組進(jìn)程名對應(yīng)開啟指令的映射表:

          const?COMMON_EDITORS_OSX?=?{
          ??'/Applications/Atom.app/Contents/MacOS/Atom':?'atom',
          ??'/Applications/Visual?Studio?Code.app/Contents/MacOS/Electron':?'code',
          ??...
          }

          然后在 macOSLinux 下,通過執(zhí)行 ps x 命令去列出進(jìn)程名,通過進(jìn)程名再去映射對應(yīng)的打開編輯器的指令。比如你的進(jìn)程里有 /Applications/Visual Studio Code.app/Contents/MacOS/Electron,那說明你用的是 VSCode,就獲取了 code 這個(gè)指令。

          之后調(diào)用 child_process 模塊去執(zhí)行命令即可:

          child_process.spawn("code",?pathInfo,?{?stdio:?"inherit"?});

          launchEditor 源碼地址[4]

          詳細(xì)接入教程

          構(gòu)建時(shí)只需要對 webpack 配置做點(diǎn)改動,加入一個(gè)全局變量,引入一個(gè) loader 即可。

          const?{?DefinePlugin?}?=?require('webpack');

          {
          ??module:?{
          ????rules:?[
          ??????{
          ????????test:?/\.(jsx|js)$/,
          ????????use:?[
          ??????????{
          ????????????loader:?'babel-loader',
          ????????????options:?{
          ??????????????presets:?['es2015',?'react'],
          ????????????},
          ??????????},
          ??????????//?注意這個(gè)?loader?babel?編譯之前執(zhí)行
          ??????????{
          ????????????loader:?'react-dev-inspector/plugins/webpack/inspector-loader',
          ????????????options:?{?exclude:?[resolve(__dirname,?'想要排除的目錄')]?},
          ??????????},
          ????????],
          ??????}
          ????],
          ??},
          ??plugins:?[
          ????new?DefinePlugin({
          ??????'process.env.PWD':?JSON.stringify(process.env.PWD),
          ????}),
          ??]
          }

          如果你的項(xiàng)目是自己搭建而非 cra 搭建的,那么有可能你的項(xiàng)目中沒有開啟 errorOverlayMiddleware 中間件提供的服務(wù),你可以在 webpack 的 devServer 中開啟:

          import?createErrorOverlayMiddleware?from?'react-dev-utils/errorOverlayMiddleware'

          {
          ??devServer:?{
          ????before(app)?{
          ??????app.use(createErrorOverlayMiddleware())
          ????}
          ??}
          }

          此外需要保證你的命令行本身就可以通過 code 命令打開 VSCode 編輯器,如果沒有配置這個(gè),可以參考以下步驟:

          1、首先打開 VSCode。

          2、使用 command + shift + p (注意 window 下使用 ctrl + shift + p) 然后搜索 code,選擇 install 'code' command in path

          最后,在 React 項(xiàng)目的最外層接入:

          import?React?from?'react'
          import?{?Inspector?}?from?'react-dev-inspector'

          const?InspectorWrapper?=?process.env.NODE_ENV?===?'development'
          ????Inspector
          ??:?React.Fragment

          export?const?Layout?=?()?=>?{
          ??//?...

          ??return?(
          ????<InspectorWrapper
          ??????keys={['control',?'shift',?'command',?'c']}?//?default?keys
          ??????...??//?Props?see?below
          ????>

          ?????<Page?/>
          ????InspectorWrapper>

          ??)
          }

          總結(jié)

          在大項(xiàng)目的開發(fā)和維護(hù)過程中,擁有這樣一個(gè)調(diào)試神器真的特別重要,再好的記憶力也沒法應(yīng)對日益膨脹的組件數(shù)量…… 接入了這個(gè)插件后,指哪個(gè)組件跳哪個(gè)組件,大大節(jié)省了我們的時(shí)間。

          在解讀這個(gè)插件的源碼過程中也能看出來,想要做一些對項(xiàng)目整體提效的事情,經(jīng)常需要我們?nèi)娴牧私膺\(yùn)行時(shí)、構(gòu)建時(shí)、Node 端的很多知識,學(xué)無止境。

          參考資料

          [1]

          react-dev-inspector: https://github.com/zthxxx/react-dev-inspector

          [2]

          預(yù)覽網(wǎng)站: https://react-dev-inspector.zthxxx.me/

          [3]

          errorOverlayMiddleware: https://github.com/facebook/create-react-app/blob/master/packages/react-dev-utils/errorOverlayMiddleware.js

          [4]

          launchEditor 源碼地址: https://github.com/facebook/create-react-app/blob/master/packages/react-dev-utils/launchEditor.js

          最后

          • 關(guān)注公眾號【前端宇宙】,每日獲取好文推薦
          • 添加微信,入群交流

          瀏覽 37
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产无码免费看 | 亚洲AV无码久久精品蜜桃小说 | 激情五月天在线视频 | 青青草青青日青青干视频在 | 欧美手机在线观看 |