VSCode是微软出的一款轻量级代码编辑器,免费并且功能强大,以功能强大、提示友好、不错的性能和颜值俘获了大量开发者的青睐,对JavaScript和NodeJS的支持很是好,自带不少功能,例如代码格式化,代码智能提示补全、Emmet插件等。
css
它是经过 Electron 实现跨平台的,而 Electron 则是基于 Chromium 和 Node.js,好比 VS Code 的界面,就是经过 Chromium 进行渲染的。同时, VS Code 是多进程架构,当 VS Code 第一次被启动时会建立一个主进程(main process),而后每一个窗口,都会建立一个渲染进程( Renderer Process)。与此同时,VS Code 会为每一个窗口建立一个进程专门来执行插件,也就是 Extension Host。除了这三个主要的进程之外,还有两种特殊的进程。第一种是调试进程,VS Code 为调试器专门建立了Debug Adapter 进程,渲染进程会经过 VS Code Debug Protocol 跟 Debug Adapter 进程通信。html
架构图以下:前端
不过此次分享咱们不过多的探讨它的架构,主要看下插件(或者称为扩展,下同)怎么写。node
从vscode
插件开发的脚手架(执行yo code
)咱们能够看到有以下选项:ios
经过cli
咱们能够直接建立扩展、主题、语言支持、代码片断、快捷键等插件项目,这些插件项目建立后开箱直用,按F5运行便可。git
webview
(好比markdown preview
)git
)Java
,.Net
,Python
,Dart
,Go
……)……等等等等web
VSCode
极其优秀的扩展架构给咱们提供了很是大的施展拳脚的空间。ajax
好比,你在项目中对反复执行某项繁杂操做很不爽,那么你是时候作一个插件解放你的双手了!!!chrome
能够参考下面这个博客,博主对主流插件功能(包括自定义跳转、自动补全、悬浮提示)作了很是全面的介绍typescript
我今天主要讲一下,本身是如何实践webview
插件的。对于前端而言,作一些能看获得的漂亮东西,老是更具备吸引力,因此我主要关注了webview
这块。先贴个成品图:
首先,安装vscode cli,
npm install -g yo generator-code
复制代码
再用cli
建立一个New Extension (TypeScript)
项目
yo code
复制代码
它会帮咱们初始化好以下几块内容 :
咱们暂时不太须要关心tsconfig.json
文件,由于是开箱即用的,除非咱们须要用到一些typescript
的独特特性。
先来看看package.json里都有什么:
{
// 插件的激活事件
"activationEvents": [
"onCommand:extension.sayHello"
],
// 入口文件
"main": "./src/extension",
"engines": {
"vscode": "^1.27.0"
},
// 贡献点,vscode插件大部分功能配置都在这里
"contributes": {
"commands": [
{
"command": "extension.sayHello",
"title": "Hello World"
}
]
}
}
复制代码
activationEvents
扩展激活事件,属性值是个数组,包含一系列事件(除了onCommand
以外还有onView
、onUri
、onLanguage
等等)。由于VSCode
为了性能考虑,并不会一打开就加载全部的插件。只有当用户行为触发了该数组中包含的事件(好比执行命令或所打开文件的语言是json
)时,才会激活插件(也能够配成"*",就会立刻加载,可是不建议这样作);main
定义了整个插件的入口点;engines
插件最低支持的VSCode
版本contributes
定义了插件全部的贡献点,好比commands
(命令)、menus
(菜单)、configuration
(配置项)、keybindings
(快捷键绑定)、snippets
(代码片断)、views
(侧边栏内view的实现)、iconThemes
(图标主题)等等。咱们要配一个右上角的菜单,直接贴配置:
"contributes": {
"commands": [{
"command": "extension.colaMovie",
"title": "Cola Movie",
"icon": {
"light": "./images/film-light.svg",
"dark": "./images/film-dark.svg"
}
}],
"menus": {
"editor/title": [{
"when": "isWindows || isMac",
"command": "extension.colaMovie",
"group": "navigation"
}]
}
}
复制代码
解释:定义一个extension.colaMovie
命令,顺便配置title
和icon
。
为了一处命令配置多处使用,
title
和icon
项放置在commands
中了。此外,icon
支持light
与dark
明暗两类主题。若是不配置icon
,则会显示文字标题。
定义一个menus
菜单,类型为editor/title
,表明右上角图标。
when
配置了该菜单出现的场景(条件),除了isWindows
与isMac
还有很是多条件可使用command
指定点击该菜单会触发什么命令(commands
中的命令)group
指定菜单分组,主要用于编辑器右键菜单而后咱们再回过头来看一下main
入口extension.ts
文件:
const vscode = require('vscode');
/** * 插件被激活时触发,全部代码总入口 * @param {*} context 插件上下文 */
exports.activate = function(context: vscode.ExtensionContext) {
console.log('恭喜,您的扩展“vscode-plugin-demo”已被激活!');
// 注册命令
context.subscriptions.push(vscode.commands.registerCommand('extension.colaMovie', function () {
vscode.window.showInformationMessage('Hello World!');
}));
};
/** * 插件被释放时触发 */
exports.deactivate = function() {
console.log('您的扩展“vscode-plugin-demo”已被释放!')
};
复制代码
该入口文件导出了两个生命周期方法activate
与deactivate
。
咱们回忆一下以前的activationEvents
属性,当里面相应的事件触发了插件时,activate
方法会被唤起,当插件被销毁时,deactivate
会被调用。
而后,咱们必须在activate
注册一个命令 extension.colaMovie
:
context.subscriptions.push(vscode.commands.registerCommand('extension.colaMovie', async () => {
vscode.window.showInformationMessage('Hello World!');
}));
复制代码
注意,全部注册的对象(不管是命令仍是语言
vscode.languages.registerDefinitionProvider
或是其它)都必需要将结果放入context.subscriptions
中去,这是为了方便deactivate
时VSCode
帮你自动注销它们。
此时,咱们按F5调试以后,已经能够看到右上角出现Cola Movie
的小图标了,当咱们点击它的时候会在右下角弹出Hello World!
的提示信息。
让咱们来完善一下点击事件,试着建立一个webview
看看:
panel = vscode.window.createWebviewPanel(
"movie",
"Cola Movie",
vscode.ViewColumn.One,
{
enableScripts: true,
retainContextWhenHidden: true,
}
);
复制代码
enableScripts
表明容许js脚本执行retainContextWhenHidden
表明当页签切换离开时保持插件上下文不销毁
VSCode
为了性能考虑,非当前页签都会销毁上下文,直到切换回来再重建上下文。因此提供了setState
与getState
两个方法供webview
使用以即时保存与恢复上下文。
此时,webview
已经建立并打开,可是却一片空白。
这时咱们须要给panel.webview.html
设置html
内容,可是:
出于安全考虑,
Webview
默认没法直接访问本地资源,它在一个孤立的上下文中运行,想要加载本地图片、js
、css
等必须经过特殊的vscode-resource:
协议,网页里面全部的静态资源都要转换成这种格式,不然没法被正常加载。
vscode-resource:
协议相似于file:
协议,但它只容许访问特定的本地文件。和file:
同样,vscode-resource:
从磁盘加载绝对路径的资源。
找了一段替换html
引用资源协议的函数,以下所示:
function getWebViewContent(context: vscode.ExtensionContext, templatePath: string) {
const resourcePath = path.join(context.extensionPath, templatePath);
const dirPath = path.dirname(resourcePath);
let html = fs.readFileSync(resourcePath, 'utf-8');
// vscode不支持直接加载本地资源,须要替换成其专有路径格式,这里只是简单的将样式和JS的路径替换
html = html.replace(/(<link.+?href="|<script.+?src="|<img.+?src=")(.+?)"/g, (m, $1, $2) => {
return $1 + vscode.Uri.file(path.resolve(dirPath, $2)).with({ scheme: 'vscode-resource' }).toString() + '"';
});
return html;
}
复制代码
我以前写过一个electron
版本的Cola Movie
,此时,我想将它移植进来试下水,看下webview
插件能作到什么程度。
我先把那边的dist
目录拷贝过来加载index.html
:
const html = getWebViewContent(context, 'dist/index.html');
panel.webview.html = html;
复制代码
一经调试就发现,这里面有一个巨大的坑:
webview
内部不容许发送ajax请求,全部ajax
请求都是跨域的,由于webview
自己是没有host
的我以前那边作electron
开发时碰到过跨域问题,经过简单的electron
配置webSecurity: false
就能够开放跨域权限:
let winProps = {
title: '******',
width: 1200,
height: 800,
backgroundColor: '#0D4966',
autoHideMenuBar: true,
webPreferences: {
webSecurity: false,
nodeIntegration: true
}
};
复制代码
但是VSCode
并不会让咱们接触electron
配置,因此我想这条路是堵死了。
那怎么发送ajax
请求把数据取到手呢?
我在extension.ts
里试了下axios
是能够发送请求并取到数据的,这里就引出咱们接下来要讲的一个重头戏了:
webview
和普通网页同样,并不能直接调用任何VSCode API
。可是,它惟一特别之处就在于多了一个名叫acquireVsCodeApi
的方法,执行这个方法会返回一个简易版的vscode
对象,具备以下三个方法:
这样的话,咱们能够发消息让extension
去帮咱们发送http
请求!
消息通讯方式以下:
// 插件发送消息给webview
panel.webview.postMessage(message);
// webview接收消息
window.addEventListener('message', event => {
const message = event.data;
console.log('Webview接收到的消息:', message);
};
// webview发送消息给插件
const vscode = acquireVsCodeApi();
vscode.postMessage(message);
// 插件端接收消息
panel.webview.onDidReceiveMessage(message => {
console.log('插件收到的消息:', message);
}, undefined, context.subscriptions);
复制代码
写过electron
程序的同窗确定知道,这同electron
的ipcMain/ipcRenderer
还有websocket
的send/onmessage
同样,两端互调接口是独立的,写出来略有些不是很好看……
因而我又封装了一个跨端通讯库cs-channel,并开源出去了,你们能够看一下使用方式。
extension
端代码
const channel = new Channel({
receiver: callback => {
panel.webview.onDidReceiveMessage((message: IMessage) => {
message.api && callback(message);
}, undefined, context.subscriptions);
},
sender: message => void panel.webview.postMessage(message)
});
channel.on('http-get', async param => {
return await Q(http.get(param.url, { params: param.params }));
});
复制代码
上面,插件端就完成了一个http-get
的接口定义
webview
端代码
const vscode = acquireVsCodeApi();
const channel = new Channel({
sender: message => void vscode.postMessage(message),
receiver: callback => {
window.addEventListener('message', (event: { data: any }) => {
event && event.data && callback(event.data);
});
}
});
const result = await channel.call('http-get', { url, ...data });
复制代码
上面,webview
端就完成了一次http-get
接口的调用,并直接拿到了插件端的http
调用结果!
Channel
对象,一个项目实例化两个(webview
+extension
)就足够了,不用常常实例化。
如果一个项目有多个通讯方式,好比websocket
+web worker
+iframe父子通讯
,就实例化各自的Channel
对象便可。
DLNA
投屏功能迁移以前electron
版本的Cola Movie
具有DLNA
投屏功能,我觉着在VSCode
的插件里既然能全量使用nodejs api
,应该也能投屏才对?
我写了段测试代码
import * as Browser from 'nodecast-js';
// 是的,你没看错,借助于nodecast-js库nodejs使用dlna就是这么简单
const browser = new Browser();
browser.onDevice(function () {
console.log(browser.getList());
});
browser.start();
复制代码
确实打印了局域网内全部的可投屏设备~
那事情就简单多了,利用刚刚和ajax
一样的原理让extension
帮忙拿设备列表,并帮忙推送投屏请求便可。
直接贴代码:
extension
端代码
const DLNA = {
browser: null,
start: (): Promise<any[]> => {
if (DLNA.browser !== null) {
DLNA.stop();
}
return new Promise(resolve => {
DLNA.browser = new Browser();
DLNA.browser.onDevice(function () {
resolve(DLNA.browser.getList());
});
setTimeout(() => {
resolve([]);
}, 8000);
DLNA.browser.start();
});
},
stop: () => {
DLNA.browser && DLNA.browser.destroy();
DLNA.browser = null;
}
};
channel.on('dlna-request', async param => {
const devices = await DLNA.start();
localDevices = devices;
return devices;
});
channel.on('dlna-destroy', async param => {
DLNA.stop();
});
channel.on('dlna-play', async param => {
localDevices.find(device => device.host === param.host).play(param.url, 60);
});
复制代码
定义三个接口:
dlna-request
获取设备列表dlna-play
投屏视频播放地址url
到某设备dlna-destroy
销毁browser
对象webview
端代码
const DLNA = {
start: async () => await chanel.call<IDevice[]>('dlna-request'),
play: (device: IDevice, url: string) => channel.call('dlna-play', { host: device.host, url }),
stop: () => channel.call('dlna-destroy');
}
复制代码
F5,调试,投屏成功!
PS:其实还有一个遗憾,就是
VSCode
自己在打包electron
的时候移除了ffmpeg
,致使webview
里根本没法使用audio
与video
标签,因此播放功能是作不了了。并且cookie
、localStorage
等接口一概没法访问。因此播放功能我就直接作成打开浏览器播放了。只不过chrome
要实现m3u8
源的播放须要安装一个插件:Play HLS M3u8;