自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

我點擊頁面元素,為什么VSCode乖乖打開了組件

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

[[355277]]

前言

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

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

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

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

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

使用方式

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

簡單來說就是三步:

構(gòu)建時:

  • 需要加一個 webpack loader 去遍歷編譯前的的 AST 節(jié)點,在 DOM 節(jié)點上加上文件路徑、名稱等相關(guān)的信息 。
  • 需要用 DefinePlugin 注入一下項目運行時的根路徑,后續(xù)要用來拼接文件路徑,打開 VSCode 相應(yīng)的文件。

運行時:需要在 React 組件的最外層包裹 Inspector 組件,用于在瀏覽器端監(jiān)聽快捷鍵,彈出 debug 的遮罩層,在點擊遮罩層的時候,利用 fetch 向本機服務(wù)發(fā)送一個打開 VSCode 的請求。

本地服務(wù):需要啟動 react-dev-utils 里的一個中間件,監(jiān)聽一個特定的路徑,在本機服務(wù)端執(zhí)行打開 VSCode 的指令。

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

原理簡化

構(gòu)建時

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

  1. <div 
  2.   data-inspector-line="11" 
  3.   data-inspector-column="4" 
  4.   data-inspector-relative-path="src/components/Slogan/Slogan.tsx" 
  5.   class="css-1f15bld-Description e1vquvfb0" 
  6.   <p 
  7.     data-inspector-line="44" 
  8.     data-inspector-column="10" 
  9.     data-inspector-relative-path="src/layouts/index.tsx" 
  10.   > 
  11.     Inspect react components and click will jump to local IDE to view component 
  12.     code. 
  13.   </p> 
  14. </div> 

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

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

