新聞中心
前言

公司主營(yíng)業(yè)務(wù):做網(wǎng)站、成都網(wǎng)站設(shè)計(jì)、移動(dòng)網(wǎng)站開(kāi)發(fā)等業(yè)務(wù)。幫助企業(yè)客戶真正實(shí)現(xiàn)互聯(lián)網(wǎng)宣傳,提高企業(yè)的競(jìng)爭(zhēng)能力。成都創(chuàng)新互聯(lián)是一支青春激揚(yáng)、勤奮敬業(yè)、活力青春激揚(yáng)、勤奮敬業(yè)、活力澎湃、和諧高效的團(tuán)隊(duì)。公司秉承以“開(kāi)放、自由、嚴(yán)謹(jǐn)、自律”為核心的企業(yè)文化,感謝他們對(duì)我們的高要求,感謝他們從不同領(lǐng)域給我們帶來(lái)的挑戰(zhàn),讓我們激情的團(tuán)隊(duì)有機(jī)會(huì)用頭腦與智慧不斷的給客戶帶來(lái)驚喜。成都創(chuàng)新互聯(lián)推出樂(lè)業(yè)免費(fèi)做網(wǎng)站回饋大家。
在大型項(xiàng)目開(kāi)發(fā)中,經(jīng)常會(huì)遇到這樣一個(gè)場(chǎng)景,QA 丟給你一個(gè)出問(wèn)題的鏈接,但是你完全不知道這個(gè)頁(yè)面 & 組件對(duì)應(yīng)的文件位置。
這時(shí)候如果可以點(diǎn)擊頁(yè)面上的組件,在 VSCode 中自動(dòng)跳轉(zhuǎn)到對(duì)應(yīng)文件,并定位到對(duì)應(yīng)行號(hào)豈不美哉?
react-dev-inspector[1] 就是應(yīng)此需求而生。
使用非常簡(jiǎn)單方便,看完這張動(dòng)圖你就秒懂:
可以在 預(yù)覽網(wǎng)站[2] 體驗(yàn)一下。
使用方式
這個(gè)插件功能很強(qiáng)大,代碼也寫得很漂亮,唯一的缺點(diǎn)就是文檔不是很完善,我閱讀了源碼總結(jié)了成功接入這個(gè)插件需要的幾個(gè)步驟,缺一不可。
簡(jiǎn)單來(lái)說(shuō)就是三步:
構(gòu)建時(shí):
- 需要加一個(gè) webpack loader 去遍歷編譯前的的 AST 節(jié)點(diǎn),在 DOM 節(jié)點(diǎn)上加上文件路徑、名稱等相關(guān)的信息 。
- 需要用 DefinePlugin 注入一下項(xiàng)目運(yùn)行時(shí)的根路徑,后續(xù)要用來(lái)拼接文件路徑,打開(kāi) VSCode 相應(yīng)的文件。
運(yùn)行時(shí):需要在 React 組件的最外層包裹 Inspector 組件,用于在瀏覽器端監(jiān)聽(tīng)快捷鍵,彈出 debug 的遮罩層,在點(diǎn)擊遮罩層的時(shí)候,利用 fetch 向本機(jī)服務(wù)發(fā)送一個(gè)打開(kāi) VSCode 的請(qǐng)求。
本地服務(wù):需要啟動(dòng) react-dev-utils 里的一個(gè)中間件,監(jiān)聽(tīng)一個(gè)特定的路徑,在本機(jī)服務(wù)端執(zhí)行打開(kāi) VSCode 的指令。
下面簡(jiǎn)單分析一下這幾步到底做了什么。
原理簡(jiǎn)化
構(gòu)建時(shí)
首先如果在瀏覽器端想知道這個(gè)組件屬于哪個(gè)文件,那么不可避免的要在構(gòu)建時(shí)就去遍歷代碼文件,根據(jù)代碼的結(jié)構(gòu)解析生成 AST,然后在每個(gè)組件的 DOM 元素上掛上當(dāng)前組件的對(duì)應(yīng)文件位置和行號(hào),所以在開(kāi)發(fā)環(huán)境最終生成的 DOM 元素是這樣的:
- data-inspector-line="11"
- data-inspector-column="4"
- data-inspector-relative-path="src/components/Slogan/Slogan.tsx"
- class="css-1f15bld-Description e1vquvfb0"
- >
- 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.
這樣就可以在輸入快捷鍵的時(shí)候,開(kāi)啟 debug 模式,讓 DOM 在 hover 的時(shí)候增加一個(gè)遮罩層并展示組件對(duì)應(yīng)的信息:
這一步通過(guò) webpack loader 拿到未編譯的 JSX 源碼,再配合 AST 的處理就可以完成。
運(yùn)行時(shí)
既然需要在瀏覽器端增加 hover 事件,添加遮罩框元素,那么肯定不可避免的要侵入運(yùn)行時(shí)的代碼,這里通過(guò)在整個(gè)應(yīng)用的最外層包裹一個(gè) Inspector 來(lái)盡可能的減少入侵。
- import React from 'react'
- import { Inspector } from 'react-dev-inspector'
- const InspectorWrapper = process.env.NODE_ENV === 'development'
- ? Inspector
- : React.Fragment
- export const Layout = () => {
- // ...
- return (
- keys={['control', 'shift', 'command', 'c']} // default keys
- ... // Props see below
- >
- )
- }
這里也可以自定義你喜歡的快捷鍵,用來(lái)開(kāi)啟 debug 模式。
開(kāi)啟了 debug 模式之后,鼠標(biāo) hover 到你想要調(diào)試的組件,就會(huì)展現(xiàn)出遮罩框,再點(diǎn)擊一下,就會(huì)自動(dòng)在 VSCode 中打開(kāi)對(duì)應(yīng)的組件文件,并且跳轉(zhuǎn)到對(duì)應(yīng)的行和列。
那么關(guān)鍵在于,這個(gè)跳轉(zhuǎn)其實(shí)是借助 fetch 發(fā)送了一個(gè)請(qǐng)求到本機(jī)的服務(wù)端,利用服務(wù)端執(zhí)行腳本命令如 code src/Inspector/index.ts 這樣的命令來(lái)打開(kāi) VSCode,這就要借助我說(shuō)的第三步,啟動(dòng)本地服務(wù)并引入中間件了。
本地服務(wù)
還記得 create-react-app 或者 vue-cli 啟動(dòng)的前端項(xiàng)目,在錯(cuò)誤時(shí)會(huì)彈出一個(gè)全局的遮罩和對(duì)應(yīng)的堆棧信息,點(diǎn)擊以后就會(huì)跳轉(zhuǎn)到 VSCode 對(duì)應(yīng)的文件么?沒(méi)錯(cuò),react-dev-inspector 也正是直接借助了 create-react-app 底層的工具包 react-dev-utils 去實(shí)現(xiàn)。(沒(méi)錯(cuò) create-react-app 創(chuàng)建的項(xiàng)目自帶這個(gè)服務(wù),不需要手動(dòng)加載這一步了)
react-dev-utils 為這個(gè)功能封裝了一個(gè)中間件:errorOverlayMiddleware[3]
其實(shí)代碼也很簡(jiǎn)單,就是監(jiān)聽(tīng)了一個(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è)核心的打開(kāi)編輯器的方法我們一會(huì)再詳細(xì)分析,現(xiàn)在可以先略過(guò),只要知道我們需要開(kāi)啟這個(gè)服務(wù)即可。
這是一個(gè)為 express 設(shè)計(jì)的中間件,webpack 的 devServer 選項(xiàng)中提供的 before也可以輕松接入這個(gè)中間件,如果你的項(xiàng)目不用 express,那么你只要參考這個(gè)中間件去重寫一個(gè)即可,只需要監(jiān)聽(tīng)接口拿到文件相關(guān)的信息,調(diào)用核心方法launchEditor 即可。
只要保證這幾個(gè)步驟的完成,那么這個(gè)插件就接入成功了,可以通過(guò)在瀏覽器的控制臺(tái)執(zhí)行 fetch('/__open-stack-frame-in-editor?fileName=/Users/admin/app/src/Title.tsx') 來(lái)測(cè)試 react-dev-utils的服務(wù)是否開(kāi)啟成功。
注入絕對(duì)路徑
注意上一步的請(qǐng)求中 fileName= 后面的前綴是絕對(duì)路徑,而 DOM 節(jié)點(diǎn)上只會(huì)保存形如 src/Title.tsx 這樣的相對(duì)路徑,源碼中會(huì)在點(diǎn)擊遮罩層的時(shí)候去取 process.env.PWD 這個(gè)變量,和組件上的相對(duì)路徑拼接后得到完整路徑,這樣 VSCode 才能順利打開(kāi)。
這需要借助 DefinePlugin 把啟動(dòng)所在路徑寫入到瀏覽器環(huán)境中:
- new DefinePlugin({
- "process.env.PWD": JSON.stringfy(process.env.PWD),
- });
至此,整套插件集成完畢,簡(jiǎn)化版的原理解析就結(jié)束了。
源碼重點(diǎn)
看完上面的簡(jiǎn)化原理解析后,其實(shí)大家也差不多能寫出一個(gè)類似的插件了,只是實(shí)現(xiàn)的細(xì)節(jié)可能不太相同。這里就不一一解析完整的源碼了,來(lái)看一下源碼中比較值得關(guān)注的一些細(xì)節(jié)。
如何在元素上埋點(diǎn)
在瀏覽器端能找到節(jié)點(diǎn)在 VSCode 里的對(duì)應(yīng)的路徑,關(guān)鍵就在于編譯時(shí)的埋點(diǎn),webpack loader 接受代碼字符串,返回你處理過(guò)后的字符串,用作在元素上增加新屬性再合適不過(guò),我們只需要利用 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
- }
這是簡(jiǎn)化后的代碼,標(biāo)準(zhǔn)的 parse -> traverse -> generate 流程,在遍歷的過(guò)程中對(duì) 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
- // 寫入行號(hào)
- const lineAttr = jsxAttribute(
- jsxIdentifier('data-inspector-line'),
- stringLiteral(node.loc.start.line.toString()),
- )
- // 寫入列號(hào)
- const columnAttr = jsxAttribute(
- jsxIdentifier('data-inspector-column'),
- stringLiteral(node.loc.start.column.toString()),
- )
- // 寫入組件所在的相對(duì)路徑
- 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)部會(huì)在 DOM 上反向的掛上它所對(duì)應(yīng)的 fiber node 的引用,這個(gè)引用在 DOM 元素上以 __reactInternalInstance 開(kāi)頭命名,可以這樣拿到:
- /**
- * 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可能對(duì)應(yīng)一個(gè)普通的 DOM 元素比如 div ,而不是對(duì)應(yīng)一個(gè)組件 fiber,我們肯定期望的是向上查找最近的組件節(jié)點(diǎn)后展示它的名字(這里使用的是 displayName 屬性),由于 fiber 是鏈表結(jié)構(gòu),可以通過(guò)向上遞歸查找 return 這個(gè)屬性,直到找到第一個(gè)符合期望的節(jié)點(diǎn)。
這里遞歸查找 fiber 的 return,就類似于在 DOM 節(jié)點(diǎn)中遞歸向上查找parentNode 屬性,不停的向父節(jié)點(diǎn)遞歸查找。
- // 這里用正則屏蔽了一些組件名 這些正則匹配到的組價(jià)名不會(huì)被檢測(cè)到
- 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ù)式組件的情況下對(duì)應(yīng)你書寫的函數(shù),在 class 組件的情況下就對(duì)應(yīng)那個(gè)類,取上面的的 displayName 屬性即可:
- export const getFiberName = (fiber?: Fiber): string | null => {
- return getSuitableFiber(fiber)?.type?.displayName;
- };
這里有些美中不足的是,大部分我們手寫的函數(shù)組件都不會(huì)人為的加上displayName,這是我認(rèn)為源碼可以優(yōu)化的點(diǎn)。
服務(wù)端跳轉(zhuǎn) VSCode 原理
雖然簡(jiǎn)單來(lái)說(shuō),react-dev-utils 其實(shí)就是開(kāi)了個(gè)接口,當(dāng)你 fetch 的時(shí)候幫你執(zhí)行code filepath 指令,但是它底層其實(shí)是很巧妙的實(shí)現(xiàn)了多種編輯器的兼容的。
如何“猜”出用戶在用哪個(gè)編輯器?它其實(shí)實(shí)現(xiàn)定義好了一組進(jìn)程名對(duì)應(yīng)開(kāi)啟指令的映射表:
- const COMMON_EDITORS_OSX = {
- '/Applications/Atom.app/Contents/MacOS/Atom': 'atom',
- '/Applications/Visual Studio Code.app/Contents/MacOS/Electron': 'code',
- ...
- }
然后在 macOS 和 Linux 下,通過(guò)執(zhí)行 ps x 命令去列出進(jìn)程名,通過(guò)進(jìn)程名再去映射對(duì)應(yīng)的打開(kāi)編輯器的指令。比如你的進(jìn)程里有 /Applications/Visual Studio Code.app/Contents/MacOS/Electron,那說(shuō)明你用的是 VSCode,就獲取了 code 這個(gè)指令。
之后調(diào)用 child_process 模塊去執(zhí)行命令即可:
- child_process.spawn("code", pathInfo, { stdio: "inherit" });
launchEditor 源碼地址[4]
詳細(xì)接入教程構(gòu)建時(shí)只需要對(duì) webpack 配置做點(diǎn)改動(dòng),加入一個(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)目中沒(méi)有開(kāi)啟 errorOverlayMiddleware 中間件提供的服務(wù),你可以在 webpack 的 devServer 中開(kāi)啟:
- import createErrorOverlayMiddleware from 'react-dev-utils/errorOverlayMiddleware'
- {
- devServer: {
- before(app) {
- app.use(createErrorOverlayMiddleware())
- }
- }
- }
此外需要保證你的命令行本身就可以通過(guò) code 命令打開(kāi) VSCode 編輯器,如果沒(méi)有配置這個(gè),可以參考以下步驟:
1、首先打開(kāi) 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 (
- keys={['control', 'shift', 'command', 'c']} // default keys
- ... // Props see below
- >
- )
- }
總結(jié)在大項(xiàng)目的開(kāi)發(fā)和維護(hù)過(guò)程中,擁有這樣一個(gè)調(diào)試神器真的特別重要,再好的記憶力也沒(méi)法應(yīng)對(duì)日益膨脹的組件數(shù)量…… 接入了這個(gè)插件后,指哪個(gè)組件跳哪個(gè)組件,大大節(jié)省了我們的時(shí)間。
在解讀這個(gè)插件的源碼過(guò)程中也能看出來(lái),想要做一些對(duì)項(xiàng)目整體提效的事情,經(jīng)常需要我們?nèi)娴牧私膺\(yùn)行時(shí)、構(gòu)建時(shí)、Node 端的很多知識(shí),學(xué)無(wú)止境。
參考資料
[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)載自微信公眾號(hào)「前端從進(jìn)階到入院」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系前端從進(jìn)階到入院公眾號(hào)。
網(wǎng)站標(biāo)題:我點(diǎn)擊頁(yè)面元素,為什么VSCode乖乖打開(kāi)了組件
URL分享:http://m.fisionsoft.com.cn/article/dpojcid.html


咨詢
建站咨詢
