VSCode WebView插件(擴展)開發實戰

VSCode是微軟出的一款輕量級代碼編輯器,免費並且功能強大,以功能強大、提示友好、不錯的性能和顏值俘獲了大量開發者的青睞,對JavaScript和NodeJS的支持很是好,自帶不少功能,例如代碼格式化,代碼智能提示補全、Emmet插件等。
css

它是經過 Electron 實現跨平臺的,而 Electron 則是基於 Chromium 和 Node.js,好比 VS Code 的界面,就是經過 Chromium 進行渲染的。同時, VS Code 是多進程架構,當 VS Code 第一次被啓動時會建立一個主進程(main process),而後每一個窗口,都會建立一個渲染進程( Renderer Process)。與此同時,VS Code 會爲每一個窗口建立一個進程專門來執行插件,也就是 Extension Host。除了這三個主要的進程之外,還有兩種特殊的進程。第一種是調試進程,VS Code 爲調試器專門建立了Debug Adapter 進程,渲染進程會經過 VS Code Debug Protocol 跟 Debug Adapter 進程通信。html

架構圖以下:前端

不過此次分享咱們不過多的探討它的架構,主要看下插件(或者稱爲擴展,下同)怎麼寫。node

VSCode插件分爲哪些類型

vscode 插件開發的腳手架(執行yo code)咱們能夠看到有以下選項:ios

  • New Extension (TypeScript)
  • New Extension (JavaScript)
  • New Color Theme
  • New Language Support
  • New Code Snippets
  • New Keymap
  • New Extension Pack

經過cli咱們能夠直接建立擴展、主題、語言支持、代碼片斷、快捷鍵等插件項目,這些插件項目建立後開箱直用,按F5運行便可。git

