本篇是 VS Code 插件開發實戰系列第三篇,前面兩篇是
《一起來寫 VS Code 插件:為你的團隊提供常用代碼片段》
《一起來寫 VS Code 插件:實現一個翻譯插件》
「CNode」 社區為國內最專業的 Node.js 開源技術社區,致力於 Node.js 的技術研究。本篇將通過實現 VS Code 版 CNode, 來帶領大家一起熟悉 VSCode Webview 強大的功能。在開始之前,我們先參考 官網關於 webview 的介紹。Webview API 允許擴展在 visualstudio 代碼中創建完全可定製的視圖,可以將 webview 看作是 VS Code 中的 iframe。
我們可以通過網頁將事件消息傳遞給我們的服務端(包括 NodeJS), 服務端處理完後可以把消息數據傳遞給網頁。因此我們能在 extensions 中開發出跟網頁一樣的內容,但實現遠比網頁更強大的功能。
2. 效果首先來看下實現的效果
主要分為 2 部分,左側是主題列表,右側是主題詳情。
3. 初始化項目首先通過腳手架初始化一個 typescript + webpack 的工程
4. 配置左側導航圖標 "icon": "icon.png",
"activationEvents": [
"onView:vs-sidebar-view"
],
"contributes": {
"viewsContainers": {
"activitybar": [
{
"id": "vs-sidebar-view",
"title": "CNODE 社區",
"icon": "media/cnode_icon_64.png"
}
]
},
"views": {
"vs-sidebar-view": [
{
"type": "webview",
"id": "vs-sidebar-view",
"name": "Topic 列表",
"icon": "media/cnode_icon_64.png",
"contextualTitle": "Topic 列表"
}
]
}
},
...
views 是配置視圖列表,activitybar 是定義下顯示在側邊導航上的視圖。
4.1. 註冊一個側邊欄在 extension.ts 中註冊一個 與 package.json 對應的 vs-sidebar-view側邊欄ID
import * as vscode from "vscode";
import { SidebarProvider } from "./SidebarProvider";
export function activate(context: vscode.ExtensionContext) {
const sidebarPanel = new SidebarProvider(context.extensionUri);
context.subscriptions.push(
vscode.window.registerWebviewViewProvider("vs-sidebar-view", sidebarPanel)
);
}
import * as vscode from "vscode";
import { getNonce } from "./getNonce";
export class SidebarProvider implements vscode.WebviewViewProvider {
_view?: vscode.WebviewView;
_doc?: vscode.TextDocument;
constructor(private readonly _extensionUri: vscode.Uri) {}
public resolveWebviewView(webviewView: vscode.WebviewView) {
this._view = webviewView;
webviewView.webview.options = {
// 在 webview 允許腳本
enableScripts: true,
localResourceRoots: [this._extensionUri],
};
webviewView.webview.html = this._getHtmlForWebview(webviewView.webview);
}
public revive(panel: vscode.WebviewView) {
this._view = panel;
}
private _getHtmlForWebview(webview: vscode.Webview) {
const scriptUri = webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, "build", "static/js/main.js")
);
const styleMainUri = webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, "build", "main.css")
);
// Use a nonce to 只允許特定腳本運行.
const nonce = getNonce();
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="img-src https: data:; style-src 'unsafe-inline' ${webview.cspSource}; script-src 'nonce-${nonce}';">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="${styleMainUri}" rel="stylesheet">
<script nonce="${nonce}">
const tsvscode = acquireVsCodeApi(); //內置函數,可以訪問 VS Code API 對象
const apiBaseUrl = 'https://cnodejs.org/'
</script>
</head>
<body>
<section id="root"></section>
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
</html>`;
}
}
上述代碼採用面向對象的方式實現一個 SidebarProvider類,根據 vscode.WebviewViewProvider,
其實實現所有的 WebviewViewProvider 都是是這段代碼,其他代碼都是相同的,因為關於 webview 中的 HTML 我們都可以使用js來生成,這不正是我們的單頁面應用開發嗎?
上述代碼中, 「Nonce」是一個在加密通信只能使用一次的數字。在認證協議中,它往往是一個隨機或偽隨機數,以避免重放攻擊。Nonce也用於流密碼以確保安全。如果需要使用相同的密鑰加密一個以上的消息,就需要Nonce來確保不同的消息與該密鑰加密的密鑰流不同。 所以我們直接拷貝官方demo 中的代碼。
// 生成特定隨機數
export function getNonce() {
let text = "";
const possible =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (let i = 0; i < 32; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
CNode 提供了允許跨域的 API,我們可以在 js 中直接調用,如果你也想開發類似的功能請在HTTP headers 中加入
Access-Control-Allow-Origin: *
在原先 webpack.config.js 中加入打包 React 的配置,webpack5 支持多份 config 配置。
const viewConfig = {
entry: "./view/index.tsx",
output: {
path: path.resolve(__dirname, "build"),
filename: "static/js/[name].js",
},
mode: "production",
plugins: [
new miniCssExtractPlugin(),
],
module: {
rules: [
{
test: /\.(ts|tsx)$/i,
loader: "ts-loader",
exclude: ["/node_modules/"],
},
{
test: /\.css$/i,
use: [miniCssExtractPlugin.loader, "css-loader", "postcss-loader"],
},
],
},
resolve: {
extensions: [".ts", ".tsx"],
},
}
module.exports = [extensionConfig, viewConfig];
然後啟動調試的時候,webpack就會自動打包了;
「注意」 這裡的 mode 必須設置為 production,webpack development 模式會使用 eval 來執行代碼,而 eval 在 VS Code webview 不允許執行。
5.2. 配置 tailwindcss為了方便,我這邊使用了tailwindcss,因為我可以使用 tailwindcss-typography 這個插件,幫我生成漂亮的文章類型排版。
yarn add tailwindcss @tailwindcss/typography autoprefixer
使用命令初始化 tailwindcss config
npx tailwindcss init
module.exports = {
mode: "jit",
purge: ["./view/**/*.tsx"],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [require("@tailwindcss/typography")],
};
mode jit 是及時編譯模式 tailwindcss 2.1 版本加的,忽略掉我們不需要的css 代碼。
生成文章頁面的樣式
.markdown-preview {
@apply prose prose-lg max-w-full bg-white p-20;
}
使用 react 實現一個列表的代碼我這邊就不敘述了,跟我們平常寫業務沒什麼區別,最主要的是 「數據通信」,當我們點擊主題列表,右邊要打開一個新的 webview 頁面
const handleClick = (item: Topic) => {
setCurrent(item.id);
tsvscode.postMessage({ type: "detail", value: item });
};
根據官方例子
scripts-webview_to_extension.gifWebviews 還可以將消息傳遞迴它們的擴展。這是通過在 webview 中的特殊 VS Code API 對象上使用 postMessage 函數來實現的。要訪問 VS Code API 對象,就要在 webview 中調用 acquireVsCodeApi函數。
定義TS 全局對象
import * as _vscode from "vscode";
declare global {
const tsvscode: {
postMessage: ({ type: string, value: any }) => void;
getState: () => any;
setState: (state: any) => void;
};
const apiBaseUrl: string;
}
接著就可以在 vs-sidebar-view 接收數據了。
webviewView.webview.onDidReceiveMessage(async (data) => {
switch (data.type) {
case "detail":
createPreviewPanel(this._extensionUri, data.value);
break;
default:
break;
}
});
收到數據後可以就可以打開一個預覽頁面。
6. 預覽頁面實現function createPreviewPanel(topic: Topic){
// 創建一個新的 panel.
const panel = vscode.window.createWebviewPanel(
"cnode-preview",
"CNODE 技術社區",
column || vscode.ViewColumn.One,
{
// 在 webview 允許腳本
enableScripts: true,
// 限制 從 media 文件夾加載資源
localResourceRoots: [vscode.Uri.joinPath(extensionUri, "build")],
}
);
panel.webview.html = _getHtmlForWebview(panel.webview, topic);
}
可以使用 內置方法 vscode.window.createWebviewPanel 創建一個新的面板,並且接收主題數據。_getHtmlForWebview 與 SidebarProvider 中的 _getHtmlForWebview 一致,返回 HTML 即可。
7. 避免重複創建預覽頁當然也可以通過 postMessage 傳遞屬性
「extension 端」
panel.webview.postMessage({text: 'hello'});
「webview 端」
window.addEventListener('message', event => {
const message = event.data;
console.log('Webview接收到的消息:', message);
}
VS Code 將主題分為三類,並在 body 元素中添加一個 class 來指示當前主題:
body.vscode-light {
color: black;
}
body.vscode-dark {
color: white;
}
body.vscode-high-contrast {
color: red;
}
Webviews 還可以使用 CSS 變量訪問 VS Code 主題顏色。這些變量名以 vscode 作為前綴,並用-替換.。例如 editor.foreground 變為 var (--vscode-editor-foreground)。
查看可用主題變量的主題顏色參考。還有一個擴展可以為變量提供智能建議。
9. 調試要調試Webview不能直接把 VSCode 的開發者工具打開,直接打開你只能看到一個<webview></webview>標籤,看不到代碼,要看代碼需要按下Ctrl+Shift+P然後執行打開Webview開發工具,英文版應該是Open Webview Developer Tools:
從上圖也可以看的 在html標籤上注入了當前皮膚的 css 變量。
10. 狀態保持與瀏覽器標籤不一樣的是,當 webview 移動到後臺又再次顯示時,webview 中的任何狀態都將丟失。因為 webview 是基於 iframe 實現的。
解決此問題的最佳方法是使你的 webview 無狀態,通過消息傳遞來保存webview的狀態。
10.1. state在 webview 的 js 中我們可以使用vscode.getState()和vscode.setState()方法來保存和恢復 JSON 可序列化狀態對象。當 webview 被隱藏時,即使 webview 內容本身被破壞,這些狀態仍然會保存。當然了,當 webview 被銷毀時,狀態將被銷毀。
10.2. 序列化通過註冊WebviewPanelSerializer可以實現在VScode重啟後自動恢復你的webview,當然,序列化其實也是建立在getState和setState之上的。
註冊方法:vscode.window.registerWebviewPanelSerializer
10.3. retainContextWhenHidden對於具有非常複雜的UI或狀態且無法快速保存和恢復的webview,我們可以直接使用retainContextWhenHidden選項。設置retainContextWhenHidden: true後即使webview被隱藏到後臺其狀態也不會丟失。
儘管retainContextWhenHidden很有吸引力,但它需要很高的內存開銷,一般建議在實在沒辦法的時候才啟用。
getState和setState是持久化的首選方式,因為它們的性能開銷要比retainContextWhenHidden低得多。
11. 發布關於發布可以看我的上一篇 一起來寫 VS Code 插件:為你的團隊提供常用代碼片段
12. 小結本篇通過實現 VS Code 版 CNode 來幫我們熟悉 webview 的 api,當然還可以增加評論系統,創建主題,基於用戶系統可以實現點讚收藏等。
開發更複雜的功能,只缺你的想像力。例如:
韭菜盒子,做最好用的股票和基金插件
create-app 可視化CLI工具
「最後」
附上本插件的下載地址和源碼
同時 vscode extensions 開發門檻不高,歡迎大家嘗試,或者將有意思的 extensions 推薦在評論區。
希望這篇文章對大家有所幫助,也可以參考我往期的文章或者在評論區交流你的想法和心得,歡迎一起探索前端。