【插件開發】VSCode插件開發全攻略(七)WebView

文章轉載於:https://www.cnblogs.com/liuxianan/p/vscode-plugin-webview.htmlcss

什麼是Webview

你們都知道,整個VSCode編輯器就是一張大的網頁,其實,咱們還能夠在Visual Studio Code中建立徹底自定義的、能夠間接和nodejs通訊的特殊網頁(經過一個acquireVsCodeApi特殊方法),這個網頁就叫WebView。內置的Markdown的預覽就是使用WebView實現的。使用Webview能夠構建複雜的、支持本地文件操做的用戶界面。html

VSCode插件的WebView相似於iframe的實現,但並非真正的iframe(我猜底層應該仍是基於iframe實現的,只不過上層包裝了一層),經過開發者工具能夠看到:vue

W1506xH802

1.1. demo

在咱們的vscode-plugin-demo中,我寫了一個很是簡單、沒啥實際意義的Webview示例僅供參考,在任意編輯器右鍵能夠看到打開Webview的菜單:node

W1424xH842

何時適合使用WebView

雖然Webview使人很振奮,由於基於它咱們能夠隨意發揮不受限制,但必須注意仍是要慎用,畢竟VSCode是很注重性能的,不能由於你一個插件拖累了整個IDE,通常僅在原有API和功能以及交互方式沒法知足你時才須要考慮,另外,設計糟糕的Webview也很容易在VS Code中讓人感受不溫馨,不能讓人家一看就以爲你這是一張網頁,好看的UI也很重要。git

這是官網給出的建議,在使用webview以前請考慮如下事項:github

  • 這個功能真的須要放在VSCode中嗎?做爲單獨的應用程序或網站會不會更好呢?
  • webview是實現這個功能的惟一方法嗎?可使用常規VS Code API嗎?
  • 您的webview是否會帶來足夠的用戶價值以證實其高資源成本?

正式開始WebView之旅

3.1. 建立WebView

