Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

简单聊一聊 React 和 VSCode Webview (一) #19

Open
hacker0limbo opened this issue Feb 7, 2021 · 0 comments
Open

简单聊一聊 React 和 VSCode Webview (一) #19

hacker0limbo opened this issue Feb 7, 2021 · 0 comments
Labels
react react 笔记整理 typescript typescript 笔记整理 vscode VSCode 笔记整理

Comments

@hacker0limbo
Copy link
Owner

实习快走之前一直在写一个插件, 当时为了赶进度, Webview 部分用的是 vue + Ant Design, 通过 cdn 的方式直接引入. 写的我痛不欲生, 后打算用 React 重写, 于是就有了这篇文章记录, 也算是一个学习和总结吧.

这篇文章更多的是倾向于配置, 我本身对于 Webpack 和 TypeScript 等的配置也不是很熟, 写的时候也基本都是随手谷歌抄过来并没有进行深究. 有错误也请谅解, 另如有更好的方案或有错误也及时指出

代码: https://github.com/hacker0limbo/vscode-webview-react-boilerplate

项目结构

├── app # React 部分
│   ├── App.tsx
│   ├── index.tsx
│   └── tsconfig.json
├── package-lock.json
├── package.json
├── src
│   ├── extension.ts
│   └── view
│       └── ViewLoader.ts
├── test
├── tsconfig.json
└── webpack.config.js

初始化项目

根据官网的 tutorial 初始化项目

npm install -g yo generator-code

yo code

这里需要修改一下目录结构. 默认 test 目录是在 src 下面的, 我个人习惯抽出来和 src 平级. 搜索了一下发现微软很多自己的库 test 文件都是不放在 src 下的, 比如 vscode-postgresql 这个库. 自己给的脚手架却又是另一种方案, 也是很无语...

测试不是本篇文章的重点, 具体信息参考官网的 Testing Extensions 这一章

初始化的项目编译后的代码在 out 目录下呈现的结构是:

out
├── extension.js
└── test

我们希望的目录结构为:

out
├── src
│   ├── extension.js
└── test

因此需要改以下几个文件:

tsconfig.json:

官网关于 rootDir 这章已经说的很清晰了, 如果编译想保留当前目录名, rootDir 需要设置为 "."

{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es6",
    "outDir": "out",
    "lib": [
      "es6"
    ],
    "sourceMap": true,
+   "rootDir": ".",
    "strict": true   /* enable all strict type-checking options */
    /* Additional Checks */
    // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
    // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
    // "noUnusedParameters": true,  /* Report errors on unused parameters. */
  },
  "exclude": [
    "node_modules",
    ".vscode-test",
  ]
}

package.json:

入口文件需要改一下:

{
- "main": "./out/extension.js",
+ "main": "./out/src/extension.js",
}

.vscode/launch.json:

我们希望编译后有对应的 source map 方便调试代码, 在 .vscode 目录下的 launch.json 文件修改一下配置:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Run Extension",
      "type": "extensionHost",
      "request": "launch",
      "args": [
        "--extensionDevelopmentPath=${workspaceFolder}"
      ],
      "outFiles": [
-       "${workspaceFolder}/out/**/*.js"
+       "${workspaceFolder}/out/src/**/*.js"
      ],
+     "sourceMaps": true,
      "preLaunchTask": "${defaultBuildTask}"
    },
    {
      "name": "Extension Tests",
      "type": "extensionHost",
      "request": "launch",
      "args": [
        "--extensionDevelopmentPath=${workspaceFolder}",
        "--extensionTestsPath=${workspaceFolder}/out/test/suite/index"
      ],
      "outFiles": [
        "${workspaceFolder}/out/test/**/*.js"
      ],
+     "sourceMaps": true,
      "preLaunchTask": "${defaultBuildTask}"
    }
  ]
}

思路

Webview 可以看成是一个独立的 iframe, 有自己独立的运行环境, 同时也可以和 extension 本身发送和监听消息. 官网的给的文档已经很全了, 这里不做深究

用 React 来写 Webview 其实也很简单, 本质就是给定好一个 html, 利用 Webpack 打包好编译 jsx 等文件到一个 script 中, 然后链接一下即可.

如之前的目录结构所示, app 目录为我们编写 React 代码的部分, 编译后的代码会打包到 out/app 目录下, 在 ViewLoader.ts 里引用这个编译后的文件即可

ViewLoader

原则上来讲, Webview 有一个即可, 这里用单例模式来实现 ViewLoader:

// src/view/ViewLoader.ts

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

export class ViewLoader {
  public static currentPanel?: vscode.WebviewPanel;

  private panel: vscode.WebviewPanel;
  private context: vscode.ExtensionContext;
  private disposables: vscode.Disposable[];

  constructor(context: vscode.ExtensionContext) {
    this.context = context;
    this.disposables = [];

    this.panel = vscode.window.createWebviewPanel('reactApp', 'React App', vscode.ViewColumn.One, {
      enableScripts: true,
      retainContextWhenHidden: true,
      localResourceRoots: [vscode.Uri.file(path.join(this.context.extensionPath, 'out', 'app'))],
    });

    // render webview
    this.renderWebview();

    // listen messages from webview
    this.panel.webview.onDidReceiveMessage(
      (message) => {
        console.log('msg', message);
      },
      null,
      this.disposables
    );

    this.panel.onDidDispose(
      () => {
        this.dispose();
      },
      null,
      this.disposables
    );
  }

