解鎖 VS Code 更多可能性,輕鬆入門 WebView

削微寒發表於2021-09-01

作者:HelloGitHub-小夏

說起 VS Code 大家普遍印象應該都差不多是這樣:不就是個編輯器嘛,最主要的還是 coding 的快感咯。

裡面很多功能都應該是圍繞如何提高 coding 效率、減少 coding 出錯率、解放 coder 小哥哥小姐姐的勞動力等等,至於程式碼以外的東西比如預覽什麼的,就交給瀏覽器咯。

所以可能很少有人會把 VS Code 和 WebView 聯想到一起。

一、隨處可見的 WebView

但是我相信,你一定在很多“有名”的 VS Code 外掛中接觸過它(WebView)的身影。比如可以在 VS Code 中畫流程圖的 vscode-drawio:

GitHub 地址:https://github.com/hediet/vscode-drawio

上班摸魚的同時還要繼續提升自我來刷題的 vscode-leetcode:

GitHub 地址:https://github.com/LeetCode-OpenSource/vscode-leetcode

還有上班摸魚的同時還要關心能否從一顆“小韭菜”實現財富自由的「韭菜盒子」 leek-fund:

GitHub 地址:https://github.com/LeekHub/leek-fund

所以你可以看到,有了 WebView 來擴充能力,外掛市場才會變得“百花齊放”,能滿足各類人各類摸魚的需求。但是上面開源專案的成功,也不僅僅靠的是我們本文介紹的簡單的 WebView 的能力,如果你對上面幾個開源專案有深挖的興趣,可以直接 clone 程式碼,一瞅到底,說不定下一個厲害的開源 VS Code 外掛就是出自你手啦。

二、WebView 到底是什麼

前面 有提過 VS Code 允許我們在它給的規則之下可以自定義很多功能,但是檢視這一塊,其實我們自定義的範圍非常小,這就限制了程式設計師們天馬行空的創造力。但是自由的靈魂不會被眼前的困難打敗,同行之間的心心相惜所以有了 WebView 的誕生。

當然這都是小編自己內心 OS 的,不過可以確定的是 WebView API 的存在允許在 VS Code 中擴充套件建立完全可自定義的檢視。例如:內建的 Markdown 擴充套件使用 webviews 來呈現 Markdown 預覽。Webviews 還可用於構建超出 VS Code 的本機 API 支援的複雜使用者介面。

你也可以簡單的把 WebView 理解為 VS Code 內部的 iframe。WebView 可以在這個框架中渲染幾乎所有的 HTML 內容,還可以使用訊息傳遞與擴充套件進行通訊。這種自由使得 webviews 非常強大,而且也擁有了一個全新的擴充套件範圍。

三、建立一個簡單的 WebView

從第一點的例子你就應該可以體會到 WebView 的功能擴充有多強大,它不僅可以作為自定義編輯器的檢視來擴充套件提供自定義 UI 以編輯工作區中的任何檔案。還允許在側邊欄或皮膚區域的 WebView 中繼續呈現 WebView 檢視等等。

如果你感興趣,可以去官網繼續學習。今天我們下文談的主要還是最簡單的一種方式:在編輯器中建立一個簡單的 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', () => {
    // 建立和展示一個 webview
    const panel = vscode.window.createWebviewPanel(
      'hgWebview', // 定義 webview 的型別,用於內部
      'HelloGitHub webview', // 給使用者展示的標題
      vscode.ViewColumn.One, // 在第幾欄編輯器裡展示這個 webview
      {} // 其他 Webview 配置.
    );
  });

	context.subscriptions.push(webviewCommand); // 這裡可以放多個,用,分隔即可
}

配置完之後看一眼效果,讓我們執行起來我們的外掛:

你可以看到這個標題就是我們上面在 package.json 上配置的“HelloGitHub webview”,或許有同學會對 ViewColumn 這個配置疑惑。

那我們來看一下這裡到底都有些什麼值:

看不懂?沒關係,我們實操一下,修改上面在 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、初始化內容

現在我們就要切入最重要的部分啦,如何豐富 WebView 的內容呢?其實也很簡單啦,把它看做一個 iframe 就好啦,那無非就是 HTML 的那些東西唄?so easy!

首先我們要有一個包含整個 HTML 內容的獨立檔案,為了好區分,我把它放在了這裡:

配置了一個非常簡單的網頁內容,裡面只有一個圖片:

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 可能會導致執行不成功,所以在進行復雜操作的時候一定要小心除錯,多看控制欄哦。

3、更新內容

是的,我們現在要從編輯器對這個 WebView 做更新操作了!比如我們給這個 WebView 加一行文字,然後在編輯器裡面加一個定時器,動態的去修改它。首先,修改我們的 html 檔案,它不在是一個靜態的文字了,他要動起來就得接收一個變數,所以改成函式咯:

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>
  `
}

其次呢,我們要跟這個函式有互動,並將要展示的值傳進去,並且這個值還是定時 1s 要進行修改的,所以就變成這樣啦:

const hgWebviewFun = require('./webview/hello-github');

// 設定我們的文案
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]);
		};

		// 設定初始化的內容
		updateWebview();

		// 設定一個簡單的定時器,讓他一秒內執行一次
		setInterval(updateWebview, 1000);
	});
...

看一下我們的效果,是不是就變成一個動感十足的網頁啦:

但是效果是實現了,你有沒有發現我們實現的方法非常的“暴力”,是直接替換了整個 html 的內容,類似於重新載入 iframe。所以要是換到複雜的頁面,效能肯定是個非常嚴重的問題,就會導致非常多令人頭大的效能問題。而且當使用者關閉 WebView 皮膚時,WebView 本身是會被銷燬的。如果嘗試使用銷燬的 WebView 會引發異常,比如我們上面的 setInterval 會繼續觸發並更新 panel.webview.html

所以我們要避免這種情況出現:

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(
    () => {
      // 當關閉 webview 的時候去掉對 webview 有後續更新的操作
      clearInterval(interval);
    },
    null,
    context.subscriptions
  );
});

4、訊息傳遞

前面說過,你可以簡單的把 WebView 理解成 iframe,那這也意味著它們都可以執行指令碼。不過預設情況下 WebView 中禁用 JavaScript,你可以通過傳入 enableScripts: true 來啟用。不過官網建議 WebView 應始終使用內容安全策略禁用內聯指令碼,所以我們這裡就不做展開。但是這一點也不影響我們發揮 WebView 的巨大作用——訊息傳遞。

WebView 除錯

在訊息傳遞內容之前,我覺得有必要說一下這個除錯工具命令 Developer: Toggle Developer Tools。你可以通過 comand+p(MacOS)喚起這個開發者除錯命令,可以幫你在除錯 WebView 的時候“如魚得水”,輕鬆捕獲異常並 fix

當然你還可以在 Elements 裡面檢視 dom 的結構,簡直就是太熟悉了~

WebView 接收訊息

首先我們先來了解一下如何從我們的外掛應用向我們的 webview 傳遞訊息。聰明的你一定猜到了對不對?沒錯就是 postMessage

修改我們的註冊命令如下:

  • createWebviewPanel 的變數存到一個新的變數上去

  • 新增了一個用於訊息傳遞的命令 webview.doRefactor

  • 同時因為在 HTML 內部需要監聽 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 指令碼許可權
			}
		);

		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 傳送訊息
		// 你可以傳送任何 JSON 序列化的資料
		currentPanel.webview.postMessage({ command: 'refactor', msg: '請多關注我們~' });
	})
  
  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"
      }
...

有了訊息的傳送,當然也需要有訊息的接收啦!這才能完成通訊嘛~所以我們要修改我們的 HTML 檔案,加一個用於接收訊息的監聽:

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');

        // 在這裡監聽訊息的傳送
        window.addEventListener('message', event => {

            const message = event.data; // 我們外掛傳送的資料
            console.log(message) // 列印一下看看是什麼樣子

            switch (message.command) {
                case 'refactor':
                    box.textContent = message.msg;
                    break;
            }
        });
      </script>
    </body>
    </html>
  `
}

上面的夠簡單吧,我們來看一下效果,記得開啟開發者除錯工具,首先是用 webview.start 命令開啟 WebView:

執行 webview.doRefactor 之後,我們就把我們的值傳到了 WebView 裡去啦:

WebView 傳送訊息

WebView 還可以將訊息傳遞迴我們的擴充套件程式。

這主要是通過使用 WebView 的 postMessage 內特殊的 VS Code API 物件上的函式來完成的。要訪問 VS Code API 物件,需要在 WebView 內部呼叫 acquireVsCodeApi 這個函式每個會話只能呼叫一次。

而且必須保留此方法返回的 VS Code API 例項,並將其分發給任何其他需要使用它的函式。

我們可以使用 VS Code API 的 postMessage 方法在我們的外掛中顯示來自 WebView 的訊息:

const vscode = acquireVsCodeApi(); // 直接使用

vscode.postMessage({ // 傳送訊息
  command: 'alert',
  text: '? 傳送成功~感謝老鐵~'
})

我們把這個事件觸發綁在了一個新的 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">點我傳送?!</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: '? 傳送成功~感謝老鐵~'
          })
        })


      </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
		);
	});
...

接下來我們先看一下點選按鈕前的樣式:

來看一下我們點選按鈕會發生什麼“神奇”的事情呢?

四、總結

那快樂的時光總是短暫的,又到了文章結束的時候啦。總的來說 WebView 就像是在 VS Code 裡的 iframe,雖然可能在效能上有那麼點弊端,但是卻能夠幫助我們實現很多豐富而又有趣的事情。

因此我們更要好好的利用這個功能,把它的力量發揮到極致。根據官網的描述,我們也要在使用的時候多注意以下幾點:

  • WebView 應該具有它所需的最少功能集。例如:如果不需要執行指令碼,則不要設定 enableScripts: true

  • WebView 嚴格遵從 內容安全策略,所以在 WebView 中可載入和執行的內容都有一定的限制。例如:內容安全策略可以確保僅允許在 WebView 中執行的指令碼列表,甚至告訴 WebView 只能載入 https 影像。

  • 出於安全考慮 WebView 預設無法直接訪問本地資源,它在一個孤立的上下文中執行,想要載入本地圖片、js、css 等必須通過特殊的 vscode-resource: 協議,網頁裡面所有的靜態資源都要轉換成這種格式,否則無法被正常載入。

  • 就像普通網頁都要求的那樣,在為 WebView 構建 HTML 時,必須清理所有使用者輸入。未能正確清理輸入可能會導致內容注入,這可能會使你的使用者面臨安全風險。比如:檔案內容、檔案和資料夾路徑、使用者和工作區設定

  • WebView 有自己的生命週期,如果在有極致體驗的場景下發揮他的最大作用,建議去官網更加深入的學習一下

最後的最後,預告一下下一篇「VS Code」系列文章,也就是本入門系列最後一篇文章將會帶大家體驗更綜合性的東西,給小編多一點點時間努力研究一下,期待我們下次再見咯!

相關文章