iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 27
0
Software Development

自己用的工具自己做! 30天玩轉VS Code Extension之旅系列 第 27

Day27 | 導入WebviewPanel

大家好,我是韋恩,今天是鐵人賽的二十六天,讓我們在專案的現有架構裡導入WebViewPanel元件吧!


CodeManager當前專案架構


在昨天,我們初步建立了View-ViewModel-Model的資料流架構。

現在我們的專案的src資料夾結構如下

./src
├── extension.ts
├── main
│   ├── common
│   │   └── control.ts
│   ├── data
│   │   └── snippets
│   │       ├── index.ts
│   │       └── snippetfile.ts
│   ├── model
│   │   ├── index.ts
│   │   └── snippet.ts
│   ├── view
│   │   └── treeview.ts
│   └── view-model
│       └── workspace
│           ├── index.ts
│           ├── initialize.ts
│           └── workspace.ts
└── test
    ├── runTest.ts
    └── suite
        ├── extension.test.ts
        └── index.ts

專案的資料夾根據不同功能的劃分為各個模組,分為以下幾類

  • common: 放置常用的vscode工具元件,如input、dropdown等等。
  • data: MVVM的model層,處理data-access的邏輯,如讀寫檔案、http request等等。
  • model: 定義專案的ts類型,view-model、model和其他模組會使用到的interface與各種type。
  • view: MVVM的View層,這裡會放置展示資料與互動的元件,如TreeView和WebviewPanel。
  • view-model: MVVM的ViewModel層,目前已經定義好的viewModel為儲存snippet用資料的workspace類別。

這些檔案模組會在extension.ts中主程式的進入點active function中初始化,並加以使用。

導入WebviewPanel與WebView


上面的樹狀圖讓我們對專案的個功能一目了然,現在,我們在view的資料夾新增一個webview-panel.ts,並實作WebViewPanel的類別。

import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';

type ObjectLike = { [key: string]: any };

interface Action<T extends ObjectLike> {
 type: string;
 payload: T
}

export class WebviewPanel {

 private static _instance: WebviewPanel | null;

 public static getInstance(context: vscode.ExtensionContext) {
  if (WebviewPanel._instance) {
   return WebviewPanel._instance;
  }
  return new WebviewPanel(context);
 }

 public static createOrShow(context: vscode.ExtensionContext) {
  const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined;
  if (WebviewPanel._instance) {
   WebviewPanel._instance._panel.reveal(column);
   return this._instance;
  } else {
   WebviewPanel._instance = new WebviewPanel(context);
   return this._instance;
  }
 }

 public readonly viewType = 'CodeManagerWebViewPanel';

 private _panel: vscode.WebviewPanel;

 private get webview() {
  return this._panel.webview;
 }

 private _disposables: vscode.Disposable[] = [];

 private _eventEmitter = new vscode.EventEmitter();

 public get onDidReceiveMessage() {
  return this._eventEmitter.event;
 }

 private constructor(private context: vscode.ExtensionContext) {
   this._panel = this.createPanel(context
   this.listenLifeCycleChanges();
   this.listenWebViewMessage();
   this.webview.html = this.loadWebviewContent();
 }

 private createPanel(context: vscode.ExtensionContext) {
  return vscode.window.createWebviewPanel(
   this.viewType,
   'SnippetManager',
   vscode.ViewColumn.Beside,
   {
    enableScripts: true,
    localResourceRoots: [
      vscode.Uri.joinPath(context.extensionUri, 'out', 'build'),
      vscode.Uri.joinPath(context.extensionUri, 'out', 'build', 'assets')
    ]
   }
  );
 }

 private _dispose() {
  this._panel.dispose();
  this._disposables.forEach((d: vscode.Disposable) => d.dispose());
  this._disposables = [];
  this._eventEmitter.dispose();
  WebviewPanel._instance = null;
 }

 private listenLifeCycleChanges() {
  this._panel.onDidDispose(() => this._dispose(), null, this._disposables);
  this._panel.onDidChangeViewState(
   e => {},
   null,
   this._disposables
  );
 }

 private listenWebViewMessage() {
  this._panel.webview.onDidReceiveMessage(
   message => {
       this._eventEmitter.fire(e);
   },
   null,
   this._disposables
  );
 }

 public sendMessage<T extends ObjectLike>(action: Action<T>) {
  this.webview.postMessage(action);
 }

 private webviewUri(relativePath: string) {
  const uri = vscode.Uri.parse(
   path.join(this.context.extensionPath,'out','build', relativePath)
  );
  return this.webview.asWebviewUri(uri);
 }

 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'));
  console.log(entrypointsJs);
  const nonce = getNonce();
  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"/>
         <title>React App</title>
         <base href="${this.webviewUri('/')}">
         <link rel="stylesheet" type="text/css" href="${this.webviewUri(mainfest.files['main.css'])}">
         <link rel="apple-touch-icon" href="${this.webviewUri('./logo192.png')}"/>
         <link rel="manifest" href="${this.webviewUri('./manifest.json')}"/>
   </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>`;
 }
}

function readJSON(path: string) {
 const jsonString = fs.readFileSync(path, 'utf-8');
 return JSON.parse(jsonString);
}

實作的方法大致與我們之前WebView(四)時提到的結構相同。

小有不同的是我們改變了WebView顯示的位置,我們會將其顯示於側邊的editor。

private createPanel(context: vscode.ExtensionContext) {
  return vscode.window.createWebviewPanel(
   this.viewType,
   'SnippetManager',
   vscode.ViewColumn.Beside,
   {
    enableScripts: true,
    localResourceRoots: [
      vscode.Uri.joinPath(context.extensionUri, 'out', 'build'),
      vscode.Uri.joinPath(context.extensionUri, 'out', 'build', 'assets')
    ]
   }
  );
 }

對外我們也會暴露onDidReceiveMessage方法,讓外部也可以監聽收到Webview傳來的資料,進行對應處理。

 ...
 public get onDidReceiveMessage() {
  return this._eventEmitter.event;
 }
 ...

完成WebviewPanel之後,我們一樣照先前教學將web專案命名為webview,並讓webview資料夾與src資料夾一樣位於根目錄。

啟動Webview後,我們會在當前編輯中的文件旁另開一個editor,如下所示。

結語


好啦,今天我們開始導入WebviewPanel到專案裡,明天也會繼續有更多實作。

我們明天見,謝謝大家。


上一篇
Day26 | 實現Extension內的MVVM架構
下一篇
Day28 | 獲取安裝的extension進行操作
系列文
自己用的工具自己做! 30天玩轉VS Code Extension之旅36

尚未有邦友留言

立即登入留言