We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
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
上一篇文章 主要讲了如何配置 Webview 的 React 开发环境. 这篇文章主要想谈谈开发 Webview 时候可能遇到的场景和问题, 一些细节就不展开了.
代码地址: https://github.com/hacker0limbo/vscode-webview-react-boilerplate
最后的效果:
需求很简单, 主要实现三个页面, 一个导航栏以及一个刷新按钮:
Reload
Extension
由于我不会 CSS, 最后效果看上去可能有点丑, 就不要在意这些细节了
Webview 默认应该是不支持 URL 的, 所以如果用 BrowserRouter 可能会失效. 为了支持路由, 这里改用 MemoryRouter, 用法也是非常简单, 以我的场景为例, 只需配置一下需要的路由端点即可:
BrowserRouter
MemoryRouter
import { MemoryRouter as Router, Link } from 'react-router-dom'; <Router initialEntries={['/', '/about', '/message', '/message/received', '/message/send']}> <ul className="navbar"> <li> <Link to="/">Home</Link> </li> <li> <Link to="/about">About</Link> </li> <li> <Link to="/message">Message</Link> </li> </ul> </Router>;
更多的用法还是参考官网的 API, 这里不展开
Extension 和 Webview 本身都支持接收和发送消息, 消息的类型没有限制, 官方给的都是 any. 这里简单介绍各自一下具体的 API
Webview
any
Extension 里接收和发送消息:
// 初始化 const panel = vscode.window.createWebviewPanel({ ... }) // 接收从 Webview 发送过来的消息 panel.webview.onDidReceiveMessage( (message: any) => { console.log('message from webview: ': message) }, undefined, context.subscriptions ); // 发送消息给 Webview panel.webview.postMessage(...);
Webview 里接收和发送消息:
// 接收从 Extension 发送过来的消息 window.addEventListener('message', (event: MessageEvent<any>) => { const message = event.data; console.log('message from extension: ', message) }); // 发送消息给 Extension const vscode = acquireVsCodeApi(); vscode.postMessage(...);
由于默认所以消息类型都是 any, 在开发的时候会有一些不方便. 因此最好规定一下消息的格式. 这里只简单讲一下我的规定.
在 src/view/message 里新建一个 messageTypes.ts 专门存放消息类型
src/view/message
messageTypes.ts
export type MessageType = 'RELOAD' | 'COMMON'; export interface Message { type: MessageType; payload?: any; } export interface CommonMessage extends Message { type: 'COMMON'; payload: string; } export interface ReloadMessage extends Message { type: 'RELOAD'; }
Message 为最基本的消息类型, 有两个属性, type 表示当前消息属于哪种类型, payload 为消息的数据, 可选. 有点像 Redux 的 Action 了...
Message
type
payload
Redux
Action
至于是否需要定义消息是属于发送还是接收, 我个人觉得没有太大意义, 由于只存在 Extension 和 Webview 两个载体, 也就是一对一关系, 在任何一方做接收或者发送的时候其实就已经能很清楚的知道这个消息的起始点或者重点对应是哪一方. 而对于定义消息的类型 type 反而很有必要, 目的是为了在一方接收的时候做区分, 不同的消息类型所携带的 payload 也是不同的. 有了 type 之后开发也可以做类型守护或者类型断言来更加严格定义消息的类型.
关于在 webview 里接收和发送消息, 虽然官方提到可以很简单的使用 const vscode = acquireVsCodeApi(); 来获取 vscode 变量进而发送消息. 但由于我们 webview 使用的是 ts. 这种注入的变量是没有任何类型声明, 编译器不知道它哪来的会直接报错. 这里其实有很多方法来解决, 为了方便我的做法是在 app 文件(也就是 Webview React 目录)下新建一个 global.d.ts, 手动声明 vscode 类型, 同时在之前的 ViewLoader.ts 文件的 render() 方法里提前注入 vscode 变量:
webview
const vscode = acquireVsCodeApi();
vscode
ts
app
global.d.ts
ViewLoader.ts
render()
// src/view/ViewLoader.ts export class ViewLoader { // ... 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> const vscode = acquireVsCodeApi(); </script> <script src="${bundleScriptPath}"></script> </body> </html> `; } }
// app/global.d.ts type Message = import('../src/view/messages/messageTypes').Message; type VSCode = { postMessage<T extends Message = Message>(message: T): void; getState(): any; setState(state: any): void; }; declare const vscode: VSCode;
这里 postMessage() 简单做一下泛型...
postMessage()
Webview 本身不提供类似浏览器的刷新按钮, 万幸的是 vscode 提供了一个命令用于刷新所有的 Webview: 'workbench.action.webview.reloadWebviewAction', 对于单个的 Webview 不支持. 具体讨论可以看这个 ISSUE
'workbench.action.webview.reloadWebviewAction'
实现思路很简单, 由于这个命令是需要从 Extension 层面触发, Webview 发送一条消息通知 Extension, Extension 接收消息触发 reload 命令, 简易代码如下:
reload
// app/components/App.tsx import React from 'react'; export const App = () => { const handleReloadWebview = () => { vscode.postMessage<ReloadMessage>({ type: 'RELOAD', }); }; return <button onClick={handleReloadWebview}>Reload Webview</button>; };
// src/view/ViewLoader.ts 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: Message) => { if (message.type === 'RELOAD') { vscode.commands.executeCommand('workbench.action.webview.reloadWebviewAction'); } }, null, this.disposables ); this.panel.onDidDispose( () => { this.dispose(); }, null, this.disposables ); } // ... }
没啥说的...
这个页面会往服务端发送 Http 请求, API 我直接用的网上给的 Random User API, 请求端点: https://randomuser.me/api/. 该 API 会返回随机伪造的用户数据, 页面会以列表形式渲染用户的姓名, 性别和邮箱地址
API
https://randomuser.me/api/
同时, 该 API 支持参数, 比如允许请求的用户只为男性, 那么 URL 变为: https://randomuser.me/api?gender=male. 这个参数我们可以选择让用户在 VSCode 的 settings 里自行配置, 然后 Webview 从中读取配置.
URL
https://randomuser.me/api?gender=male
VSCode
settings
官方文档有详细的说明如何做配置, 这里我做的配置如下:
{ "contributes": { "configuration": { "title": "Webview React", "properties": { "webviewReact.userApiGender": { "type": "string", "default": "male", "enum": ["male", "female"], "enumDescriptions": [ "Fetching user information with gender of male", "Fetching user information with gender of female" ] } } } } }
最后在 VSCode 的配置 UI 展示为一个下拉框, 默认值为 male.
male
读取配置也很简单:
// src/config/index.ts import * as vscode from 'vscode'; export const getAPIUserGender = () => { const gender = vscode.workspace.getConfiguration('webviewReact').get('userApiGender', 'male'); return gender; };
和之前注入 vscode 变量一样, 在 render() 方法里注入 gender, 同时在 global.d.ts 文件里声明好类型
gender
// src/view/ViewLoader.ts export class ViewLoader { // ... render() { const bundleScriptPath = this.panel.webview.asWebviewUri( vscode.Uri.file(path.join(this.context.extensionPath, 'out', 'app', 'bundle.js')) ); const gender = getAPIUserGender(); 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> const vscode = acquireVsCodeApi(); const apiUserGender = "${gender}" </script> <script> console.log('apiUserGender', apiUserGender) </script> <script src="${bundleScriptPath}"></script> </body> </html> `; } }
// global.d.ts type Message = import('../src/view/messages/messageTypes').Message; type VSCode = { postMessage<T extends Message = Message>(message: T): void; getState(): any; setState(state: any): void; }; declare const vscode: VSCode; declare const apiUserGender: string;
没啥说的, 这部分反而是最简单的
import React, { useState, useCallback, useEffect } from 'react'; import { apiUrl } from '../api'; type UserInfo = { name: string; gender: string; email: string; }; export const About = () => { const [userInfo, setUserInfo] = useState<UserInfo>({ name: '', gender: '', email: '', }); const [loading, setLoading] = useState(false); const fetchUser = useCallback(() => { setLoading(true); fetch(apiUrl) .then((res) => res.json()) .then(({ results }) => { const user = results[0]; setLoading(false); setUserInfo({ name: `${user.name.first} ${user.name.last}`, gender: user.gender, email: user.email, }); }) .catch((err) => { setLoading(false); }); }, []); useEffect(() => { fetchUser(); }, [fetchUser]); return ( <div> <h1>About</h1> <h3>User Info</h3> {loading ? ( <div>Loading...</div> ) : ( <ul> <li>Name: {userInfo.name}</li> <li>Gender: {userInfo.gender}</li> <li>Email: {userInfo.email}</li> </ul> )} <button onClick={fetchUser}>Fetch</button> </div> ); };
数据请求我就用了原生的 Fetch, 因为我不想再装库了...
Fetch
这里有一个小 BUG, 如果用户在打开 Webview 之后再更新了配置, 点击按钮之后请求的数据还是更新前的. 原因在于 render() 方法中的 html 并不会根据配置的更新而重新渲染, 要改其实也不难, VSCode 提供了一个 onDidChangeConfiguration 的方法用于监听配置更改, 只要在这个方法中重新渲染 html 即可. 但因为本人比较懒, 就没实现这个需求...
html
onDidChangeConfiguration
该页面有两个子页面, 一个为 ReceivedMessages.tsx, 一个为 SendMessage.tsx. 前者用于监听 Extension 发送过来的消息, 后者可以发送消息给 Extension.
ReceivedMessages.tsx
SendMessage.tsx
在 Extension 端, VSCode 提供一个 InputBox API showInputBox, 可供输入简单的单行文本. 当用户输入文本之后按下 Enter 键, 消息即被发送到 Webview. 如果选择 ESC, 不做任何操作
InputBox
Enter
ESC
// src/extension.ts const disposable = vscode.commands.registerCommand('extension.sendMessage', () => { vscode.window .showInputBox({ prompt: 'Send message to Webview', }) .then((result) => { result && ViewLoader.postMessageToWebview<CommonMessage>({ type: 'COMMON', payload: result, }); }); });
在 Webview 端需要做监听, 如果直接放在 ReceivedMessages 这个比较深的组件里, 虽然是可行的. 但切换路由的时候组件就 umount 了, 没有了监听即使 Extension 发送了任何消息过来也不会有任何响应. 所以我选择放在 App.tsx 这个比较顶层的组件, 该组件一直存在. 监听到的消息通过 context 传递给 children 组件. 任何组件有需要消息的, 只要订阅 context 即可.
ReceivedMessages
umount
App.tsx
context
当然了, 怎么写放在哪还是看具体的业务需求. 这里只是提供思路
// app/context/MessageContext.tsx import React from 'react'; export const MessagesContext = React.createContext<string[]>([]);
// app/components/App.tsx export const App = () => { const [messagesFromExtension, setMessagesFromExtension] = useState<string[]>([]); const handleMessagesFromExtension = useCallback( (event: MessageEvent<Message>) => { if (event.data.type === 'COMMON') { const message = event.data as CommonMessage; setMessagesFromExtension([...messagesFromExtension, message.payload]); } }, [messagesFromExtension] ); useEffect(() => { window.addEventListener('message', (event: MessageEvent<Message>) => { handleMessagesFromExtension(event); }); return () => { window.removeEventListener('message', handleMessagesFromExtension); }; }, [handleMessagesFromExtension]); // ... return ( <MessagesContext.Provider value={messagesFromExtension}> <Switch> <Route exact path="/"> <Home /> </Route> <Route path="/about"> <About /> </Route> <Route path="/message"> <Message /> </Route> </Switch> </MessagesContext.Provider> ); };
具体渲染消息的页面就不多说了
Webview 作为发送端, 渲染一个 input 框和一个 button, 不多描述, 代码如下:
input
button
import React, { useState } from 'react'; import { CommonMessage } from '../../src/view/messages/messageTypes'; export const SendMessage = () => { const [message, setMessage] = useState(''); const handleMessageChange = (e: React.ChangeEvent<HTMLInputElement>) => { setMessage(e.target.value); }; const sendMessage = () => { vscode.postMessage<CommonMessage>({ type: 'COMMON', payload: message, }); }; return ( <div> <p>Send Message to Extension:</p> <input value={message} onChange={handleMessageChange} /> <button onClick={sendMessage}>Send</button> </div> ); };
Extension 作为接收端, 在 ViewLoader.ts 里接受消息. 这里选择将接受的消息以 InformationMessage Dialog 的形式展示出来:
InformationMessage
this.panel.webview.onDidReceiveMessage( (message: Message) => { if (message.type === 'RELOAD') { vscode.commands.executeCommand('workbench.action.webview.reloadWebviewAction'); } else if (message.type === 'COMMON') { const text = (message as CommonMessage).payload; vscode.window.showInformationMessage(`Received message from Webview: ${text}`); } }, null, this.disposables );
至此整个项目就算完善了, 可能有更加复杂的业务场景没有考虑到, 后续遇到了也会及时更新. 本人水平也不高, 开发的时候可能也有很多地方有错误, 看代码的话也请及时指出.
Webview 只算开发插件的很小一部分. VSCode 整个生态系统是非常庞大的, 同时也暴露了非常好的接口供开发者自行开发插件, 虽然有些时候文档不一定全, 但大多数问题还是可以通过谷歌, Stackoverflow, 或者搜 Github Issue 来解决的.
社区也有对应的中文文档, 地址: https://liiked.github.io/VS-Code-Extension-Doc-ZH/#/
新年快乐!
The text was updated successfully, but these errors were encountered:
有个专门的 package @types/vscode-webview 定义了 acquireVsCodeApi 的返回类型。 另外 https://zhuanlan.zhihu.com/p/483842887 讲了怎么实现热更新
Sorry, something went wrong.
@tjx666 多谢, 我之前写这篇文章的时候貌似是还没有对应这个类型的包, 当时的解决办法也是去 stackoverflow 上问别人给的答案. 现在来看文章其实很多地方过时了, 自己也很长一段时间没去碰过 vscode 的插件开发. 有空我去研究拜读一些你的文章, 感谢大佬!
No branches or pull requests
上一篇文章 主要讲了如何配置 Webview 的 React 开发环境. 这篇文章主要想谈谈开发 Webview 时候可能遇到的场景和问题, 一些细节就不展开了.
代码地址: https://github.com/hacker0limbo/vscode-webview-react-boilerplate
最后的效果:
需求
需求很简单, 主要实现三个页面, 一个导航栏以及一个刷新按钮:
Reload
功能Extension
发送的消息并实时渲染, 一个类似表单可以往Extension
发送消息由于我不会 CSS, 最后效果看上去可能有点丑, 就不要在意这些细节了
导航栏与路由
Webview 默认应该是不支持 URL 的, 所以如果用
BrowserRouter
可能会失效. 为了支持路由, 这里改用MemoryRouter
, 用法也是非常简单, 以我的场景为例, 只需配置一下需要的路由端点即可:更多的用法还是参考官网的 API, 这里不展开
消息
消息的传递
Extension
和Webview
本身都支持接收和发送消息, 消息的类型没有限制, 官方给的都是any
. 这里简单介绍各自一下具体的 APIExtension
里接收和发送消息:Webview
里接收和发送消息:消息的类型
由于默认所以消息类型都是
any
, 在开发的时候会有一些不方便. 因此最好规定一下消息的格式. 这里只简单讲一下我的规定.在
src/view/message
里新建一个messageTypes.ts
专门存放消息类型Message
为最基本的消息类型, 有两个属性,type
表示当前消息属于哪种类型,payload
为消息的数据, 可选. 有点像Redux
的Action
了...至于是否需要定义消息是属于发送还是接收, 我个人觉得没有太大意义, 由于只存在
Extension
和Webview
两个载体, 也就是一对一关系, 在任何一方做接收或者发送的时候其实就已经能很清楚的知道这个消息的起始点或者重点对应是哪一方. 而对于定义消息的类型type
反而很有必要, 目的是为了在一方接收的时候做区分, 不同的消息类型所携带的payload
也是不同的. 有了type
之后开发也可以做类型守护或者类型断言来更加严格定义消息的类型.注入 vscode
关于在
webview
里接收和发送消息, 虽然官方提到可以很简单的使用const vscode = acquireVsCodeApi();
来获取vscode
变量进而发送消息. 但由于我们webview
使用的是ts
. 这种注入的变量是没有任何类型声明, 编译器不知道它哪来的会直接报错. 这里其实有很多方法来解决, 为了方便我的做法是在app
文件(也就是 Webview React 目录)下新建一个global.d.ts
, 手动声明vscode
类型, 同时在之前的ViewLoader.ts
文件的render()
方法里提前注入vscode
变量:这里
postMessage()
简单做一下泛型...Reload
Webview
本身不提供类似浏览器的刷新按钮, 万幸的是vscode
提供了一个命令用于刷新所有的Webview
:'workbench.action.webview.reloadWebviewAction'
, 对于单个的Webview
不支持. 具体讨论可以看这个 ISSUE实现思路很简单, 由于这个命令是需要从
Extension
层面触发,Webview
发送一条消息通知Extension
,Extension
接收消息触发reload
命令, 简易代码如下:Home 页面
没啥说的...
About 页面
这个页面会往服务端发送 Http 请求,
API
我直接用的网上给的 Random User API, 请求端点:https://randomuser.me/api/
. 该API
会返回随机伪造的用户数据, 页面会以列表形式渲染用户的姓名, 性别和邮箱地址同时, 该
API
支持参数, 比如允许请求的用户只为男性, 那么URL
变为:https://randomuser.me/api?gender=male
. 这个参数我们可以选择让用户在VSCode
的settings
里自行配置, 然后Webview
从中读取配置.配置
官方文档有详细的说明如何做配置, 这里我做的配置如下:
最后在
VSCode
的配置 UI 展示为一个下拉框, 默认值为male
.读取配置也很简单:
注入配置到 Webview
和之前注入
vscode
变量一样, 在render()
方法里注入gender
, 同时在global.d.ts
文件里声明好类型画页面
没啥说的, 这部分反而是最简单的
数据请求我就用了原生的
Fetch
, 因为我不想再装库了...这里有一个小 BUG, 如果用户在打开
Webview
之后再更新了配置, 点击按钮之后请求的数据还是更新前的. 原因在于render()
方法中的html
并不会根据配置的更新而重新渲染, 要改其实也不难,VSCode
提供了一个onDidChangeConfiguration
的方法用于监听配置更改, 只要在这个方法中重新渲染html
即可. 但因为本人比较懒, 就没实现这个需求...Message
该页面有两个子页面, 一个为
ReceivedMessages.tsx
, 一个为SendMessage.tsx
. 前者用于监听Extension
发送过来的消息, 后者可以发送消息给Extension
.ReceivedMessages
在
Extension
端,VSCode
提供一个InputBox
API showInputBox, 可供输入简单的单行文本. 当用户输入文本之后按下Enter
键, 消息即被发送到Webview
. 如果选择ESC
, 不做任何操作在
Webview
端需要做监听, 如果直接放在ReceivedMessages
这个比较深的组件里, 虽然是可行的. 但切换路由的时候组件就umount
了, 没有了监听即使Extension
发送了任何消息过来也不会有任何响应. 所以我选择放在App.tsx
这个比较顶层的组件, 该组件一直存在. 监听到的消息通过context
传递给 children 组件. 任何组件有需要消息的, 只要订阅context
即可.当然了, 怎么写放在哪还是看具体的业务需求. 这里只是提供思路
具体渲染消息的页面就不多说了
SendMessage
Webview
作为发送端, 渲染一个input
框和一个button
, 不多描述, 代码如下:Extension
作为接收端, 在ViewLoader.ts
里接受消息. 这里选择将接受的消息以InformationMessage
Dialog 的形式展示出来:后记
至此整个项目就算完善了, 可能有更加复杂的业务场景没有考虑到, 后续遇到了也会及时更新. 本人水平也不高, 开发的时候可能也有很多地方有错误, 看代码的话也请及时指出.
Webview 只算开发插件的很小一部分.
VSCode
整个生态系统是非常庞大的, 同时也暴露了非常好的接口供开发者自行开发插件, 虽然有些时候文档不一定全, 但大多数问题还是可以通过谷歌, Stackoverflow, 或者搜 Github Issue 来解决的.社区也有对应的中文文档, 地址: https://liiked.github.io/VS-Code-Extension-Doc-ZH/#/
新年快乐!
The text was updated successfully, but these errors were encountered: