點選下方「前端開發愛好者」,選擇「設為星標」
第一時間關註技術幹貨!
前言
大家好,我是林三心,用最通俗易懂的話講最難的知識點是我的座右銘,基礎是進階的前提是我的初心~
背景及相關資訊
不知道你是否遇到過產品或者測試給你一個頁面讓你改一點東西,你卻找不到頁面原始碼在哪裏的場景?對於一些大型計畫,檔數量多、檔層級深、程式碼行數多,尋找一個頁面上元件對應的原始碼位置,往往需要花費大量時間。
為了解決這個問題,我開發了
code-inspector-plugin
外掛程式,只需要點選頁面上的元素,就能夠自動開啟 vscode 定位到原始碼。已經在快手內部30+計畫中接入了使用,取得了不錯的反響。效果如下圖所示:
點選下述的 demo,也可以快速線上體驗效果:
vue online demo [1]
react online demo [2]
preact online demo [3]
solid online demo [4]
接入:想要使用的小夥伴,可以參考 code-inspector-plugin接入文件 [5] 接入使用
github 源碼:覺得外掛程式好用的可以辛苦動下小手幫作者 github 點個 star: code-inspector [6]
code-inspector-plugin 的優點
其實
code-inspector-plugin
是之前我看到過一篇 react 點選頁面元素定位原始碼的文章,受到啟發後實作的。但是相比而言,
code-inspector-plugin
在支持場景的豐富性以及接入的便捷程度上,都得到了巨大的提升,具備以下優勢:
支持的打包器更加廣泛:支持
webpack/vite/rspack
以及
umi
等一切基於上述三個打包器實作的打包工具
支持的框架及場景更加廣泛:支持
vue2/vue3/react/preact/solid
框架以及
next/nuxt
等SSR場景(以及一切以
vue2/vue3/react/preact/solid
框架為基礎封裝的 SSR 場景),支持在微前端中使用。
支持多種系統及 IDE:支持 Mac、Windows 和 Linux 系統,支持 vscode、webstorm、atom、hbuilderX、IDEA、phpsotrm 等多種 IDE,也支持自訂 IDE 的支持
接入更加簡便,對程式碼無侵入:無論是在什麽計畫中,只需要在
webpack/vite/rspack
的配置中添加
code-inspector-plugin
外掛程式即可,不需要修改任何原始碼或者其他的配置
自動辨識環境:外掛程式內部會針對
webpack/vite/rspack
開發環境下的一些內建資訊,自動辨識環境,僅在開發環境下生效,不會影響生產環境
code-inspector-plugin 實作原理
下面我們重點解析一下
code-inspector-plugin
的實作原理,外掛程式的整體功能可以簡單拆解為以下幾部份:
參與源碼編譯:打包工具(webpack/vite/rspack)編譯時,
code-inspector-plugin
外掛程式會參與編譯過程,對於
vue/jsx
語法會進行 ast 解析,獲取到 dom 部份的原始碼所在的
檔路徑、行、列
資訊,並將這些資訊作為 dom 上的
attribute
額外添加進去。
執行時互動程式碼:編譯完成後,外掛程式會向網頁中註入監聽按鍵定位原始碼的互動邏輯,當使用者點選定位 dom 時,能夠獲取 dom 的
attribute
上的
檔路徑、行、列
資訊,將資訊發送一個 http 請求給後台
啟動一個 node server 服務:在後台啟動一個 node server 服務,用於接收上一步發送過來的 http 請求
辨識並開啟 IDE:node server 收到請求後,根據請求帶過來的
檔路徑、行、列
資訊,使用 node 的
spawn
或者
exec
子行程開啟 IDE,並將滑鼠定位到 IDE 對應的位置
編譯 vue/jsx 原始碼
要參與原始碼的編譯過程,對於
vite
計畫,我們可以透過 vite 外掛程式的
transform
函式入口中實作;對於
webpack/rspack
計畫,可以實作一個
loader
實作。不同的打包工具只是對應的入口不同,而對於
vue/jsx
語法的編譯和解析過程都是公用的。
編譯 vue 語法
對於 vue 語法的編譯,我們可以使用 vue 內建的包
@vue/compiler-dom
實作,以及透過
magic-string
包來向 ast 註入額外的資訊,簡化的程式碼如下:
import { parse, transform } from'@vue/compiler-dom';
import MagicString from'magic-string';
// content 是由 vite transform 函式或者 webpack/rspack loader 傳過來的原始碼
const s = new MagicString(content);
// vue/react 部份內建元素添加 attrs 可能報錯,不處理
const escapeTags = [
' style',
'script',
'template',
'transition',
'keepalive',
'keep-alive',
'component',
'slot',
'teleport',
'transition-group',
'transitiongroup',
'suspense',
"fragment"
];
if (fileType === 'vue') {
// vue template 處理
const ast = parse(content, {
comments: true,
});
transform(ast, {
nodeTransforms: [
((node: TemplateChildNode) => {
// node.type === 1 說明是元素(排除掉 text、comment 等)
if (
!node.loc.source.includes('data-insp-path') &&
node.type === 1 &&
escapeTags.indexOf(node.tag.toLowerCase()) === -1
) {
// 向 dom 上添加一個帶有 filepath/row/column 的內容
const insertPosition =
node.loc.start.offset + node.tag.length + 1;
const { line, column } = node.loc.start;
// filePath 也是 vite transform 函式或者 webpack/rspack loader 傳過來的
const addition = ` data-insp-path="${filePath}:${line}:${column}:${
node.tag
}"${node.props.length ? ' ' : ''}`;
s.prependLeft(insertPosition, addition);
}
}) asNodeTransform,
],
});
returns.toString();
}
編譯 tsx 程式碼
對於 tsx 語法的編譯和解析使用
babel
實作,並且需要引入一些 babel 相關的包,完成對於 ts、vueJsx 等場景的相容,簡化的程式碼如下:
import MagicString from'magic-string';
importtype { TemplateChildNode, NodeTransform } from'@vue/compiler-dom';
import vueJsxPlugin from'@vue/babel-plugin-jsx';
import { parse as babelParse, traverse as babelTraverse } from'@babel/core';
import tsPlugin from'@babel/plugin-transform-typescript';
import importMetaPlugin from'@babel/plugin-syntax-import-meta';
import proposalDecorators from'@babel/plugin-proposal-decorators';
// content 是由 vite transform 函式或者 webpack/rspack loader 傳過來的原始碼
const s = new MagicString(content);
// vue/react 部份內建元素添加 attrs 可能報錯,不處理
const escapeTags = [
' style',
'script',
'template',
'transition',
'keepalive',
'keep-alive',
'component',
'slot',
'teleport',
'transition-group',
'transitiongroup',
'suspense',
"fragment"
];
if (fileType === 'jsx') {
// jsx 處理
const ast = babelParse(content, {
babelrc: false,
comments: true,
configFile: false,
plugins: [
importMetaPlugin,
[vueJsxPlugin, {}],
[tsPlugin, { isTSX: true, allowExtensions: true }],
[proposalDecorators, { legacy: true }],
],
});
babelTraverse(ast, {
enter({ node }: any) {
if (
node.type === 'JSXElement' &&
escapeTags.indexOf(
(node?.openingElement?.name?.name || '').toLowerCase()
) === -1 &&
node?.openingElement?.name?.name
) {
if (
node.openingElement.attributes.some(
(attr: any) =>
attr.type !== 'JSXSpreadAttribute' &&
attr.name.name === 'data-insp-path'
)
) {
return;
}
// 向 dom 上添加一個帶有 filepath/row/column 的內容
const insertPosition =
node.openingElement.end -
(node.openingElement.selfClosing ? 2 : 1);
const { line, column } = node.loc.start;
// filePath 也是 vite transform 函式或者 webpack/rspack loader 傳過來的
const addition = ` data-insp-path="${filePath}:${line}:${column + 1}:${
node.openingElement.name.name
}"${node.openingElement.attributes.length ? ' ' : ''}`;
s.prependLeft(insertPosition, addition);
}
},
});
return s.toString();
}
上面
vue/jsx
編譯完成後,其實相當於在原始碼基礎上為每個 dom 註入了一個
data-insp-path
內容,最終元素到頁面上,對應的 dom 就會添加一個這樣的內容,如下圖所示:
執行時互動註入
code-inspector-plugin
外掛程式的互動功能主要包含監聽兩部份:
監聽組合鍵按住時,滑鼠在 dom 上移動時會出現 DOM 遮罩層資訊
點選遮罩層會獲取 DOM
attribute
上的原始碼資訊,向後台發送一個請求
這部份功能的實作上難度不大,就是基礎的
html+js+css
,為了保證 js 邏輯和 css 樣式不會影響到宿主頁面,我采用了 web component 元件的方式來封裝了這部份邏輯(基於 lit 實作的 web component)。具體的實作細節將不多講了,源碼位於
packages/core/src/client/index.ts
[7]
檔中。
為了簡化使用者的使用,不需要使用者手動向頁面中添加互動邏輯的元件,我透過
webpack/vite/rspack
外掛程式,在 development 環境下將 web component 元件註入到頁面中。
原生的 Node Server 服務
Node Server 同樣是外掛程式在
webpack/vite/rspack
開始編譯的時候啟動的,用於監聽使用者發送 http 請求。
我們設定了一個預設的埠
6666
,為了防止埠沖突,我們需要使用
portFinder
繼續向下尋找一個可用的介面去啟動服務:
import http from'http';
import portFinder from'portfinder';
import path from'path';
import launchEditor from'./launch-editor';
const DefaultPort = 6666;
exportfunctionstartServer(callback: (port: number) => any, editor?: Editor) {
const server = http.createServer((req: any, res: any) => {
// 收到請求喚醒vscode
const params = new URLSearchParams(req.url.slice(1));
const file = params.get('file') asstring;
const line = Number(params.get('line'));
const column = Number(params.get('column'));
res.writeHead(200, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': '*',
'Access-Control-Allow-Headers': '*',
'Access-Control-Allow-Private-Network': 'true',
});
res.end('ok');
launchEditor(file, line, column, editor);
});
// 尋找可用介面
portFinder.getPort({ port: DefaultPort }, (err: Error, port: number) => {
if (err) {
throw err;
}
server.listen(port, () => {
callback(port);
});
});
}
辨識並開啟 IDE
Node Server 接收到了請求後,需要開啟使用者的 IDE 並定位到原始碼,這一步是如何實作的呢?
市面上大多數的 IDE,多支持透過
{IDE路徑} -g {path}:{line}:{column}
的終端命令,開啟 IDE 並將滑鼠光標定位到指定的位置,部份 IDE 還支持在全域安裝命令列工具簡化使用。以 vscode 為例,有兩種方式:
在終端透過 vscode 套用路徑直接開啟套用
透過安裝 vscode 提供的命令列工具,在終端透過
code
指令喚醒, launching-from-the-command-line [8]
這裏我們采用了第二種方式,透過 node 的
spwan
或者
exec
啟動一個子行程,執行
code -g 檔路徑:行:列
就能開啟 vscode 並定位到對應的檔路徑、行、列位置,簡化程式碼如下:
functionlaunchEditor(
fileName: string,
lineNumber: unknown,
colNumber: unknown,
_editor?: Editor
) {
// others code....
let [editor, ...args] = guessEditor(_editor);
// others code....
_childProcess = child_process.spawn(editor, args, { stdio: 'inherit' });
}
除了如何開啟 IDE 的問題,另一個要解決的問題是,如果使用者裝置上安裝了多種 IDE,我們要開啟哪個 IDE?
這個功能我們是基於 react-devtools [9] 的源碼實作了,它會去匹配使用者當前裝置上正在執行的行程,在 IDE 列表中匹配開啟。在此基礎上我們最佳化並豐富了這部份的功能,支持了以下特性:
最佳化了 IDE 的匹配順序:因為對於 web 計畫,大多數開發者使用的 IDE 是 vscode 或者 webstorm,所以我們會優先匹配這兩個 IDE
支持使用者指定 IDE:支持使用者透過在
.env.local
檔中指定聲明要開啟的 IDE,除了內建支持辨識的 IDE 外,使用者也可以用 IDE 可執行路徑方式指定(意味著支持所有 IDE)
程式碼架構設計
上面的實作原理中,我們講述了
code-inspector-plugin
外掛程式的核心內容,除了這部份之外,還想分享下我們在程式碼可維護性和使用者使用體驗方面所做的努力。
分包提升可維護性
上述核心內容的實作,絕大部份是與
vite/webpack/rspack
等打包器無關的,打包器外掛程式只是作為程式碼編譯和互動程式碼註入的入口承載。所以我們采用 monorepo 架構,將核心程式碼都提取到了
core
中,monorepo 的包如下:
📦packages
┣ 📂code-inspector-plugin -------------------------- 入口包
┣ 📂core ---------------------------------------- 核心程式碼處理
┣ 📂vite-plugin --------------------------------- vite 外掛程式
┗ 📂webpack-plugin --------------------------- webpack 外掛程式
其中,
vite-plugin
和
webpack-plugin
分別作為 vite 和 webpack 的入口。(rspack 由於在外掛程式系統的設計上完全支持了 webpack,所以可以直接使用 webpack-plugin 作為 rspack 的入口,如果後面二者出現差異,會考慮再分一個
rspack-plugin
的包)。
同時為了降低使用者在多種打包器中的接入心智,我們使用
code-inspector-plugin
將
vite/webpack/rspack
等不同計畫的外掛程式進行了整合作為唯一入口,使用者只需要透過
bundler
參數指定計畫的打包器即可,其他配置完全一致。
降低使用者接入本
在降低使用者成本方面,我們主要做了兩件事情:
為了讓使用者不需要修改任何的原始碼,我們對於頁面互動的程式碼,直接透過外掛程式註入,不需要使用者手動引入任何的元件,對使用者程式碼無任何侵入。
對於 webpack 和 rspack 的計畫,像啟動 node 服務這種邏輯是在外掛程式中實作的,而參與原始碼的編譯需要在
loader
中實作。雖然讓使用者同時接入一個plugin
和一個loader
成本也沒有那麽高,但是為了最大程度降低使用者接入成本,我們外掛程式會在 webpack/rspack 編譯前,自動將loader
添加到module.rules
中,使用者只需要接入一個plugin
即可,免去了loader
的接入成本。
原文地址:https://juejin.cn/post/7326002010084311079
作者:快手基礎平台前端 o翔哥o
結語
我是林三心
一個待過 小型toG型外包公司、大型外包公司、小公司、潛力型創業公司、大公司 的作死型前端選手;
一個偏前端的全幹工程師;
一個不正經的金塊作者;
逗比的B站up主;
不帥的小紅書博主;
喜歡打鐵的籃球菜鳥;
喜歡歷史的乏味少年;
喜歡rap的五音不全弱雞如果你想一起學習前端,一起摸魚,一起研究簡歷最佳化,一起研究面試進步,一起交流歷史音樂籃球rap,可以來俺的摸魚學習群哈哈,點這個,有7000多名前端小夥伴在等著一起學習哦 -->
廣州的兄弟可以約飯哦,或者約球~我負責打鐵,你負責進球,謝謝~