likes
comments
collection
share

这个300万下载量的VSCode插件竟是这样开发的

作者站长头像
站长
· 阅读数 59

今天,我准备在 VSCode 插件市场上寻找一个 PDF 阅读插件。找到一个下载量最高的插件,点击进去查看了详细信息,发现界面给我一种似曾相识的感觉。我查看了一下插件的源代码,果然发现它直接嵌入了 pdf.js 的 web 界面。开发这样一个插件其实并不复杂,只需要了解一些插件开发的知识,就能够实现它。接下来,我会分享一下我是如何开发这个插件的,虽然与源代码可能有一些不同,但基本原理是相同的。

这个300万下载量的VSCode插件竟是这样开发的

前面的文章已经详细介绍了如何创建插件项目,因此我们直接进入功能实现部分,重点是实现点击 PDF 文件后能够直接在插件中进行浏览。

定义自定义编辑器

默认情况下,VSCode 并不直接支持某些特殊文件类型的查看,但是您可以通过使用自定义编辑器 customEditors 来扩展其功能。

通过 customEditors,您可以创建完全可自定义的读/写编辑器,以取代 VSCode 的标准文本编辑器,用于处理特定类型的资源。举例来说,在编辑 Markdown 文件时,您可以创建一个自定义编辑器,用于实时预览 Markdown 渲染的效果。

对于 PDF 文件的预览,我们也可以使用 customEditors 功能。首先,在您的插件的 package.json 文件中进行定义:

"contributes": {
  "customEditors": [
    {
      "viewType": "dodo-reader.pdfEditor",
      "displayName": "PDF Viewer",
      "selector": [
        {
          "filenamePattern": "*.pdf"
        }
      ]
    }
  ]
}

注册自定义编辑器

  1. 创建一个实现了 vscode.CustomEditorProvider 接口的类。这个接口包含了用于管理自定义编辑器的方法,包括打开、保存等操作。例如:
    class PdfEditorProvider implements Partial<CustomEditorProvider> {
        constructor() {
            // 在这里可以进行一些初始化操作
        }
    
        resolveCustomEditor(document: CustomDocument, webviewPanel: WebviewPanel, _token: CancellationToken) {
            // 根据 URI 创建自定义文档,并返回一个自定义文档对象
        }
    
        openCustomDocument(uri: vscode.Uri, openContext: vscode.CustomDocumentOpenContext, token: vscode.CancellationToken) {
            // 将自定义文档与 Webview 面板关联,处理编辑器内容与 Webview 之间的交互
        }
    }
    
  2. 在类中实现完 CustomEditorProvider 接口后,注册您的提供程序:
    const myProvider = new PdfEditorProvider();
    const disposable = vscode.window.registerCustomEditorProvider('dodo-reader.pdfEditor', myProvider); // 注册提供程序,dodo-reader.pdfEditor 对应上面定义标识
    context.subscriptions.push(disposable); // 将提供程序添加到订阅中
    

完善视图提供程序

在上述自定义编辑器的注册后,接下来的重点是完善 PdfEditorProvider 接口,以定义视图的显示。视图的显示将涉及使用 webview,而你将要使用的是 pdf.js 的 web 视图程序。首先去下载程序Prebuilt (modern browsers),然后解压到项目目录。这个程序其实是可以直接浏览器访问的,在目录下启动一个服务,然后可以直接打开地址:

这个300万下载量的VSCode插件竟是这样开发的

你可以在网址上添加 query 参数 ?file=fileUrl 打开你的 PDF 文件。

openCustomDocument

打开 PDF 文件时,首先会调用 openCustomDocument,你可以根据传入的 URI 创建自定义文档对象,该文档对象将包含您要编辑的内容,在这个程序里不做任何处理,直接返回包含 URI 的对象,这个对象将会传递到下面 resolveCustomEditor 方法的 document 参数。

openCustomDocument(uri: vscode.Uri, openContext: vscode.CustomDocumentOpenContext, token: vscode.CancellationToken) {
  return {
    uri,
    dispose: () => { }
  }
}

resolveCustomEditor

resolveCustomEditor 方法中,你可以定义显示的视图,程序如下:

resolveCustomEditor(document: CustomDocument, webviewPanel: WebviewPanel, _token: CancellationToken) {
  webviewPanel.webview.html = 'Hello World!';
}

显示如图:

这个300万下载量的VSCode插件竟是这样开发的

那么要显示 PDF 文件就只需要把下载的 PDF 程序 html 内容替换为当前 webviewPanel.webview.html 的值。本来以为可以改成:

