日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務時間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
我點擊頁面元素,為什么VSCode乖乖打開了組件

前言

公司主營業(yè)務:做網(wǎng)站、成都網(wǎng)站設(shè)計、移動網(wǎng)站開發(fā)等業(yè)務。幫助企業(yè)客戶真正實現(xiàn)互聯(lián)網(wǎng)宣傳,提高企業(yè)的競爭能力。成都創(chuàng)新互聯(lián)是一支青春激揚、勤奮敬業(yè)、活力青春激揚、勤奮敬業(yè)、活力澎湃、和諧高效的團隊。公司秉承以“開放、自由、嚴謹、自律”為核心的企業(yè)文化,感謝他們對我們的高要求,感謝他們從不同領(lǐng)域給我們帶來的挑戰(zhàn),讓我們激情的團隊有機會用頭腦與智慧不斷的給客戶帶來驚喜。成都創(chuàng)新互聯(lián)推出樂業(yè)免費做網(wǎng)站回饋大家。

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

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

react-dev-inspector[1] 就是應此需求而生。

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

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

使用方式

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

簡單來說就是三步:

構(gòu)建時:

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

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

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

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

原理簡化

構(gòu)建時

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

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

     
 

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

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

運行時

既然需要在瀏覽器端增加 hover 事件,添加遮罩框元素,那么肯定不可避免的要侵入運行時的代碼,這里通過在整個應用的最外層包裹一個 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.     
  13.       keys={['control', 'shift', 'command', 'c']} // default keys 
  14.       ...  // Props see below 
  15.     > 
  16.       
  17.      
  18.   ) 

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

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

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

本地服務

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

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)在可以先略過,只要知道我們需要開啟這個服務即可。

這是一個為 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的服務是否開啟成功。

注入絕對路徑

注意上一步的請求中 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 里的對應的路徑,關(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) { 
  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 

這是簡化后的代碼,標準的 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 } 

獲取組件名稱

在運行時鼠標 hover 在 DOM 節(jié)點上,這個時候拿到的只是 DOM 元素,如何獲取組件的名稱?其實 React 內(nèi)部會在 DOM 上反向的掛上它所對應的 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可能對應一個普通的 DOM 元素比如 div ,而不是對應一個組件 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ù)式組件的情況下對應你書寫的函數(shù),在 class 組件的情況下就對應那個類,取上面的的 displayName 屬性即可:

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

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

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

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

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

 
 
 
 
  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 命令去列出進程名,通過進程名再去映射對應的打開編輯器的指令。比如你的進程里有 /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 中間件提供的服務,你可以在 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.     
  13.       keys={['control', 'shift', 'command', 'c']} // default keys 
  14.       ...  // Props see below 
  15.     > 
  16.       
  17.      
  18.   ) 

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

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

參考資料

[1]react-dev-inspector:

https://github.com/zthxxx/react-dev-inspector[2]預覽網(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)系前端從進階到入院公眾號。


當前標題:我點擊頁面元素,為什么VSCode乖乖打開了組件
瀏覽地址:http://www.dlmjj.cn/article/dpojcid.html