大家好,我是韋恩,今天是第十八天,我們會繼續了解如何組織Webview,並進一步整合當前流行的SPA前端框架。
在前面的單元裡,我們已經練習了如何使用Webview的API,並且也了解相關的API與載入本地檔案資源的規範。現在我們要將其運用到實戰中,讓我們進一步組織WebView的API,設計一個模組化的CustomWebViewPanel的class,讓WebView API更好使用與易於管理吧!
對於VSCode的WebPanel而言,我們一次只需要一個panel,重複產生panel物件,會造成資源浪費,因此在這種情境,非常適合使用單例模式來處理。
底下我們提供一個CreateOrShow方法,用以取得WebViewPanel物件。當已經存在創建過的webviewPanel物件時,就會直接呼叫WebPanel的reveal方法將已經創建的webview在當前editor group中的。若是先前未創建WebviePanel,就會直接創建並回傳一個WebviewPanel。
export class WebviewPanel {
private static _instance: WebviewPanel | null;
public static createOrShow(context: vscode.ExtensionContext) {
const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined;
if (WebviewPanel._instance) {
WebviewPanel._instance._panel.reveal(column);
} else {
WebviewPanel._instance = new WebviewPanel(context);
}
return WebviewPanel._instance;
}
public readonly viewType = 'SinglePageApplicationPanel';
private _panel: vscode.WebviewPanel;
private get webview() {
return this._panel.webview;
}
private constructor(private context: vscode.ExtensionContext) {
this._panel = this.createPanel(context);
this.webview.html = this.loadWebviewContent();
...
}
private createPanel(context: vscode.ExtensionContext) {
return vscode.window.createWebviewPanel(
this.viewType,
'CustomWebview',
vscode.ViewColumn.One,
{
enableScripts: true,
localResourceRoots: [
vscode.Uri.joinPath(context.extensionUri, 'out', 'build')
]
}
);
}
...
}
這裡我們直接使用比較傳統的Singleton模式,而不使用配合ES6模組直接export類別實例的方式。為什麼呢?Webview的物件會佔用耗資源,並且不是像OutputChannl任何時候都會使用到。因此我們不需要再extensiont初始運作時,就直接把WebViewPanel實例化並佔用系統資源,等到需要啟動WebviewPanel再創建物件即可。
另外,因為我們僅使用createOrShow方法來創建與獲取物件,上面我們就直接將constructor設為private,防止有人直接new出另一個類別的實例。
有了createOrShow方法後,我們可以在extension.ts裡這樣使用它
export function activate(context: vscode.ExtensionContext) {
let disposable = vscode.commands.registerCommand('day18-singleton-webview-spa.createOrShowWebview', () => {
const webviewPanel = WebviewPanel.createOrShow(context);
...
context.subscriptions.push(disposable);
});};
export function deactivate() {}
好,現在已經有了Webview的創建和呈現方法,讓我們進一步封裝原有的vscode.WebviewPanel裡面的方法,讓外部更好使用。
export class WebviewPanel {
...
private _panel: vscode.WebviewPanel;
private get webview() {
return this._panel.webview;
}
private _viewStateEmitter = new vscode.EventEmitter();
public get onDidChangeViewState() {
return this._viewStateEmitter.event;
}
private _messageEmitter = new vscode.EventEmitter();
public get onDidReceiveMessage() {
return this._messageEmitter.event;
}
private _disposables: vscode.Disposable[] = [];
private constructor(private context: vscode.ExtensionContext) {
this._panel = this.createPanel(context);
this.onLifeCycleChanges();
this.onWebViewMessage();
this.webview.html = this.loadWebviewContent();
this._disposables.push(this._panel);
}
...
private onLifeCycleChanges() {
this._panel.onDidChangeViewState(
e => {
this._viewStateEmitter.fire(e);
...
},
null,
this._disposables
);
this._disposables.push(this._viewStateEmitter);
this._panel.onDidDispose(() => {
this._disposables.forEach((d: vscode.Disposable) => d.dispose());
this._disposables = [];
WebviewPanel._instance = null;
}, null, this._disposables);
}
private onWebViewMessage() {
this._panel.webview.onDidReceiveMessage(
message => {
this._messageEmitter.fire(e);
...
},
null,
this._disposables
);
this._disposables.push(this._messageEmitter);
}
...
}
這裡我們就用之前實作Treeview的onDidTreeViewData同樣的方法,讓我們的WebviewPanel一樣可以被外部監聽資料改變的事件。同時我們也做好dispose資源的處理,並且監聽WebviewPanel傳來的訊息。
想當然爾,接下來我們就可以提供一個sendMessage方法提供給外部使用
export class WebviewPanel {
...
public sendMessage(action) {
this.webview.postMessage(action);
}
...
}
好啦,已經封裝了常用的方法,現在我們要實作最重要的loadWebviewContent方法了
我們先完成一個內部用的webviewUri簡化之後uri轉換的程式碼
private webviewUri(relativePath: string) {
const uri = vscode.Uri.parse(
path.join(this.context.extensionPath,'out','build', relativePath)
);
return this.webview.asWebviewUri(uri);
}
接下來我們就要在loadWebviewContent方法方面設計給spa使用的index.html樣板,底下我們會以react為例。
為什麼是react,而不選用其他流行的框架呢?
這裡選用react原因主要幾點:
生態系最豐富,其生態系的開源專案(如redux),對前端的各框架影響跟啓發性大
沒有預設立場,寫法自由,不會預設限制使用者使用何種方式(如物件導向、函數式程式設計)開發。
時間跟篇幅的關係,僅能選用一個SPA框架在這三十天裡的文章做示範。
另外,就筆者的觀點,如果對方會有「 因為你介紹XXX框架,所以你就是哪一派的人(或哪種人)」的想法,其實也只是對方心理有預設立場,或依此獲取名利、區分派系,才會計較些有的沒的。
不然,搞個技術為什麼要這麼複雜?有什麼跟私心無關的理由隨意幫別人貼標籤呢?
好的,以上是幾點筆者選用react當範例的理由。
鐵人賽的三十天以後,筆者規劃將在附錄介紹angular與vue3.0的整合教學,供選用其餘框架的讀者參考。
下面因為我們要載入react網站,會先進行react專案的配置。
這裡我們使用Create-React-APP快速產生React專案,此處創建react的教學亦可參考CRA的官方網站
首先,讓我們在vscode底下再另外開一個termainal,進入src/webview資料夾的路徑下,使用CRA創建一個名為webview的專案。
$ npx create-react-app --typescript <your-react-webview-app-name>
創建react的app以後,進入創建的react資料夾下,並使用react專案底下的npm script啟動react
cd <your-react-webview-app-name>
npm start
好的,如此我們就可以正常的開發react app,現在,我們需要使用production編譯後的react程式。
npm run build
現在我們直接serve這index.html,即可啟動react應用程式:
/**
* 如無安裝servce套件
*/
yarn global add serve
/**
* serve build資料夾下面的專案
*/
serve -s build
執行serve後,我們可以看到已經listen了localhost為5000的port
現在在瀏覽器下輸入http://localhost:5000 的url,即會顯示react程式。
現在我們已經知道正常情況下怎麼build完react程式了,但這並不能夠很好的與vscode的extension結合。還記得嗎?在第一天的章節,我們將要讀取的html檔案也複製到extension程式編譯完的資料夾out
。
現在我們一樣要在build完之後,將production使用的code放置於out資料夾下底下,因此讓我們在react專案下package.json的build script修改為
...
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build && mv build ../out/build || move build ../out/build",
...
},
讀者可以看到,我們使用了mv來搬運檔案,並於沒有mv指令的windows平台使用了move指令進行檔案搬運。如此一來,在react程式編譯完成後,build資料夾就會被自動搬運到out資料夾下底下。
接下來,讓我們查看一下主程式的進入點index.html吧
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<link rel="icon" href="/favicon.ico"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta name="theme-color" content="#000000"/>
<meta name="description" content="Web site created using create-react-app"/>
<link rel="apple-touch-icon" href="/logo192.png"/>
<link rel="manifest" href="/manifest.json"/>
<title>React App</title>
<link href="/static/css/main.5f361e03.chunk.css" rel="stylesheet"></head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script>...</script>
<script src="/static/js/2.390c405e.chunk.js"></script>
<script src="/static/js/main.0efac7b8.chunk.js"></script>
</body>
</html>
我們可以看到,除了一些logo圖像跟icon以外,script的src下的js與css檔案均加上了webpack隨機產生的hash編號。要動態將正確的路徑轉換為webviewUri,我們需要透過react-script編譯完後產生的一個asset-manifest.json檔案下面的對應表找到對應的javascript/css檔案路徑。
讓我們寫一個小方法讀取這個對應表檔案
function readJSON(path: string) {
const jsonString = fs.readFileSync(path, 'utf-8');
return JSON.parse(jsonString);
}
現在我們就可以實作我們提供給Webview的html樣板了
class WebViewPanel {
...
public loadWebviewContent() {
const mainfest = readJSON(path.join(this.context.extensionPath, 'out/build/asset-manifest.json');
const entrypointsJs = mainfest['entrypoints'].filter((p: string) => p.includes('static/js'));
return `<!DOCTYPE html>
<html lang="en">
<head>
<link rel="icon" href="${this.webviewUri('./favicon.ico')}"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta name="theme-color" content="#000000"/>
<meta name="description" content="Web site created using create-react-app"/>
<link rel="apple-touch-icon" href="${this.webviewUri('./logo192.png')}"/>
<link rel="manifest" href="${this.webviewUri('./manifest.json')}"/>
<title>React App</title>
<link rel="stylesheet" type="text/css" href="${this.webviewUri(mainfest.files['main.css'])}">
<base href="${this.webviewUri('/')}">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script src="${this.webviewUri(entrypointsJs[0])}"></script>
<script src="${this.webviewUri(entrypointsJs[1])}"></script>
<script src="${this.webviewUri(entrypointsJs[2])}"></script>
</body>
</html>`;
}
...
}
透過上面的方法,我們將manifest裡面的對應路徑轉換成VSCode的WebviewUri。
同時,為了讓所有我們在react專案底下的相對路徑都能夠生效,我們在index.html中加上base標籤,讓所有react專案下面的相對路徑都能對應到轉換後的webviewUri。
<base href="${this.webviewUri('/')}">
完成以上方法後,再啟動extension並執行創建Webview的命令,我們會看到Webview的react渲染出網站。
啟動Webview後我們發現,react的logo讀取不到圖檔資源。為什麼會這樣呢?讓我們打開webview的devtool檢視下這個image元素
讀者們可以看到,在react程式裡import的圖像的路徑預設會被Create React APP編譯成/static/media/...
的方式處理。
這種形式的Url是無法配合我們前面宣告的Base標籤運作的
<base href="${this.webviewUri('/')}">
為此,我們需要改變Create React APP使用Webpack編譯時的路徑,讓我們在Webview專案的資料夾下面加入一個.env
檔案,在檔案裡面設定一個PUBLIC_URL的環境變數
PUBLIC_URL=./
設定完環境變數檔案後,再讓我們再Webview專案下運行npm run build方法執行react-scripts編譯react檔案,並將檔案搬移到out資料夾。讓我們來看一下執行結果:
恭喜讀者,我們成功的整合react與專案裡的圖像資源到擴充套件裡了。
現在我們再來檢視下Webview Devtool下的image路徑,可以看到圖像路徑開頭已經成功地轉為./
的相對路徑了。
總算結束啦!
今天我們練習使用物件導向的方式組織WebView API,並進一步整合當前流行的SPA前端框架至WebView。
明天我們將開始extension的專案實戰,我們明天見,謝謝大家。