context.subscriptions.push(vscode.commands.registerCommand('extension.demo.openWebview', function (uri) {
    // 建立webview
    const panel = vscode.window.createWebviewPanel(
        'testWebview', // viewType
        "WebView演示", // 視圖標題
        vscode.ViewColumn.One, // 顯示在編輯器的哪一個部位
        {
            enableScripts: true, // 啓用JS,默認禁用
            retainContextWhenHidden: true, // webview被隱藏時保持狀態,避免被重置
        }
    );
    panel.webview.html = `<html><body>你好,我是Webview</body></html>`

 

幾點說明:web

  • 默認狀況下,在Web視圖中禁用JavaScript,但能夠經過傳入enableScripts: true選項輕鬆啓用;
  • 默認狀況下當webview被隱藏時資源會被銷燬,經過retainContextWhenHidden: true會一直保存,但會佔用較大內存開銷,僅在須要時開啓;

3.2. 加載本地資源

出於安全考慮,Webview默認沒法直接訪問本地資源,它在一個孤立的上下文中運行,想要加載本地圖片、js、css等必須經過特殊的vscode-resource:協議,網頁裏面全部的靜態資源都要轉換成這種格式,不然沒法被正常加載json

vscode-resource:協議相似於file:協議,但它只容許訪問特定的本地文件。和file:同樣,vscode-resource:從磁盤加載絕對路徑的資源。api

我簡單封裝了一個轉換方法:安全

/**
 * 獲取某個擴展文件相對於webview須要的一種特殊路徑格式
 * 形如:vscode-resource:/Users/toonces/projects/vscode-cat-coding/media/cat.gif
 * @param context 上下文
 * @param relativePath 擴展中某個文件相對於根目錄的路徑,如 images/test.jpg
 */
getExtensionFileVscodeResource: function(context, relativePath) {
    const diskPath = vscode.Uri.file(path.join(context.extensionPath, relativePath));
    return diskPath.with({ scheme: 'vscode-resource' }).toString();
}

 

默認狀況下,vscode-resource:只能訪問如下位置中的資源:

  • 擴展程序安裝目錄中的文件。
  • 用戶當前活動的工做區內。
  • 固然,你還可使用dataURI直接在Webview中嵌入資源,這種方式沒有限制;

3.3. 從文件加載HTML內容

默認不支持從文件加載HTML,須要本身封裝代碼,我簡單封裝了一個供你們參考:

/**
 * 從某個HTML文件讀取能被Webview加載的HTML內容
 * @param {*} context 上下文
 * @param {*} templatePath 相對於插件根目錄的html文件相對路徑
 */
function getWebViewContent(context, templatePath) {
    const resourcePath = path.join(context.extensionPath, templatePath);
    const dirPath = path.dirname(resourcePath);
    let html = fs.readFileSync(resourcePath, 'utf-8');
    // vscode不支持直接加載本地資源,須要替換成其專有路徑格式,這裏只是簡單的將樣式和JS的路徑替換
    html = html.replace(/(<link.+?href="|<script.+?src="|<img.+?src=")(.+?)"/g, (m, $1, $2) => {
        return $1 + vscode.Uri.file(path.resolve(dirPath, $2)).with({ scheme: 'vscode-resource' }).toString() + '"';
    });
    return html;
}

 

運行這段代碼以後,會自動將HTML文件中linkhrefscriptimg的資源相對路徑所有替換成正確的vscode-resource:絕對路徑,例如:

../../lib/vue-2.5.17/vue.js
變成
vscode-resource:/Users/test/workspace/vscode-plugin-demo/lib/vue-2.5.17/vue.js

 

使用方法以下:

panel.webview.html = getWebViewContent(context, 'src/view/test-webview.html');

 

3.4. 消息通訊

重頭戲來了,Webview和普通網頁很是相似,不能直接調用任何VSCodeAPI,可是,它惟一特別之處就在於多了一個名叫acquireVsCodeApi的方法,執行這個方法會返回一個超級閹割版的vscode對象,這個對象裏面有且僅有以下3個能夠和插件通訊的API:

W624xH430

插件和Webview之間如何互相通訊呢?

插件給Webview發送消息(支持發送任意能夠被JSON化的數據):

panel.webview.postMessage({text: '你好,我是小茗同窗!'});

 

Webview端接收:

window.addEventListener('message', event => {
    const message = event.data;
    console.log('Webview接收到的消息:', message);
}

 

Webview主動發送消息給插件:

vscode.postMessage({text: '你好,我是Webview啊!'});

 

插件接收:

panel.webview.onDidReceiveMessage(message => {
    console.log('插件收到的消息:', message);
}, undefined, context.subscriptions);

 

3.4.1. 簡單通訊封裝

爲了雙方通訊方便,我把它們簡單封裝了一下,僅供參考,Webview端:

const callbacks = {}; // 存放全部的回調函數
/**
 * 調用vscode原生api
 * @param data 能夠是相似 {cmd: 'xxx', param1: 'xxx'},也能夠直接是 cmd 字符串
 * @param cb 可選的回調函數
 */
function callVscode(data, cb) {
    if (typeof data === 'string') {
        data = { cmd: data };
    }
    if (cb) {
        // 時間戳加上5位隨機數
        const cbid = Date.now() + '' + Math.round(Math.random() * 100000);
        // 將回調函數分配一個隨機cbid而後存起來,後續須要執行的時候再撈起來
        callbacks[cbid] = cb;
        data.cbid = cbid;
    }
    vscode.postMessage(data);
}
window.addEventListener('message', event => {
    const message = event.data;
    switch (message.cmd) {
        // 來自vscode的回調
        case 'vscodeCallback':
            console.log(message.data);
            (callbacks[message.cbid] || function () { })(message.data);
            delete callbacks[message.cbid]; // 執行完回調刪除
            break;
        default: break;
    }
});

 

插件端:

let global = { projectPath, panel};
panel.webview.onDidReceiveMessage(message => {
    if (messageHandler[message.cmd]) {
        // cmd表示要執行的方法名稱
        messageHandler[message.cmd](global, message);
    } else {
        util.showError(`未找到名爲 ${message.cmd} 的方法!`);
    }
}, undefined, context.subscriptions);

/**
 * 存放全部消息回調函數,根據 message.cmd 來決定調用哪一個方法,
 * 想調用什麼方法,就在這裏寫一個和cmd同名的方法實現便可
 */
const messageHandler = {
    // 彈出提示
    alert(global, message) {
        util.showInfo(message.info);
    },
    // 顯示錯誤提示
    error(global, message) {
        util.showError(message.info);
    },
    // 回調示例:獲取工程名
    getProjectName(global, message) {
        invokeCallback(global.panel, message, util.getProjectName(global.projectPath));
    }
}
/**
 * 執行回調函數
 * @param {*} panel 
 * @param {*} message 
 * @param {*} resp 
 */
function invokeCallback(panel, message, resp) {
    console.log('回調消息:', resp);
    // 錯誤碼在400-600之間的,默認彈出錯誤提示
    if (typeof resp == 'object' && resp.code && resp.code >= 400 && resp.code < 600) {
        util.showError(resp.message || '發生未知錯誤!');
    }
    panel.webview.postMessage({cmd: 'vscodeCallback', cbid: message.cbid, data: resp});
}

 

按上述方法封裝以後,例如,Webview端想要執行名爲openFileInVscode命令只須要這樣:

callVscode({cmd: 'openFileInVscode', path: `package.json`}, (message) => {
    this.alert(message);
});

 

而後在插件端的messageHandler實現openFileInVscode方法便可,其它都不用管:

const messageHandler = {
    // 省略其它方法
    openFileInVscode(global, message) {
        util.openFileInVscode(`${global.projectPath}/${message.path}`);
        invokeCallback(global.panel, message, '打開文件成功!');
    }
};

 

以上封裝的比較隨便,只是給你們提供一個思路,有時間能夠好好封裝一下。

3.5. 主題適配

Webview能夠根據VS Code的當前主題更改其外觀,原理是body上面添加當前主題名稱,主要有如下三種:

vscode-light - 淺色主題;
vscode-dark -深色主題;
vscode-high-contrast - 高對比度主題;

 

  • 因此咱們能夠經過本身寫樣式來適配不一樣主題:
/* 淺色主題 */
.body.vscode-light {
    background: white;
    color: black;
}
/* 深色主題 */
body.vscode-dark {
    background: #252526;
    color: white;
}
/* 高對比度主題 */
body.vscode-high-contrast {
    background: white;
    color: red;
}

 

深色主題效果:

W1404xH770

3.6. 生命週期

webview由建立它的擴展程序全部,返回的panel對象你必須本身保存,若是你的擴展程序丟失了這個引用,那麼將沒法再次從新訪問該webview,即便Web視圖繼續顯示在vscode中。

用戶也能夠隨時關閉webview面板。當用戶關閉webview面板時,webview自己將被銷燬,此時不能再使用panel引用,不然將會出現異常,能夠經過監聽onDidDispose事件在這裏面作一些銷燬操做。

能夠經過panel.dispose()方法主動關閉webview。

3.7. 狀態保持

當webview移動到後臺又再次顯示時,webview中的任何狀態都將丟失。

解決此問題的最佳方法是使你的webview無狀態,經過消息傳遞來保存webview的狀態。

3.7.1. state

在webview的js中咱們可使用vscode.getState()vscode.setState()方法來保存和恢復JSON可序列化狀態對象。當webview被隱藏時,即便webview內容自己被破壞,這些狀態仍然會保存。固然了,當webview被銷燬時,狀態將被銷燬。

3.7.2. 序列化

經過註冊WebviewPanelSerializer能夠實如今VScode重啓後自動恢復你的webview,固然,序列化其實也是創建在getStatesetState之上的。

註冊方法:vscode.window.registerWebviewPanelSerializer

3.7.3. retainContextWhenHidden

對於具備很是複雜的UI或狀態且沒法快速保存和恢復的webview,咱們能夠直接使用retainContextWhenHidden選項。設置retainContextWhenHidden: true後即便webview被隱藏到後臺其狀態也不會丟失。

儘管retainContextWhenHidden頗有吸引力,但它須要很高的內存開銷,通常建議在實在沒辦法的時候才啓用。
getStatesetState是持久化的首選方式,由於它們的性能開銷要比retainContextWhenHidden低得多。

調試

若是配置了快捷鍵,那麼能夠直接在插件頁面按command+shift+p,便可調出控制檯,以下圖所示

 

 

注意,要調試Webview不能直接把VSCode的開發者工具打開,直接打開就會和咱們最前面的截圖看到的那樣,你只能看到一個<webview></webview>標籤,看不到代碼,要看代碼須要按下Ctrl+Shift+P而後執行打開Webview開發工具,英文版應該是Open Webview Developer Tools

W906xH526

審查Webview:

W1152xH1086

這個時候須要特別注意錯誤日誌出現的位置,若是是Webview的錯誤,通常打印在前面說的這個開發者工具,但若是是插件端的錯誤只會打印在整個VSCode的開發者工具裏。

相關文章
相關標籤/搜索