// 嵌套一个 PDF 视图浏览的 iframe
webviewPanel.webview.html = `
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
  </head>
  <body>
    <iframe src=""></iframe>
  </body>
</html>
`;

这种方法是最简单,但出乎我的意料,iframe 并没有显示出内容。这可能是由于 VSCode 的安全策略限制所导致的。因此,我考虑了另一种方法:通过读取 PDF 视图首页的 HTML 内容,并将其赋值给 webviewPanel.webview.html。然而,这种方法可能会遇到一个问题:如何向 PDF 视图程序提供要打开的文件链接。

PDF 视图程序上面提到会通过查询参数 "file" 来获取要显示的文件链接。如果没有提供这个参数,它会读取默认的链接。以下是相关的源代码示例:

file = params.get("file") ?? _app_options.AppOptions.get("defaultUrl");

然而,直接赋值 HTML 文本的操作并不能通过网址提供 "file" 值。这可能需要修改源代码。虽然修改源代码可能会带来一些不便,但一开始看来似乎没有其他办法,只能尝试一下。一种思路是将获取 "file" 值的方式改为直接从全局变量中获取,可以在读取的 HTML 内容中添加 "file" 值:

window.file = 'https://...'

然后将上述源代码修改为:

file = window.file ?? _app_options.AppOptions.get("defaultUrl");

然而,就在以为即将成功的时候,又出现了红色的报错:加载 PDF 时发生错误,file origin does not match viewer's,实在让人抓狂。接着找到对应源码,发现源码在打开 PDF 前会比较当前页面的 url origin 跟文件的 url origin 是否相同,这怎么可能相同嘛,一个是开头vscode-webview://的链接,一个是开头https://的链接。虽然去除此校验可能行得通,但总体来看,这种方式似乎并不可行,修改内容太过繁琐。那么,是否有一种方法可以在不修改 PDF 视图源代码的情况下解决问题呢?继续从源代码入手,后来发现了一个"fileinputchange"事件监听处理程序,代码如下:

var webViewerFileInputChange = function (evt) {
  if (PDFViewerApplication.pdfViewer?.isInPresentationMode) {
    return;
  }
  const file = evt.fileInput.files[0];
  PDFViewerApplication.open({
    url: URL.createObjectURL(file),
    originalUrl: file.name
  });
};

通过代码,很容易看出,一旦检测到文件有变动,会立即调用 open 方法进行打开。需要注意的是,这个 open 方法并不会验证 URL 是否与当前 origin 相同,当前是在 blob 链接。鉴于这种情况,我们是否可以在初始化完成后直接调用 open 方法来打开文件呢?通过一番改造,最终证明这是可行的。以下是修改后的代码示例:

resolveCustomEditor(document: CustomDocument, webviewPanel: WebviewPanel, _token: CancellationToken) {
  webviewPanel.webview.options = {
    enableScripts: true,
    localResourceRoots: [vscode.Uri.file(path.dirname(document.uri.fsPath)), this.context.extensionUri]
  };
  const base = vscode.Uri.joinPath(this.context.extensionUri, 'dist/web/pdf/web/')
  webviewPanel.webview.html = readFileSync(path.join(base.fsPath, 'viewer.html'), 'utf8').replace('<head>', `<head>
    <base href="${webviewPanel.webview.asWebviewUri(base).toString()}">
    <script>
    window.addEventListener('load', async function () {
      PDFViewerApplication.initializedPromise.then(() => {
        setTimeout(() => {
          PDFViewerApplication.open({url: "${webviewPanel.webview.asWebviewUri(document.uri)}"})
        })
      })
    })
    </script>
    <style>
      body {
        padding: 0;
      }
    </style>
  `)
}

对上述关键代码进行分析如下:

  1. localResourceRoots 参数:该参数的作用是定义允许通过 web URL 访问的目录。在这里,你需要明确定义两个目录:一是用于打开 PDF 文件的当前目录,二是插件所在的目录。确保两者都定义正确,否则在访问时可能会遇到 401 错误。

  2. <base href="..."> 标签:通过设置 <base> 标签,可以为网页内加载资源提供基本路径。确保资源能够正确加载。

  3. setTimeout 函数:在调用程序打开文件时,使用 setTimeout 的目的是在打开默认 PDF 文件后,再执行打开当前所需的 PDF 文件。这样做是防止后面执行的打开默认 PDF 覆盖你所要打开的 PDF(实际默认文件不存在会报错,但不会产生实质影响)。

这种实现方式相比前面提到的 PDF 插件实现要简单得多,但是原理是基本相同的。让我们来看一下最终的效果如何:

这个300万下载量的VSCode插件竟是这样开发的

转载自:https://juejin.cn/post/7267470426292486159
评论
请登录