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
插件開發的腳手架(執行yo code
)咱們能夠看到有以下選項:ios
經過cli
咱們能夠直接建立擴展、主題、語言支持、代碼片斷、快捷鍵等插件項目,這些插件項目建立後開箱直用,按F5運行便可。git
webview
(好比markdown preview
)git
)Java
,.Net
,Python
,Dart
,Go
……)……等等等等web
VSCode
極其優秀的擴展架構給咱們提供了很是大的施展拳腳的空間。ajax
好比,你在項目中對反覆執行某項繁雜操做很不爽,那麼你是時候作一個插件解放你的雙手了!!!chrome
能夠參考下面這個博客,博主對主流插件功能(包括自定義跳轉、自動補全、懸浮提示)作了很是全面的介紹typescript
我今天主要講一下,本身是如何實踐webview
插件的。對於前端而言,作一些能看獲得的漂亮東西,老是更具備吸引力,因此我主要關注了webview
這塊。先貼個成品圖:
首先,安裝vscode cli,
npm install -g yo generator-code
複製代碼
再用cli
建立一個New Extension (TypeScript)
項目
yo code
複製代碼
它會幫咱們初始化好以下幾塊內容 :
咱們暫時不太須要關心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
以外還有onView
、onUri
、onLanguage
等等)。由於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
命令,順便配置title
和icon
。
爲了一處命令配置多處使用,
title
和icon
項放置在commands
中了。此外,icon
支持light
與dark
明暗兩類主題。若是不配置icon
,則會顯示文字標題。
定義一個menus
菜單,類型爲editor/title
,表明右上角圖標。
when
配置了該菜單出現的場景(條件),除了isWindows
與isMac
還有很是多條件可使用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」已被釋放!')
};
複製代碼
該入口文件導出了兩個生命週期方法activate
與deactivate
。
咱們回憶一下以前的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
中去,這是爲了方便deactivate
時VSCode
幫你自動註銷它們。
此時,咱們按F5調試以後,已經能夠看到右上角出現Cola Movie
的小圖標了,當咱們點擊它的時候會在右下角彈出Hello World!
的提示信息。
讓咱們來完善一下點擊事件,試着建立一個webview
看看:
panel = vscode.window.createWebviewPanel(
"movie",
"Cola Movie",
vscode.ViewColumn.One,
{
enableScripts: true,
retainContextWhenHidden: true,
}
);
複製代碼
enableScripts
表明容許js腳本執行retainContextWhenHidden
表明當頁籤切換離開時保持插件上下文不銷燬
VSCode
爲了性能考慮,非當前頁籤都會銷燬上下文,直到切換回來再重建上下文。因此提供了setState
與getState
兩個方法供webview
使用以即時保存與恢復上下文。
此時,webview
已經建立並打開,可是卻一片空白。
這時咱們須要給panel.webview.html
設置html
內容,可是:
出於安全考慮,
Webview
默認沒法直接訪問本地資源,它在一個孤立的上下文中運行,想要加載本地圖片、js
、css
等必須經過特殊的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
對象,具備以下三個方法:
這樣的話,咱們能夠發消息讓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
程序的同窗確定知道,這同electron
的ipcMain/ipcRenderer
還有websocket
的send/onmessage
同樣,兩端互調接口是獨立的,寫出來略有些不是很好看……
因而我又封裝了一個跨端通訊庫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
里根本沒法使用audio
與video
標籤,因此播放功能是作不了了。並且cookie
、localStorage
等接口一概沒法訪問。因此播放功能我就直接作成打開瀏覽器播放了。只不過chrome
要實現m3u8
源的播放須要安裝一個插件:Play HLS M3u8;