开始之前,请下载 vscode pdf 预览的代码
https://github.com/tomoki1207/vscode-pdfviewer
安装这个插件可以让 vscode 可以查看 PDF ,我们在此基础上进行开发
先确定代码结构(别忘了 npm install)
简单说一下,lib 里面主要是和预览 PDF 有关的代码,用到了谷歌的 pdfjs,src 中主要是和插件相关的代码,使用到 vscode 的 API,与编辑器打交道的代码在这里面
在右键菜单栏中添加选项
比较合适的做法是注册一个命令,将该命令与视图的右键菜单进行绑定
如图,我们在 vscode 中右键打开的菜单中有很多选项,那么要如何在这个菜单中添加我们自己的功能呢?
找到我们刚刚的代码中的 package.json,找到 contributes 项,在下面添加 commands 项,如下图
注册一个命令
"commands": [
{
"command": "ftl-preview-enhanced.openPreviewToTheSide",
"title": "%ftl-preview-enhanced.openPreviewToTheSide.title%",
"category": "Ftl"
}
]
我在这里填了三个内容,它们对应的值其实都是自定义的,只是要约定各处使用时保持一致即可。
因为我们是绑定右键菜单,这里的 title 就是具体显示在菜单当中的文字,如果你写法和我一样,那么还需要在根目录中添加两个文件
// 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 属性
"commands": [···],
"menus": {
"editor/context": [
{
"command": "ftl-preview-enhanced.openPreviewToTheSide"
}
]
}
这时候我们就成功添加好了,CTRL + shift + B 进行编译,然后 F5 运行
我们还可以在添加 when 属性,用来控制在何时显示该选项,比如
"menus": {
"editor/context": [
{
"command": "ftl-preview-enhanced.openPreviewToTheSide",
"when": "editorLangId == ftl"
}
]
}
“when”: “editorLangId == ftl” 就表示仅在文件是 FTL 格式的时候才显示该菜单栏,但是别急,还差一步,我们需要设置在 FTL 格式文件时激活插件才能正常显示,需要额外配置一个属性,该属性配置在最外层,与 contributes 同级
"activationEvents": [
"onLanguage:typescript", // 这里很重要,会直接影响后面代码能否运行
"onLanguage:ftl"
],
这样就可以了,其余类型的文件同理。
打开右侧视图
上面的代码只是添加了一个右键菜单选项,但是该选项并没有具体的功能,很简单,因为我们目前为止一直在进行配置,并没有写相关的代码
打开 src 下的 extension,能看到下面的代码
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 文件,这不是我们要的功能,直接删掉即可
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 留着,后面能用上
现在我们给我们刚刚注册的命令绑定事件(部分重复代码不再展示)
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 便是我们需要写的函数
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
到这里就很简单了,大家可以根据自己需要定制页面内容,这里就简单演示一个欢迎界面
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>
`
}
这个方法写在类里,然后修改一下调用的方法就行了
this.webviewEditor.webview.html = this.getWelcomPageContent();
this.update();
预览当前编辑的 HTML
右侧视图代码也就是 html 视图,那么本地当前编辑的 html 文件是否可以通过右侧进行预览呢
肯定是可以的,只要知道如何获取当前编辑的代码文本内容即可,再传进去就能预览了
不要忘记在 package.json 中添加 html 的激活事件
"activationEvents": [
"onLanguage:typescript",
"onLanguage:html"
],
在 vscode 官方提供的 API 中,有很多都可以拿到当前正在编辑的文本内容,给两个常用的
vscode.workspace.onDidSaveTextDocument
vscode.workspace.onDidChangeTextDocument
第一个是在保存的时候会调用,第二个是只要文本内容发生变更就会调用,推荐第一种
我们回到 extension.ts 中,在 activate 函数中添加以下代码
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 方法,也非常简单
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 里面,我们直接给类增加一个方法
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 方法,我们进行一定的修改
--- 修改前:private getWebviewContents(): string {
--- 修改后:private getWebviewContents(dataStream: any): string {
这里没啥,我打算直接将 dataStream 传进来
··· // 省略部分代码
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,这里不展开),这里不删也行,删掉主要是方便。
接下来修改下面这段内容
改之前:
<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};">
改之后:
<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
我们回到上面,既然已经改了传参,我们调用肯定要传进去
this.queryPDF().then(data => {
this.webviewEditor.webview.html = this.getWebviewContents(data);
this.update();
});
如果你是跟着一步一步坐下来的,这行代码大概在这个位置
好的,接下来我们到 lib/main.js 这个文件当中,这个文件主要用来做一些预处理,包括导入配置等等。当然,最后使用 pdfjs 展示 pdf 的也是这里
我们搜索下面代码,进行定位
window.addEventListener('load', async function () {
定位到之后我们在这个代码块中添加下面代码
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行左右,可以看到下面的代码
PDFViewerApplication.open(config.path).then(async function () {
const doc = await pdfjsLib.getDocument(loadOpts).promise
doc._pdfInfo.fingerprints = [config.path]
PDFViewerApplication.load(doc)
})
我们将这段代码全部注掉,在下面添加新的代码
// 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 部分的内容自行开发,其实整个逻辑是一致的