1. 背景
写这篇文章的初衷是看到vscode 市场上的中英翻译插件都是将翻译结果以弹窗的形式做的,体验感非常不好。如果有像有道字典那种打开一个弹窗或者新tab的翻译面板来进行使用就好了。但是找了很久都没有找到,所以就自己写了一个翻译面板插件 。成品如下:
仓库地址:https://github.com/atdow/translation-panel
如果想体验该翻译面板插件,请在vscode插件市场中搜索translation panel 并安装:
右键打开菜单栏,并选择translation panel ,即可打开该翻译插件:
2. 技术栈
开发所需的技术是vscode 、vue 、webpack 。选择vue的原因是本人开发多以vue为主,所以就首选了vue。单从开发web来说,使用jq是比较合适的,毕竟页面的交互是比较简单的,难点是对vscode内部插件机制和api的熟练程度。
3. 新建模版工程
3.1 安装yo并创建工程
yo 是开发vscode插件的一个官方脚手架。
安装yo:
npm install -g yo generator-code
创建模板工程:
yo code
运行yo code 后出现以下信息:
我们选择New Extension(JavaScript) 即可,也可以选择typescripe版本,本文章选择的是javascript版本。
选择完后,将下面的信息一一补充完整即可:
等待创建完毕安装并所需依赖,模板工程如下:
vscode的插件资源都是在package.json 文件中指定开启的,extension.js 是整个插件的入口文件。
3.2 运行和调试
在打开extension.js 文件的面板上,我们按下f5 ,这个时候就会进入调试状态,vscode会打开一个新的运行窗口:
在新的窗口中,我们同时按下Ctrl+Shift+P 后输入helloWorld ,这个时候激发插件,然后会在控制台打印“Congratulations, your extension “translation-panel2” is now active!”,同时弹窗显示“Hello World from translation panel2!”:
4. 开发核心功能
4.1 注册资源
vscode插件的资源开启都是在package.json 中指定的,所以我们需要在package.json 在指定打开插件命令和激活插件时机:
{
"activationEvents": [
"*"
],
"main": "./extension.js",
"contributes": {
"commands": [
{
"command": "translation-panel2.helloWorld",
"title": "Hello World"
},
{
"command": "translation-panel2.open",
"title": "translation panel"
}
],
"menus": {
"editor/context": [
{
"when": "editorFocus",
"command": "translation-panel2.open",
"group": "navigation@1"
}
],
"explorer/context": [
{
"command": "translation-panel2.open",
"group": "navigation"
}
]
}
}
}
通过上面的配置,我们在vscode打开的时候就激活插件,同时注册了一个translation-panel2.open 指令用于打开翻译插件;在右键菜单栏上(menus)配置了translation-panel2.open 指令,那么在右键打开菜单的时候就可以看到translation panel 的条目,这个条目指定的就是translation-panel2.open 命令。
4.2 extension.js核心功能开发
除了需要在package.json中指定资源外,我们还要在入口文件(extension.js )中注册我们的指令:
function activate(context) {
console.log("translation panel activated")
context.subscriptions.push(vscode.commands.registerCommand('translation-panel2.open', (uri) => {
vscode.commands.executeCommand("extension.demo.showPanel")
}));
context.subscriptions.push(vscode.commands.registerCommand("translation-panel2.showPanel", function (uri) {
}))
}
上面我们注册了两个指令:translation-panel2.open 和translation-panel2.showPanel 指令,translation-panel2.open用于右键菜单用,translation-panel2.open调用了translation-panel2.showPanel,translation-panel2.showPanel用于真正打开一个窗口来放我们的翻译面板代码。
我们在translation-panel2.showPanel在加入以下代码:
function activate(context) {
context.subscriptions.push(vscode.commands.registerCommand("translation-panel2.showPanel", function (uri) {
const panel = vscode.window.createWebviewPanel(
"translationPanel",
"translation panel",
vscode.ViewColumn.One,
{
enableScripts: true,
retainContextWhenHidden: true
}
)
let global = { panel }
panel.webview.html = getWebViewContent(context, 'src/view/panel.html')
}))
}
vscode.window.createWebviewPanel 用于创建一个新窗口,getWebViewContent 加载我们页面需要的html文件。 getWebViewContent代码如下:
const fs = require('fs');
const path = require('path');
function getWebViewContent(context, templatePath) {
const resourcePath = path.join(context.extensionPath, templatePath);
const dirPath = path.dirname(resourcePath);
let html = fs.readFileSync(resourcePath, 'utf-8');
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;
}
4.3 panel.html翻译面板页面开发
我们在src/view中新建三个文件panel.html、panel.css和panel.js,由于都是静态页面,我们需要新建lib文件来放我们的静态资源(vue.js).
panel.html代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="./base.css">
<link rel="stylesheet" href="./panel.css">
</head>
<body>
<div id="app" class="container-fluid">
<div class="container">
<div class="header">
<div class="header-left">
<div class="dropdown" style="width:50%">
<div class="dropdown-btn" @click="changeShowSourceMenu">
<span class="not-select">{{isChina?source.labelChinese:source.label}}</span>
<div :class="['triangle', {'triangle__rotate': showSourceMenu}]"></div>
</div>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1" v-if="showSourceMenu">
<li class="dropdown-menu-item not-select" v-for="(item, index) in option" :key="index"
@click="changeSource(item)">{{isChina?item.labelChinese:item.label}}</li>
</ul>
</div>
<?xml version="1.0" encoding="UTF-8"?><svg width="16" height="16" viewBox="0 0 48 48" fill="none"
@click="switchSourceTarget" class="switch" xmlns="http://www.w3.org/2000/svg">
<rect width="48" height="48" fill="white" fill-opacity="0.01" />
<path d="M42 19H5.99998" stroke="white" stroke-width="4" stroke-linecap="round"
stroke-linejoin="round" />
<path d="M30 7L42 19" stroke="white" stroke-width="4" stroke-linecap="round"
stroke-linejoin="round" />
<path d="M6.79897 29H42.799" stroke="white" stroke-width="4" stroke-linecap="round"
stroke-linejoin="round" />
<path d="M6.79895 29L18.799 41" stroke="white" stroke-width="4" stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</div>
<div class="header-right" style="margin-left:20px;width:50%">
<div class="dropdown">
<div class="dropdown-btn" @click="changeShowTargetMenu">
<span class="not-select">{{isChina?target.labelChinese:target.label}}</span>
<div :class="['triangle', {'triangle__rotate': showTargetMenu}]"></div>
</div>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1" v-if="showTargetMenu">
<li class="dropdown-menu-item not-select" v-for="(item, index) in option" :key="index"
@click="changeTarget(item)">{{isChina?item.labelChinese:item.label}}</li>
</ul>
</div>
<div class="translation-btn not-select" @click="translation">
<?xml version="1.0" encoding="UTF-8"?><svg width="16" height="16" viewBox="0 0 48 48"
fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="48" height="48" fill="white" fill-opacity="0.01" />
<path d="M17 32L19.1875 27M31 32L28.8125 27M19.1875 27L24 16L28.8125 27M19.1875 27H28.8125"
stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
<path d="M43.1999 20C41.3468 10.871 33.2758 4 23.5999 4C13.9241 4 5.85308 10.871 4 20L10 18"
stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
<path
d="M4 28C5.85308 37.129 13.9241 44 23.5999 44C33.2758 44 41.3468 37.129 43.1999 28L38 30"
stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<span
class="translation-btn-text">{{isChina?translateBtnText.chinese:translateBtnText.english}}</span>
</div>
</div>
</div>
<div class="content">
<div class="inputContainer">
<textarea rows="10" class="input" v-model="inputText" @keydown="inputKeyDown"
:placeholder="isChina?placeholder.chinese:placeholder.english"></textarea>
</div>
<div class="result">
<p v-if="loading===false">{{result}}</p>
<div v-else class="sk-circle">
<div class="sk-circle1 sk-child"></div>
<div class="sk-circle2 sk-child"></div>
<div class="sk-circle3 sk-child"></div>
<div class="sk-circle4 sk-child"></div>
<div class="sk-circle5 sk-child"></div>
<div class="sk-circle6 sk-child"></div>
<div class="sk-circle7 sk-child"></div>
<div class="sk-circle8 sk-child"></div>
<div class="sk-circle9 sk-child"></div>
<div class="sk-circle10 sk-child"></div>
<div class="sk-circle11 sk-child"></div>
<div class="sk-circle12 sk-child"></div>
</div>
</div>
</div>
</div>
</div>
<script src="../../lib/vue-2.5.17/vue.js"></script>
<script src="../../src/view/panel.js"></script>
</body>
</html>
panel.html主要放了我们的页面接口,同时引入了css资源和vue.js,以及我们panel.js
4.4 panel.js开发
panel.js中我们主要解决点击翻译的时候调用vscode的内部通信,然后在extension.js中调用翻译api的过程。为什么不在panel.js 直接调用翻译api进行请求呢?由于我们开发的是静态页面,直接调用翻译api会跨域 ,这个问题是不可以避免的。但是在extension.js中进行请求时不会跨域的,因为vscode在extension.js没有做跨域限制。
const vue = new Vue({
el: '#app',
methods: {
translation() {
callVscode({
cmd: 'translation',
queryParams: {
inputText: that.inputText,
from: that.source.value,
to: that.target.value,
}
}, (data) => {
const result = data.trans_result || []
if (result && result.length > 0) {
that.result = result[0].dst
}
});
},
}
});
在翻译方法中,我们调用callVscode 方法,这个主要是调用vscode.postMessage() 方法进行全局通信:
const testMode = false;
const vscode = testMode ? {} : acquireVsCodeApi();
const callbacks = {};
function callVscode(data, cb) {
if (typeof data === 'string') {
data = { cmd: data };
}
if (cb) {
const cbid = Date.now() + '' + Math.round(Math.random() * 100000);
callbacks[cbid] = cb;
data.cbid = cbid;
}
vscode.postMessage(data);
}
我们调用panel.js的翻译方法的时候,向全局发送了一个{cmd: 'translation'} 的信息,我们只要在extension.js接收,同时调用翻译api,最后再向panel.js返回翻译结果即可:
function activate(context) { context.subscriptions.push(vscode.commands.registerCommand("translation-panel2.showPanel", function (uri) {
let global = { panel }
panel.webview.onDidReceiveMessage(message => {
if (messageHandler[message.cmd]) {
messageHandler[message.cmd](global, message)
} else {
util.showError(`未找到名为${message.cmd}回调方法!`)
}
}, undefined, context.subscriptions)
}))
}
我们在上面的代码中加入了注册监听全局事件,同时响应特殊的全局信息({cmd: 'translation'} )。 messageHandler主要是存放特殊信息的响应:
const messageHandler = {
translation(global, message) {
const {
inputText = "",
from = "",
to = "",
} = message.queryParams
let appId = '20220505001204018'
let appKey = 'iTQzl__y2EL_sq3iaDmy'
const baseUrl = 'https://fanyi-api.baidu.com/api/trans/vip/translate'
const salt = uuidv4.v4();
const sign = Md5(appId + inputText + salt + appKey).toString()
const queryParams = {
q: inputText,
from: from,
to: to,
appid: appId,
salt: salt,
sign: sign
}
axios({
method: 'post',
url: baseUrl,
params: queryParams
}).then(res => {
invokeCallback(global.panel, message, res.data);
}).catch(err => {
}).finally(() => {
global.panel.webview.postMessage({ cmd: 'loading' });
})
}
};
messageHandler中主要响应translation ,也就是翻译指令。上面的翻译api是有道翻译,可以换成自己想用的翻译api,同时替换自己的key。翻译结果回来后,我们调用了invokeCallback(global.panel, message, res.data) ,用于发送信息到panel.js。
function invokeCallback(panel, message, resp) {
panel.webview.postMessage({ cmd: 'vscodeCallback', cbid: message.cbid, data: resp });
}
这个时候我们需要在panel.js中监听extension发送的翻译结果信息 :
window.addEventListener('message', event => {
const message = event.data;
switch (message.cmd) {
case 'vscodeCallback':
(callbacks[message.cbid] || function () { })(message.data);
delete callbacks[message.cbid];
break;
}
});
panel.js的监听信息主要是响应{ cmd: 'vscodeCallback'} 的指令,上面我们缓存的callbacks是加了唯一标识来缓存的,直接调用即可,也就是callbacks[message.cbid] 。
const vue = new Vue({
el: '#app',
methods: {
translation() {
callVscode({
cmd: 'translation',
queryParams: {
inputText: that.inputText,
from: that.source.value,
to: that.target.value,
}
}, (data) => {
const result = data.trans_result || []
if (result && result.length > 0) {
that.result = result[0].dst
}
});
},
}
});
至此,一个翻译流程已经闭环了。从panel.js 中发送翻译指令到extension.js 中,extension.js 调用翻译api,然后将翻译结果发送回panel.js 。
5. 打包
5.1 打包extension.js
打包的时候我们需要借助webpack,由于我们在extension调用api的时候引入了axios、md5等资源,需要打包压缩。
新建build文件夹,同时新建node-extension.webpack.config.js:
'use strict';
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const config = {
target: 'node',
mode: 'none',
entry: './extension.js',
output: {
path: path.resolve(__dirname, '..', 'dist'),
filename: 'extension.min.js',
libraryTarget: 'commonjs2',
devtoolModuleFilenameTemplate: '../[resource-path]'
},
devtool: 'source-map',
externals: {
vscode: 'commonjs vscode'
},
resolve: {
},
plugins: [
new CleanWebpackPlugin()
],
module: {
rules: []
}
};
module.exports = config;
然后我们在package.js中加入打包命令:
{
"scripts": {
"build": "webpack --mode production --config ./build/node-extension.webpack.config.js",
}
}
5.2 打包插件
先全局安装vsce
npm i vsce -g
然后执行打包命令
vsce package ## yarn方式
vsce package --no-yarn ## npm 方式
6. 本地测试
经过上线的打包插后生成了一个以.vsix 后缀的插件包,我们在插件包的上右键,选择安装。
安装完毕后,我们在空白处右键菜单,就可以看到我们的插件名称了,同时选择translation panel ,就可以打开我们的插件:
最后的成品如下:
7. 发布
7.1 申请Microsoft账号
访问 Sign in to your Microsoft account 登录你的Microsoft 账号,没有的先注册一个
7.2 创建Azure DevOps 组织
访问: https://aka.ms/SignupAzureDevOps
点击继续,默认会创建一个以邮箱前缀为名的组织。
7.3 创建令牌
进入组织的主页后,点击右上角的Security, 点击创建新的个人访问令牌,这里特别要注意Organization 要选择all accessible organizations ,Scopes 要选择Full access ,否则后面发布会失败。
创建令牌成功后你需要本地记下来 ,因为网站是不会帮你保存的,后面登录需要用到,如果没保存,后面需要用到的话,就只能重新创建令牌了
7.4 创建发布账号
访问:https://aka.ms/vscode-create-publisher
7.5 发布
登录账号:
vsce login atdow
发布:
vsce publish
发布成功后我们可以在https://marketplace.visualstudio.com/manage 看到我们插件的状态:
7.6 发布后安装
当插件成功发布后,我们可以在插件插件市场中找到我们插件,并安装使用:
8. 总结
我们从零开始,从搭建项目、开发插件到最后的插件发布,基本涵盖了制作一个vscode翻译插件流程,虽然拓展的vscode api细节没有在本文中提到。如果有需要学习更多的vscode插件的知识,可以到官方网站中查阅相关文章。
|