-
Notifications
You must be signed in to change notification settings - Fork 1
Description
实习快走之前一直在写一个插件, 当时为了赶进度, 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 之间传递消息进行沟通, 如何动态传递插件本身的变量
有时间会写篇文章讲讲上面的思路, 没时间就算了直接看源码吧
参考
- https://code.visualstudio.com/api/extension-guides/webview
- https://github.com/microsoft/vscode-postgresql
- https://github.com/rebornix/vscode-webview-react
- https://medium.com/younited-tech-blog/reactception-extending-vs-code-extension-with-webviews-and-react-12be2a5898fd
- https://stackoverflow.com/a/5092846/12733140