vscode 插件开发:右键菜单定制 & 预览视图 & HTML/FTL/PDF 预览

开始之前,请下载 vscode pdf 预览的代码

github.com/tomoki1207/...

安装这个插件可以让 vscode 可以查看 PDF ,我们在此基础上进行开发

先确定代码结构(别忘了 npm install)

简单说一下,lib 里面主要是和预览 PDF 有关的代码,用到了谷歌的 pdfjs,src 中主要是和插件相关的代码,使用到 vscode 的 API,与编辑器打交道的代码在这里面

在右键菜单栏中添加选项

比较合适的做法是注册一个命令,将该命令与视图的右键菜单进行绑定

如图,我们在 vscode 中右键打开的菜单中有很多选项,那么要如何在这个菜单中添加我们自己的功能呢?

找到我们刚刚的代码中的 package.json,找到 contributes 项,在下面添加 commands 项,如下图

注册一个命令

json 复制代码
"commands": [
  {
    "command": "ftl-preview-enhanced.openPreviewToTheSide",
    "title": "%ftl-preview-enhanced.openPreviewToTheSide.title%",
    "category": "Ftl"
  }
]

我在这里填了三个内容,它们对应的值其实都是自定义的,只是要约定各处使用时保持一致即可。

因为我们是绑定右键菜单,这里的 title 就是具体显示在菜单当中的文字,如果你写法和我一样,那么还需要在根目录中添加两个文件

javascript 复制代码
// package.nls.json
{
  "ftl-preview-enhanced.openPreviewToTheSide.title": "frameWork Preview Enhanced: Open Preview to the Side"
}

// package.nls.zh.json
{
  "ftl-preview-enhanced.openPreviewToTheSide.title": "FTL:打开侧边预览"
}

当然,这只是命令的配置,现在我们配置具体的菜单

我们在 commands 的同一级当中,添加 menu 属性

javascript 复制代码
"commands": [···],
"menus": {
  "editor/context": [
    {
      "command": "ftl-preview-enhanced.openPreviewToTheSide"
    }
  ]
}

这时候我们就成功添加好了,CTRL + shift + B 进行编译,然后 F5 运行

我们还可以在添加 when 属性,用来控制在何时显示该选项,比如

javascript 复制代码
"menus": {
  "editor/context": [
    {
      "command": "ftl-preview-enhanced.openPreviewToTheSide",
			"when": "editorLangId == ftl"
    }
  ]
}

"when": "editorLangId == ftl" 就表示仅在文件是 FTL 格式的时候才显示该菜单栏,但是别急,还差一步,我们需要设置在 FTL 格式文件时激活插件才能正常显示,需要额外配置一个属性,该属性配置在最外层,与 contributes 同级

javascript 复制代码
"activationEvents": [
  "onLanguage:typescript",	// 这里很重要,会直接影响后面代码能否运行
  "onLanguage:ftl"
],

这样就可以了,其余类型的文件同理。

打开右侧视图

上面的代码只是添加了一个右键菜单选项,但是该选项并没有具体的功能,很简单,因为我们目前为止一直在进行配置,并没有写相关的代码

打开 src 下的 extension,能看到下面的代码

javascript 复制代码
import * as vscode from 'vscode';
import { PdfCustomProvider } from './pdfProvider';

export function activate(context: vscode.ExtensionContext): void {
  const extensionRoot = vscode.Uri.file(context.extensionPath);
  // Register our custom editor provider
  const provider = new PdfCustomProvider(extensionRoot);
  context.subscriptions.push(
    vscode.window.registerCustomEditorProvider(
      PdfCustomProvider.viewType,
      provider,
      {
        webviewOptions: {
          enableFindWidget: false, // default
          retainContextWhenHidden: true,
        },
      }
    )
  );
  
}

export function deactivate(): void {}

这是 pdf 这个插件原本的功能,他会打开一个窗口用来渲染拖拽进来的 PDF 文件,这不是我们要的功能,直接删掉即可

javascript 复制代码
import * as vscode from 'vscode';
import { PdfPreview } from './pdfPreview';	// 注意这里添加了一行引入,后面要用