運行時

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

  1. import React from 'react' 
  2. import { Inspector } from 'react-dev-inspector' 
  3.  
  4. const InspectorWrapper = process.env.NODE_ENV === 'development' 
  5.   ? Inspector 
  6.   : React.Fragment 
  7.  
  8. export const Layout = () => { 
  9.   // ... 
  10.  
  11.   return ( 
  12.     <InspectorWrapper 
  13.       keys={['control''shift''command''c']} // default keys 
  14.       ...  // Props see below 
  15.     > 
  16.      <Page /> 
  17.     </InspectorWrapper> 
  18.   ) 

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

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

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

本地服務(wù)

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

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

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

  1. // launchEditorEndpoint.js 
  2. module.exports = "/__open-stack-frame-in-editor";
  1. // errorOverlayMiddleware.js 
  2. const launchEditor = require("./launchEditor"); 
  3. const launchEditorEndpoint = require("./launchEditorEndpoint"); 
  4.  
  5. module.exports = function createLaunchEditorMiddleware() { 
  6.   return function launchEditorMiddleware(req, res, next) { 
  7.     if (req.url.startsWith(launchEditorEndpoint)) { 
  8.       const lineNumber = parseInt(req.query.lineNumber, 10) || 1; 
  9.       const colNumber = parseInt(req.query.colNumber, 10) || 1; 
  10.       launchEditor(req.query.fileName, lineNumber, colNumber); 
  11.       res.end(); 
  12.     } else { 
  13.       next(); 
  14.     } 
  15.   }; 
  16. }; 

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

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

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

注入絕對路徑

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

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

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

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

源碼重點

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

如何在元素上埋點

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

  1. export default function inspectorLoader( 
  2.   this: webpack.loader.LoaderContext, 
  3.   source: string 
  4. ) { 
  5.   const { rootContext: rootPath, resourcePath: filePath } = this; 
  6.  
  7.   const ast: Node = parse(source); 
  8.  
  9.   traverse(ast, { 
  10.     enter(path: NodePath<Node>) { 
  11.       if (path.type === "JSXOpeningElement") { 
  12.         doJSXOpeningElement(path.node as JSXOpeningElement, { relativePath }); 
  13.       } 
  14.     }, 
  15.   }); 
  16.  
  17.   const { code } = generate(ast); 
  18.  
  19.   return code 

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

  1. const doJSXOpeningElement: NodeHandler< 
  2.   JSXOpeningElement, 
  3.   { relativePath: string } 
  4. > = (node, option) => { 
  5.   const { stop } = doJSXPathName(node.name
  6.   if (stop) return { stop } 
  7.  
  8.   const { relativePath } = option 
  9.  
  10.   // 寫入行號 
  11.   const lineAttr = jsxAttribute( 
  12.     jsxIdentifier('data-inspector-line'), 
  13.     stringLiteral(node.loc.start.line.toString()), 
  14.   ) 
  15.  
  16.   // 寫入列號 
  17.   const columnAttr = jsxAttribute( 
  18.     jsxIdentifier('data-inspector-column'), 
  19.     stringLiteral(node.loc.start.column.toString()), 
  20.   ) 
  21.  
  22.   // 寫入組件所在的相對路徑 
  23.   const relativePathAttr = jsxAttribute( 
  24.     jsxIdentifier('data-inspector-relative-path'), 
  25.     stringLiteral(relativePath), 
  26.   ) 
  27.  
  28.   // 在元素上增加這幾個屬性 
  29.   node.attributes.push(lineAttr, columnAttr, relativePathAttr) 
  30.  
  31.   return { result: node } 

獲取組件名稱

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

  1. /** 
  2.  * https://stackoverflow.com/questions/29321742/react-getting-a-component-from-a-dom-element-for-debugging 
  3.  */ 
  4. export const getElementFiber = (element: HTMLElement): Fiber | null => { 
  5.   const fiberKey = Object.keys(element).find( 
  6.     key => key.startsWith('__reactInternalInstance$'), 
  7.   ) 
  8.  
  9.   if (fiberKey) { 
  10.     return element[fiberKey] as Fiber 
  11.   } 
  12.  
  13.   return null 

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

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

  1. // 這里用正則屏蔽了一些組件名 這些正則匹配到的組價名不會被檢測到 
  2. export const debugToolNameRegex = /^(.*?\.Provider|.*?\.Consumer|Anonymous|Trigger|Tooltip|_.*|[a-z].*)$/; 
  3.  
  4. export const getSuitableFiber = (baseFiber?: Fiber): Fiber | null => { 
  5.   let fiber = baseFiber; 
  6.  
  7.   while (fiber) { 
  8.     // while 循環(huán)向上遞歸查找 displayName 符合的組件 
  9.     const name = fiber.type?.displayName; 
  10.     if (name && !debugToolNameRegex.test(name)) { 
  11.       return fiber; 
  12.     } 
  13.     // 找不到的話 就繼續(xù)找 return 節(jié)點 
  14.     fiber = fiber.return
  15.   } 
  16.  
  17.   return null
  18. }; 

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

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

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

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

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

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

  1. const COMMON_EDITORS_OSX = { 
  2.   '/Applications/Atom.app/Contents/MacOS/Atom''atom'
  3.   '/Applications/Visual Studio Code.app/Contents/MacOS/Electron''code'
  4.   ... 

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

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

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

launchEditor 源碼地址[4]

詳細接入教程構(gòu)建時只需要對 webpack 配置做點改動,加入一個全局變量,引入一個 loader 即可。

  1. const { DefinePlugin } = require('webpack'); 
  2.  
  3.   module: { 
  4.     rules: [ 
  5.       { 
  6.         test: /\.(jsx|js)$/, 
  7.         use: [ 
  8.           { 
  9.             loader: 'babel-loader'
  10.             options: { 
  11.               presets: ['es2015''react'], 
  12.             }, 
  13.           }, 
  14.           // 注意這個 loader babel 編譯之前執(zhí)行 
  15.           { 
  16.             loader: 'react-dev-inspector/plugins/webpack/inspector-loader'
  17.             options: { exclude: [resolve(__dirname, '想要排除的目錄')] }, 
  18.           }, 
  19.         ], 
  20.       } 
  21.     ], 
  22.   }, 
  23.   plugins: [ 
  24.     new DefinePlugin({ 
  25.       'process.env.PWD': JSON.stringify(process.env.PWD), 
  26.     }), 
  27.   ] 

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

  1. import createErrorOverlayMiddleware from 'react-dev-utils/errorOverlayMiddleware' 
  2.  
  3.   devServer: { 
  4.     before(app) { 
  5.       app.use(createErrorOverlayMiddleware()) 
  6.     } 
  7.   } 

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

1、首先打開 VSCode。

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

最后,在 React 項目的最外層接入:

  1. import React from 'react' 
  2. import { Inspector } from 'react-dev-inspector' 
  3.  
  4. const InspectorWrapper = process.env.NODE_ENV === 'development' 
  5.   ? Inspector 
  6.   : React.Fragment 
  7.  
  8. export const Layout = () => { 
  9.   // ... 
  10.  
  11.   return ( 
  12.     <InspectorWrapper 
  13.       keys={['control''shift''command''c']} // default keys 
  14.       ...  // Props see below 
  15.     > 
  16.      <Page /> 
  17.     </InspectorWrapper> 
  18.   ) 

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

在解讀這個插件的源碼過程中也能看出來,想要做一些對項目整體提效的事情,經(jīng)常需要我們?nèi)娴牧私膺\行時、構(gòu)建時、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

本文轉(zhuǎn)載自微信公眾號「前端從進階到入院」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系前端從進階到入院公眾號。

責(zé)任編輯:武曉燕 來源: 前端從進階到入院
相關(guān)推薦

2020-06-03 15:14:35

Chrome進程架構(gòu)

2022-02-17 20:51:00

vuevscode前端

2023-08-01 08:18:09

CSSUnset

2023-02-13 11:34:13

數(shù)字孿生工業(yè)4.0

2012-02-28 09:11:51

語言Lua

2020-07-17 14:06:36

Scrum敏捷團隊

2012-04-04 22:07:12

Android

2024-09-13 11:49:15

2013-10-22 15:18:19

2014-01-09 09:24:40

2014-01-17 14:39:18

12306 搶票

2015-03-02 15:13:52

Apple Watch

2012-06-18 14:51:09

Python

2015-06-04 11:22:22

前端程序員

2023-07-23 17:19:34

人工智能系統(tǒng)

2014-09-22 10:06:07

2019-09-17 15:30:13

Java編程語言

2012-11-14 20:55:07

容錯服務(wù)器選型CIO

2022-11-11 17:06:43

開發(fā)組件工具

2022-11-08 08:53:56

插件IDE
點贊
收藏

51CTO技術(shù)棧公眾號