Open hacker0limbo opened 3 years ago
实习快走之前一直在写一个插件, 当时为了赶进度, 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 这个库. 自己给的脚手架却又是另一种方案, 也是很无语...
test
src
测试不是本篇文章的重点, 具体信息参考官网的 Testing Extensions 这一章
初始化的项目编译后的代码在 out 目录下呈现的结构是:
out
out ├── extension.js └── test
我们希望的目录结构为:
out ├── src │ ├── extension.js └── test
因此需要改以下几个文件:
tsconfig.json:
tsconfig.json
官网关于 rootDir 这章已经说的很清晰了, 如果编译想保留当前目录名, 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:
package.json
入口文件需要改一下:
{ - "main": "./out/extension.js", + "main": "./out/src/extension.js", }
.vscode/launch.json:
.vscode/launch.json
我们希望编译后有对应的 source map 方便调试代码, 在 .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 本身发送和监听消息. 官网的给的文档已经很全了, 这里不做深究
Webview
iframe
用 React 来写 Webview 其实也很简单, 本质就是给定好一个 html, 利用 Webpack 打包好编译 jsx 等文件到一个 script 中, 然后链接一下即可.
html
Webpack
jsx
script
如之前的目录结构所示, app 目录为我们编写 React 代码的部分, 编译后的代码会打包到 out/app 目录下, 在 ViewLoader.ts 里引用这个编译后的文件即可
app
out/app
ViewLoader.ts
原则上来讲, Webview 有一个即可, 这里用单例模式来实现 ViewLoader:
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.
extension.ts
showWebview()
// 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 目录下, 保持一致
bundle.js
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.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 目录下新建一个 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 了...
// 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 效果了. 至此基本的框架完成
cmd + shift + p
open webview
该篇文章只是很简单的说明了如何用 React 来写 VSCode 插件中的 Webview. 但真实的场景下会有很多其他的需求, 比如该如何模拟路由, 如何在插件和 Webview 之间传递消息进行沟通, 如何动态传递插件本身的变量
有时间会写篇文章讲讲上面的思路, 没时间就算了直接看源码吧
实习快走之前一直在写一个插件, 当时为了赶进度, Webview 部分用的是 vue + Ant Design, 通过 cdn 的方式直接引入. 写的我痛不欲生, 后打算用 React 重写, 于是就有了这篇文章记录, 也算是一个学习和总结吧.
这篇文章更多的是倾向于配置, 我本身对于 Webpack 和 TypeScript 等的配置也不是很熟, 写的时候也基本都是随手谷歌抄过来并没有进行深究. 有错误也请谅解, 另如有更好的方案或有错误也及时指出
代码: https://github.com/hacker0limbo/vscode-webview-react-boilerplate
项目结构
初始化项目
根据官网的 tutorial 初始化项目
这里需要修改一下目录结构. 默认
test
目录是在src
下面的, 我个人习惯抽出来和src
平级. 搜索了一下发现微软很多自己的库test
文件都是不放在src
下的, 比如 vscode-postgresql 这个库. 自己给的脚手架却又是另一种方案, 也是很无语...测试不是本篇文章的重点, 具体信息参考官网的 Testing Extensions 这一章
初始化的项目编译后的代码在
out
目录下呈现的结构是:我们希望的目录结构为:
因此需要改以下几个文件:
tsconfig.json
:官网关于 rootDir 这章已经说的很清晰了, 如果编译想保留当前目录名,
rootDir
需要设置为"."
package.json
:入口文件需要改一下:
.vscode/launch.json
:我们希望编译后有对应的
source map
方便调试代码, 在.vscode
目录下的launch.json
文件修改一下配置:思路
Webview
可以看成是一个独立的iframe
, 有自己独立的运行环境, 同时也可以和 extension 本身发送和监听消息. 官网的给的文档已经很全了, 这里不做深究用 React 来写
Webview
其实也很简单, 本质就是给定好一个html
, 利用Webpack
打包好编译jsx
等文件到一个script
中, 然后链接一下即可.如之前的目录结构所示,
app
目录为我们编写 React 代码的部分, 编译后的代码会打包到out/app
目录下, 在ViewLoader.ts
里引用这个编译后的文件即可ViewLoader
原则上来讲, Webview 有一个即可, 这里用单例模式来实现
ViewLoader
:对应的在
extension.ts
里面注册好打开 Webview 的命令后, 只需要用静态方法showWebview()
即可初始化或显示之前被隐藏的Webview
panel.同时规定, 所有关于 React 的文件均放在
app
目录下, 编译后的文件名为bundle.js
也在out/app
目录下, 保持一致安装依赖
这里提一下, 为了让编译 Webview 的任务和编译插件本身的任务同时进行, 安装了 npm-run-all 这个库.
配置 Webpack
本人对 Webpack 不熟, 这些配置基本都是从官网或者谷歌抄过来的, 如果你有更好的自定义方案求轻喷:
这里需要更改一下根目录下的
tsconfig.json
文件, 如下:app
下的所有文件均交给 Webpack 来处理, 该tsconfig.json
文件仅负责对 extension 代码编译app
在
app
目录下新建一个tsconfig.json
文件, 这个文件是用于编译app
部分的 ts 和 tsx 代码, 注意和根目录下的tsconfig.json
区别:同样的, 我对 ts 配置也不熟, 这里只是给出从网上拔过来的最基本方案, 求轻喷
React & TSX
于是可以欢快的写 React 和 TSX 了...
编译运行
修改一下
package.json
文件里的命令:运行插件后, VSCode 会开两个进程, 一个负责编译 Extension 部分代码, 一个负责编译 Webview React 部分代码,
cmd + shift + p
输入open webview
命令就能看到最后的 web view 效果了. 至此基本的框架完成后续
该篇文章只是很简单的说明了如何用 React 来写 VSCode 插件中的 Webview. 但真实的场景下会有很多其他的需求, 比如该如何模拟路由, 如何在插件和 Webview 之间传递消息进行沟通, 如何动态传递插件本身的变量
有时间会写篇文章讲讲上面的思路, 没时间就算了直接看源码吧
参考