  private renderWebview() {
    const html = this.render();
    this.panel.webview.html = html;
  }

  static showWebview(context: vscode.ExtensionContext) {
    const cls = this;
    const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined;
    if (cls.currentPanel) {
      cls.currentPanel.reveal(column);
    } else {
      cls.currentPanel = new cls(context).panel;
    }
  }

  static postMessageToWebview(message: any) {
    // post message from extension to webview
    const cls = this;
    cls.currentPanel?.webview.postMessage(message);
  }

  public dispose() {
    ViewLoader.currentPanel = undefined;

    // Clean up our resources
    this.panel.dispose();

    while (this.disposables.length) {
      const x = this.disposables.pop();
      if (x) {
        x.dispose();
      }
    }
  }

  render() {
    const bundleScriptPath = this.panel.webview.asWebviewUri(
      vscode.Uri.file(path.join(this.context.extensionPath, 'out', 'app', 'bundle.js'))
    );

    return `
      <!DOCTYPE html>
        <html lang="en">
        <head>
          <meta charset="UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <title>React App</title>
        </head>
    
        <body>
          <div id="root"></div>
          <script src="${bundleScriptPath}"></script>
        </body>
      </html>
    `;
  }
}

对应的在 extension.ts 里面注册好打开 Webview 的命令后, 只需要用静态方法 showWebview() 即可初始化或显示之前被隐藏的 Webview panel.

// src/extension.ts

export function activate(context: vscode.ExtensionContext) {
  const disposable = vscode.commands.registerCommand('webview.open', () => {
    ViewLoader.showWebview(context);
  });

  context.subscriptions.push(disposable);
}

同时规定, 所有关于 React 的文件均放在 app 目录下, 编译后的文件名为 bundle.js 也在 out/app 目录下, 保持一致

安装依赖

npm install react react-dom react-router-dom
npm install --save-dev @types/react @types/react-dom @types/react @types/react-router-dom webpack webpack-cli ts-loader css-loader style-loader npm-run-all

这里提一下, 为了让编译 Webview 的任务和编译插件本身的任务同时进行, 安装了 npm-run-all 这个库.

配置 Webpack

本人对 Webpack 不熟, 这些配置基本都是从官网或者谷歌抄过来的, 如果你有更好的自定义方案求轻喷:

// webpack.config.js

const path = require('path');

module.exports = {
  entry: path.join(__dirname, 'app', 'index.tsx'),
  resolve: {
    extensions: ['.ts', '.tsx', '.js', '.jsx', '.css'],
  },
  devtool: 'inline-source-map',
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: '/node_modules/',
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'out', 'app'),
  },
};

这里需要更改一下根目录下的 tsconfig.json 文件, 如下:

{
  "exclude": [
    "node_modules",
    ".vscode-test",
+    "app"
  ]
}

app 下的所有文件均交给 Webpack 来处理, 该 tsconfig.json 文件仅负责对 extension 代码编译

app

app 目录下新建一个 tsconfig.json 文件, 这个文件是用于编译 app 部分的 ts 和 tsx 代码, 注意和根目录下的 tsconfig.json 区别:

// app/tsconfig.json

{
  "compilerOptions": {
    "module": "ESNext",
    "allowSyntheticDefaultImports": true,
    "moduleResolution": "node",
    "target": "ES5",
    "jsx": "react",
    "sourceMap": true,
    "experimentalDecorators": true,
    "lib": ["dom", "ES2015"],
    "strict": true
  },
  "exclude": ["node_modules"]
}

同样的, 我对 ts 配置也不熟, 这里只是给出从网上拔过来的最基本方案, 求轻喷

React & TSX

于是可以欢快的写 React 和 TSX 了...

// app/index.tsx

import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(<div>Hello World</div>, document.getElementById('root'));

编译运行

修改一下 package.json 文件里的命令:

"scripts": {
  "compile": "npm-run-all compile:*",
  "compile:extension": "tsc -p ./",
  "compile:view": "webpack --mode development",
  "watch": "npm-run-all -p watch:*",
  "watch:extension": "tsc -watch -p ./",
  "watch:view": "webpack --watch --mode development",
  "pretest": "npm run compile && npm run lint",
  "lint": "eslint src --ext ts",
  "test": "node ./out/test/runTest.js"
},

运行插件后, VSCode 会开两个进程, 一个负责编译 Extension 部分代码, 一个负责编译 Webview React 部分代码, cmd + shift + p 输入 open webview 命令就能看到最后的 web view 效果了. 至此基本的框架完成

后续

该篇文章只是很简单的说明了如何用 React 来写 VSCode 插件中的 Webview. 但真实的场景下会有很多其他的需求, 比如该如何模拟路由, 如何在插件和 Webview 之间传递消息进行沟通, 如何动态传递插件本身的变量

有时间会写篇文章讲讲上面的思路, 没时间就算了直接看源码吧

参考

@hacker0limbo hacker0limbo added react react 笔记整理 typescript typescript 笔记整理 vscode VSCode 笔记整理 labels Feb 7, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
react react 笔记整理 typescript typescript 笔记整理 vscode VSCode 笔记整理
Projects
None yet
Development

No branches or pull requests

1 participant