作者:HelloGitHub-小夏
說起 VS Code 大家普遍印象應該都差不多是這樣:不就是個編輯器嘛,最主要的還是 coding 的快感咯。
里面很多功能都應該是圍繞如何提高 coding 效率、減少 coding 出錯率、解放 coder 小哥哥小姐姐的勞動力等等,至于代碼以外的東西比如預覽什么的,就交給瀏覽器咯。
所以可能很少有人會把 VS Code 和 WebView 聯(lián)想到一起。
一、隨處可見的 WebView
但是我相信,你一定在很多“有名”的 VS Code 插件中接觸過它(WebView)的身影。比如可以在 VS Code 中畫流程圖的 vscode-drawio:
上班摸魚的同時還要繼續(xù)提升自我來刷題的 vscode-leetcode:
還有上班摸魚的同時還要關(guān)心能否從一顆“小韭菜”實現(xiàn)財富自由的「韭菜盒子」 leek-fund:
所以你可以看到,有了 WebView 來拓展能力,插件市場才會變得“百花齊放”,能滿足各類人各類摸魚的需求。但是上面項目的成功,也不僅僅靠的是我們本文介紹的簡單的 WebView 的能力,如果你對上面幾個項目有深挖的興趣,可以直接 clone 代碼,一瞅到底,說不定下一個厲害的插件就是出自你手啦。
二、WebView 到底是什么
前面有提過 VS Code 允許我們在它給的規(guī)則之下可以自定義很多功能,但是視圖這一塊,其實我們自定義的范圍非常小,這就限制了程序員們天馬行空的創(chuàng)造力。但是自由的靈魂不會被眼前的困難打敗,同行之間的心心相惜所以有了 WebView 的誕生。
當然這都是小編自己內(nèi)心 OS 的,不過可以確定的是 WebView API 的存在允許在 VS Code 中擴展創(chuàng)建完全可自定義的視圖。例如:內(nèi)置的 Markdown 擴展使用 webviews 來呈現(xiàn) Markdown 預覽。Webviews 還可用于構(gòu)建超出 VS Code 的本機 API 支持的復雜用戶界面。
你也可以簡單的把 WebView 理解為 VS Code 內(nèi)部的 iframe。WebView 可以在這個框架中渲染幾乎所有的 html 內(nèi)容,還可以使用消息傳遞與擴展進行通信。這種自由使得 webviews 非常強大,而且也擁有了一個全新的擴展范圍。
三、創(chuàng)建一個簡單的 WebView
從第一點的例子你就應該可以體會到 WebView 的功能拓展有多強大,它不僅可以作為自定義編輯器的視圖來擴展提供自定義 UI 以編輯工作區(qū)中的任何文件。還允許在側(cè)邊欄或面板區(qū)域的 WebView 中繼續(xù)呈現(xiàn) WebView 視圖等等。
如果你感興趣,可以去官網(wǎng)繼續(xù)學習。今天我們下文談的主要還是最簡單的一種方式:在編輯器中創(chuàng)建一個簡單的 WebView 面板。
1、配置命令
第一步首先肯定是配置命令啦,我們再次打開package.json文件,新配置一個command:
"contributes": {
"commands": [
..., // 省略其他命令
{
"command": "webview.start",
"title": "open a webview page",
"category": "HelloGitHub webview"
}
],
... // 省略其他配置項
}
配置完之后要把這個新的命令在 extension.js 中注冊一下:
function activate(context) {
... // 省略其他命令注冊
const webviewCommand = vscode.commands.registerCommand('webview.start', () => {
// 創(chuàng)建和展示一個 webview
const panel = vscode.window.createWebviewPanel(
'hgWebview', // 定義 webview 的類型,用于內(nèi)部
'HelloGitHub webview', // 給用戶展示的標題
vscode.ViewColumn.One, // 在第幾欄編輯器里展示這個 webview
{} // 其他 Webview 配置.
);
});
context.subscriptions.push(webviewCommand); // 這里可以放多個,用,分隔即可
}
配置完之后看一眼效果,讓我們運行起來我們的插件:
你可以看到這個標題就是我們上面在 package.json 上配置的“HelloGitHub webview”,或許有同學會對 ViewColumn 這個配置疑惑。
那我們來看一下這里到底都有些什么值:
看不懂?沒關(guān)系,我們實操一下,修改上面在 extension.js 里的配置如下:
const webviewCommand = vscode.commands.registerCommand('webview.start', () => {
const panel = vscode.window.createWebviewPanel(
'hgWebview',
'HelloGitHub webview',
vscode.ViewColumn.Two, // 從 One 改成 Two
{}
);
});
效果如下:
這里多了一個 js 的文件其實沒有什么意義,因為如果沒有這個文件占編輯器的第一個 ViewColumn 的話,其實效果和上面的配置是一樣的,有了這個文件之后,我們的 WebView 才會在第二欄打開。這些單詞是不是非常簡單易懂?
2、初始化內(nèi)容
現(xiàn)在我們就要切入最重要的部分啦,如何豐富 WebView 的內(nèi)容呢?其實也很簡單啦,把它看做一個 iframe 就好啦,那無非就是 HTML 的那些東西唄?so easy!
首先我們要有一個包含整個 HTML 內(nèi)容的獨立文件,為了好區(qū)分,我把它放在了這里:
配置了一個非常簡單的網(wǎng)頁內(nèi)容,里面只有一個圖片:
module.exports = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello GitHub</title>
</head>
<body>
<img src="https://cdn.jsdelivr.net/gh/521xueweihan/img_logo@main/logo/readme.gif" width="300" />
</body>
</html>
`
在 extension.js 中引入文件并配置到我們的 WebView:
const hgWebview = require('./webview/hello-github');
...
const webviewCommand = vscode.commands.registerCommand('webview.start', () => {
const panel = vscode.window.createWebviewPanel(
'hgWebview',
'HelloGitHub webview',
vscode.ViewColumn.One,
{}
);
panel.webview.html = hgWebview; // 對沒錯就是這里配置,非常簡單
});
...
看一下效果:
這里要提醒大家的是,你配置的應該始終是一個完整的 HTML 文檔。HTML 片段或格式錯誤的 HTML 可能會導致運行不成功,所以在進行復雜操作的時候一定要小心調(diào)試,多看控制欄哦。
3、更新內(nèi)容
是的,我們現(xiàn)在要從編輯器對這個 WebView 做更新操作了!比如我們給這個 WebView 加一行文字,然后在編輯器里面加一個定時器,動態(tài)的去修改它。首先,修改我們的 html 文件,它不在是一個靜態(tài)的文本了,他要動起來就得接收一個變量,所以改成函數(shù)咯:
module.exports = (txt) => {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello GitHub</title>
</head>
<body>
<img src="https://cdn.jsdelivr.net/gh/521xueweihan/img_logo@main/logo/readme.gif" width="300" />
<div>
${txt} // 注意這里是接收變量的寫法
</div>
</body>
</html>
`
}
其次呢,我們要跟這個函數(shù)有互動,并將要展示的值傳進去,并且這個值還是定時 1s 要進行修改的,所以就變成這樣啦:
const hgWebviewFun = require('./webview/hello-github');
// 設(shè)置我們的文案
const webviewTxt = {
'descripton': 'HelloGitHub 是一個熱愛開源項目的開源組織。',
'slogon': '我們雖然沒有錢,但是我們有夢想!'
};
...
const webviewCommand = vscode.commands.registerCommand('webview.start', () => {
const panel = vscode.window.createWebviewPanel(
'hgWebview',
'HelloGitHub webview',
vscode.ViewColumn.One,
{}
);
let iteration = 0;
const updateWebview = () => {
// 做一個簡單的判斷用于取值
const key = iteration++ % 2 ? 'descripton' : 'slogon';
panel.title = webviewTxt[key];
panel.webview.html = hgWebviewFun(webviewTxt[key]);
};
// 設(shè)置初始化的內(nèi)容
updateWebview();
// 設(shè)置一個簡單的定時器,讓他一秒內(nèi)執(zhí)行一次
setInterval(updateWebview, 1000);
});
...
看一下我們的效果,是不是就變成一個動感十足的網(wǎng)頁啦:
但是效果是實現(xiàn)了,你有沒有發(fā)現(xiàn)我們實現(xiàn)的方法非常的“暴力”,是直接替換了整個 html 的內(nèi)容,類似于重新加載 iframe。所以要是換到復雜的頁面,性能肯定是個非常嚴重的問題,就會導致非常多令人頭大的性能問題。而且當用戶關(guān)閉 WebView 面板時,WebView 本身是會被銷毀的。如果嘗試使用銷毀的 WebView 會引發(fā)異常,比如我們上面的 setInterval 會繼續(xù)觸發(fā)并更新 panel.webview.html。
所以我們要避免這種情況出現(xiàn):
const webviewCommand = vscode.commands.registerCommand('webview.start', () => {
const panel = vscode.window.createWebviewPanel(
'hgWebview',
'HelloGitHub webview',
vscode.ViewColumn.One,
{}
);
let iteration = 0;
const updateWebview = () => {
const key = iteration++ % 2 ? 'descripton' : 'slogon';
panel.title = webviewTxt[key];
panel.webview.html = hgWebviewFun(webviewTxt[key]);
};
updateWebview();
const interval = setInterval(updateWebview, 1000);
panel.onDidDispose(
() => {
// 當關(guān)閉 webview 的時候去掉對 webview 有后續(xù)更新的操作
clearInterval(interval);
},
null,
context.subscriptions
);
});
4、消息傳遞
前面說過,你可以簡單的把 WebView 理解成 iframe,那這也意味著它們都可以運行腳本。不過默認情況下 WebView 中禁用 JAVAScript,你可以通過傳入 enableScripts: true 來啟用。不過官網(wǎng)建議 WebView 應始終使用內(nèi)容安全策略禁用內(nèi)聯(lián)腳本,所以我們這里就不做展開。但是這一點也不影響我們發(fā)揮 WebView 的巨大作用——消息傳遞。
WebView 調(diào)試
在消息傳遞內(nèi)容之前,我覺得有必要說一下這個調(diào)試工具命令 Developer: Toggle Developer Tools。你可以通過 comand+p(macOS)喚起這個開發(fā)者調(diào)試命令,可以幫你在調(diào)試 WebView 的時候“如魚得水”,輕松捕獲異常并 fix
當然你還可以在 Elements 里面查看 dom 的結(jié)構(gòu),簡直就是太熟悉了~
WebView 接收消息
首先我們先來了解一下如何從我們的插件應用向我們的 webview 傳遞消息。聰明的你一定猜到了對不對?沒錯就是 postMessage!
修改我們的注冊命令如下:
- 把 createWebviewPanel 的變量存到一個新的變量上去
- 新增了一個用于消息傳遞的命令 webview.doRefactor
- 同時因為在 HTML 內(nèi)部需要監(jiān)聽 message 的傳遞,所以我們必須確保開啟腳本,也就是上文說的 enableScripts:true
- 為了確保我們不眼花繚亂,這里也去掉了之前的定時器 setInterval
...
let currentPanel; // 重新定義一個變量用于多個命令之間的使用
const webviewCommand = vscode.commands.registerCommand('webview.start', () => {
currentPanel = vscode.window.createWebviewPanel(
'hgWebview',
'HelloGitHub webview',
vscode.ViewColumn.One,
{
enableScripts: true // 開啟 js 腳本權(quán)限
}
);
let iteration = 0;
const updateWebview = () => {
const key = iteration++ % 2 ? 'descripton' : 'slogon';
currentPanel.title = webviewTxt[key];
currentPanel.webview.html = hgWebviewFun(webviewTxt[key]);
};
updateWebview();
// const interval = setInterval(updateWebview, 1000); 去掉定時器
currentPanel.onDidDispose(
() => {
// clearInterval(interval); 去掉定時器
currentPanel = undefined; // 銷毀 webview 的時候釋放變量
},
null,
context.subscriptions
);
});
// 注冊一個新的命令
const webviewRefactorCommand = vscode.commands.registerCommand('webview.doRefactor', () => {
if (!currentPanel) {
return;
}
// 向 webview 發(fā)送消息
// 你可以發(fā)送任何 JSON 序列化的數(shù)據(jù)
currentPanel.webview.postMessage({ command: 'refactor', msg: '請多關(guān)注我們~' });
})
context.subscriptions.push(webviewCommand, webviewRefactorCommand);
...
為了防止有人在跟著敲的時候漏掉這一步,我決定還是再提醒一下~要在 package.json 里面加上新注冊的這個命令哦:
...
{
"command": "webview.start",
"title": "open a webview page",
"category": "HelloGitHub webview"
},
{
"command": "webview.doRefactor",
"title": "doRefactor a webview page",
"category": "HelloGitHub webview"
}
...
有了消息的發(fā)送,當然也需要有消息的接收啦!這才能完成通信嘛~所以我們要修改我們的 HTML 文件,加一個用于接收消息的監(jiān)聽:
module.exports = (txt) => {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello GitHub</title>
</head>
<body>
<img src="https://cdn.jsdelivr.net/gh/521xueweihan/img_logo@main/logo/readme.gif" width="300" />
<h1 id="message-show">hello</h1>
<div>
${txt}
</div>
<script>
const box = document.getElementById('message-show');
// 在這里監(jiān)聽消息的發(fā)送
window.addEventListener('message', event => {
const message = event.data; // 我們插件發(fā)送的數(shù)據(jù)
console.log(message) // 打印一下看看是什么樣子
switch (message.command) {
case 'refactor':
box.textContent = message.msg;
break;
}
});
</script>
</body>
</html>
`
}
上面的夠簡單吧,我們來看一下效果,記得打開開發(fā)者調(diào)試工具,首先是用 webview.start 命令打開 WebView:
運行 webview.doRefactor 之后,我們就把我們的值傳到了 WebView 里去啦:
WebView 發(fā)送消息
WebView 還可以將消息傳遞回我們的擴展程序。
這主要是通過使用 WebView 的 postMessage 內(nèi)特殊的 VS Code API 對象上的函數(shù)來完成的。要訪問 VS Code API 對象,需要在 WebView 內(nèi)部調(diào)用 acquireVsCodeApi 這個函數(shù)每個會話只能調(diào)用一次。
而且必須保留此方法返回的 VS Code API 實例,并將其分發(fā)給任何其他需要使用它的函數(shù)。
我們可以使用 VS Code API 的 postMessage 方法在我們的插件中顯示來自 WebView 的消息:
const vscode = acquireVsCodeApi(); // 直接使用
vscode.postMessage({ // 發(fā)送消息
command: 'alert',
text: ' 發(fā)送成功~感謝老鐵~'
})
我們把這個事件觸發(fā)綁在了一個新的 button 上,完整的代碼如下:
module.exports = (txt) => {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello GitHub</title>
</head>
<body>
<img src="https://cdn.jsdelivr.net/gh/521xueweihan/img_logo@main/logo/readme.gif" width="300" />
<h1 id="message-show">hello</h1>
<div>
${txt}
</div>
<button id="btn_submit">點我發(fā)送!</button>
<script>
const box = document.getElementById('message-show');
const vscode = acquireVsCodeApi();
window.addEventListener('message', event => {
const message = event.data;
console.log(message)
switch (message.command) {
case 'refactor':
box.textContent = message.msg;
break;
}
});
document.getElementById('btn_submit').addEventListener('click', function(){
vscode.postMessage({
command: 'alert',
text: ' 發(fā)送成功~感謝老鐵~'
})
})
</script>
</body>
</html>
`
}
同時也需要在我們的插件代碼里接收來自 WebView 的消息:
...
currentPanel.webview.onDidReceiveMessage(
message => {
switch (message.command) {
case 'alert':
vscode.window.showInformationMessage(message.text);
return;
}
},
undefined,
context.subscriptions
);
...
完整的代碼如下,在打開 WebView 的時候就要將事件綁定都搞定:
...
const webviewCommand = vscode.commands.registerCommand('webview.start', () => {
currentPanel = vscode.window.createWebviewPanel(
'hgWebview',
'HelloGitHub webview',
vscode.ViewColumn.One,
{
enableScripts: true
}
);
let iteration = 0;
const updateWebview = () => {
const key = iteration++ % 2 ? 'descripton' : 'slogon';
currentPanel.title = webviewTxt[key];
currentPanel.webview.html = hgWebviewFun(webviewTxt[key]);
};
updateWebview();
// const interval = setInterval(updateWebview, 1000);
currentPanel.onDidDispose(
() => {
// clearInterval(interval);
currentPanel = undefined;
},
null,
context.subscriptions
);
// 處理來自 webview 的消息
currentPanel.webview.onDidReceiveMessage(
message => {
switch (message.command) {
case 'alert':
vscode.window.showInformationMessage(message.text);
return;
}
},
undefined,
context.subscriptions
);
});
...
接下來我們先看一下點擊按鈕前的樣式:
來看一下我們點擊按鈕會發(fā)生什么“神奇”的事情呢?
四、總結(jié)
那快樂的時光總是短暫的,又到了文章結(jié)束的時候啦。總的來說 WebView 就像是在 VS Code 里的 iframe,雖然可能在性能上有那么點弊端,但是卻能夠幫助我們實現(xiàn)很多豐富而又有趣的事情。
因此我們更要好好的利用這個功能,把它的力量發(fā)揮到極致。根據(jù)官網(wǎng)的描述,我們也要在使用的時候多注意以下幾點:
- WebView 應該具有它所需的最少功能集。例如:如果不需要運行腳本,則不要設(shè)置 enableScripts: true
- WebView 嚴格遵從 內(nèi)容安全策略,所以在 WebView 中可加載和執(zhí)行的內(nèi)容都有一定的限制。例如:內(nèi)容安全策略可以確保僅允許在 WebView 中運行的腳本列表,甚至告訴 WebView 只能加載 https 圖像。
- 出于安全考慮 WebView 默認無法直接訪問本地資源,它在一個孤立的上下文中運行,想要加載本地圖片、js、css 等必須通過特殊的 vscode-resource: 協(xié)議,網(wǎng)頁里面所有的靜態(tài)資源都要轉(zhuǎn)換成這種格式,否則無法被正常加載。
- 就像普通網(wǎng)頁都要求的那樣,在為 WebView 構(gòu)建 HTML 時,必須清理所有用戶輸入。未能正確清理輸入可能會導致內(nèi)容注入,這可能會使你的用戶面臨安全風險。比如:文件內(nèi)容、文件和文件夾路徑、用戶和工作區(qū)設(shè)置
- WebView 有自己的生命周期,如果在有極致體驗的場景下發(fā)揮他的最大作用,建議去官網(wǎng)更加深入的學習一下
最后的最后,預告一下下一篇「VS Code」系列文章,也就是本入門系列最后一篇文章將會帶大家體驗更綜合性的東西,給小編多一點點時間努力研究一下,期待我們下次再見咯!