【vscode】vscode插件学习(四)

前言

  • 今天继续学习制作vscode插件,本篇基本把vscode插件开发套路给摸清了。

官网与参考资料

  • VSCode WebView说明 https://code.visualstudio.com/api/extension-guides/webview

  • VSCode 自身内置命令大全 https://code.visualstudio.com/docs/getstarted/keybindings

  • 小茗同学的博客园 https://www.cnblogs.com/liuxianan/p/vscode-plugin-webview.html

  • VSCode插件开发入门 https://zhuanlan.zhihu.com/p/99198980

  • 从零开始实现VS Code基金插件 https://juejin.cn/post/6864712731484749831

  • github小说插件原型 https://github.com/aooiuu/z-reader

前置理解

  • vscode的插件开发主要是调api,那么如何找api和如何快速知道自己想要的是哪个api就比较重要。一般是需要查看ts声明或者直接查看文档。
  • vscode的每个模块一般都是个类,自己去实现或者继承或者直接new出实例进行使用。反正最终都是要new出实例。此实例的生命周期以及一些方法都在类上。如果出了实例那就在实例上找,用ts很容易找到。
  • 另外剩余一些跳出这个套路的查看文档或者别人写的示例即可。

插件制作

  • 本次制作线上查找小说,并且可以在右下角显示读取进度,在每次tab页关闭后再次打开可以直接跳到进度处。
  • 上一次制作的本地小说是TreeDataProvider,这次就直接在这个上加上线上功能。
  • TreeDataProvider是可以注册多个的,注册多个的效果就类似于手风琴那种。我们要在这个Provider的右侧增加按钮,则需要在贡献点配上:
	"menus": {
			"view/title": [
				{
					"command": "searchOnlineNovel",
					"when": "view == myread-list",
					"group": "navigation"
				}
			]
		}
	"commands": [
			{
				"command": "storyvs.helloWorld",
				"title": "Hello World"
			},
			{
				"title": "线上搜索",
				"command": "searchOnlineNovel"
			}
		],
  • 这个when其实就是因为那个可以注册多个,只显示在一个上。
  • 下面是改造provider,我们知道Provider的每个列表项是treeItem,那么本地就是实现的本地的列表项,而线上实现的线上的列表项。而数据源就是getChildren或者construcor里获取:
class Provider implements vscode.TreeDataProvider<NovelItem> {
	// 发布订阅的事件
	public refreshEvent: vscode.EventEmitter<NovelItem | null> = new vscode.EventEmitter<NovelItem | null>();
	// 挂到该方法上实现刷新
	onDidChangeTreeData: vscode.Event<NovelItem | null> = this.refreshEvent
		.event;

	// 判断列表是本地还是在线
	public isOnline = false;

	public treeNode: NovelItem[] = [];

	constructor() {
		// 默认列表上先加载本地
		getLocalBooks().then((res) => {
			this.treeNode = res;
		});
	}
	// 封装一个本地和在线的切换方法
	refresh(isOnline: boolean) {
		this.isOnline = isOnline;
		this.refreshEvent.fire(null);
	}
	// 根据本地还是在线会加载不同的列表项
	getTreeItem(info: NovelItem): NovelTreeItem {
		if (this.isOnline) {
			return new OnlineTreeItem(info);
		}

		return new NovelTreeItem(info);
	}
	// 现在把列表每项的数据放在treenode上,除了在线小说展开章节的情况
	async getChildren(element?: NovelItem | undefined): Promise<NovelItem[]> {
		if (element) {
			return await getChapter(element.path);
		}
		return this.treeNode;
	}
}
  • novelItem是自己定义的类型,结合实际需要:
export interface NovelItem {
	name: string;
	path: string;
	isDirectory: boolean;
	type: string;
}
  • treeItem的数据类型是Provider传来的,你可以在constructor里获取到类型,继承改写它内置的属性方法来实现显示文字,点击触发命令等等:
export default class OnlineTreeItem extends TreeItem {
	constructor(info: NovelItem) {
		super(`${info.name}`);

		const tips = [`名称: ${info.name}`];

		this.tooltip = tips.join("\r\n");
		// 根据isDirectory属性判断是不是可折叠的组
		this.collapsibleState = info.isDirectory
			? vscode.TreeItemCollapsibleState.Collapsed
			: vscode.TreeItemCollapsibleState.None;
		// 这里命令也换了一个,换成openOnlineNovel(注意注册一下)
		this.command = info.isDirectory
			? undefined
			: {
					command: "openOnlineNovel",
					title: "打开该网络小说",
					arguments: [{ name: info.name, path: info.path }],
			  };
	}
	contextValue = "online";
}
  • 点击搜索时跳出inputbox,并注册进前面贡献点那配置的命令,这样当搜索后,provider刷新,获得了新的数据源,从而渲染线上资源。
export const searchOnline = async function (provider: Provider) {
	const msg = await vscode.window.showInputBox({
		password: false,
		ignoreFocusOut: false,
		placeHolder: "请输入小说的名字",
		prompt: "",
	});
	if (msg) {
		provider.treeNode = await search(msg);
		provider.refresh(true);
	}
};
  • statusBar是vscode底部的玩意,它可以设置在左边还是在右边,当然,你还需要处理它销毁,否则它不会随着webview关闭主动消失。所以对statusBar进行个简单封装
