这个300万下载量的VSCode插件竟是这样开发的
今天,我准备在 VSCode 插件市场上寻找一个 PDF 阅读插件。找到一个下载量最高的插件,点击进去查看了详细信息,发现界面给我一种似曾相识的感觉。我查看了一下插件的源代码,果然发现它直接嵌入了 pdf.js 的 web 界面。开发这样一个插件其实并不复杂,只需要了解一些插件开发的知识,就能够实现它。接下来,我会分享一下我是如何开发这个插件的,虽然与源代码可能有一些不同,但基本原理是相同的。
前面的文章已经详细介绍了如何创建插件项目,因此我们直接进入功能实现部分,重点是实现点击 PDF 文件后能够直接在插件中进行浏览。
定义自定义编辑器
默认情况下,VSCode 并不直接支持某些特殊文件类型的查看,但是您可以通过使用自定义编辑器 customEditors
来扩展其功能。
通过 customEditors
,您可以创建完全可自定义的读/写编辑器,以取代 VSCode 的标准文本编辑器,用于处理特定类型的资源。举例来说,在编辑 Markdown 文件时,您可以创建一个自定义编辑器,用于实时预览 Markdown 渲染的效果。
对于 PDF 文件的预览,我们也可以使用 customEditors
功能。首先,在您的插件的 package.json 文件中进行定义:
"contributes": {
"customEditors": [
{
"viewType": "dodo-reader.pdfEditor",
"displayName": "PDF Viewer",
"selector": [
{
"filenamePattern": "*.pdf"
}
]
}
]
}
注册自定义编辑器
- 创建一个实现了 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 之间的交互 } }
- 在类中实现完 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),然后解压到项目目录。这个程序其实是可以直接浏览器访问的,在目录下启动一个服务,然后可以直接打开地址:
你可以在网址上添加 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!';
}
显示如图:
那么要显示 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>
`)
}
对上述关键代码进行分析如下:
-
localResourceRoots
参数:该参数的作用是定义允许通过 web URL 访问的目录。在这里,你需要明确定义两个目录:一是用于打开 PDF 文件的当前目录,二是插件所在的目录。确保两者都定义正确,否则在访问时可能会遇到 401 错误。 -
<base href="...">
标签:通过设置<base>
标签,可以为网页内加载资源提供基本路径。确保资源能够正确加载。 -
setTimeout
函数:在调用程序打开文件时,使用 setTimeout 的目的是在打开默认 PDF 文件后,再执行打开当前所需的 PDF 文件。这样做是防止后面执行的打开默认 PDF 覆盖你所要打开的 PDF(实际默认文件不存在会报错,但不会产生实质影响)。
这种实现方式相比前面提到的 PDF 插件实现要简单得多,但是原理是基本相同的。让我们来看一下最终的效果如何:
转载自:https://juejin.cn/post/7267470426292486159