VSCode插件能作些什麼?

  • 不受限的本地磁盤訪問
  • 自定義命令、快捷鍵、菜單
    • 資源管理器右鍵菜單
    • 編輯器右鍵菜單
    • 標題菜單
    • 下拉菜單
    • 右上角圖標
  • 自定義跳轉
  • 自動補全
  • 懸浮提示
  • 自定義設置
  • 自定義歡迎頁
  • 自定義webview(好比markdown preview
  • 自定義左側功能面板(好比git
  • 自定義顏色主題、圖標主題
  • 新增語言支持(Java.NetPythonDartGo……)

……等等等等web

VSCode極其優秀的擴展架構給咱們提供了很是大的施展拳腳的空間。ajax

好比,你在項目中對反覆執行某項繁雜操做很不爽,那麼你是時候作一個插件解放你的雙手了!!!chrome

能夠參考下面這個博客,博主對主流插件功能(包括自定義跳轉、自動補全、懸浮提示)作了很是全面的介紹typescript

VSCode插件開發全套攻略

如何實現一個webview插件

我今天主要講一下,本身是如何實踐webview插件的。對於前端而言,作一些能看獲得的漂亮東西,老是更具備吸引力,因此我主要關注了webview這塊。先貼個成品圖:

首先,安裝vscode cli,

npm install -g yo generator-code
複製代碼

再用cli建立一個New Extension (TypeScript)項目

yo code
複製代碼

它會幫咱們初始化好以下幾塊內容 :

  • tsconfig.json
  • package.json
  • extension.ts
  • .vscode目錄下的包括一鍵調試在內的配置項

咱們暫時不太須要關心tsconfig.json文件,由於是開箱即用的,除非咱們須要用到一些typescript的獨特特性。

先來看看package.json裏都有什麼:

{
    // 插件的激活事件
    "activationEvents": [
        "onCommand:extension.sayHello"
    ],
    // 入口文件
    "main": "./src/extension",
    "engines": {
        "vscode": "^1.27.0"
    },
    // 貢獻點,vscode插件大部分功能配置都在這裏
    "contributes": {
        "commands": [
            {
                "command": "extension.sayHello",
                "title": "Hello World"
            }
        ]
    }
}
複製代碼
  • activationEvents擴展激活事件,屬性值是個數組,包含一系列事件(除了onCommand以外還有onViewonUrionLanguage等等)。由於VSCode爲了性能考慮,並不會一打開就加載全部的插件。只有當用戶行爲觸發了該數組中包含的事件(好比執行命令或所打開文件的語言是json)時,纔會激活插件(也能夠配成"*",就會立刻加載,可是不建議這樣作);
  • main定義了整個插件的入口點;
  • engines插件最低支持的VSCode版本
  • contributes定義了插件全部的貢獻點,好比commands(命令)、menus(菜單)、configuration(配置項)、keybindings(快捷鍵綁定)、snippets(代碼片斷)、views(側邊欄內view的實現)、iconThemes(圖標主題)等等。

咱們要配一個右上角的菜單,直接貼配置:

"contributes": {
  "commands": [{
    "command": "extension.colaMovie",
    "title": "Cola Movie",
    "icon": {
      "light": "./images/film-light.svg",
      "dark": "./images/film-dark.svg"
    }
  }],
  "menus": {
    "editor/title": [{
      "when": "isWindows || isMac",
      "command": "extension.colaMovie",
      "group": "navigation"
    }]
  }
}
複製代碼

解釋:定義一個extension.colaMovie命令,順便配置titleicon

爲了一處命令配置多處使用,titleicon項放置在commands中了。此外,icon支持lightdark明暗兩類主題。若是不配置icon,則會顯示文字標題。

定義一個menus菜單,類型爲editor/title,表明右上角圖標。

  • when 配置了該菜單出現的場景(條件),除了isWindowsisMac還有很是多條件可使用
  • command 指定點擊該菜單會觸發什麼命令(commands中的命令)
  • group 指定菜單分組,主要用於編輯器右鍵菜單

而後咱們再回過頭來看一下main入口extension.ts文件:

const vscode = require('vscode');

/** * 插件被激活時觸發,全部代碼總入口 * @param {*} context 插件上下文 */
exports.activate = function(context: vscode.ExtensionContext) {
    console.log('恭喜,您的擴展「vscode-plugin-demo」已被激活!');
    // 註冊命令
    context.subscriptions.push(vscode.commands.registerCommand('extension.colaMovie', function () {
        vscode.window.showInformationMessage('Hello World!');
    }));
};

/** * 插件被釋放時觸發 */
exports.deactivate = function() {
    console.log('您的擴展「vscode-plugin-demo」已被釋放!')
};
複製代碼

該入口文件導出了兩個生命週期方法activatedeactivate

咱們回憶一下以前的activationEvents屬性,當裏面相應的事件觸發了插件時,activate方法會被喚起,當插件被銷燬時,deactivate會被調用。

而後,咱們必須在activate註冊一個命令 extension.colaMovie

context.subscriptions.push(vscode.commands.registerCommand('extension.colaMovie', async () => { 
  vscode.window.showInformationMessage('Hello World!');  
}));
複製代碼

注意,全部註冊的對象(不管是命令仍是語言vscode.languages.registerDefinitionProvider或是其它)都必需要將結果放入context.subscriptions中去,這是爲了方便deactivateVSCode幫你自動註銷它們。

此時,咱們按F5調試以後,已經能夠看到右上角出現Cola Movie的小圖標了,當咱們點擊它的時候會在右下角彈出Hello World!的提示信息。

讓咱們來完善一下點擊事件,試着建立一個webview看看:

panel = vscode.window.createWebviewPanel(
  "movie",
  "Cola Movie",
  vscode.ViewColumn.One,
  {
    enableScripts: true,
    retainContextWhenHidden: true,
  }
);
複製代碼
  • enableScripts表明容許js腳本執行
  • retainContextWhenHidden表明當頁籤切換離開時保持插件上下文不銷燬

VSCode爲了性能考慮,非當前頁籤都會銷燬上下文,直到切換回來再重建上下文。因此提供了setStategetState兩個方法供webview使用以即時保存與恢復上下文。

此時,webview已經建立並打開,可是卻一片空白。

這時咱們須要給panel.webview.html設置html內容,可是:

出於安全考慮,Webview默認沒法直接訪問本地資源,它在一個孤立的上下文中運行,想要加載本地圖片、jscss等必須經過特殊的vscode-resource:協議,網頁裏面全部的靜態資源都要轉換成這種格式,不然沒法被正常加載。
vscode-resource:協議相似於file:協議,但它只容許訪問特定的本地文件。和file:同樣,vscode-resource:從磁盤加載絕對路徑的資源。

找了一段替換html引用資源協議的函數,以下所示:

function getWebViewContent(context: vscode.ExtensionContext, templatePath: string) {
  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;
}
複製代碼

我以前寫過一個electron版本的Cola Movie,此時,我想將它移植進來試下水,看下webview插件能作到什麼程度。

我先把那邊的dist目錄拷貝過來加載index.html

const html = getWebViewContent(context, 'dist/index.html');
panel.webview.html = html;
複製代碼

一經調試就發現,這裏面有一個巨大的坑:

webview內部不容許發送ajax請求,全部ajax請求都是跨域的,由於webview自己是沒有host

我以前那邊作electron開發時碰到過跨域問題,經過簡單的electron配置webSecurity: false就能夠開放跨域權限:

let winProps = {
  title: '******',
  width: 1200,
  height: 800,
  backgroundColor: '#0D4966',
  autoHideMenuBar: true,
  webPreferences: {
    webSecurity: false,
    nodeIntegration: true
  }
};
複製代碼

但是VSCode並不會讓咱們接觸electron配置,因此我想這條路是堵死了。

那怎麼發送ajax請求把數據取到手呢?

我在extension.ts裏試了下axios是能夠發送請求並取到數據的,這裏就引出咱們接下來要講的一個重頭戲了:

消息通訊

webview和普通網頁同樣,並不能直接調用任何VSCode API。可是,它惟一特別之處就在於多了一個名叫acquireVsCodeApi的方法,執行這個方法會返回一個簡易版的vscode對象,具備以下三個方法:

  • getState()
  • postMessage(msg)
  • setState(newState)

這樣的話,咱們能夠發消息讓extension去幫咱們發送http請求!

消息通訊方式以下:

// 插件發送消息給webview
panel.webview.postMessage(message);

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

// webview發送消息給插件
const vscode = acquireVsCodeApi();
vscode.postMessage(message);

// 插件端接收消息
panel.webview.onDidReceiveMessage(message => {
    console.log('插件收到的消息:', message);
}, undefined, context.subscriptions);
複製代碼

寫過electron程序的同窗確定知道,這同electronipcMain/ipcRenderer還有websocketsend/onmessage同樣,兩端互調接口是獨立的,寫出來略有些不是很好看……

cs-channel 跨端通訊庫

因而我又封裝了一個跨端通訊庫cs-channel,並開源出去了,你們能夠看一下使用方式。

extension端代碼

const channel = new Channel({
  receiver: callback => {
    panel.webview.onDidReceiveMessage((message: IMessage) => {
      message.api && callback(message);
    }, undefined, context.subscriptions);
  },
  sender: message => void panel.webview.postMessage(message)
});
channel.on('http-get', async param => {
  return await Q(http.get(param.url, { params: param.params }));
});
複製代碼

上面,插件端就完成了一個http-get的接口定義

webview端代碼

const vscode = acquireVsCodeApi();
const channel = new Channel({
  sender: message => void vscode.postMessage(message),
  receiver: callback => {
    window.addEventListener('message', (event: { data: any }) => {
      event && event.data && callback(event.data);
  });
  }
});
const result = await channel.call('http-get', { url, ...data });
複製代碼

上面,webview端就完成了一次http-get接口的調用,並直接拿到了插件端的http調用結果!

Channel對象,一個項目實例化兩個(webview + extension)就足夠了,不用常常實例化。
如果一個項目有多個通訊方式,好比websocket + web worker + iframe父子通訊,就實例化各自的Channel對象便可。

DLNA 投屏功能遷移

以前electron版本的Cola Movie具有DLNA投屏功能,我覺着在VSCode的插件裏既然能全量使用nodejs api,應該也能投屏纔對?

我寫了段測試代碼

import * as Browser from 'nodecast-js';
// 是的,你沒看錯,藉助於nodecast-js庫nodejs使用dlna就是這麼簡單
const browser = new Browser();
browser.onDevice(function () {
  console.log(browser.getList());
});
browser.start();
複製代碼

確實打印了局域網內全部的可投屏設備~

那事情就簡單多了,利用剛剛和ajax一樣的原理讓extension幫忙拿設備列表,並幫忙推送投屏請求便可。
直接貼代碼:

extension端代碼

const DLNA = {
  browser: null,
  start: (): Promise<any[]> => {
    if (DLNA.browser !== null) {
      DLNA.stop();
    }
    return new Promise(resolve => {
      DLNA.browser = new Browser();
      DLNA.browser.onDevice(function () {
        resolve(DLNA.browser.getList());
      });
      setTimeout(() => {
        resolve([]);
      }, 8000);
      DLNA.browser.start();
    });
  },
  stop: () => {
    DLNA.browser && DLNA.browser.destroy();
    DLNA.browser = null;
  }
};

channel.on('dlna-request', async param => {
  const devices = await DLNA.start();
  localDevices = devices;
  return devices;
});

channel.on('dlna-destroy', async param => {
  DLNA.stop();
});

channel.on('dlna-play', async param => {
  localDevices.find(device => device.host === param.host).play(param.url, 60);
});
複製代碼

定義三個接口:

  • dlna-request獲取設備列表
  • dlna-play投屏視頻播放地址url到某設備
  • dlna-destroy銷燬browser對象

webview端代碼

const DLNA = {
  start: async () => await chanel.call<IDevice[]>('dlna-request'),
  play: (device: IDevice, url: string) => channel.call('dlna-play', { host: device.host, url }),
  stop: () => channel.call('dlna-destroy');
}
複製代碼

F5,調試,投屏成功!

PS:其實還有一個遺憾,就是VSCode自己在打包electron的時候移除了ffmpeg,致使webview里根本沒法使用audiovideo標籤,因此播放功能是作不了了。並且cookielocalStorage等接口一概沒法訪問。因此播放功能我就直接作成打開瀏覽器播放了。只不過chrome要實現m3u8源的播放須要安裝一個插件:Play HLS M3u8

相關文章
相關標籤/搜索