From 820c5196b3556358627cd7d5df76530058269224 Mon Sep 17 00:00:00 2001 From: yetao Date: Thu, 31 Oct 2024 11:23:01 +0800 Subject: [PATCH] =?UTF-8?q?refactor=F0=9F=8E=A8:=20=20(=E9=98=85=E8=AF=BB?= =?UTF-8?q?=E4=BB=A3=E7=A0=81)=EF=BC=9Awebview.ts=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/webview.ts | 138 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 130 insertions(+), 8 deletions(-) diff --git a/src/webview.ts b/src/webview.ts index 0bb093a..3bd55eb 100644 --- a/src/webview.ts +++ b/src/webview.ts @@ -1,132 +1,251 @@ +// 导入 common/fileSystemConfig 模块中的 getLocations 函数 import { getLocations } from 'common/fileSystemConfig'; +// 导入 common/webviewMessages 模块中的 Message 和 Navigation 类型 import type { Message, Navigation } from 'common/webviewMessages'; +// 导入 fs 模块,用于文件系统操作 import * as fs from 'fs'; +// 导入 path 模块,用于路径操作 import * as path from 'path'; +// 导入 vscode 模块,用于 VS Code 扩展 API import * as vscode from 'vscode'; +// 导入 config 模块中的 deleteConfig、getConfigs、loadConfigs、updateConfig 函数 import { deleteConfig, getConfigs, loadConfigs, updateConfig } from './config'; +// 导入 logging 模块中的 DEBUG、LOGGING_NO_STACKTRACE、Logging 类型 import { DEBUG, LOGGING_NO_STACKTRACE, Logging as _Logging } from './logging'; +// 导入 utils 模块中的 toPromise 函数 import { toPromise } from './utils'; +// 使用 _Logging 模块创建一个名为 'WebView' 的日志记录器 const Logging = _Logging.scope('WebView'); +// 存储当前激活的 WebView 面板实例,用于在扩展中与 WebView 进行交互 let webviewPanel: vscode.WebviewPanel | undefined; +// 存储待处理的导航信息,当用户在 WebView 中点击链接或按钮时,可能会触发导航事件 let pendingNavigation: Navigation | undefined; +/** + * 获取 VS Code 扩展的路径 + * @returns 扩展的路径,如果扩展未找到则返回 undefined + */ function getExtensionPath(): string | undefined { + // 使用 vscode.extensions.getExtension 方法获取名为 'Kelvin.vscode-sshfs' 的扩展 const ext = vscode.extensions.getExtension('Kelvin.vscode-sshfs'); + // 如果找到了扩展,则返回扩展的路径,否则返回 undefined return ext && ext.extensionPath; } +/** + * 获取调试内容 + * @returns 调试内容字符串,如果未启用调试则返回 false + * @description + * 这个函数用于获取调试内容。它首先检查是否启用了调试模式(DEBUG 标志),如果未启用,则直接返回 false。 + * 如果启用了调试模式,它会构建一个 URL,指向本地开发服务器的根路径(默认为 http://localhost:3000/)。 + * 然后,它使用 Node.js 的 http 模块向该 URL 发送 GET 请求,并等待响应。 + * 当接收到响应时,它会检查响应的状态码是否为 200。如果不是,它会返回一个错误。 + * 如果状态码为 200,它会读取响应的 body,并将其转换为字符串。 + * 接着,它会对字符串进行一些替换操作,以确保其中的资源路径和 CSP 元数据正确地指向开发服务器。 + * 最后,它将处理后的字符串作为结果返回。 + */ async function getDebugContent(): Promise { + // 如果未启用 DEBUG 模式,则直接返回 false if (!DEBUG) return false; + // 构建 React 开发服务器的 URL const URL = `http://localhost:3000/`; + // 异步导入 http 模块 const http = await import('http'); + // 发送 GET 请求到 React 开发服务器,并处理响应 return toPromise(cb => http.get(URL, async (message) => { - if (message.statusCode !== 200) return cb(new Error(`Error code ${message.statusCode} (${message.statusMessage}) connecting to React dev server}`)); + // 如果响应状态码不是 200,则返回错误 + if (message.statusCode!== 200) return cb(new Error(`Error code ${message.statusCode} (${message.statusMessage}) connecting to React dev server`)); + // 初始化响应 body 字符串 let body = ''; + // 当接收到数据块时,将其添加到 body 字符串中 message.on('data', chunk => body += chunk); + // 等待数据接收完毕 await toPromise(cb => message.on('end', cb)); + // 替换 body 字符串中的资源路径 body = body.toString().replace(/\/static\/js\/bundle\.js/, `${URL}/static/js/bundle.js`); - // Make sure the CSP meta tag also includes the React dev server (including connect-src for the socket, which uses both http:// and ws://) + // 确保 CSP 元数据包含 React 开发服务器的 URL body = body.replace(/\$WEBVIEW_CSPSOURCE/g, `$WEBVIEW_CSPSOURCE ${URL}`); + // 添加额外的 CSP 规则,允许连接到 React 开发服务器的 socket body = body.replace(/\$WEBVIEW_CSPEXTRA/g, `connect-src ${URL} ${URL.replace('http://', 'ws://')};`); + // 替换 body 字符串中的资源路径前缀 body = body.replace(/src="\/static\//g, `src="${URL}/static/`); + // 回调函数,返回处理后的 body 字符串 cb(null, body); + // 处理请求过程中的错误 }).on('error', err => { + // 记录错误信息 Logging.warning(`Error connecting to React dev server: ${err}`); + // 返回错误信息 cb(new Error('Could not connect to React dev server. Not running?')); })); } +/** + * 打开一个 WebView 面板,显示 SSH-FS 的设置界面 + * @description + * 这个函数用于创建并显示一个 WebView 面板,该面板用于展示 SSH-FS 的设置界面。 + * 它首先检查是否已经存在一个 WebView 面板实例,如果不存在,则创建一个新的实例。 + * 然后,它设置了 WebView 面板的一些基本属性,如标题、位置和图标。 + * 接着,它尝试获取调试内容,如果获取成功,则将其设置为 WebView 面板的内容;如果获取失败,则读取本地文件系统中的静态 HTML 文件,并将其设置为 WebView 面板的内容。 + * 在设置内容之前,它会替换 HTML 中的一些特定字符串,以确保资源路径和 CSP 元数据正确。 + * 最后,它显示 WebView 面板,并在必要时处理错误。 + */ export async function open() { + // 检查是否已经存在 WebView 面板实例 if (!webviewPanel) { + // 获取扩展的路径 const extensionPath = getExtensionPath(); + // 创建一个新的 WebView 面板实例 webviewPanel = vscode.window.createWebviewPanel('sshfs-settings', 'SSH-FS', vscode.ViewColumn.One, { enableFindWidget: true, enableScripts: true }); + // 当 WebView 面板被关闭时,将 webviewPanel 变量设置为 undefined webviewPanel.onDidDispose(() => webviewPanel = undefined); + // 如果扩展路径存在,则设置 WebView 面板的图标 if (extensionPath) webviewPanel.iconPath = vscode.Uri.file(path.join(extensionPath, 'resources/icon.svg')); + // 获取 WebView 实例 const { webview } = webviewPanel; + // 监听 WebView 接收到的消息 webview.onDidReceiveMessage(handleMessage); + // 尝试获取调试内容 let content = await getDebugContent().catch((e: Error) => (vscode.window.showErrorMessage(e.message), null)); + // 如果没有获取到调试内容 if (!content) { + // 如果扩展路径不存在,则抛出错误 if (!extensionPath) throw new Error('Could not get extensionPath'); - // If we got here, we're either not in debug mode, or something went wrong (and an error message is shown) + // 读取本地文件系统中的静态 HTML 文件 content = fs.readFileSync(path.resolve(extensionPath, 'webview/build/index.html')).toString(); - // Built index.html has e.g. `href="/static/js/stuff.js"`, need to make it use vscode-resource: and point to the built static directory - // Scrap that last part, vscode-resource: is deprecated and we need to use Webview.asWebviewUri - //content = content.replace(/\/static\//g, vscode.Uri.file(path.join(extensionPath, 'webview/build/static/')).with({ scheme: 'vscode-resource' }).toString()); + // 替换 HTML 中的资源路径,使其指向 WebView 的资源 content = content.replace(/\/static\//g, webview.asWebviewUri(vscode.Uri.file(path.join(extensionPath, 'webview/build/static/'))).toString()); } - // Make sure the CSP meta tag has the right cspSource + // 替换 HTML 中的 CSP 元数据 content = content.replace(/\$WEBVIEW_CSPSOURCE/g, webview.cspSource); - // The EXTRA tag is used in debug mode to define connect-src. By default we can (and should) just delete it + // 移除 HTML 中的额外 CSP 规则 content = content.replace(/\$WEBVIEW_CSPEXTRA/g, ''); + // 设置 WebView 面板的内容 webview.html = content; } + // 显示 WebView 面板 webviewPanel.reveal(); } +/** + * 处理导航请求 + * @param navigation - 导航请求对象,包含导航的目标和其他相关信息 + * @returns 一个 Promise,当导航请求处理完成时解决 + * @description + * 这个函数用于处理来自 WebView 的导航请求。当用户在 WebView 中点击链接或触发导航事件时,会调用此函数。 + * 它首先记录一条调试信息,表明接收到了导航请求,并将请求的详细信息记录下来。 + * 然后,它将导航请求存储在 `pendingNavigation` 变量中,以便后续处理。 + * 接着,它向 WebView 发送一个消息,通知 WebView 有导航请求正在等待处理,并提供导航请求的详细信息。 + * 最后,它调用 `open` 函数来打开 WebView 面板,显示 SSH-FS 的设置界面。 + * 请注意,这个函数返回一个 Promise,当导航请求处理完成时解决。这意味着调用者可以等待导航请求处理完成后再执行其他操作。 + */ export async function navigate(navigation: Navigation) { + // 记录接收到导航请求的调试信息 Logging.debug`Navigation requested: ${navigation}`; + // 将导航请求存储在 pendingNavigation 变量中 pendingNavigation = navigation; + // 向 WebView 发送导航请求消息 postMessage({ navigation, type: 'navigate' }); + // 打开 WebView 面板 return open(); } +/** + * 向 WebView 面板发送消息 + * @param message - 要发送的消息对象,必须继承自 Message 类型 + * @description + * 这个函数用于向 WebView 面板发送消息。它首先检查 `webviewPanel` 是否存在,如果存在,则使用 `webviewPanel.webview.postMessage` 方法发送消息。 + * 请注意,`message` 参数必须是 `Message` 类型或其子类型。 + */ function postMessage(message: T) { + // 检查 webviewPanel 是否存在,如果存在则发送消息 webviewPanel?.webview.postMessage(message); } +/** + * 处理来自 WebView 面板的消息 + * @param message - 从 WebView 面板接收到的消息对象 + * @returns 一个 Promise,当消息处理完成时解决 + * @description + * 这个函数用于处理来自 WebView 面板的消息。它首先检查 `webviewPanel` 是否存在,如果不存在,则记录一条警告信息。 + * 然后,它记录接收到的消息的调试信息。如果存在待处理的导航请求,它会发送一个导航消息,并将 `pendingNavigation` 变量重置为 `undefined`。 + * 接着,它根据消息的类型进行相应的处理。目前支持的消息类型包括 `requestData`、`saveConfig`、`promptPath` 和 `navigated`。 + * - `requestData`:请求配置数据。函数会加载配置数据并返回给 WebView 面板。 + * - `saveConfig`:保存配置数据。函数会根据消息中的指示更新或删除配置数据,并返回操作结果。 + * - `promptPath`:提示用户选择路径。函数会显示一个文件选择对话框,并将用户选择的路径返回给 WebView 面板。 + * - `navigated`:导航事件。函数会根据导航的目标更新 WebView 面板的标题。 + * 请注意,这个函数返回一个 Promise,当消息处理完成时解决。这意味着调用者可以等待消息处理完成后再执行其他操作。 + */ async function handleMessage(message: Message): Promise { + // 如果 webviewPanel 不存在,则记录警告信息 if (!webviewPanel) return Logging.warning`Got message without webviewPanel: ${message}`; + // 记录接收到的消息的调试信息 Logging.debug`Got message: ${message}`; + // 如果存在待处理的导航请求 if (pendingNavigation) { + // 发送导航消息 postMessage({ type: 'navigate', navigation: pendingNavigation, }); + // 重置 pendingNavigation 变量 pendingNavigation = undefined; } + // 根据消息的类型进行相应的处理 switch (message.type) { case 'requestData': { + // 请求配置数据 const configs = await (message.reload ? loadConfigs : getConfigs)(); + // 获取配置数据的位置信息 const locations = getLocations(configs); + // 返回配置数据和位置信息 return postMessage({ configs, locations, type: 'responseData', }); } case 'saveConfig': { + // 保存配置数据 const { uniqueId, config, name, remove } = message; let error: string | undefined; try { + // 根据 remove 标志决定是删除还是更新配置数据 if (remove) { await deleteConfig(config); } else { await updateConfig(config, name); } } catch (e) { + // 记录错误信息 Logging.error('Error handling saveConfig message for settings UI:', LOGGING_NO_STACKTRACE); Logging.error(JSON.stringify(message), LOGGING_NO_STACKTRACE); Logging.error(e); error = e.message; } + // 返回操作结果 return postMessage({ uniqueId, config, error, type: 'saveConfigResult', }); } case 'promptPath': { + // 提示用户选择路径 const { uniqueId } = message; let uri: vscode.Uri | undefined; let error: string | undefined; try { + // 显示文件选择对话框 const uris = await vscode.window.showOpenDialog({}); if (uris) [uri] = uris; } catch (e) { + // 记录错误信息 Logging.error`Error handling promptPath message for settings UI:\n${message}\n${e}`; error = e.message; } + // 返回用户选择的路径 return postMessage({ uniqueId, path: uri && uri.fsPath, @@ -134,9 +253,11 @@ async function handleMessage(message: Message): Promise { }); } case 'navigated': { + // 导航事件 const { view } = message; type View = 'startscreen' | 'newconfig' | 'configeditor' | 'configlocator'; let title: string | undefined; + // 根据导航目标更新 WebView 面板的标题 switch (view as View) { case 'configeditor': title = 'SSH FS - Edit config'; @@ -148,6 +269,7 @@ async function handleMessage(message: Message): Promise { title = 'SSH FS - New config'; break; } + // 设置 WebView 面板的标题 webviewPanel.title = title || 'SSH FS'; } }