export function activate(context: vscode.ExtensionContext): void {
  const extensionRoot = vscode.Uri.file(context.extensionPath);
  // Register our custom editor provider
  
}

export function deactivate(): void {}

看我代码好像没删干净,这个 extensionRoot 留着,后面能用上

现在我们给我们刚刚注册的命令绑定事件(部分重复代码不再展示)

javascript 复制代码
let extensionRoot: vscode.Uri;	// 注意,我修改了extensionRoot位置,以便下面代码能够访问该变量
export function activate(context: vscode.ExtensionContext): void {

  extensionRoot = vscode.Uri.file(context.extensionPath);
  context.subscriptions.push(
    vscode.commands.registerCommand(
      'ftl-preview-enhanced.openPreviewToTheSide',
      openPreviewToTheSide,
    ),
  );
}

这里要注意,绑定的命令一定要和刚刚自己注册的命令保持一致

这个 openPreviewToTheSide 便是我们需要写的函数

typescript 复制代码
async function openPreviewToTheSide(uri?: vscode.Uri) {
  const editor = vscode.window.activeTextEditor;
  if (!editor) {
    // 判断是否有打开的视图,没有则不打开右侧视图
    return;
  }
  if (!uri) {
    // 避免空值
    uri = editor.document.uri;
  }

  const resourceRoot = uri.with({
    path: uri.path.replace(/\/[^/]+?\.\w+$/, '/'),
  });

  if (!previewsContainer.length) {
    // 打开额外的视图
    const webviewPanel = vscode.window.createWebviewPanel(
      'ftl-preview',
      'FTL PREVIEW',	// 标题
      {
        viewColumn: vscode.ViewColumn.Two,
        preserveFocus: true,
      },
      {
        // 资产路径
        localResourceRoots: [resourceRoot, extensionRoot],
        enableFindWidget: true,
        // 允许运行 script,这里指的webview
        enableScripts: true,
      }
    );
    // 用现成的方法初始化该视图
    const preview = new PdfPreview(
      extensionRoot,
      uri,
      webviewPanel
    );

    // 关闭后清除视图
    webviewPanel.onDidDispose(() => {
      preview.dispose();
    }); 
  }

然后运行即可

成功打开侧边视图,我们在下面的内容中讲如何调整右侧视图中显示的内容

定制右侧视图内容

我们打开 pdfPreview.ts,在74 行左右

右侧显示的内容就是这个 getWebviewContents 函数返回的,我们点进去看一下就会发现其实就是普通的 html

到这里就很简单了,大家可以根据自己需要定制页面内容,这里就简单演示一个欢迎界面

javascript 复制代码
private getWelcomPageContent() {
    return `<!DOCTYPE html>
    <html dir="ltr" mozdisallowselectionprint>
    <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <meta name="google" content="notranslate">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <style>
      body {
        display: flex;
        width: 100%;
        height: 100vh;
        justify-content: center;
        align-items: center;
        background-color: white;
      }
      .content {
        font-size: 34px;
        font-weight: bold;
        color: black;
      }
    </style>
    </head>
    <body tabindex="1">
      <div class='content'>欢迎</div>
    </body>
    </html>
    `
  }

这个方法写在类里,然后修改一下调用的方法就行了

javascript 复制代码
this.webviewEditor.webview.html = this.getWelcomPageContent();
this.update();

预览当前编辑的 HTML

右侧视图代码也就是 html 视图,那么本地当前编辑的 html 文件是否可以通过右侧进行预览呢

肯定是可以的,只要知道如何获取当前编辑的代码文本内容即可,再传进去就能预览了 不要忘记在 package.json 中添加 html 的激活事件

javascript 复制代码
"activationEvents": [
  "onLanguage:typescript",
  "onLanguage:html"
],

在 vscode 官方提供的 API 中,有很多都可以拿到当前正在编辑的文本内容,给两个常用的

javascript 复制代码
vscode.workspace.onDidSaveTextDocument
vscode.workspace.onDidChangeTextDocument

第一个是在保存的时候会调用,第二个是只要文本内容发生变更就会调用,推荐第一种 我们回到 extension.ts 中,在 activate 函数中添加以下代码

javascript 复制代码
let myPreview: PdfPreview;
export function activate(context: vscode.ExtensionContext): void {
  ···	// 省略其他代码
  
  vscode.workspace.onDidSaveTextDocument((e) => {
    // 这里编写让预览视图重新加载的方法
    myPreview.reloadPage(e.getText());
  });
  
  ··· // 省略其他代码
}

async function openPreviewToTheSide(uri?: vscode.Uri) {
  ···	// 省略其他代码
	myPreview = new PdfPreview(
    extensionRoot,
    uri,
    webviewPanel
  );
  
  webviewPanel.onDidDispose(() => {
    myPreview.dispose();
  });
}

显然 reloadPage 这个方法我们并没有写,现在 pdfPreview 类中增加这个方法即可,记得要写为 public 方法,也非常简单

javascript 复制代码
public reloadPage(content: string) {
  this.webviewEditor.webview.html = content;
  this.update();
}

背景颜色没有设置默认跟随主题,这个自己调一下就行了

FTL / PDF 预览

这是我写这个插件的原因,公司业务有 PDF 结果物输出,模板是前端写的,后端拿到前端写的 FTL 模板后转换成PDF ,公司有平台专门做预览,倒是还行。但是我还是嫌麻烦,要上传预览看效果,因此我希望能够直接在 vscode 里面就能看预览,于是我就做了个插件

接下来的内容仅针对通过后端解析 FTL 的场景,即你有一个能接收你的 FTL 文件内容并生成 PDF 流回传的接口。

时间限制,这里就不模拟完整的 上传-回传-预览 流程(可以直接安装 axios 进行上传的处理),为了演示,这里简单写了一个返回 pdf 流的接口

这个是用于测试的接口,只要调了就会把这个 PDF 返回,简单的模拟

http://localhost:3000/api/common/tools/generate-pdf

这是我本地的服务地址,我用的 next 开发的接口,大家如果实在没有现成的接口也可以简单写一个

回到我们的插件代码中

我们需要在插件中调用这个接口,直接安装 axios

npm i axios

这里为了方便全部写在 pdfPreview 里面,我们直接给类增加一个方法

javascript 复制代码
const axios = require('axios');	//	axios 不要用 import 导入哦

···	// 省略其他代码
private async queryPDF() {
  const res = await axios({
    method: 'get',
    url: 'http://localhost:3000/api/common/tools/generate-pdf',
    responseType: 'arraybuffer',
  })
  if (res.status === 200) {
    if (
      res.headers['content-type'].indexOf('application/json') !== -1
    ) {
      // 其他处理
    } else {
      return res.data;
    }
  }
}

这里的代码只做参考,演示阶段跑通就行,细节请大家自己完善

这里还很简单,接下来就有不少坑了,如果对 vscode 插件开发环境不熟悉的强烈建议跟着步骤走

我们还是在这个文件中,找到 getWebviewContents 方法,我们进行一定的修改

javascript 复制代码
--- 修改前:private getWebviewContents(): string {
--- 修改后:private getWebviewContents(dataStream: any): string {

这里没啥,我打算直接将 dataStream 传进来

javascript 复制代码
··· // 省略部分代码
const settings = {
  cMapUrl: resolveAsUri('lib', 'web', 'cmaps/').toString(),
  // path: docPath.toString(),
  path: '',	// path 直接置空,因为我们要显示的是后端给的 PDF,这个路径就没意义了
  dataStream,	// 注意这里增加了一个 dataStream,是传进来的
  defaults: {
    cursor: 'select',
    scale: 'auto',
    sidebar: 'false',
    scrollMode: 'vertical',
    spreadMode: 'none',
  },
};
··· // 省略部分代码

这里删掉了原来的 config,直接把很多配置写死了(这个配置如果要自定义需要改 package.json,这里不展开讲了),这里不删也行,删掉主要是方便。

接下来修改下面这段内容

改之前:

javascript 复制代码
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; connect-src ${cspSource}; script-src 'unsafe-inline' ${cspSource}; style-src 'unsafe-inline' ${cspSource}; img-src blob: data: ${cspSource};">

改之后:

javascript 复制代码
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; connect-src * blob:; script-src 'unsafe-inline' ${cspSource}; style-src 'unsafe-inline' ${cspSource}; img-src blob: data: ${cspSource};">

主要就是改了 connect-src,使其支持 blob

我们回到上面,既然已经改了传参,我们调用肯定要传进去

javascript 复制代码
this.queryPDF().then(data => {
  this.webviewEditor.webview.html = this.getWebviewContents(data);
  this.update();
});

如果你是跟着一步一步坐下来的,这行代码大概在这个位置

好的,接下来我们到 lib/main.js 这个文件当中,这个文件主要用来做一些预处理,包括导入配置等等。当然,最后使用 pdfjs 展示 pdf 的也是这里

我们搜索下面代码,进行定位

javascript 复制代码
window.addEventListener('load', async function () {

定位到之后我们在这个代码块中添加下面代码

javascript 复制代码
window.addEventListener('load', async function () {
    const config = loadConfig()	// 这行原来就有
    const arrayBuffer = config.dataStream.data;	// 拿到我们刚刚传入的 dataStream
    const blob = new Blob([arrayBuffer], {
      type: 'application/octet-stream',
    });
    config.path = URL.createObjectURL(blob);	// 这部分代码应该不需要我多解释了
··· // 省略其他代码

有人可能看到这段代码之后会疑惑,为什么不在外面使用 URL.createObjectURL(blob),而要在这里做处理。原因是外面用会因为安全限制,提示不能访问本地文件,即便它是用 blob 创建的 url,只有在里面创建 url 才能正常显示

我们继续往下,大概在75行左右,可以看到下面的代码

javascript 复制代码
PDFViewerApplication.open(config.path).then(async function () {
  const doc = await pdfjsLib.getDocument(loadOpts).promise
  doc._pdfInfo.fingerprints = [config.path]
  PDFViewerApplication.load(doc)
})

我们将这段代码全部注掉,在下面添加新的代码

javascript 复制代码
// PDFViewerApplication.open(config.path).then(async function () {
//   const doc = await pdfjsLib.getDocument(loadOpts).promise
//   doc._pdfInfo.fingerprints = [config.path]
//   PDFViewerApplication.load(doc)
// })
pdfjsLib.getDocument({ data: arrayBuffer }).promise.then(doc => {
  PDFViewerApplication.load(doc)
})

这里就不好解释了,主要就是我们使用方式比较特殊,要做的对应的变更

如上,就完成了全部的修改,有很多细节需要大家自行完善,这里主要带个头把功能跑通 我们看一看效果

最后再次提醒,这个 pdf 是从接口拿到的,原本正常的流程是 本地 FTL 上传 -> PDF 回传 -> 右侧视图预览

因为时间和条件限制,FTL 上传的部分就没给大家做,大家可以结合我们上面预览 HTML 部分的内容自行开发,其实整个逻辑是一致的

相关推荐
张小小大智慧2 小时前
TypeScript 的发展与基本语法
前端·javascript·typescript
幼儿园的小霸王3 小时前
通过socket设置版本更新提示
前端·vue.js·webpack·typescript·前端框架·anti-design-vue
疯狂的沙粒3 小时前
对 TypeScript 中高级类型的理解?应该在哪些方面可以更好的使用!
前端·javascript·typescript
endingCode8 小时前
45.坑王驾到第九期:Mac安装typescript后tsc命令无效的问题
javascript·macos·typescript
前端百草阁10 小时前
【TS简单上手,快速入门教程】————适合零基础
javascript·typescript
彭世瑜10 小时前
ts: TypeScript跳过检查/忽略类型检查
前端·javascript·typescript
FØund40410 小时前
antd form.setFieldsValue问题总结
前端·react.js·typescript·html
Backstroke fish10 小时前
Token刷新机制
前端·javascript·vue.js·typescript·vue
小曲程序10 小时前
vue3 封装request请求
java·前端·typescript·vue
临枫54110 小时前
Nuxt3封装网络请求 useFetch & $fetch
前端·javascript·vue.js·typescript