class StatusBar {
	private statusBar: vscode.StatusBarItem | undefined;
	constructor() {
		this.statusBar = vscode.window.createStatusBarItem(
			vscode.StatusBarAlignment.Right
		);
		this.statusBar.text = "yehuozhili";
		this.statusBar.show();
	}
	set(val: string) {
		if (this.statusBar) {
			this.statusBar.text = val;
		}
	}
	dispose() {
		if (this.statusBar) {
			this.statusBar.dispose();
		}
	}
}
  • 爬虫部分不是本文重点,这里就不说了:
import * as cheerio from "cheerio";
import * as https from "https";
import { NovelItem } from "./extension";

const request = async (url: string): Promise<string> => {
	return new Promise((resolve, reject) => {
		https.get(url, (res) => {
			let chunks = "";
			if (!res || res.statusCode !== 200) {
				reject(new Error("网络请求错误!"));
				return;
			}
			res.on("data", (chunk) => {
				chunks += chunk.toString("utf8");
			});
			res.on("end", () => {
				resolve(chunks);
			});
		});
	});
};

// 我们从笔趣阁小说站抓页面
const DOMAIN = "https://www.biquge.com.cn";

// 搜索关键词相对应的小说
export const search = async (keyword: string) => {
	const result = [] as any;
	try {
		const res = await request(
			DOMAIN + "/search.php?q=" + encodeURI(keyword)
		);
		console.log(res);

		const $ = cheerio.load(res);
		$(".result-list .result-item.result-game-item").each(function (
			i: number,
			elem: any
		) {
			const title = $(elem)
				.find("a.result-game-item-title-link span")
				.text();
			const author = $(elem)
				.find(
					".result-game-item-info .result-game-item-info-tag:nth-child(1) span:nth-child(2)"
				)
				.text();
			const path = $(elem).find("a.result-game-item-pic-link").attr()
				.href;
			console.log(title, author, path);

			result.push({
				type: ".biquge",
				name: `${title} - ${author}`,
				isDirectory: true,
				path,
			});
		});
	} catch (error) {
		console.warn(error);
	}
	return result;
};

// 搜索该小说对应的章节
export const getChapter = async (pathStr: string) => {
	const result: NovelItem[] = [];
	try {
		const res = await request(DOMAIN + pathStr);
		const $ = cheerio.load(res);
		$("#list dd").each(function (i: number, elem: any) {
			const name = $(elem).find("a").text();
			const path = $(elem).find("a").attr().href;
			result.push({
				type: ".biquge",
				name,
				isDirectory: false,
				path,
			});
		});
	} catch (error) {
		console.warn(error);
	}
	return result;
};
// 获取章节对应的内容并html化
export const getContent = async (pathStr: string) => {
	let result = "";
	try {
		const res = await request(DOMAIN + pathStr);
		const $ = cheerio.load(res);
		const html = $("#content").html();
		result = html ? html : "";
	} catch (error) {
		console.warn(error);
	}
	return result;
};
  • 下面需要整一下webview。当点击treeItem时,触发其command,打开pannel,webview是挂在pannel上的,所以对于statusBar的处理可以通过pannel的onDidChangeViewState来判断它是不是激活状态。如果是激活状态,则new一个statusBar出来显示,如果是非激活状态,则把statusBar干掉,保证只有一个statusBar出现。另外在pannel关闭时,需要把statusBar给关掉。
  • webview与vscode的通信靠postmessage,由于我们需要记录每个小说的位置,那么就激活命令时创建一个对象,每个webview会写入这个对象看的进度:
	const progressSetting: Record<
		string,
		any
	> = vscode.workspace.getConfiguration().get("novel.progress", {});
  • 最后靠通信每次打开时滚动到对应位置,在看的过程当中,每隔一段时间向vscode写入位置即可。
const oponline = vscode.commands.registerCommand(
		"openOnlineNovel",
		async function (args) {
			const panel = vscode.window.createWebviewPanel(
				"novelReadWebview",
				args.name,
				vscode.ViewColumn.One,
				{
					enableScripts: true,
					retainContextWhenHidden: true,
				}
			);
			let status: StatusBar;
			panel.onDidChangeViewState((e) => {
				const active = e.webviewPanel.active;
				if (active) {
					status = new StatusBar();
				} else {
					if (status) {
						status.dispose();
					}
				}
			});

			const handleMessage = (message: {
				command: string;
				progress: number;
			}) => {
				progressSetting[args.name] = message.progress;
				if (status) {
					status.set((message.progress * 100).toFixed(2) + "%");
				}
				switch (message.command) {
					case "updateProgress":
						return vscode.workspace
							.getConfiguration()
							.update("novel.progress", progressSetting, true);
				}
			};

			const content = await getContent(args.path);
			panel.webview.html = `<html><script>
			const vscode = acquireVsCodeApi();
			setInterval(() => {
				vscode.postMessage({
					command: 'updateProgress',
					progress: window.scrollY / document.body.scrollHeight
				})
			}, 1000);  
			window.addEventListener('message', event => {
				const message = event.data; 
				switch (message.command) {
					case 'goProgress':
						window.scrollTo(0, document.body.scrollHeight * message.progress);
						break;
				}
			});
		</script><body>${content}</body></html>`;

			panel.webview.onDidReceiveMessage(
				handleMessage,
				undefined,
				context.subscriptions
			);
			panel.webview.postMessage({
				command: "goProgress",
				progress: progressSetting[args.name],
			});

			panel.onDidDispose((e) => {
				status.dispose();
			});
		}
	);
  • 这样插件就完成了。可以参考前面文章进行打包发布。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

业火之理

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值